预计阅读 7 分钟

投机解码:EAGLE / n-gram / Medusa 在 SGLang

谁该读这一篇? 想把 TPOT 再降一截、但 CUDA Graph + FlashInfer 已经用满了的工程师。 前置阅读: 03-code-walkthrough/04-model-runner.md00-prerequisites.md(Decode 是什么)。 耗时: 40 分钟 学完能: 1. 讲清投机解码的核心思想(draft → verify); 2. 区分 EAGLE / n-gram / Medusa / MTP 四种方案; 3. 算"接受率 + draft 步数 → 实际加速比"的公式; 4. 在 SGLang 启动时配 EAGLE; 5. 排查"投机开了但 TPOT 没降"的常见原因。


1. 核心思想

普通 decode:每步 LLM forward 算 1 个 token,N 个 token 就 N 步。 投机 decode:

1) "Draft"(轻量模型 / 算法)一次猜 K 个 token
2) "Target"(主模型)一次 forward 验证这 K 个
3) 接受前 m 个连续匹配的 token,第 m+1 个被采样替换
4) 剩下 K-m 个丢弃

如果接受率高,一次 target forward 出 m+1 个 token,远比一次出 1 个划算。


2. 四种 Draft 方案

方案 Draft 模型 准确率 实现复杂度
N-gram 从 prompt / 已生成 token 里查 n-gram 中(场景敏感) 极低
Medusa 模型加几个 head 直接出 multi-token 候选
EAGLE 训一个小模型预测 hidden state,再走主模型 LM head
EAGLE-2 EAGLE 升级,动态调 draft 步数 更高
MTP (Multi-Token Prediction) 训练时就让模型预测多 token,推理直接用 高(需训练支持)

SGLang 主推 EAGLE-2MTP(DeepSeek-V3 系列原生支持)。

源码:srt/speculative/


3. EAGLE 流程

sequenceDiagram
    participant U as User
    participant T as Target Model<br/>(70B Llama)
    participant D as Draft Model<br/>(EAGLE head)

    U->>T: prompt
    T->>T: prefill → hidden_states_last
    T->>D: hidden_states_last + last_token
    loop K 步 draft
        D->>D: predict next hidden state
        D->>D: target.lm_head(hidden) → token
    end
    D-->>T: K 个 draft token + 对应 hidden
    T->>T: verify forward (K token + tree attention)
    T->>T: compare logits with draft, accept m
    T-->>U: m+1 个 token

要点:

  • Draft model 是个小模型(~1B 参数),跟目标模型共用 embedding 和 lm_head(省内存)。
  • Verify 阶段用 tree attention:K 个 draft token 一起送回 target 做一次 forward(每 token 看不同的因果 mask)。
  • 接受过程用 rejection sampling 保持采样分布正确(Leviathan 2023 论文)。

4. 数学:实际加速比

