自回归推理的瓶颈

标准自回归解码每次只生成 1 个 token,每个 token 都需要完整前向传播:

$$t_{\text{per_token}} = t_{\text{prefill}} + t_{\text{decode}}$$

其中 decode 阶段是访存密集型的——计算量小但需要加载全部权重和 KV Cache,GPU 利用率通常 <10%。

投机解码的核心洞察:用小模型快速生成草稿,大模型批量验证,将多次串行 decode 变为一次并行验证。

投机解码原理

算法流程

1. Draft Model 快速生成 k 个候选 token: [t1, t2, ..., tk]
2. Target Model 一次前向传播计算这 k 个位置的 logits
3. 对每个候选 token,按拒绝采样决定接受/拒绝:
   - 若 p_target(ti) / p_draft(ti) >= 1: 接受
   - 否则以概率 p_target(ti)/p_draft(ti) 接受,否则拒绝并从调整分布重采样
4. 接受的 token 加入序列,从拒绝点重新开始

拒绝采样数学

对于候选 token $x_i$,定义接受概率:

$$\alpha(x_i) = \min\left(1, \frac{p_{\text{target}}(x_i)}{p_{\text{draft}}(x_i)}\right)$$

若被拒绝,从残差分布采样:

$$r(x) = \frac{\max(0, p_{\text{target}}(x) - p_{\text{draft}}(x))}{\sum_{x’} \max(0, p_{\text{target}}(x’) - p_{\text{draft}}(x’))}$$

关键性质:投机解码的输出分布与纯 Target Model 完全一致——无损加速。

加速比分析

设 Draft Model 每步耗时 $t_d$,Target Model 每步耗时 $t_t$($t_d \ll t_t$),草稿长度 $k$,平均接受率 $\beta$。

期望接受 token 数:$E[\text{accepted}] = \frac{1 - \beta^{k+1}}{1 - \beta}$

加速比:

$$S = \frac{E[\text{accepted}] \cdot t_t}{t_d \cdot k + t_t} \approx \frac{E[\text{accepted}]}{1 + \frac{t_d \cdot k}{t_t}}$$

当 $t_d/t_t \to 0$ 时,$S \to E[\text{accepted}]$。

Draft Model 选择

方案 1:独立小模型

如用 Llama-3-1B 作为 Llama-3-70B 的 draft model。

优势:实现简单。劣势:需要额外加载模型,且分布差异大导致接受率低(~50-60%)。

方案 2:自推测解码

大模型用更少层数/更短上下文做草稿,完整模型做验证。无需额外模型。

Medusa:多头预测

Medusa(Cai et al., 2024)的突破:不需要 Draft Model,直接在 Target Model 上添加多个预测头。

架构

class MedusaHead(nn.Module):
    def __init__(self, d_model, vocab_size):
        super().__init__()
        self.linear = nn.Sequential(
            nn.Linear(d_model, d_model),
            nn.SiLU(),
            nn.Linear(d_model, vocab_size)
        )

    def forward(self, hidden_states):
        return self.linear(hidden_states)

class MedusaModel(nn.Module):
    def __init__(self, base_model, num_heads=4):
        super().__init__()
        self.base_model = base_model
        self.medusa_heads = nn.ModuleList([
            MedusaHead(base_model.config.d_model, 
                       base_model.config.vocab_size)
            for _ in range(num_heads)
        ])
    
    def forward(self, input_ids):
        # 基础模型前向传播
        hidden = self.base_model(input_ids, output_hidden_states=True)
        h = hidden.hidden_states[-1]
        
        # 主头预测 next token (heads[0] = 原始 LM head)
        main_logits = self.base_model.lm_head(h)
        
        # Medusa 头预测 future tokens
        future_logits = [head(h) for head in self.medusa_heads]
        # future_logits[0] = t+2 的预测
        # future_logits[1] = t+3 的预测
        # ...
        return main_logits, future_logits

Tree Attention 验证

Medusa 生成树状候选而非线性序列:

          [the]
         /  |  \
     [cat] [dog] [bird]
      / \    |
   [sat] [ran] [barked]

用 Tree Attention 一次性验证整棵树:

