vLLM系统拆解-08-性能优化:参数背后的系统级权衡
vLLM系统拆解-08-性能优化:参数背后的系统级权衡
很多教程讲 vLLM 调优,都是直接给一张参数表:max_num_batched_tokens 设多少,gpu_memory_utilization 设多少。这种方式的问题在于——你知道了答案,但不知道为什么,下次换个场景就不会用了。
这篇文章从 vLLM 处理混合工作负载的根本矛盾讲起,把 chunked prefill、preemption 和几个关键参数放到同一个框架下理解。读完后,你能解释这些参数的设计动机,也能针对不同业务场景给出合理的调优方向——而不只是背一个参数默认值。
前置知识:建议先读 vLLM系统拆解-04-Scheduler:为什么调度单位是token budget 和 vLLM系统拆解-05-KV Cache与PagedAttention:核心不是Attention公式,而是存放方式。
LLM 推理是一种混合工作负载
线上 LLM 推理和传统批处理服务有一个根本区别:它同时跑着性质完全不同的任务。
同一时刻,系统里可能有:刚进来的 16k token 长 prompt、正在生成第 200 个 token 的对话请求、还差几步就完成的短生成任务,以及前缀高度重复的批量请求。它们最优的处理方式并不一样。
更关键的是:prefill 和 decode 是两种截然不同的计算瓶颈。
Prefill 阶段一次性处理整段 prompt,通常是计算密集型(compute-bound);decode 阶段每轮只生成 1 个 token,但必须读取所有历史 KV Cache,通常是显存带宽密集型(memory-bound)。两者混在同一系统里,单独优化任何一个都会损害另一个。
这就是 vLLM 性能优化要解决的核心矛盾,也是接下来所有参数设计的出发点。
vLLM 所有性能优化几乎都围绕四件事展开:
- 让更多请求能够同时留在系统里
- 让 GPU 每一轮都吃到合适的工作量
- 不让长 prompt 把短请求和 decode 请求拖死
- 在显存不够时,系统不崩,而是优雅退化
Chunked Prefill:不是切 prompt,而是让 prefill 变成可调度的工作项
真正的问题是什么
第一次听到 chunked prefill,很多人的理解是:「哦,就是把长 prompt 切成几段慢慢处理。」这只说对了表象。
真正的问题不是 prompt 太长,而是:长 prompt 如果以整块方式进入 batch,会强行占满本轮 token 预算,让 decode 请求被迫等待。
举个具体例子:
- 请求 A:16k token 的超长 prompt,刚进来
- 请求 B:已经生成了 150 个 token,正在 decode,用户盯着屏幕看
如果调度器把 A 的整段 prefill 塞进来,本轮 token 预算几乎被 A 吃完。B 明明只需要生成下一个 token,却要等 A 的大块 prefill 跑完。用户感受到的就是生成流突然停顿了。
吞吐可能不低,但 ITL(inter-token latency,生成相邻两个 token 之间的延迟)会明显变差。这是线上服务最典型的「指标看起来还行,但体感很差」。
设计思想
vLLM 的 chunked prefill 是统一调度框架下的一种预算分配方式,核心逻辑如下:
1 | 每轮 iteration: |
本质不是「切 prompt」,而是:让 prefill 以可中断、可分片、可混排的方式进入调度系统。
三个好处
好处一:decode ITL 更稳定。 decode 拥有更高优先级,长 prompt 只能利用剩余预算推进,不会占满全部资源。
好处二:GPU 利用率通常更好。 纯 prefill batch 是 compute-bound,纯 decode batch 是 memory-bound。把两者按预算混排后,GPU 更容易同时利用算力和显存带宽,不至于某一种资源过度闲置。
好处三:长请求不再是系统节奏的破坏者。 没有 chunked prefill 时,遇到一个 16k 请求就是系统抖动;有了它,超长请求变成「可被吸收的细粒度工作项」,系统节奏更平稳。
两个代价
| 代价 | 具体表现 |
|---|---|
| 调度逻辑更复杂 | 不再只是「这轮跑谁」,要回答每个请求跑多少 token、谁让位给谁、多模态 token 能否被切分 |
| 三指标更容易拉扯 | decode 保护越强,TTFT 越慢;TTFT 越优先,decode 越容易被挤;吞吐越大,稳定性越容易变差 |
chunked prefill 给系统提供了更多权衡空间,但它本身不承诺「所有指标同时更好」。
三个核心参数
这三个参数是大多数调优场景里最先要动的。
max_num_batched_tokens:单轮 token 预算上限
这是调度行为最直接的控制旋钮。它决定单轮 iteration 最多允许处理多少 token,也就是上文 chunked prefill 逻辑里的「总预算」。
1 | 单轮 token 预算 |
调小和调大的效果截然相反:
max_num_batched_tokens 较小 |
max_num_batched_tokens 较大 |
|
|---|---|---|
| ITL | 更好(decode 更容易被保护) | 更差(prefill 可能挤占更多) |
| TTFT | 更差(prompt 推进慢) | 更好(prefill 大步推进) |
| 吞吐 | 较低(每轮吃的 token 少) | 较高(batch 做得更大) |
| 适合场景 | 交互式聊天、中低并发 | 批量离线推理、吞吐优先 |
一个有用的心理模型:这个参数控制的不是「batch 大小」,而是系统每轮愿意承担多少工作量,从而决定它更像低延迟交互服务还是高吞吐处理引擎。
max_num_seqs:并发对象数上限
和 max_num_batched_tokens 经常要一起看。max_num_batched_tokens 控制「装多少货」,max_num_seqs 控制「坐多少人」。
同样是 4096 token,可能是 4 条序列各 1024,也可能是 256 条序列各 16。序列数更多时:
- scheduler 元数据更多,调度开销更高
- KV block 管理更分散
- 某些场景下显存碎片和管理开销更明显
- 活跃请求更多,占用的 KV Cache block 也更多
最后一点直接关联 preemption:max_num_seqs 设得过大,KV Cache 更容易不够,preemption 更容易发生,端到端延迟反而变差。
gpu_memory_utilization:给 KV Cache 腾多少地盘
GPU 显存要同时装下:模型权重 + activation/runtime buffer + KV Cache。这个参数控制当前 vLLM 实例愿意使用多少比例的 GPU 显存,间接决定 KV Cache 能分到多大空间。
调高这个值通常意味着 KV Cache 空间变大 → 能容纳更多活跃请求 → preemption 频率下降。
但它不能设到 100%,原因很实际:runtime 期间有临时 buffer 需求,某些操作有瞬时峰值,别的进程也可能需要显存。设太满会让系统对突发负载更脆弱,稳定性下降。
Preemption:为什么它必然存在
面试常见问题:「系统设计得够好,是不是就不需要 preemption?」
答案是否定的。在 LLM 推理里,尤其是高并发 + 长上下文场景,preemption 是系统必须具备的保护机制,而不是设计缺陷。
原因在于 autoregressive 推理的一个天然特性:
- 请求在持续生成时,KV Cache 会持续增长
- 不同请求的长度和生成速度不同
- 活跃请求集合随时变化
- 某一轮看起来能容纳,下几轮未必还能容纳
即使有分页式 KV 管理,也无法静态保证「任意时刻所有活跃请求都能同时住在显存里」。当空间不够时,系统只有三条路:崩掉、拒绝新请求、或者让部分请求暂时退出活跃执行。
vLLM 选的是第三条——优雅退化而非失控崩溃。
RECOMPUTE 还是 SWAP
被抢占的请求有两种恢复方式:
- RECOMPUTE:丢弃被抢占请求的 KV Cache,恢复时重新计算
- SWAP:把被抢占请求的 KV Cache 搬到 CPU 内存,恢复时搬回来
vLLM V1 默认更偏向 RECOMPUTE。原因是:在 V1 的整体架构下,重算的综合开销通常比维护复杂的 swap 路径更低。swap 不是免费的——CPU-GPU 数据搬运有延迟,状态管理也更复杂。RECOMPUTE 是用算力换显存和状态复杂度,是一种被明确接受的工程权衡,而不是低劣的兜底方案。
Preemption 的代价
preemption 虽然保护了系统稳定性,但它打断了请求的连续推进。被抢占的请求需要重新排队、重新补齐执行状态,端到端 latency 会增加。频繁看到 preemption 日志,不应简单理解为「功能正常」,而应判断系统是否已经在显存压力下吃力维持了。
排查频繁 preemption
flowchart TD
A[频繁 preemption] --> B{KV Cache 空间是否太小?}
B -->|是| C[提高 gpu_memory_utilization\n或指定 kv_cache_memory_bytes\n或换用更省的 kv_cache_dtype]
B -->|否| D{并发对象数是否过高?}
D -->|是| E[降低 max_num_seqs\n或降低 max_num_batched_tokens]
D -->|否| F{模型权重是否占了太多显存?}
F -->|是| G[增大 tensor_parallel_size / pipeline_parallel_size\n或使用量化 / offload]
G --> H[注意: TP/PP 引入通信延迟\n不是免费午餐]
注意模型并行度这一路:TP(张量并行)和 PP(流水线并行)能释放单卡显存压力,但会引入额外通信或流水线延迟。不要把「显存不够」机械地等同于「上更多并行度」。
CPU 也会成为瓶颈
只盯 GPU 利用率是一个常见误区。
vLLM 是多进程服务系统,不是单纯的 forward 库。API server 进程、engine core 进程、每张 GPU 的 worker 进程,全都需要 CPU 资源。其中 engine core 的 busy loop 对 CPU 调度能力很敏感。
CPU 资源不足时,典型现象是:
- GPU 明明还有活可做,却吃不到
- 你看到 GPU 利用率偏低,但问题出在 CPU 喂不动,而不是 GPU 不够强
CPU 需要承担的工作包括:接收请求、tokenizer 处理、scheduler 做本轮决策、向 worker 分发执行指令、输出回传和流式发送、状态维护和日志统计。任何一个环节成为瓶颈,都会让 GPU 空转等待。
这也是为什么真正的 infra 调优需要先判断瓶颈在哪里:GPU、显存带宽、调度逻辑、CPU 核心数,还是进程间通信。
其余参数:精度、粒度与容量
kv_cache_dtype
控制 KV Cache 用什么数据类型存储。类型越省,同等显存下能容纳的 token 更多,但数值精度和实现支持会有差异。是精度、实现复杂度、KV 容量三者之间的取舍。
block_size
分页式 KV Cache 的块大小,可以理解为「KV Cache 的页大小」。
| block_size 较小 | block_size 较大 |
|---|---|
| 分配更灵活 | 管理更简单 |
| 末尾碎片可能更小 | 末尾浪费可能更明显 |
| 管理复杂度更高 | 不匹配粒度的请求浪费更多 |
offload
把部分状态或权重借助 CPU 内存存储,以 CPU-GPU 间数据搬运换取「看起来更大的可用显存」。
核心点:offload 不是凭空增加性能,而是用带宽和搬运复杂度换容量。如果 CPU-GPU 互连不够快(比如 PCIe 带宽有限),offload 可能让性能明显下降。它是显存压力下的空间换时间手段,而不是默认加速手段。
Chunked Prefill 进阶参数
这三个参数用于更细粒度地控制长 prompt 的调度行为:
| 参数 | 控制内容 |
|---|---|
max_num_partial_prefills |
允许多少条序列同时以「部分 prefill」方式推进 |
max_long_partial_prefills |
长 prompt 里最多允许多少个同时进行部分 prefill |
long_prefill_token_threshold |
多长的 prompt 算「长 prompt」 |
max_long_partial_prefills 和 max_num_partial_prefills 配合使用有个典型效果:把前者设得更小,系统会更积极地让短 prompt 插队。背后的思路是——长请求不能把全场都堵死,短请求要有机会穿过。
三指标不能同时拉满
这是调优里最需要理解清楚的约束。
1 | 高吞吐 ←————→ 低 TTFT ←————→ 低 ITL |
三个方向各自要求的配置方向是相反的:
| 优化目标 | 倾向配置 | 代价 |
|---|---|---|
| 高吞吐 | 更大的 max_num_batched_tokens、更高的显存利用率、更激进地容纳请求 |
decode 体验和端到端稳定性可能变差 |
| 低 ITL | 更保护 decode,限制大 prefill 对单轮预算的侵占,max_num_batched_tokens 偏保守 |
TTFT 和整体吞吐可能下降 |
| 低 TTFT | 给 prefill 更多预算,允许大 prompt 大步推进 | decode 更容易被拖慢,ITL 恶化 |
成熟的调优思路不是「把三个指标都拉满」,而是根据 workload 的业务目标,先确定优先级,再围绕这个优先级做取舍。
按业务场景做调优决策
不同场景的优先级不同,参数方向也随之变化:
| 场景 | 核心指标 | 调优方向 |
|---|---|---|
| 交互式聊天 | ITL、生成流畅度 | max_num_batched_tokens 适中偏小;谨慎控制长 prompt 并发推进;减少 preemption;保护 decode |
| 批量离线推理 | 吞吐、GPU 利用率 | max_num_batched_tokens 偏大;提高显存利用率;接受一定 ITL 退化 |
| 长上下文混合流量 | 防止超长请求拖崩系统 | 合理设置 long_prefill_token_threshold;限制 max_long_partial_prefills;更注意 KV Cache 空间与并发对象数 |
| 显存紧张单卡 | 系统稳定性 | 先检查 gpu_memory_utilization 是否合理;考虑更省的 kv_cache_dtype;必要时考虑 offload 或量化;目标是先让系统稳定运行,再谈极致指标 |
显存紧张场景有一个常见误区:一上来就想着量化或 offload。正确顺序应该是先把 gpu_memory_utilization 和 max_num_seqs 调到合理范围,确认 preemption 频率降下来,系统能稳定跑目标并发,再考虑更激进的显存优化手段。
核心结论
这篇文章讲了很多参数,但真正的结论只有一条:
vLLM 的「快」,不是来自某个神奇的 kernel,而是来自调度 + 显存 + 执行链 + 资源配置一起对齐。
每个参数都是这个系统的一个调节旋钮,调节某个旋钮必然在另一个方向上有代价。
几条实用判断依据:
- 看到频繁 preemption → 先查显存侧,再查并发侧,最后才考虑模型并行
- GPU 利用率偏低但吞吐不高 → 先排查 CPU 调度链是否成为瓶颈
- ITL 差但吞吐还行 → 大概率是 prefill 在抢占 decode 的预算,减小
max_num_batched_tokens或限制长 prefill 并发 - TTFT 差但 ITL 正常 → prefill 推进慢,可以适当提高单轮 token 预算
最后一点:preemption 本身不代表系统设计失败,它是高压场景下的优雅退化机制。但频繁 preemption 是一个信号,说明系统正在显存压力下吃力维持,需要找到并消除那个压力来源。