设:

  • $K$ = draft 步数(一次猜几个)
  • $\alpha$ = 平均接受率(每步接受概率)
  • $T_{target}$ = target 一次 forward 时间
  • $T_{draft}$ = draft 一次 forward 时间(通常 $\ll T_{target}$

每次"target verify"产出的期望 token 数:

$$E[\text{tokens per verify}] = \sum_{k=0}^{K} \alpha^k = \frac{1 - \alpha^{K+1}}{1 - \alpha}$$

每次产出的耗时:

$$T_{\text{per verify}} = K \cdot T_{draft} + T_{target}$$

实际 TPOT:

$$\text{TPOT}_{\text{spec}} = \frac{T_{\text{per verify}}}{E[\text{tokens per verify}]}$$

普通 TPOT = $T_{target}$,加速比:

$$\text{speedup} = \frac{T_{target}}{T_{\text{per verify}} / E[\text{tokens per verify}]}$$

4.1 数值例子

$T_{target} = 20$ ms, $T_{draft} = 2$ ms, $K = 5$, $\alpha = 0.8$:

  • $E[\text{tokens}] = (1 - 0.8^6)/(1 - 0.8) = 3.69$
  • $T_{\text{per verify}} = 5 × 2 + 20 = 30$ ms
  • $\text{TPOT}_{\text{spec}} = 30 / 3.69 = 8.13$ ms
  • 加速比 = $20 / 8.13 = 2.46×$

接受率从 0.8 降到 0.5:

  • $E[\text{tokens}] = 1.94$
  • $\text{TPOT} = 30 / 1.94 = 15.5$ ms
  • 加速比 = $20 / 15.5 = 1.29×$

接受率是决定一切的关键变量


5. 在 SGLang 启动 EAGLE

python -m sglang.launch_server \
    --model-path meta-llama/Llama-3.1-70B-Instruct \
    --speculative-algorithm EAGLE \
    --speculative-draft-model-path yuhuili/EAGLE-LLaMA3.1-Instruct-70B \
    --speculative-num-steps 5 \
    --speculative-eagle-topk 8 \
    --speculative-num-draft-tokens 32 \
    --tp 4

关键参数:

参数 含义
--speculative-algorithm EAGLE / EAGLE3 / NGRAM / STANDALONE_NGRAM
--speculative-draft-model-path Draft 模型权重(HF 上有公开 checkpoint)
--speculative-num-steps K:draft 步数
--speculative-eagle-topk 每步 draft 保留多少候选(tree-attention 宽度)
--speculative-num-draft-tokens 整个 draft tree 的总 token 上限

EAGLE 训练好的 draft model 在 HuggingFace 上有现成的(yuhuili 仓库)。


6. n-gram speculative

不需要 draft model:

python -m sglang.launch_server \
    --speculative-algorithm NGRAM \
    --speculative-num-steps 8 \
    --speculative-num-draft-tokens 16

实现:在已生成 token 流里查 n-gram 匹配。 适合"重复结构高"的任务(代码、JSON、模板化输出)。

源码:speculative/cpp_ngram/ 是 C++ 加速版本。


7. MTP(DeepSeek-V3 原生)

DeepSeek-V3 训练时就加了"Multi-Token Prediction"目标——每一层都预测下一个、下下一个 token 的 logits。 推理时直接拿前几层的预测作为 draft,跳过专门的 draft model:

python -m sglang.launch_server \
    --model-path deepseek-ai/DeepSeek-V3 \
    --speculative-algorithm MTP \
    --speculative-num-steps 1

通常 $K=1$ 就足够(DeepSeek-V3 的 MTP 头训练目标就是 1 步)。


8. 实战 cheatsheet

现象 排查
投机开了 TPOT 没降 看接受率(看 metrics spec_accept_rate);接受率 < 0.5 时往往是负优化
接受率很低 Draft 模型不匹配;任务过于发散;试不同 K
启动 OOM EAGLE 需要装第二个模型;显存别留满
长 prompt 时优势消失 TTFT 不受影响;只优化 decode
输出质量下降 rejection sampling 不应该影响分布;检查 draft 模型是否同 tokenizer

9. EAGLE-2 改进

EAGLE-1 用固定 $K$ 步。EAGLE-2 引入:

  • 动态 $K$:根据 draft model 信心调整步数(信心高就多猜)。
  • Tree pruning:丢掉低概率的 draft 分支。
  • 接受率提升 10-20%。

SGLang 用 --speculative-algorithm EAGLE3 触发 v3(也支持 v2)。


10. 源码结构

speculative/
├── eagle_worker.py                   ← EAGLE 主 worker
├── eagle_utils.py                    ← utils
├── eagle_info.py / eagle_info_v2.py
├── eagle_draft_cuda_graph_runner.py  ← Draft 的 CUDA Graph
├── eagle_disaggregation.py
├── cpp_ngram/                        ← n-gram C++ 实现
├── draft_utils.py
├── adaptive_runtime_state.py
├── adaptive_spec_params.py
├── base_spec_worker.py
└── dflash_*.py                       ← DFlash 变体

主入口:EagleWorkereagle_worker.py)。


11. 性能监控

sglang:spec_accept_rate{...}            # 接受率
sglang:spec_avg_accepted_tokens{...}    # 平均每次接受多少 token
sglang:spec_target_forward_ms{...}      # target forward 时间
sglang:spec_draft_forward_ms{...}       # draft forward 时间

健康范围:

  • accept_rate > 0.6
  • avg_accepted > 2
  • speedup(用上面公式算)> 1.5×

