共计 3129 个字符,预计需要花费 8 分钟才能阅读完成。
上个月在重构内部 Agent 平台时,线上遇到了一个非常典型的性能拐点:原本用 Ollama 顶着的几台单卡 RTX 4090 24G 节点,当并发请求冲上 20 左右时,推理排队时间暴涨,前端流式打字机直接卡成 PPT,随后甚至触发 CUDA OOM。很多同学在本地搭小模型写 Demo 觉得 Ollama 很方便,但一旦进入多用户、长 Prompt(Agent ReAct 循环或多轮 RAG)的高并发生产环境,Ollama 那套调度机制就完全不够看了。
为了榨干单张 24G 显存的极限性能,我花了整整一周对底层推理引擎与分发网关进行了重构。最终选定的方案是:vLLM(底座推理引擎)+ LiteLLM(统一智能网关)+ Nginx(流式反向代理)。在 Qwen2.5-14B-Instruct-AWQ 模型下,成功在单卡 24G 上稳定扛住 50+ 真实并发,首字响应(TTFT)压到了 120ms 左右。今天把整套落地配置、显存精细化调优参数和踩过的深坑完整复盘出来。
一、架构选型与 24G 显存瓶颈拆解
在单卡 24G 显存(如 RTX 3090 / 4090 / L4)的物理约束下跑高并发,必须先算清两笔账:静态显存 和动态显存。
- 模型权重(静态占用):以 14B 模型为例,FP16 需要 28GB 显存,单卡直接暴毙;量化到 INT4/AWQ 后,静态权重占用压缩至 8.5GB 左右。剩下的显存空间(约 14.5GB)才能全部挪给 KV Cache。
- KV Cache(动态伸缩):高并发的本质就是抢占 KV Cache。传统推理引擎(包括早期的 HuggingFace TGI)由于内存分配不连续,显存碎片率高达 60% 以上。vLLM 靠 PagedAttention 机制像虚拟内存分页一样管理 KV 缓存,几乎把碎片浪费降为 0。
- Continuous Batching(连续批处理):告别传统静态 Batching 中“短文本等长文本跑完”的愚蠢机制,每个 iteration 动态插入新请求或释放已完成请求,吞吐量直接成倍提升。
二、vLLM 底层调优:把显存压到毫厘之间
直接裸跑 python3 -m vllm.entrypoints.openai.api_server 进生产环境基本必崩。以下是我在反复压测后沉淀出的核心启动命令与生产参数组合:
python3 -m vllm.entrypoints.openai.api_server \
--model /data/models/Qwen2.5-14B-Instruct-AWQ \
--served-model-name qwen2.5-14b \
--host 0.0.0.0 \
--port 8000 \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.92 \
--max-model-len 8192 \
--max-num-seqs 64 \
--max-num-batched-tokens 8192 \
--enable-prefix-caching \
--enforce-eager \
--disable-log-requests
核心参数避坑详解
--gpu-memory-utilization 0.92:默认是 0.9,很多新手喜欢改成 0.98 试图挤出更多 KV Cache。实测千万别拉到 0.95 以上!PyTorch 运行时的 CUDA 临时开销、激活值分配以及偶尔突发的长 Context 会瞬间打满最后那 5%,直接抛出CUDA out of memory导致进程当场挂掉。留出 8%(约 1.9G)作为安全缓冲线至关重要。--enable-prefix-caching(神级开关):做 Agent 或复杂 RAG 时必须开启!它能够缓存多轮对话中完全重复的 System Prompt 和历史上下文对应的 KV Cache。在 ReAct 循环或长文档召回场景下,重复计算直接省去,首字延迟(TTFT)断崖式下降 70% 以上。--enforce-eager:vLLM 默认会尝试使用 CUDA Graph 来优化短序列延迟。但在单卡显存吃紧(尤其是跑 14B AWQ 并发)时,CUDA Graph 会预先占用 1~2GB 的显存用来做 Capture。加上--enforce-eager虽然微幅牺牲单请求的极小 token 推理时延,但能为并发 KV Cache 腾出足足近 2GB 的宝贵空间,并发上限立升 20%。--max-num-seqs 64:显式硬限制当前推理引擎的最大并发上下文通道,防止上游瞬时流量脉冲直接打爆显存。
三、网关层落地:LiteLLM 路由与反代排障
在生产架构中,绝对不要让业务端直接连裸跑的 vLLM。我们需要网关实现负载均衡、超时重试、鉴权以及 Fallback 机制(当本地卡跑满时自动切到公有云 API)。LiteLLM 是目前最轻量、与 OpenAI 协议兼容度极高的选择。
准备一份轻量级的 config.yaml:
model_list:
- model_name: prod-chat-agent
litellm_params:
model: openai/qwen2.5-14b
api_base: http://127.0.0.1:8000/v1
api_key: "none"
timeout: 60
- model_name: prod-chat-agent
litellm_params:
model: deepseek/deepseek-chat
api_key: "sk-your-backup-key"
router_settings:
routing_strategy: "least-busy"
num_retries: 2
timeout: 30
general_settings:
master_key: "sk-internal-master-key-xyz"
启动网关服务:
docker run -d \
--name litellm-gateway \
-p 4000:4000 \
-v $(pwd)/config.yaml:/app/config.yaml \
ghcr.io/berriai/litellm:main-latest \
--config /app/config.yaml --port 4000 --num_workers 4
排障深坑:Nginx 导致流式输出(SSE)“便秘”
很多老哥部署完上线后反馈:“为什么流式接口响应特别慢,卡了 5 秒钟后突然哗啦啦全吐出来?”
这是被 Nginx 的反向代理缓冲(Proxy Buffering)给坑了。Nginx 默认会把后端的 HTTP Response 攒满一个 Buffer 块再往下游推,Server-Sent Events (SSE) 的单 chunk 特征直接被它按死。Nginx 代理层必须加入以下配置:
location /v1/chat/completions {
proxy_pass http://127.0.0.1:4000/v1/chat/completions;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 核心!彻底关掉流式代理缓冲
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 300s;
proxy_set_header X-Accel-Buffering no;
}
四、实测压测数据对比:优化前后差距
我使用定制的压测脚本对单张 RTX 4090 运行 Qwen2.5-14B-Instruct-AWQ 进行梯度压测(Prompt 长度统一设为 1200 tokens,生成 300 tokens,模拟典型 Agent 调用),对比 Ollama 默认配置与本次优化的 vLLM+LiteLLM 方案:
| 并发量 (Clients) | 方案 | 平均首字延迟 (TTFT) | 推理吞吐 (Tokens/s) | 请求成功率 |
|---|---|---|---|---|
| 10 并发 | Ollama (默认) | 840 ms |