阶段 6 · 让模型跑起来

投机解码:让大模型少跑几趟
让小的先猜答案

上一章《高效注意力》说:瓶颈不在「算」,而在「搬」——Decode 也一样,为生成 1 个 token 要把全部权重读一遍。 那能不能一次前向多产出几个 token?
投机解码用一个很巧的办法做到了,而且输出分布和原来一模一样。

1

先看清浪费在哪

一次 Decode 前向要做什么:把全部权重从显存读进计算单元,做完矩阵乘法,吐出一个 token。

互动 · 一次前向到底忙不忙

Prefill 阶段(读 prompt,也就是你输入的那段提示词):一次处理几百上千个位置, 矩阵足够大,GPU 算力被吃满,这叫 compute-bound(算力受限)。
Decode 阶段(吐 token):一次只处理 1 个位置,但还是要读一遍全部权重—— 读了几十 GB,只做了极少的乘加,算力利用率可能只有个位数百分比,这叫 memory-bound(带宽受限)。
卡住 Decode 的不是算得快不快,而是显存搬得快不快。

互动 · 一次 Decode 的时间,都花在哪

一张图看清 Prefill 和 Decode 差在哪

🎯 类比

像一个每次只运一块砖的卡车。卡车的载重能力(算力)完全不是问题, 问题是跑一趟要烧的油(读取全量权重)永远不变。
那最直接的想法就是:能不能一趟多运几块砖?

2

大部分 token 其实很好猜

「一趟多运几块砖」这个想法卡在一个地方:你没法确定下一块砖在哪。
所以在讲怎么运之前,先解决一个更前置的疑问——凭什么猜得准?

下面是一句普通的中文,切成 10 个 token。逐个位置问模型:「下一个词是什么?」 把模型在每个位置给出的最高概率画出来。

互动 · 逐个位置的预测置信度(真过一遍 softmax)

平均置信度
—
把握 ≥ θ 的位置
—
要留给大模型兜底
—
整串一次全猜对
—

把上面那张图从上往下扫一遍——10 个位置里有 6 个的置信度在 0.83 以上: 「的」(0.92)、「是」(0.95)、「于」(0.89)、「北京」(0.89)、「年」(0.92)这些位置, 模型几乎没有第二种答案,它们被语法和常识锁死了。 真正没底的位置只有两个:句首(0.36)和那个年份(最高一项只有 0.20,1949 和 1978 几乎打平)。
所以「让小模型去猜」这句话才成立:它要猜的不是难的位置,而是那些本来就没有悬念的位置。

互动 · 分布越平,熵越大:拖一拖「分布平坦度」

互动 · 「每个位置都好猜」为什么不等于「整串都能猜对」

🎯 类比

这像一条只有一两个岔路口的山路——剩下八九个弯都是死弯,闭着眼也开得过去。
于是真正的问题不是「怎么把山路修直」,而是:既然大部分弯不需要思考, 能不能先派个便宜的人去开?

3

办法:让小的先猜,大的来验

既然大部分位置本来就只有一个合理答案,「猜」这件事就有戏了。 但要真的省下时间,还得先过一道坎:你猜的这一串,大模型要怎么验?

这里有个看起来很矛盾的地方:大模型天生就只能一次吐一个 token—— 因为第 2 个 token 依赖第 1 个,你没法并行猜。

但换个角度:「验证」不需要串行。如果你已经拿到了一串候选 token, 大模型可以一次前向同时给这串里每个位置打分——这是并行的。

① 小模型(draft)快速猜 k 个 它小得多、跑得快,一口气连猜 k 个 token。猜错也没关系。
② 大模型一次前向,同时验证这 k 个 一次前向就能拿到这 k 个位置各自的概率分布——这是并行的那一步。
③ 从头开始比对,接受到第一个不匹配为止 接受前缀,拒绝的那个位置重新采样,然后丢弃后面全部。
④ 从被拒绝的位置重新开始下一轮 如果 k 个全被接受,还能白拿一个额外 token(叫 bonus token)。

一张图看完一轮:猜 → 一次并行验证 → 接前缀 → 白拿一个

