共计 4292 个字符,预计需要花费 11 分钟才能阅读完成。
最近团队内部需要落地一套面向智能客服与内部 Code Agent 的底层推理网关。最初很多同事直接用 Ollama 或者纯 HuggingFace 的 pipeline 顶上去,并发请求刚过 10,首字延迟(TTFT,Time to First Token)就飙到 3 秒以上,显存更是因为 Context 碎片化动不动就 OOM。如果只是本地单人玩,Ollama 确实开箱即用,但在多线程并发、长上下文混合 prompt 的生产场景下,基于连续内存分配的方案几乎等于灾难。
为了榨干手头那几张 RTX 4090 和单卡 A100 (80G) 的算力,我花了几天时间重新基于 vLLM (v0.6+) 搭建了一套高并发流式反向代理服务,将 Qwen2.5-14B 的高并发吞吐提升了将近 4 倍。今天把整套落地踩坑经验、核心参数配置以及前置 Nginx 流式反代的调优细节完整复盘出来。
一、核心痛点拆解:高并发推理究竟卡在哪?
大语言模型并发推理的核心瓶颈从来不是纯算力,而是 显存带宽(Memory Bandwidth)与 显存碎片化。当多个长提示词(比如包含 4k tokens 上下文的 RAG 召回文本)同时涌入时,传统推理引擎面临两大死穴:
- KV Cache 显存暴涨与碎片: 传统的动态张量分配需要预留连续物理显存,并发一高,即便显存总容量还有 30%,也会因为碎片化导致分配失败崩溃。
- Prefill(预填充)阻碍 Decode(生成): 一个超长的新请求进入队列做 Prefill 阶段运算时,会抢占计算单元,导致正在输出流式 Token 的短请求出现明显的“卡顿”断流。
针对这两个问题,vLLM 靠 PagedAttention(把 KV 缓存拆成虚拟分页,彻底根除碎片)和 Chunked Prefill(分块预填充)给出了目前最优解。
二、vLLM 生产环境启动调优全参数解析
我们采用 Docker 方式部署 vLLM。模型选择目前综合能力极强的开源顶流 Qwen/Qwen2.5-14B-Instruct-AWQ(针对 24G 显存 4090 单卡)或者 Qwen/Qwen2.5-32B-Instruct-GPTQ-Int4。如果是 A100/H100 用户可以直接上 BF16 原版。
以下是我在线上跑得最稳的一组启动参数与 docker-compose.yml 范式:
version: '3.8'
services:
vllm-service:
image: vllm/vllm-openai:v0.6.3.post1
container_name: vllm-qwen25
runtime: nvidia
environment:
- CUDA_VISIBLE_DEVICES=0
- NCCL_IGNORE_DISABLED_P2P=1
volumes:
- /data/huggingface/hub:/root/.cache/huggingface/hub
ports:
- "8000:8000"
ipc: host
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
command: >
--model Qwen/Qwen2.5-14B-Instruct-AWQ
--quantization awq
--served-model-name qwen2.5-14b
--host 0.0.0.0
--port 8000
--max-model-len 8192
--gpu-memory-utilization 0.92
--max-num-seqs 64
--enable-chunked-prefill
--max-num-batched-tokens 2048
--enable-prefix-caching
--trust-remote-code
踩坑核心参数调优笔记:
--gpu-memory-utilization 0.92:默认 0.9。对于 24G 显存,如果卡得太死(比如 0.95),CUDAGraph 初始化时极容易 OOM;留出约 8% 的缓冲显存,刚好能在保证 KV 块最大化的同时不炸显存。--enable-prefix-caching:强烈推荐开启! 特别是跑 Agent 或密集型 RAG 系统,大量请求的前缀 Prompt(如 System Prompt 或基础工具定义)是完全重合的。开启后,相同前缀的 KV Cache 直接重用,TTFT 直接从几百毫秒打到 20 毫秒以内。--enable-chunked-prefill 与 --max-num-batched-tokens 2048:这是解决“流式输出忽快忽慢甚至假死”的神级参数。它把长的 Prefill 任务切碎成多个 2048 大小的 token 块,与 Decode 阶段交替执行,避免长请求把整张卡算力独占。--max-num-seqs 64:限制最大并发批次。对于 14B AWQ 模型,单卡 24G 跑 64 个并发流已经是兼顾延迟与吞吐的甜点位,再高会导致 Decode 延迟显著增加。
三、前置 Nginx 高并发流式代理配置
许多团队把 vLLM 裸暴露在内网,或者前置配了个 Nginx 却用了默认配置,结果客户端调用 SSE(Server-Sent Events)流式返回时,发现文字不是一个词一个词蹦出来的,而是一大坨一大坨地吐出来。这是因为 Nginx 默认开启了 HTTP 缓冲。
针对大模型流式代理,Nginx 必须进行以下精细化反代改造:
upstream vllm_backend {
server 127.0.0.1:8000 max_fails=3 fail_timeout=10s;
keepalive 64; # 关键:保持与后端长连接,减少 TCP 握手损耗
}
server {
listen 80;
server_name api.llm.internal;
client_max_body_size 64M;
location /v1/ {
proxy_pass http://vllm_backend;
proxy_http_version 1.1;
# 解决流式 SSE 输出缓冲的核心三行配置
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
# 设置更长的读超时,防止长文本推理导致 504 Gateway Timeout
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 60s;
# 透传客户端真实 IP 与 Host
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 开启底层 TCP 无延迟
tcp_nodelay on;
}
}
注意: proxy_buffering off; 是保证流式输出“即出即推”的关键所在。如果关闭失效,你的客户端就会持续等待直到 Nginx 的 Buffer 被填满或者连接关闭,极度影响交互体验。
四、实战测试与压测效果验证
配置完成并启动后,先用简单的 Python 异步脚本进行基础连通性与流式时延测试:
import asyncio
import time
from openai import AsyncOpenAI
client = AsyncOpenAI(
base_url="http://api.llm.internal/v1",
api_key="EMPTY" # vLLM 默认无鉴权或自定义 token
)
async def test_stream():
start_time = time.time()
first_token_time = None
token_count = 0
response = await client.chat.completions.create(
model="qwen2.5-14b",
messages=[{"role": "system", "content": "You are a helpful Linux expert."},
{"role": "user", "content": "Write a python script to monitor memory usage using eBPF."}
],
stream=True,
temperature=0.7,
max_tokens=512
)
async for chunk in response:
content = chunk.choices[0].delta.content
if content:
if first_token_time is None:
first_token_time = time.time()
token_count += 1
print(content, end="", flush=True)
end_time = time.time()
print("\n" + "="*40)
print(f"TTFT (首字延迟): {(first_token_time - start_time):.3f}s")
print(f"总生成耗时: {(end_time - start_time):.3f}s")
print(f"平均吞吐: {token_count / (end_time - first_token_time):.2f} tokens/s")
if __name__ == "__main__":
asyncio.run(test_stream())
实测对比数据(RTX 4090 单卡):
我们采用 benchmark_serving.py(vLLM 自带压测套件)模拟 32 个并发用户同时请求,Prompt 长度为 1024,输出长度 256:
- 未调优(Ollama 默认配置):并发达到 16 后,请求开始排队,TTFT 均值超过 2.8 秒,总吞吐约在 110 tokens/s 上下波动,后续请求频繁报上下文溢出。
- vLLM + Prefix Caching + Chunked Prefill:在 32 并发打满情况下,平均 TTFT 压到 480ms 左右,整体吞吐稳定在 430+ tokens/s,显存占用稳定在 22.1 GB,几乎没有发生过显存碎片导致的 OOM。
总结与避坑心得
在大模型落地的工程栈中,别一上来就想着加卡做张量并行(Tensor Parallelism)。对于多数业务而言,一张消费级旗舰卡配合优秀的推理引擎,榨干潜力后足以应付几十甚至上百的内部日常并发。
部署排错时记住两个核心经验:第一,严查前置网关的代理缓冲 ,只要是 SSE 场景,必须禁用反向代理缓存; 第二,按需开启 Chunked Prefill,如果你的场景以高并发代码生成、文档问答为主,它就是消灭流式输出顿挫感的银弹。