共计 3822 个字符,预计需要花费 10 分钟才能阅读完成。
上个月把家里那台闲置的 4090 机器改成了内网小助手,一开始图省事直接上了 Ollama,单个人用着挺爽。结果团队里五个人同时连上来问需求文档,响应时间直接从 3 秒飙到 40 秒,日志里一排 request queued。折腾了两周换到 vLLM,同样的卡、同样的模型,8 并发总吞吐从 42 tok/s 拉到 320 tok/s。这篇把配置、参数、踩的坑和压测数据全摊开写。
一、核心背景与痛点拆解
先明确场景,不然选型全是空谈。我的需求是:内网 5~10 人团队共用,主要是长 system prompt 的问答和代码补全,平均输入 800~2000 token,输出 200~500 token,要求首 token 延迟别超过 2 秒。硬件是单卡 RTX 4090 24G,配 i9-13900K 和 64G 内存。
三套方案我都跑了一遍,问题各不相同:
Ollama 默认 OLLAMA_NUM_PARALLEL=1,请求是严格串行的,多人用就是排队。调到 4 之后显存直接吃满,因为它按上下文长度预分配 KV Cache,一个 8K 上下文的槽位就要吃掉几 G。
llama.cpp server 的 -np 连续批处理做得不错,但每轮 batch 只做一次 forward,长 prompt 的 prefill 阶段没法和其他请求的 decode 阶段重叠,并发一高 TTFT 就崩。
vLLM 的 PagedAttention + Continuous Batching 正好治这个,KV Cache 按 block 分配不浪费,新请求随时能插进正在跑的 batch 里,这是吞吐差距的根源。
二、vLLM 环境搭建与关键参数
我直接用官方镜像,省得跟 CUDA、torch、flash-attn 的版本地狱纠缠。docker 一定要加 --shm-size,默认 64MB,多 worker 加载模型时百分百报 Bus error,这个坑我第一次部署时就撞上了。
docker run -d --name vllm --gpus all --shm-size 16g \
-p 8000:8000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--restart unless-stopped \
vllm/vllm-openai:v0.6.6 \
--model Qwen/Qwen2.5-14B-Instruct-AWQ \
--quantization awq_marlin \
--served-model-name qwen2.5-14b \
--max-model-len 16384 \
--gpu-memory-utilization 0.92 \
--max-num-seqs 32 \
--enable-prefix-caching \
--disable-log-requests
几个参数值得掰开说:
--max-model-len 16384:这玩意儿不是越大越好。14B 模型在 AWQ 量化后权重约 9.5G,24G 卡按 0.92 预留约 22G,剩下 12G 左右给 KV Cache。Qwen2.5-14B 是 48 层、4 个 KV head、head_dim 128,单个 token 的 KV 开销约 96KB,12G 能存大约 12 万 token。设成 32K 上下文,最多只能同时跑 3 个请求,并发直接废掉。
--gpu-memory-utilization 0.92:别贪到 0.98,显卡还跑着桌面环境的话,模型加载完那一刻容易 OOM。0.90~0.92 是比较稳的区间。
--enable-prefix-caching:这是 vLLM 的隐藏王牌。我们所有请求共享一段 1500 token 的系统提示词,开了前缀缓存后这段 prefill 只算一次。实测 TTFT 从 900ms 掉到 180ms 左右。
--quantization awq_marlin:v0.6.x 对 AWQ 的 marlin kernel 支持最好,比默认路径快 15% 上下。如果你的版本能自动识别,这句可以省。
验证流式输出是否真的在流
服务起来后别急着接前端,先用 curl 确认 token 是逐个吐的,而不是攒完一次性返回:
curl -N http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"qwen2.5-14b","stream": true,"messages": [{"role":"user","content":" 用三句话解释 PagedAttention"}]
}'
如果输出是一卡一卡成块出现的,说明中间有缓冲层在捣鬼,继续往下看。
三、流式代理踩坑:Nginx 把 SSE 全吞了
最折磨人的坑在这儿。vLLM 本地 curl 一切正常,挂上 Nginx 反代给内网用,前端就变成 ” 转圈 15 秒,然后整段文字一次性蹦出来 ”。
原因很简单:Nginx 默认 proxy_buffering on,它会把上游响应先攒进 4KB/8KB 的 buffer 再转发。SSE 的数据帧又小又碎,被攒满才发出去,流式语义直接被毁。改法就是关掉缓冲,同时顺手把超时调长——大模型一个请求跑两分钟太正常了。
location /v1/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Connection "";
# 关键三行,缺一不可
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
add_header X-Accel-Buffering no;
}
add_header X-Accel-Buffering no 这行是给前面还套了一层 CDN 或 Cloudflare 的场景用的,告诉上游别再缓冲。我之前在网关后面套了个 Traefik,忘了这茬,又白排查了半天。
还有一个前端侧的坑:如果用 fetch 而不是 EventSource,记得手动读 response.body.getReader(),别直接 await res.text(),那一样会把流吃干榨净。
import json, requests, time
def stream_chat(prompt: str):
t0 = time.time()
first = None
with requests.post(
"http://127.0.0.1:8000/v1/chat/completions",
json={"model": "qwen2.5-14b", "stream": True,
"messages": [{"role": "user", "content": prompt}]},
stream=True, timeout=300,
) as r:
for line in r.iter_lines():
if not line or not line.startswith(b"data:"):
continue
payload = line[6:]
if payload == b"[DONE]":
break
if first is None:
first = time.time() - t0
delta = json.loads(payload)["choices"][0]["delta"]
print(delta.get("content", ""), end="", flush=True)
print(f"\n[TTFT] {first:.2f}s")
四、压测实测数据与选型建议
压测用 vLLM 自带的 bench 工具,模拟真实分布:输入 1024 token,输出 256 token,200 条请求,固定 8 并发发压。
vllm bench serve \
--backend openai-chat \
--model qwen2.5-14b \
--host 127.0.0.1 --port 8000 \
--endpoint /v1/chat/completions \
--dataset-name random \
--random-input-len 1024 --random-output-len 256 \
--num-prompts 200 --request-rate 8
三套方案在同一张卡、同一个 AWQ 权重下的结果:
方案 单请求 tok/s 8 并发总吞吐 平均 TTFT P99 延迟
Ollama (parallel=1) 38.2 41.5 6.8s 52.3s
Ollama (parallel=4) 31.7 98.4 4.1s 28.7s
llama.cpp server (-np 4) 36.4 112.6 2.9s 21.4s
vLLM (max-num-seqs=32) 44.8 321.7 0.9s 6.2s
结论很直白:单请求场景 vLLM 只比 Ollama 快 17%,但一上并发就是 6~7 倍差距,P99 延迟更是从半分钟压到 6 秒。这个提升不是参数调出来的,是 PagedAttention 的显存管理和连续批处理带来的结构性优势。
但也不是无脑选 vLLM。给几条我的实际判断:
个人自用、一天问几十次 :Ollama 挺好,一条 ollama run 就能跑,省心。vLLM 光冷启动加载模型就要 40 秒以上,还吃满显存,桌面都卡。
MacBook / 纯 CPU 环境 :别想了,vLLM 需要 NVIDIA GPU 和较新的 compute capability,M 系列只能走 llama.cpp 或 MLX。
多人共享、要接生产应用 :vLLM 几乎是