对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 9 · Wed 2/11 · Generation and Speculative Decoding。 阅读材料:Leviathan, Kalman & Matias (2023) Fast Inference from Transformers via Speculative Decoding(ICML);Chen et al. (2023) Accelerating Large Language Model Decoding with Speculative Sampling;Li et al. (2024) EAGLE: Speculative Sampling Requires Rethinking Feature Uncertainty(ICML);Freitag & Al-Onaizan (2017) Beam Search Strategies for Neural Machine Translation;Kool et al. (2019) Stochastic Beams and Where to Find Them。 依据说明:本讲以原课第 9 讲公开的 slides(llmsys-09-decoding.pdf,58 页)为主线:回顾、序列解码概览(argmax、穷举、贪心 / max 解码、采样)、离散采样的三种算法与 torch.multinomial、Gumbel-max 技巧与 GumbelSampler 代码、束搜索(算法、例子、伪代码、剪枝、采样与束搜索结合、代码示例)、自回归解码为什么慢、推测解码(草稿模型、并行验证、slides 的 top-k 接受规则、拒绝后由目标模型生成、为什么更快、N 的选择、对齐)、EAGLE(特征层面的草稿、token + 特征、树形注意力、训练损失、EAGLE-2/3)、总结。城市语言上的穷举对照、长度惩罚与剪枝的实验、三种采样算法的对比、GumbelSampler 复用噪声的问题、温度 / top-k / top-p 的精确评估、纸面 GPU 的解码代价与本机实测、接受–拒绝规则的逐点证明与端到端检验、top-k 规则的偏差、接受率与加速比、链 vs 树、极简 EAGLE 头,都是本讲义自己设计的,不代表课堂上讲了什么。slides 没有讲 top-k / top-p 采样,本讲补上(原课第 1 讲提到了温度)。 配套代码 Code:
code/lecture09/(5 个脚本 +decoding.py;都在 15 秒以内,复用第 1 讲缓存的小 GPT) 预计阅读时间:3–4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说清楚解码的目标(argmax P(y | x) 或从 P(y | x) 采样)、为什么不能穷举,以及贪心、束搜索、采样各自的取舍。
- 手算一次束搜索,说出束宽、长度惩罚、剪枝对结果和代价的影响,理解”概率最大的句子”为什么往往不像一句典型的话。
- 写出三种离散采样算法和 Gumbel-max 技巧,解释温度、top-k、top-p 怎样改变输出分布。
- 用 roofline 解释自回归解码为什么慢(访存受限),以及”一次前向验证多个 token 几乎不多花时间”。
- 写出推测解码的精确接受–拒绝规则,证明它不改变输出分布;用接受率 α 估算每次目标前向的 token 数与加速比,说出 N 怎么选。
- 说出 EAGLE 的两个想法:在特征层面做草稿(并把采样到的 token 喂回去)、用树形注意力一次验证一棵候选树。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| decoder-only 模型与 KV 缓存(第 1 讲 §3.4) | 束搜索的系统优化(层级搜索 HARS、显存复用,第 10 讲) |
| Transformer、因果掩码(第 6 讲) | 连续批处理、服务调度(第 22 讲);PagedAttention 与束搜索的写时复制(第 24 讲) |
| roofline、访存受限 vs 计算受限(第 1、4 讲) | Medusa、lookahead 等其他推测解码变体 |
0.3 本讲地图 Map
§1 解码的目标:argmax 还是采样;穷举为什么不行
§2 束搜索:算法、例子、和穷举对照、长度惩罚、剪枝、多样性 ← 代码 01
§3 采样:三种离散采样算法、Gumbel-max、温度 / top-k / top-p ← 代码 02
§4 自回归解码为什么慢:访存受限、批大小、一次前向多算几个 token ← 代码 03
§5 推测解码:精确规则与无损性、slides 的 top-k 规则、接受率与加速比、N 的选择 ← 代码 04
§6 EAGLE:特征层面的草稿、树形注意力 ← 代码 05
§7 小结 §8 常见误区 §9 练习 §10 延伸阅读 §11 下一讲预告
1. 解码的目标 · What decoding is for
模型给出的是 P(y_t | y_<t, x)。解码要从它得到一段输出,有两种目标(slides 第 4 页):
- 找概率最大的序列:argmax_y P(y | x) = Π_t P(y_t | y_<t, x)。翻译、摘要这类”有标准答案”的任务通常要这个。
- 按分布采样:y ~ P(y | x)。对话、写作这类开放式生成要的是多样、自然的输出。
穷举所有序列要 O(V^T):词表 3 万、长度 20 就是 3 万的 20 次方,不可能。所以有三类近似:贪心(每步取最大)、束搜索(近似的动态规划,每步保留 k 个最好的部分序列)、采样。slides 第 6 页指出一个小细节:贪心取 argmax 时不需要归一化,比较 logits 就行(softmax 是单调的)。
城市语言(第 1 讲)的 9127 个句子和它们的概率全部已知,所以本讲的每个解码算法都可以和”真正的答案”对照——这在真实语言上做不到。decoding.py 把真实分布(Oracle)、n-gram、小 GPT 统一成同一个接口 next_probs(前缀),同一个算法可以在三者上运行。
2. 束搜索 · Beam search
2.1 算法与例子
slides 第 12–14 页:从空序列开始;每一步,束里的每个部分序列用模型往前算一步,取自己的前 k 个 token;所有候选放在一起,只留下得分(对数概率之和)最高的 k 个;已经结束(遇到 EOS)的序列原样保留。01 号脚本第 1 部分逐步复现了 slides 第 13 页的例子(k = 2):
| 步 | 扩展出的候选 | 保留 |
|---|---|---|
| 1 | I 0.400、We 0.300 | I、We |
| 2 | I like 0.160、I love 0.160、We like 0.120、We do 0.090 | I like、I love |
| 3 | I like singing 0.096、I love singing 0.080、I love dancing 0.048、I like song 0.032 | I like singing、I love singing |
束宽 k 时每步做 k 次前向,代价是贪心的 k 倍;换来的是保留”暂时不是第一、后面可能更好”的路径。
2.2 和穷举对照
01 号脚本第 2 部分在真实分布上找概率最大的句子:
| 方法 | 找到的句子 | 真实概率 | 在 9127 句中的排名 | 调用模型次数 |
|---|---|---|---|---|
| 贪心 | the artists from pittsburgh who cook after work love the bridges . | 0.000233 | 196 | 12 |
| 束搜索 k = 2、3 | 同上 | 0.000233 | 196 | 23 / 34 |
| 束搜索 k = 4 | new york is a city of museums . | 0.015 | 3 | 29 |
| 束搜索 k = 8 | pittsburgh is a city of bridges . | 0.030 | 1 | 53 |
| 穷举 | pittsburgh is a city of bridges . | 0.030 | 1 | 9127 |
贪心第一步选了最可能的首词 the(T3 句型占 35%),之后每一步都是 1/6、1/5 这样的均匀选择,最后的句子概率只有最优解的 1/130。局部最优 ≠ 全局最优。束宽到 8 才找到真正的最优解。换成训练出来的小 GPT(第 5 部分),贪心和束搜索犯的是同样的错、找到的是同样的句子——模型几乎学会了真实分布,搜索找的是模型的最优解。
2.3 概率最大的句子不像一句典型的话;长度惩罚
不加长度惩罚时,概率最大的句子都是短句:每多一个词就多乘一个小于 1 的概率。可是这门语言里最常见的句型恰恰是长的 T3(占 35%),只是它的 9000 种组合各自概率都很小。“最可能的那一句”并不代表”一句典型的话”;从真实分布采样 2000 句,句型比例与真实比例一致(T3 0.34)。
T5 用长度惩罚 0.6(slides 第 4 页),即按 log P / ((5 + |Y|)/6)^α 排序(GNMT 的写法)。01 号脚本第 3 部分:在全部句子上按这个得分取最大,α 要到 3 才翻到 T3;而束宽 16 的束搜索在任何 α 下都给出短句——长度惩罚只用来给结束了的候选排序,搜索途中比较的仍是原始概率,长句早早就被挤出了束。这是一种搜索误差:目标函数改了,搜索没跟着改。实现上要么把长度归一化也用在搜索过程中,要么只用温和的值。
2.4 剪枝,以及采样与束搜索结合
slides 第 15 页的剪枝(Freitag & Al-Onaizan 2017):相对阈值(比最好的得分差一定倍数就丢)、绝对阈值(差一定的 nat 就丢)、局部相对阈值。01 号脚本第 4 部分(束宽 16):
| 剪枝 | 调用模型 | 省下 | 结果 |
|---|---|---|---|
| 不剪枝 | 94 | — | 第 1 名 |
| 绝对阈值 1.5 nat | 80 | 15% | 第 1 名 |
| 绝对阈值 1.0 nat | 38 | 60% | 第 2 名 |
| 相对阈值 1.5 | 158 | −68% | 第 377 名 |
相对阈值在这里很危险:第一步最好的是 the(log P = −1.05),1.5 倍是 −1.57,而通向最优解的 pittsburgh 是 −2.41——第一步就被剪掉了,剩下的全是长句,反而多算。剪枝的阈值要和得分的尺度、句子长度一起调。
束搜索是确定性的。slides 第 16 页建议”先采样开头的几个 token,再做束搜索”来增加多样性:01 号脚本里重复 50 次得到 10 种不同的句子——多样性来自采样,局部质量来自搜索。
3. 采样 · Sampling
3.1 三种离散采样算法
从 k 个类别(概率 p₁…p_k)里抽 n 个样本(slides 第 8 页):
| 算法 | 预处理 | 每个样本 | 合计 |
|---|---|---|---|
| 直接法:逐个累加概率直到超过均匀随机数 u | — | O(k) | O(nk) |
| 二分查找:先建累积分布(CDF),再找第一个 > u 的位置 | O(k) | O(log k) | O(k + n log k) |
| 别名法(Walker / Vose):把 k 个概率削峰填谷成 k 个等高的桶,每桶最多两个类别 | O(k)(slides 写 O(k log k),取决于实现) | O(1) | O(k + n) |
02 号脚本第 1 部分(k = 1000 的长尾分布,n = 2 万):三者的经验分布与 p 的 TV 距离都在有限样本的正常波动范围内(与 numpy 的 0.040 相当);每个样本的比较次数 481.3 / 10.0 / 1.0。语言模型每步只抽一个样本、分布每步都变,预处理摊不薄,所以一次 O(k) 的扫描(GPU 上是并行前缀和 + 查找,即 torch.multinomial)就够了;别名法适合”同一个分布抽很多次”。
3.2 Gumbel-max 技巧
slides 第 9 页:从 Categorical(softmax(h)) 采样,等价于取 argmax(h + g),g_i = −log(−log U_i) 独立服从 Gumbel 分布。02 号脚本第 2 部分重复 20 万次,频率与 softmax(h) 一致(例如 0.5630 vs 0.5629)。好处是不用算 softmax 的归一化、不用前缀和:一次逐元素加法 + 一次 argmax(归约),很适合 GPU;取前 k 个最大值就是不放回地抽 k 个(Gumbel-top-k,Kool et al. 2019,可用于”随机束搜索”)。
slides 第 10 页代码里的一个坑:GumbelSampler 在构造函数里只生成一次噪声 self.noise,之后每次 sample 都复用它。02 号脚本用同一组 logits 连抽 20 次,20 次都是同一个类别;生成一句话时,各步的”随机性”也完全相关。预计算噪声可以省下生成随机数的时间,但每一步必须用新的噪声(例如预先生成一大块、每步取不同的一行)。
3.3 温度、top-k、top-p
- 温度 T:p^(1/T) 再归一化(等价于 logits / T)。T < 1 更尖,T → 0 就是贪心;T > 1 更平。
- top-k:只保留概率最大的 k 个 token,再归一化。
- top-p(nucleus):按概率从大到小累加,保留累计概率刚超过 p 的最小集合(Holtzman et al. 2020)。
在城市语言上,可以把每个策略生成每一个合法句子的概率逐 token 精确算出来。02 号脚本第 3 部分(作用在 d = 64 的小 GPT 上):
| 策略 | 生成合法句子的概率 | 覆盖的真实概率 | KL(真实 ‖ 策略) | 熵(nat) |
|---|---|---|---|---|
| 真实分布 | 1.0000 | 1.0000 | 0 | 6.60 |
| 模型原样(T = 1) | 0.9910 | 1.0000 | 0.0238 | 6.54 |
| T = 0.5 | 1.0000 | 1.0000 | 0.6395 | 7.08 |
| T = 1.5 | 0.8924 | 1.0000 | 0.2092 | 5.58 |
| 贪心 | 1.0000 | 0.0002 | ∞ | 0 |
| top-k = 10 | 0.9949 | 0.9595 | ∞ | 6.56 |
| top-p = 0.9 | 1.0000 | 0.8309 | ∞ | 6.32 |
| top-p = 0.99 | 0.9987 | 1.0000 | 0.0161 | 6.59 |
几点值得记住:
- 模型原样采样有 1% 的概率生成不合法的句子——来自模型分给”不该出现的 token”的一点尾部概率。top-p = 0.99 只砍掉这点尾部,合法率升到 0.9987,KL 反而降到 0.0161:轻微的截断能修正模型尾部的小错误。
- 截断再狠一些(top-p = 0.9、top-k = 10)就开始砍掉真实存在的句子(覆盖不到 1,KL = ∞)。top-k 的 k 是固定的,在很平(T3 的填充词有 5–6 个等概率选项)和很尖(句号)的位置都不合适;top-p 自适应。
- 温度的效果不总是直觉的样子:T = 0.5 时句子分布的熵上升(6.54 → 7.08)。降温把首词推向最常见的 the(T3 句型),而 T3 的填充词等概率、降温改变不了它们,于是更多概率落到 9000 种 T3 组合上。
聊天模型常用 T ≈ 0.7 加 top-p ≈ 0.9:宁可少一些多样性,也不要偶尔冒出一个离谱的 token。
4. 自回归解码为什么慢 · Why autoregressive decoding is slow
slides 第 24 页:一次只能生成一个 token,每个 token 可能要几百毫秒。慢在哪里?每生成一个 token,都要把全部权重从显存读一遍,而每个参数只做一次乘加——算术强度约 1 FLOP / 字节,远低于 GPU 的平衡点(第 1、4 讲)。03 号脚本第 1 部分的纸面估算(bf16,批大小 1,忽略 KV 缓存和通信):
| 模型 | 硬件 | 读权重 ms / token | 计算 ms / token | 最多 token / 秒 |
|---|---|---|---|---|
| LLaMA-2 7B | 1 × H100 | 4.02 | 0.0136 | 249 |
| LLaMA-2 13B | 1 × A100 | 12.75 | 0.0833 | 78 |
| LLaMA-2 70B | 4 × H100 | 10.30 | 0.0349 | 97 |
读权重比计算慢两个数量级以上:批大小 1 的解码完全是访存受限的,GPU 的算力几乎都在闲着。这给了两条出路(第 2 部分):一次处理 b 条请求,算术强度约为 b,在平衡点(A100 约 153、H100 约 295)以内每步时间几乎不变、吞吐线性增长——这是服务端攒批(第 22 讲);单个请求则可以一次验证多个猜测的 token——推测解码。
03 号脚本第 3 部分在本机实测:一个 1 亿参数(404 MB)的模型,KV 缓存里已有 64 个 token,一次前向再处理 T 个新 token。⏱ T 从 2 到 32 耗时都在 11–15 ms,几乎不变;T = 1 只要 4–5 ms(本机 BLAS 对”矩阵 × 向量”有专门的快速路径,第 1 讲也遇到过),即便如此验证 8 个 token 也只是生成 1 个的约 3 倍。在 GPU 上只要 T 远小于平衡点,一次前向的时间就由”读一遍权重”决定——这就是 slides 第 39–41 页说的”验证比生成快”:因果注意力让一次前向同时给出 N 个位置的似然。
5. 推测解码 · Speculative decoding
5.1 想法
slides 第 26–38 页:用一个小的草稿模型自回归地猜 N 个 token(便宜),再让大的目标模型一次前向验证这 N 个位置(因为访存受限,几乎和生成 1 个一样贵);被接受的 token 直接用,第一个不被接受的位置由目标模型自己生成。草稿猜得越准,每次目标前向得到的 token 越多。
5.2 怎样”接受”:slides 的 top-k 规则 vs 精确规则
slides 第 35–37 页的规则是:草稿 token 如果在目标分布的前 k 名里就接受;第一次不在时,由目标模型从那里开始生成。这是一个实用的近似,但会改变输出分布。
Leviathan et al. (2023) 和 Chen et al. (2023) 给出了精确的规则(slides 的引用论文):草稿从 q 里抽出 x,
- 以概率 min(1, p(x) / q(x)) 接受 x;
- 拒绝时,从剩余分布 norm(max(0, p − q)) 里重新抽一个 token,本步结束;
- N 个全部接受时,再从目标在第 N + 1 个位置的分布里白送一个 token。
为什么输出分布恰好是 p:输出 x 有两条路——草稿抽到 x 并被接受,概率 q(x)·min(1, p(x)/q(x)) = min(p(x), q(x));或者被拒绝(总概率 1 − Σ min(p, q))后从剩余分布抽到 x,概率 (1 − Σ_y min(p(y), q(y))) · max(0, p(x) − q(x)) / Σ_y max(0, p(y) − q(y))。因为 Σ max(0, p − q) = 1 − Σ min(p, q),第二项就是 max(0, p(x) − q(x)),两项相加恰好是 p(x)。04 号脚本第 1 部分在 1000 对随机分布上逐点验证:最大差 2.8e-16(浮点舍入)。接受率 α = Σ min(p, q) = 1 − TV(p, q):草稿和目标越像越容易被接受;即使草稿很差,分布也不变,只是加速没了——推测解码是无损的。
端到端检验(04 号脚本第 2 部分):目标用城市语言的真实分布、草稿用 bigram,各生成 2 万句,按(句型 × 城市)40 格与精确比例比较:
| 方法 | TV 距离 | 卡方统计量(自由度 39) | 每次目标”前向”生成的 token |
|---|---|---|---|
| 直接从真实分布采样 | 0.0165 | 38.6 | 1.00 |
| 推测解码,γ = 2 | 0.0165 | 32.9 | 2.22 |
| 推测解码,γ = 4 | 0.0179 | 48.7 | 2.64 |
和 top-k 规则对比(04 号脚本第 3 部分,同样的目标与草稿、γ = 4):
| 接受规则 | 接受率 | 首词是 the 的比例(真实 0.350) | 不合法的句子 |
|---|---|---|---|
| 精确规则 | 0.795 | 0.346 | 0 |
| top-k,k = 1 | 0.550 | 0.578 | 0 |
| top-k,k = 10 | 0.664 | 0.361 | 13.4% |
k = 1 时只接受目标的首选,输出偏向高概率的 token;k = 10 时混进了目标认为不可能的 token(目标分布在很多位置只有不到 10 个非零选项)。而且在这门有很多等概率位置的语言上,top-k 规则的接受率反而更低——它会拒绝排名靠后但概率不小的草稿 token。今天的推理框架实现的是精确规则。
5.3 接受率、期望 token 数与加速比
若每个草稿 token 以概率 α 被接受(近似独立),每次目标前向期望得到
个 token(接受的草稿 token,加上拒绝处重抽或全部接受时白送的那 1 个)。设草稿每步的耗时是目标的 c 倍,加速比约为 E / (γc + 1)。04 号脚本第 4 部分以第 1 讲的小 GPT(d = 64)为目标:
| 草稿 | c | α | γ = 4:期望 token / 加速比 |
|---|---|---|---|
| unigram | 0 | 0.073 | 1.08 / 1.08× |
| bigram | 0 | 0.787 | 3.28 / 3.28× |
| trigram | 0 | 0.907 | 4.15 / 4.15× |
| 小 GPT d = 8 | 0.024 | 0.929 | 4.34 / 3.95× |
| 小 GPT d = 16 | 0.077 | 0.970 | 4.71 / 3.60× |
实测(d = 16 草稿、γ = 4)每次目标前向生成 3.46 个 token,比公式的 4.71 少:句子平均只有 8.8 个 token,最后一次前向用不满;各位置的 α 差别很大(55% 的位置 α > 0.99,最难的 10% 只有 0.87),按平均 α 代入公式会高估。slides 第 46 页的”对齐”讲的就是 α:同一家族的模型做草稿–目标对,分布越像 α 越高;但草稿太大,γ·c 的开销会吃掉收益。
5.4 N 怎么选
slides 第 45 页:N 大,理论上加速多;但被拒绝的概率累积、拒绝的代价变大、要在更多位置算词表大小的 softmax、实时应用每一”拍”的延迟变长。常用 N = 4、8。04 号脚本第 5 部分按加速比公式求最优 γ:α = 0.7–0.8、c = 0.1 时是 4–6;草稿越便宜、越准,最优 γ 越大(α = 0.9、c = 0.02 时到 16)。
6. EAGLE · Feature-level drafting and tree attention
6.1 特征层面的草稿
slides 第 48–50 页的观察:预测目标模型最后一层的特征比预测下一个 token 容易。EAGLE(Li et al. 2024)用一个很小的头(一层 Transformer),输入当前特征 f_t 和下一个 token 的嵌入 e(x_{t+1}),预测下一个特征 f̂_{t+1},再借用目标模型自己的输出层得到 token 分布;猜出的 token 和特征再喂回去继续草稿。训练损失(slides 第 52–53 页)是特征的 SmoothL1 加上 token 分布的交叉熵:L = L_reg + w·L_cls。
为什么要喂 token?特征只决定下一个 token 的分布;真正采样到哪个 token,会让后面的特征走向完全不同的方向(slides:I am 之后接 begin 还是 excited,后面完全不同)。
05 号脚本第 3 部分训练了一个极简的 EAGLE 头(两层 MLP、只看最近一个特征、12,416 个参数,嵌入和输出层借用目标模型),沿真实测试句计算精确规则的接受率:
| 草稿 | 参数 | α₁ | α₂ | α₃ | α₄ | 期望接受(≈ Σ Π α) |
|---|---|---|---|---|---|---|
| EAGLE 头 [f_t, e(x_{t+1})] → f_{t+1} | 12,416 | 0.949 | 0.926 | 0.896 | 0.857 | 2.76 |
| 消融:只用 f_t | 8,320 | 0.773 | 0.677 | 0.591 | 0.494 | 1.61 |
| 小 GPT d = 8 | 2,656 | 0.925 | 0.922 | 0.910 | 0.897 | 2.72 |
| 小 GPT d = 16 | 8,384 | 0.972 | 0.976 | 0.972 | 0.969 | 3.05 |
- 只看最近一个特征的 MLP,第一步接受率就有 0.949,高于 d = 8 的小 GPT:特征已经把上下文总结好了。
- 去掉”下一个 token 的嵌入”,α₁ 从 0.949 掉到 0.773——喂 token 的作用很大。
- 接受率随步数下降(误差会累积,草稿自己预测的特征被不断喂回去);真正的 EAGLE 用带注意力的层、在大数据上训练来减轻漂移。
- 在这门简单的小语言上,它没有赢过 d = 16 的小 GPT(期望 2.76 vs 3.05):8 千参数的小模型就几乎学会了真实分布。真实的 LLM 很难找到又小又对齐的同族草稿,EAGLE 借用目标模型的特征和输出层,换来了更高的接受率;配合树形草稿,EAGLE 论文在 MT-Bench 上报告约 3 倍的加速。
6.2 树形注意力
草稿不必只给一条链:每层留几个候选,组成一棵树,用树形注意力掩码把整棵树拍平成一个序列、一次前向验证(slides 第 51 页)。每个节点只能看见前缀、它的祖先和它自己;位置编号按深度给。05 号脚本第 1 部分打印了 slides 那棵树(How → can → I → {make, help} → …)的掩码,并在小 GPT 上验证:4 条候选路径合并成 10 个节点的树,一次前向与 4 条路径各跑一次的 logits 最大差 1.9e-06。
第 2 部分比较链和树(目标 d = 64、草稿 d = 8、贪心验证):
| 草稿形状 | 一次验证的 token | 平均接受的草稿 token |
|---|---|---|
| 链 γ = 3 | 2.8 | 1.74 |
| 链 γ = 6 | 4.4 | 2.13 |
| 树:每层 2 个、深 3 层 | 13.0 | 2.37 |
| 树:每层 3 个、深 3 层 | 35.9 | 2.44 |
链只押一条路,第一选择错了后面全部作废;树只要正确答案在草稿的前几名里就能接上。代价是一次验证的 token 多——在访存受限的区间里几乎不花时间。EAGLE-2 按草稿的置信度动态修剪这棵树,EAGLE-3 把方法扩展到更多训练数据(slides 第 55 页)。
7. 小结 · Summary
- 解码的目标是 argmax(翻译、摘要)或采样(开放式生成);穷举 O(V^T) 不可行。
- 束搜索是近似的动态规划:束宽越大越接近最优,代价线性增长;贪心在城市语言上只找到第 196 名的句子,束宽 8 才找到最优解。概率最大的句子往往是短句、不典型;长度惩罚只给结束的候选排序时救不回被搜索挤掉的长句;剪枝要按得分尺度调。
- 采样:直接法 O(k)、二分 O(log k)、别名法 O(1);Gumbel-max 用”加噪声取 argmax”采样,噪声每步都要重新生成。温度、top-k、top-p 在合法性、覆盖与多样性之间取舍;轻微的 top-p 能修正模型的尾部错误。
- 解码慢是因为批大小 1 时每个 token 都要读一遍全部权重(访存受限),而一次前向多处理几个 token 几乎不多花时间。
- 推测解码:草稿猜 γ 个、目标一次验证;精确的接受–拒绝规则保证输出分布不变(slides 的 top-k 规则会偏);每次前向期望 (1 − α^{γ+1}) / (1 − α) 个 token,加速比还要扣掉草稿的开销 γc;常用 γ = 4–8。
- EAGLE:用目标模型的特征 + 下一个 token 做草稿,借用目标的输出层;树形注意力一次验证一棵候选树。
8. 常见误区 · Common pitfalls
- “束搜索的束越宽,生成的文本越好。” 束越宽越接近模型的最优解,而最优解常常是短的、平淡的;开放式生成通常用采样。
- “加了长度惩罚,束搜索就能找到长句。” 只在最后排序时用的长度惩罚救不回被搜索途中挤掉的长句。
- “降温总会让输出更单一。” 分布里有等概率的部分时,降温可能把概率推向组合更多的分支,句子的熵反而上升。
- “top-k / top-p 总是在牺牲质量换多样性。” 很轻的截断(top-p = 0.99)反而能去掉模型尾部的错误、让分布更接近真实。
- “Gumbel 噪声可以算一次反复用。” 每一步必须用新的噪声,否则各步完全相关。
- “解码慢是因为模型算得慢。” 批大小 1 时 GPU 的算力几乎闲着,瓶颈是读权重的带宽。
- “推测解码会让输出变差。” 精确的接受–拒绝规则下输出分布与目标模型完全相同;会变的是 slides 那种 top-k 近似规则。
- “草稿模型越大越好。” 接受率高了,但每步草稿的开销 γc 也大了,加速比可能反而下降。
- “接受率 α 代入公式就是实际加速。” 序列长度有限、各位置 α 差别很大时,实测会比公式少。
- “树形草稿要跑很多次前向。” 用树形掩码一次前向就能验证整棵树。
9. 练习 · Exercises
- 在 slides 第 13 页的玩具模型里,贪心解码得到什么?第 2 步 like 和 love 并列时,选哪一个会影响最终结果吗?
- 束宽 k、生成 T 步、词表 V:束搜索一共调用模型多少次?每一步要从多少个候选里选出 k 个?
- p = (0.5, 0.3, 0.2),q = (0.2, 0.5, 0.3)。按精确规则,接受率 α 是多少?拒绝后从什么分布重抽?验证输出第 0 个 token 的概率恰好是 0.5。
- α = 0.8、γ = 4,每次目标前向期望得到几个 token?若 c = 0.1,加速比约是多少?
- LLaMA-2 13B(bf16)在一块 A100(2.0 TB/s)上,批大小 1 时每个 token 至少要多少毫秒?批大小多大时开始变成计算受限(A100 的 BF16 峰值 312 TFLOP/s)?
- 解释 Gumbel-max 技巧为什么能采样,以及 slides 的 GumbelSampler 为什么不能直接用于逐 token 生成。
- 分布 (0.5, 0.3, 0.1, 0.05, 0.05),top-p = 0.9 保留哪几个 token?归一化后各是多少?
- 分布 (0.5, 0.3, 0.2),温度 T = 0.5 之后是多少?
- 宽度 2、深度 3 的满树有多少个节点?05 号脚本里平均只有 13.0 个,为什么?
- 在城市语言上,为什么束宽 16 的束搜索在 α = 3 时仍然找不到穷举给出的 T3 句子?
- top-k 规则取 k = 1,目标 p = (0.6, 0.4)、草稿 q = (0.5, 0.5)、只猜 1 个 token(拒绝后由目标采样)。输出 token 0 的概率是多少?和 p 比较。
- 词表 128256 时,用二分查找从 CDF 里采样一个 token 大约要几次比较?
参考答案
- I → like / love(0.4 并列)→ 如果选 like 则下一步 singing(0.6),得到 I like singing(0.096);如果选 love,下一步也是 singing(0.5),得到 I love singing(0.080)。所以并列的选择会影响结果,而贪心只是碰巧(按词表顺序)选到了更好的那条。
- 每步对束里每个未结束的序列调用一次,最多 k·T 次;每步候选最多 k·k(每个序列取前 k 个)——若不先各取前 k 个,则是 k·V 个(这就是第 10 讲 LightSeq 要优化的排序)。
- α = Σ min(p, q) = 0.2 + 0.3 + 0.2 = 0.7。max(0, p − q) = (0.3, 0, 0),归一化为 (1, 0, 0)。P(输出 0) = min(0.5, 0.2) + (1 − 0.7) × 1 = 0.2 + 0.3 = 0.5 ✓。
- E = (1 − 0.8⁵) / 0.2 = (1 − 0.32768) / 0.2 ≈ 3.36;加速比 ≈ 3.36 / (4 × 0.1 + 1) ≈ 2.40。
- 26 GB ÷ 2.039 TB/s ≈ 12.75 ms(约 78 token / 秒)。平衡点 312 / 2.039 ≈ 153,批大小超过约 153 后变成计算受限。
- Gumbel 分布在”取最大值”下封闭,h_i + g_i 的 argmax 落在 i 的概率恰好是 e^{h_i} / Σ e^{h_j}。GumbelSampler 在构造时只生成一次噪声,之后每次调用都加同一组噪声:同一组 logits 每次都得到同一个结果,逐 token 生成时各步的随机性完全相关——必须每步重新生成噪声。
- 累加 0.5 → 0.8 → 0.9,到 0.9 时达到阈值,保留 前 3 个;归一化 (0.5, 0.3, 0.1) / 0.9 = (0.556, 0.333, 0.111)。
- p² = (0.25, 0.09, 0.04),和 0.38,归一化 (0.658, 0.237, 0.105)。
- 2 + 4 + 8 = 14 个。05 号脚本里有的分支在句号处就停了(句号之后不再展开),所以平均只有 13.0 个。
- 长度惩罚只在最后给结束了的候选排序;搜索途中比较的是原始对数概率,T3 句子的前缀概率低,几步之内就被短句的前缀挤出了束,到最后根本不在候选里。
- 草稿抽到 0(概率 0.5):0 是目标的首选,接受,输出 0。草稿抽到 1(概率 0.5):不是首选,拒绝,由目标采样,输出 0 的概率 0.6。合计 P(输出 0) = 0.5 + 0.5 × 0.6 = 0.8,而目标是 0.6——输出偏向了目标的首选。
- log₂ 128256 ≈ 17 次。
10. 延伸阅读 · Further reading
- Leviathan, Kalman & Matias (2023) Fast Inference from Transformers via Speculative Decoding(ICML);Chen et al. (2023) Accelerating Large Language Model Decoding with Speculative Sampling:精确规则与加速比分析。
- Li et al. (2024) EAGLE(ICML)、EAGLE-2(EMNLP 2024)、EAGLE-3(2025)。
- Miao et al. (2024) SpecInfer(ASPLOS):基于树的推测推理与多草稿的无损验证。
- Cai et al. (2024) Medusa:用多个解码头代替草稿模型。
- Holtzman et al. (2020) The Curious Case of Neural Text Degeneration(ICLR):nucleus(top-p)采样与”最可能的文本反而退化”。
- Freitag & Al-Onaizan (2017) Beam Search Strategies for Neural Machine Translation;Wu et al. (2016) GNMT(长度惩罚与覆盖惩罚)。
- Kool, van Hoof & Welling (2019) Stochastic Beams and Where to Find Them(ICML):Gumbel-top-k。
- Meister, Cotterell & Vieira (2020) If Beam Search is the Answer, What was the Question?(EMNLP):束搜索的偏差。
11. 下一讲预告 · Next lecture
第二单元到此结束:我们有了 Transformer、预训练 LLM、分词器和解码算法。第三单元回到 GPU:怎样让 Transformer 在 GPU 上跑得快?第 6 讲 03 号脚本数过——一个 Transformer 层的前向里,矩阵乘法只占少数,其余几十个都是逐元素运算和归约,每个都要读写一遍显存。第 10 讲(LightSeq)的答案是算子融合:把 bias、dropout、残差、LayerNorm、softmax、交叉熵这些”非矩阵乘法”的运算融合成少数几个内核;推理时还要处理束搜索在大词表上的排序(层级搜索 HARS)、显存复用。我们会在第 2 讲的 SIMT 模拟器里写出融合的 softmax 和 LayerNorm 内核(HW4 的结构),一笔一笔地数省下的显存读写。