第 3 讲 · 训练大语言模型与解码 · Learning LLMs & Decoding

Generative AI · Lecture 03。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

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

本篇目录

对应 Maps to:CMU 10-423 / 10-623 / 10-723 Generative AI(Spring 2026;Aran Nayebi、Matt Gormley),Lecture 3 · Wed 21-Jan · Learning LLMs / Decoding。 阅读材料:Graves (2014), Generating Sequences With Recurrent Neural Networks;Mikolov et al. (2010), Recurrent neural network based language model;Radford et al. (2018), Improving Language Understanding by Generative Pre-Training(GPT-1);Radford et al. (2019), Language Models are Unsupervised Multitask Learners(GPT-2)。 依据说明:本讲以原课第 3 讲公开的 slides(lecture3-llms.pdf)为主线:回顾(模块化自动微分、两种画法、RNN-LM、Transformer-LM、采样)、机器学习的配方、反向传播、SGD 与小批量 SGD、用 RNN 做序列标注、深度语言模型的最大似然、批处理(填充与截断)、KV 缓存、分词、贪心解码与祖先采样。slides 在祖先采样处结束;束搜索、温度、top-k、top-p 和 GPT-1/GPT-2 两节(§6.4–6.6、§7)是本讲义根据阅读材料和原始论文补充的。全部实验是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:code/lecture03/(6 个脚本 + 4 个模块;04 只用标准库,其余用 PyTorch CPU 版;每个脚本 1–20 秒) 预计阅读时间:3.5–4.5 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

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

  1. 写出深度语言模型的训练目标(训练集的对数似然),说清它和 n-gram”数数”的关系,以及为什么需要 START/END。
  2. 解释小批量 SGD:为什么用小批量、批大小怎样影响梯度噪声和每个样本的计算成本。
  3. 把不等长的句子组成批:截断、填充、损失掩码;知道右填充和左填充的区别、注意力掩码和位置编号的作用;比较填充、分桶和打包。
  4. 解释 KV 缓存:为什么 K、V 可以缓存而 Q 不需要;它节省了多少计算、占多少内存;为什么它让 Transformer 的”状态”随长度增长。
  5. 比较以词、字符、子词为单位的分词;会手工执行 BPE 的训练和编码,知道字节级 BPE 为什么没有 OOV。
  6. 区分搜索(贪心、束搜索)和采样(祖先采样、温度、top-k、top-p),说出它们各自的失败方式,并解释”最大化概率”为什么会导致平淡和重复。
  7. 概括 GPT-1 的”生成式预训练 + 判别式微调”和 GPT-2 的零样本思路。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
第 1 讲:n-gram、RNN-LM、困惑度、自动微分预训练与微调的系统讨论(第 4 讲)
第 2 讲:注意力、因果掩码、GPT 的结构推测解码等高效解码(第 18 讲)
概率:条件概率、期望、方差指令微调、RLHF(第 11 讲)
前置讲 P1 §5 的 KV 缓存公式(不读也行)长上下文与 KV 缓存的压缩(第 19 讲)

0.3 本讲地图 Map

§1  回顾:机器学习的配方、反向传播、SGD;用 RNN 做序列标注
§2  深度语言模型的最大似然:数数 vs 梯度;END 那一列;小批量的噪声          ← 代码 02
§3  批处理:截断、填充、损失掩码、左右填充、分桶与打包                      ← 代码 01
§4  KV 缓存:存什么、省多少、占多少                                      ← 代码 03
§5  分词:词、字符、子词;BPE;字节级 BPE                                ← 代码 04
§6  解码:贪心、束搜索、祖先采样、温度、top-k、top-p;复读                  ← 代码 05、06
§7  GPT-1 与 GPT-2:生成式预训练                                        (补充)
§8–12  小结、误区、练习、延伸阅读、下一讲

0.4 记号 Notation

沿用前两讲。新增:BB 批大小;TT 块大小(block size,即一个训练序列的最大长度);τ\tau 温度;WW 束宽;D={w(1),…,w(N)}\mathcal{D} = \{w^{(1)}, \dots, w^{(N)}\} 训练集,每个 w(i)w^{(i)} 是一个句子(token 序列)。

0.5 本讲的实验模型

除了 02 第 1 部分、04 和 06,所有实验都直接读取第 2 讲 06 号脚本缓存的农场语言 GPT(2 层、4 头、d=64d = 64,测试困惑度 2.5305,真实下界 2.4913)。农场语言的好处这一讲会特别明显:decoding.farm_true_logprob 能精确算出任意一句话在真实分布下的概率(对训练集的 2 万句,它和生成时记录的概率逐位相同),所以解码策略的好坏可以客观地衡量,而不只是”看起来通不通顺”。


1. 回顾:怎样训练一个神经网络 · Recap: learning a neural network

1.1 到目前为止

slides 把前两讲分成两条线:

  • 深度学习:自动微分是计算可微函数梯度的工具,核心构件是有 forward() 和 backward() 的模块;计算图是定义函数的另一种方式。我们见过两张很深的计算图:RNN-LM 和 Transformer-LM。
  • 语言建模:关键想法是”以前面的词为条件,采样下一个词”。n-gram 用一大堆 5 万面的骰子定义下一个词的分布,RNN-LM 和 Transformer-LM 用神经网络。n-gram 很容易学——数共现次数就行;**但我们还没说过怎样学 RNN-LM 或 Transformer-LM。**这一讲先回答这个问题。

1.2 机器学习的配方

slides 的四步配方:

  1. 给定训练数据 D={(x(i),y(i))}i=1N\mathcal{D} = \{(x^{(i)}, y^{(i)})\}_{i=1}^N;
  2. 选择决策函数 y^=hθ(x)\hat{y} = h_\theta(x) 和损失函数 ℓ(y^,y)\ell(\hat{y}, y);
  3. 定义目标:θ^≈arg⁡min⁡θ∑iℓ(hθ(x(i)),y(i))\hat\theta \approx \arg\min_\theta \sum_i \ell(h_\theta(x^{(i)}), y^{(i)});
  4. 用 SGD 训练:朝梯度的反方向走一小步,θ(t+1)=θ(t)−ηt∇ℓ(hθ(x(i)),y(i))\theta^{(t+1)} = \theta^{(t)} - \eta_t \nabla \ell(h_\theta(x^{(i)}), y^{(i)})。

梯度由反向传播(第 1 讲 §5)计算——slides 特别强调”整个 Backward 那一列现在都由自动微分替我们算了”。

1.3 SGD 与小批量 SGD

slides 并排写了两个算法:

  • SGD:每个 epoch 把训练样本打乱,一次取一个样本 (x(i),y(i))(x^{(i)}, y^{(i)}),算它的梯度,更新参数。
  • 小批量 SGD:把样本随机分成 BB 个一组的小批量 IbI_b,每次用小批量的平均梯度 g=1m∑i∈Ib∇Ji(θ)g = \frac{1}{m}\sum_{i \in I_b} \nabla J_i(\theta) 更新。

为什么要用小批量?两个原因,02 第 3 部分都测了(随机初始化的小 GPT,“全量梯度”用 4096 句计算):

