一个 Transformer 块:FFN、残差、归一化、门控
本章问题:一个 Transformer 块里,除了注意力还有什么?为什么偏偏是这几样?
C3 结束时,「小明把苹果给了小王」的 5 个 token 已经各自从别的 token 那里取回了信息。可如果你打开 Qwen3.8-27B 的权重,一层里的参数大头根本不在注意力:一层 FFN 有 267,386,880 个参数,是一层全注意力的 2.55 倍;全模型 61.6% 的参数都在 FFN 里。每层还有两个归一化、两次「加回去」,注意力的 q_proj 还多输出了一倍。
这一章不直接端出这些零件,而是每次只问一个问题:只有注意力行不行?直接往上叠行不行?数值越叠越大怎么办?怎么让模型自己决定放行多少?每个零件都是被上一个方案在某个真实数字上的失败逼出来的。
本章问题链
| 课 | 本课一问 | 你会亲手做什么 |
|---|---|---|
| L1 FFN | 注意力取回信息之后,每个 token 自己还要做什么加工? | 让线性方案在「好吃 / 不好吃 / 难吃 / 不难吃」上全给 0.500,再用两个神经元的 FFN 精确分开 |
| L2 残差 | 把注意力和 FFN 一层层直接叠起来,叠深了会怎样? | 在种子固定的深层网络里看梯度逐层缩小或放大、token 逐层变得一模一样,再打开残差 |
| L3 归一化 | 残差流越加越大,怎么让每一层看到尺度稳定的输入? | 看 BatchNorm 在生成时把「苹果」归一成全 0;对照 LayerNorm 与 RMSNorm 的平移、缩放不变性 |
| L4 门控 | 门控:为什么现代 FFN 有三个矩阵,注意力的 q_proj 还要多输出一倍? | 在门 × 值平面上读 SwiGLU;用真实 config 对账「8d/3」规则;数清 Qwen3.8 的输出门 |
| ↳ 岔路 B1 | 参数到底花在哪? | 对照 safetensors 实测分项;拖上下文长度 T,找到注意力计算反超 FFN 的那一点 |
| 实验课 | 在我们自己的模型上,把块里的零件一个个拆掉、换掉 | 回放项目的真实训练曲线:去残差、去 FFN、Post-LN、LayerNorm、GELU |
主线定位
上游: C1 把文字变成 [T × 5120] 的向量,C2 让模型知道顺序,C3 让每个 token 从别的 token 那里取回信息。
本章: 注意力之外的全部零件——FFN 负责每个 token 内部的加工,残差让几十层叠得起来,归一化把每层输入的尺度拉回来,门控让模型自己决定放行多少。
下游: C5 问「块搭好了,参数怎么更新得又快又稳」;C8 兑现本章留下的疑问「Post-LN 为什么离不开 warmup」;C11 揭晓 48 层 Gated DeltaNet 的输出门,并把 FFN 拆成 MoE 专家;C15 讲 SiTU-GLU 的软封顶、mHC 与 AttnRes 这些对残差的新改法。
点任一已学环节,看这一步的输入 → 输出形状、参数量,以及它解决了什么问题(数字取自 Qwen/Qwen3.8-27B 真实 config)。
下面是本章要拆开的那个块。先整体走一遍,后面每一课只放大其中一步。
试三件事:① 走到第 4 步,注意主路上只有加法——子层的输出必须和输入一样是 [T × 5120],L2 会解释为什么这件事这么重要;② 走到第 6 步,FFN 在每个 token 上单独计算,token 之间在这里不交换信息;③ 走完最后一步,数一数 64 层一共往残差流里写了几次——这个数在 L2 会变成一个真实的方差问题。
演进图:每个零件都是被逼出来的
下面这张表是本章的地图:四行是四个零件(每行一课),五列是五代做法。每一格写这一代的做法,带「卡在」的格子写逼出下一代的那个真数字。每一课都会再出现这张表,并高亮那一课的那一行。
项目推进:把零件组装成块
对应 04-实践项目/MILESTONES.md 的 C4。代码已经在 minillm/model.py 里(Block、RMSNorm / make_norm、MLP),形状测试覆盖 768 种配置组合;你要做的是拍板和验收。
| 要拍板的决策 | 依据什么 | 在哪一课学到 |
|---|---|---|
| 归一化放在残差支路里面(Pre-Norm)还是外面(Post-LN)? | 首 100 步 grad_norm 的最大值;不带 warmup、学习率 1e-2 时会不会发散。为什么会这样,到 C8 再讲 | L3 → C8 |
| LayerNorm 还是 RMSNorm? | val loss 的差是否超出噪声底线(以 C14 为准:同配置 5 次重跑极差 0.016,差距小于 3σ ≈ 0.020 不下结论);tokens/s 差多少(单独运行 3 次取中位) | L3 |
| GELU 还是 SwiGLU? | 参数必须对齐:SwiGLU 中间维 688 对 GELU 1024,单层 528,384 对 524,288 | L4 |
| 残差输出投影的初始化要不要缩小? | 说清 model.py 里 is_residual_out 为什么按 1/√(2·n_layer) 缩小(4 层时 std = 0.02/√8 = 0.00707) | L2 |
验收标准
- 对照表:val loss、tokens/s、首 100 步 grad_norm 最大值、是否发散;对照组之间参数量差 < 1%。
- 手算基线参数量并与 README 的 3,688,704 对上:embedding 524,288(输入输出共享,只算一次)+ 4 × (注意力 262,144 + FFN 528,384 + 两个 RMSNorm 512)+ 最终 RMSNorm 256。
- 用项目实测的数字说出:去掉残差、去掉 FFN(参数对齐的 12 层纯注意力)各让 val loss 变差多少,差异是否超出噪声底线。数据见实验课。
坑与带走
| 坑 | 症状 → 后果 | 怎么办 |
|---|---|---|
| 残差连接漏了(旧 M2 坑表) | 层数一深,训练 loss 降得比浅模型还慢;纯注意力堆叠时各位置的输出越来越像 → 深度白加 | 每个子层都写成 x = x + sublayer(norm(x)),形状保持 [T × d] |
| 以为残差只是「防梯度消失」 | 看到梯度范数正常就以为不需要残差 → 深网仍然退化(He 2015 的 plain 网梯度正常,34 层仍比 18 层差) | 记住它的作用是让「什么都不做」成为默认,每层只学增量 |
| 语言模型里用 BatchNorm | 同一句话的输出随批里别的句子变化;生成时单个 token 的批内方差为 0 → 训练与推理不一致 | 用逐 token 统计的 LayerNorm / RMSNorm |
| Post-LN 不带 warmup 就上大学习率 | 文献里的表现是训练开头 grad_norm 飙升甚至发散;我们的小模型上表现为停滞而不是发散:学习率 ≥ 3e-3 时有没有 warmup 都停在字频基线 6.63;降到 1e-3,不 warmup 仍停在 6.63,加 100 步 warmup 就训到 4.87 | 用 Pre-Norm + 最终 norm;原因见 C8 第 2 课 |
| SwiGLU 与 GELU 对比时没对齐参数 | 中间维都取 4d,SwiGLU 多 50% 参数 → 结论不可信 | SwiGLU 中间维取 8d/3(向上取整到 16 或 256 的倍数) |
| RMSNorm 增益用标准写法,又对它做 weight decay | weight decay 把增益往 0 拉,等于慢慢把这一层的输出压没 | 用 zero-centered 写法(乘 1 + w,w 初始化 0),或把 norm 权重排除在 weight decay 之外 |
| 把 SwiGLU 的门当成 0 到 1 的开关 | 误读激活:SiLU 的门可以是负数、也可以远大于 1 | 只有 sigmoid 门(GLU、Qwen 的注意力输出门)在 (0, 1) 里 |
这一章带走的五句话
- L1 注意力在 token 之间搬信息,FFN 在每个 token 内部做非线性组合。
- L2 残差让每层只学一个增量:x_L = x_0 + 所有子层写入之和,梯度总有一条直通路径。
- L3 归一化只拿掉每个 token 的整体尺度:RMSNorm 除以均方根,残差流再大,子层看到的输入都回到同一个尺度。
- L4 门控就是两条投影逐元素相乘:一条决定放行多少,一条提供内容;它更好是实验选出来的,不是证明出来的。
- B1 参数大头在 FFN(Qwen3.8 为 61.6%),长上下文里计算大头却可能在注意力(T = 87,040 时两者相等)。
深入这一章
下面每条都在 2026-09-26 实际打开核对过,并写清它讲清了什么、对应哪一课。建议顺序:先玩 bbycroft 的三维模型,再看 3Blue1Brown 第 7 章,想把残差和 LayerNorm 亲手写一遍就看 Karpathy 那 13 分钟。
🎮 想更直观 10–30 分钟
🔬 想吃透原理 1–3 小时
📄 想读原文 按需
我替你排除了什么: 苏剑林《为什么 Pre Norm 的效果不如 Post Norm?》——本机访问 spaces.ac.cn 与 kexue.fm 都返回 403,无法核对内容,暂不收录,留到 C8 再核;Sebastian Raschka 的 LLM Architecture Gallery——访问返回 406,未能核对;Transformer Explainer——它不展开残差与 LayerNorm,C3 已经用过;「几分钟看懂残差 / 归一化」类速通视频与只贴公式不讲动机的转载——只能得到结论,得不到本章每一课要的「朴素方案为什么失败」。