跳到主要内容
推理优化 · 更新于 2026年11月1日

5.4 投机解码的收益边界

建立平均提交长度与完整轮耗时的性能账本,推导加速条件与投机长度的收益递减,分析并发、长上下文、量化、调度与流式延迟对收益的影响,以及动态投机长度的思路

Acceptance Rate TPOT Continuous Batching Dynamic Speculative Decoding 量化 性能分析

“草稿接受率 80%,是不是一定变快?“这是投机解码最常见的误判。接受率只说明猜得准不准,没说明猜一次花多久、验证一段草稿要多少算力、多出来的缓存挤掉了多少请求。每轮提交的 Token 多了,每轮的耗时也长了,两者相除才是真正的收益。

本节先用一个简化账本推导加速条件,再把真实服务中的并发、长上下文、量化、调度和流式输出逐个加进来。目标不是给出一个通用的”最佳投机长度”,而是让你能解释:为什么这组条件下变快了,换一组条件却不值得。

⏰ 信息时效:公式推导与框架无关;vLLM 相关的建议、参数与限制基于 v0.32.0(2026-10-18 发布)文档。接受率、平均接受长度的定义见第 5.2 节第 7 小节。

📑 目录


1. 先定义收益:一轮提交多少 Token

一轮提出 γ\gamma 个候选,记 K∈{0,…,γ}K\in\{0,\ldots,\gamma\} 为连续被接受的前缀长度。没有被 EOS 或长度上限截断时,这一轮提交的 Token 数是:

L=K+1L=K+1

多出来的 1 是首个拒绝位置的修正 Token,或全部接受后的额外 Token。所以哪怕一个候选都没通过,这一轮也能前进一步。

📌 关键点:“至少前进一个 Token”是进度保证,不是性能保证。全军覆没的一轮,花费的是 Draft 加验证的时间,却只换来普通 Decode 一步的产出。


2. 性能账本:从接受前缀推导加速条件

2.1 预期提交长度

对取非负整数的 KK,有尾概率恒等式:

E[L]=1+∑i=1γPr⁡(K≥i)\mathbb E[L]=1+\sum_{i=1}^{\gamma}\Pr(K\ge i)

记 aia_i 为”前 i−1i-1 个都已接受时,第 ii 个也被接受”的条件概率。由链式法则:

Pr⁡(K≥i)=∏j=1iaj\Pr(K\ge i)=\prod_{j=1}^{i}a_j

这一步不需要假设各位置相互独立。如果再简化成所有 aia_i 都等于同一个 aa:

E[L]=1+a+a2+⋯+aγ=1−aγ+11−a(a≠1)\mathbb E[L]=1+a+a^2+\cdots+a^\gamma=\frac{1-a^{\gamma+1}}{1-a}\quad(a\ne1)

a=1a=1 时取极限 γ+1\gamma+1。动态长度和树形方案每轮候选数不同,应直接用实测的平均接受长度 1+C/R1+C/R。

2.2 分母是完整的一轮

记普通 Decode 每个 Token 的平均耗时为 T0T_0,投机一轮的平均耗时为:

Tround=Tdraft+Tverify+TotherT_{\text{round}}=T_{\text{draft}}+T_{\text{verify}}+T_{\text{other}}

TotherT_{\text{other}} 包括采样、缓存维护、CPU/GPU 同步和调度。在单请求、稳定 Decode、忽略首尾边界轮的简化条件下,加速比为:

S≈T0⋅E[L]E[Tround]S\approx\frac{T_0\cdot\mathbb E[L]}{\mathbb E[T_{\text{round}}]}

要变快,必须满足 E[Tround]<T0⋅E[L]\mathbb E[T_{\text{round}}]<T_0\cdot\mathbb E[L]:一轮的耗时要少于普通 Decode 生成同样多 Token 的时间。实测时用”总提交 Token 数 / 总耗时”计算,不要对每轮的速度比取算术平均。

2.3 验证不是免费的

一次验证 γ+1\gamma+1 个位置能复用同一次权重读取,提高算术强度。但计算量、Attention、候选 KV 写入和 Logits 处理都随位置数增长。小 Batch 时 TverifyT_{\text{verify}} 接近 T0T_0;候选长、Batch 大或上下文长时,它会明显超过 T0T_0。