批大小 BBE∥gB−g∥2\mathbb{E}\lVert g_B - g\rVert^2和全量梯度的余弦相似度⏱ 每个样本的耗时
125.0180.5840.58 ms
45.1360.8430.24 ms
161.4470.9460.13 ms
640.3620.9860.11 ms
2560.0880.9960.11 ms
  1. 噪声更小:随机抽取的小批量梯度(近似)是全量梯度的无偏估计,误差的平方大约按 1/B1/B 下降(BB 从 1 到 64 缩小到约 1/69)。
  2. 每个样本更便宜:把很多小的矩阵-向量乘法合成大的矩阵乘法,算术强度更高(前置讲 P3 §3.4)。在 GPU 上这个差别会大得多。

代价是每一步更贵。批大小是”每一步走得多准”和”每一步花多少计算”之间的权衡;前置讲 P4 §5 的数据并行,本质上就是在多块卡上凑一个更大的批。

1.4 用 RNN 做序列标注

在讲语言模型之前,slides 先回顾了一个更简单的有监督任务:序列标注(sequence tagging)。每个训练样本是一对等长的序列 (x(n),y(n))(x^{(n)}, y^{(n)}),要为每个输入位置预测一个标签:

  • 词性标注:time flies like an arrow → n v p d n。同一句话可以有不同的正确标注(n n v d n:“时间苍蝇”喜欢一支箭),这正是需要上下文的原因。
  • 手写识别:一串字母图像 → 字母序列(u n e x p e c t e d)。
  • 音素识别:一段语音的声学特征 → 音素序列(TIMIT 数据集)。

RNN 做序列标注:ht=σ(Wahht−1+Waxxt+ba)h_t = \sigma(W_{ah} h_{t-1} + W_{ax} x_t + b_a),yt=softmax(Wyhht+by)y_t = \mathrm{softmax}(W_{yh} h_t + b_y),每个位置的损失是交叉熵 ℓt=−∑k(yt∗)klog⁡(yt)k\ell_t = -\sum_k (y_t^*)_k \log (y_t)_k,总损失 ℓ=∑tℓt\ell = \sum_t \ell_t。

slides 问:“在真正的实现里(比如 RNN-LM 或 Transformer-LM),这些步骤里哪些可以为了效率跳过?“两个常见的答案:

  • 不必显式算出 softmax 的概率向量 yty_t 再取对数。交叉熵只需要目标那一项的 log⁡\log 概率,而 log⁡softmax(z)k=zk−log⁡∑jezj\log \mathrm{softmax}(z)_k = z_k - \log\sum_j e^{z_j} 可以直接从 logits 算出来,既省计算又更稳定(前置讲 P1 §3 的稳定 softmax)。PyTorch 的 F.cross_entropy 接收的就是 logits。
  • 不必把目标写成 one-hot 向量 yt∗y_t^* 再做内积,直接用目标的整数编号去”取”那一项。

slides 还问:“这些参数张量是从哪来的?“——从随机初始化开始,然后用梯度下降学出来。这正是下一节的内容。


2. 深度语言模型的最大似然 · MLE for deep neural LMs

2.1 语言建模的数据集

语言建模的数据集只有输入、没有标签:D={w(n)}n=1N\mathcal{D} = \{w^{(n)}\}_{n=1}^N,每个样本是一个句子(time flies like an arrow;the cows eat grass;birds fly fast……)。“标签”就是句子自己:每个位置的目标是下一个词。这就是为什么语言模型的训练叫自监督(self-supervised)——不需要人工标注,互联网上的任何文本都能用。

2.2 n-gram:数数就是最大似然

slides 的例子:要估计 p(wt∣wt−2=cows,wt−1=eat)p(w_t \mid w_{t-2} = \text{cows}, w_{t-1} = \text{eat}),就去语料里数”cows eat”后面跟着什么——corn 4 次、grass 3 次、hay 2 次、if 1 次、which 1 次,共 11 次,于是 p(corn∣cows eat)=4/11p(\text{corn} \mid \text{cows eat}) = 4/11。第 1 讲 §3.2 推导过:写出句子在 n-gram 模型下的似然、加上”概率和为 1”的约束、令梯度为零,解出来正好是这个计数比例。

2.3 深度语言模型:同样的目标,没有闭式解

slides:“我们也可以用最大似然估计来学习 RNN-LM 或 Transformer-LM 的参数——但不是闭式解,而是按另一个配方:写出句子在深度语言模型下的似然;用自动微分计算(小批量)似然对参数的梯度;沿负梯度方向走(小批量 SGD,或你喜欢的优化器)。”

具体地,一个句子的对数似然按链式法则拆成每个位置的和:

log⁡p(w)=log⁡p(w1,…,wT)=∑t=1Tlog⁡p(wt∣ht),J(θ)=∑i=1Nlog⁡pθ(w(i))\log p(w) = \log p(w_1, \dots, w_T) = \sum_{t=1}^{T} \log p(w_t \mid h_t),\qquad J(\theta) = \sum_{i=1}^{N} \log p_\theta(w^{(i)})

其中 hth_t 由 w<tw_{<t} 算出(RNN 的隐藏状态,或 Transformer 最后一层在第 tt 个位置的输出)。训练就是最大化 J(θ)J(\theta)——等价于最小化每个 token 的平均交叉熵。slides 最后说:“训练 Transformer-LM 完全一样,只是把深度语言模型换成另一个。”

同一个模型族时,梯度下降会走到数数的答案吗?02 第 2 部分做了一个最简单的检验:一个”神经 bigram”模型——参数就是一张 V×VV \times V 的 logits 表,p(b∣a)=softmax(W[a])bp(b \mid a) = \mathrm{softmax}(W[a])_b——在农场语言的 232,011 个 bigram 上用 Adam 训练:

Adam 步数1101005001500
平均负对数似然4.50673.19551.34741.33301.3315

数数得到的最大似然解是 1.331175。梯度下降从上方逼近它,永远不会更低;1500 步后,出现过的 bigram 上两者的概率最大只差 5.4×10−45.4 \times 10^{-4}。差别在没见过的组合上:数数给 0,神经网络给一个越训练越小、但不为 0 的概率(softmax 的输出永远是正的)。更深的模型还会让相似的上下文共享参数——这才是神经语言模型比 n-gram 强的地方(第 1 讲 §4.7)。

2.4 START 和 END:为什么概率加起来是 1

slides 在 RNN-LM 的计算图上加了一列,然后问:“如果去掉这整一列,我们定义的是什么概率分布?“这里说的是预测 END(</s>)的那一步。02 第 1 部分用一个只有 {a, b} 两个词的小 GPT 枚举了所有句子:

长度 nn有 END:长度 ≤ nn 的累计概率无 END:长度恰为 nn 的总概率无 END:长度 ≤ nn 的累计
00.01101.00001
20.55881.00003
80.87361.00009
120.92501.000013
  • 有 END 时,所有长度的句子的概率加起来趋于 1:模型定义的是一个在”任意长度的有限句子”上的分布,长句和短句可以直接比较。
  • 去掉 END 后,每个固定长度的句子概率之和都是 1:模型定义的是一族分布,每个长度一个,全部加起来是 n+1n+1。它不知道句子该在哪里结束,生成时也就不知道何时停。
  • 同理,START(<s>)让第一个词也有条件分布 p(w1∣⟨s⟩)p(w_1 \mid \langle s\rangle);没有它,模型只能定义 p(w2,…,wT∣w1)p(w_2, \dots, w_T \mid w_1)。

slides 的另一个提示:state-of-the-art 的语言模型今天都是 Transformer,但最早的神经语言模型大多是 RNN-LM(Mikolov et al., 2010),它们为今天的结构铺了路。