12. 小结

  • 投机解码 = draft 猜 K → target 验证 → 接受 m+1。
  • 加速比由"接受率 × draft/target 比例"决定。
  • SGLang 主推 EAGLE-2 / EAGLE-3、MTP(DeepSeek 原生),简单场景用 n-gram。
  • 投机不影响 TTFT、不影响输出分布(rejection sampling 保证)。
  • 监控 spec_accept_rate;< 0.5 通常是负优化,要么换 draft 要么关掉。

13. 自检

  1. 用自己的话说清"为什么投机解码不影响输出分布"。
答案 关键是 **rejection sampling**(Leviathan 2023 论文):draft 模型给出候选 token $x$ 时,target 模型也算出自己的概率 $p_t(x)$;draft 概率为 $p_d(x)$。 接受规则:以概率 $\min(1, p_t(x) / p_d(x))$ 接受 $x$;拒绝时按 $\max(0, p_t(x) - p_d(x)) / Z$ 重采样。 数学上可证明:最终采样的边际分布 == target 模型的原始采样分布。 直观:当 draft 倾向某 token 而 target 不倾向,接受概率低;反过来 draft 不倾向但 target 强烈倾向,拒绝时重采样补回。结果完全等价于直接从 target 采样,只是用了 draft 的"猜测"做加速。
  1. 接受率 0.3 时还应该开投机吗?
答案 **通常不应该**。用 §4 的公式:$T_{target}=20$ms, $T_{draft}=2$ms, $K=5$, $\alpha=0.3$$E[\text{tokens}] = (1-0.3^6)/(1-0.3) = 1.43$ $T_{\text{per verify}} = 5×2 + 20 = 30$ms $\text{TPOT} = 30/1.43 = 21$ms > 20ms 直接 decode 接受率 < ~0.5 时投机是负优化(draft 算了白算)。 例外:(a) `K` 调小(如 2)能在低 $\alpha$ 时仍小赚;(b) draft 模型极轻($T_{draft} \ll 1$ms);(c) target 模型超大($T_{target} > 100$ms),任何加速都值。
  1. n-gram 投机在什么任务上最有用?
答案 重复结构高的任务: - **代码生成**:变量名、关键字、import 路径反复出现,n-gram 命中率 60-80%。 - **JSON / 结构化输出**:字段名、标点符号是重复字面量。 - **文档生成 / 总结**:用户内容里的关键名词被复述。 - **多轮 chat 回引用上文**:用户 prompt 里的实体在 assistant 回答里再次出现。 不适合:开放创作、对话头脑风暴(每 token 都新鲜)。 优势:免训练、零部署成本,C++ 实现([`speculative/cpp_ngram/`](../sglang/python/sglang/srt/speculative/cpp_ngram/))速度快。
  1. EAGLE 比 Medusa 准确率高的原因?
答案 Medusa:在 target 模型最后一层加几个"prediction head",每个 head 直接预测未来某步的 token——但只用最终 hidden,**信息不够,且失去时序依赖**。 EAGLE:训一个小模型预测下一步的 **hidden state**(不是直接 token),然后走 target 模型自己的 LM head 出 logits——保留完整时序依赖 + 与 target 同 lm_head 保证分布一致。 后果:EAGLE 接受率常 0.7-0.85,Medusa 通常 0.5-0.65。代价是 EAGLE 训练更复杂、需独立 checkpoint。 EAGLE-2 进一步加动态 K 和 tree pruning,接受率再涨 10-20%。
  1. DeepSeek-V3 用 MTP 时 K 通常取多少?为什么?
答案 通常 $K=1$(一次猜下一个 token)。 原因:DeepSeek-V3 训练时 MTP 目标就是"下一个 + 下下一个 token",模型的 MTP head 只对 1-2 步有良好预测能力;K>2 准确率快速衰减。 实测 K=1 接受率约 0.85,K=2 降到 0.65,K=3 已 < 0.5 不值得。 配合 K=1 + EAGLE-style verify tree(topk_branch 2-4),实际每次 target forward 平均吐 1.7-1.8 token,TPOT 降 40%。 不同 MTP 训练方式的最佳 K 不同,看模型卡的 release notes。

14. 下一步

上游源码:sglang/python/sglang/srt/speculative/