投机解码:EAGLE / n-gram / Medusa 在 SGLang
谁该读这一篇? 想把 TPOT 再降一截、但 CUDA Graph + FlashInfer 已经用满了的工程师。 前置阅读:
03-code-walkthrough/04-model-runner.md、00-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-2 和 MTP(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 数:
每次产出的耗时:
实际 TPOT:
普通 TPOT = $T_{target}$,加速比:
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 变体
主入口:EagleWorker(eagle_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. 自检
- 用自己的话说清"为什么投机解码不影响输出分布"。
答案
关键是 **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 的"猜测"做加速。- 接受率 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),任何加速都值。- n-gram 投机在什么任务上最有用?
答案
重复结构高的任务: - **代码生成**:变量名、关键字、import 路径反复出现,n-gram 命中率 60-80%。 - **JSON / 结构化输出**:字段名、标点符号是重复字面量。 - **文档生成 / 总结**:用户内容里的关键名词被复述。 - **多轮 chat 回引用上文**:用户 prompt 里的实体在 assistant 回答里再次出现。 不适合:开放创作、对话头脑风暴(每 token 都新鲜)。 优势:免训练、零部署成本,C++ 实现([`speculative/cpp_ngram/`](../sglang/python/sglang/srt/speculative/cpp_ngram/))速度快。- 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%。- 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. 下一步
03-quantization.md— 另一个 TPOT 杀手锏。09-advanced-features/02-structured-output.md— 结构化输出与投机的协同。- 源码:
srt/speculative/。 - 参考:EAGLE 论文、EAGLE-2 论文。