3. 批处理:填充、截断与掩码 · Batching, padding & truncation

3.1 为什么效率这么重要

slides 用 GPT-3 作案例:训练数据约 5000 亿 token(论文里数据集的总量;实际训练了 3000 亿个 token)、1750 亿个参数。slides 写的计算量是”50 petaflop/s-days”;按 GPT-3 论文附录 D 的表,175B 模型的训练计算量是 3.14×10233.14 \times 10^{23} FLOPs,约 3640 petaflop/s-days(1 petaflop/s-day = 8.64×10198.64 \times 10^{19} FLOPs),用 前置讲 P3 §6.4 的 6ND=6×1.75×1011×3×1011=3.15×10236ND = 6 \times 1.75 \times 10^{11} \times 3 \times 10^{11} = 3.15 \times 10^{23} 也能核对。我们按论文的数字。

在这个规模上,任何浪费都要乘以几千个 GPU 天。slides 列出了 Transformer 能高效训练的几个原因:

  • 批处理:一次处理 BB 个句子,每个句子的计算完全相同,可以简单地并行;
  • 缩放点积注意力:一个时间步的注意力分数不依赖其他时间步,可以并行(第 2 讲 §3.6);
  • 多头注意力:每个头独立计算,更多并行;
  • 矩阵乘法:注意力的核心是矩阵乘法,GPU 和 TPU 这类专门硬件让它非常快;
  • 模型并行:模型太大时,把它切到多个 GPU 或多台机器上(第 17 讲);
  • KV 缓存:键和值在许多时间步之间被重复使用,但查询、相似度分数和注意力权重不需要缓存(§4)。

这一节讲第一条:怎样把句子组成批。

3.2 slides 的例子:截断、填充、查词表、查嵌入

slides 取 8 个训练句子,块大小(block size,最大序列长度)设为 10。组成一个批之前要做四件事:

  1. 把太长的句子截断;
  2. 把太短的句子填充(pad);
  3. 用一张查找表(词表)把每个 token 变成整数;
  4. 把每个整数变成一个固定长度的嵌入向量。

01_batching_padding.py 第 1 部分逐字复现了 slides 的编号矩阵:

句子原长度截断 + 填充后的编号被截掉的词
In the hole in the ground there lived a hobbit102 41 17 19 41 13 42 23 6 16—
It was the best of times it was the worst of times123 50 41 9 30 46 21 50 41 55of times
Even miracles take a little time61 25 39 6 22 45 0 0 0 0—
The important thing is to never stop questioning84 18 43 20 47 27 37 33 0 0—

(只列了 4 句。)词表有 57 个条目:<PAD> 编号 0,其余按 Python 的字符串顺序排,大写字母排在小写前面,所以 ‘Even’、‘In’、‘It’、‘The’ 排在最前。8 句里 3 句被截断、3 句被填充,共 7 个 <PAD>,占 9% 的位置。查嵌入之后得到形状 (8, 10, 4) 的张量;padding_idx=0 让 <PAD> 的嵌入固定为零、不接收梯度。

块大小就是模型的最长上下文:截断掉的词模型永远看不到。填充的位置则必须在两个地方被排除——损失和注意力。

3.3 损失掩码

01 第 2 部分把验证集前 256 句组成一个批(形状 (256, 19),真实 token 2,967 个,填充 1,897 个):

损失的算法平均交叉熵计入的位置数
忽略填充(ignore_index=-100)0.92972,967
把填充当成要预测的 <pad>5.10454,864

不排除填充,损失就被这些毫无意义的位置主导了;如果训练时也不排除,模型会把力气花在”句子结束后全是 <pad>”这种无用的事上。第 1 讲的 make_batches 已经这样做了:填充位置的目标设为 −100,F.cross_entropy 会跳过它们。

**按 token 平均还是按句子平均?**同一个批,按 token 平均是 0.9297,先在每句内平均、再对句子平均是 0.8876——按 token 平均时长句子权重更大。slides 的目标函数 J(θ)=∑ilog⁡p(w(i))J(\theta) = \sum_i \log p(w^{(i)}) 是对所有 token 求和,按 token 平均只差一个常数因子,所以 LLM 的训练损失和困惑度都按 token 计(练习 2)。

3.4 右填充还是左填充?注意力掩码与位置编号

填充放在哪一边,对因果 Transformer 有很大区别。01 第 3 部分把短句 “the hens eat corn every day .”(8 个 token)和一个 19 个 token 的长句放进一个批,比较短句在批里和单独计算时的 logits(float64):

做法max|logits 差|
右填充,直接用模型2.7×10−152.7 \times 10^{-15}
左填充,直接用模型11
左填充 + 位置从真实 token 起算(不屏蔽填充)6.3
左填充 + 位置从真实 token 起算 + 屏蔽填充的键2.7×10−152.7 \times 10^{-15}
  • 右填充对因果模型是”免费”的:填充在所有真实 token 的右边,因果掩码本来就不让真实 token 看它们。
  • 左填充时,真实 token 能看到左边的 <pad>,而且位置编号整体后移了。必须同时做两件事:在注意力里屏蔽填充的键(key padding mask),并让位置编号从第一个真实 token 开始数。

为什么还要用左填充?批量生成时,左填充让所有序列的最后一个 token 对齐在同一列,下一步可以整批一起接上新 token(§4 的 KV 缓存、第 18 讲的批量推理)。

3.5 浪费了多少?分桶与打包

01 第 4 部分统计了农场语言训练集(2 万句、长度 8–19、共 232,011 个真实位置)在不同组批方式下的浪费:

做法(批大小 32)实际计算的位置数浪费在填充上的比例
随机分批365,79236.6%
按长度排序后分批(分桶)232,1920.1%
打包成 64 个 token 的块232,0000.0%
  • 分桶(bucketing)几乎消除了浪费,但同一批里都是长度相近的句子,批与批之间不再是同分布的随机样本。常见的折中是”桶内随机、桶间打乱”。
  • 打包(packing)是 GPT 系列预训练的做法:把文档首尾相接、用分隔符(GPT-2 的 <|endoftext|>)隔开,再切成定长的块,几乎没有浪费。代价是一个块里有好几篇不相关的文档——在我们的设置里,100% 的块中间都有句子边界——注意力会跨过边界去看上一篇。Llama 3 在块内再加一个”文档内掩码”,只许看同一篇文档(第 4 讲 slides 的 intra-document masking)。

4. KV 缓存 · The KV cache

4.1 生成时的浪费

训练时一次前向就能算出所有位置的预测(第 2 讲 §4.8)。生成时却不行:第 tt 个 token 要等第 t−1t-1 个生成出来才能算。最朴素的做法是每生成一个 token,就把整个前缀重新喂一遍模型——第 2 讲的 GPT.forward 就只能这样用。

可是前面那些位置的计算结果根本不会变。因果掩码保证第 jj 个位置的 kjk_j、vjv_j 只取决于前 jj 个 token;后面来了新 token,它们也不会改变。slides 的总结:

生成的每一步,我们要重用所有之前的键和值(所以要把它们缓存起来),但可以丢掉查询、相似度分数和注意力权重。KV 缓存:每一步为新 token 算出新的 kk 和 vv,把它们存起来供之后的时间步使用。

4.2 实现

