vLLM 性能调优:把吞吐从 1200 提到 3400 tok/s
一次完整的推理服务优化记录。连续批处理、PagedAttention、前缀缓存、投机解码,每一项带来多少提升都有实测数字。
本文目录(9)
接手一个内部推理服务时,它的吞吐是 1200 tok/s,P99 延迟 4.2 秒,机器是单张 A100 80G 跑 13B 模型。听起来还行,但实际并发一上来就开始排队。
调了一周之后:吞吐 3400 tok/s,P99 降到 1.6 秒。 这篇把每一步和对应的数字都记下来。
先建立基线,别急着调
调优最大的陷阱是「改一堆参数然后感觉变快了」。我第一件事是搭一套固定的压测:
# 固定输入输出长度,只变并发数
python benchmark_serving.py \
--backend vllm \
--base-url http://localhost:8000 \
--model Qwen/Qwen2.5-13B-Instruct \
--dataset-name sharegpt \
--num-prompts 800 \
--request-rate 8 \
--max-concurrency 32
关键指标只看三个:输出吞吐(tok/s)、首 token 延迟(TTFT)、P99 端到端延迟。
只看吞吐会得出错误结论 —— 有些配置能把吞吐堆上去,代价是长尾延迟爆炸。
第一项:把 gpu-memory-utilization 调对(+35%)
原始配置是 0.90,但机器上还跑着一个小的 embedding 服务,实际可用显存比预期少。vLLM 启动时按 0.90 预留 KV Cache 池,结果就是池子偏小、频繁触发抢占。
# 优化前:池子不够,KV Cache 频繁换出
--gpu-memory-utilization 0.90
# 优化后:留出合理余量,池子反而更大更稳
--gpu-memory-utilization 0.88
听起来反直觉 —— 比例调低了,吞吐反而涨了 35%。原因是避免了抢占(preemption)。当 KV Cache 块耗尽时,vLLM 会把某些序列换出重算,这个代价极高。
# 调优时要盯这个日志,GPU KV cache usage 长期贴近 100% 就是池子不够
INFO: GPU KV cache usage: 99.2%
INFO: Preemption count: 1847
看到 Preemption count 持续增长,就该调显存比例或者降 max-model-len 了。
第二项:限制 max-model-len(+28%)
这是最容易被忽略的一项。模型原生支持 128K 上下文,配置里就真的写了 128K。但实际上这个服务的请求平均只有 1.5K。
KV Cache 的池子是固定的,块大小也是固定的。 允许 128K 上下文意味着单个序列最多可能占用 128K 那么多块,调度器分配时会非常保守。
# 128K → 8K,显存池能容纳的并发序列数直接涨了 6 倍
--max-model-len 8192
经验做法:统计线上请求的 P99 长度,然后乘以 1.5 作为上限。 别按模型能力上限配。
第三项:开前缀缓存(+41%,取决于业务)
我们的场景是客服问答,系统提示词特别长(约 2000 token)而且所有请求完全一样。这种情况前缀缓存(prefix caching)几乎是白捡的。
--enable-prefix-caching
开启后,系统提示词那段 KV 只算一次,后续请求直接复用:
| 指标 | 未开 | 开启 |
|---|---|---|
| TTFT(P50) | 380 ms | 96 ms |
| 吞吐 | 2100 tok/s | 2960 tok/s |
第四项:批处理参数(+18%)
三个参数需要一起看:
--max-num-seqs 64 # 调度器同时处理的最大序列数
--max-num-batched-tokens 8192 # 单次前向的总 token 预算
--max-padding-tokens 512
调参逻辑:
max-num-seqs 不是越大越好。 从 32 提到 64 时吞吐涨了 18%,提到 128 反而下降 —— 因为同时活跃的序列太多,KV Cache 池不够,又开始抢占。这个值必须和显存池大小匹配。
max-num-batched-tokens 是真正控制批大小的旋钮。 它比 max-num-seqs 更直接影响单次前向的计算量。
我的调参顺序是:先固定 max-num-seqs,扫一遍 max-num-batched-tokens;找到最优点后再回过头调 max-num-seqs。
第五项:投机解码(延迟 -30%,吞吐基本不变)
投机解码(speculative decoding)用一个小模型预测多个 token,大模型批量验证。它降低的是延迟,不是提高吞吐。
--speculative-model Qwen/Qwen2.5-0.5B-Instruct \
--num-speculative-tokens 5 \
--speculative-draft-tensor-parallel-size 1
实测:
| 指标 | 未开 | 开启 |
|---|---|---|
| 单请求生成速度 | 42 tok/s | 61 tok/s |
| TTFT | 96 ms | 118 ms |
| 总吞吐 | 3400 tok/s | 3320 tok/s |
单请求快了 45%,但总吞吐略降 —— 因为小模型也占显存和计算。如果你的服务是低并发、重体验(比如桌面端助手),开它;如果是高并发服务,别开。
还有个前提:草稿模型和目标模型必须同词表,否则接受率会低到没有收益。接受率低于 60% 基本就是负优化。
优化清单汇总
| 步骤 | 改动 | 吞吐变化 |
|---|---|---|
| 基线 | — | 1200 tok/s |
| 调 gpu-memory-utilization | 0.90 → 0.88 | +35% |
| 限制 max-model-len | 128K → 8K | +28% |
| 开前缀缓存 | 新增 | +41% |
| 调批处理参数 | — | +18% |
| 投机解码 | 新增 | 延迟优化(吞吐 -2%) |
累计吞吐 1200 → 3400 tok/s。注意这些收益不是简单相加,它们有重叠,表里的百分比是每一步相对上一步的增量。
几个反直觉的结论
显存利用率调低反而更快。 因为避免了抢占。
限制上下文长度是最省事的优化。 一行配置,28% 提升。
不要同时改多个参数。 我吃过这个亏 —— 一口气改了四个参数,吞吐涨了但延迟变差,完全不知道是哪一项导致的。一次只改一个,每次都跑压测。
复现
压测脚本、调优前后的配置文件和完整报告都在下载区。要复现的话建议用同一套 sharegpt 数据集,否则数字不可比。
最后说一句:先把基线建起来,再谈优化。 没有基线的调优都是玄学。
资源下载
2 个入口链接若失效,欢迎发邮件告诉我,我会尽快重新上传。