vLLM系统拆解-09-高级加速:Speculative Decoding、CUDA Graph与量化
vLLM系统拆解-09-高级加速:Speculative Decoding、CUDA Graph与量化
前几篇把 vLLM 的主干讲清楚了:请求进入引擎 → scheduler 分配 token 预算 → worker 在 GPU 上做 forward → KV Cache 分页管理 → prefix caching 复用跨请求前缀。
这篇进入另一个层次:在主干系统已经做得比较好的基础上,还能从哪些方向继续榨性能?
vLLM 当前比较重要的高阶加速可以归为三类:
- 算法级:Speculative Decoding(减少 decode 串行等待)
- 执行级:CUDA Graph(减少重复执行的固定开销)
- 数值级:量化(降低显存占用与数据移动成本)
这三类加速有一个共同特点:每一种都只对特定瓶颈有效,开错场景收益有限,甚至有副作用。 把它们当成无脑加速开关是常见误区。
三类加速总览
先建立整体认知,再展开每一类。
| 机制 | 本质优化对象 | 常见收益 | 常见代价 |
|---|---|---|---|
| Speculative Decoding | decode 串行生成的等待成本 | 降低 ITL,改善低 QPS 场景响应流畅度 | 额外 draft 计算、调度更复杂、高并发下收益不稳 |
| CUDA Graph | 重复 launch 与框架调度的固定开销 | 降低每轮执行开销,提升 decode 稳定性 | 需要额外 capture 内存,对动态 batch 不友好 |
| 量化 | 显存占用、数据移动与带宽压力 | 降低显存占用,可能提升吞吐 | 精度风险、算子兼容性、硬件收益差异大 |
这三者不是 vLLM 的地基,而是在「调度 + 内存 + 执行链已经对齐」之后,继续拉高性能上限的手段。
Speculative Decoding:减的不是单次 forward,是串行等待
为什么 decode 天然是串行的
Autoregressive 生成的根本约束是:生成第 t+1 个 token,必须等第 t 个 token 已经确定。 这意味着即使 GPU 算力很强,decode 阶段也只能一步一步串行走。
单次 forward 跑得再快,每一步之间的等待成本仍然存在。这是 speculative decoding 要解决的问题——注意,它不是让单次 forward 更快,而是减少必须严格串行等待的轮数。
猜测-验证的基本框架
1 | ┌─────────────────────────────────────────────────┐ |
核心逻辑:先用一个更便宜的 drafter/proposer 预测接下来几个 token,再让 target model 并行验证。如果 drafter 猜得准,target model 一轮就能确认多个 token,平均串行等待时间下降,用户感受到的输出速度提高。
这套机制是正确的(被拒绝的 token 不会改变分布),但有成本:drafter 本身要算,verification 逻辑更复杂,batch 结构可能更不规则,scheduler 处理路径也更复杂。
适用场景有明确边界
Speculative decoding 更适合:
- QPS 不高、memory-bound decode 是主要瓶颈的场景
- 用户对 ITL(inter-token latency)敏感、更在意对话流畅度的交互式应用
高并发吞吐压榨场景下,收益不一定稳定。 这时系统更怕额外计算和复杂调度,而不是怕单个请求的 token 间等待。用一句话概括:speculative decoding 是延迟优化工具,不是吞吐优化工具。
vLLM 为什么支持多种 speculation 方法
vLLM 没有把 speculative decoding 做成单一机制,而是支持 draft model、EAGLE、MTP、PARD、MLP speculator、n-gram、suffix decoding 等多种方法。
| 方法类型 | 思路 | 优点 | 局限 |
|---|---|---|---|
| Draft model | 额外小模型先草拟候选 token | 通用,适合多数场景 | 需维护额外模型,drafter 精度和延迟都很关键 |
| MTP(Multi-Token Prediction) | 目标模型原生支持一次预测多个未来 token | 若模型原生支持则路径更自然,协调成本低 | 依赖模型架构,不通用 |
| n-gram / suffix | 利用上下文重复模式或简单启发式猜测 | 轻量,易启用,在代码补全等重复性场景有价值 | 收益不如 model-based 稳定,上限有限 |
这背后的设计判断很清楚:不同模型家族、不同硬件环境、不同业务目标,适合的方案并不一样。vLLM 给出的是一套统一的承载框架,而不是把某种具体方案写死。
对主干系统的影响
Speculative decoding 不只是「多了个 drafter」,它改变了整个调度-执行链的语义:
- scheduler 层:需要考虑请求是否走 speculation 路径、当前 speculation 深度、accept/reject 后下一轮预算如何分配
- worker 层:需要执行 draft path、verify path、处理 accept/reject 逻辑、维护 speculative metadata
- 性能分析层:不能只看原始吞吐,还要看 acceptance ratio、drafter 额外成本是否合算、高 batch 下是否退化
CUDA Graph:固定开销问题,不是计算问题
Decode 阶段的固定开销困境
Decode 每步工作量不大,但如果每步都要经历:Python 层发起调用 → 构图/调度 → 发起一串 GPU kernel launch,那么固定管理成本在整个执行时间里的占比会相当高。
batch 较小、模型较小、decode 路径重复度高的场景尤其明显。CUDA Graph 的出发点就是:既然很多 decode batch 的执行结构高度相似,为什么不能把这套执行图录下来,后面直接重放?
这是执行路径的优化,不是数学计算本身的优化。Replay 后减少的是 CPU 参与度和 launch 开销,GPU 跑的还是同样的计算。
为什么不能对所有 batch 都用 CUDA Graph
关键约束:CUDA Graph 要求执行形状足够稳定。 但 LLM serving 的核心特点恰好是高度动态:
- batch size 在变
- 序列长度在变
- prefill/decode/mixed batch 形态在变
- LoRA、多模态、attention backend 条件在变
这就是 vLLM CUDA Graph 设计复杂性的来源——不是 API 难,而是「大多数时候 batch 不足够稳定」。
Full 与 Piecewise 两种模式
vLLM 不是简单二选一,而是用分层退化策略:
| 模式 | 适合场景 | 优点 | 代价 |
|---|---|---|---|
| Full CUDA Graph | 结构高度稳定的均匀 decode batch | 固定开销压得最狠,性能上限高 | capture 条件苛刻,额外内存更多 |
| Piecewise CUDA Graph | 更复杂的 batch,只对可稳定的子段做 capture | 更灵活,对动态 batch 适配度更高 | 理论极限性能通常低于 full graph |
| Eager(不走 graph) | 形态变化太大无法 capture 时兜底 | 无额外约束 | 每步都有 launch 开销 |
实际策略:能 full 就尽量 full;不适合 full 时退到 piecewise;再不行就 eager。
在 V1 里,CUDA Graph 是运行时模式,不是手动开关
这一点很重要。在 vLLM V1 中,CUDA Graph 不是用户勾选的 feature,而是由 dispatcher 根据当前 batch 条件做模式选择:当前 batch 是否规整、attention backend 是否兼容、是否是 pure decode / mixed batch、当前 compilation level 是什么。
这个设计说明 vLLM 已经把 CUDA Graph 放进了主运行时体系,而不是外部包装器。理解这一点,就超过了「知道如何开启 CUDA Graph」这个层次。
CUDA Graph 的代价同样要正视:capture 本身占额外内存,启动阶段需要编译和准备多个 batch size 变体,batch 越动态则 graph 命中率越低,收益越小。用一句话总结:用更多前期准备与额外内存,换取运行期更低的固定开销。
量化:不是为了「装得下」,而是为了「跑得动」
四类价值
量化不只是「模型太大装不下所以压缩一下」。更完整的理解是:量化同时影响容量、带宽、计算路径、吞吐和部署成本。
具体来说,在推理里量化至少有四类价值:
- 让更大的模型放进显存
- 减少权重读取的数据量(直接降低带宽压力)
- 在合适硬件上提高吞吐(使用低比特专用 kernel)
- 降低部署成本
LLM 的 decode 阶段本来就容易是 memory-bound 的。如果把权重从 FP16/BF16 压到 INT8/FP8/INT4,等于从「数据移动成本」这个根上减负,直接影响 tokens/s。
权重量化 vs KV Cache 量化:必须分开理解
这是面试里很容易被追问的点,两类量化解决的是完全不同的问题:
| 权重量化 | KV Cache 量化 | |
|---|---|---|
| 优化目标 | 减小模型参数占用与读取带宽 | 减小历史上下文缓存的显存占用 |
| 主要受益阶段 | prefill 和 decode 都有帮助 | decode 更关键,长上下文、高并发更关键 |
| 核心机制 | 每次 forward 访问权重的数据量减少 | KV Cache 体积随序列增长和并发增长的累积速度降低 |
两者最终都会反馈到调度层:可用 KV Cache 空间变大 → preemption 频率降低 → batch 能做更大 → 整体吞吐改善。量化虽然发生在数值表示层,但收益会传导到整个 serving 调度层。
vLLM 支持大量量化格式的原因
AWQ、GPTQ、bitsandbytes、GGUF、INT4 W4A16、INT8 W8A8、FP8 W8A8、KV cache 量化、TorchAO、Model Optimizer……量化方案很多,原因是量化从来不是「一招打天下」:
- 某些方法更适合离线权重量化(部署前转换)
- 某些方法更适合特定 GPU 架构(依赖专用 kernel)
- 某些方法重点在保持精度,某些在极致压缩
- 某些方法需要模型架构层的配合
所以 vLLM 的正确定位不是「某种量化工具」,而是「可接入多量化实现的推理平台」。
量化的代价也需要正视:精度损失风险(信息压缩必然损耗)、硬件专用性(不是所有平台都能把低比特收益吃满)、工程兼容性(模型结构、attention/MoE/LoRA 路径是否兼容对应 kernel)。不是 bit 数越低越好,正确思路是在可接受精度损失的前提下,找到最适合当前硬件和业务目标的方案。
三类加速与主干系统的关联
这一节值得单独拿出来。学完三类加速之后,最容易犯的错误是把它们当成孤立的 feature 去记忆。正确的方式是把它们嵌回 vLLM 的主干系统。
flowchart TD
subgraph 主干系统
A[Scheduler\n token 预算分配] --> B[Worker\n GPU forward]
C[KV Cache Manager\n 分页显存] --> B
end
subgraph 高级加速
D[Speculative Decoding] -- 改变调度语义\n speculation深度/accept/reject预算 --> A
E[CUDA Graph] -- 改变 worker 执行模式\nfull/piecewise/eager dispatch --> B
F[量化] -- 改变显存分配\n权重更小→KV Cache空间更大 --> C
end
D -- 改变 worker 执行结构\ndraft path + verify path --> B
F -- 影响 preemption 频率\n和可用 batch 预算 --> A
三类加速之间也会互相影响:
- speculative decoding 会改变 batch 形态和执行路径,可能影响 CUDA Graph 的可捕获性
- 量化影响 memory budget,从而影响 batch size、KV Cache 容量和 preemption 频率
- CUDA Graph 本身需要额外内存,也会和量化、KV Cache 预算形成耦合
这就是为什么 vLLM 要把这些优化放进统一运行时框架,而不是做成简单开关:现实 serving 环境的 batch 高度动态、硬件条件不同、模型家族不同、业务目标不同,每种优化都需要根据当前条件决定是否启用、以哪种模式启用。
几个核心结论
Speculative decoding 优化的是 decode 的串行等待,不是单次 forward 的底层效率。它更偏延迟优化,尤其适合低到中等 QPS、memory-bound 场景;高并发下收益不稳。
CUDA Graph 优化的是重复执行中的固定开销,不改变数学计算本身。LLM serving batch 高度动态,所以 vLLM 必须有 full/piecewise/eager 的分层退化设计,而不是靠单一 graph 模式走到底。
量化优化的是显存占用、数据移动和带宽压力,不是只为了「模型装得下」。权重量化和 KV Cache 量化解决的是两类不同问题,最终收益都会向上传导到调度层。
三类加速有一个共同的适用边界:它们都只对特定瓶颈有效。 判断系统当前真正的瓶颈是什么,比选择哪种加速机制更重要。