kv_cache.forward_cached(model, ids, cache) 对每一层:

  1. 只为新的 token 算 q,k,vq, k, v;
  2. 把新的 k,vk, v 接到缓存后面:K=[K旧;k新]K = [K_\text{旧}; k_\text{新}],V=[V旧;v新]V = [V_\text{旧}; v_\text{新}];
  3. 新 token 的查询对全部 KK 做注意力(新 token 之间仍要遵守因果性);
  4. 前馈层、残差、层归一化都是逐位置的,只对新 token 算。

第一次调用把整个提示一次喂进去,叫预填充(prefill);之后每次只喂 1 个新 token,叫解码(decode)。前置讲 P3 §6 讲过两个阶段的性能特征完全不同:预填充是大矩阵乘法(计算受限),解码是矩阵-向量乘法(访存受限)。

缓存的形状是每层一对 (K,V)(K, V),各 (B,h,T已缓存,dk)(B, h, T_\text{已缓存}, d_k)——这正是第 2 讲 GPT 接口 logits, state = model(ids) 里那个空着的 state 该装的东西。

正确性。03_kv_cache.py 第 1 部分:对一个 19 个 token 的句子,不管一次喂 1 个、先喂 5 个再一个一个、还是每次 3 个,带缓存的结果都和整段前向逐位一致(float64 下差 10−1510^{-15} 量级);float32 下贪心生成 30 个 token,有缓存和无缓存生成的序列完全相同。

4.3 缓存里有什么

农场 GPT 有 2 层、每层 4 个头、每头 dk=16d_k = 16。03 第 2 部分打印了缓存的增长:预填充 4 个 token 后,第 1 层 KK 的形状是 (1, 4, 4, 16),缓存共 4,096 字节;之后每解码一步,长度加 1、增加 1,024 字节。公式是

每个 token 的缓存=2 (K 和 V)×L×h×dk×每个数的字节数=2×2×4×16×4=1024 字节\text{每个 token 的缓存} = 2\ (\text{K 和 V}) \times L \times h \times d_k \times \text{每个数的字节数} = 2 \times 2 \times 4 \times 16 \times 4 = 1024\ \text{字节}

为什么不缓存 QQ?第 tt 步之后,旧的查询再也不会被用到——注意力只需要”新查询 × 所有旧键”。分数和权重也只为当前这一步服务。

4.4 省了多少计算

不带缓存时,生成第 tt 个 token 要把前 t−1t-1 个 token 全部重算一遍,生成 NN 个 token 的总计算量随 N2N^2 增长;带缓存时每一步只算 1 个 token,总量随 NN 线性增长(注意力那一项每步 4td4td,仍随 tt 增长,但远小于重算整个前缀)。03 第 3 部分按公式估算农场 GPT(提示 4 个 token):

生成 NN 个无缓存有缓存倍数
812.5M FLOPs2.28M5.5×
30119.5M7.01M17.0×
60453.3M13.87M32.7×

⏱ 实测(批大小 32、生成 56 个 token):无缓存 113 ms,有缓存 20 ms,快 5.8 倍——比公式小,因为这个模型非常小,每一步的耗时主要是 Python 和 PyTorch 的固定开销(前置讲 P3 §5.3),而不是 FLOPs。模型越大,实测越接近公式。

4.5 缓存有多大:RNN 的状态 vs Transformer 的缓存

03 第 4 部分比较了每个序列需要保存的”状态”:

上下文长度第 1 讲 RNN(H=64)农场 GPT 的 KV 缓存(按公式外推)Llama-3 8B 的 KV 缓存(BF16)占 Llama-3 8B 权重的比例
16256 B16.0 KiB2.0 MiB0%
1,024256 B1.0 MiB128.0 MiB1%
8,192256 B8.0 MiB1.0 GiB7%
131,072256 B128.0 MiB16.0 GiB107%
  • RNN 的状态大小固定:它被迫把历史压缩进一个向量(第 2 讲”遗忘”的根源)。
  • Transformer 的 KV 缓存把每个 token 的 KK、VV 原样保留:不压缩,所以随时能”回头看”任何位置,代价是内存随长度线性增长。Llama-3 8B 在 128K 上下文时,一个序列的缓存已经比权重还大。

这是第 4 讲 GQA(减少 K、V 的头数——Llama-3 8B 已经在用,否则上表的数字还要乘以 4)、第 18 讲推理优化、第 21 讲状态空间模型(回到固定大小的状态)共同面对的问题。


5. 分词 · Tokenization

5.1 token 应该是什么?

到目前为止我们的”token”都是词(农场语言只有 53 个词)。真实文本该怎样切?slides(改编自 Henry Chai 的课件)比较了三种做法。

以词为单位:输入 “Matt is giving a lecture on transformers”,输出 [matt, is, giving, a, lecture, on, transformers]。缺点:

  • 很难在词表大小和计算量之间取舍(每种词形都要一个条目);
  • 相似的词(transformers 和 transformer)会被映射到完全不同的表示;
  • 拼写错误通常是词表外的词(out-of-vocabulary, OOV):“Matt is givin’ a lectrue on transformers” → [matt, is, , a, , on, transformers]。

以字符为单位:词表小得多,但很多语义信息丢失了;序列比以词为单位长得多,可能带来计算问题;对汉字这样的表意文字效果不错。

以子词为单位:把长的或罕见的词切成更小的、有意义的片段(子词);没有 OOV——任何不是子词的 token 都能用其他子词拼出来(所有字符永远都在子词表里)。学习子词切分的算法有 BPE、WordPiece、SentencePiece。

04_tokenization.py 第 1 部分在一个玩具英语语料上验证了前两种做法的问题:以词为单位时,拼写错误的两个词变成 <OOV>;以字符为单位时,序列长度是以词为单位的 4.84 倍。

5.2 一个带”词形变化”的玩具语料

农场语言没有词形变化,用来演示子词不合适。bpe.make_morph_corpus 生成了一门有规则词缀的玩具英语(2 万句、298 种词):动词加 -s、-ed、-ing,名词加 -s,动词加 -er 变成”做这件事的人”(farmer、painter),形容词加 -ly、-er、-est、-ness,前缀 un-、re-。另外留出了 16 个词形(repainting、unkindly、climbers、softest……):它们的词根和词缀在训练语料里都出现过很多次,但这个组合一次都没出现。

5.3 BPE 算法

字节对编码(Byte-Pair Encoding)原本是一种数据压缩算法,Sennrich, Haddow & Birch (2016) 把它用于分词:

  1. 把每个词拆成字符,末尾加一个词尾标记 </w>(这样词尾的 ing</w> 和词中间的 ing 是不同的 token);
  2. 统计语料里所有相邻 token 对出现的次数;
  3. 把出现最多的那一对合并成一个新 token,记下这条合并规则;
  4. 重复 2–3,直到做够指定的合并次数。

词表 = 所有字符 + 每次合并产生的新 token。编码一个新词时,从字符开始,按学到的先后顺序反复应用合并规则(练习 5 手工做一遍)。

04 第 2 部分打印了前 24 次合并:先是最常见的字母组合和最常见的词(e</w>、th、the</w>、s</w>、er……),紧接着就是规则的词尾——s</w> 第 4 次、ers</w> 第 15 次、ly</w> 第 18 次、ing</w> 第 19 次、ed</w> 第 22 次、est</w> 第 59 次、ness</w> 第 90 次合并时成为完整的 token。BPE 没有任何语言学知识,只是因为这些词缀出现在很多不同的词后面,字母组合的总频次很高。