判断一条猜测留不留,规则很朴素:以概率 min(1, p/q) 接受 (p 是大模型给这个 token 的概率,q 是 draft 给的)。
直觉是:只有当 draft 比大模型还自信(q 比 p 大)时才打折,其余照单全收。 被拒的那一位,从残差分布(draft 漏掉的那部分概率,归一化之后)里重采一个补上—— 为什么这样拼出来的分布和原来一模一样,到第 M 节证明。

💡 这张四步图,和它最妙的一句

整章的机制就在上面那张四步图里:小模型猜 k 个 → 大模型一次前向并行验证 → 接住前缀、在第一个被拒处重采。后面几节只是把它的账算清楚。
最妙的是:组合出来的最终分布严格等于大模型自己的分布 p——它不是近似,只是快。
(别把大模型想成「审稿人」:审稿人会按自己的意思改稿;这里的验证是一条数学规则, 保证最终输出的分布和大模型自己写的一模一样。前提是 draft 和 target 用同一套采样设置,破坏它的是实现细节,见第 7 节「四条必须知道的限制」。)

4

亲手跑一遍:分布真的没变吗

下面用一个小词表(6 个候选词,大模型的答案集中在第一个词上)真实地跑投机采样。 注意最下面那一行指标——它会检查采样出来的分布是不是真的等于大模型的分布。

互动 · 真实的投机采样 + 分布正确性检验

实测接受率
—
理论接受率
—
每轮平均产出
—
推算加速比
—

💡 看「分布正确性检验」那一栏

它里面有两个都叫 TV 的数,说的是两件事:
一个是 TV(采样出来的分布, p)——「跑出来的分布」和大模型真实分布差多远,应该非常小;
另一个是 TV(p, q)——大模型和 draft 本身差多远,它决定了接受率(1 − TV(p, q) = 理论接受率)。
(总变差距离 TV:两个分布逐项差的一半之和,0 表示一模一样。)
把 draft 质量拉到最差(q 接近均匀分布)再跑一次:接受率会掉,但第一个 TV 仍然很小。 接受率影响的是速度,不影响正确性。

互动 · 「分布没变」这件事,跑得越多越看得清

5

k 不是越大越好

直觉上你会想:多猜几个不是更好吗?不是。因为每多猜一个,就要多付一份小模型的成本, 而越靠后的猜测越可能被拒绝——一旦第 2 个就错了,后面猜的全白费。

α = 接受率 c = 小模型成本 ÷ 大模型成本 k = 每轮猜几个

互动 · 找最优的 k

当前 k 的加速比
—
最优 k
—
最优加速比
—

接受率高(draft 好)时:最优 k 可以很大,常见设置下加速比 3~4 倍; 本页这个成本模型在 α=0.99 时读数能到 5 倍(那是很理想的情况)。
接受率低(draft 差)时:最优 k 缩到 1~2,加速比可能只有 1.1 倍,甚至低于 1(反而更慢)。
所以投机解码不是"开了就快"——它高度依赖你的 draft 模型和目标任务是否匹配。

M

数学 · 猜错为什么一点不亏

第 3 节留下了一句最重的话:接受规则不是随便定的——用上它之后,输出分布严格等于大模型自己的分布。 这句话值得单独画一张图。

下面这张图只做一件事:把「大模型的概率 p」一格一格拆开,看它是由哪两段拼起来的。 猜得准不准,只影响这两段各有多高,不影响它们叠起来正好等于 p。
(这一节换一张更平的词表:大模型最高的一项只有 0.52——所以就算用同一套混合规则(draft 偏差都取 0.35),接受率也会和第 4 节不一样。)

一张图说清:猜错的部分去哪了

绿色那一段 = min(p, q):draft 猜到的部分,直接接受。
粉色那一段 = max(0, p − q):draft 漏掉的部分,拒绝之后从残差分布里重新采一次补回来—— 这件事就叫拒绝采样。
绿色原样留下,粉色被整体放大再补上,两段叠起来一格不差地等于 p。 所以这不是近似——是把同一个分布换了条路生成。

微型图解 · 公式里那个 αk+1 是从哪来的

🎬 拖一拖看规律

