预计阅读 9 分钟

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

谁该读这一篇? 想把 TPOT 再降一截、但 CUDA Graph + FlashInfer 已经用满了的工程师。 前置阅读: 03-code-walkthrough/04-model-runner.md、00-prerequisites.md(Decode 是什么)。 耗时: 40 分钟 学完能: 1. 讲清投机解码的核心思想(draft → verify); 2. 区分 EAGLE / EAGLE3、MTP、独立 Draft、DFLASH / DSPARK 与 n-gram; 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 来源 当前 CLI 值 适用条件
EAGLE-2 / EAGLE-3 EAGLE draft checkpoint 预测 feature,再经 LM head 产生候选 EAGLE / EAGLE3 目标模型与 draft checkpoint 必须匹配
MTP (Multi-Token Prediction) 模型内置 next-token / multi-token head 常用 EAGLE 或 NEXTN,以模型说明为准 checkpoint 必须带兼容的 MTP 层
Standalone 一个更小的普通语言模型逐 token 起草 STANDALONE 需要额外 draft checkpoint
DFLASH / DSPARK 专用 draft checkpoint,走线性 block verify 路径 DFLASH / DSPARK 模型、设备和并行方式有额外限制
N-gram 从已有 token 构建的 n-gram cache 检索候选 NGRAM 无额外模型,收益高度依赖文本重复度

内置枚举还包含内部使用的 FROZEN_KV_MTP 和关闭态 NONE,插件也可以注册额外算法;以 SpeculativeAlgorithm 和 --speculative-algorithm 帮助文本 为当前事实源。 NEXTN 是 CLI alias:通常归一化为 EAGLE,检测到 Gemma 4 assistant draft 时会转为 FROZEN_KV_MTP(speculative_hook.py:35)。 MTP 和旧的 STANDALONE_NGRAM 都不是当前有效的内置 CLI 值。

源码: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

要点:

  • EAGLE draft 通常显著小于 target,并复用 target 的 embedding / LM head;具体规模和复用方式取决于 checkpoint 架构。
  • Verify 阶段用 tree attention:K 个 draft token 一起送回 target 做一次 forward(每 token 看不同的因果 mask)。
  • 接受过程用 rejection sampling 保持采样分布正确(Leviathan 2023 论文)。

4. 数学:实际加速比

下面是单路径、固定接受概率的估算模型,用于理解变量关系;EAGLE tree、batch size、 CUDA Graph、memory bandwidth 和 kernel 效率都会让实测偏离这个公式。设:

  • $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×$

接受率是关键变量之一,但必须和 draft / verify 成本一起看。


5. 在 SGLang 启动 EAGLE

sglang serve 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 / NEXTN / STANDALONE / NGRAM / DFLASH / DSPARK,也可使用插件注册值
--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:

sglang serve MODEL_PATH \
    --speculative-algorithm NGRAM \
    --speculative-num-draft-tokens 16

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

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


7. MTP(模型内置 Draft 层)

MTP checkpoint 在训练时加入额外的 next-token / multi-token prediction 层,推理时可直接把这些层当 draft, 因此很多模型不需要独立 draft checkpoint。但启用值和所需参数是模型相关的,不能把 MTP 当成通用算法名。 上游通用文档当前用 MiMo 的单层 MTP 路径作为示例:

sglang serve XiaomiMiMo/MiMo-7B-RL \
    --trust-remote-code \
    --speculative-algorithm EAGLE \
    --speculative-num-steps 1 \
    --speculative-eagle-topk 1 \
    --speculative-num-draft-tokens 2

有些内置 MTP 模型使用 NEXTN,有些仍使用 EAGLE,多层 head 还可能要求 --enable-multi-layer-eagle。直接采用对应模型的当前 cookbook 参数,并用 acceptance 与端到端 benchmark 调参。


8. 实战 cheatsheet

现象 排查
投机开了 TPOT 没降 看 sglang:spec_accept_rate / sglang:spec_accept_length,再与同负载的非投机基线对比;没有跨模型通用阈值
接受率很低 Draft 模型不匹配;任务过于发散;试不同 K
启动 OOM 外置 EAGLE / STANDALONE / DFLASH / DSPARK 要加载 draft 权重;降低静态显存占比或并发上限
长 prompt 时优势消失 投机主要优化 decode,不会缩短 target prefill;分别看 TTFT 与 TPOT
输出质量下降 rejection sampling 不应该影响分布;检查 draft 模型是否同 tokenizer

9. EAGLE-2 与 EAGLE-3

EAGLE-2 相比初版 EAGLE 使用置信度驱动的 dynamic draft tree,并在有限 token budget 下保留更有希望的候选。 在 SGLang 中用 --speculative-algorithm EAGLE。EAGLE-3 使用多层 target feature 训练 draft, 需要匹配的 EAGLE-3 checkpoint,并用 --speculative-algorithm EAGLE3。

这与下一节的 runtime adaptive 模式不是一回事:EAGLE-2 的 dynamic tree 在一次 draft 中分配候选, runtime adaptive 则根据近期接受长度在请求批次之间调整 num_steps。


10. Adaptive speculative decoding

当前 EAGLE / EAGLE3 还可以用接受长度的 EMA 在运行时调整 draft steps:

sglang serve TARGET_MODEL \
    --speculative-algorithm EAGLE3 \
    --speculative-draft-model-path DRAFT_MODEL \
    --speculative-adaptive