合并次数 = 词表大小与序列长度的取舍:

合并次数词表大小平均每个词几个 token整个词是一个 token 的比例
0(字符级)264.9780.0%
50762.43349.5%
2002261.35772.7%
4004261.03696.6%

这份语料只有 298 种词,合并 400 次后几乎每个词都成了一个 token——回到了以词为单位。真实模型取中间:GPT-2 合并了 50,000 次(词表 50,257),Llama 3 的词表约 128K。

5.4 没见过的词形:切得对吗?

04 第 3 部分(”_” 表示词尾 </w>):

词语素切分合并 100 次合并 200 次合并 400 次
repaintingre|paint|ingre paint ing_re paint ing_re painting_
unkindlyun|kind|lyun k ind ly_un kind ly_un kindly_
climbersclimb|er|sc li m b ers_climb ers_climb ers_
softestsoft|ests of t est_sof t est_sof test_
visitorsvisit|or|sv i s i t or s_visit or s_visit or s_
合并次数切分位置的精确率召回率
1000.450.88
2000.810.85
4000.820.54
  • 16 个留出词形以词为单位全是 OOV;BPE 下没有一个是 OOV——总能退回到更小的片段,最坏退回到字符。
  • 合并 200 次时,很多切分和语素惊人地一致:模型可以从 “paint” 和 “ing” 的已有知识推断 “repainting” 的意思。
  • 但 BPE 的依据只是频次:soft 很少单独出现,softest 被切成 “sof t est_“,再多合并就变成 “sof test_“;合并更多次后,常见的整词(painting、kindly)吞掉了语素边界,召回率从 0.85 降到 0.54。
  • visitors 被切对了,但只是因为 “or” 这个字母组合在 work、short 里很常见,并不是 BPE 认识 -or 这个词缀。

用合并 400 次的 BPE 切 slides 那句话(它的词大多不在玩具语料里):“matt is giving a lecture on transformers” 变成 28 个 token(m at t_ is_ g i v ing_ ……),而玩具语料本身平均每个词只要 1.036 个 token。和训练语料差别很大的文本会被切得很碎——同一个模型处理不同语言、不同领域时,“每个词几个 token”差别很大。拼写错误的 lectrue 被切成 “l e c true_“(true 来自语料里的 untrue):不再是 OOV,而且解码可以原样还原(BPE 是无损的)。

5.5 字节级 BPE

以字符为基本单位的 BPE 仍然有一个漏洞:训练时没见过的字符(汉字、带重音的字母、表情符号)不在基本词表里。04 第 5 部分:

文本字符数UTF-8 字节数字节
naïve566E 61 C3 AF 76 65
漢字26E6 BC A2 E5 AD 97
生成式39E7 94 9F E6 88 90 E5 BC 8F
🐄14F0 9F 90 84

GPT-2 起的做法是字节级 BPE:先把文本变成 UTF-8 字节,基本单位只有 256 个,任何文本都能表示,彻底没有 OOV;再在字节序列上学合并。GPT-2 的词表 50,257 = 256 个字节 + 50,000 次合并 + 1 个 <|endoftext|>。代价是:一个汉字占 3 个字节,如果合并规则里很少有中文,同样的意思用中文写可能要多花好几倍的 token——也就多花好几倍的计算和 API 费用。

(GPT-2 的预分词还会把标点、数字、空格单独处理,并把空格并进下一个词的开头;SentencePiece 则直接在原始文本上学,不需要预先按空格切分。细节不影响 BPE 的核心思想。)


6. 解码 · Decoding

6.1 搜索还是采样?

语言模型给出的是每一步”下一个 token”的分布 p(wt∣w<t)p(w_t \mid w_{<t})。生成一段文字时,怎样从这些分布里挑?有两类做法:

  • 搜索:找概率最高的那句话 arg⁡max⁡wp(w)\arg\max_w p(w)——贪心、束搜索;
  • 采样:按概率随机抽一句话 w∼p(w)w \sim p(w)——祖先采样,以及改变分布形状的温度、top-k、top-p。

在农场语言上,我们知道真实分布,所以每种方法的结果都能和”真相”比较。

6.2 贪心搜索

slides 先介绍一般的贪心搜索:搜索空间是带权重的节点和边,目标是找从根到叶子的总权重最小的路径;贪心在每个节点选(眼前)权重最小的边。它是一种启发式方法,不一定找到最好的路径,计算时间和最长路径的长度成正比。

用到语言模型上:节点是部分句子,边的权重是负对数概率,目标是找负对数概率最小(概率最大)的完整句子。贪心解码每一步选概率最大的 token。

05_decoding.py 第 1 部分看 “the dog” 之后的第一步:

下一个词模型概率真实概率
behind0.1870.183
beside0.1800.183
near0.1480.183
eats0.1410.113
hears0.1330.075
chases0.1020.188
makes0.0980.075

模型选了 behind;按真实分布,第一步最可能的是 chases(0.1875)。但真实分布下最可能的整句话是 “the dog eats bones every day .”,它的第一个词 eats 的真实概率只有 0.1125。原因是概率被”摊薄”了:选介词之后还要选 the、8 种动物、单复数,每一个具体的句子只分到 1/48;选 chases 之后还要在几种猎物和单复数之间分;而 eats 之后,bones 几乎是唯一的选择。结果:

方法生成的句子模型 log p真实 log p(给定主语)
贪心the dog behind the foxes hears the foxes in the field .−8.245−10.163
束搜索(W=4)the dog eats bones every day .−3.157−3.150
真实分布的众数the dog eats bones every day .—−3.150

这正是 slides 搜索树的例子:每一步选”眼前最好”的边,不保证整条路径最好。即使模型完全准确,贪心也会失败(练习 8:按真实分布贪心,得到的是 “the dog chases the cat in the morning .”,概率只有众数的约三分之一)。

6.3 祖先采样

slides 的另一种做法:从语言模型采样——把每一步的分布当成一颗加权骰子,选对应前缀的那颗骰子,掷一下,把结果接到前缀后面,重复。在搜索树上,就是在每个节点按(由负对数概率换算回来的)概率随机选一条边。slides 指出:

  • 这是一种精确的采样方法:假设分布是局部归一化的(每一步的条件分布各自加起来是 1),按这种方式抽出的路径恰好服从它的总概率;
  • 计算时间和最长路径的长度成正比。

这叫祖先采样(ancestral sampling),第 1 讲的 n-gram 和 RNN-LM 采样都是这样做的。

6.4 束搜索(补充)

束搜索(beam search)是贪心和穷举之间的折中:每一步保留累计对数概率最高的 WW 条部分序列(“束”),把每条束扩展出 VV 个候选,从全部 W×VW \times V 个候选里再取最好的 WW 个;以 </s> 结尾的候选放进”已完成”列表。W=1W = 1 就是贪心。decoding.beam_search 的停止条件是:已完成的最好句子得分不低于所有未完成束的得分(再扩展只会让得分更低)。

05 第 2 部分在 16 个主语上比较束宽:

束宽 WW找到真实众数的主语数最好句子的平均模型 log p
1(贪心)5 / 16−4.620
28 / 16−4.041
412 / 16−2.589
813 / 16−2.547