把上面微型图解的 α 拖到 0.9,再把 k 从 3 拖到 12:柱子越加越多, 但新加进来的那根越来越矮——这就是第 5 节那条曲线先升后降的原因。
反过来把 α 拖到 0.3:第二根柱子就几乎没了,每轮平均只产出 1.4 个 token——多猜的全白花。

6

加速的六条路线

方法猜的来源特点
Speculative Decoding
原始版
另一个小模型(同族或蒸馏出来的) 思路最清晰,理论保证严格。代价:要多加载一个小模型
Self-Speculative 不用额外模型——只用大模型的前几层当 draft 零额外显存。因为浅层的特征本来就够猜个大概
Medusa 在模型上挂几个额外的解码头,各自预测不同位置的未来 token 不改变主干,只训很轻的头。可以用树状结构一次猜多个分支(一次把多个候选一起猜,再挑通过的那条)
Lookahead Decoding 不用任何额外网络,用Jacobi 迭代在已生成的轨迹里找可用的 token(Jacobi 迭代:把相互依赖的几步拆成同时猜、反复修正) 完全不需要训练。但加速幅度通常不如专门训过的方法
EAGLE 在特征层(模型内部的中间表示,而不是最后的词概率)做自回归猜(用已生成的词接着猜下一个) 目前公开报告加速比最高的方法之一。代价是要额外训练一个 draft 头
连续批处理
Continuous Batching
(不是投机,但同属推理加速三板斧) 不逐条请求等完,而是每个 step 动态重组 batch。提升的是吞吐,不是单条延迟

投机解码提升的是「单条请求的生成速度」(延迟)。 连续批处理提升的是「同时服务多少请求」(吞吐)。
两者可以叠加,而且经常一起用。但它们的"快"说的是两件不同的事—— 看你是在优化你的等待时间,还是在优化服务器能扛多少用户。

一张图看完六条路线:要「猜」的那一份从哪来

7

四条必须知道的限制

限制说明什么时候真的会痛 → 换什么
① 要多一份显存 draft 模型本身要占显存,本来就被 KV Cache 卡死显存的场景更难塞下 显存本来就紧 → 选 Self-Speculative / Medusa / EAGLE,它们不加载第二个完整模型
② 接受率低时反而更慢 每轮固定付出 k 次小模型 + 1 次大模型的成本;大部分猜测被拒时,比老老实实一个个生成还慢 draft 和任务不匹配、α 很低 → 换更贴合的 draft,或把 k 调小
③ 分布等价有前提 严格等价对任意温度都成立,前提是 draft 和 target 用同一套采样设置 (同样的 temperature / top-p / top-k) 只对 draft 做 top-p 截断,或用了 typical acceptance(非精确的接受准则,拿等价换接受率)→ 要可复现就别这么配
④ 大 batch 时收益消失 大 batch 下 GPU 算力本就被喂饱,不再是 memory-bound,投机解码不但没收益,还白白多算 k 倍 draft 高并发、高吞吐场景 → 优先做批处理,别开投机解码
⚠️ 记住这句话

投机解码是「延迟优化」,不是「吞吐优化」。
它拿空闲的算力去换更少的显存访问次数。一旦算力不再空闲(大 batch),这个交易就不成立了。
看到"提速 3 倍"的宣传时,先问一句:这是在什么 batch size 下测的?

互动 · 批一大,这份「闲置算力」就没了

8

小结

它对应哪条线 ② 没有免费午餐——但用的是它的前半句:任何「更快」都要有对价。
投机解码看起来像免费午餐——输出分布一字不变、生成还更快。世界上没有这种事, 所以这一章真正在讲的是:它对价付在哪里,什么时候这个价会涨到不划算。
一句话 这一章的做法,本质上是在把「你一直在浪费的那份闲置算力」拿出来花,去买「更少的显存访问次数」—— 因为 Decode 卡住的从来不是算得快不快,而是把几十 GB 权重搬进计算单元的那条带宽。
它牺牲了什么 牺牲了「开了就一定快」这个保证——加速比随接受率 α 漂移,事先不知道能快多少; 还搭上一份本可以拿来装更多请求的显存。但输出分布不变,一个字都不变。
它只在「算力本来就闲着」的时候成立——大 batch 时算力不再闲置,这个交易直接不成立(见第 7 节最后一行)。
🎬 自己验一遍

