← Di's Notes
Speculative Decoding · 可交互图解

投机解码

先猜再验分布不变

大模型每次前向只能确定一个 token。如果先让一个小模型猜几个,再让大模型一次性批改,会发生什么?

草稿模型猜的 大模型认可 被拒绝 大模型补上
普通解码一次一个
投机解码γ = 4
两条赛道写的是同一句话,按同一个时钟走。
图 0时间条里每段蓝色是一次大模型前向,黄色是一次草稿模型前向(这里按大模型的 0.12 倍计)。

两条赛道的输出一字不差。大模型每次前向的耗时没有变,变的是每次前向能确定几个 token。下面分五步拆开它:

  1. 瓶颈在搬数据
  2. 一轮投机:猜、验、改
  3. 接受规则与分布不变
  4. 到底能快多少
  5. 草稿从哪来
Chapter 1

瓶颈在搬数据,不在计算

生成一个 token,要把模型的全部参数从显存读进计算单元一遍。一个 700 亿参数的模型用 BF16 存储,就是 140 GB。H100 的显存带宽约 3.35 TB/s,光是搬运就要 ≈ 42 ms。

真正的乘加呢?每个 token 大约需要 \(2N\) 次浮点运算,也就是 1400 亿次。H100 的 BF16 算力约 989 TFLOPS,只要 ≈ 0.14 ms。

所以逐个生成 token 时,GPU 绝大部分时间都在等数据。权重既然已经搬进来了,顺手多算几个 token 几乎不多花时间。拖动滑块试试:

一次前向的耗时roofline
4
这一次前向的时间线
图 1理想化估算:假设计算与搬运完全重叠,忽略 KV cache、注意力和通信。拐点 ≈ 989 TFLOPS ÷ 3.35 TB/s ≈ 295,与模型大小无关(多卡张量并行时,每张卡各读一份切片,比例不变)。

麻烦在于,自回归生成里第 \(t+1\) 个 token 依赖第 \(t\) 个。我们不知道后面几个 token 是什么,也就没有东西可以"顺手"一起算。

那就找一个便宜的模型先猜后面几个,
再让大模型一次前向全部验掉。

Chapter 2

一轮投机:猜、验、改

参与者有两个。草稿模型(draft model)小而快,给出分布 \(\color[RGB]{244,211,69}{q}\);目标模型(target model)就是我们真正想用的大模型,给出分布 \(\color[RGB]{88,196,221}{p}\)。用一句大家都会背的诗走一遍完整的一轮。

一轮投机解码γ = 5
q 草稿概率p 目标概率
床前明月光,疑是地上霜。
举头望明月,
第 4 个位置的残差 max(0, p − q)
图 2每个格子里左边黄柱是 q(x),右边蓝柱是 p(x)。概率数字为示意。

这一轮里有两条规矩。

第一,一旦某个位置被拒绝,它后面的草稿全部作废,因为它们是接在错误的 token 后面猜出来的。

第二,被拒绝的位置由目标模型重新采样补上;如果 γ 个草稿全部通过,目标模型在第 γ+1 个位置的分布也已经算好,可以免费再采一个。所以每一轮至少前进 1 个 token,最多前进 γ+1 个。最坏情况下,只比普通解码多了草稿那点开销。

Chapter 3

接受规则:为什么输出分布不变

第 2 章里,"低"被直接接受,"家"只有 7% 的机会被接受。这条规则有明确的来由:不管草稿模型多差,最终每个 token 的分布都必须严格等于目标模型的 \(p\)。加速不能以改变大模型的输出为代价。

草稿从 \(q\) 里抽出 \(x\)。以概率 \(\min\!\big(1,\ p(x)/q(x)\big)\) 接受它;如果拒绝,就从残差分布 \(\operatorname{norm}\big(\max(0,\ p-q)\big)\) 里重新抽一个。

先别急着看几何。下面用一组具体数字,把一个位置上发生的事情拆成最小的步骤,每一步都只做一件事。可以切换"被拒绝"和"被接受"两种结局,也可以随机重抽。

1 / 12
「低头思_」这一位置 · p 实线 / q 虚线
图 3au₁、u₂、u₃ 是三个独立的 0 到 1 均匀随机数:u₁ 用来按 q 抽草稿,u₂ 决定接受还是拒绝,u₃ 只在被拒绝时用来从残差里补采。

把上面的过程压缩成一句话,就是开头那条规则。它还有一个几何版本,能一眼看出为什么结果是 \(p\):把"从 \(q\) 抽一个 \(x\),再抽一个 \(u\)"想成往黄色柱子围成的区域里均匀地扔一个点,点的横坐标决定 \(x\),高度是 \(u\cdot q(x)\)。点落在蓝色柱子里面(重叠区)就接受,落在蓝色外面(红区)就拒绝,然后在绿区里重新均匀扔一个点。

接受的点均匀铺满重叠区,补上的点均匀铺满绿区,两块拼起来恰好是蓝色柱子 \(p\) 的形状。下面可以自己动手:拖动圆点改变 \(p\) 和 \(q\),然后扔点验证。

「低头思_」的下一个 token
p 目标 q 草稿 重叠:接受 q 多出:拒绝 p 多出:补采
接受率 α = Σ min(p, q) =0.70
最终输出的分布 · 实测
样本数 0 实测接受率 — 与 p 的总变差距离 —
图 3b上方账本条说明:q 的总面积 1 = 接受 α + 拒绝 1−α;p 的总面积 1 = 接受 α + 补采 1−α。下方柱子中蓝色来自被接受的草稿,绿色来自补采,白色虚线是目标 p。

把上面的面积写成公式。对任意 token \(x\):