默认配置按 padded batch size 选择候选集合,例如 bs=1 可在 [1, 3, 7] 间切换, bs=64 可降到 0(暂时不 draft)。AdaptiveStepSlot(adaptive_spec_params.py:140)根据接受 token 数更新 EMA,经过 warmup、update interval 和 hysteresis 后升降档;--speculative-adaptive-config FILE.json 可以覆盖每个 batch 档位的 candidate_steps 和阈值。

限制:目前只支持 EAGLE / EAGLE3 与 topk=1,不支持 DP attention、multi-layer EAGLE、TBO 或 PDMux;不满足时会告警并回退静态 speculative 参数。启动时 ModelRunner.max_decode_logits_rows(model_runner.py:824)会按所有候选 steps 的最大 logits 行数分配共享 buffer,不能只按初始档位估算。


11. 源码结构

speculative/
├── spec_info.py                      ← 内置算法枚举与统一 dispatch
├── spec_registry.py                  ← 插件算法注册表
├── eagle_worker_v2.py                ← EAGLE / EAGLE3 worker
├── standalone_worker_v2.py           ← 小模型 token-level draft
├── frozen_kv_mtp_worker_v2.py        ← Gemma 4 Frozen-KV MTP
├── multi_layer_eagle_worker_v2.py    ← 多层 MTP / EAGLE
├── ngram_worker.py / cpp_ngram/      ← n-gram worker 与 C++ corpus
├── dflash_worker_v2.py               ← DFLASH worker
├── dspark_components/                ← DSPARK 组件
├── eagle_draft_cuda_graph_runner.py  ← Draft 的 CUDA Graph
├── eagle_disaggregation.py
├── draft_utils.py
├── adaptive_runtime_state.py
├── adaptive_spec_params.py
└── base_spec_worker.py

算法先由 SpeculativeAlgorithm dispatch;EAGLE 家族的主 worker 是 EagleDraftWorker (eagle_worker_v2.py)。


12. 性能监控

sglang:spec_accept_rate{...}            # 接受率
sglang:spec_accept_length{...}          # 平均每次接受多少 token
sglang:spec_num_steps{...}              # 当前 speculative steps
sglang:spec_num_draft_tokens{...}       # draft token 数
sglang:spec_verify_calls_total{...}     # verify 调用计数器

这些指标在 metrics_collector.py:421 注册。它们没有通用“健康范围”:同一个 acceptance 在不同 target/draft 成本、batch size 和算法下可能对应不同收益。 用固定模型、采样参数和请求分布做 A/B benchmark,比较 TPOT、吞吐和显存,才是开关或调档依据。


13. 小结

  • 投机解码 = draft 猜 K → target 验证 → 接受 m+1。
  • 加速比由"接受率 × draft/target 比例"决定。
  • adaptive 模式按 batch size 与接受长度动态调 steps,并预分配所有候选档位的 buffer 上界。
  • 当前内置路径包括 EAGLE / EAGLE3、模型内置 MTP、STANDALONE、NGRAM、DFLASH 与 DSPARK。
  • 投机主要优化 decode;采用精确 rejection sampling 的路径保持 target 分布。
  • 监控 acceptance,但最终用相同负载下的 TPOT / 吞吐 / 显存 A/B 结果决策。

14. 自检

  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 阈值。 例外:(a) `K` 调小(如 2)能在低 $\alpha$ 时仍小赚;(b) draft 模型极轻($T_{draft} \ll 1$ms);(c) target 模型超大($T_{target} > 100$ms),任何加速都值。
  1. n-gram 投机在什么任务上最有用?
答案 重复结构高的任务: - **代码生成**:变量名、关键字、import 路径容易重复。 - **JSON / 结构化输出**:字段名、标点符号是重复字面量。 - **文档生成 / 总结**:用户内容里的关键名词被复述。 - **多轮 chat 回引用上文**:用户 prompt 里的实体在 assistant 回答里再次出现。 不适合:开放创作、对话头脑风暴(每 token 都新鲜)。 优势:免训练、零部署成本,C++ 实现([`speculative/cpp_ngram/`](../sglang/python/sglang/srt/speculative/cpp_ngram/))速度快。
  1. EAGLE、EAGLE3、NEXTN 和 MTP 是什么关系?
答案 `EAGLE` 和 `EAGLE3` 是当前用户可选的 EAGLE-2 / EAGLE-3 CLI 值,必须与 draft 架构匹配。 MTP 是模型训练和 draft head 的类别,不是当前通用 CLI 值;不同模型可能要求 `EAGLE` 或 `NEXTN`。 `NEXTN` 是启动参数解析阶段处理的 alias:通常转为 `EAGLE`,Gemma 4 assistant draft 则转为内部的 `FROZEN_KV_MTP`。
  1. MTP 的 num_steps 应该怎么选?
答案 没有跨模型固定答案。先采用该 checkpoint 的当前 cookbook 或模型卡组合;然后固定请求分布和采样参数, 联合观察 `sglang:spec_accept_length`、TPOT、吞吐和显存,对 `num_steps`、`topk`、`num_draft_tokens` 做成组 A/B benchmark。若模型带多层 MTP head,还要确认是否要求 `--enable-multi-layer-eagle`。

15. 下一步

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