对应 Maps to:CMU 10-423 / 10-623 / 10-723 Generative AI(Spring 2026;Aran Nayebi、Matt Gormley),Lecture 4 · Mon 26-Jan · Pre-training, fine-tuning / Modern Transformers(slides 标题:Pretraining vs. finetuning + Modern Transformers (RoPE, GQA, Longformer))。 阅读材料:Ainslie et al. (2023), GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints;Beltagy, Peters & Cohan (2020), Longformer: The Long-Document Transformer;Su et al. (2021), RoFormer: Enhanced Transformer with Rotary Position Embedding。 依据说明:本讲以原课第 4 讲公开的 slides(lecture4-rope-gqa.pdf)为主线:深度学习的起点与 2006 年的预训练、预训练与微调的定义和例子、MNIST 上的预训练、自编码器、视觉的有监督预训练、LLM 的生成式预训练与训练数据、预训练 → 中期训练 → 后训练的流程、现代开源模型的配方清单、RoPE(包括被标了 “wrong” 的那一版公式、两个思考题、高效实现和矩阵版)、GQA、滑动窗口注意力的三种实现。模型清单里的细节以 slides 为准,个别地方对照了原始论文。全部实验是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:
code/lecture04/(5 个脚本 + 2 个模块;只需要 PyTorch CPU 版;021 秒,其余 50–70 秒;05 --seeds 3约 2 分钟) 预计阅读时间:4 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说出预训练和微调的定义(两种预训练:无监督 / 有监督),举出视觉和语言里的例子;解释”先在便宜的大数据上预训练、再在昂贵的小数据上微调”为什么有效,以及全量微调可能带来的问题。
- 描述现代 LLM 的训练流程:预训练 → 中期训练 → 后训练。
- 列出 2017 年之后开源 Transformer 的常见改动:RMSNorm、SwiGLU、去掉偏置、RoPE、GQA/MQA、滑动窗口……
- 推导 RoPE:为什么按绝对位置旋转 、 会得到只依赖相对位置的注意力分数;回答 slides 的两个问题(为什么切成二维小块、为什么每块转不同的角度);会写高效实现。
- 写出 GQA 的公式,说明 MHA 和 MQA 是它的两个特例;会计算 KV 缓存省了多少,知道怎样把 MHA 模型”改装”成 GQA。
- 画出滑动窗口注意力的掩码,比较三种实现的代价,计算多层叠加后的感受野,解释 Mistral 的滚动缓存。
- 用实验说明:相对位置编码并非在所有任务上都更好。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 第 2 讲:注意力、多头、位置编码、GPT 的结构 | 指令微调、RLHF、DPO(第 11–12 讲) |
| 第 3 讲:KV 缓存、批处理 | 参数高效微调 LoRA(第 10 讲) |
| 二维旋转矩阵 | 长上下文外推(位置插值、YaRN,第 19 讲) |
| 前置讲 P1 §5 的参数公式 | ViT 的有监督预训练(第 5 讲);混合专家 MoE(第 16 讲) |
0.3 本讲地图 Map
§1 回顾
§2 预训练 vs 微调:2006 年的火花;定义与例子;自编码器;LLM 的预训练与训练流程;实验 ← 代码 01
§3 现代 Transformer:开源模型的配方清单;RMSNorm、SwiGLU、去掉偏置
§4 旋转位置编码 RoPE ← 代码 02
§5 分组查询注意力 GQA ← 代码 03
§6 滑动窗口注意力 ← 代码 04
§7 拼起来:HW1 式的现代 TransformerLM;位置编码与长度外推的再讨论 ← 代码 05
§8–12 小结、误区、练习、延伸阅读、下一讲
1. 回顾 · Recap
slides 把前三讲总结为两条线:
- 深度学习:自动微分以模块(
forward()和backward())为构件;计算图是定义函数的另一种方式;我们见过 RNN-LM 和 Transformer-LM 两张很深的计算图。 - 语言建模:以前面的词为条件采样下一个词;n-gram 用巨大的骰子表,RNN-LM / Transformer-LM 用神经网络;n-gram 靠数数学习,神经语言模型通过 SGD 优化目标函数、用自动微分算梯度(第 3 讲)。
这一讲回答两个新问题:(1) 训练好的语言模型怎样用到别的任务上——预训练与微调;(2) 2017 年的 Transformer 在今天的开源模型里变成了什么样子。
2. 预训练 vs 微调 · Pre-training vs fine-tuning
2.1 深度学习的火花来自预训练
slides 指出,现代深度学习的结构其实早就有了:
- 1960 年代:Rosenblatt 的三层感知机,ReLU;
- 1970–80 年代:RNN 和 CNN;
- 1990 年代:线性化的自注意力。
但”深度”网络在 2006 年之前很难训练(第 2 讲讲过的梯度消失是原因之一)。火花来自 2006 年的预训练(Hinton & Salakhutdinov, 2006):先用无监督的方式一层一层地训练网络,再在有标注的数据上整体训练。
2.2 定义
slides 的定义:
- 预训练:随机初始化参数,然后——
- 选项 A:在非常大的无标注数据集上做无监督训练;
- 选项 B:在非常大的有标注数据集上做有监督训练。
- 微调:用预训练得到的值初始化参数;(可选地)加一个只有少量随机初始化参数的预测头;在感兴趣的具体任务上用反向传播训练。
| 视觉 | 语言 | |
|---|---|---|
| 预训练 | A:在大量无标注图像(如 MNIST)上训练自编码器;B:在大型图像分类数据集上有监督训练(如 ImageNet-21k:2.1 万类、1400 万张图) | 在大量无标注句子上最大化似然:The Pile(800 GB 文本)、Dolma(3 万亿 token) |
| 微调 | 目标检测:在 COCO 的 20 万张标注图上训练;语义分割:在 ADE20k 的 2 万张标注图上训练 | MMLU:57 个任务(从小学数学到遗传学、法律)里各取几个例子;代码生成:MBPP 的约 400 个训练例子 |
模式很清楚:预训练用便宜、海量的数据,微调用昂贵、少量的数据。
2.3 MNIST 上的预训练
slides 展示了 Bengio et al. (2006) 在 MNIST 手写数字分类上的结果(错误率越低越好),比较了四种做法:浅层网络、不预训练的深层网络、有监督逐层预训练的深层网络、无监督逐层预训练的深层网络——每种做法最后都在有标注的数据上微调。结论是无监督预训练的深层网络错误率最低:同样深的网络,先预训练再微调,比直接训练好。
2.4 自编码器:无监督预训练”第一层”该预测什么?
slides 问:“无监督地预训练第一层,它应该预测什么?我们还观察到了什么?“答案是:输入本身。
- 编码器:;解码器:;
- 损失:重构误差 ,用两层神经网络同样的反向传播训练, 既是输入又是输出。
如果隐藏层 比输入小,网络就被迫学一种压缩的表示。逐层预训练就是:训练好第一层的自编码器后,把它的 当作下一层的输入,再训练下一层,依此类推;最后把所有编码器叠起来,加上分类层,整体微调。(第 9 讲的变分自编码器会把自编码器变成一个生成模型。)
2.5 有监督预训练(视觉)
slides:“现在我们倾向于直接在一个巨大的有标注数据集上做有监督预训练。“Vision Transformer 的成功在很大程度上归功于更大的预训练数据集——第 5 讲会用实验看到这一点。
2.6 LLM 的无监督预训练
对深度语言模型来说,“无监督预训练”就是第 3 讲的生成式预训练:每个训练样本是一个(无标注的)句子,目标函数是观察到的句子的似然;实践中把很多样本组成批来提高效率。
slides 给出了两个训练数据的例子:
- GPT-3 的训练数据:以过滤后的 Common Crawl 为主,再加上 WebText2、两个图书语料、维基百科,按质量给不同的采样权重(第 3 讲 §3.1)。
- The Pile:一个开源的语言模型训练集,由 22 个较小的数据集组成,偏重高质量文本,约 825 GB。
2.7 LLM 的训练流程
slides 把今天的 LLM 训练分成三个阶段:
| 阶段 | 做什么 | 例子 |
|---|---|---|
| 预训练 | 在大量无标注句子上最大化似然 | The Pile(800 GB)、Dolma(3 万亿 token) |
| 中期训练(mid-training) | 和预训练一样,但换成更高质量的数据 | Dolmino Mix 1124(1000 亿–3000 亿 token) |
| 后训练(post-training) | 以下的某种组合:指令微调;RLHF / DPO;可验证奖励的强化学习(RLVR);安全微调;任务相关的微调。通常只用少量有监督数据 | 第 11、12、20 讲 |
2.8 实验:预训练 + 微调 vs 从零训练
01_pretrain_finetune.py 在农场语言上设计了一个下游任务:给定句子开头到动词之前的部分(如 “the dogs near the cat behind the cow”),判断主语是单数还是复数。测试集里干扰名词全部和主语相反,“看最后一个名词”这条捷径在测试集上的准确率是 0。
预训练模型就是第 2 讲在 2 万句无标注文本上训练的 GPT。比较几种做法(3 个随机种子的平均;k = 4–6 是比训练句子更长的”外推”测试):
| 做法 | 标注样本数 N | k = 1–3 | k = 4–6 |
|---|---|---|---|
| 零样本(只用语言模型:比较复数动词和单数动词的总概率) | 0 | 1.000 | 1.000 |
| 从零训练 | 16 | 0.461 | 0.374 |
| 从零训练 | 256 | 0.982 | 0.587 |
| 线性探针(冻结预训练模型,只训练分类头) | 16 | 1.000 | 0.999 |
| 全量微调,学习率 1e-3 | 16 | 0.910 | 0.842 |
| 全量微调,学习率 1e-3 | 256 | 1.000 | 0.932 |
| 全量微调,学习率 1e-4 | 16 | 0.931 | 0.868 |
- 从零训练:16 个样本只有 0.461(比乱猜还差);256 个样本能到 0.982,但外推到更长的句子只有 0.587。几百个样本不够让模型自己发现”主语是第一个名词、要跳过介词短语”这条规则。
- 用预训练模型:16 个样本、只训练分类头就全对。预训练时模型为了预测动词的形式,已经学会了在动词前的位置表示主语的单复数(第 2 讲 §6.4 的干预实验);标注数据只需要教它”把这个信息读出来”。
- 全量微调反而不如线性探针:16 个样本、学习率 1e-3 时只有 0.910,外推也更差。用很少的样本更新十万个参数,会破坏预训练学到的表示(过拟合 / 灾难性遗忘)。把学习率降到 1e-4 只略有改善。在这个任务上,最保守的做法——只训练分类头——反而最好。这是第 10 讲参数高效微调(LoRA 等只训练很少的参数)的动机之一。
- 零样本也是 1.000:语言模型本身就会做这个任务,只是要换一种问法——让它预测下一个词(GPT-2 的思路,第 3 讲 §7)。
这个例子里”预训练数据”和下游任务高度相关,所以效果特别夸张。真实情况下两者的距离更远,但道理相同:便宜的无标注数据教会模型通用的表示,昂贵的标注数据只用来做最后一步。
3. 现代 Transformer 模型 · Modern Transformer models
3.1 开源模型的配方清单
slides 列了一张很长的清单(按时间顺序,节选):
| 模型 | 时间 | 相对于前代 / GPT 的主要改动(以 slides 为准) |
|---|---|---|
| PaLM | 2022.10 | 5400 亿参数,闭源;SwiGLU 代替 ReLU/GELU;多查询注意力(MQA);输入输出嵌入共享 |
| Llama-1 | 2023.02 | 半开源;13B 平均超过 GPT-3;输入端的 RMSNorm(代替输出端的 LayerNorm);SwiGLU;RoPE 代替绝对位置;AdamW,1.0–1.4 万亿 token |
| Falcon | 2023.06–11 | 7B、40B、180B,首个 Apache 2.0 的完全开源模型;多查询 / 分组查询注意力代替 MHA(slides 这一行写得有些混乱:7B 用 MQA,更大的版本用分组的变体);RoPE(比 ALiBi 好);GELU;最多 3.5 万亿 token,z-loss |
| Llama-2 | 2023.08 | 7B、13B、70B;推出 Llama 2-Chat;GQA(论文里只有 34B、70B 用了 GQA);上下文 4096;2 万亿 token |
| Mistral | 2023.10 | 7B,Apache 2.0;平均超过 Llama-2 13B;GQA;滑动窗口注意力 + 滚动缓存 |
| OLMo | 2024.02 | 1B、7B,完全开放(权重、数据、训练代码);无偏置;无参数的 LayerNorm;SwiGLU;RoPE;QKV 截断 |
| Gemma | 2024.02 | 2B、7B,开放权重;多查询注意力;RoPE;GeGLU、RMSNorm;3–6 万亿 token |
| Gemma 2 | 2024.06 | GQA、RoPE;pre-norm 和 post-norm 都用 RMSNorm;QK-Norm;局部滑动窗口和全局注意力交替 |
| Llama 3 / 3.1 | 2024.04 | 8B、70B、405B;GQA(更少的 KV 头);文档内掩码;405B 用了 15.6 万亿 token |
| OLMo 2 | 2024.12–2025.03 | RMSNorm、重新排列归一化的位置、QK-norm;RoPE 但 base 大得多;z-loss,4–6 万亿 token |
| DeepSeek-R1 | 2025.01 | MIT 开源;R1-Zero 纯强化学习,R1 是 SFT + RL(第 20 讲) |
| Gemma 3、Llama 4、Qwen 3 | 2025.03–05 | 量化感知训练(Gemma 3)等 |
| gpt-oss | 2025.08 | 20B、120B;RoPE、GQA、SwiGLU;混合专家(MoE)(第 16 讲);预训练用了 210 万 H100 小时 |
| Olmo 3 | 2025.12 | 7B、32B,完全开放;7B 用 MHA、32B 用 GQA;RoPE;滑动窗口注意力 |
slides 还开了个玩笑:“要是 LLM 能帮我自动更新这张清单就好了。”——“你会这么想,但是……”(它们会编造不存在的模型细节。)
几个反复出现的改动:
- RMSNorm 代替 LayerNorm,并放在子层的输入端(pre-norm);
- SwiGLU / GeGLU 门控前馈网络代替 ReLU / GELU;
- 去掉线性层的偏置;
- RoPE 代替绝对位置嵌入;
- GQA / MQA 代替普通多头注意力;
- 滑动窗口注意力(常和全局注意力交替);
- 训练稳定性技巧:z-loss、QK-norm 等。
slides 重点讲其中三个:RoPE、GQA、滑动窗口注意力(§4–6)。前三个在 modern_lm.py 里都有实现,这里简要说明。
3.2 RMSNorm、SwiGLU、去掉偏置
RMSNorm(Zhang & Sennrich, 2019):。和 LayerNorm(第 2 讲 §4.3)相比,不减均值、没有偏置 ,只按均方根缩放;少一次求均值、少一组参数,效果相当(练习 9)。
SwiGLU(Shazeer, 2020):。一路算”内容”,一路算”门开多大”,逐元素相乘。比 GELU 前馈多一个矩阵;为了参数量相当,中间维度取约 (练习 8)。
去掉偏置:实践中偏置几乎不影响效果,去掉后参数更少、训练更稳定(PaLM 的报告)。
4. 旋转位置编码 RoPE · Rotary position embeddings
4.1 动机:相对位置
第 2 讲的位置编码都是绝对的:把”我在第 个位置”的向量加到输入上。但语言里大多数关系是相对的:“形容词在名词前一个位置”、“动词离它的主语 3 个词”,不管这句话出现在文档的开头还是第 1000 个 token。我们希望注意力分数 只依赖内容和相对距离 。
RoPE(Su et al., 2021)用一个漂亮的办法做到了这一点:按每个 token 的绝对位置旋转它的 和 。
4.2 slides 的定义
标准注意力:,,。RoPE 注意力:
其中对某个固定的绝对位置 ,旋转矩阵是块对角的:
也就是说:把 维向量切成 个二维小块,第 个小块旋转 弧度, 是这个查询或键的绝对位置(slides 的图:Key idea)。02_rope.py 第 1 部分打印了 的左上角,并验证三种写法(完整矩阵、slides 的逐元素写法、矩阵版)给出同一个结果。
4.3 slides 上被标了 “wrong” 的那一版
slides 先放了一版公式,上面被红笔标了好几处 “wrong”,并自嘲:“为什么这张 slide 有这么多错误?我真的不知道,我都是自己仔细敲的 LaTeX。“那一版的意思大致是: 和 都旋转 ,其中 。
这为什么错?02 第 3 部分真的按它算了一遍:
| 版本 | 和”不加位置信息”的分数的 max|差| |
|---|---|
| 错误版:、 都旋转 | |
| 正确版: 旋转 , 旋转 | 4.57 |
两边转同一个角度,旋转在点积里完全抵消(),模型什么位置信息都得不到。正确的做法是每个向量按自己的位置旋转——相对位置会在点积里”自己冒出来”。
4.4 为什么只依赖相对位置
旋转矩阵满足 (转 再倒转 等于转 )。于是
绝对位置在相乘时抵消了,只剩相对距离 。02 第 2 部分把同一对 、 放在 (5, 2)、(105, 102)、(1005, 1002) 三组位置上,点积都是 +1.685028392114;矩阵恒等式的误差 。
和第 2 讲的正弦位置编码比较:正弦编码加在输入上, 只依赖 ,但经过 、 的线性变换之后这个性质就不严格了;RoPE 直接作用在每一层的 、 上,相对性在每一层都严格成立。
4.5 slides 的两个问题
问题 1:把向量切成二维小块再旋转,有什么好处?
- 旋转不改变向量的长度(
02第 4 部分:差 ),所以位置信息只改变 、 的方向,不会放大或缩小内容; - 两个旋转过的二维向量的点积只取决于它们转过的角度之差——这正是相对位置的来源;
- 块对角矩阵乘向量只需要 次运算(§4.6),而不是 。
问题 2:为什么不同的小块转不同的角度?
如果所有块都用同一个转速 ,“相隔 的两个位置有多像”就是 ,以 为周期反复出现,模型分不清这些距离。02 第 4 部分():
| 距离 | 所有块都用 | RoPE: |
|---|---|---|
| 0 | +1.000 | +1.000 |
| 1 | +0.540 | +0.966 |
| 44 | +1.000 | +0.598 |
| 355 | −1.000 | +0.225 |
| 4000 | −0.730 | +0.002 |
同一转速时,(约 7 圈)和 一模一样。RoPE 让各块的转速按几何级数从 1 降到约 1/10000:最快的一对每个位置转 1 弧度(周期 6.3 个位置),最慢的一对周期约 4.7 万个位置。就像时钟的秒针、分针、时针,合起来才能在很大的范围内唯一地表示位置。相似度随距离整体变小,RoFormer 论文称之为长程衰减:离得越远的 token,在不考虑内容时得到的分数越小。
4.6 高效实现
slides:“因为 的块稀疏结构,可以更高效地计算它和任意向量 的乘积”:
矩阵版(一次处理所有位置):构造 的角度矩阵 (第 行是 ),则
(slides 对这一页的自评是”我也不确定它对不对,但这是我自己写的”。)注意矩阵版把第 维和第 维配成一对,而逐元素版把相邻两维配成一对——这正是 Llama / GPT-NeoX 的写法和 RoFormer 论文写法的区别。两者只是约定不同,重排维度后完全等价(02 第 1 部分),但加载预训练权重时必须用训练时的约定。
4.7 RoPE 与 KV 缓存
RoPE 按每个 token 自己的绝对位置旋转 ,旋转后的 存进缓存,以后再也不用改;新 token 来了只需按它自己的位置旋转它的 和 。所以 RoPE 和 KV 缓存天然兼容(02 第 5 部分:逐个解码和整段前向差 )。RoPE 没有位置嵌入表,也就没有 max_len 这个硬上限;但模型只在训练长度内见过这些旋转角度——§7.3 会看到这一点的后果。
5. 分组查询注意力 GQA · Grouped-query attention
5.1 动机:KV 缓存
slides 的说法:Transformer 的计算和内存都很密集。第 3 讲 §4.5 看到,推理时每个序列都要为每一层保存所有 token 的 K 和 V,长上下文时缓存比模型还大;而且解码每一步都要把整个缓存从显存读一遍(前置讲 P3 §6.2–6.3:访存受限)。缓存的大小和 KV 头数成正比——能不能少用几个 KV 头?
5.2 定义
GQA(Ainslie et al., 2023)的关键想法:多个查询头共用同一组键头和值头。
- = 查询头数, = 键/值头数,假设 能被 整除; 是每组的大小(每个 KV 向量配几个查询向量)。
- slides:“参数矩阵的大小都一样,只是键/值参数矩阵(头)比查询的少。”
- 两个极端: 就是普通的多头注意力(MHA); 就是多查询注意力(MQA,Shazeer, 2019)。
03_gqa.py 第 1 部分验证:GQA 和”把每个 K、V 头复制 份的 MHA”输出逐位相同。注意力层的参数只省了一点(、8 个查询头:MHA 4096 → MQA 2304)——GQA 真正省的是推理时的缓存。
5.3 省了多少:Llama-3 8B
Llama-3 8B:32 层、32 个查询头、每头 128 维、BF16 权重 15.0 GiB。03 第 2 部分算了批大小 32、每个序列 8192 个 token 时的情况(H100:80 GB、3.35 TB/s):
| 结构 | 每个 token 的缓存 | 整批的缓存 | 权重 + 缓存放得进一块 H100? | 解码一步至少(只算读显存) | 每秒最多生成的 token |
|---|---|---|---|---|---|
| MHA() | 512 KiB | 128.0 GiB | 不能 | 45.8 ms | 698 |
| GQA(,Llama-3 实际用的) | 128 KiB | 32.0 GiB | 能 | 15.1 ms | 2,126 |
| MQA() | 16 KiB | 4.0 GiB | 能 | 6.1 ms | 5,266 |
用 MHA 的话,这一批的缓存连同权重根本放不进一块 H100;GQA 把缓存缩小到 1/4,每一步也快了约 3 倍。MQA 还能再缩小 8 倍,但 Ainslie et al. (2023) 发现它的质量下降更明显、训练也更不稳定;GQA 是两者之间的折中。
5.4 质量与”改装”
03 第 3 部分在农场语言上训练了三个 ModernLM(4 个查询头,其余完全相同,5 个 epoch):
| 结构 | 参数量 | 每个 token 的 KV 缓存 | 测试集困惑度 |
|---|---|---|---|
| MHA() | 107,840 | 1024 B | 2.5255 |
| GQA() | 99,648 | 512 B | 2.5262 |
| MQA() | 95,552 | 256 B | 2.5290 |
在这个简单任务上,代价几乎可以忽略。
**已经训练好的 MHA 模型能直接改成 GQA 吗?**Ainslie et al. (2023) 的做法(uptraining):把每组 K 头(V 同理)的投影矩阵取平均作为新的 K 头,再用原预训练计算量的一小部分继续训练。我们的结果:
| 模型 | 测试集困惑度 |
|---|---|
| 原来的 MHA | 2.5255 |
| 按组取平均,不继续训练 | 4.0066 |
| 再继续训练 1 个 epoch(原训练量的 1/5,学习率 3e-4) | 2.5017 |
| 原来的 MHA 也继续训练 1 个 epoch(公平的对照) | 2.5010 |
直接取平均损失很大(原来每个 K 头学到的东西不同),继续训练一小段就恢复了。2.5017 看起来比原来的 MHA 还好,但那是因为它多训练了一个 epoch、而且最后用了更小的学习率(相当于学习率退火);公平的对照(MHA 也多训练一个 epoch)是 2.5010,两者相当。这让已有的 MHA 模型也能享受 GQA 的推理效率。
6. 滑动窗口注意力 · Sliding window attention
6.1 动机与定义
slides:也叫局部注意力,在 Longformer(Beltagy et al., 2020)里提出。问题:普通注意力计算量大、需要很多内存( 的分数矩阵)。解决办法:用一个因果掩码,只看一个窗口内的 token——窗口里有 个 token,最右边的元素是当前 token(对角线上):
04_sliding_window.py 第 1 部分画出了 slides 里 (每个 token 看 3 个)和 (看 4 个)的掩码:每一行只剩对角线及其左边几个位置。(slides 的 是 Longformer 的双向窗口宽度;代码和 Mistral 一样直接用”能看的 token 数” 作参数。)
6.2 三种实现
slides 列出了三种实现方式:
- 朴素实现:照常做矩阵乘法、再加掩码——但这样仍然很慢;
- for 循环实现:渐近更快、更省内存,但在 PyTorch 里因为 for 循环太慢而无法实用;
- 滑动分块(sliding chunks):把 和 切成 的块,块之间重叠 ;在每个块内计算完整的注意力再加掩码(实践中又快又省内存)。
04 第 2 部分实现了三种,结果相同(差 )。分块的做法:把序列切成长度为 的块,第 块的查询只需要看第 块和第 块的键,每块算一个 的小矩阵,全部块一次批量算完。⏱ 本机单线程(,):
| 朴素:分数矩阵元素数 | 分块:分数矩阵元素数 | ⏱ 朴素 | ⏱ 分块 | ⏱ for 循环 | |
|---|---|---|---|---|---|
| 256 | 65,536 | 32,768 | 0.2 ms | 0.1 ms | 2 ms |
| 1,024 | 1,048,576 | 131,072 | 3.0 ms | 0.3 ms | 7 ms |
| 4,096 | 16,777,216 | 524,288 | 46.2 ms | 1.2 ms | (太慢,跳过) |
朴素实现省了”看太远”,没省计算;for 循环只算需要的分数,但每个查询一次小运算,开销太大(在 GPU 上会更糟);分块的计算量和内存都是 ,又能批量执行, 越大优势越明显。
6.3 多层叠加:感受野
每个 token 只看最近 个,模型还能用到更远的信息吗?能——间接地。04 第 3 部分()改动第 1 个 token,看第 层的输出最远影响到哪里:
| 层数 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|
| 最远受影响的距离 | 3 | 6 | 9 | 12 |
正好是 :每一层把信息往右传 个位置(和第 5 讲 CNN 的感受野随层数增长是同一个道理)。Mistral 7B 用 、32 层,理论感受野约 13 万个 token。但”间接依赖”不等于”能用好”——信息要经过很多次接力。
6.4 滚动缓存
slides 在 Mistral 那一条里提到:滚动缓冲区缓存(rolling buffer cache)——缓存长到窗口大小后,把位置 写到第 格,覆盖掉最旧的。窗口之外的 K、V 永远不会再被看到,扔掉也不影响结果:缓存大小从”随长度线性增长”变成固定的 (练习 3)。
04 第 4 部分验证:生成 40 个 token、每一步之后缓存只保留最近 个位置,结果和整段前向一致(差 )。能这样做的前提是 RoPE:缓存里的 K 已经按自己的绝对位置旋转过,放在缓冲区哪一格都没关系。但新 token 必须按它真正的绝对位置旋转,而不是按缓存的长度——缓存截短以后两者不再相等。代码的第一版就在这里出了错(差 ),修正后才逐位一致。
6.5 窗口太小会怎样
04 第 5 部分在农场语言上训练了不同窗口的 ModernLM(2 层)。主谓一致测试里,预测动词的位置离主语 个 token:
| 窗口 | 2 层的理论感受野 | 测试集困惑度 | (距离 3) | (距离 6) | (距离 9) |
|---|---|---|---|---|---|
| 不限 | 全部 | 2.5255 | 1.000 | 1.000 | 1.000 |
| 8 | 14 | 2.5317 | 1.000 | 1.000 | 1.000 |
| 4 | 6 | 2.6859 | 1.000 | 0.990 | 0.000 |
| 2 | 2 | 2.9047 | 0.560 | 0.495 | 0.485 |
窗口 4 时,距离 9 的主语超出了感受野,一致性掉到 0——模型只能跟着最近的名词走;窗口 2 时连距离 3 都够不着。滑动窗口是一种取舍:把注意力的代价从 降到 ,代价是放弃了”任意两个位置直接相连”(第 2 讲 §5 的长度为 1 的通路)。所以 Gemma 2/3、OLMo 3、gpt-oss 等把滑动窗口层和全局注意力层交替使用。
7. 拼起来:现代 TransformerLM · Putting it together
7.1 现代配方 vs GPT 配方
modern_lm.ModernLM 就是原课 HW1 要实现的”带 GQA 和 RoPE 的 TransformerLM”:
| 部件 | 第 2 讲的 GPT(GPT-2 式) | 现代开源模型(Llama 式) |
|---|---|---|
| 归一化 | LayerNorm(、) | RMSNorm(只有 ) |
| 归一化的位置 | GPT-1 post-norm;GPT-2 起 pre-norm | pre-norm(Gemma 2、OLMo 2 还在子层输出上再归一化一次) |
| 前馈网络 | GELU,2 个矩阵, | SwiGLU,3 个矩阵, |
| 位置编码 | 可学习的绝对位置嵌入(加在输入上) | RoPE(每层旋转 、) |
| 注意力 | MHA | GQA 或 MQA |
| 线性层偏置 | 有 | 无 |
| 接口 | logits, None = model(ids) | logits, cache = model(ids, cache) |
05_modern_transformer.py 第 1 部分用 前置讲 P1 的 param_count 公式核对:ModernLM(、2 层、4 个查询头、2 个 KV 头)PyTorch 数出 99,648,公式也是 99,648。Llama-3 8B 的 8.03B 参数里,前馈网络占 70.2%,注意力只占 16.7%(GQA 让 、 只有 的 1/4),128K 的词表让嵌入层和输出层合起来占约 13%。
7.2 和第 2 讲的 GPT 比
都是 、2 层、4 个查询头,训练 5 个 epoch:GPT 111,360 个参数、测试困惑度 2.5305;ModernLM 99,648 个参数、2.5262。参数更少、困惑度相当。在这么小的玩具模型上,现代配方的优势很有限;它们的价值主要体现在大模型上的训练稳定性、推理效率(GQA)和长上下文。
7.3 一个意外:RoPE 外推得最差
第 2 讲 §6.5 发现:超出训练长度时(主谓一致的 ),可学习位置嵌入和正弦编码不稳,不加位置编码反而最好。我原本预期 RoPE 会表现得像”不加位置编码”一样好——它只编码相对位置,没有”没训练过的位置向量”。05 第 3 部分(ModernLM,只换位置编码,3 个种子):
| 位置编码 | 种子 | 测试困惑度 | |||||
|---|---|---|---|---|---|---|---|
| RoPE | 0 | 2.5262 | 0.650 | 0.205 | 0.000 | 0.000 | 0.000 |
| RoPE | 1 | 2.5220 | 0.995 | 0.925 | 0.450 | 0.415 | 0.295 |
| RoPE | 2 | 2.5210 | 0.305 | 0.225 | 0.000 | 0.835 | 0.945 |
| 可学习 | 0 | 2.5249 | 1.000 | 0.990 | 0.955 | 0.900 | 0.715 |
| 可学习 | 1 | 2.5179 | 1.000 | 1.000 | 0.750 | 0.785 | 0.770 |
| 可学习 | 2 | 2.5189 | 1.000 | 1.000 | 0.995 | 0.975 | 0.815 |
| 不加 | 0 | 2.5285 | 0.995 | 1.000 | 1.000 | 0.980 | 0.990 |
| 不加 | 1 | 2.5275 | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
| 不加 | 2 | 2.5204 | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
RoPE 反而最差(3 个种子里最低 0.000;可学习 0.715;不加 0.980)。诊断:在预测动词的位置,所有头里分给主语的最大注意力权重(种子 0):
| 位置编码 | (训练见过) | (没见过) |
|---|---|---|
| RoPE | 0.997 | 0.061 |
| 可学习 | 0.926 | 0.466 |
| 不加 | 0.951 | 0.949 |
原因在于这门语言的结构:主语永远是第 3 个 token(“<s> the 主语”)。可学习的绝对位置嵌入可以直接学会”去看下标 2”,这条规则对任何长度都成立(虽然它只部分做到了);不加位置编码的模型也能借助因果掩码认出句首。RoPE 只告诉模型相对距离,“主语”对它来说是”往回 个位置”, 时这个距离(12–24)在训练中从未在主语和动词之间出现过。
所以相对位置编码并非处处更好:它擅长”相对位置固定”的依赖(自然语言里大量的局部搭配),而对”锚定在绝对位置上”的信息反而要外推。现代模型选 RoPE,是因为真实文本里前者占多数,而且它和 KV 缓存、长上下文扩展(第 19 讲)配合得好。这个玩具实验也再次说明:超出训练分布的行为,必须用多个种子、配合诊断来看。
8. 小结 · Summary
- 预训练:在大量(通常无标注的)数据上训练;微调:用预训练的参数初始化,加一个小的预测头,在少量标注数据上训练。2006 年的逐层无监督预训练(自编码器)点燃了深度学习;今天的 LLM 用生成式预训练,流程是预训练 → 中期训练 → 后训练。实验中,预训练模型用 16 个标注样本就能做到从零训练用 256 个都做不到的事;但小数据上的全量微调可能破坏预训练的表示,只训练分类头反而最稳。
- 现代开源 Transformer 的常见改动:pre-norm 的 RMSNorm、SwiGLU、去掉偏置、RoPE、GQA/MQA、滑动窗口(常与全局注意力交替)。
- RoPE:把 、 切成二维小块,按各自的绝对位置 旋转 ; 让分数只依赖相对距离。旋转保持长度;不同频率避免混叠;块对角结构让它只需 运算;和 KV 缓存天然兼容。两边都转 的”错误版”会让位置信息完全抵消。
- GQA: 个查询头共用一组 K、V 头;MHA、MQA 是两个极端。它几乎不省参数,但把 KV 缓存和解码带宽缩小 倍;已有的 MHA 模型可以按组取平均后少量继续训练来改装。
- 滑动窗口注意力:每个 token 只看最近 个;分块实现把计算和内存降到 ; 层的感受野是 ;配合 RoPE 可以用固定大小的滚动缓存。窗口太小会切断长程依赖。
- 相对位置编码不是在所有任务上都更好:在我们的玩具语言里,主语锚定在绝对位置上,RoPE 外推得最差。
9. 常见误区 · Common pitfalls
- “微调就是把所有参数都再训练一遍,越多越好。” 标注数据很少时,全量微调可能过拟合、破坏预训练的表示;线性探针或参数高效微调可能更好(§2.8)。
- “RoPE 是把位置向量加到输入上。” RoPE 不加任何东西,它在每一层旋转 和 ; 不旋转。
- “RoPE 用相对位置旋转。” 每个向量按自己的绝对位置旋转;相对位置是在点积里出现的。两边都按 旋转会让位置信息完全消失(§4.3)。
- “RoPE 的两种实现(相邻配对 / 前后两半配对)不一样。” 数学上等价;但加载权重时必须用训练时的那一种。
- “GQA 主要是为了省参数。” 它省的主要是推理时的 KV 缓存和显存带宽;参数只少了一点。
- “MQA 最省,所以最好。” MQA 的质量下降和训练不稳定更明显;GQA 是折中。
- “滑动窗口注意力的朴素实现能省计算。” 先算完整矩阵再加掩码,计算量还是 ;要用分块实现。
- “滑动窗口让模型看不到窗口之外的信息。” 多层叠加后感受野是 ,但信息要经过多次接力,越远越弱(§6.5 的窗口 4 在距离 9 处失败)。
- “滚动缓存截短后,新 token 的位置就是缓存的长度。” 必须按它真正的绝对位置旋转(§6.4)。
- “相对位置编码一定外推得更好。” 取决于任务需要的是哪种位置信息(§7.3)。
10. 练习 · Exercises
练习 1(手算 RoPE)。 ,只有一个频率 。。(a) 查询在位置 、键在位置 时,旋转后的点积是多少?(b) 换成 、 呢?(c) 用 解释。
参考答案
(a) ,,点积 。(b) ,,点积 ,相同。(c) ,只依赖 。
练习 2(RoPE 的转速范围)。 、base = 10000 时,最快和最慢的一对每个位置转多少弧度?周期各是多少个位置?Llama 3 把 base 改成 500,000,最慢的那对周期变成多少?为什么长上下文模型要把 base 调大?
参考答案
(周期 );,周期约 54,410 个位置。base = 500,000 时 ,周期约 256 万个位置。上下文变长后,需要有足够慢的”指针”在整个上下文范围内都不转完一圈,否则远处的位置会和近处的混叠;调大 base 让所有频率都变慢(第 19 讲)。
练习 3(Mistral 的缓存)。 Mistral 7B:32 层、8 个 KV 头、每头 128 维、BF16、滑动窗口 4096。(a) 每个 token 的 KV 缓存多大?(b) 用滚动缓存时,一个序列的缓存最多多大?(c) 如果不用滚动缓存,32K 个 token 的序列要多大?
参考答案
(a) 字节 = 128 KiB。(b) 最多保留 4096 个位置:512 MiB,和序列多长无关。(c) 32,768 × 128 KiB = 4 GiB。
练习 4(GQA 的参数)。 、32 个查询头、每头 128 维。分别求 MHA(32 个 KV 头)和 GQA(8 个 KV 头)的注意力层参数量(不计偏置)。
参考答案
MHA: 都是 ,共 M。GQA:、 仍是 ,、 是 ,共 M。少了约 38%——但在整个模型里(前馈网络占大头),这个差别不大。
练习 5(滑动窗口省了多少)。 、。(a) 普通因果注意力”有用”的分数占 的多少?(b) 滑动窗口实际需要的分数占多少?(c) §6.2 的分块实现计算 个分数,占多少?
参考答案
(a) 。(b) 每个查询最多看 个,前 个查询看得更少:。(c) ——比需要的多算了一倍(块内有一半被掩码屏蔽),但和 100% 相比已经省了很多,而且全是规整的矩阵乘法。
练习 6(感受野)。 Mistral 7B 有 32 层、窗口 4096。按 §6.3 的公式,最后一层理论上能间接依赖多远的 token?一个 2 层、窗口 4 的模型呢?
参考答案
,约 13 万(Mistral 论文的说法是”约 131K”)。2 层、窗口 4:——这就是 §6.5 里距离 9 的主语够不着的原因。
练习 7(“错误版”的 RoPE)。 证明:如果查询和键都乘以同一个旋转矩阵 ,点积不变。这对 slides 上那一版公式意味着什么?
参考答案
,因为旋转矩阵是正交矩阵,。slides 那一版让 和 都旋转 ,于是分数和完全不加位置信息时一模一样(§4.3 的实验:差 量级)。
练习 8(SwiGLU 的中间维度)。 GELU 前馈网络有 2 个 的矩阵,SwiGLU 有 3 个 的矩阵。要让两者参数量相同, 应该是多少? 时,向上取到 256 的倍数是多少?
参考答案
。 时 ,向上取到 256 的倍数是 11,008——正是 Llama 2 7B 的中间维度。(Llama 3 8B 用的是 14,336,比这个更大。)
练习 9(RMSNorm vs LayerNorm)。 不带缩放参数,求 的 RMSNorm 和 LayerNorm。
参考答案
RMSNorm:,输出 ——只缩放,不改变均值(输出仍然全是正数)。LayerNorm:先减去均值 2.5 再除以标准差 1.118,输出 (第 2 讲练习 5)。
练习 10(为什么线性探针更好)。 §2.8 里,16 个样本时线性探针 1.000、全量微调 0.910。用”可训练参数的个数”和”样本数”解释;如果把样本数加到 256,你预计两者的差距会怎样?实测呢?
参考答案
线性探针只训练 个参数,16 个样本足以确定;需要的信息(主语的单复数)已经在冻结的表示里。全量微调要更新约 11 万个参数,16 个样本远不足以约束它们,模型可以用很多”捷径”拟合训练集、同时破坏原有的表示。样本增加后两者的 k = 1–3 准确率都是 1.000,但外推(k = 4–6)上全量微调仍然更差(0.932 vs 1.000):微调后的模型更依赖训练分布里的细节。
11. 延伸阅读 · Further reading
- 预训练的起点:Hinton & Salakhutdinov (2006), Reducing the Dimensionality of Data with Neural Networks;Bengio et al. (2006), Greedy Layer-Wise Training of Deep Networks;Erhan et al. (2010), Why Does Unsupervised Pre-training Help Deep Learning?。
- 数据与流程:Gao et al. (2020), The Pile;Soldaini et al. (2024), Dolma;OLMo 2 技术报告(OLMo et al., 2024)中关于中期训练(Dolmino)的部分。
- RoPE:Su et al. (2021), RoFormer;EleutherAI 的博客 Rotary Embeddings: A Relative Revolution。
- GQA / MQA:Shazeer (2019), Fast Transformer Decoding: One Write-Head is All You Need;Ainslie et al. (2023)。
- 滑动窗口:Beltagy et al. (2020), Longformer;Jiang et al. (2023), Mistral 7B;Child et al. (2019), Sparse Transformers。
- 其他现代改动:Zhang & Sennrich (2019), Root Mean Square Layer Normalization;Shazeer (2020), GLU Variants Improve Transformer;Touvron et al. (2023), LLaMA。
- 位置编码与外推:Press et al. (2022), ALiBi;Kazemnejad et al. (2023),第 2 讲已引用。
12. 下一讲预告 · Next lecture
第 5 讲进入图像。我们会:
- 从零实现卷积层(填充、步长、多通道、反向传播、池化),理解 CNN 的归纳偏置;回顾 LeNet → AlexNet → VGG → ResNet;
- 看计算机视觉的任务谱系(分类、检测、分割、生成……);
- 认识仅编码器的 Transformer:去掉因果掩码的 BERT,以及它的掩码语言模型预训练;
- 把图像切成小块,当成 token 喂给 Transformer——ViT;并用一个自己生成的”形状世界”数据集比较 MLP、CNN、ViT 的归纳偏置和数据需求。