vLLM系统拆解-16-性能瓶颈定位:从现象到根因的分层诊断方法

盲目调参是最常见的调优误区:吞吐上不去就加大 batch,显存不够就降并发,首 token 慢就加机器。问题在于,同一个现象可能由不同层的瓶颈导致,同一个参数可能同时改善一个指标、恶化另一个指标

这篇文章的目的是把 scheduler、KV Cache、chunked prefill、prefix caching、worker、CPU/GPU 协同这些机制,从"知识点"转化为"诊断能力"。读完应该能做到:看到一个性能现象,先判断更可能是哪一层的问题,再决定看哪些指标、调哪些参数、预期付出什么代价。


先建立分层归因框架

性能问题归根结底来自六个层次,每个层次的优化方向完全不同:

层次 典型症状来源 代表性问题
请求侧 workload 本身不友好 prompt 极长、输出不可控、长度分布极端不均、前缀重复率低
调度侧 scheduler token budget 分配问题 token budget 过大/小、decode/prefill 混排不当、waiting queue 堆积
KV Cache 侧 缓存空间和复用问题 KV 空间不够、prefix cache 命中率低、频繁 preemption/recompute
GPU 计算侧 kernel 和执行路径问题 attention backend 不匹配、CUDA graph 未命中、量化策略不兼容
CPU 侧 前后处理和调度链路问题 tokenizer 太重、detokenization/streaming 抢 CPU、engine core 缺核
分布式通信侧 多卡并行带来的额外开销 TP 同步、PP 流水气泡、MoE all-to-all、跨机网络

归因到正确的层次,才能避免"在错误的地方用力"。


指标体系:没有指标就没有诊断

线上没有这些指标,大多数判断只能靠猜。

核心体验指标

  • TTFT(Time To First Token):受 prompt 长度、prefill 负载、scheduling 延迟、CPU 前处理、KV 压力影响
  • ITL(Inter-Token Latency):受 decode 是否被大 prefill 干扰、memory bandwidth 压力、decode batch 大小影响
  • 吞吐(tokens/s / requests/s):受 GPU 是否被喂满、batch 能否做起来、KV 是否允许高并发、CPU 能否持续供给影响

服务运行态指标(vLLM 通过日志统计和 Prometheus 暴露)

1
2
3
4
5
6
7
8
9
running requests        → 当前活跃请求数
waiting requests → 排队等待的请求数
GPU cache usage → KV Cache 占用率
prompt tokens/s → prefill 吞吐
generation tokens/s → decode 吞吐
prefix cache hit rate → 前缀复用命中率
preemption count → KV Cache 压力信号
GPU utilization → GPU 算力利用
CPU utilization → CPU 资源压力

TTFT histogram 和 preemption 次数是判断 prefill 路径和 KV 压力最直接的信号。


七类症状的诊断路径

症状 A:TTFT 很高,但生成阶段正常

这类问题的核心是从收到请求到进入稳定 decode 之前,某一段特别慢

A1. Prompt 本来就很长
这是 workload 导致的必然 prefill 成本,不是系统 bug。长 prompt 的 prefill 是把全部输入一次性过模型,token 数越多计算越重,TTFT 自然变大。在诊断时,先区分"必然高 TTFT"还是"额外延迟"很关键。

A2. Waiting requests 很多
说明请求进入 engine 后没有尽快拿到执行机会——可能是 token budget 不够、当前并发太高、其他长请求占用太多 KV 或计算资源、CPU 调度环节变慢。

A3. CPU 忙、GPU 却不满
这是最容易被忽视的情况。官方文档明确提醒:CPU 不足会影响输入处理、scheduler 延迟、输出处理;如果 GPU 利用率比预期低,CPU contention 可能就是瓶颈。

排查方向:tokenizer/chat template 是否太重?detokenization/streaming 是否抢 CPU?engine core busy loop 是否缺 CPU 核?多进程部署下是否出现 CPU starvation?

A4. max_num_batched_tokens 设得太小
官方文档明确说:更大的 max_num_batched_tokens 通常更利于 TTFT 和高吞吐;更小的值更利于 ITL。原因很直接:token budget 越小,大 prompt 被切碎的程度越高,首 token 出来前要经历更多轮调度和执行。