还是主编的比喻:审一段稿比自己写快,但审十页不可能和审一句话一样快,助手写稿的时间也得算进去。


3. 投机长度:多猜几个何时变成浪费

在固定 aa 的简化模型里,把候选从 γ\gamma 个加到 γ+1\gamma+1 个,预期提交长度只增加:

ΔE[L]=aγ+1\Delta\mathbb E[L]=a^{\gamma+1}

越往后,前面全部通过的机会越小。a=0.8a=0.8 时,第 1 个候选能带来 0.8 个预期 Token,第 8 个只能带来约 0.168 个。可 Draft 和验证每多一个位置的成本并不会跟着下降,到某个长度之后,多猜就成了净亏损。

3.1 深度与宽度是两种预算

链式投机只调深度;树形方案(第 5.3 节)还要调每层的分支数。树越宽,找到可接受路径的机会越大,但没被选中的节点也全要计算。所以只报告”投机深度 4”不够,还要报告每轮实际验证了多少个节点。

3.2 动态长度:随负载调整

最优长度会随负载变化。vLLM 的 Dynamic Speculative Decoding 用 num_speculative_tokens_per_batch_size 按并发分段设置长度,例如 [[1, 64, 3], [65, 128, 1], [129, 512, 0]] 表示并发 1–64 时猜 3 个、65–128 时猜 1 个、129 以上不投机。文档给出的理由是:Batch 为 BB、每个请求猜 γ\gamma 个候选时,验证的有效 Batch 约为 B×γB\times\gamma,超过某个临界值后反而拖慢 TPOT。

v0.32.0 中该功能在 EAGLE、EAGLE-3 和 DFlash 上测过,开启 Data Parallel 时会自动退回固定长度。另有 Adaptive Verification 为每个请求决定验证长度,支持 EAGLE、MTP、DFlash、DSpark 和独立 Draft 这类基于模型的草稿方法:DSpark 用自带的置信度头,其余方法用在线估计的接受概率。它要求完整 CUDA Graph,不支持 LoRA 和 Pipeline Parallel。

好的策略优化的是”单位时间推进多少合格输出”,而不是单独把接受率推到最高。


4. 代码与对话:任务标签不是性能结论

“代码接受率高、开放对话收益有限”是常见的经验,但真正起作用的是历史重复程度和分布匹配:

📊 场景可能有利的原因仍会失败的情况
代码编辑大段原文保留,标识符反复出现改动集中,续写方向变化大
结构化抽取输出复制输入,格式固定Prompt 里没有答案,或格式约束与候选冲突
Agent 轨迹重复步骤、相似续写多工具返回结果改变了后续走向
开放对话强 Draft 仍可能与 Target 合拍表达多样,便宜的草稿频繁被否

温度、Top-p、模型的对齐方式和 Prompt 分布都会影响接受情况。一个”原样复制代码”的题目代表不了所有代码任务,一个高温创作题也代表不了所有对话。

⚠️ 注意:随机 Token 的 Benchmark 适合控制性能变量,但它没有真实业务的重复结构。要评估 N-gram/Suffix 的实际收益,必须用真实 Prompt。


5. 高并发与长上下文:空余算力会变化

5.1 Continuous Batching 已经吃掉了一部分空闲

低并发时,每步只处理少数请求,Target 的计算单元大量空闲,投机正好利用这部分算力。并发上来后,Continuous Batching 让一次权重读取服务更多请求,普通 Decode 自己就把算力用起来了。

这时再加候选,就从”用空闲算力”变成了”和其他请求抢算力”。vLLM 文档也把投机解码定位为降低中低 QPS、访存受限场景下的 Token 间延迟。它的方法选择表给出的定性判断是:EAGLE、MTP 在低 QPS 下收益高、高 QPS 下中到高;N-gram 和 Suffix 在低 QPS 下收益低到中、高 QPS 下中等。文档开头还提到,这类简单方法加速有限,但不会在流量高峰时增加负载。

高并发也不一定无效:更准的 Draft、并行草稿和合适的 Kernel 都可能保住收益。应该按负载画曲线,不要凭一条经验全盘否定。

5.2 长上下文增加读写与临时状态

上下文越长,每个验证位置的 Attention 越贵,候选也要占额外的 KV;独立 Draft 自己的缓存同样随上下文增长。设 Batch 中有 BB 个请求、每个预留约 γ\gamma 个候选位置,Target 额外 KV 的粗略量级是:

Mcandidate≈B⋅γ⋅mKV/tokenM_{\text{candidate}}\approx B\cdot\gamma\cdot m_{\text{KV/token}}

这个估算忽略了分页取整、lookahead 槽位、共享前缀和树布局,只用来说明多出了哪一项,实际容量以引擎日志为准。

5.3 抢占会吃掉纸面收益

Draft 权重和临时缓存减少了可用 KV 空间,可能触发更多抢占和重算,或者让引擎接纳更少的请求。单轮验证更快、整个请求完成得反而更慢,这两件事并不矛盾。

因此记录接受长度时,要同时记录 KV 容量、抢占次数、成功请求数和实际吞吐。


6. 与量化叠加:先明确正确性基准

6.1 量化 Draft 与量化 Target 是两回事

  • 只量化 Draft:改变的是提议分布 qq。按第 5.1 节的证明,最终输出仍是未量化 Target 的 pp,变化的只是速度和接受率。
  • 量化 Target:验证基准变成了 pquantp_{\text{quant}}。投机解码保证与量化后的模型一致,不会把它恢复成原始精度。
  • 近似验收 + 量化:两种变化叠加,需要分别评估质量。

EAGLE、MTP 这类读取 Target 特征的模块,训练时见的是原精度特征。Target 换了量化方案,特征分布随之变化,接受率可能下降,需要重新测量。v0.32.0 的发布说明里有一项修复,针对的是 DeepSeek-V4 系列 NVFP4 检查点上 Draft 接受率为 0% 的问题。这类组合值得单独验证。

6.2 加速比不能直接相乘

量化和投机缓解的都是 Decode 的访存瓶颈。量化减少了 Target 的权重读取量,本来被访存掩盖的验证计算可能变得更显眼;同时它也释放了显存,为更多候选和请求腾出空间。

所以”量化快 1.5 倍、投机快 2 倍”推不出组合快 3 倍。应分别测原始基线、仅量化、仅投机和组合方案,并写明各自的质量基准。

📌 关键点:第 4 章的精度账本和第 5 章的采样保证,要在同一个 Target 上对齐,才谈得上组合收益。


7. 调度与流式输出:一次推进变长以后

7.1 调度器要管的不止一个新位置

开启投机后,引擎每步要为候选预留位置、分配 Token 预算,再根据接受结果更新每个请求的真实进度。同一个 Batch 里,不同请求的候选数、接受长度和剩余输出额度都可能不同。

下图上半部分是调度器的一轮,下半部分是同一 Batch 里三个请求在这一轮结束时的状态(绿格是接受的候选,蓝格是 Target 补的 Token,灰色虚线格是回退的 KV 槽位):

投机解码调度循环:分配预算、Draft 提议、Target 验证、提交前缀、更新状态;同一 Batch 内三个请求的 γ 分别为 4、2、0,本轮分别前进 3、3、1 个 Token

这条链路会和 Continuous Batching、Chunked Prefill、CUDA Graph 的固定形状以及异步调度相互影响。例如 Dynamic SD 在 v0.32.0 中只有 V2 Model Runner 支持完整 CUDA Graph。哪些组合可用,以所用版本的文档和启动日志为准。

7.2 TPOT 变小,出字不一定更均匀

投机解码一次验证可能返回好几个 Token,下一轮又要等一阵,用户看到的流式输出更像”一段一段地冒出来”。

  • TPOT:首 Token 之后,平均每个 Token 的时间。
  • ITL / 流式间隔:相邻两次返回之间的停顿。一个 chunk 里可能有多个 Token,不能直接等同于单 Token 耗时。
  • TTFT:包含排队和 Prefill,减少 Decode 轮数对它帮助不大。

评估时要把 TTFT、TPOT、流式停顿和端到端时间放在一起看。实时交互场景还要看 P95/P99,以及业务能接受的最长停顿。

7.3 停止条件与结构化输出要及时生效

接受的前缀里如果出现 EOS、Stop String 或达到输出上限,只提交结束之前的部分。结构化输出、Tool Calling 这类带状态的约束也要随每个候选前缀更新;vLLM 逐请求指标里的 num_draft_tokens,已经扣除了被结构化输出约束提前作废的候选。

遇到问题时,先让普通生成和最简单的投机配置跑通,再逐个加功能,方便定位是哪一步改变了正确性或性能。


