第 9 讲 · 生成与推测解码 · Decoding: Sampling, Beam Search and Speculative Decoding

Large Language Model Systems · Lecture 09。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 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

学完这一讲,你应该能够:

  1. 说清楚解码的目标(argmax P(y | x) 或从 P(y | x) 采样)、为什么不能穷举,以及贪心、束搜索、采样各自的取舍。
  2. 手算一次束搜索,说出束宽、长度惩罚、剪枝对结果和代价的影响,理解”概率最大的句子”为什么往往不像一句典型的话。
  3. 写出三种离散采样算法和 Gumbel-max 技巧,解释温度、top-k、top-p 怎样改变输出分布。
  4. 用 roofline 解释自回归解码为什么慢(访存受限),以及”一次前向验证多个 token 几乎不多花时间”。
  5. 写出推测解码的精确接受–拒绝规则,证明它不改变输出分布;用接受率 α 估算每次目标前向的 token 数与加速比,说出 N 怎么选。
  6. 说出 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.1 算法与例子

slides 第 12–14 页:从空序列开始;每一步,束里的每个部分序列用模型往前算一步,取自己的前 k 个 token;所有候选放在一起,只留下得分(对数概率之和)最高的 k 个;已经结束(遇到 EOS)的序列原样保留。01 号脚本第 1 部分逐步复现了 slides 第 13 页的例子(k = 2):

步扩展出的候选保留
1I 0.400、We 0.300I、We
2I like 0.160、I love 0.160、We like 0.120、We do 0.090I like、I love
3I like singing 0.096、I love singing 0.080、I love dancing 0.048、I like song 0.032I 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.00023319612
束搜索 k = 2、3同上0.00023319623 / 34
束搜索 k = 4new york is a city of museums .0.015329
束搜索 k = 8pittsburgh is a city of bridges .0.030153
穷举pittsburgh is a city of bridges .0.03019127

贪心第一步选了最可能的首词 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 nat8015%第 1 名
绝对阈值 1.0 nat3860%第 2 名
相对阈值 1.5158−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.00001.000006.60
模型原样(T = 1)0.99101.00000.02386.54
T = 0.51.00001.00000.63957.08
T = 1.50.89241.00000.20925.58
贪心1.00000.0002∞0
top-k = 100.99490.9595∞6.56
top-p = 0.91.00000.8309∞6.32
top-p = 0.990.99871.00000.01616.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 7B1 × H1004.020.0136249
LLaMA-2 13B1 × A10012.750.083378
LLaMA-2 70B4 × H10010.300.034997

读权重比计算慢两个数量级以上:批大小 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,

  1. 以概率 min(1, p(x) / q(x)) 接受 x;
  2. 拒绝时,从剩余分布 norm(max(0, p − q)) 里重新抽一个 token,本步结束;
  3. 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.016538.61.00
推测解码,γ = 20.016532.92.22
推测解码,γ = 40.017948.72.64

和 top-k 规则对比(04 号脚本第 3 部分,同样的目标与草稿、γ = 4):

接受规则接受率首词是 the 的比例(真实 0.350)不合法的句子
精确规则0.7950.3460
top-k,k = 10.5500.5780
top-k,k = 100.6640.36113.4%

k = 1 时只接受目标的首选,输出偏向高概率的 token;k = 10 时混进了目标认为不可能的 token(目标分布在很多位置只有不到 10 个非零选项)。而且在这门有很多等概率位置的语言上,top-k 规则的接受率反而更低——它会拒绝排名靠后但概率不小的草稿 token。今天的推理框架实现的是精确规则。

5.3 接受率、期望 token 数与加速比

若每个草稿 token 以概率 α 被接受(近似独立),每次目标前向期望得到

E[tokens]=1−αγ+11−αE[\text{tokens}] = \frac{1-\alpha^{\gamma+1}}{1-\alpha}

个 token(接受的草稿 token,加上拒绝处重抽或全部接受时白送的那 1 个)。设草稿每步的耗时是目标的 c 倍,加速比约为 E / (γc + 1)。04 号脚本第 4 部分以第 1 讲的小 GPT(d = 64)为目标:

草稿cαγ = 4:期望 token / 加速比
unigram00.0731.08 / 1.08×
bigram00.7873.28 / 3.28×
trigram00.9074.15 / 4.15×
小 GPT d = 80.0240.9294.34 / 3.95×
小 GPT d = 160.0770.9704.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,4160.9490.9260.8960.8572.76
消融:只用 f_t8,3200.7730.6770.5910.4941.61
小 GPT d = 82,6560.9250.9220.9100.8972.72
小 GPT d = 168,3840.9720.9760.9720.9693.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
链 γ = 32.81.74
链 γ = 64.42.13
树:每层 2 个、深 3 层13.02.37
树:每层 3 个、深 3 层35.92.44

链只押一条路,第一选择错了后面全部作废;树只要正确答案在草稿的前几名里就能接上。代价是一次验证的 token 多——在访存受限的区间里几乎不花时间。EAGLE-2 按草稿的置信度动态修剪这棵树,EAGLE-3 把方法扩展到更多训练数据(slides 第 55 页)。


7. 小结 · Summary

  1. 解码的目标是 argmax(翻译、摘要)或采样(开放式生成);穷举 O(V^T) 不可行。
  2. 束搜索是近似的动态规划:束宽越大越接近最优,代价线性增长;贪心在城市语言上只找到第 196 名的句子,束宽 8 才找到最优解。概率最大的句子往往是短句、不典型;长度惩罚只给结束的候选排序时救不回被搜索挤掉的长句;剪枝要按得分尺度调。
  3. 采样:直接法 O(k)、二分 O(log k)、别名法 O(1);Gumbel-max 用”加噪声取 argmax”采样,噪声每步都要重新生成。温度、top-k、top-p 在合法性、覆盖与多样性之间取舍;轻微的 top-p 能修正模型的尾部错误。
  4. 解码慢是因为批大小 1 时每个 token 都要读一遍全部权重(访存受限),而一次前向多处理几个 token 几乎不多花时间。
  5. 推测解码:草稿猜 γ 个、目标一次验证;精确的接受–拒绝规则保证输出分布不变(slides 的 top-k 规则会偏);每次前向期望 (1 − α^{γ+1}) / (1 − α) 个 token,加速比还要扣掉草稿的开销 γc;常用 γ = 4–8。
  6. EAGLE:用目标模型的特征 + 下一个 token 做草稿,借用目标的输出层;树形注意力一次验证一棵候选树。

8. 常见误区 · Common pitfalls

  • “束搜索的束越宽,生成的文本越好。” 束越宽越接近模型的最优解,而最优解常常是短的、平淡的;开放式生成通常用采样。
  • “加了长度惩罚,束搜索就能找到长句。” 只在最后排序时用的长度惩罚救不回被搜索途中挤掉的长句。
  • “降温总会让输出更单一。” 分布里有等概率的部分时,降温可能把概率推向组合更多的分支,句子的熵反而上升。
  • “top-k / top-p 总是在牺牲质量换多样性。” 很轻的截断(top-p = 0.99)反而能去掉模型尾部的错误、让分布更接近真实。
  • “Gumbel 噪声可以算一次反复用。” 每一步必须用新的噪声,否则各步完全相关。
  • “解码慢是因为模型算得慢。” 批大小 1 时 GPU 的算力几乎闲着,瓶颈是读权重的带宽。
  • “推测解码会让输出变差。” 精确的接受–拒绝规则下输出分布与目标模型完全相同;会变的是 slides 那种 top-k 近似规则。
  • “草稿模型越大越好。” 接受率高了,但每步草稿的开销 γc 也大了,加速比可能反而下降。
  • “接受率 α 代入公式就是实际加速。” 序列长度有限、各位置 α 差别很大时,实测会比公式少。
  • “树形草稿要跑很多次前向。” 用树形掩码一次前向就能验证整棵树。