A5. Prefix cache hit rate 低
业务里大量重复 system prompt,但没复用上——前缀"很像"和"完全相同"不是一回事。Prefix caching 是块级前缀一致,不是语义相似。常见原因:system prompt 混入时间戳或 request ID、chat template 不统一、few-shot 示例顺序偶尔变化、blocks 没形成足够多 full blocks、cache 容量不足导致热前缀被驱逐。


症状 B:TTFT 还行,但 ITL 很差、生成一卡一卡

进入 decode 后单步生成不流畅,这通常是 decode 路径问题,不是 prefill 路径问题。

B1. Decode 被 prefill 干扰
这正是 chunked prefill 要解决的核心问题。V1 中 chunked prefill 默认尽量开启,调度策略优先 decode——这样 decode 不需要等待完整 prefill 执行完。

检查:是否关闭了 chunked prefill?是否给 prefill 过大的 token budget?是否存在超长 prompt 持续插队?

B2. max_num_batched_tokens 过大
这是典型的 trade-off:更大的 token budget 有利于 TTFT 和 aggregate throughput,但 decode 每一步需要等待更多 prefill token 处理完,ITL 会恶化。吞吐优化和单用户流式体验优化本质上是不同的目标。

B3. 高 GPU 利用率下 ITL 仍差
可能是有意的吞吐导向配置。vLLM 的 performance-mode 现在提供 balancedinteractivitythroughput 三种取向,throughput 模式会明确牺牲单请求延迟换取高并发 total tokens/s。需要结合业务目标判断是否合理。


症状 C:吞吐上不去,GPU 利用率也不高

这个症状非常高频,本能反应是"batch 不够大",但真实原因有三种:

C1. CPU 供给不足
CPU 在输入处理、chat template 渲染、tokenizer、scheduler、detokenization、网络输出等环节卡住,GPU 就会出现阶段性断粮。表面看是 GPU utilization 低,根因是 CPU。

C2. 请求形态不利于 batching
请求很稀疏、到达率很低、prompt/output 长度差异极大、大量请求很快结束——这时系统不是"不会 batch",而是"没有足够好的 batch 机会"。这是 workload 问题,不是参数问题。

C3. KV Cache 压力让调度器保守
如果再加请求就触发 preemption,scheduler 会变得保守,主动限制并发。吞吐上不去,不是调度器笨,而是 KV Cache 空间不允许。

调参方向:依次检查 max_num_batched_tokensmax_num_seqsgpu_memory_utilization 是否过保守,是否需要 TP/PP 给 KV Cache 腾显存,以及 CPU 资源是否充分。


症状 D:日志频繁出现 preemption / recompute

Preemption 几乎直接指向 KV Cache 不够用。

官方文档说明:KV Cache 空间不足时 vLLM 会 preempt 请求释放空间,被 preempt 的请求后续 recompute;V1 默认采用 RECOMPUTE 而不是 SWAP,因为在 V1 架构下 recompute 开销更低。

Preemption 是 robustness 保底手段——空间不够时不让系统死掉,而是牺牲一部分重复计算换可运行性。偶发 preemption 可以接受,但高频 preemption 说明系统进入不健康运行区:被抢占请求后续重算、scheduler 决策空间变差、running/waiting 队列抖动、GPU 做了大量无效重复劳动。

官方推荐的修复方向及其代价:

动作 为什么有效 代价
增大 gpu_memory_utilization 给 KV Cache 更多预留显存 headroom 变小,workload 波动时更脆弱
减小 max_num_seqs 减少同时占用 KV Cache 的请求量 并发能力下降,GPU 利用率可能降低
减小 max_num_batched_tokens 减少每轮 KV Cache 消耗 吞吐可能下降
增大 tensor_parallel_size 权重摊到更多 GPU,间接腾出 KV 空间 层内同步开销,通信延迟增加
增大 pipeline_parallel_size 类似 TP,减少单卡权重占用 流水气泡,PP 延迟增加

症状 E:显存监控没满,但系统仍然 OOM 或不稳定