8. 动手算账与部署门槛

8.1 接受情况相同,分母可以完全不同

下面是纯标准库的假设成本模型,时间常数只为演示,不对应任何 GPU 或真实模型。

def expected_length(conditional_acceptance):
    """E[L] = 1 + Σ Pr(K >= i),输入为各位置的条件接受概率 a_i。"""
    survival, total = 1.0, 1.0
    for a in conditional_acceptance:
        assert 0 <= a <= 1
        survival *= a
        total += survival
    return total


base_ms = 10.0  # 假设普通 Decode 每 Token 10 ms
print(f"{'γ':>2} {'E[L]':>6} {'便宜草稿':>8} {'昂贵草稿':>8}")
for gamma in [1, 2, 4, 8]:
    length = expected_length([0.8] * gamma)
    cheap_round = 10 + 0.7 * gamma + 0.1 * gamma ** 2  # 草稿快、验证增长慢
    costly_round = 10 + 5.0 * gamma + 0.8 * gamma ** 2  # 草稿慢、验证增长快
    print(f"{gamma:>2} {length:>6.3f} "
          f"{base_ms * length / cheap_round:>8.3f} "
          f"{base_ms * length / costly_round:>8.3f}")

assert expected_length([0.0] * 4) == 1
assert expected_length([1.0] * 4) == 5

两组方案的条件接受概率完全相同:便宜的那组在 γ=4\gamma=4 时达到约 2.3 倍,到 γ=8\gamma=8 反而回落;昂贵的那组只在 γ≤2\gamma\le2 时略快,γ=4\gamma=4 起就比普通 Decode 还慢。实际实验时,把两个假设的成本函数换成实测的轮耗时和提交统计即可。

8.2 上线前的四道门槛

🎯 门槛要求不通过时
正确性Target、采样参数与缓存历史对齐排查实现与配置
质量业务指标达到既定基准拆开量化、训练、近似验收各自的影响
延迟目标负载下 TTFT/TPOT 与流式停顿达标调候选长度,或换一种方法
容量与成本吞吐、KV 容量与资源成本合理比较更小的 Draft、无模型方案,或关闭投机

平均接受长度变大、轮耗时也变长、TPOT 却没改善时,沿 Draft、验证、采样和缓存维护逐项定位。若只有空载时效果好,就在目标负载下重测,再决定是否按场景启用,或交给 Dynamic SD 自动降级。


📝 总结

  • 每轮提交长度是连续接受前缀加 1,边界轮会被停止条件截断。
  • E[L]=1+∑iPr⁡(K≥i)\mathbb E[L]=1+\sum_i\Pr(K\ge i) 不要求位置独立;固定条件接受概率 aa 只是简化模型。
  • 加速的条件是完整一轮的耗时低于普通 Decode 生成同样多 Token 的时间。
  • 第 γ+1\gamma+1 个候选只多带来 aγ+1a^{\gamma+1} 个预期 Token,收益递减;树的宽度同样有成本。
  • Dynamic SD 按并发分段调整投机长度,高并发时可以自动减少甚至关闭投机。
  • 代码和对话只是实验分组,真正解释差异的是重复结构和分布匹配。
  • 高并发、长上下文和抢占会改变空闲算力与显存预算,vLLM 把投机定位在中低 QPS 场景。
  • 量化 Draft 不改变输出基准,量化 Target 会;两种加速比不能直接相乘。
  • TPOT、流式停顿、容量与业务质量共同决定是否部署。

🎯 自我检验清单

  • 能从连续接受前缀推导出预期提交长度
  • 能说明条件接受概率链式相乘为什么不需要独立假设
  • 能写出包含 Draft、验证与其他开销的加速条件
  • 能用 aγ+1a^{\gamma+1} 解释投机长度的收益递减
  • 能为 vLLM 写出一份按并发分段的 num_speculative_tokens_per_batch_size 配置,并解释它的依据
  • 能说明随机 Prompt 为什么不足以评估 N-gram/Suffix
  • 能分析高并发、长上下文与抢占如何吃掉投机收益
  • 能区分量化 Draft 与量化 Target 对输出基准的影响
  • 能解释 TPOT 改善为什么不保证流式停顿改善
  • 能按正确性、质量、延迟与容量四道门槛做部署决策

📚 参考资料