共计 4510 个字符,预计需要花费 12 分钟才能阅读完成。
最近团队内部基于 Qwen-2.5-72B-Instruct 跑企业知识库 Agent 时,业务端反馈一到早晚高峰并发进来,接口就会偶发性卡死,甚至直接在终端抛出 CUDA out of memory。排查后发现,用原生 Transformers 或简单的 HuggingFace TGI 配合单机多卡跑,在高上下文拼接(超过 16k tokens)和几十个并发会话冲进来时,KV Cache 的显存碎片化极其严重,首字延迟(TTFT)直接飙到了 4 秒以上。
为了给内网服务做止血降本,我花了两个通宵把底层推理栈迁移到了 vLLM,并在前面搭了一套轻量级的 FastAPI 生产级流式中转网关。实测在双卡 RTX 4090(24G×2)利用 AWQ 4-bit 量化跑 72B,或直接四卡跑 FP16 模式下,整机吞吐提升了近 4 倍,TTFT 稳定压在 600ms 以内。本文将核心调优参数与工程避坑点直接整理出来,省去大家反复试错的时间。
一、vLLM 多卡生产级部署命令与核心参数剖析
很多人启动 vLLM 时往往直接贴官方最简命令,但在真实生产环境中,默认配置往往会埋下显存分配失控的祸根。特别是在混合部署了 Embedding 模型的服务器上,vLLM 默认吃掉 90% 显存会导致其他模型直接崩溃。
这是我反复压测后定型的生产环境启动脚本:
#!/usr/bin/env bash
export CUDA_VISIBLE_DEVICES=0,1
export NCCL_DEBUG=WARN
export VLLM_WORKER_MULTIPROC_METHOD=spawn
python3 -m vllm.entrypoints.openai.api_server \
--model /data/models/Qwen2.5-72B-Instruct-AWQ \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.92 \
--max-model-len 16384 \
--max-num-seqs 64 \
--max-num-batched-tokens 8192 \
--kv-cache-dtype auto \
--trust-remote-code \
--port 8000 \
--host 0.0.0.0 \
--disable-log-requests
核心参数压测结论:
--gpu-memory-utilization 0.92:默认的 0.90 偏保守,但在并发打满时,0.95 会有极大概率因为中间激活值(Activation)突增而瞬间触发硬 OOM。留出 8% 的显存缓冲最为稳妥。--max-num-batched-tokens 8192:单次迭代中最多处理的 Token 总量。配得太高(比如 16384)会导致前向计算(Prefill 阶段)耗时过长,阻塞正在生成的请求(Decode 阶段),从而劣化整体流式响应的平滑度。--max-num-seqs 64:严格限制并发请求队列数量。配合 PagedAttention,避免无限排队导致内网网关超时。--disable-log-requests:生产环境务必加上!默认的详细请求日志输出在极高并发下会导致 Python 进程在标准输出上产生大量 I/O 阻塞,显著拉低 QPS。
二、为什么不能直接将 vLLM 暴露给前端?
vLLM 自带的 api_server 确实提供了兼容 OpenAI 格式的接口,但在生产落地时,如果你直接把 8000 端口暴露给业务层,马上会遇到两个致命问题:
- 长连接心跳保活与网络闪断:业务端或移动端经常因为信号问题断开,vLLM 无法敏锐感知客户端断流,导致服务端白白空转几十秒生成 Token,极度浪费算力。
- 熔断降级与请求鉴权缺失:当推理引擎队列满载时,直接返回 500 甚至连接挂起,前端界面直接卡死白屏,需要一套轻量网关在前方提供动态队列拦截和 SSE(Server-Sent Events)规范保活。
三、生产级 FastAPI 异步流式代理网关实现
我们在前端与 vLLM 之间加入了一层基于 FastAPI + HTTPX 异步客户端 的微型反向代理。这套网关的核心在于:支持客户端断开事件实时监听、SSE 流式打碎保活 ping 包、全局并发限流。
# gateway.py
import asyncio
import json
import logging
from typing import AsyncGenerator
import httpx
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import StreamingResponse
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("ProxyGateway")
app = FastAPI(title="LLM Production Gateway")
# 限制网关层到 vLLM 的总连接池
HTTPX_CLIENT = httpx.AsyncClient(
base_url="http://127.0.0.1:8000",
timeout=httpx.Timeout(connect=5.0, read=60.0, write=5.0, pool=10.0),
limits=httpx.Limits(max_connections=128, max_keepalive_connections=64)
)
SEM = asyncio.Semaphore(60) # 对应 vLLM max-num-seqs,做前置拦截保护
async def sse_event_stream(payload: dict, client_request: Request) -> AsyncGenerator[str, None]:
async with SEM:
try:
req = HTTPX_CLIENT.build_request("POST", "/v1/chat/completions", json=payload)
response = await HTTPX_CLIENT.send(req, stream=True)
if response.status_code != 200:
error_body = await response.aread()
yield f"data: {json.dumps({'error':'vLLM upstream error','detail': error_body.decode()})}\n\n"
return
async for line in response.aiter_lines():
# 检查客户端是否已主动断开(如用户关闭标签页)if await client_request.is_disconnected():
logger.warning("Client disconnected prematurely. Aborting upstream stream.")
await response.aclose()
break
if line:
yield f"{line}\n\n"
except httpx.ReadTimeout:
logger.error("Upstream inference timeout.")
yield f"data: {json.dumps({'error':'Inference timeout'})}\n\n"
except Exception as e:
logger.error(f"Stream error: {str(e)}")
yield f"data: {json.dumps({'error':'Gateway proxy exception','msg': str(e)})}\n\n"
@app.post("/v1/chat/completions")
async def chat_proxy(request: Request):
if SEM.locked():
# 网关层直接平滑拦截,避免 vLLM 爆显存,返回标准的 429 提示前端排队
raise HTTPException(status_code=429, detail="Inference engine busy, please retry shortly.")
try:
body = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON payload")
# 强制将请求重定向为流式传输,提升端到端响应体验
body["stream"] = True
return StreamingResponse(sse_event_stream(body, request),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # 解决 Nginx 缓冲导致的流式吐字延迟卡顿
}
)
四、踩坑复盘:那些官方文档没写的坑
在将这套系统压测上线的一周内,我踩了两个非常隐蔽的底层死锁和通信 Bug,写在下面供大家排雷:
1. NCCL 通信引发的多卡启动死锁
在多张消费级显卡(如 4090,走 PCIe 拆分而非 NVLink)运行 --tensor-parallel-size 2 时,偶尔会在模型加载完成后,主进程卡在广播权重的环节不动。查了 issue 才发现是 PyTorch 默认的多进程方式与 CUDA 驱动环境发生了资源竞争冲突。解决方案是必须显式导出:
export VLLM_WORKER_MULTIPROC_METHOD=spawn
严禁使用默认的 fork,这会导致 CUDA Driver 上下文被不安全地继承,进而在初始握手时直接挂起无响应。
2. 前置 Nginx 的反向代理缓存问题
部署好网关后,如果在最外层套了 Nginx 进行 SSL 卸载和域名映射,必须在 Nginx 的 location 块中明确关闭缓冲:
location /v1/ {
proxy_pass http://127.0.0.1:8080; # 指向我们的 FastAPI 网关
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
proxy_buffering off;
proxy_cache off;
}
如果漏了 proxy_buffering off;,哪怕 FastAPI 端推的是流,Nginx 也会默认凑齐 4KB 或 8KB 缓存才往外吐一次,用户端看到的效果就是完全丧失逐字打印感,变成卡顿几秒后一次性崩出一大段。
写在最后
对于个人或中小技术团队,盲目追求多机分布式推理往往会带来不可控的运维开销。采用「vLLM 算力池下沉 + 异步代理网关做防御」的分层架构,仅用 2-4 张主流显卡,就能把 30B 到 72B 级别的开源主力大模型调教得极其平稳。在后续的文章中,我将继续拆解基于这套推理基建接入真实企业级 Agent 工具调用(Tool-Calling)与并行 RAG 检索的具体流水线设计。