束宽 8 还没找到的 3 个,不是搜索不够:模型给束搜索结果的概率本来就比给真实众数的高(例如 “the bats eat flies at night .” −4.743 vs “the bats make noise at night .” −4.758)。束搜索忠实地找到了模型最可能的句子,错的是模型的估计(差距都很小)。

**从 <s> 开始时更明显。**束宽 1、4、16 都给出 “the foxes chase the hens at night .”,模型 log p −4.633;真实的全局众数 “the bat makes noise at night .” 在模型看来只有 −5.022。模型对 16 种主语的估计不均匀(foxes 0.098、cats 0.041,真实各 0.0625),最大化概率会把模型的这点偏差放大成”永远只生成这一句”。在真实的大模型上,“模型最可能的句子”往往很短、很平淡;Stahlberg & Byrne (2019) 发现机器翻译模型的精确众数经常是空译文。最大化概率不等于生成好文本。

(束搜索还偏爱短句:每多一个 token 就多乘一个小于 1 的概率。机器翻译里常用长度归一化——按平均每个 token 的 log p 排序——来抵消;beam_search 的 length_norm 参数实现了这一点。农场语言里最可能的句子本来就是最短的,所以这里看不出这个问题。)

6.5 温度、top-k 与 top-p(补充)

采样可以通过改变每一步分布的形状来调节:

  • 温度 τ\tau:pi∝exp⁡(zi/τ)p_i \propto \exp(z_i / \tau)。τ<1\tau < 1 让分布更尖,偏向高概率的词;τ>1\tau > 1 让分布更平;τ→0\tau \to 0 就是贪心(练习 6)。
  • top-k(Fan et al., 2018):只保留概率最高的 kk 个 token,其余设为 0,再重新归一化。
  • top-p / nucleus 采样(Holtzman et al., 2020):把 token 按概率从大到小排,保留”累积概率刚好达到 pp“的最小集合。和 top-k 不同,保留的个数随分布形状自动变化(练习 7)。

05 第 3 部分从 <s> 开始各生成 2000 句:

策略合法可能不同句子真实 NLL/tokenk=0k=0 占比覆盖
真实分布(直接抽样)1.0001.0000.7220.8830.450.384
贪心 / 束搜索 W=41.0001.0000.0010.6201.000.004
祖先采样 τ=10.9900.9650.6830.8600.490.362
温度 τ=0.51.0000.9990.6010.8230.460.302
温度 τ=1.50.8460.7500.7810.8790.530.366
温度 τ=2.50.1440.0970.9800.8700.680.139
top-k, k=31.0000.9960.2280.7870.550.058
top-k, k=100.9970.9800.6440.8620.470.255
top-p, p=0.91.0001.0000.6550.8570.450.307
top-p, p=0.51.0001.0000.3160.7950.460.049
τ=2.5 + top-p 0.90.4090.2790.9460.9000.580.253

(合法 = 语法结构正确;可能 = 在真实分布下概率大于 0,还要符合各种动物的习性;真实 NLL/token = 可能的句子在真实分布下每个 token 的负对数概率;覆盖 = 生成过的不同句子在真实分布下的概率之和。)

  • 贪心和束搜索每次都生成同一句话。它的真实 NLL/token 只有 0.620,比真实样本的典型值 0.883 低得多:太”典型”反而不像真实文本的样本。搜索找的是”最可能的一句”,而不是”一句可能的话”。
  • 祖先采样的各项统计都和真实分布接近(可能率 0.965):模型准,样本就像真的。剩下 3.5% 不可能的句子来自模型分给”长尾”的那一点点概率——每一步只有很小的概率出错,但一句话有十几步。
  • 温度:τ=0.5 几乎全是可能的句子,代价是多样性;τ=1.5 时可能率降到 0.750,τ=2.5 时只剩 0.097,开始胡言乱语。
  • top-p 0.9 把长尾砍掉,可能率回到 1.000,多样性只略有下降(不同句子 0.655 vs 0.683)。即使在 τ=2.5 时,加上 top-p 0.9 也能把可能率从 0.097 拉回到 0.279。
  • 截得太狠也有代价:top-k=3 时不同句子只剩 0.228——k 是固定的,而选主语时分布很平(16 个各约 1/16),k=3 砍掉了大部分合理的选择;top-p 按累积概率截断,能随分布形状自动调整。

没有一种策略在所有指标上都最好:质量和多样性之间总要权衡。实际产品里常见的组合是”温度 0.7–1.0 + top-p 0.9–0.95”;需要确定性答案(比如做选择题、写代码时)就用很低的温度或贪心。

6.6 一直往下写:复读(补充)

05 的模型每次只生成一句话。真实的语言模型是在连续的文本流上训练的(§3.5 的打包),生成时也常常一口气写很多句。06_degeneration.py 在打包的农场语言文本流(块长 128)上训练了一个 GPT,然后从 32 个句子开头出发各写 120 个 token:

解码方式每段完整句子数和前文重复的句子比例可能的句子比例每段不同句子数
贪心12.60.5941.0005.1
温度 τ=0.510.40.0090.99410.3
祖先采样 τ=110.20.0000.93510.2
top-p 0.910.00.0030.9919.9
真实分布(每段独立抽 8 句)8.00.0031.0008.0

把一段里相同的句子用同一个字母表示,贪心的前几段是 ABCDCECFFFFF、ABCBCBCBDEDDD、ABCCCDBCCDBC……;top-p 0.9 的是 ABCDEFGHIJK。贪心生成的每一句单独看都完全合法,合在一起却是典型的”复读机”。

为什么?真实分布里前后两句话是独立的,下一句的主语是 16 选 1 的均匀分布;可模型对下一句的估计总有一点偏好,贪心每次都选最偏好的那个,再接上它最可能的那一句。贪心又是确定性的,同样的前文永远得到同样的下一句,所以一旦前文开始重复,后面就跟着重复。Holtzman et al. (2020) 在 GPT-2 上观察到同样的现象,称之为退化(degeneration),并据此提出了 top-p 采样:人写的文本并不总是选最可能的词,最大化概率的解码会落入重复的”高概率陷阱”。


7. GPT-1 与 GPT-2:生成式预训练(补充) · GPT-1 and GPT-2

本讲的两篇阅读材料把”训练一个语言模型”推向了”用语言模型解决其他任务”。这里只概括要点,第 4 讲会系统讨论预训练和微调。

GPT-1(Radford et al., 2018):“生成式预训练 + 判别式微调”。

  1. 无监督预训练:在 BooksCorpus(7000 多本未出版的书)上,用本讲的目标函数——最大化 ∑tlog⁡p(wt∣wt−k,…,wt−1)\sum_t \log p(w_t \mid w_{t-k}, \dots, w_{t-1})——训练一个 12 层的 Transformer 解码器(第 2 讲 §4.7 数过它的参数:约 1.17 亿)。
  2. 有监督微调:对每个下游任务(文本分类、蕴含判断、相似度、多项选择问答),把输入改写成一个 token 序列(例如蕴含任务把前提和假设用一个特殊的分隔符 token 连起来),在最后一个 token 的表示上加一个线性分类层,用少量标注数据微调整个网络。微调时还把语言模型的损失作为辅助目标加进去。

关键在于:模型结构几乎不随任务改变,预训练学到的知识被所有任务共享。GPT-1 在论文测试的 12 个任务里有 9 个超过了当时专门设计的模型。

