对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 6 · Mon 2/2 · Transformer。 阅读材料:Vaswani et al. (2017) Attention Is All You Need(NeurIPS);Rush, The Annotated Transformer(slides 第 26 页的代码走读);Xiong et al. (2020) On Layer Normalization in the Transformer Architecture(ICML,HW3 说明引用);原课 HW3 说明(Decoder-only Transformer)。 依据说明:本讲以原课第 6 讲公开的 slides(llmsys-06-transformer.pdf,26 页)为主线:回顾、三类语言模型、编码器–解码器范式与 seq2seq、新结构的动机、Transformer 总图、嵌入(共享嵌入、正弦位置编码)、多头注意力(“为什么除以 √d”)、FFN、LayerNorm、解码器自注意力掩码、残差连接与 Post-norm / Pre-norm、原论文的配置、训练(交叉熵、teacher forcing、dropout、标签平滑、词表、按长度分批、硬件、Adam 与学习率预热、模型平均、解码长度)、小结、代码走读。软查表的演示、√d 与 softmax 饱和的测量、置换等变、RNN 对比、位置编码的旋转性质、HW3 模块与 PyTorch 的逐个对照、残差 / LayerNorm 的深层堆叠、Post-LN 与 Pre-LN 的梯度和训练实验、标签平滑在城市语言上的精确代价、检查点平均、成本核算、用自己的框架训练的分类器,都是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:
code/lecture06/(7 个脚本 +transformer.py、torch_ref.py、city_lab.py;04 约 1 分钟,其余 15 秒以内) 预计阅读时间:4–5 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说清楚编码器–解码器范式,以及 RNN 编码器的两个问题(串行、长距离路径),由此理解 Transformer 的动机:“全上下文 + 并行”。
- 写出缩放点积注意力 softmax(QKᵀ/√d + M)V,解释为什么除以 √d、掩码怎样让解码器看不见未来、为什么必须有位置编码。
- 画出多头注意力里张量形状的变化(B × S × D → B × h × S × Dh → B × S × D),说出多头为什么不增加计算量。
- 解释 FFN、残差连接、LayerNorm 各自的作用,区分 Post-LN 和 Pre-LN,说明为什么原论文离不开学习率预热、为什么今天的 LLM 都用 Pre-LN。
- 说出原论文的训练配方:teacher forcing、dropout、标签平滑、Adam + 预热的学习率公式、检查点平均,以及每一项的代价。
- 用”信封背面计算”估算一个 Transformer 的参数量、每 token 的 FLOPs、训练要保存的激活值,并指出注意力的 n² 项从什么长度开始占主导。
- 用第 5 讲自己写的框架实现原课 HW3 的全部模块,并和 PyTorch 逐位对照。
0.2 先修与本讲范围 Prerequisites & scope
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| softmax、交叉熵、Adam、LayerNorm 的公式(P3) | LLaMA 的改动:RMSNorm、SwiGLU、RoPE、GQA(第 7 讲) |
| 三类语言模型与注意力掩码、KV 缓存(第 1 讲 §3.4) | 分词与 BPE(第 8 讲);解码、束搜索、推测解码(第 9 讲) |
| 自动微分与 tensor.py(第 5 讲) | 融合内核(第 10–11 讲);FlashAttention(第 21 讲);激活重计算(第 18 讲) |
0.3 本讲地图 Map
§1 从 seq2seq 说起:编码器–解码器、RNN 的两个问题、新结构的动机 ← 代码 01
§2 注意力:软查表、缩放点积、为什么除以 √d、掩码、置换等变 ← 代码 01
§3 多头注意力:为什么多头、形状怎样变、交叉注意力 ← 代码 03
§4 嵌入与位置编码:共享嵌入、正弦编码、学出来的位置表、乘以 √d ← 代码 02
§5 FFN、残差连接、LayerNorm;Post-LN vs Pre-LN ← 代码 04
§6 整体结构与原论文配置;Decoder-only(GPT-2 / HW3)
§7 训练:teacher forcing、dropout、标签平滑、学习率预热、检查点平均 ← 代码 04、05
§8 系统视角:参数、FLOPs、激活值、注意力的 n²、小运算的数量 ← 代码 03、06
§9 HW3:用自己的框架搭 Transformer,并解决第 5 讲留下的转折句问题 ← 代码 03、07
§10 小结 §11 常见误区 §12 练习 §13 延伸阅读 §14 下一讲预告
1. 从 seq2seq 说起 · From seq2seq to attention
1.1 编码器–解码器范式
slides 先回顾第 1 讲的三类语言模型(encoder-only 如 BERT、encoder-decoder 如 T5、decoder-only 如 GPT / LLaMA),然后聚焦条件生成:给定源句 x(例如”我喜欢唱歌和跳舞。”),生成目标句 y(“I like singing and dancing.”)。模型的是一个条件概率,按链式法则拆成逐 token 的乘积:
编码器读完整个源句,把它变成一组表示;解码器每一步看着”源句的表示 + 已经生成的前缀”,给出下一个 token 的分布。机器翻译、摘要、问答,甚至”指令 → 回答”,都可以放进这个框架。
1.2 RNN 的 seq2seq 和它的两个问题
2014 年的 seq2seq(Sutskever et al.)用 LSTM / GRU 做编码器和解码器:编码器一个词一个词地读,h_t = f(h_{t−1}, x_t)。这带来两个问题:
- 串行:第 t 步要等第 t−1 步算完,长度为 n 的句子就是 n 步串行,GPU 上成千上万个核心只能”排队”。
- 长距离路径:第 1 个词的信息要经过 n 次变换才到达最后一个位置,中途很容易丢失。
01 号脚本第 5 部分把两者摆在一起(d = 512):
| n | RNN 串行步数 | 注意力串行步数 | RNN 最远路径 | 注意力最远路径 | 注意力 FLOPs ÷ RNN FLOPs |
|---|---|---|---|---|---|
| 16 | 16 | 1 | 16 | 1 | 2.03 |
| 1024 | 1024 | 1 | 1024 | 1 | 4.0 |
| 8192 | 8192 | 1 | 8192 | 1 | 18.0 |
注意最后一列:注意力并不省计算,它的四个投影本身就是 RNN 两个矩阵的 2 倍,长序列时更贵。它赢在别处:⏱ 本机实测,RNN 每秒只完成约 50 GFLOP,注意力约 700–860 GFLOP(几次运行之间有波动)——RNN 每一步只是”矩阵 × 向量”(算术强度约 1,第 1、4 讲),还要一步一步发起;注意力是大矩阵乘法,正好喂饱硬件。
1.3 动机:全上下文 + 并行
slides 第 7 页的一句话就是 Transformer 的设计目标:在编码器和解码器里都用注意力,让每个位置能直接看到全部上下文;去掉循环,让所有位置同时计算。 这是一个典型的”模型–系统协同设计”(第 1 讲 §4):结构本身是为并行硬件挑的。代价我们会在 §8 算清楚——注意力的分数矩阵是 n × n 的。
2. 注意力 · Attention
2.1 直觉:软查表
普通字典是”硬查表”:拿一个键去匹配,命中的那一项被取出来。注意力把它”软化”:
- 每个位置有一个查询 q(“我想找什么”)、一个键 k(“我是什么”)、一个值 v(“我能提供什么”);
- 查询和每个键算相似度 q·k,经过 softmax 变成权重;
- 输出是所有值的加权平均。
01 号脚本第 1 部分:4 个正交的键、值是 10 / 20 / 30 / 40,查询最像第 2 个键。把分数放大 1、4、16、64 倍,输出从 23.795 变到 20.000——分数越”尖”,查表越”硬”。软的好处是处处可导:模型能用梯度学会”该看谁”。
2.2 缩放点积注意力,以及为什么除以 √d
把所有位置的 q、k、v 摞成矩阵 Q、K、V(每行一个位置),一次算完:
slides 第 12 页留了一个问题:为什么除以 √d? 设 q、k 的每一维独立、均值 0、方差 1,那么 q·k 是 d 个独立乘积之和,方差 = d。01 号脚本第 2 部分实测(每个查询对 32 个键做 softmax,2000 次平均):
| d | 分数 | 分数的标准差 | 最大权重 | 熵(nat,均匀时 3.466) | ‖softmax 的雅可比‖ |
|---|---|---|---|---|---|
| 8 | 不缩放 | 2.83 | 0.489 | 1.679 | 0.3103 |
| 512 | 不缩放 | 22.53 | 0.939 | 0.153 | 0.0792 |
| 4096 | 不缩放 | 63.88 | 0.977 | 0.055 | 0.0319 |
| 4096 | 除以 √d | 1.00 | 0.163 | 3.018 | 0.2407 |
分数太大时 softmax 饱和成独热:几乎所有权重压在一个键上,softmax 的雅可比矩阵 diag(p) − ppᵀ 接近 0,梯度传不回 Q 和 K,训练初期学不动。除以 √d 让分数的标准差回到 1、与 d 无关。原论文的解释正是这样:d 大时点积的量级变大,把 softmax 推进梯度极小的区域。
为什么这样做的思考:缩放不是”让数值好看”,而是一个初始化尺度的问题。和第 5 讲之前 P3 讲的”权重初始化让每层方差稳定”是同一类考虑:深度学习里很多”奇怪的常数”(√d、1/√fan_in、0.02)都在保证某个量的方差不随宽度变化。
2.3 掩码:解码器看不见未来
slides 第 14 页:在 softmax 之前把”右边”(未来的位置)置为 −∞。01 号脚本第 3 部分打印了 5 × 5 的因果掩码和一个随机初始化层的注意力权重:上三角的权重恰好是 0。再做一个实验——把第 j 个 token 换掉:位置 < j 的输出一位都不变(最大变化 0.0),位置 ≥ j 的输出都变了。
同一个掩码同时服务两件事:
- 训练时并行:一个句子的所有位置一次算完,每个位置只看到它之前的真实 token(teacher forcing,§7.1)。
- 推理时自回归:模型只依赖已经生成的部分,旧位置的结果不会因为新 token 而改变——这正是第 1 讲说的”只有因果模型才能用 KV 缓存”的原因。
另一种掩码是补齐掩码:一批句子长短不一,补齐的位置不许被任何查询看到(transformer.MultiHeadAttention 的 pad_mask,07 号脚本用到)。实现上,”−∞“在代码里用 −1e9:float64 里 exp(−1e9 − m) 恰好是 0,又不会出现真正的 −∞ 带来的 nan。
2.4 没有位置信息时:置换等变
自注意力只看内容之间的相似度。如果输入里没有位置信息,把输入的顺序打乱,输出就只是跟着打乱——数学上叫置换等变(permutation equivariant)。01 号脚本第 4 部分验证:打乱 9 个 token,输出与原输出按同样顺序打乱后的最大误差 4.4e-16。
后果很直接:转折句 “the plot was good but the acting was bad” 和对调后的 “the acting was bad but the plot was good” 用的是同一组词,平均之后的表示只差 1.1e-16——和第 5 讲的词袋模型一样分不出来。加上位置嵌入后两者差 0.084,模型才有可能学会”but 之后的从句说了算”(§9.3 用训练实验验证)。所以位置编码不是可有可无的装饰,而是 Transformer 的必需品。
3. 多头注意力 · Multi-head attention
3.1 为什么要多个头
一组 Q、K、V 只能表达一种”相关性”。一个词可能同时需要看它的主语、它修饰的名词、前一个词……slides 第 11 页的做法:把 d 维拆成 h 个头,每个头有自己的投影,各自做注意力,最后拼接起来再乘一个输出矩阵:
3.2 形状怎样变(HW3 的核心)
实现时并不真的建 h 组小矩阵,而是”一个大投影 + 改变看法”。03 号脚本第 3 部分(B = 2、S = 5、D = 12、h = 3):
| 步骤 | 形状 | 含义 |
|---|---|---|
| 输入 X | (2, 5, 12) | B × S × D |
| Q = X·W_Q | (2, 5, 12) | 一个 D × D 的投影 |
| view 拆成 h 个头 | (2, 5, 3, 4) | B × S × h × Dh |
| permute(0, 2, 1, 3) | (2, 3, 5, 4) | B × h × S × Dh |
| K 再交换最后两维 | (2, 3, 4, 5) | B × h × Dh × S |
| q @ kT | (2, 3, 5, 5) | 每个头一张 S × S 的分数表 |
| softmax 后 @ v | (2, 3, 5, 4) | B × h × S × Dh |
| permute 回来 + contiguous + view | (2, 5, 12) | 各头拼接 |
| 输出投影 W_O | (2, 5, 12) | B × S × D |
两点值得记住:
- 多头不增加参数和计算:投影还是 D × D;view 只是换形状,permute 只是换步长(零拷贝,第 5 讲);B × h 组注意力由一次批量矩阵乘法算完。
- permute 之后张量不再连续,必须先
contiguous()(真正拷贝一次)才能view——这是 HW3 最常见的报错之一。
3.3 交叉注意力
编码器–解码器结构的解码器层里有第二个注意力:查询来自解码器,键和值来自编码器的输出。它让解码器的每个位置去”查”源句。解码器自注意力要因果掩码,交叉注意力不需要(源句已经完整可见)。Decoder-only 模型(GPT、LLaMA、HW3)没有编码器,也就没有交叉注意力:源句和目标句拼成一条序列,用同一个因果自注意力处理。
4. 嵌入与位置编码 · Embeddings and positions
4.1 token 嵌入,以及共享嵌入
slides 第 10 页:token 嵌入就是查表(第 5 讲:前向是 gather、反向是 scatter-add),并且输入和输出共享同一个嵌入表。输出层算的是 logits = h · Eᵀ——“这个向量和每个词有多像”,和输入嵌入”词 → 向量”度量的是同一件事。
02 号脚本第 5 部分按原论文的配置算参数(En-De 共享 37K 的 BPE 词表,第 8 讲):
| 模型 | 6+6 层 | 一个嵌入表 | 共享时合计 | 不共享(三张表)合计 |
|---|---|---|---|---|
| base(d = 512, ff = 2048) | 44.1M | 18.9M | 63.1M | 101.0M |
| big(d = 1024, ff = 4096) | 176.4M | 37.9M | 214.2M | 290.0M |
原论文报告 base 约 65M、big 约 213M,和”共享”一列吻合。共享在 base 上省下 37.5% 的参数。词表大、模型小时省得多;今天的大模型(如 LLaMA)嵌入只占很小一部分,通常不共享。
4.2 正弦位置编码
原论文给每个位置一个和 token 嵌入同维的向量,直接加到嵌入上(slides 第 10 页):
直觉是一个多指针的钟:第 i 对维度以角频率 ω_i = 10000^(−2i/d) 转动。02 号脚本第 1 部分(d = 512):最快的一对波长 6.3 个位置,最慢的一对约 60,611 个位置。前 2048 个位置两两最小距离 3.714(编码各不相同),每个位置的最近邻都是相邻位置。每个分量都在 [−1, 1] 之内,不会像”直接输入 t”那样越来越大。
为什么用 sin / cos?因为它有一个漂亮的相对位置性质(02 号脚本第 2 部分):
- 用和角公式把 PE(t) 的每一对 (sin, cos) 转过 ω_i·k,恰好得到 PE(t+k)(误差 ~5e-14)。这个”旋转”只和 k 有关、和 t 无关:模型学一个固定的线性变换,就能从”我在 t”推到”前面第 k 个位置”。
- 两个位置编码的点积只依赖距离:PE(t)·PE(t+k) = Σ_i cos(ω_i k),随 t 的标准差只有 ~1e-14。
第 7 讲的 RoPE 把这个想法做彻底:不把位置”加”到嵌入上,而是直接旋转 q 和 k,让注意力分数只依赖相对位置。
4.3 学出来的位置表
GPT-2、BERT、HW3 用的是另一种做法:位置也是一张嵌入表(n_positions × d),和 token 嵌入一样随模型训练。原论文的消融实验显示两者效果几乎一样。代价是长度上限写死在参数里:02 号脚本第 3 部分里,n_positions = 8 的 DecoderLM 遇到长度 9 的序列直接报错。正弦编码能算出任意位置(例如 t = 9999),但”算得出”不等于”模型会用”——训练时没见过的位置照样可能出错(第 7 讲比较长度外推)。
4.4 为什么把嵌入乘以 √d
原论文在嵌入层”把权重乘以 √d_model”,没有解释原因。一种常见的解读(02 号脚本第 4 部分):正弦编码的长度恒为 √(d/2)(每一对 sin² + cos² = 1),d = 512 时约 16;按 N(0, 1/d) 初始化的嵌入长度只有 1 左右。直接相加时,位置编码把 token 信息淹没了——相加后与原嵌入的余弦相似度只有 0.064;乘以 √d 后变成 0.816。学出来的位置表没有这个问题,两张表一起训练,尺度自己会调好。
5. FFN、残差连接与 LayerNorm · FFN, residuals, LayerNorm
5.1 前馈网络
每个位置各自过一个两层的 MLP(slides 第 13 页):
注意力只做”加权平均”,在位置之间搬运信息,本身几乎是线性的;FFN 给每个位置一个非线性变换,而且装着一层里 2/3 的参数(8d² vs 注意力的 4d²,06 号脚本)。原论文用 ReLU,GPT-2 和 HW3 用 GELU,LLaMA 用 SwiGLU(第 7 讲)。
5.2 残差连接:让梯度走直路
把 L 个变换直接叠起来,信号和梯度每穿过一层都会被放大或缩小一截,深了就消失或爆炸。04 号脚本第 1 部分叠 L 层 Linear + ReLU(宽 256),看梯度从输出传回输入时被放大的倍数:
| 层数 L | 无残差 | 只加残差 x + f(x) | 残差 + LayerNorm:x + f(LN(x)) |
|---|---|---|---|
| 4 | 2.8e-02 | 1.37 | 1.31 |
| 16 | 6.3e-07 | 4.12 | 2.13 |
| 64 | 1.4e-25 | 1.4e+05 | 4.82 |
- 没有残差:64 层后梯度只剩 1e-25,前面的层收不到任何信号。
- 只加残差:恒等路径让梯度能传回去,但每层都往”残差流”里加一份,数值指数增长(输出标准差 6.5e+05)——反过来爆炸了。
- 残差 + LayerNorm:既不消失也不爆炸。残差负责让梯度走直路,LayerNorm 负责控制尺度,所以 slides 第 15 页把两者放在一起讲。
5.3 LayerNorm:每个 token 自己归一化
μ、σ² 沿特征维(最后一维)计算,每个 token 一组。04 号脚本第 2 部分对比 BatchNorm(沿批统计):同一个序列和不同的序列一起成批,LayerNorm 的输出变化 0,BatchNorm 最大变化 1.6–1.8。序列模型的批里长度不一、有补齐,推理时批大小常常是 1、还要逐 token 生成——BatchNorm 的统计量在这些场合都靠不住,LayerNorm 没有这个问题。另外 LayerNorm 对输入的整体缩放不敏感(放大 7.5 倍,输出只变 8.1e-06,来自 eps)。
5.4 Post-LN vs Pre-LN
LayerNorm 放在哪里?slides 第 15 页给了两种:
Post-LN(2017 原论文): x = LN(x + Attn(x)); x = LN(x + FFN(x))
Pre-LN (GPT-2、HW3、今天的 LLM):x = x + Attn(LN(x)); x = x + FFN(LN(x)); 最后再加一个 LN
区别在于残差路径上有没有 LayerNorm。04 号脚本第 3 部分测初始化时的梯度随层数 L 的变化(3 个种子平均):
| L | Post-LN 最后一层梯度 | Post-LN 全部 | Pre-LN 最后一层梯度 | Pre-LN 全部 |
|---|---|---|---|---|
| 2 | 0.665 | 0.928 | 0.618 | 0.883 |
| 12 | 0.804 | 2.615 | 0.527 | 2.108 |
| 48 | 1.186 | 7.638 | 0.373 | 3.836 |
Post-LN 越深,输出附近的梯度越大;Pre-LN 正相反,最后一层的梯度随深度变小,整体更温和。这正是 Xiong et al. (2020) 的理论结论:Post-LN 输出附近的梯度不随层数减小,一开始用大学习率容易把那几层推坏。
04 号脚本第 4 部分在城市语言上训练 6 层小模型(400 步,测试集交叉熵超出理论下界的毫 nat / token):
| 学习率 | 结构 | 预热 | 超出下界 |
|---|---|---|---|
| 1e-3 | Post-LN | 无 | 30.1 |
| 1e-3 | Pre-LN | 无 | 34.6 |
| 1e-2 | Post-LN | 无 | 3169.4(失败:测试交叉熵 3.922,比只看词频的一元模型 3.807 还差) |
| 1e-2 | Post-LN | 100 步 | 246.0(第 100 步损失 0.837,之后又变差到 0.942) |
| 1e-2 | Pre-LN | 无 | 169.3 |
| 1e-2 | Pre-LN | 100 步 | 40.1 |
学习率小时两者都没问题;学习率大时,Post-LN 不预热就训练失败,预热之后能训练但仍不稳定,Pre-LN 不预热也能训练。这解释了原论文的学习率公式为什么离不开预热(§7.5),也是 GPT-2 之后几乎所有 LLM 改用 Pre-LN 的原因。Pre-LN 的代价是残差流本身不归一化,所以最后要多一个 LayerNorm(GPT-2 的 ln_f、HW3 DecoderLM 的 self.ln);也有研究指出 Post-LN 调好之后最终效果可能略好,这是稳定性和上限之间的权衡。
6. 整体结构与原论文配置 · The full architecture
6.1 一层里的数据流
编码器层(× N): 解码器层(× N):
x ─► 自注意力 ─► Add & Norm y ─► 带掩码的自注意力 ─► Add & Norm
─► FFN ─────► Add & Norm ─► 交叉注意力(K、V 来自编码器输出)─► Add & Norm
─► FFN ─► Add & Norm
输入:token 嵌入 × √d + 位置编码 ─► dropout 输出:Linear(与嵌入共享)─► softmax ─► P(y_t | y_<t, x)
6.2 原论文的配置(slides 第 18 页)
| base | big | |
|---|---|---|
| 编码器 / 解码器层数 | 6 / 6 | 6 / 6 |
| d_model | 512 | 1024 |
| FFN 中间维度 | 2048 | 4096 |
| 头数 h | 8 | 16 |
| dropout | 0.1 | 0.3(En-De) |
| 参数(原论文) | 65M | 213M |
| 训练(8 块 GPU 一台机器) | 10 万步,约 12 小时 | 30 万步,约 3.5 天 |
6.3 Decoder-only:GPT-2 和 HW3
去掉编码器和交叉注意力,只留”带掩码的自注意力 + FFN”,就是 GPT 系列。HW3 要实现的正是 GPT-2 结构:token 嵌入 + 位置嵌入 → dropout → N 个 Pre-LN 层 → LayerNorm → 线性层到词表。翻译任务在 Decoder-only 模型里的做法是把源句和目标句拼成一条序列,损失只算目标句的部分(HW3 Problem 4 用 IWSLT14 德英翻译,10 轮后 BLEU 约 20)。
7. 训练 Transformer · Training
7.1 损失与 teacher forcing
训练目标是交叉熵:所有句子、所有位置的 −log P(y_t | y_<t, x) 之和(slides 第 19 页)。teacher forcing:训练时直接把正确答案右移一位作为解码器输入(slides 第 18 页的 “outputs (shifted right)”),每个位置都假装前缀是对的。配合因果掩码,一次前向就得到所有位置的损失。05 号脚本第 1 部分:一次前向和”每个前缀单独前向”的 log 概率差 1.8e-15,前向次数 1 vs 7。代价是训练和推理不一致:推理时前缀是模型自己生成的,可能有错(exposure bias,第 9 讲)。
7.2 Dropout
原论文在每个子层的输出加到残差之前、以及”嵌入 + 位置编码”之后做 dropout,p = 0.1–0.3(slides 第 20 页)。GPT-2 还在注意力权重上做 dropout(本讲的 transformer.py 也这样做)。实现要点:训练时随机置零并把剩下的放大 1/(1−p),评估时什么都不做(03 号脚本第 2 部分:保留比例 0.8994,平均值 0.9993)。
7.3 标签平滑
slides 第 21 页:把独热目标”抹平”一点——正确类 1 − ε,其余每类 ε/(V−1),常用 ε = 0.1。05 号脚本第 2 部分:
- 最优 logits 变成有限的:V = 37000、ε = 0.1 时,正确类的 logit 只需比其他类大 log((1−ε)(V−1)/ε) = 12.72;不平滑时要让差距趋于无穷,模型会被推向”无限自信”。
- 两种约定:PyTorch 的
label_smoothing把 ε 均分给全部 V 类(含正确类),slides 均分给其余 V−1 类。梯度都是 softmax(z) − p(p 为平滑后的目标),但 p 不同(这里差 0.010)。第 10 讲 LightSeq 的融合交叉熵用的是前一种。 - 精确代价:城市语言的真实分布已知,用平滑交叉熵训练到最优时模型给出 q* = (1−ε)p + ε/V。把它放到测试集上算:
| ε | 交叉熵 | 超出理论下界(毫 nat) | 困惑度 | argmax 不变的比例 |
|---|---|---|---|---|
| 0 | 0.7530 | 0.0 | 2.1234 | 1.000 |
| 0.1 | 0.8530 | 100.0 | 2.3467 | 1.000 |
| 0.2 | 0.9643 | 211.3 | 2.6229 | 1.000 |
困惑度变差了,但每个位置最可能的 token 一个都没变。这和原论文的说法一致:标签平滑”让模型更不确定,损害困惑度”,但改善了准确率和 BLEU。翻译看的是 BLEU,所以原论文用它;LLM 预训练要的正是准确的概率,通常不用。
7.4 词表与分批
原论文的词表:英德用 BPE、共 37K(源和目标共享),英法用 32K 的 word-piece(slides 第 20 页,第 8 讲细讲)。分批时按句长相近的分到一批(减少补齐),但仍要打乱批的顺序(slides 第 22 页)。第 1 讲 03 号脚本量过补齐的浪费:随机分批 29.9%,按长度分桶 0.1%。
7.5 Adam 与学习率预热
原论文用 Adam(β₁ = 0.9、β₂ = 0.98、ε = 1e-9),学习率按下面的公式变化(slides 第 22 页):
前 4000 步线性上升,之后按 1/√step 下降。05 号脚本第 3 部分:base 模型的峰值 512^(−0.5) · 4000^(−0.5) = 6.99e-4,10 万步时降到 1.40e-4;d = 1024 时峰值小 √2 倍。
为什么一定要预热?除了 §5.4 的 Post-LN 梯度问题,还有 Adam 本身(05 号脚本第 4 部分):偏差修正后第一步 m̂ = g、v̂ = g²,更新量 ≈ lr · sign(g)——不管梯度是 1e-6 还是 1e3,每个参数都走满一步。训练刚开始 v̂ 只是一两个样本的估计,很不准,Adam 却给每个参数(包括梯度几乎为 0 的)都迈出满步。预热让最初几百步的步子小一点,等估计稳定再放大(Liu et al. 2020 的 RAdam 分析的正是这一点)。今天常见的”线性预热 + 余弦衰减”是同一思路的变体。
7.6 检查点平均与解码长度
slides 第 24 页:base 模型取最后 5 个检查点(每 10 分钟存一个)的参数平均作为最终模型(big 取 20 个);解码时输出长度不超过源句长度 + 50(第 9 讲)。05 号脚本第 5 部分(常数学习率 3e-3,每 50 步存一个检查点,超出下界的毫 nat / token):
| 模型 | 超出下界 |
|---|---|
| 最后 5 个检查点各自 | 13.7–19.8 |
| 5 个检查点的参数平均 | 6.1 |
| 对照:学习率余弦衰减到 10% | 5.8 |
常数学习率下参数一直被最近几个小批量”拽来拽去”;平均把这些抖动抵消掉,效果和”学习率衰减到很小”相近,而成本几乎为零。
8. 系统视角:参数、FLOPs、激活值 · Costs
8.1 参数在哪里
06 号脚本第 1 部分的公式和模块逐个相等。GPT-2 small(d = 768、12 层、V = 50257、上下文 1024、共享嵌入)合计 124,439,808 个参数,与 OpenAI 公开的数字完全一致:
| 部件 | 占比 |
|---|---|
| token 嵌入 | 31.0% |
| 注意力 Q / K / V / O × 12 层 | 22.8% |
| 前馈 × 12 层 | 45.5% |
| 位置嵌入 + LayerNorm | 0.6% |
每层:注意力 4d²、前馈 8d²,合计约 12d²(第 1 讲的参数量公式)。
8.2 每个 token 的 FLOPs
只数矩阵乘法:每层 24·S·d²(投影 8 + 前馈 16)+ 4·S²·d(QKᵀ 与 AV),加输出层 2·S·d·V。这个公式和 PyTorch 的 FLOP 计数器逐个相等(06 号脚本第 2 部分)。GPT-2 small 在 n = 1024 时每 token 284.8 MFLOPs:前馈 39.8%、输出层 27.1%、QKV 投影 14.9%、两项注意力合计 13.3%。2N = 248.9 MFLOPs,同一量级——每个权重对每个 token 做一次乘加,这就是 6ND 里”前向 2N”的来历。
8.3 注意力的 n²
每层每 token:投影 + 前馈 24d²,注意力 4nd。两者相等时 n = 6d:GPT-2 small 是 4608,LLaMA-7B(d = 4096)是 24576。LLaMA-7B 的注意力占比在 n = 2048 时只有 7.7%,n = 32768 时 57.1%,n = 131072 时 84.2%。常见长度下 FLOPs 的大头仍是投影和前馈。
真正让长序列吃力的是显存和带宽。06 号脚本第 4 部分用 PyTorch 的 saved-tensor 钩子实测一个 Pre-LN 层为反向保存的激活(d = 64、h = 4、float32):拟合下来约是 16 份 S × d + 1 份 h × S × S(softmax 的输出,softmax 的反向和 AV 的反向都要用它)。两部分在 S ≈ 257 时相等,更长时 n² 部分占多数。换成 LLaMA-7B 的一层、S = 8192、fp16:一个 h × S × S 矩阵就是 4.0 GiB,而一份 S × d 的激活只有 64 MiB。⏱ 第 5 部分实测(d = 256):注意力核心占一层时间的比例约 15% / 40% / 70%(S = 128 / 512 / 2048,几次运行之间差几个百分点),高于它的 FLOPs 占比 8% / 25% / 57%——核心里的缩放、加掩码、softmax 几乎没有 FLOPs,却要把 h × S × S 的大矩阵读写好几遍。
8.4 小运算的数量
03 号脚本第 5 部分数了我们的框架里每个模块调用了多少次底层运算(每次都对应 GPU 上的一次内核启动、一次读写全局内存):
| 模块 | 前向运算次数 | 其中矩阵乘法 | 反向中的运算次数 |
|---|---|---|---|
| LayerNorm1d | 14 | 0 | 51 |
| MultiHeadAttention | 27 | 6 | 89 |
| TransformerLayer(Pre-LN) | 68 | 8 | 233 |
| DecoderLM(4 层)+ softmax_loss | 305 | 33 | 982 |
一个 Pre-LN 层前向 68 次运算,矩阵乘法只有 8 次。其余 60 次逐元素运算和归约几乎没有 FLOPs,却每次都读写整个张量。这就是第 10–11 讲(LightSeq)和 HW4 的出发点:矩阵乘法交给 cuBLAS,把其余小运算融合成少数几个内核。
9. HW3:用自己的框架搭 Transformer · HW3 in our own framework
9.1 HW3 的四个 Problem 与 transformer.py
| HW3 | 内容 | 本讲代码 |
|---|---|---|
| Problem 1(nn.py) | logsumexp、softmax_loss | transformer.logsumexp、softmax_loss |
| Problem 2(modules_basic.py) | Linear、Dropout、LayerNorm1d、Embedding | 同名类 |
| Problem 3(transformer.py) | MultiHeadAttention、FeedForward、TransformerLayer(Pre-LN)、DecoderLM | 同名类(另有 Post-LN 选项) |
| Problem 4 | 机器翻译训练与 generate(逐句贪心解码) | 第 9 讲讲解码 |
全部建在第 5 讲的 tensor.py 上:只用它已有的十几个基本运算。两个小技巧:GELU 的 tanh 近似 0.5x(1 + tanh u) 恰好等于 x·sigmoid(2u),可以用 sigmoid 拼出来;1/√x 写成 exp(−½ log x)。softmax 和 logsumexp 要减去最大值,这个最大值当作常量求导是精确的,因为 softmax(x − m) 对任何 m 都相同。
HW3 Problem 1 的 softmax_loss = logsumexp(z) − z_y:03 号脚本第 1 部分里 logits 为 1000 时,直接 log Σ exp 得到 inf,logsumexp 得到 1000.407606;梯度自动就是 softmax(z) − onehot(y)。
9.2 与 PyTorch 逐个对照
torch_ref.py 用 PyTorch 写了结构完全相同、参数名一致的双胞胎,copy_from 把参数原样复制过去,dropout 掩码也从我们这边原样传过去。03 号脚本第 4 部分:注意力、前馈、Pre-LN / Post-LN 层、带 dropout 的 DecoderLM,输出和全部参数的梯度最大差 3.6e-15。反向传播一行都没有手写。
9.3 第 5 讲留下的问题:转折句
07 号脚本用 tensor.py + transformer.py 训练一个影评分类 Transformer(嵌入 → 双向 Transformer 层 → 平均池化 → Linear)。先和 PyTorch 同初始化、同批次训练 20 步:损失最大差 2.2e-16。然后在与第 5 讲同一个测试集上比较:
| 模型 | 全部 | single | negated | contrast | both |
|---|---|---|---|---|---|
| 第 5 讲的词袋网络 | 0.892 | 1.000 | 1.000 | 0.457 | 1.000 |
| Transformer 1 层,无位置嵌入 | 0.895 | 1.000 | 1.000 | 0.472 | 1.000 |
| Transformer 1 层,有位置嵌入 | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
没有位置嵌入时,Transformer 在转折句上和词袋模型一样只能猜(§2.4 的置换等变);加上位置嵌入,转折句全部做对。训练好的模型把最多的注意力给了 but 之后的形容词(位置 8),其次是 but 本身。第 5 讲结尾的问题有了答案:缺的不是”更大的网络”,而是”让模型看见顺序”。
10. 小结 · Summary
- 动机:RNN 编码器串行、长距离路径长;Transformer 用注意力让所有位置一次算完、任意两个位置直接相连。它并不省 FLOPs,赢在并行和大矩阵乘法。
- 注意力 = 软查表:softmax(QKᵀ/√d + M)V。除以 √d 让分数尺度与 d 无关,避免 softmax 饱和;因果掩码让解码器看不见未来,同时服务训练的并行和推理的 KV 缓存。
- 注意力本身置换等变,必须加位置编码。正弦编码的 PE(t+k) 是 PE(t) 的旋转,点积只依赖距离(RoPE 的前身);学出来的位置表更灵活,但长度写死。
- 多头只是把 D 维看成 h 组,不增加参数和计算;permute 之后要 contiguous 才能 view。
- FFN 装着 2/3 的参数;残差让梯度走直路,LayerNorm 控制尺度。Post-LN 输出附近梯度大、需要预热;Pre-LN 更稳定,是今天的默认。
- 训练配方:teacher forcing、dropout、标签平滑(伤困惑度、不伤 argmax)、Adam + 预热的学习率公式、检查点平均(≈ 免费的学习率衰减)。
- 成本:每层约 12d² 参数、每 token 约 24d² + 4nd 次运算;n = 6d 时注意力追平其余部分;激活里有一份 h × S × S 的矩阵,长序列时它主导显存;一层里大部分运算是不做矩阵乘法的小运算——融合内核的用武之地。
11. 常见误区 · Common pitfalls
- “注意力比 RNN 省计算。” 不省,注意力的 FLOPs 是 RNN 的 2 倍以上,长序列时多一个数量级。它赢在并行和硬件效率。
- “除以 √d 只是为了数值稳定,不除也能训练。” 不除时 d 越大 softmax 越饱和,Q、K 几乎收不到梯度;这是初始化尺度问题。
- “Transformer 天然知道词序。” 没有位置编码时它是置换等变的,和词袋模型一样分不出转折句。
- “多头注意力的计算量是单头的 h 倍。” 一样多;只是把 D 维拆成 h 组。
- “permute 之后可以直接 view。” permute 只改步长,张量不再连续,必须先 contiguous()。
- “有了残差就不怕深。” 只有残差、没有归一化时数值随层数指数增长;残差和 LayerNorm 要一起用。
- “Pre-LN 和 Post-LN 只是写法不同。” 梯度随深度的行为相反:Post-LN 需要预热、对学习率敏感,Pre-LN 稳定但最后要多一个 LayerNorm。
- “标签平滑总能提升模型。” 它损害困惑度(城市语言上 ε = 0.1 多出 100 毫 nat),只是不改变 argmax;LLM 预训练通常不用。
- “PyTorch 的 label_smoothing 就是 slides 的公式。” PyTorch 把 ε 均分给全部 V 类,slides 均分给其余 V−1 类。
- “长上下文的瓶颈是注意力的 FLOPs。” 常见长度下 FLOPs 的大头仍在投影和前馈;更常见的瓶颈是 S × S 矩阵带来的显存和读写。
12. 练习 · Exercises
- q、k 的每一维独立、均值 0、方差 1,d = 256。q·k 的标准差是多少?除以 √d 之后呢?
- 因果掩码下,长度为 S 的序列里位置 i 能看到几个位置?一个头的 S × S 分数矩阵里有多少个元素被掩掉?
- B = 4、S = 10、D = 64、h = 8。写出 q、kT、分数矩阵、注意力输出(拼接前)的形状。
- 证明正弦编码的点积 PE(t)·PE(t+k) = Σ_i cos(ω_i k),与 t 无关。
- 一个 d = 1024、FFN 中间维度 4096、有偏置的 Decoder-only 层有多少参数?其中前馈占多少?
- GPT-2 small(d = 768、12 层)在 n = 4608 时,每层每 token 注意力的 FLOPs 和投影 + 前馈的 FLOPs 各是多少?
- 用原论文的学习率公式,d = 512、warmup = 4000,在 step = 2000 和 step = 16000 时学习率各是多少?
- V = 10、ε = 0.1,按 slides 的约定写出正确类为 3 时的平滑目标;按 PyTorch 的约定呢?
- 为什么 Adam 的第一步更新量约等于 lr,与梯度大小无关?这对”要不要预热”有什么启示?
- 一个 Pre-LN 层(d = 4096、h = 32),S = 4096、fp16、批大小 1,按”16 份 S × d + 1 份 h × S × S”估算保存的激活。哪部分更大?
- 为什么因果自注意力训练时能并行,推理时却要一个 token 一个 token 地生成?
- 07 号脚本里,无位置嵌入的 Transformer 能做对否定句,却做不对转折句。分别解释原因。
参考答案
- 方差 = d = 256,标准差 16;除以 √d = 16 后标准差 1。
- 位置 i(从 0 开始)能看 i + 1 个位置;被掩掉的是严格上三角,S(S−1)/2 个。
- q:(4, 8, 10, 8);kT:(4, 8, 8, 10);分数:(4, 8, 10, 10);拼接前的输出:(4, 8, 10, 8),拼接后 (4, 10, 64)。
- PE(t)·PE(t+k) = Σ_i [sin(ω_i t) sin(ω_i (t+k)) + cos(ω_i t) cos(ω_i (t+k))] = Σ_i cos(ω_i (t+k) − ω_i t) = Σ_i cos(ω_i k)(余弦差角公式),与 t 无关。
- 注意力 4d² + 4d = 4,198,400;前馈 2·d·4d + 4d + d = 8,393,728;两个 LayerNorm 4d = 4096。合计 12,596,224,前馈占 66.6%。
- 注意力 4nd = 4 × 4608 × 768 ≈ 14.2 MFLOPs;投影 + 前馈 24d² = 24 × 768² ≈ 14.2 MFLOPs——n = 6d 时恰好相等。
- step = 2000 < warmup:512^(−0.5) × 2000 × 4000^(−1.5) ≈ 0.0442 × 2000 / 252,982 ≈ 3.49e-4;step = 16000:512^(−0.5) × 16000^(−0.5) ≈ 0.0442 × 0.00791 ≈ 3.49e-4。两者恰好相等,都是峰值 6.99e-4 的一半:上升段是 step/warmup 倍峰值,下降段是 √(warmup/step) 倍峰值,所以峰值前 2 倍处和峰值后 4 倍处学习率相同。
- slides:第 3 类 0.9,其余 9 类各 0.1/9 ≈ 0.0111。PyTorch:第 3 类 0.9 + 0.01 = 0.91,其余各 0.01。
- 偏差修正后 m̂₁ = g、v̂₁ = g²,更新 = lr · g / (|g| + ε) ≈ lr · sign(g)。一开始 v̂ 只基于一个样本、极不准确,Adam 仍给每个参数走满步,大学习率下容易一开始就把模型推坏;预热让最初的步子小一些。
- 16 × S × d × 2 字节 = 16 × 4096 × 4096 × 2 ≈ 512 MiB;h × S × S × 2 = 32 × 4096² × 2 ≈ 1 GiB。注意力矩阵更大(S = 4096 > 16d/h = 2048,分界点按 a·d/(c·h) 计算)。
- 训练时整句是已知的(teacher forcing),所有位置的输入一次给全,因果掩码保证各位置互不”偷看”,所以能并行;推理时第 t+1 个 token 的输入就是第 t 步的输出,必须先算出第 t 步——这是第 9 讲推测解码要打破的串行。
- 否定句只需要”not 和形容词是否同时出现”:自注意力让 not 和形容词互相看见,内容就够了,与顺序无关(“not good” 不会被写成 “good not”)。转折句的两个从句对调后词完全相同、标签相反,只有知道”哪个形容词在 but 之后”才能区分;没有位置信息时模型对顺序完全不敏感,最多猜对一半。
13. 延伸阅读 · Further reading
- Vaswani et al. (2017) Attention Is All You Need(NeurIPS):原论文;§3.2 的 √d 解释、§3.5 的正弦编码、§5 的训练配方、Table 3 的消融。
- Rush, The Annotated Transformer(https://nlp.seas.harvard.edu/annotated-transformer/ ):slides 第 26 页的代码走读,逐段对照论文的 PyTorch 实现。
- Alammar, The Illustrated Transformer:slides 第 12 页的配图来源,适合建立直觉。
- Xiong et al. (2020) On Layer Normalization in the Transformer Architecture(ICML):Post-LN / Pre-LN 的梯度分析,为什么 Pre-LN 可以不预热。
- Liu et al. (2020) On the Variance of the Adaptive Learning Rate and Beyond(ICLR,RAdam):Adam 早期方差大,为什么需要预热。
- Müller, Kornblith & Hinton (2019) When Does Label Smoothing Help?(NeurIPS):标签平滑对校准、蒸馏的影响。
- Korthikanti et al. (2022) Reducing Activation Recomputation in Large Transformer Models:每层激活显存的详细公式(与 06 号脚本的实测对照),第 18 讲会用到。
14. 下一讲预告 · Next lecture
这一讲的 Transformer 是 2017 年的版本。从它到今天的 LLM,结构上改动不多,但每一处都有讲究。第 7 讲(预训练 LLM)看三个代表:T5(编码器–解码器、“一切任务都是文本到文本”、span corruption 预训练目标)、GPT-3(规模、上下文学习、少样本提示、交替的稠密 / 稀疏注意力)、LLaMA(Pre-RMSNorm、SwiGLU、RoPE,以及 LLaMA-2/3 的 GQA)。我们会把第 1 讲的小 GPT 逐项升级成 LLaMA 式的结构,看每个改动在城市语言上带来什么,并算清楚它们的系统含义:RMSNorm 少一次归约,RoPE 不需要位置表,GQA 把 KV 缓存缩小好几倍。