Skip to content

第12章 投机解码:以小博大

"It is better to be approximately right than precisely wrong." -- Warren Buffett

本章要点

  • 理解自回归瓶颈:为什么解码阶段 GPU 利用率低
  • 掌握投机解码的核心思想:猜测-验证范式
  • 深入多种投机策略:Draft Model、EAGLE、n-gram
  • 理解验证算法:如何保证投机解码的输出与标准解码数学等价
  • 认识投机解码的适用场景与局限

12.1 自回归的枷锁

LLM 的解码是**自回归(Autoregressive)**的——第 N 个 Token 的生成依赖于第 N-1 个 Token。这意味着:

  • 每步只能生成 1 个 Token
  • 每步都要执行一次完整的模型前向传播
  • GPU 大部分时间在等待 KV Cache 数据从显存搬运到计算单元

在一张 A100 上,Llama-2-70B 的解码速度约为每秒 30 Token(单请求)。模型有 140 GB 的参数需要读取,即使 A100 有 2 TB/s 的显存带宽,也需要约 70ms 读一遍——这就是解码延迟的理论下限。

投机解码的核心洞察:既然每步都要读一遍参数,为什么不在一次读取中"顺便"验证多个候选 Token?

12.2 猜测-验证范式

投机解码分为两个阶段:

  1. 猜测阶段——用一个快速的方法生成 k 个候选 Token(如用 1B 参数的小模型,或 n-gram 统计)
  2. 验证阶段——将 k 个候选一次性送入大模型,计算每个位置的概率分布
  3. 接受/拒绝——从左到右逐个检查候选 Token:如果候选的概率足够高,接受;否则拒绝,并用大模型自己的采样结果替代

关键点:验证阶段只需要一次前向传播(处理 k 个 Token),而不是 k 次。因为模型可以并行计算所有位置的注意力——这正是预填充阶段的工作方式。

如果 k 个候选全部被接受,一步就生成了 k+1 个 Token(k 个接受 + 1 个大模型新采样)。即使只接受了 n 个(n < k),也比标准解码的 1 个 Token 多。

数学保证

投机解码不是"近似"——它可以保证输出分布与标准解码完全相同。这通过**拒绝采样(Rejection Sampling)**实现:

对于每个候选 Token x_i:

  • 如果 p_big(x_i) >= p_draft(x_i)(大模型的概率 ≥ 小模型的概率),直接接受
  • 否则,以概率 p_big(x_i) / p_draft(x_i) 接受
  • 拒绝时,从修正分布 max(0, p_big - p_draft) 中重新采样

这个算法保证了最终的 Token 分布与直接使用大模型采样完全一致——投机解码是精确的,不是近似的。

12.3 猜测策略

vLLM 支持多种猜测策略:

Draft Model(草稿模型)

用一个小版本的同系列模型做猜测。例如用 Llama-2-7B 为 Llama-2-70B 做猜测。小模型与大模型共享词表和分词器,推理速度快很多。

优势:猜测质量高(同系列模型的输出分布相似),接受率通常在 60-80%。 劣势:需要加载两个模型,额外占用 GPU 显存。

EAGLE / EAGLE3

源码vllm/v1/spec_decode/eagle.py

EAGLE 使用一个轻量级的"预测头"替代完整的 Draft 模型。这个预测头直接在大模型的隐藏状态上工作,不需要独立的模型前向传播。

python
# vllm/v1/spec_decode/eagle.py:22-39 (简化)
class EagleProposer:
    def __init__(self, vllm_config, device):
        self.num_speculative_tokens = (
            vllm_config.speculative_config.num_speculative_tokens)
        # ...

    def propose(
        self,
        target_token_ids: torch.Tensor,       # 大模型已生成的 token
        target_hidden_states: torch.Tensor,    # 大模型最后一层隐藏状态
        next_token_ids: torch.Tensor,          # 当前步大模型采样的 token
        sampling_metadata: SamplingMetadata,
        # ...
    ) -> torch.Tensor:
        """用大模型的隐藏状态预测接下来的 k 个 token"""

EAGLE 的核心洞察:大模型最后一层的隐藏状态已经包含了丰富的语义信息——用一个小的预测头(通常只有 1-2 层 Transformer)就能从中高效地预测后续 token。

优势:显存占用极小(预测头通常 < 500MB),猜测速度快(复用大模型的隐藏状态,不需要独立前向传播)。EAGLE3 进一步改进了预测头的架构。

劣势:需要额外训练预测头(针对特定的大模型)。

n-gram

源码vllm/v1/spec_decode/ngram_proposer.py

最简单的策略——基于已生成文本的 n-gram 统计做猜测:

python
# vllm/v1/spec_decode/ngram_proposer.py:10-26
class NgramProposer:
    def __init__(self, vllm_config):
        self.min_n = vllm_config.speculative_config.prompt_lookup_min
        self.max_n = vllm_config.speculative_config.prompt_lookup_max
        self.k = vllm_config.speculative_config.num_speculative_tokens

        # 预热 Numba JIT 编译(< 1秒)
        self.propose(np.zeros(1024, dtype=np.int32))

    def propose(self, context_token_ids: np.ndarray) -> Optional[np.ndarray]:
        """在上下文中查找 n-gram 匹配,返回匹配后的 k 个 token"""
        # 例:context = [1,2,3,4,2,3], min_n=2
        # 最后 2 个 token [2,3] 在位置 1-2 出现过
        # 返回位置 2 之后的 token: [3,4,...]

n-gram 的实现使用了 Numba JIT 编译(@jit 装饰器),将 Python 循环编译为原生机器码,在 CPU 上高效执行 n-gram 查找。

优势:零 GPU 开销(纯 CPU),不需要额外模型。 劣势:猜测质量依赖文本重复性——代码生成(大量重复模式)效果好,创意写作效果差。

使用方式:--speculative-model "[ngram]"

三种策略对比

策略接受率GPU 开销显存占用适用场景
Draft Model60-80%中(小模型前向)大(需加载小模型)通用,高质量
EAGLE50-70%低(预测头)小(< 500MB)有预训练头的模型
n-gram30-60%零(纯CPU)代码生成、翻译

12.4 在 vLLM 中的实现

V1 的投机解码模块位于 vllm/v1/spec_decode/。核心流程:

  1. 调度器分配投机预算——除了正常的 Token 预算,还分配投机步数 k
  2. Draft 阶段——Worker 调用 Draft 模型/n-gram 生成 k 个候选
  3. Verify 阶段——Worker 将 k 个候选送入大模型验证
  4. 接受/拒绝——根据拒绝采样算法确定接受多少个 Token
  5. 更新状态——接受的 Token 追加到请求上下文,更新 KV Cache

投机解码在 V1 初始 alpha 版中未被支持(因为 V1 的有状态 Worker 增加了投机解码的状态管理复杂度),在后续版本中逐步添加。

12.5 加速比分析

投机解码的理论加速比取决于两个因素:

接受率(α):候选 Token 被大模型接受的概率。接受率越高,每步有效生成的 Token 数越多。

猜测开销比(γ):Draft 阶段的计算时间 / Verify 阶段的计算时间。理想情况下 γ ≈ 0(Draft 几乎不花时间),此时加速比 ≈ 1/(1-α)。

理论加速比 = (1 + α + α² + ... + α^k) / (1 + γ × k)
           ≈ 1/(1-α) / (1 + γ × k)  (当 k 较大时)

实际数字:

  • α = 0.7, k = 5, γ = 0.1:加速比 ≈ 2.1×
  • α = 0.8, k = 5, γ = 0.1:加速比 ≈ 2.8×
  • α = 0.5, k = 5, γ = 0.3:加速比 ≈ 1.2×(几乎没有收益)

这说明投机解码需要高接受率 + 低 Draft 开销才能有效。n-gram 方法的 γ ≈ 0(纯 CPU 查表),但 α 通常只有 0.3-0.5(除非文本高度重复)。Draft Model 的 α 可以到 0.7-0.8,但 γ 不为零(小模型也要 GPU 计算)。

12.6 何时使用投机解码

投机解码不是银弹,它的收益取决于:

  • 接受率——猜测的准确度。接受率越高,加速越明显
  • Draft 成本——猜测的计算开销。如果 Draft 模型太大,猜测本身就很慢
  • batch size——大批次下,验证阶段的额外 Token 可能挤占其他请求的预算

经验法则:

  • 单请求/低并发 → 投机解码收益大(GPU 利用率低,有空间做投机)
  • 高并发 → 收益减小(GPU 已经满负荷,投机的额外计算反而成为负担)
  • 文本重复性高的任务(如代码生成、翻译)→ n-gram 策略效果好
  • 通用对话 → Draft Model 或 EAGLE 更稳定

12.7 本章小结

  • 自回归瓶颈——解码每步只生成 1 个 Token,GPU 利用率低
  • 猜测-验证范式——小模型快速猜测 k 个候选,大模型一次验证
  • 数学等价——拒绝采样保证输出分布与标准解码完全相同
  • 多种策略——Draft Model(高质量)、EAGLE(低开销)、n-gram(零成本)
  • 适用场景——低并发、高重复性任务收益最大

源码导航

基于 VitePress 构建