GPT-2(Radford et al., 2019):“语言模型是无监督的多任务学习者”。

  • 在 WebText(约 800 万个网页、40 GB 文本,取自 Reddit 上获得较多赞的外链)上训练,最大的模型 15 亿参数(第 1 节的表)。
  • 核心主张:如果语言模型足够大、训练数据足够多样,它在预测下一个词的过程中就会学会很多任务,因为这些任务的”示范”自然地出现在文本里。于是可以不做任何微调(零样本),只用合适的提示让模型”续写”出答案:例如在文章后面加上 “TL;DR:” 让它写摘要,或者用 “英语句子 = 法语句子” 的格式让它翻译。
  • 零样本的成绩在很多任务上还远不如专门的模型,但随着模型变大稳定提升——这为 GPT-3 的”上下文学习”(第 11 讲)埋下了伏笔。

GPT-2 的论文也用到了本讲的两个技术:字节级 BPE(§5.5)和 top-k 采样(§6.5,论文生成样例时用的是 k = 40)。


8. 小结 · Summary

  1. 深度语言模型的训练目标和 n-gram 一样是训练集的对数似然 J(θ)=∑ilog⁡pθ(w(i))J(\theta) = \sum_i \log p_\theta(w^{(i)}),按链式法则拆成每个 token 的交叉熵;没有闭式解,用自动微分 + 小批量 SGD 优化。同一个模型族时,梯度下降会逼近数数的解。START/END 让模型成为任意长度句子上的一个分布。
  2. 小批量让梯度噪声按约 1/B1/B 下降,并让每个样本的计算更便宜;批大小是准确度和每步成本之间的权衡。
  3. 批处理:截断到块大小、填充到等长、填充位置不计损失;因果模型右填充是免费的,左填充必须同时屏蔽填充的键并修正位置编号;分桶减少填充,打包消除填充但会跨越文档边界。
  4. KV 缓存:kj,vjk_j, v_j 只依赖前 jj 个 token,算一次存起来;查询、分数和权重用完即丢。生成 NN 个 token 的计算从 O(N2)O(N^2) 降到 O(N)O(N)(注意力项除外),代价是内存随长度线性增长——长上下文时可以超过模型权重。
  5. 分词:以词为单位有 OOV、词表大;以字符为单位序列长;子词(BPE)是折中:从字符开始反复合并最常见的相邻对。BPE 能学到常见的词缀,但依据只是频次。字节级 BPE 以 256 个字节为基础,没有 OOV。
  6. 解码:贪心和束搜索找模型概率最高的句子,平淡、缺乏多样性,并会把模型的小偏差放大;长文本里贪心会复读。祖先采样是精确的采样方法,但会采到长尾里的错误;温度、top-k、top-p 在质量和多样性之间调节。
  7. GPT-1 证明了”生成式预训练 + 少量微调”的威力;GPT-2 进一步提出,足够大的语言模型可以零样本完成很多任务。

9. 常见误区 · Common pitfalls

  1. “训练时填充的位置也算损失没关系。” 会严重扭曲损失(本讲的例子从 0.93 变成 5.10),还会让模型去学预测 <pad>。
  2. “左填充和右填充一样。” 对因果模型,左填充必须配合注意力掩码和位置编号,否则结果是错的(误差达到 11)。
  3. “KV 缓存也要缓存 Q。” 旧的查询不会再被用到;只有 K 和 V 会被之后的每一步重复使用。
  4. “KV 缓存只是一个小优化。” 它把生成的计算从平方降到线性,而且它的大小决定了推理时能放多少并发请求、能支持多长的上下文。
  5. “束搜索总比贪心好,束越宽越好。” 束搜索只是更好地找到模型的众数;模型的众数可能很平淡(甚至是空句子),而且会放大模型的偏差。开放式生成通常用采样,而不是搜索。
  6. “温度越低越好 / 越高越有创意。” 低温牺牲多样性,高温产生大量不可能的输出;没有一个温度在所有指标上最好。
  7. “BPE 切出来的就是语素。” BPE 只看频次,经常切出 “sof test” 这样不符合语言学的片段。
  8. “字符级分词可以处理任何语言。” 只能处理训练时见过的字符;字节级 BPE 才真正没有 OOV。
  9. “小批量梯度就是真实梯度。” 它只是一个有噪声的估计,噪声随 1/B1/B 下降;B=1 时和全量梯度的余弦相似度只有 0.58。
  10. “去掉 END 也能定义句子的分布。” 那样定义的是每个长度各一个的一族分布,模型不知道何时停止。

10. 练习 · Exercises

练习 1(截断与填充)。 slides 的 8 个句子长度分别是 10、10、12、6、11、9、11、8。(a) 块大小为 10 时,一共截掉多少个词?填充多少个 <PAD>?(b) 块大小改成 8 呢?填充占全部位置的比例各是多少?

参考答案

(a) 截掉 2+1+1=42 + 1 + 1 = 4 个词(of times、know、play),填充 4+1+2=74 + 1 + 2 = 7 个,占 7/80=8.75%7/80 = 8.75\%。 (b) 截掉 2+2+4+3+1+3=152 + 2 + 4 + 3 + 1 + 3 = 15 个词,填充只有 2 个(长度 6 的那句),占 2/64=3.1%2/64 = 3.1\%。块越小,填充越少,但被截掉、模型永远看不到的词越多。

练习 2(按 token 平均 vs 按句子平均)。 一个批里有两句话:A 有 2 个预测位置,每个位置的交叉熵都是 2.0;B 有 8 个位置,每个都是 1.0。分别求按 token 平均和按句子平均的损失。哪一个和”整个训练集的对数似然”成正比?

参考答案

按 token:(2×2.0+8×1.0)/10=1.2(2 \times 2.0 + 8 \times 1.0)/10 = 1.2。按句子:(2.0+1.0)/2=1.5(2.0 + 1.0)/2 = 1.5。训练集的对数似然是对所有 token 的对数概率求和,所以按 token 平均和它成正比(差一个常数因子:总 token 数)。按句子平均相当于给短句子更大的权重。

练习 3(KV 缓存的大小)。 Llama-2 7B:32 层、32 个头、每头 128 维,用普通多头注意力,FP16 存储。(a) 一个 4096 token 的序列,KV 缓存多大?(b) 批大小 16 呢?(c) 如果改用 8 个 KV 头的 GQA(第 4 讲),(a) 变成多少?

参考答案

(a) 2×32×32×128×4096×22 \times 32 \times 32 \times 128 \times 4096 \times 2 字节 = 2 GiB。(b) 32 GiB——比模型权重(约 13 GB)还大得多,这就是推理时并发数受限于显存的原因。(c) KV 头数减为 1/4,缓存也变为 0.5 GiB。

练习 4(缓存省了多少)。 只看线性层(每个 token 每层 24d224d^2 FLOPs),从空提示开始生成 NN 个 token。不带缓存时,第 tt 步要处理 tt 个 token;带缓存时每步只处理 1 个。求两者总 FLOPs 之比。N=1000N = 1000 时是多少?

参考答案

无缓存:∑t=1N24td2L=12N(N+1)d2L\sum_{t=1}^N 24 t d^2 L = 12 N(N+1) d^2 L;有缓存:24Nd2L24 N d^2 L。比值 (N+1)/2(N+1)/2,N=1000N = 1000 时约 500 倍。(这是只看线性层的上界估计;注意力项和固定开销会让实测倍数小一些,见 §4.4。)