回到第 5 节那张「找最优的 k」互动。先把「接受率 α」这个滑块一路拖到 0.1 附近 (它下面那张曲线图会跟着重画)。看右下角「最优加速比」那个读数——它会掉到 1 以下。

那个数字跌破 1 的瞬间,你看到的就是这件事的价钱: 你拿「闲置的算力」去换「更少的显存访问次数」,而这个兑换比例由接受率决定—— 接受率低的时候,你换回来的带宽还不够付出去的那些 draft 计算。 如果你刚才没有想指着某个东西说这句话,那这一节对你就是没用的——回到第 3 节那张四步流程图上再走一遍。

它在暗线里站在哪

这一章是纯工程章,回答集中在「参数账本」和「跑在什么上」两条暗线上,外加一条它默认成立的假设;其余几条暗线本章没有特别的话要说。

暗线这一章的回答
C 参数账本 以 7B 模型(bf16)为例:权重 = 7×109 × 2 字节 = 14 GB。 Decode 每生成 1 个 token,就要把这 14 GB 读一遍。
A100 的显存带宽约 2 TB/s → 理论上限 = 14 GB ÷ 2 TB/s ≈ 7 ms/token (约 143 tok/s,单请求)——这就是 Decode 的物理天花板。
算力侧:一次 7B 前向约 2 × 7×109 = 1.4×1010 FLOP, A100 的 bf16 算力约 312 TFLOPS,所以算力只需要 0.045 ms。 7 ms 和 0.045 ms 差了 156 倍——这就是「算力闲置」的来源, 也是投机解码能拿去交换的那块闲置产能。
draft 的头也一起算:用同系列的小模型(比如 1B),权重 2 GB, 成本比 c 大约就是 2 ÷ 14 ≈ 0.14——和互动里那个默认值 0.12 是同一个数量级。
D 跑在什么上 显存带宽受限,不是算力受限。最贵的动作是 「把全部权重从显存搬进计算单元」——它每生成一个 token 就要发生一次, 多少算力都省不掉它。
算术强度(每读一字节能做多少次乘加)在 Decode 阶段低到个位数, 远在硬件的「脊点」(算力与带宽的平衡点)左边,所以要靠带宽而不是算力去理解它。 完整的算术强度推导见 《硬件与算力账本》。
E 它假设了什么 假设 draft 和 target 在「容易的位置」上足够一致。 否则接受率塌掉,方案不但不快还更慢——这个假设可以用一个数字(α)检验, 但 α 只有跑了才知道,论文里不会写你这个任务上的 α。
更深的假设在语言本身:它假设自然语言里大部分位置的熵很低—— 「的」「是」「北京」这些位置本来就没有第二种答案(第 2 节那个观测)。 如果文字真的是逐字高熵的,这个方案从根上就不成立。

一张图看清代价:接受率 α 决定加速比的天花板

🎯 前后钩子

它接住了上一章的什么:《高效注意力》说 「瓶颈不在算,而在搬」。这一章把那个观测变成了一个可执行的交易—— 空转的算力原来是可以花掉的。

它给同阶段后面几章留了什么:投机解码解决的是单条请求的延迟。 如果瓶颈换成了「同时服务多少人」,它就不对症了—— 《服务化与吞吐》 接的是那一半; 而如果一张卡根本放不下整个模型,那要回到本阶段前面的 《分布式训练与集群》。

一句话带走投机解码

让小模型一次猜 k 个、大模型一次前向并行验证,用 min(1, p/q) 接受、残差重采, 输出分布严格不变——投机解码不是近似,只是把「搬权重的等待」换成了并行计算。
但它是延迟优化不是吞吐优化:收益全看接受率 α(低了反而更慢),大 batch 下算力不再闲置,好处直接消失—— 问「提速几倍」之前先问「在什么 batch 下测的」。

9

拓展阅读

上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。

📄 这一章的说法从哪来

💻 工业界怎么写

∑ 更严格的形式