独立草稿模型2022–23
最早的形式(Leviathan 等;DeepMind 的 Chen 等)。用同系列的小模型给大模型打草稿,两者需要共用词表。
先猜再验分布不变
大模型每次前向只能确定一个 token。如果先让一个小模型猜几个,再让大模型一次性批改,会发生什么?
两条赛道的输出一字不差。大模型每次前向的耗时没有变,变的是每次前向能确定几个 token。下面分五步拆开它:
生成一个 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 几乎不多花时间。拖动滑块试试:
麻烦在于,自回归生成里第 \(t+1\) 个 token 依赖第 \(t\) 个。我们不知道后面几个 token 是什么,也就没有东西可以"顺手"一起算。
那就找一个便宜的模型先猜后面几个,
再让大模型一次前向全部验掉。
参与者有两个。草稿模型(draft model)小而快,给出分布 \(\color[RGB]{244,211,69}{q}\);目标模型(target model)就是我们真正想用的大模型,给出分布 \(\color[RGB]{88,196,221}{p}\)。用一句大家都会背的诗走一遍完整的一轮。
这一轮里有两条规矩。
第一,一旦某个位置被拒绝,它后面的草稿全部作废,因为它们是接在错误的 token 后面猜出来的。
第二,被拒绝的位置由目标模型重新采样补上;如果 γ 个草稿全部通过,目标模型在第 γ+1 个位置的分布也已经算好,可以免费再采一个。所以每一轮至少前进 1 个 token,最多前进 γ+1 个。最坏情况下,只比普通解码多了草稿那点开销。
第 2 章里,"低"被直接接受,"家"只有 7% 的机会被接受。这条规则有明确的来由:不管草稿模型多差,最终每个 token 的分布都必须严格等于目标模型的 \(p\)。加速不能以改变大模型的输出为代价。
草稿从 \(q\) 里抽出 \(x\)。以概率 \(\min\!\big(1,\ p(x)/q(x)\big)\) 接受它;如果拒绝,就从残差分布 \(\operatorname{norm}\big(\max(0,\ p-q)\big)\) 里重新抽一个。
先别急着看几何。下面用一组具体数字,把一个位置上发生的事情拆成最小的步骤,每一步都只做一件事。可以切换"被拒绝"和"被接受"两种结局,也可以随机重抽。
把上面的过程压缩成一句话,就是开头那条规则。它还有一个几何版本,能一眼看出为什么结果是 \(p\):把"从 \(q\) 抽一个 \(x\),再抽一个 \(u\)"想成往黄色柱子围成的区域里均匀地扔一个点,点的横坐标决定 \(x\),高度是 \(u\cdot q(x)\)。点落在蓝色柱子里面(重叠区)就接受,落在蓝色外面(红区)就拒绝,然后在绿区里重新均匀扔一个点。
接受的点均匀铺满重叠区,补上的点均匀铺满绿区,两块拼起来恰好是蓝色柱子 \(p\) 的形状。下面可以自己动手:拖动圆点改变 \(p\) 和 \(q\),然后扔点验证。
把上面的面积写成公式。对任意 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"。做一个简化(原论文也这么做):每个草稿位置独立地以概率 \(\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)} \]拖一拖就能看出几件事。
γ 不是越大越好。草稿越长,靠后的位置越难被走到(\(\alpha^{i-1}\) 指数衰减),草稿开销却线性增长,所以曲线先升后降。
α 比 c 重要得多。α 从 0.6 提到 0.9,收益翻倍;c 从 0.05 降到 0.02,几乎看不出差别。所以后来的工作都在想办法让草稿"更像"目标模型。
α 还取决于任务。代码、格式化输出、复述原文这类可预测的文本,α 常常很高;开放式的创意写作就低得多。
"猜、验、改"的框架不变,各种方法的区别主要在于草稿怎么来、一次验几条。
最早的形式(Leviathan 等;DeepMind 的 Chen 等)。用同系列的小模型给大模型打草稿,两者需要共用词表。
不要额外的模型:在大模型最后一层接几个解码头,分别预测往后第 2、3、4… 个 token,再把候选组合成树一次验证。
草稿头在特征层(大模型的隐藏状态)上自回归,再借用大模型的输出头。特征比 token 更好预测,α 明显更高。
草稿是一棵树而不是一条链。用特制的注意力掩码,一次前向同时验证多个分支,取通过得最长的那条路径。
直接从输入里找与当前结尾匹配的 n-gram,把它后面的内容当草稿。摘要、改代码这类大量照抄原文的场景特别有效。
让大模型跳过一部分层,充当自己的草稿模型(如 Draft & Verify、LayerSkip),省掉第二个模型的显存。
目前主流推理框架(vLLM、SGLang、TensorRT-LLM 等)都内置了投机解码。
# 一轮投机解码(γ 个草稿) # 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'