练习 5(手工 BPE)。 Sennrich et al. (2016) 的例子:语料里 low 出现 5 次、lower 2 次、newest 6 次、widest 3 次。(a) 写出前 5 次合并(并列时取字典序最小的一对)。(b) 用学到的前 5 次合并,编码 lowest。

参考答案

(a) 第 1 次:(e, s) 出现 6 + 3 = 9 次 → es;第 2 次:(es, t) 9 次 → est;第 3 次:(est, </w>) 9 次 → est</w>;第 4 次:(l, o) 5 + 2 = 7 次 → lo;第 5 次:(lo, w) 7 次 → low。(bpe.BPE.train 得到的前 5 次合并完全相同;之后的第 6 次是 (e, w) 和 (n, e) 并列 6 次,按字典序取 (e, w)。) (b) l o w e s t </w> → 先合并 e s → es,再 es t → est,再 est </w> → est</w>,再 l o → lo,再 lo w → low:得到 [low, est</w>]——虽然 lowest 从没在语料里出现过。

练习 6(温度)。 logits z=[2,1,0]z = [2, 1, 0]。求 τ=1\tau = 1、0.50.5、22 时的分布。

参考答案

τ=1\tau = 1:[0.665, 0.245, 0.090]。τ=0.5\tau = 0.5(相当于 logits 乘 2):[0.867, 0.117, 0.016],更尖。τ=2\tau = 2:[0.506, 0.307, 0.186],更平。τ→0\tau \to 0 时趋于 [1, 0, 0](贪心),τ→∞\tau \to \infty 时趋于均匀分布。

练习 7(top-k 与 top-p)。 某一步的分布是 [0.5, 0.2, 0.15, 0.1, 0.05]。分别求 top-k(k=2)、top-p(p=0.8)、top-p(p=0.5)之后重新归一化的分布。

参考答案

top-k=2:保留 0.5 和 0.2,归一化为 [0.714, 0.286, 0, 0, 0]。top-p=0.8:按概率从大到小累加,0.5、0.7、0.85——加上第三个时首次达到 0.8,保留前三个,归一化为 [0.588, 0.235, 0.176, 0, 0]。top-p=0.5:第一个就达到 0.5,只保留它,等于贪心。(和 decoding.filter_logits 的输出一致。)

练习 8(即使模型完全准确,贪心也会失败)。 用真实分布对 “the dog” 做贪心解码。已知:第一步 chases 0.1875 是最大的(每个介词 0.1833);chase 之后是 “the” 再选猎物,最大的是 cat(4/9 × 1/2 = 0.222,cats 并列);时间短语的第一个词,“in”(in the morning 3/9 + in the field 2/9)比 “every”(every day 4/9)大;“in the” 之后 morning(3/5)比 field 大。求贪心得到的句子及其条件概率,并和真实众数 “the dog eats bones every day .” 的概率比较。

参考答案

贪心得到 “the dog chases the cat in the morning .”,条件概率 0.45×512×49×12×39=0.013890.45 \times \frac{5}{12} \times \frac49 \times \frac12 \times \frac39 = 0.01389。真实众数的概率 0.45×312×67×49=0.042860.45 \times \frac{3}{12} \times \frac67 \times \frac49 = 0.04286,是贪心结果的约 3.1 倍。贪心在两个地方被”摊薄”骗了:chases 之后要在猎物和单复数之间分;“in” 之后要在 morning 和 field 之间分。(farm_true_logprob 算出的两个概率与此相同。)

练习 9(END 那一列)。 一个只在 {a, b} 上预测、从不预测 END 的模型,所有长度为 3 的句子的概率之和是多少?长度 0 到 3 的所有句子呢?为什么这说明它不是”句子上的分布”?

参考答案

长度为 3 的 8 个句子的概率之和是 1(每一步都在 a、b 之间归一化)。长度 0 到 3 的所有句子加起来是 4。一个概率分布在所有结果上的总和必须是 1;这里每个长度单独加起来就是 1,所以模型实际上为每个长度定义了一个分布,不能比较不同长度的句子,也不能决定何时停止。

练习 10(小批量的噪声)。 §1.3 的表里,B=1B = 1 时 E∥gB−g∥2=25.018\mathbb{E}\lVert g_B - g \rVert^2 = 25.018。如果误差严格按 1/B1/B 下降,B=64B = 64 时应该是多少?实测是 0.362,为什么不完全相等?

参考答案

25.018/64=0.39125.018 / 64 = 0.391。实测 0.362,接近但不相等:每种批大小只抽了 16 个小批量,估计本身有波动;而且句子长度不同、损失按 token 平均,各个样本对梯度的权重不完全相等,严格的 1/B1/B 关系只对”独立同分布、等权重的样本平均”成立。

练习 11(字节与 token)。 “生成式” 三个字,UTF-8 是多少字节?在一个完全没有中文合并规则的字节级 BPE 里,它会被切成几个 token?以字符为单位呢?

参考答案

9 个字节(每个汉字 3 个字节)。没有中文的合并规则时,就是 9 个 token;以字符为单位是 3 个。真实的多语言分词器会学到常见汉字的合并,把很多汉字合成 1–2 个 token,但中文的”每个字几个 token”通常仍高于英文的”每个词几个 token”。


11. 延伸阅读 · Further reading

  • 语言模型与生成:Mikolov et al. (2010),RNN 语言模型;Graves (2014),用 LSTM 逐字符生成文本和手写轨迹——“在长序列上训练、一步一步采样”的早期经典。
  • GPT 系列:Radford et al. (2018) GPT-1;Radford et al. (2019) GPT-2;Brown et al. (2020) GPT-3(附录 D 有训练计算量的表)。
  • 分词:Sennrich, Haddow & Birch (2016), Neural Machine Translation of Rare Words with Subword Units(BPE);Kudo & Richardson (2018), SentencePiece;Wu et al. (2016) 的 Google 神经机器翻译系统(WordPiece)。
  • 解码:Holtzman et al. (2020), The Curious Case of Neural Text Degeneration(top-p、退化);Fan, Lewis & Dauphin (2018), Hierarchical Neural Story Generation(top-k);Stahlberg & Byrne (2019), On NMT Search Errors and Model Errors: Cat Got Your Tongue?(精确众数常是空译文);Meister, Cotterell & Vieira (2020), If beam search is the answer, what was the question?。
  • KV 缓存与推理系统:Pope et al. (2022), Efficiently Scaling Transformer Inference;Kwon et al. (2023), Efficient Memory Management for Large Language Model Serving with PagedAttention(vLLM)。
  • 打包与文档掩码:Llama 3 技术报告(Dubey et al., 2024)中关于文档内注意力掩码的描述。

12. 下一讲预告 · Next lecture

第 4 讲会做两件事:

  • 预训练 vs 微调:从 2006 年的逐层无监督预训练、自编码器,到 GPT 式的生成式预训练;“预训练 → 中期训练 → 后训练”的现代流程;我们会在农场语言上做一个”预训练 + 微调 vs 从零训练”的对比实验。
  • 现代 Transformer:2017 年之后,开源模型(Llama、Mistral、OLMo、Gemma……)把 Transformer 改了哪些地方——RMSNorm、SwiGLU,以及三个重点:旋转位置编码 RoPE(让注意力分数只依赖相对位置)、分组查询注意力 GQA(缩小本讲的 KV 缓存)、滑动窗口注意力(让每个 token 只看最近的 ww 个 token)。
← 返回课程目录