“显存监控显示没满"不等于"KV Cache 空间够用”。真实系统里显存被多个部分共同占用:

1
2
3
4
5
6
7
GPU 显存总量
├── 模型权重 (静态,常驻)
├── 激活和临时 buffer (动态,随执行变化)
├── KV Cache (动态,随请求增长)
├── CUDA Graph 开销 (预先 capture,额外占用)
├── 通信 buffer (多卡时额外)
└── 其他 runtime 开销

系统需要的不只是"还有多少 MB",而是"当前这轮调度能不能放下更多 block、后续 decode 增长后还能不能持续、新请求进来后有没有足够 headroom"。

如果 workload 波动很大、输出长度不可控、多模态输入偶发很大,或者还有并行通信开销,过于激进的 gpu_memory_utilization 会让系统在峰值时更脆弱。


症状 F:Prefix Cache 命中率低,业务上提示词"很像"

这里最容易出错:语义"很像"不等于 block 级前缀"完全相同"。

Prefix caching 的复用逻辑是块级 token 序列完全一致后才能命中缓存(基于哈希)——只要某一段 token 不同,后面整条 block hash 链就会不同。

六种常见根因:

  1. system prompt 中混入时间戳、request ID、随机字段
  2. chat template 不统一(空格/换行/顺序差异都算)
  3. few-shot 示例顺序偶尔变化
  4. tools/schema 每次拼接顺序不同
  5. 前缀虽然相同,但 block 大小内没凑满 full blocks
  6. cache 容量不足,热前缀不断被 LRU 驱逐

正确的修复方向是输入标准化,不是调 vLLM 参数:统一 system prompt、消除前缀中的动态字段、固定 few-shot 和 tools 排序、固定 chat template 格式。Prefix caching 的收益强依赖上层产品/服务接入的规范程度。


症状 G:多卡后显存压力缓解,但延迟反而变差

这是分布式推理的经典现象。

如果模型单卡根本放不下,TP/PP 是必要条件没有选择。但如果模型单卡能放下,只是为了多一点 KV Cache 空间或更高并发而上多卡,必须对比节省出来的显存收益和新增的通信/同步开销:

  • TP 引入层内同步(每个 Transformer 层的 all-reduce)
  • PP 引入 stage 间传递和流水气泡
  • MoE/EP 场景下有额外 all-to-all
  • 跨机时网络更可能成为瓶颈

加卡不是免费扩容。如果原先瓶颈是 decode memory bandwidth、CPU 调度或请求稀疏度,不是显存不够,加卡未必能提高 end-to-end 性能。


五个完整案例

以"现象 → 第一判断 → 验证指标 → 可能根因 → 动作"格式整理,可对照实际情况使用。

案例 现象 第一判断层 优先验证 可能根因 动作方向
案例1
首 token 慢,生成正常
TTFT↑,ITL 正常,GPU 中等,等待队列偶积 prefill 路径或 CPU 前处理 prompt 长度分布、prefix cache hit rate、CPU 利用率 prompt 太长 / token budget 太小 / prefix 没命中 / CPU 重 workload 长 prompt 接受成本;hit rate 低做模板标准化;CPU 紧补核或拆分前处理
案例2
token 一卡一卡,总吞吐不低
ITL↑,高并发时更明显 throughput 导向 batching 策略 performance-mode、chunked prefill 配置、token budget 大小 decode 被大 prefill 拖住 / token budget 过大 / throughput mode 降 token budget;切 interactivity 配置;对交互/离线流量分池部署
案例3
GPU 只跑到 40%
GPU 利用率低,CPU 高,running 不多 CPU 瓶颈 tokenizer/detokenizer CPU 占用、API server 与 engine 的核分配、streaming 开销 CPU 核不够 / chat template 或多模态前处理重 / 请求太稀疏 给足 CPU 核;优化前后处理;提高请求聚合度
案例4
高峰期延迟抖
平均值还行,P95/P99 很差,频繁 preemption KV Cache 进入紧张区 GPU cache usage、preemption 次数、输出长度尾部分布 KV Cache 预算不够 / 输出长度不可控 / 并发参数过激进 降并发上限;长上下文流量单独分池;提高 KV Cache 预算
案例5
Prefix cache 命中低
相似业务请求多,hit rate 低,TTFT 无收益 接入层模板不稳定 system prompt token 级一致性、动态字段注入位置、full block 形成比例 前缀有隐藏动态字段 / 模板顺序不固定 / cache 容量不足 统一模板;消除随机字段;固定 tools/few-shot 顺序

