vLLM系统拆解-08-性能优化:参数背后的系统级权衡

很多教程讲 vLLM 调优,都是直接给一张参数表:max_num_batched_tokens 设多少,gpu_memory_utilization 设多少。这种方式的问题在于——你知道了答案,但不知道为什么,下次换个场景就不会用了。

这篇文章从 vLLM 处理混合工作负载的根本矛盾讲起,把 chunked prefill、preemption 和几个关键参数放到同一个框架下理解。读完后,你能解释这些参数的设计动机,也能针对不同业务场景给出合理的调优方向——而不只是背一个参数默认值。

前置知识:建议先读 vLLM系统拆解-04-Scheduler:为什么调度单位是token budgetvLLM系统拆解-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 所有性能优化几乎都围绕四件事展开:

  1. 让更多请求能够同时留在系统里
  2. 让 GPU 每一轮都吃到合适的工作量
  3. 不让长 prompt 把短请求和 decode 请求拖死
  4. 在显存不够时,系统不崩,而是优雅退化

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
2
3
4
5
每轮 iteration:
① decode 请求优先占位(每条各占 1 token)
② 剩余 budget → 填充 prefill
③ 某个 prefill 太大放不下 → 只放部分,下轮继续
④ 重复,直到预算耗尽

本质不是「切 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
2
3
单轮 token 预算
├── decode 请求:每条 1 token(优先占位)
└── 剩余 → prefill token(按 chunked prefill 规则填充)

调小和调大的效果截然相反:

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_prefillsmax_num_partial_prefills 配合使用有个典型效果:把前者设得更小,系统会更积极地让短 prompt 插队。背后的思路是——长请求不能把全场都堵死,短请求要有机会穿过。


三指标不能同时拉满

这是调优里最需要理解清楚的约束。

1
2
3
高吞吐 ←————→ 低 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_utilizationmax_num_seqs 调到合理范围,确认 preemption 频率降下来,系统能稳定跑目标并发,再考虑更激进的显存优化手段。


核心结论

这篇文章讲了很多参数,但真正的结论只有一条:

vLLM 的「快」,不是来自某个神奇的 kernel,而是来自调度 + 显存 + 执行链 + 资源配置一起对齐。

每个参数都是这个系统的一个调节旋钮,调节某个旋钮必然在另一个方向上有代价。

几条实用判断依据:

  • 看到频繁 preemption → 先查显存侧,再查并发侧,最后才考虑模型并行
  • GPU 利用率偏低但吞吐不高 → 先排查 CPU 调度链是否成为瓶颈
  • ITL 差但吞吐还行 → 大概率是 prefill 在抢占 decode 的预算,减小 max_num_batched_tokens 或限制长 prefill 并发
  • TTFT 差但 ITL 正常 → prefill 推进慢,可以适当提高单轮 token 预算

最后一点:preemption 本身不代表系统设计失败,它是高压场景下的优雅退化机制。但频繁 preemption 是一个信号,说明系统正在显存压力下吃力维持,需要找到并消除那个压力来源。



系列导航