def tree_attention_verify(candidates_tree, target_model):
    # 将树展平为序列
    flat_tokens = tree.flatten()  # [the, cat, dog, bird, sat, ran, barked]
    # 构造因果掩码:只允许父→子方向
    attention_mask = build_tree_mask(tree)  # (n, n) 三角矩阵
    
    logits = target_model(flat_tokens, attention_mask=attention_mask)
    
    # 从根到叶逐层验证,找到最长接受路径
    best_path = find_longest_accepted_path(tree, logits)
    return best_path

接受率与加速

候选数Medusa 头数平均接受长度加速比
1 (无)01.01.0×
442.32.1×
842.82.5×
1643.12.7×

EAGLE

EAGLE(Li et al., 2024)改进 Medusa:

  1. 用隐状态而非 token作为 draft 输入——信息更丰富
  2. 自回归草稿生成——draft 头可以多步预测
  3. 动态草稿树——根据置信度调整树结构
class EAGLEDraftNet(nn.Module):
    def __init__(self, d_model, vocab_size):
        super().__init__()
        # 融合隐状态 + embedding
        self.fuse = nn.Linear(d_model + d_model, d_model)
        # 轻量自回归层
        self.layers = nn.ModuleList([
            nn.TransformerEncoderLayer(d_model, nhead=8, 
                                        batch_first=True)
            for _ in range(2)
        ])
        self.lm_head = nn.Linear(d_model, vocab_size)

    def forward(self, hidden_states, token_embeddings, num_steps=5):
        x = self.fuse(torch.cat([hidden_states, token_embeddings], dim=-1))
        drafts = []
        h = x
        for _ in range(num_steps):
            for layer in self.layers:
                h = layer(h.unsqueeze(0)).squeeze(0)
            logits = self.lm_head(h)
            next_token = logits.argmax(dim=-1)
            drafts.append(next_token)
            h = self.fuse(torch.cat([h, token_embeddings[next_token]], dim=-1))
        return drafts

EAGLE 在 Vicuna/LLaMA 系列上实现 3× 加速,接受率比 Medusa 高 30-50%。

Lookahead Decoding

Lookahead Decoding(Fu et al., 2024)完全不需要训练——通过 Jacobi 迭代并行生成多个 token:

原理

将自回归解码转化为 Jacobi 不动点迭代:

$$x_{i}^{(t+1)} = f(x_{<i}^{(t)}), \quad i = 1, 2, \ldots, n$$

并行更新 $n$ 个位置,收敛后得到 $n$ 个 token。

def lookahead_decoding(model, prompt, n_window=5, max_steps=100):
    seq = prompt
    for step in range(max_steps):
        # 并行预测 n_window 个 token
        candidate = seq + [0] * n_window  # padding
        logits = model(candidate)
        
        # Jacobi 更新
        changed = False
        for i in range(n_window):
            new_token = logits[len(seq) + i].argmax()
            if candidate[len(seq) + i] != new_token:
                candidate[len(seq) + i] = new_token
                changed = True
        
        if not changed:
            break  # 收敛
        seq = candidate[:len(seq) + n_window]
    
    return seq

优势:零训练成本,即插即用。劣势:加速比有限(~1.5-2×),依赖任务收敛性。

方案综合对比

方法需要训练额外参数加速比接受率适用场景
Standard Spec是(draft)2-2.5×60-70%有匹配小模型
Medusa是(轻量)2-2.7×70-80%通用
EAGLE是(轻量)2.5-3.2×80-90%追求极致
Lookahead01.5-2×N/A无训练资源
Self-Spec01.5-2×50-60%简单部署

工程实践建议

  1. 接受率监控:在线服务中持续监控接受率,低于阈值时动态减小草稿长度
  2. 草稿长度自适应:根据历史接受率调整 $k$ 值
  3. 批处理兼容:投机解码与连续批处理(Continuous Batching)需要协调
  4. Tree Attention 优化:候选树形状对性能影响大,推荐使用均匀 $k$-ary 树

总结

投机解码是当前最实用的 LLM 推理加速技术,无损输出质量。EAGLE 代表了 SOTA 方案,3× 加速使其在大多数推理场景中成为标配。未来方向是多模态投机解码和与 MoE 的深度结合。


本文由硅基 AGI 技术团队撰写,转载请注明出处。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。