上两篇讲了 Self-Attention 和 Transformer 的骨架。这一篇讲 2017 年的原始结构到 Llama 之间改了哪五处,每一处是被什么具体问题逼出来的。一共 6 个问题,每个都用能手算的小例子。内容来自我做的一套 34 课大模型课程的第 3 课。
- 本课在线阅读(免登录、手机可看):https://zhuangzhaoyeye.github.io/llm-course/lesson-3.html
- 全部 34 课:https://zhuangzhaoyeye.github.io/llm-course/
- 源码(MIT 许可):https://github.com/zhuangzhaoyeye/llm_course
问题 1:「我打你」和「你打我」三个字一样,模型怎么知道意思相反?
Self-Attention 本身不看顺序。给三个字各配一个向量:我 = [0.5, 0.3, 0.8],打 = [0.2, 0.9, 0.4],你 = [0.7, 0.1, 0.6]。两句话拿到的向量集合完全一样,只是排列不同,而"打"在位置 2 时,两句都是同一个 [0.2, 0.9, 0.4]。
原论文的补救是给每个位置加一个固定的正弦、余弦向量:
| 位置 | sin(pos) | cos(pos) |
|---|---|---|
| 1 | 0.84 | 0.54 |
| 2 | 0.91 | -0.42 |
| 3 | 0.14 | -0.99 |
"我打你"里,"我"在位置 1 变成 [1.34, 0.84, 0.8],"你"在位置 3 变成 [0.84, -0.89, 0.6];"你打我"里"你"在位置 1 是 [1.54, 0.64, 0.6]。两句话终于不同了。
但这个办法只告诉模型"你在第几位",没告诉它"你离我多远"。位置 1 和位置 3 的距离都是 2,两句话里这一对向量的点积却是 0.860 和 1.026,不相等,因为点积里混进了词本身的内容,没有一个干净的"距离 = 2"信号。
另外,正弦编码在训练长度之外会退化。ALiBi 论文(Press 等,2021)的实验里,用它训练的模型换到更长的输入上,表现明显变差。
问题 2:RoPE 怎么让同一个词在不同位置「看起来不一样」?
RoPE(旋转位置编码,Su 等,2021)换了个思路:不把位置加到向量上,而是把向量旋转一个角度,角度由位置决定。二维里旋转 θ 就是 x' = x·cosθ − y·sinθ,y' = y·cosθ + x·sinθ。
为什么旋转比相加好?因为相加会同时改变长度和方向。拿 [0.8, 0.6](模长 1.0)加上位置向量 [0.84, 0.54],得到 [1.64, 1.14],模长变成 2.0,方向也从 36.9° 偏到 34.8°,词本身的语义被搅动了。旋转只改方向、不改长度。
走一遍:同一个"打" = [0.8, 0.6],位置 1 转 0.01 弧度,位置 3 转 0.03 弧度。
| 旋转后 | |
|---|---|
| 位置 1 | [0.794, 0.608] |
| 位置 3 | [0.782, 0.624] |
两者点积 0.9998,几乎等于旋转前的 1.0,因为角度差只有 0.02 弧度。如果相隔 99 步,角度差 99 × 0.01 = 0.99 弧度,约 56.7°,点积降到 cos(0.99) ≈ 0.55。点积只取决于角度差,也就是相对位置。
要说清两点:第一,这里把角速度固定成 0.01 只是为了手算,真实的 RoPE 每个维度有不同的频率,从高到低跨好几个数量级。第二,旋转对任意角度都有定义,所以比"查位置表"更容易外推,但超过训练长度后模型没在那些角度上训练过,仍然会退化,长文本要靠位置插值之类的缩放方法。
问题 3:生成第 5 个字时,前 4 个字的 K 和 V 不能扔,显存撑不住怎么办?
GPT 逐字生成,生成第 5 个字要用到前 4 个字的 Key 和 Value。每次重算太慢,所以把它们存下来,这就是 KV Cache。问题是它多大。
用一个接近 7B 模型的配置:32 个头、每头 128 维、32 层、1024 个 token、fp16(每个数 2 字节):
| 方案 | 算式 | KV Cache |
|---|---|---|
| MHA(32 个头各自独立 K、V) | 32 × 2 × 128 × 1024 × 32 × 2 字节 | 512 MB |
| GQA(32 个头分 8 组,每组共享) | 8 × 2 × 128 × 1024 × 32 × 2 字节 | 128 MB |
| MQA(所有头共享 1 组) | 1 × 2 × 128 × 1024 × 32 × 2 字节 | 16 MB |
算式里第一个 2 是 K 和 V 两份,最后的 2 是 fp16 的字节数。GQA 省 75%,MQA 省约 96.9%。
MQA(Shazeer,2019)省得最多,但所有头被迫看同一份 K、V。论文摘要的结论是:解码明显更快,质量只有"轻微下降"(minor quality degradation)。我没能在论文里读到具体的困惑度表,二手资料里有"29.9 对 30.2"的说法,我不确定,所以不写进正文。GQA(Ainslie 等,2023)折中:把 Q 头分成若干组,组内共享 K、V,论文称质量接近 MHA。
Llama 2 里只有 34B 和 70B 用了 GQA,7B 和 13B 仍是标准多头;70B 是 64 个 Q 头、8 组 KV。
问题 4:LayerNorm 的「减均值」和 ReLU 的「截断负值」能砍吗?
先看 LayerNorm。拿向量 [1.2, -0.8, 0.5, -1.5]:
- 均值 = (1.2 − 0.8 + 0.5 − 1.5) / 4 = −0.15,减掉得 [1.35, −0.65, 0.65, −1.35]
- 标准差 = √1.1225 ≈ 1.059,相除得 [1.27, −0.61, 0.61, −1.27]
RMSNorm(Zhang 和 Sennrich,2019)砍掉"减均值",只保留缩放:
- RMS = √((1.44 + 0.64 + 0.25 + 2.25) / 4) = √1.145 ≈ 1.07
- 直接相除得 [1.12, −0.75, 0.47, −1.40]
少算一步中心化。论文摘要的说法是:表现与 LayerNorm 相当,运行时间减少 7% 到 64%,视模型而定。这里有个常见误传要澄清:LayerNorm 是对每个 token 自己的特征维做统计,本来就不需要跨卡同步,需要跨卡同步统计量的是 BatchNorm。
再看激活函数。ReLU 把 [0.6, −0.3, 0.9, −0.2] 变成 [0.6, 0, 0.9, 0],两个负值的信息整个丢了。SwiGLU(Shazeer,2020)加了一个"门":用一条分支算出每一维该放行多少,再乘到另一条分支上,所以负值是被部分抑制而不是直接清零。Shazeer 的论文里,在 C4 文段填充任务上训练 524k 步后,对数困惑度是 SwiGLU 1.636、GELU 1.679、ReLU 1.677,SwiGLU 最低。课程里另有一张"ReLU 9.7、GELU 8.9、SwiGLU 8.5"的表,那是示意值,只表示相对高低,不要当成实测。
问题 5:为什么 Post-Norm 难堆深,Pre-Norm 能堆到几十层?
位置不同。原始 Transformer 是 Post-Norm:Norm(x + 子层(x)),Norm 在残差之后,梯度回传要穿过它。Pre-Norm 是 x + 子层(Norm(x)),Norm 在支路里,残差主干是一条直通的路。
假设每经过一个 Norm,梯度被乘以 0.5(这是示意,真实衰减因模型而异):
| 层数 | 0.5 的 N 次方 |
|---|---|
| 4 | 0.0625 |
| 12 | 0.00024 |
| 24 | 5.96 × 10⁻⁸ |
| 32 | 2.33 × 10⁻¹⁰ |
Pre-Norm 里梯度沿残差直接回去,不被 Norm 压缩。Xiong 等人(2020)研究的是为什么 Post-LN Transformer 必须用学习率预热:他们发现这和 Norm 的位置密切相关。按论文的结论,把 Norm 移进残差支路(Pre-LN)后,在他们测的任务上可以去掉预热,效果仍与基线相当。
要诚实说明的一点:课程里写"Post-Norm 超过约 24 层就难以为继、Pre-Norm 能到 80 层",这是经验说法,我没有找到一手来源给出这样的硬阈值。实际情况是,BERT-large 本身就是 24 层 Post-Norm,训得动,只是需要小心调预热和学习率。Llama 2 的 70B 确实是 80 层,这一点可以查到。
问题 6:五处改造能同时装进同一个 Block 吗?
能,因为它们作用在 Block 的不同环节,互不抢位置:
| 改造 | 原始 Transformer (2017) | 现代 Llama | 一个数字 |
|---|---|---|---|
| 位置编码 | 正弦绝对位置 | RoPE | 点积只取决于角度差 |
| 注意力 | MHA | GQA | KV Cache 512 MB → 128 MB |
| 归一化 | LayerNorm | RMSNorm | 运行时间减少 7%~64% |
| 激活函数 | ReLU | SwiGLU | 对数困惑度 1.636 对 1.677 |
| Norm 位置 | Post-Norm | Pre-Norm | 梯度可直达第一层 |
要注意各项不是同时出现的:比如 GQA,Llama 2 里只有 34B 和 70B 用了,7B 和 13B 仍是标准多头注意力。
小结
| 问题 | 结论 | 一个数字 |
|---|---|---|
| 怎么知道词序 | 绝对位置只告诉"第几位" | 点积 0.860 对 1.026,不是干净的距离 |
| RoPE | 旋转只改方向,点积只剩角度差 | 相隔 99 步,点积降到 0.55 |
| GQA | 分组共享 K、V | KV Cache 省 75% |
| RMSNorm | 少算一步中心化 | 运行时间减少 7%~64% |
| SwiGLU | 门控,负值部分保留 | 对数困惑度 1.636 对 1.677 |
| Pre-Norm | Norm 移出残差主干 | 0.5³² ≈ 2.3 × 10⁻¹⁰ |
参考与说明
- 正弦位置编码:Vaswani 等,Attention Is All You Need,2017,https://arxiv.org/abs/1706.03762
- RoPE:Su 等,RoFormer,2021,https://arxiv.org/abs/2104.09864
- ALiBi:Press 等,Train Short, Test Long,2021,https://arxiv.org/abs/2108.12409
- MQA:Shazeer,Fast Transformer Decoding: One Write-Head is All You Need,2019,https://arxiv.org/abs/1911.02150 (我只读到摘要和翻译实验的设置,没读到语言模型的困惑度表,所以文中没引用具体数字)
- GQA:Ainslie 等,2023,https://arxiv.org/abs/2305.13245
- RMSNorm:Zhang 和 Sennrich,2019,https://arxiv.org/abs/1910.07467
- SwiGLU:Shazeer,GLU Variants Improve Transformer,2020,https://arxiv.org/abs/2002.05202
- Pre-LN:Xiong 等,On Layer Normalization in the Transformer Architecture,2020,https://arxiv.org/abs/2002.04745
- Llama 2:Touvron 等,2023,https://arxiv.org/abs/2307.09288
- 文中的数字(位置编码值、RoPE 旋转、KV Cache、LayerNorm 与 RMSNorm、梯度衰减)我都用代码独立复算过。SwiGLU 的 1.636 / 1.679 / 1.677 我对着 Shazeer 2020 的 Table 1 核对过(524,288 步,C4 文段填充任务,保留集对数困惑度);RMSNorm 的"7%~64%"来自论文摘要;Llama 2 只有 34B 和 70B 用 GQA、70B 是 80 层 8 组 KV,我对着论文和模型配置核对过。标"示意"的是为了演示取的数字,不是真实模型的输出。
- 说明:这套课程由 AI 在我设计的规则下生成,再经过脚本检查和独立审查;审查时发现过不少问题,已写在仓库 README 里。发现错误欢迎在仓库提 Issue,写清位置,我会逐条核实。
继续学
完整课程有 34 课,上一篇是第 2 课「Transformer 模型结构全景」,下一课讲 Tokenization。每张卡片后面有 AI 助教可以追问(每人每天有次数限制)。