\[ P(\text{输出}=x) \;=\; \underbrace{q(x)\cdot\min\!\Big(1,\frac{p(x)}{q(x)}\Big)}_{\color[RGB]{88,196,221}{\text{草稿抽中并被接受}}} \;+\; \underbrace{(1-\alpha)\cdot\frac{\max\big(0,\,p(x)-q(x)\big)}{1-\alpha}}_{\color[RGB]{131,193,103}{\text{被拒绝后补采}}} \] \[ =\; \color[RGB]{88,196,221}{\min\big(p(x),q(x)\big)} \;+\; \color[RGB]{131,193,103}{\max\big(0,\,p(x)-q(x)\big)} \;=\; p(x) \]

其中总拒绝概率 \(1-\alpha\) 正是红区面积,接受率

\[ \alpha \;=\; \sum_x \min\big(p(x),\,q(x)\big) \;=\; 1-\mathrm{TV}(p,q). \]

草稿和目标越像,重叠越多,α 越接近 1。即使草稿很烂,输出分布照样是 \(p\),只是加速没了。这就是投机解码被称为无损的原因。

温度为 0 的贪心解码是特例:\(p\)、\(q\) 都是 one-hot,规则退化为"草稿 token 等于大模型的 argmax 就接受,否则换成 argmax"。
Chapter 4

到底能快多少

做一个简化(原论文也这么做):每个草稿位置独立地以概率 \(\alpha\) 被接受。第 \(i\) 个 token 能在这一轮被产出,需要前面 \(i-1\) 个草稿全部通过,概率是 \(\alpha^{i-1}\)。把 \(\gamma+1\) 个位置加起来:

\[ \mathbb{E}[\text{每轮产出}] \;=\; \sum_{i=1}^{\gamma+1}\alpha^{\,i-1} \;=\; \frac{1-\alpha^{\gamma+1}}{1-\alpha} \]

每轮的成本是 \(\gamma\) 次草稿前向加 1 次目标前向。记 \(c\) 为两者单次前向的耗时比,则相对普通解码的加速比为

\[ \text{加速比} \;=\; \frac{1-\alpha^{\gamma+1}}{(1-\alpha)(\gamma c+1)} \]
加速比计算器
每轮期望产出
—
相对普通解码
—
当前 α、c 下的最佳 γ
—
0.80 4 0.05
第 i 个位置被走到的概率 αⁱ⁻¹
加速比随 γ 的变化
图 4上图柱子的面积之和就是每轮期望产出,绿柱是全部通过时白送的那一个。下图浅色曲线是其他 α 的对照,绿圈标出最佳 γ。

拖一拖就能看出几件事。

γ 不是越大越好。草稿越长,靠后的位置越难被走到(\(\alpha^{i-1}\) 指数衰减),草稿开销却线性增长,所以曲线先升后降。

α 比 c 重要得多。α 从 0.6 提到 0.9,收益翻倍;c 从 0.05 降到 0.02,几乎看不出差别。所以后来的工作都在想办法让草稿"更像"目标模型。

α 还取决于任务。代码、格式化输出、复述原文这类可预测的文本,α 常常很高;开放式的创意写作就低得多。

Chapter 5

草稿从哪来:各种变体

"猜、验、改"的框架不变,各种方法的区别主要在于草稿怎么来、一次验几条。

独立草稿模型2022–23

最早的形式(Leviathan 等;DeepMind 的 Chen 等)。用同系列的小模型给大模型打草稿,两者需要共用词表。

Medusa2024

不要额外的模型:在大模型最后一层接几个解码头,分别预测往后第 2、3、4… 个 token,再把候选组合成树一次验证。

EAGLE 系列2024–

草稿头在特征层(大模型的隐藏状态)上自回归,再借用大模型的输出头。特征比 token 更好预测,α 明显更高。

树形验证SpecInfer 等

草稿是一棵树而不是一条链。用特制的注意力掩码,一次前向同时验证多个分支,取通过得最长的那条路径。

Prompt Lookup无需模型

直接从输入里找与当前结尾匹配的 n-gram,把它后面的内容当草稿。摘要、改代码这类大量照抄原文的场景特别有效。

自投机self-speculative

让大模型跳过一部分层,充当自己的草稿模型(如 Draft & Verify、LayerSkip),省掉第二个模型的显存。

目前主流推理框架(vLLM、SGLang、TensorRT-LLM 等)都内置了投机解码。

Recap

一页纸版本

为什么能快

解码受显存带宽限制。权重搬一次,验 1 个和验 γ+1 个 token 的耗时几乎一样。

为什么无损

接受概率 min(1, p/q),拒绝后从 max(0, p−q) 补采。两块面积拼起来恰好是 p。

能快多少

由接受率 α 主导。每轮期望产出 (1−αγ+1)/(1−α) 个 token,γ 有最优值。
# 一轮投机解码(γ 个草稿)
# 1. 猜:草稿模型自回归地猜 γ 个 token
for i in 1..γ:
    x_i ~ q_i = draft(prefix + x_1..x_{i-1})

# 2. 验:目标模型只做 1 次前向,并行拿到 γ+1 个位置的分布
p_1, ..., p_{γ+1} = target(prefix + x_1..x_γ)

# 3. 改:从左到右逐个核对
for i in 1..γ:
    if rand() < min(1, p_i(x_i) / q_i(x_i)):
        accept(x_i)
    else:
        x' ~ norm(max(0, p_i - q_i))         # 补采
        return prefix + x_1..x_{i-1} + x'     # 后面的草稿作废

x' ~ p_{γ+1}                                  # 全部通过,白送一个
return prefix + x_1..x_γ + x'