9. 练习 · Exercises

  1. 在 slides 第 13 页的玩具模型里,贪心解码得到什么?第 2 步 like 和 love 并列时,选哪一个会影响最终结果吗?
  2. 束宽 k、生成 T 步、词表 V:束搜索一共调用模型多少次?每一步要从多少个候选里选出 k 个?
  3. p = (0.5, 0.3, 0.2),q = (0.2, 0.5, 0.3)。按精确规则,接受率 α 是多少?拒绝后从什么分布重抽?验证输出第 0 个 token 的概率恰好是 0.5。
  4. α = 0.8、γ = 4,每次目标前向期望得到几个 token?若 c = 0.1,加速比约是多少?
  5. LLaMA-2 13B(bf16)在一块 A100(2.0 TB/s)上,批大小 1 时每个 token 至少要多少毫秒?批大小多大时开始变成计算受限(A100 的 BF16 峰值 312 TFLOP/s)?
  6. 解释 Gumbel-max 技巧为什么能采样,以及 slides 的 GumbelSampler 为什么不能直接用于逐 token 生成。
  7. 分布 (0.5, 0.3, 0.1, 0.05, 0.05),top-p = 0.9 保留哪几个 token?归一化后各是多少?
  8. 分布 (0.5, 0.3, 0.2),温度 T = 0.5 之后是多少?
  9. 宽度 2、深度 3 的满树有多少个节点?05 号脚本里平均只有 13.0 个,为什么?
  10. 在城市语言上,为什么束宽 16 的束搜索在 α = 3 时仍然找不到穷举给出的 T3 句子?
  11. top-k 规则取 k = 1,目标 p = (0.6, 0.4)、草稿 q = (0.5, 0.5)、只猜 1 个 token(拒绝后由目标采样)。输出 token 0 的概率是多少?和 p 比较。
  12. 词表 128256 时,用二分查找从 CDF 里采样一个 token 大约要几次比较?

参考答案

  1. I → like / love(0.4 并列)→ 如果选 like 则下一步 singing(0.6),得到 I like singing(0.096);如果选 love,下一步也是 singing(0.5),得到 I love singing(0.080)。所以并列的选择会影响结果,而贪心只是碰巧(按词表顺序)选到了更好的那条。
  2. 每步对束里每个未结束的序列调用一次,最多 k·T 次;每步候选最多 k·k(每个序列取前 k 个)——若不先各取前 k 个,则是 k·V 个(这就是第 10 讲 LightSeq 要优化的排序)。
  3. α = Σ 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 ✓。
  4. E = (1 − 0.8⁵) / 0.2 = (1 − 0.32768) / 0.2 ≈ 3.36;加速比 ≈ 3.36 / (4 × 0.1 + 1) ≈ 2.40。
  5. 26 GB ÷ 2.039 TB/s ≈ 12.75 ms(约 78 token / 秒)。平衡点 312 / 2.039 ≈ 153,批大小超过约 153 后变成计算受限。
  6. Gumbel 分布在”取最大值”下封闭,h_i + g_i 的 argmax 落在 i 的概率恰好是 e^{h_i} / Σ e^{h_j}。GumbelSampler 在构造时只生成一次噪声,之后每次调用都加同一组噪声:同一组 logits 每次都得到同一个结果,逐 token 生成时各步的随机性完全相关——必须每步重新生成噪声。
  7. 累加 0.5 → 0.8 → 0.9,到 0.9 时达到阈值,保留 前 3 个;归一化 (0.5, 0.3, 0.1) / 0.9 = (0.556, 0.333, 0.111)。
  8. p² = (0.25, 0.09, 0.04),和 0.38,归一化 (0.658, 0.237, 0.105)。
  9. 2 + 4 + 8 = 14 个。05 号脚本里有的分支在句号处就停了(句号之后不再展开),所以平均只有 13.0 个。
  10. 长度惩罚只在最后给结束了的候选排序;搜索途中比较的是原始对数概率,T3 句子的前缀概率低,几步之内就被短句的前缀挤出了束,到最后根本不在候选里。
  11. 草稿抽到 0(概率 0.5):0 是目标的首选,接受,输出 0。草稿抽到 1(概率 0.5):不是首选,拒绝,由目标采样,输出 0 的概率 0.6。合计 P(输出 0) = 0.5 + 0.5 × 0.6 = 0.8,而目标是 0.6——输出偏向了目标的首选。
  12. 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 的结构),一笔一笔地数省下的显存读写。

← 返回课程目录