参数调整的代价

每调一个参数都要同时考虑收益和代价,这是面试里体现系统思维的关键:

参数/动作 可能收益 可能代价
调大 max_num_batched_tokens 更好 TTFT、更高吞吐、更高 GPU 利用率 更差 ITL、更大 KV Cache 压力、高峰期更容易抖动
调大 gpu_memory_utilization 更多 KV Cache 空间、更少 preemption、更高并发 headroom 变小、workload 波动时更脆弱、其他 runtime 容易顶满
调大 max_num_seqs 更高并发潜力、更高 aggregate throughput KV Cache 消耗更快、调度复杂度更高、对长请求更敏感
加 TP/PP 模型能放下、权重显存下降、间接给 KV Cache 腾空间 通信开销、同步延迟、不是所有 workload 都赚
切 throughput 模式 高并发 total tokens/s 更高 单用户体验可能下降,ITL 和尾延迟可能更差

很多性能问题其实是 workload 问题

这一点值得单独强调:很多问题来自 workload 形态本身,不是参数没调好。

  • Prompt 极长 → 必然高 TTFT
  • 输出极长且不可控 → KV Cache 持续扩张
  • 请求到达很稀疏 → batch 很难组起来
  • 请求长度分布极不均匀 → batch 一直处于次优状态
  • 重复前缀很少 → prefix caching 收益接近零
  • 并发高峰非常陡 → 峰值时 KV Cache 必然被挤

vLLM 提供了很强的调度、KV Cache 和服务优化能力,但最终表现由 workload、硬件、并行配置、前后处理链路共同决定。性能调优不是单参数问题,而是 workload-aware 的系统工程问题。


诊断方法论:症状 → 分层归因 → 动作 → 副作用

flowchart TD
    A[观察到性能现象] --> B[归类症状\nTTFT高?ITL差?吞吐低?尾延迟差?频繁preemption?]
    B --> C[查核心指标\nrunning/waiting请求数\nGPU cache usage\nprefix cache hit rate\nCPU/GPU utilization\npreemption次数\nTTFT直方图]
    C --> D[分层归因\n请求侧→调度侧→KV Cache侧\nGPU计算侧→CPU侧→分布式通信侧]
    D --> E[确认根因所在层]
    E --> F[决定动作\n调参数 / 改架构 / 规范输入 / 分池部署]
    F --> G[预期副作用\n这个动作会改善哪个指标?\n同时会恶化哪个指标?]

标准诊断步骤:

  1. 归类症状(TTFT 高 / ITL 差 / 吞吐低 / preemption 频繁)
  2. 查核心指标(见上方指标列表)
  3. 按层归因(区分 workload / CPU / scheduler / KV Cache / GPU kernel / 分布式通信)
  4. 决定动作(调参 / 改架构 / 规范上层输入)
  5. 说明 trade-off(每个优化付出的代价是什么)

十条诊断准则

  1. 先分清问题属于 TTFT、ITL、吞吐还是尾延迟——这四者不能混在一起。
  2. Prefill 问题不要按 decode 思路查,decode 问题不要按 prefill 思路查。
  3. GPU 不满不一定是 GPU 问题,CPU 很可能是根因。
  4. 吞吐和交互体验往往是 trade-off,不能同时拉满。
  5. 频繁 preemption 基本等价于 KV Cache 空间不够。
  6. Prefix cache 命中率低,优先查模板稳定性,不是先怪框架。
  7. 加卡不一定更快,显存收益要和通信开销一起算。
  8. 显存"看着没满"不代表系统就没有 cache 压力。
  9. 参数调优必须结合 workload 形态,不能脱离业务场景。
  10. 真正的性能定位不是改参数,而是先做分层归因。

系列导航