Transformer Block 设计哲学:从『看似冗余』到『刚好够用』
写作日期:2026-06-25
面向读者:知道什么是 attention 和 FFN,但不清楚“为什么里面每个矩阵都是必要的”
核心问题:transformer block 看起来只是几行矩阵乘法,里面真的没有“凑数”的设计吗?
行文约定:中文为主;英文术语保留(Q/K/V、attention、residual、block、head、FFN、SwiGLU、gating 等);第一次出现的英文术语给全称
引子:看起来全是冗余
本文是三篇系列里的第一本账。 后面两本是《Decoder-Only 大模型的推理过程详解》(运行期怎么跑得更快)和《同一块积木的两本账》(把两本账逐条对上)。本文讲第一本:训练期为什么这样设计。
如果你第一次翻开 transformer 的实现代码,大概率会被“多余感”劝退:
- 既然已经有了 token 的 embedding,为什么还要投影出 Q / K / V 三个矩阵?
- 既然 Q / K / V 已经做了这么多事,为什么后面还要再接一个 FFN(Feed-Forward Network,前馈网络)?
- 既然 attention + FFN 已经够复杂,为什么 FFN 内部还要再拆出“门控(gating)”这种诡异结构?
- 既然一个 block 已经这么大,为什么一个 block 里只放一层 attention 和一层 FFN,而不是堆叠两层 FFN?
- 既然一个头已经能看全序列了,为什么还要分成 个头(Multi-Head Attention,多头注意力)?
- 既然残差已经把梯度接回来了,为什么每个子层外面还要再包一层归一化(Layer Normalization,层归一化)?
每一处看起来都像“工程冗余”。但每当你真的把某一项删掉、改掉、合并掉,模型就会毫无悬念地退化。这不是代码的偶然,而是架构设计的内在必然。
这篇文章要做的事,就是从这些“看似冗余”的设计出发,把它们拆到零件级别,讲清楚每个零件为什么必须在那个位置。读完之后,你应该能自己回答:
- Q / K / V 三个矩阵各自承担什么不可替代的语义角色?
- 一次 attention 之后,一个 token 到底发生了什么不可逆的变换?
- FFN 内部那三个矩阵、那个“门控”、那个“先升维再降维”的设计,分别在解决什么问题?
- 业界为什么全都收敛到 1 : 1?这是被证明的最优解,还是工程惯例?
- 模型深度靠什么扩展——是 block 内部更厚,还是 block 数量更多?
- 多头到底在分什么工?为什么说“每个头都不可替代”这句话是错的?
- 归一化该放在子层前面还是后面?为什么主流模型几乎都改用了后一种?
母题预告:所有“看似冗余”的设计,背后都是同一个原则——让模型在数据里学到合适的“子空间分工”。这不是过度设计,是架构归纳偏置(inductive bias)和统计效率(statistical efficiency)的工程折中。
先看一眼要拆的到底是什么。绿色是本文 1–7 节逐个拆开的对象:
三处岔路,正好对应本文的三段:为什么 Attention 里要拆成三个矩阵(第 1、2 节)、为什么它只“借”不“做”(第 3 节)、为什么借完还得再加工(第 5 节)。红色那一步是第 4 节的产物。
1. 把 Q / K / V 三个矩阵合成一个会怎样
先打掉一个最显眼的“冗余”。
1.1 一个看起来合理的极简方案
假设你不相信 Q / K / V 必须分开,最朴素的怀疑会导向一个极简方案:
“token 的 embedding 已经包含了语义。直接拿 自己和 context 里的 做点积得到权重,再用这个权重对 context 的 V(其实就是 本身)做加权求和——这不就够了吗?”
写成公式:
或者更直白地说:令 (单位矩阵)。
这个方案能跑——PyTorch 一行能写,CUDA 能编译,前向反向都不报错。
1.2 简化方案的三个致命问题
但它在实际效果上完全不可用。问题出在三个层面:
问题 1:表达瓶颈——一个向量塞三个角色
这个 维向量,在 attention 里要同时承担三个角色:
| 角色 | 含义 |
|---|---|
| 查询者 | “我要查什么” |
| 被查者 | “我如何被检索到” |
| 贡献者 | “我被关注时传递什么信息” |
把这三个角色绑死在同一个向量上,等于强迫一个向量同时优化三个互相冲突的目标。几何上看,就是强迫三个几乎正交的子空间塞进同一组基——不可能同时最优。
最直观的反例来自 BERT 的 attention 可视化研究(Clark et al. 2019, “What Does BERT Look At?”)。研究者把训练好的 BERT 某一层的 Q / K / V 投影回词表空间,看每个向量的最近邻:
1 | X[bank] → [bank, river, money, account, ...] ← 静态语义 |
三个向量的最近邻几乎不重叠。如果它们来自同一个 、没有 Q/K/V 投影,模型根本拿不到这些不同的“询问方式”。
问题 2:关系对称——主语和宾语分不开
是对称函数:
这意味着 attention 矩阵天然对称:token 关注 token 的强度永远等于 token 关注 token 的强度。
但语言里这种“对称关注”几乎不存在。看一个具体例子:
“猫追老鼠” vs “老鼠追猫”
两个句子的 token 集合完全一样({猫, 追, 老鼠}),区别只在主语和宾语的角色。“猫” 应该强烈关注“追”(作为施事者),而“追” 应该分别关注“猫”和“老鼠”(作为谓词的两个论元)。在“老鼠追猫”里,关系完全反转。
如果打分函数对称,模型算出的“猫–追”这对的分数就与谁是施事者无关——两个句子里这对 token 得到的分数是同一个数。
这里要说句公道话:单靠“对称”不足以推出“模型分不开这两个句子”。因果掩码已经让 时 根本不存在,再加上绝对位置编码,模型完全有办法通过“位置 0 是主语”这类线索把两者分开。位置编码破不了关系对称(2.3 节),但它能补上「谁在前面」这一半。 真正丢掉的,是「相似度本身要依赖角色」这一维表达力——这才是 换来的东西。
Q / K 投影的本质作用,就是让这个非对称函数成为可能——这一点从公式上已经成立(见第 2 节 的推导),不需要额外的统计来背书。
我原本在这里引了一个「真实模型里 与 相关性只有 0.3-0.5」的统计,核课时没能找到出处,已撤掉。相近的数字在别处出现过(比如 attention 熵与 attention 距离的相关系数约 0.61),但那是另一件事,不能拿来当非对称的证据。
问题 3:秩瓶颈——attn 输出无法突破 token 维度的限制
如果 ,那 attn 输出只能由 自身的线性组合而成:
也就是说,输出的所有信息都来自 context 中 token 的 embedding。模型没有升维、没有跨维混合、没有学到“比 更高阶的特征”。
这相当于把模型的表达力强行限制在“token embedding 的仿射空间”里——任何超出这个空间的结构都学不到。
1.3 Q 的独立角色:定义“我要查什么”
把 Q 单独拆出来,让每个 token 学会生成“我最想问的问题”向量。
一个 token 的 X 是它的身份(“我是 bank,名词,可能指河岸也可能指银行”),而它的 Q 应该是它的意图(“我想知道我的主语是什么”)。这两种向量在几何上几乎正交——身份和意图本来就是两个独立维度。
经典例子来自 attention 可视化:BERT 某一层有一个 head,其 Q 空间专门编码“主语 / 宾语 / 谓词”的句法位置信号。这个信号几乎不存在于 X 空间——同一个 token 在主语位置和宾语位置的 X 表示差异很小,但 Q 表示差异巨大。
1.4 K 的独立角色:定义“我能被怎么查”
K 投影让每个 token 学会“我如何能被检索到”。
X 是 token 的静态特征(词性、形态、词频、语义),K 是 token 的可被检索特征(支配关系、论元类别、情感极性、语篇角色)。这两个特征子空间在 BERT 同一层的不同 head 里被独立学习:
- 一层中某些 head的 K 子空间专门编码句法依存(动词 → 主语 / 宾语)
- 另一些 head的 K 子空间专门编码共指(it → the cat)
- 还有一些 head 的 K 子空间专门编码长距离回指(“虽然” → “但是”)
这些结构化信号不在 X 的几何里直接可见——没有 K 投影,模型根本拿不到这些特征。
1.5 V 的独立角色:定义“我能贡献什么”
V 投影容易被忽略,但可能是最关键的一个。
V 决定的是“我被关注时,把什么信息传出去”。看起来和 X 应该差不多(“我是什么就传什么”),但实际差别巨大。
反例 1:the 和 , 这些“无意义 token”
the、a、, 这些 token 几乎没语义,但它们的 V 向量包含丰富的句法信号:
the的 V 可能编码“当前位置是主语 / 宾语”——决定后面动词的形式,的 V 可能编码“前面是个从句的边界”——影响后续 attention 的窗口选择
这些结构性信号和 the / , 本身的语义没有强对应。X 的几何里看不到,只有专门学一个 V 投影才能稳定输出。
反例 2:K 和 V 能不能合并?
直觉上 K 和 V 都“对 token 表征”,能不能共用一个投影?
诚实地说:我能找到的相关实证不支持一个这么干净的分级。 Voita et al. 2019 做的是用 LRP 给每个 head 打重要性分,然后剪枝,头条结果是「48 个 encoder head 剪掉 38 个,BLEU 只掉 0.15」——它证明的是大部分 head 无关紧要,而不是「V 比 K 比 Q 更重要」。那篇论文里没有共享 的实验,也没有把 Q/K/V 维度各砍一半的对照。
所以这里能确定的只有结构性那一条:K 和 V 服务的是两件不同的事——K 决定「谁被找到、权重多大」,V 决定「找到之后传递什么内容」。前者的目标是可分辨,后者的目标是可传递,二者的最优子空间没有理由重合。至于「V 的影响比 K 大多少」,那需要自己去跑消融,不要引用二手数字。
有例外:multi-query / multi-group attention(MQA / GQA,LLaMA / Qwen / Mistral 都支持)会把多个 head 共用一个 K 和 V。这是推理时减小 KV cache 的工程优化,不是训练时的架构选择。
为什么 K / V 能共享而 Q 不能,正好是第 1、2 节那条论证的镜像:Q 决定“每个头往哪里看”,一旦共享,所有头就会问同一个问题,第 1.7 节那套路由的多样性当场消失。 反过来,K 决定“我能被怎么找到”、V 决定“我传递什么”,而这两件事天然只和 token 有关、和“这是第几个头”无关——跨头共享的信息损失小得多。
省下来的显存是可算的:KV Cache 的字节数只和 K/V 头数成正比,32 个头分成 4 组 KV 就是 1/8。具体数字见《Decoder-Only 大模型的推理过程详解》的 4.3 节;而为什么 Q 偏偏不能共享、共享了会失去什么,另一篇《同一块积木的两本账》1.3 节有完整推导。
1.6 小结:三个矩阵 = 三个独立可学习子空间
把 Q / K / V 拆开,本质是给模型三个独立的可学习投影空间:
| 矩阵 | 学习的子空间 | 失败时的表现 |
|---|---|---|
| “询问方式” | 模型分不清不同句法角色的关注需求 | |
| “可被检索性” | 模型找不到正确的依存 / 共指关系 | |
| “传递内容” | 模型无法传递结构性信号(the / , 等) |
任何“简化方案”(共用、合并、identity)都会让模型丢掉至少一个维度的表达力。注意其中两条论证强度不同:对称性那条是数学上必然的( 对称);「子空间不重合」那条是经验判断,我没能给它找到干净的实验支撑,按经验理解就好。
Q / K / V 拆开,不是为了“算 attention 权重”——权重只是副产品。真正的目的是“让三种语义角色各自有独立的可学习子空间”。
1.7 同一母题的另一次复制:Multi-Head
Q / K / V 的三路分工只在单头的层面讲过。Multi-Head Attention(多头注意力)做的事,是把这三路投影分成 组,每组各自再做一遍分工:
动机很直接:语言里的依赖关系不是一个模子。句法依存、代词指代、局部搭配、长距离回指,各自需要的关注模式完全不同。一个头只有一个 维的子空间,装不下这么多模式。
和 CNN 多个卷积核的类比是常用的,但只能当直觉辅助——卷积核共享权重且作用在局部窗口,多头是每个头一套独立权重、作用在全局。两者的相似之处仅限于“一个组件不足以覆盖所有模式”这个泛泛的结论。
一个容易被忽略的点:多头不花额外的参数。 在固定 的前提下, 个头每个 维, 个投影矩阵加起来仍然是 。所以多头是一次免费的结构变换——同样的参数量,换一种切分方式。这和第 9 节「深度靠堆 block」是同一种思路:不加钱,改分配。
所以 §1 节的母题在这里原样复制了一遍:每个头各自再分工一遍, 个头就是 份互不重叠的功能子空间。
但这一条必须降级
到这里很容易顺手写出一句“每个头都学到了独特的关注模式,缺一不可”。这句话有一半是错的,而且错的正是“缺一不可”。
“每个头学到了不同的模式”是描述性观察——你去看训练好的模型,确实能看到不同头呈现不同的关注结构。但它不是必要性论证:如果每个头都不可或缺,那就应该剪掉任何一个都掉点。事实并非如此。
Michel et al. 2019(“Are Sixteen Heads Really Better than One?”)的结果值得记住:
| 设置 | 剪枝结果 |
|---|---|
| WMT 翻译 encoder 自注意力(16 头 / 层,共 96 个) | 逐个移除后只有 8 个造成统计显著的 BLEU 变化,其中约一半剪掉之后 BLEU 反而上升 |
| BERT 微调 MultiNLI | 可剪掉最多 40% 的头,GPU 推理提速 17.5% |
| encoder–decoder 的 cross-attention | 剪掉超过 60% 直接崩 |
这和 1.5 节引的 Voita et al. 2019(“48 个 encoder 头剪掉 38 个,BLEU 只掉 0.15”)指向同一件事:头是高度冗余的,而且冗余程度极不均匀。
诚实的分级:
- 多头优于单头(固定总参数量下)——经验上稳定成立
- 每个头学到了不同的关注模式——描述性观察,不是设计必要性
- “ 个头不可替代”——不成立,连自注意力里都有一大半是冗余的
真正有意思的是第三行那一条的例外:cross-attention 剪超 60% 就崩,而自注意力能剪 40% 还不掉点。同一个 block 里同样的组件,冗余度差了一个量级——这说明「多头有几份冗余」取决于那一部分在干什么活,不是架构决定的。
最后一句正好接到 §1.5 的脚注:MQA / GQA(Multi-Query / Multi-Group Attention)就是这条冗余性的极致利用——既然 K / V 跨头共享还能保住质量,那就共享。区别是 MQA / GQA 是推理时的工程选择,训练时业界仍然用标准的多头。
到这里,§1 节的母题算讲完了:单头里 Q / K / V 三个角色要分开,多头里每个头又要各自再分一遍。但这两种“分开”都发生在子空间方向上。另一个问题还悬着——打分函数本身能不能也简化?把 Q 和 K 合并会丢掉什么,这是第 2 节的题目。
2. 如果 Q 和 K 共用同一个矩阵
Q / K / V 都拆开了——一个自然的后续问题是:Q 和 K 能不能再合并?毕竟它们都是“双向沟通”的双方。
2.1 对称性陷阱的精确化
上一节我们提过:——打分函数天然对称。Q / K 投影让这个对称被打破,但到底是怎么打破的?
把 Q / K 拆开的数学表达:
令 ,这就是一个 矩阵。当 不是对称矩阵时:
也就是说,“i 查 j 的分数”和“j 查 i 的分数”可以不同。
对称性破缺 = 矩阵 不是对称的 = 和 独立。
如果 ,那 必然是对称的(任意矩阵乘自己的转置都是对称的)。所以Q / K 共用必然导致 attn 矩阵对称。
2.2 关系不对称的语言现象
把“主谓宾”这类句法不对称放大看几个真实句子:
| 句子 | token | token | 查 的强度 | 查 的强度 |
|---|---|---|---|---|
| “猫追老鼠” | 猫 | 追 | 强(主语→谓词) | 弱 |
| “猫追老鼠” | 追 | 老鼠 | 强(谓词→宾语) | 弱 |
| “猫追老鼠” | 猫 | 老鼠 | 弱 | 中等(经由谓词) |
| “老鼠追猫” | 老鼠 | 追 | 强 | 弱 |
| “老鼠追猫” | 追 | 猫 | 强 | 弱 |
注意同一对 token(“猫”和“追”)的相互关注强度在两个句子里方向不同——“猫”作主语时强烈关注“追”,但“追”作主语时不关注“猫”(关注的是“老鼠”)。
这张表用的是双向注意力的直觉框架。decoder 里因为有因果掩码,只有“靠后的位置看靠前的位置”那一个方向会被真正算出来,右列描述的是如果两边都算会得到什么。要看的不是某个具体数字,而是:同一对 token 的相关性分数不该在两个句子里取同一个值——这正是非对称函数提供的。
如果 attn 矩阵对称,这些方向性完全消失。模型只能看到“猫和追的相互关注强度”是个数字,但不知道谁主动、谁被动。
2.3 位置编码为什么破不了关系对称
“加位置编码不就让 不对称了吗?”
这里有一个常见误解。位置编码(无论 sinusoidal、learned 还是 RoPE,Rotary Position Embedding,旋转位置编码)破的是位置对称——它让 和 的 embedding 因为位置不同而不同。
但关系对称和位置对称是两件不同的事:
| 对称类型 | 描述 | 位置编码能修吗? |
|---|---|---|
| 位置对称 | 和 的位置信息混合到 attention 分数里 | 能修 |
| 关系对称 | (i 查 j 和 j 查 i 同分) | 不能修 |
为什么?位置编码给每个位置加一个单向偏移(i 位置加 ,j 位置加 ),但点积 这一项在 和 里完全一样——两个 X 位置编码不同,但点积的输入集合是 ,顺序对点积没影响。
只有非对称函数才能区分 和 :
- ,其中 不必对称
- 任何输入顺序不交换的 MLP:
- 任何包含 或 的特征(差分 / 元素积)
位置编码解决的是“哪个位置的 token 更重要”,关系对称解决的是“谁主动查询谁”。这是两个独立问题。
2.4 非对称双线性 = 表达自由度的体现
更深一层:Q / K 投影的真正作用是让“相似度”成为一个可学习的双线性函数。
把打分写成统一形式 ,差别全在 :
| 参数量 | 说明 | |
|---|---|---|
| (固定点积) | 0 | 写死,逐元素积再求和 |
| 对称且可学 | 落在这里 | |
| 任意且可学 | 落在这里 |
注意中间那行: 并不会让 变成 ,而是让它变成一个对称可学矩阵——所以它丢掉的是非对称方向( 个自由度),而不是全部可学性。这个数字比笼统说「自由度从 涨到 」准确得多。
这就是为什么 和 独立学—— 是模型学到的“最优相似度度量”。这个度量在语言里远不是简单的余弦相似度——它要捕捉句法、语义、语篇、修辞等多种关系。
Q / K 独立 = 把相似度从「对称可学」放宽到「任意可学」= 多出 个非对称自由度。
3. 一个 token 在 attention 里到底经历了什么
Q / K / V 各司其职后——现在跟着一个 token 走完一次 attention,看它经历了什么不可逆的变换。
3.1 attention 输出的真实形态
对于一个具体的 token (位于句子“猫追老鼠”的“猫”位置):
- 求和遍历所有 (causal mask,未来位置看不到)——这把锁为什么必须存在,在《Decoder-Only 大模型的推理过程详解》第 1 节。本文后面所有公式都默认它成立,不再重复论证
- 权重 由 Q · K 决定
- 加权对象是
3.2 “补丁生成器”视角
把 attention 输出的几何意义说清楚: 是一个 维向量,由 context 中所有可见 token 的 V 向量线性组合而成。
这意味着:
attention 输出不是 token 的“新身份”,而是“应该往 上加的补丁”。
打个比方:
- = 你今天穿的衣服
- context = 你衣柜里的所有衣服
- = 你看每件衣服的“想不想搭配它”
- = 那件衣服“能给你贡献的风格元素”(颜色 / 材质 / 风格)
- attention 输出 = “今天应该加的搭配补丁”(围巾 / 帽子 / 鞋子)
注意:补丁 = 一组风格元素的加权和,不是“换一套衣服”。
3.3 残差连接:保留身份信息
如果 attention 输出只是补丁,那 token 的“原始身份”()怎么保留?
答:残差连接(residual connection):
跳过 attention 直接加到输出上。这意味着:
- 永远无损保留在信号流里
- attention 的贡献叠加在 上,不替代它
- 无论多少层,身份信息都不会消失(理论上)
这就是为什么残差不能去掉:
- 去掉残差 = 模型每层完全重写 token 表征
- 去掉残差后,每层都要从零重建表征,层数一多梯度就传不回来——深度模型确实训不动(浅层的无残差网络还能训)
- 残差是深度 transformer 能训练的关键
3.4 attention 不重写 token,只生成补丁
总结一下 attention layer 的精确角色:
| 任务 | 谁来做 |
|---|---|
| 跨位置信息混合(“看 context”) | attention |
| 保留 token 自身身份 | 残差 |
| 更新 token 特征 | 不归 attention 管(第 5 节讲) |
attention layer 的工作 = “在保留原身份的前提下,从 context 里借信息”。它不重写 token,只在原表征上叠加补丁。
这个视角是理解第 5 节“FFN 存在的必要性”的关键。
3.5 attention 的局限性
既然 attention 只做“借信息”,那它做不到的事就很明确:
| 它做不到 | 原因 |
|---|---|
| 在单个位置做深度特征提取 | 它的输出 = context 中 V 的加权和,只混合、不加工 |
| 学习“哪些维度需要更新” | 所有通道统一加权,没有“通道注意力” |
| 学到层级化的抽象特征 | 一次 attention 只是“借”一层信息,没有“思考”过程 |
这就是为什么FFN 必须存在——attention 留下的“特征加工”空白,必须由另一层来填。
不过残差这条支路本身还有一笔账没算清:它保证了梯度能一路传下去,代价是信号尺度也一路加上去、没人回收。第 4 节算这笔账。
4. 归一化:残差流的另一半
上一节把残差讲完了。但“残差 + 归一化包裹”经常被当成一个整体叫,实际上它们解决的是两件不同的事:
| 解决什么 | 去掉之后 | |
|---|---|---|
| 残差(3.3 节) | 梯度能不能回传到浅层 | 深度模型训不动 |
| 归一化(本节) | 信号流的尺度能不能被控制 | 还能训,但难训 |
最后一行这个差别值得强调:残差是结构上必需的,归一化不是——原始 Transformer 之前有更深的前馈网络,只是每一层都得手工配一个合适的初始化和学习率。
4.1 残差的副作用:尺度一路叠加
残差的形式是 。它让梯度跳过子层直接回传,代价是残差流上只有加号、没有衰减。
一个 层的模型,残差流上有 次累加——每个 block 两条支路(attention 一条、FFN 一条)。初始 embedding 被加进去之后再没有任何环节会把它压小,层数越深,它在信号流里占的比例越低。
深了之后怎么办? 逐层手工调学习率不现实——各层接收到的信号大小根本不同,统一的学习率对深层过大、对浅层过小。归一化接管的就是这件事:把每个 token 的特征向量重新标定到固定尺度,不管它进来时是多大。
残差保证梯度能穿过每一层;归一化保证每一层拿到的信号是可比的。
4.2 Post-LN 与 Pre-LN:LN 放在支路前还是支路后
原始 Transformer(Vaswani et al. 2017)用的是 Post-LN,后来的模型几乎全部改成了 Pre-LN:
1 | # Post-LN(原始 Transformer):归一化作用在「支路输出 + 残差」的和上 |
区别不在“有没有归一化”,而在归一化的是残差流本身,还是支路的输出:
- Post-LN 归一化的是和。 残差流从头到尾没有被任何东西重标定过,每个 LN 都在压制一条已经很大的支路输出。
- Pre-LN 归一化的是输入。 子层收到的永远是尺度可控的输入,残差流则带着初始值一路往上加。
Pre-LN 为什么赢了。 Xiong et al. 2020 用平均场理论给出了解释(ICML 2020):在初始化时,Post-LN 网络中 “the expected gradients of the parameters near the output layer are large”——离输出层越近,参数的预期梯度越大。直接对这个梯度用大学习率,训练就不稳定。原始 Transformer 那个前 4000 步的预热(warmup),正是为了躲过这一段。
Pre-LN 则让 “the gradients are well-behaved at initialization”,所以预热可以去掉。论文报告,去掉预热后能打平基线,而训练时间和需要调的超参都显著减少。
一个需要澄清的流传说法:现代 LLM 仍然普遍用 warmup,但它现在不再是架构稳定性的必要补偿——Pre-LN 网络去掉它也能训。把 warmup 的理由说成“Transformer 没有递归结构,所以需要一段稳定期”,是把效果当成了机制。真正的机制是 4.3 节这条,而它属于 Post-LN 的问题。
LLaMA 论文 §2.2 把这个选择讲得很直白:
“Pre-normalization [GPT3]. To improve the training stability, we normalize the input of each transformer sub-layer, instead of normalizing the output.”
方括号里的 [GPT3] 说明这不是 LLaMA 的首创。
4.3 Pre-LN 的代价:得补一个 final norm
Pre-LN 有个直接的副作用:既然归一化作用在子层输入,残差流到最后也没有被任何东西约束过。最后一层的输出是一个尺度未受控的向量,直接送进输出层做 softmax 并不合适。
所以 Pre-LN 变体要在预测层之前额外加一个归一化——Xiong et al. 2020 的 Pre-LN 结构里明确包含这一层。这就是现代实现里那句孤零零的 x = norm(x) 的来历。
对照 nanochat 的 Block.forward,两行正好就是 Pre-LN 的标准写法:
1 | def forward(self, x, ve, cos_sin, window_size, kv_cache): |
而在 GPT.forward 里,block 循环结束、送进 lm_head 之前,同样补了一次 x = norm(x)。
4.4 RMSNorm:砍掉减均值
LayerNorm(Ba et al. 2016)做两件事:
- 中心化——减去特征维的均值
- 重标定——除以标准差,再乘一个学到的 gain、加一个 bias
Zhang & Sennrich 2019 的主张是:中心化这一步可以省掉。他们的理由是均值归一化 “does not reduce the variance of hidden states or model gradients”——减均值并不降低隐藏状态或梯度的方差,因此不提供额外的稳定性。
RMSNorm(Root Mean Square Layer Normalization)只保留第 2 件事:
省掉的是:一次求均值、一次减法、整个 bias 参数。论文报告性能与 LayerNorm 相当,运行时间在不同模型上减少 7%~64%。
LLaMA 也明确采用,并给出了出处:「We use the RMSNorm normalizing function, introduced by Zhang and Sennrich (2019)」。
一个实现层面的观察:nanochat 的
norm()是个无参数的函数——
1
2 def norm(x):
return F.rms_norm(x, (x.size(-1),))
F.rms_norm的weight参数留空,所以连 Zhang & Sennrich 保留的那个学到的 gain 都没有。gpt.py里的nn.Parameter只有四个(resid_lambdas、x0_lambdas、smear_lambda、backout_lambda),归一化一个可学习参数都没有。想把它和 LLaMA 版对齐,还差一个 gain。
4.5 QK-Norm:把归一化再往里推一层
上面所有归一化都作用在子层的输入输出上。还有一种做法直接作用在 attention 内部的 Q 和 K 上——点积算之前,先各自归一化:
1 | # nanochat/gpt.py,CausalSelfAttention.forward 里 |
这一步压住的是 的幅度。Q、K 的范数被固定之后,logits 就不容易出现大到让 softmax 饱和的值。
这里有一个容易搞混的点:QK-Norm 对 Q 和 K 做的处理是对称的,但它完全没有改变 Q、K 是两次独立投影这件事。它压的是幅度,不是子空间——第 2 节那条“ 才有非对称相似度”的论证,一点都不受影响。
归一化管的是信号的尺度,子空间分工管的是信号的方向。这两件事正交,可以各做各的。
第 4 节到此结束——残差流这一半的账算清了:梯度回传靠残差,尺度可控靠归一化,两者互不干涉。而 3.5 节留的那个空白还摆在那儿:注意力只“借”,从不“加工”。补上它的就是第 5 节的 FFN。
5. 既然 attention 已经做这么多,FFN 是不是多余
走完 attention 之后,token 的表征确实“丰富”了——但这就够了吗?答案显然是否。
5.1 attention 做不到的事
回到“补丁生成器”的视角:attention 输出 = context 中 V 的加权和。这是一组已经存在的特征的线性组合。
但模型的真实需求是抽象特征:
| 任务 | 需要的“加工” |
|---|---|
| 命名实体识别 | “在主语位置上的名词 → 可能是实体” |
| 情感分析 | “多个情感词的组合 → 整体极性” |
| 句法依存 | “动词 + 宾语 → 动宾关系” |
这些都需要非线性 + 跨维组合——单纯 attention 提供不了。
5.2 横向混合 vs 纵向加工
把 transformer 一层 block 的工作切成两个正交方向:
| 维度 | 含义 | 谁来做 |
|---|---|---|
| 横向(cross-position) | 不同 token 之间交换信息 | attention |
| 纵向(per-position) | 在每个位置内部做特征变换 | FFN |
两个维度正交、不可替代:
- 横向混合 = 把 context 借过来
- 纵向加工 = 消化借来的信息
- 缺一不可
把 transformer block 想成“开会 + 思考”:
- attention = “开会”——每个 token 听其他 token 的发言
- FFN = “思考”——每个 token 单独消化刚才听到的内容
- 残差 = “不忘本”——保留自己原本的判断
只开会不思考 = 一群人在喊话、各说各的
只思考不开会 = 闭门造车、没有信息输入
5.3 MLP-Mixer 反例
一个反直觉的事实:有研究者完全去掉 attention,只用 MLP——在某些任务上居然能行。
MLP-Mixer(Tolstikhin et al. 2021)的设计:
- token-mixing MLP:跨 token 全连接,混合位置信息(替代 attention)
- channel-mixing MLP:跨通道全连接,加工特征(替代 FFN)
- 完全没有 attention
在 ImageNet 分类上,MLP-Mixer 达到了和 ViT 可比的精度。
这直接证明:attention 不是信息混合的唯一方式。横向混合可以用全连接替代。
但有两个关键限制:
- MLP-Mixer 只能在视觉上打平 ViT——在语言任务上显著掉点
- MLP-Mixer 必须固定输入尺寸——无法处理变长序列(这是 attention 最大的优势)
5.4 视觉 vs 语言的归纳偏置差异
为什么 MLP-Mixer 在视觉上能行、在语言上不行?
| 任务 | 主要依赖 | attention 的优势 | MLP 的劣势 |
|---|---|---|---|
| 图像分类 | 局部空间结构(相邻 patch 关联强) | 弱 | token-mixing MLP 的全连接刚好匹配 |
| 语言建模 | 任意长距离依赖(“the cat … sat on the mat”) | 强(自适应选相关 token) | 全连接把近邻和远距混在一起训 |
架构选择 = 归纳偏置的选择。
Attention 的归纳偏置 = “动态地、稀疏地、按内容选相关 token”
MLP 的归纳偏置 = “所有位置同等对待”
视觉任务里“所有位置同等对待”是合理的归纳偏置;语言任务里不是。这就是为什么 transformer 在语言上统治、但 MLP-Mixer 在视觉上能分庭抗礼。
5.5 小结:FFN 是“思考”,attention 是“开会”
回到 block 设计的根本逻辑:
- Attention = 开会:让 token 之间交换信息
- FFN = 思考:让每个 token 单独消化信息
- 残差 = 保留自我:不丢失 token 的原始身份
两个角色正交、不可替代。FFN 不是“多余”,是 attention 留下的另一半工作。
Attention 不重写 token,只生成补丁。补丁必须被“消化”,消化的工作归 FFN。
6. FFN 内部:先升维再降维的几何
FFN 是 transformer block 的另一半。它内部到底在做什么?
6.1 教科书 FFN 的结构
原始 Transformer 论文(2017)给出的 FFN:
形状(以 GPT-2 为例):
1 | 输入 x: (B, T, 768) ← batch, seq_len, d_model |
参数量:(不含 bias)
6.2 升维-激活-降维的几何直觉
把 维中间层想成一个高维特征空间:
| 步骤 | 几何动作 | 类比 |
|---|---|---|
| 把 维 token 表征展开到 维 | 把压缩包拆开看 | |
| ReLU | 在高维空间里挑出有用的零件 | 丢掉无用的零件 |
| 把高维特征重新组装回 维 | 重新打包 |
为什么要先升维再降维?——给非线性足够的“工作空间”。
类比:
- 维 = 一个压缩后的“标签云”(“主语 / 谓词 / 实体”几个词)
- 维 = 拆开看里面的所有细节
- ReLU = “挑出当前任务需要的几个细节”
- = “把挑出的细节重新压缩成新标签云”
维空间里能表达的非线性太有限——只有 个“方向”可以激活。升维 = 把“可激活的方向”扩展到 个,让网络能学更复杂的特征组合。
6.3 激活函数的演化谱系
FFN 中间的非线性激活函数经历了多次演化:
| 年份 | 激活函数 | 公式 | 特点 |
|---|---|---|---|
| 2010 | ReLU | 简单、稀疏、0 点不可微 | |
| 2016 | GELU | 高斯 CDF 加权,平滑 | |
| 2016 | GLU | 双分支门控(Dauphin et al.) | |
| 2017 | SiLU | 自门控、平滑 | |
| 2019 | Swish | 与 SiLU 是同一个函数,只是被重新发现 | |
| 2020 | SwiGLU | GLU + Swish(Shazeer 2020) |
原稿这张表的年份错得离谱(把 GLU 标成 2020、SiLU 标成 2019)。顺带一提 SiLU 与 Swish 完全等价,只是 2019 年被 Ramachandran 等人重新命名推广;现在两种叫法混用,看到哪个都不奇怪。
演化的核心方向:
- 从硬阈值到软阈值(ReLU → GELU → SiLU)—— 0 点从不可微变可微
- 从单分支到双分支(ReLU/GELU/SiLU → GLU/SwiGLU)—— 加入门控机制
6.4 FFN 在做“通道探测 + 重组”
把 维中间层想成 个并行的特征探测器:
- 的每一行 = “一个探测器”,专门检测某种特征(“是否是主语位置”、“是否是实体”、“是否包含否定”)
- 探测器激活 = “我检测到了这个特征”
- = “把检测结果重新组合”成新的 token 表征
这是 CNN 中“卷积核 = 特征探测器”思路在 transformer 里的对应物。
FFN 的本质 = 在每个位置独立运行 个“特征探测器 + 重组器”。Attention 在 token 之间借信息,FFN 在 token 内部自己加工这些信息。
这个视角也是理解下一节“门控”的关键。
7. SwiGLU 详解:门控的精确语义
FFN 的形状固定后,剩下的问题就是激活函数。SwiGLU 是当前所有现代 transformer(LLaMA、Mistral、Qwen、DeepSeek)的事实标准。
7.1 从 ReLU 到 GLU 的演化
ReLU 的根本问题:所有通道统一处理。负值全砍、正值原样传。没有“哪些通道更重要”的区分。
GLU(Gated Linear Unit,门控线性单元)的核心想法:用一条专门的分支来“门控”另一条分支。
GLU 的标准形式:
- = “门”(0~1 之间的值)
- = “内容”(任意实数)
- = element-wise 乘(用门过滤内容)
门控的本质:让网络逐位置、逐通道决定“哪些信号流过去、哪些截断”。
7.2 SwiGLU 的三矩阵角色
LLaMA 用的 SwiGLU(也称 SiGLU):
再额外加一个降维投影:
三个矩阵的角色:
| 矩阵 | 维度 | 角色 | 类比 |
|---|---|---|---|
| 生成“门控信号” | 阀门的开度 | ||
| 生成“候选值” | 水管里的水 | ||
| 投影回模型维度 | 重新混合水流 |
通常 (LLaMA 标准)。
7.3 完整计算流
以 LLaMA-7B 的真实配置为例(,,即 ):
1 | # === Step 1: 两条并行路径 === |
数据形状变化:
1 | x: (B, T, 4096) ← d |
7.4 水管类比:门控的直觉
把 11008 个通道想成 11008 根并行的水管:
1 | x ──┬──→ W_gate → 算"每根管子的增益 g" ──→ SiLU ──→ g ∈ [-0.278, +∞) ──┐ |
核心思想:
- W_up = “每根管子要送什么水”(每根管子的流速)
- W_gate = “每根管子的增益调多大”(有符号,不是 0~1)
- SiLU = 把增益整形成 ,见下
- element-wise 乘 = “增益调制水流量”
- W_down = “把 11008 根管子的水重新混合成一个出口”
门控让网络可以“逐位置、逐通道”决定“哪些信息流过去”——比 ReLU 那种“硬阈值”精细得多。
注意门不是 0~1 的阀门。 这是 SwiGLU 和普通 GLU 最容易被讲错的一处。
记 ,则
- 很负 → ,通道关闭,且是负增益(会翻转符号)
- → ,通道关闭
- 很正 → ,,线性直通、幅度基本不衰减
- 全域最小值约 ,值域是
对比一下普通 GLU 的 sigmoid 门 :那个门恒正,只能衰减、永远不能翻转符号。SiLU 门多出来的正是“负增益”这一维表达力——通道不只是“开/关”,而是“开 / 关 / 反向”。
7.5 参数不变魔法:8d/3 的由来
直觉上 SwiGLU 用了 3 个矩阵(),应该比 ReLU FFN 的 2 个矩阵()多 50% 参数?
不是。办法是缩小隐藏维度来保参数——注意这是 LLaMA 的做法:LLaMA 论文原话是“用 2/3·4d 代替 PaLM 的 4d”。PaLM 540B 用的仍是未缩减的 4d,Qwen 等后续模型才跟进到 8/3。
| 方案 | 参数量 | 隐藏维度 |
|---|---|---|
| ReLU FFN | ||
| GLU | ||
| SwiGLU |
关键魔法的核心是 取 这个值,推导如下:
SwiGLU 和 ReLU FFN 的总参数完全一样(),但多一个门控分支。
为什么 ?
- ReLU FFN: 隐藏维度 × 2 矩阵 =
- SwiGLU 想要总参数同样是 这个量级,3 个矩阵 ⇒ 每矩阵 ⇒ 隐藏维度
Shazeer 2020 给出了这个 2/3 匹配的推导(门控 FFN 用 3 个矩阵,令 即与原版参数量相等)并做了扫描。 是把 代进去的结果;LLaMA 把它向上取整到 256 的倍数(4096 → 11008),Mistral 取整到 128 的倍数。
一个值得单独指出的共同结构:这个“参数不变”和推理侧 KV Cache 的“精确等价”是同一种论证——两者能成立,都只因为被缓存 / 被替换的那个量本来就是恒等的。
SwiGLU 不是比 ReLU FFN 更便宜,是把同样的一份预算重新分配了一次;KV Cache 不是近似加速,是把本来就不必重算的东西存了下来。一个是训练期把参数花在刀刃上,一个是推理期把算力从冗余里省出来——同一个道理的两面。KV Cache 那条见另一篇《Decoder-Only 大模型的推理过程详解》的 4.2 节,两本账逐条对齐的版本见《同一块积木的两本账》1.1 节。
7.6 门值学到了什么
回到 7.4 算出来的值域 ,门值能做三件事,而且每一维都是逐位置重新算的:
| 门值 的量级 | 通道行为 |
|---|---|
| 基本关闭 | |
| 为负 | 反向通过——信息被翻转符号 |
| 很大 | 线性直通,幅度基本不衰减 |
这里的关键不是“有多少通道被关掉”,而是“同一个通道在不同 token 上得到不同的增益”。 门控矩阵 的输入是 ,而 是 token 的上下文表示——所以增益是 形状的,每个位置一套。这和 CNN 里的通道注意力(SE-Net, Hu et al. 2018)是同一个思想:用一组共享权重为每个通道算一个标量权重,再乘回去。
一个诚实的补充:坊间流传的“SwiGLU 门值有百分之多少接近 0 / 接近 1”这类统计,在 Shazeer 2020 里查不到出处——那篇论文分析的是门非线性函数的选择(sigmoid / ReLU / GELU / Swish),并没有给出门值分布的统计。要引用具体百分比,请以你自己跑出来的实测为准,不要转述二手数字。
7.7 FLOPs 对比:零成本升级
| 方案 | FLOPs (per token) | 参数量 | 性能 |
|---|---|---|---|
| ReLU FFN | baseline | ||
| SwiGLU | PPL -0.3 ~ -0.4 |
FLOPs 完全一样。SwiGLU 是零成本升级。
至于涨多少,Shazeer 2020 的结论比“某个具体阶梯”更微妙:它报告 GeGLU 与 SwiGLU 表现最好、两者基本打平,相对 GELU 的增益约 0.04 nats;而论文自己强调,下游指标(GLUE / SuperGLUE / SQuAD)上的差异落在多次运行的方差范围内。
也就是说,可靠的结论只有一句:整个门控家族(GLU 系)稳定优于非门控基线,至于 SwiGLU 比 GeGLU 强多少,现有证据不足以排序。
主流架构选 SwiGLU 的原因:在参数/FLOPs 预算不变的前提下换来的稳定增益,而不是“它比其它门控函数精确地好 X 个点”。
7.8 中间激活更小:还省显存
| 方案 | 中间张量大小 |
|---|---|
| ReLU FFN | |
| SwiGLU |
SwiGLU 的中间激活比 ReLU FFN 小 33%——反向省了显存。这是它在长序列 / 大 batch 训练中的额外优势。
8. Attention + FFN 配比:业界为什么都选 1:1
SwiGLU 把 FFN 内部讲透了——但还有一个更宏观的问题没解决:为什么一个 block 里只有一层 attention 和一层 FFN?为什么不是 2 层 FFN?
8.1 前提:两个方向正交且不可替代
回忆一下 block 的两个角色:
| 角色 | 谁来做 | 信息混合方向 |
|---|---|---|
| 开会 | attention | 横向(跨位置) |
| 思考 | FFN | 纵向(单位置内部) |
两个方向正交、不可替代:任何一个都替代不了另一个,这是 1:1 成立的前提。
但要小心,“正交”本身并不推出“多加一层就是浪费”。第 5.3 节的 MLP-Mixer 恰好是反例:它把 attention 换成堆叠的 token-mixing MLP,照样能打平 ViT。所以正确的说法不是“做两遍”,而是:
- 2 层 attention = 横向混合的容量翻倍,代价是参数和 FLOPs 都翻倍
- 2 层 FFN = 纵向加工的深度翻倍,代价同上
- 1 attention + 1 FFN = 用最小的块,覆盖两个正交方向(性价比最高的单元)
1 : 1 是“两个正交方向上的最小完整单元”。但这只是事后解释,不是证据——业界收敛到 1 : 1 靠的是 8.3 到 8.5 里那三条间接证据,而“重复即浪费”这个说法在 MLP-Mixer 面前根本站不住。
8.2 残差路径的预算
每个 block 至少贡献 2 条残差路径(attention 一条 + FFN 一条):
- 1 block = 2 残差
- 2 block = 4 残差
- L block = 2L 残差
如果改成 “1 attn + 2 FFN”:
- 1 block = 3 残差(attn + 2 FFN)
- L block = 3L 残差
听起来更多、更好?残差不是白给的。每增加一条残差分支,就要同时处理:
- 残差流的方差累积——每个 block 要单独缩放,否则深层的激活/梯度尺度会爆掉。DeepNet(Wang et al. 2022)把模型堆到 1000 层时,正是靠给每条残差支路加 、 两个可学习常数来修正这个尺度
- 初始化与归一化的耦合——分支越多,Post-LN 的方差增长越难控
- 同一份输入被反复加工,收益递减
需要说明的是:这三条是架构层面的普遍困难,不是“3L 条残差一定失败”的证明。 业界停在每 block 1~2 条,是经验收敛,不是被证明的上界。
8.3 计算均衡的考量
| 层 | 典型参数量 | 相对 attention |
|---|---|---|
| Attention() | 1× | |
| FFN (SwiGLU) | 2× |
FFN 本来就是 attention 的两倍参数量(SwiGLU 因为把 hidden 缩到 才没变成 1.5 倍)。再加一层 FFN,FFN 就变成 4× attention。
值得澄清的是:这种“失衡”并没有被证明是坏事。 Kaplan et al. 2020 §3.1 的结论恰恰相反——在固定非嵌入参数量下, 从约 0.5 扫到 10,损失只差约 2%,是一条平坦盆地,4× 只是论文 Table 1 里的默认形状,不是被扫出来的最优点。论文原话是“performance depends very weakly on the shape parameters”。
所以这里能说的只是:经验上业界收敛在 attention : FFN ≈ 1 : 2 ~ 1 : 4,LLaMA / Mistral / Qwen 落在这个区间;并不是因为偏离了它就会坏。
8.4 1:1 的实验证据
没有任何主流架构用 “1 attn + 2 FFN”。LLaMA / Mistral / Qwen / DeepSeek 全部是 1 : 1。
至于“为什么没有”,公开文献里并没有一个干净的、直接针对 block 内层数的对照实验。能站得住的证据是三条间接的:
| 来源 | 实际做了什么 | 说明 |
|---|---|---|
| Press et al. 2019 | 打乱 attention / FFN 子层的顺序,做 ratio 扫描 | 结论是 ratio 与 PPL 没有强相关,均衡配比平均略好。真正的发现是 sandwich:底层多放 attention、顶层多放 FFN 反而更好 |
| Kaplan et al. 2020 | 扫 | 1–10 是一条平坦盆地,弱依赖形状参数 |
| 业界收敛 | LLaMA / Mistral / Qwen / DeepSeek | 一律 1 : 1,且 FFN 占两倍参数 |
一个诚实的补充:把“1 attn + 2 FFN 会变差”当成有出处的实验结论来引,是不成立的。下一节就记录了一个我一度写错、核查后删掉的例子。
8.5 “1 attn + 2 FFN” 真的有人试过吗
没有可考的实验。 这一节原本写着“GLM(Du et al. 2022)尝试过 1 attn + 2 FFN,结论是不划算”——核查后发现 GLM 论文里没有这个实验。GLM 的贡献是自回归空白填充、2D 位置编码、LayerNorm/残差顺序调整和 GeLU,通篇没有子层数量的 ablation,也没有“先试后改回 1:1”这回事。
与这个问题真正相关的是 Press et al. 2019 的 sandwich 结论:子层的位置分布(attention 沉底、FFN 置顶)比全局比例更有信号。这说明真正被研究过的自由度是“放在哪里”,而不是“放几个”。
所以这里诚实的答案是:“1 attn + 2 FFN 会更好还是更差”这个问题,公开文献没有直接答案;业界收敛到 1:1 是工程惯例 + 残差/训练稳定性的经验,不是被证明的最优解。
8.6 真正变的是 FFN 内部
block 内部结构是稳定的,真正变化的是 FFN 内部设计:
| 方案 | 来源 | 改动 |
|---|---|---|
| ReLU | GPT-2 | 标准 |
| GELU | GPT-3 / BERT | 平滑近似 |
| SwiGLU | LLaMA / PaLM | 加门控 |
| GeGLU | Shazeer 2020 | GELU + 门控 |
| MoE 化 FFN | Mixtral / DeepSeek | FFN 变 MoE(Mixture-of-Experts,混合专家):容量↑、参数不变 |
| 共享 FFN 跨层 | ALBERT(Attention-Layer Bidirectional Encoder Representations from Transformers,跨层参数共享的 BERT 变体) | 全层复用:参数 -90%、精度 -1 |
MoE(Mixtral 8x7B)把 FFN 从“一个 MLP”换成“8 个 MLP 的专家网络 + 路由”——这是对 FFN 维度的扩展,不是对 block 结构的改造。
9. 深度靠什么扩展:block 堆叠的逻辑
block 内部结构清楚了——那 block 之间怎么堆叠?模型深度靠什么扩展?
9.1 block 数 = 模型深度
transformer 的深度是 block 数,不是 block 内层数:
| 模型 | block 数 | 单 block 内部 |
|---|---|---|
| GPT-2 small | 12 | 1 attn + 1 FFN |
| GPT-2 medium | 24 | 1 attn + 1 FFN |
| GPT-2 large | 36 | 1 attn + 1 FFN |
| LLaMA-7B | 32 | 1 attn + 1 FFN |
| LLaMA-70B | 80 | 1 attn + 1 FFN |
block 内部永远是 1 : 1,深度靠堆 block 数量。
9.2 浅 vs 深:实际收益曲线
直觉上更深 = 更强,但实际有收益递减:
| 深度 | 收益 | 训练成本 |
|---|---|---|
| 浅 | 每个 block 涨点明显 | 低 |
| 中 | 甜蜜区 | 中 |
| 深 | 涨点递减 | 高(梯度 / 显存 / 优化器挑战) |
上面这张分档表是经验之谈,不来自任何一篇论文。 有出处的结论只有一条:在固定 FLOPs 预算下,深度、宽度、数据量存在最优配比(Kaplan et al. 2020),扫出来 的最佳区间大致在 100 附近。注意这是宽度深度比,不是绝对层数——「12 层以下甜、48 层以上衰」这种说法取决于 ,换个宽度结论就变了,所以原文那几个具体数字我撤掉了。
更深确实也更难:层数上去之后,残差流的方差累积、显存占用和优化器稳定性都变糟——这也是 §8.2 那套残差预算问题在大模型上更突出的原因。
9.3 ALBERT:跨层共享 block
一个极端的反例:所有 block 共享同一组权重。
ALBERT(Lan et al. 2019, “A Lite BERT”):
- 12 层 transformer
- 所有 12 层共享同一组 attention + FFN 权重
- 参数减少 ~90%
- GLUE 性能下降 ~1 分
这反过来说明:深度 = block 数而不是 block 内部层数。block 数足够时,权重共享也能工作——但效果显著变差。
9.4 MoE 改造:block 内部扩 FFN
另一种“扩展容量”的路线——保持 block 1 : 1 结构,扩 FFN 内部。
Mixtral 8x7B(Mistral AI 2023):
- 每个 block 仍然是 1 attn + 1 FFN
- 但 FFN 变成 8 个专家(expert)网络
- 每个 token 路由到 2 个专家(top-2 routing)
- 总参数 = 8 × 7B ≈ 47B
- 激活参数 = 2 × 7B = 14B(每次前向只激活 2/8)
数据流长这样:
DeepSeek-V2 进一步把“专家”做成更细粒度(细粒度专家 + 共享专家)。
MoE 的关键洞察:深度靠堆 block、容量靠扩 FFN 内部。Block 1 : 1 结构稳定不动。
上面那条红色虚线值得单独说一句:“激活参数少”不等于“计算量少”。训练账上 MoE 极其划算,可推理账上它是另一回事——稀疏激活和批处理天然对立,token 必须被送到它选中的专家那儿,负载也不保证均匀。另一篇《同一块积木的两本账》5.2 节把这一侧的账逐条算了一遍。
9.5 头数扩展 vs 深度扩展
除了深度,模型还有两个扩展维度:
| 维度 | 含义 | 缩放方式 |
|---|---|---|
| 深度 | block 数 | LLaMA 32 → 80 |
| 宽度 | 4096 → 8192 | |
| 头数 | attention head 数 | 32 → 64 |
最优缩放(Kaplan 2020 / Hoffmann 2022 “Chinchilla”):
- 固定 FLOPs 时,深度和宽度要平衡缩放
- 头数一般随宽度自动调整(保持 head_dim ≈ 64-128)
- 数据量也要按比例扩展(Chinchilla 结论:约 20 个 token 换 1 个参数 —— Chinchilla 自己就是 1.4T tokens 对 70B 参数。注意方向:是「每个参数配 20 个 token」,反过来记就差了近三个数量级)
muP(μP,最大更新参数化)风格的自动缩放:
- 只调一个“深度”旋钮,其余超参按解析式联动
- 深度 ↔ 宽度 ↔ head 数 ↔ batch size ↔ lr ↔ weight decay 全部联动
- 好处是小模型上搜出的超参能直接迁移到大模型,省掉大模型上极其昂贵的那部分搜索
结语:从“看似冗余”到“刚好够用”
走完整台机器的内部,回头看每个“冗余”设计。
母题升华
| 设计 | 看起来“多余” | 实际在解决什么 |
|---|---|---|
| Q / K / V 拆开 | 三个矩阵似乎可以合并 | 三种语义角色(询问 / 被查 / 传递)需要独立的可学习子空间 |
| Q ≠ K | 共用能省一半参数 | 关系不对称(主谓 vs 谓主)需要非对称双线性相似度 |
| V 独立 | 似乎和 K 一样都是表征 | K 偏检索、V 偏传递,子空间不重合 |
| 多头 | 一个头看全序列不就够了 | 个头 = 个独立子空间,且不加参数。但“每个头不可替代”是错的——见 1.7 |
| 归一化包裹 | 残差不是已经把梯度接回来了吗 | 残差管梯度、归一化管尺度,两件事正交。位置还从 Post-LN 挪到了 Pre-LN——见第 4 节 |
| Attention 之后接 FFN | attention 似乎已经做完所有事 | attention 只做横向混合,纵向加工必须由 FFN 做 |
| FFN 升维再降维 | 直接做非线性不行吗 | 给非线性足够的工作空间 |
| SwiGLU 加门控 | ReLU 不就够了吗 | 让网络逐位置、逐通道决定哪些信息流过去 |
| Block 1 : 1 | 2 层 FFN 不是更强吗 | 两个正交方向的最少单元覆盖。但这是工程惯例,不是被证明的最优解——见 8.5 |
所有“看似冗余”的设计,背后都是同一个原则:
让模型在数据里学到合适的“子空间分工”。
这是架构的归纳偏置(inductive bias)和统计效率(statistical efficiency)的工程折中。
2017 年定型至今的工程意义
| 设计 | 定型时间 | 之后发生了什么 |
|---|---|---|
| 1 attn + 1 FFN 的 block 结构 | 2017 | 基本没动(9 年)——但这是惯例,不是定论,见 8.5 |
| Q / K / V 三个投影 | 2017 | 投影本身还在,但头层面已被 MQA / GQA 改写(K、V 跨头共享) |
| 残差 + 归一化包裹 | 2017 | 位置换了:Post-LN → Pre-LN(Xiong et al. 2020);LayerNorm → RMSNorm(Zhang & Sennrich 2019)——见 4.2、4.4 |
| 架构形态 | 2017 | Encoder-Decoder → 纯 Decoder(GPT 系)或纯 Encoder(BERT 系);cross-attention 在纯 Decoder 里整体消失 |
| 参数共享 | 2017 | 无 → 跨层共享(ALBERT 实验过,没有成为主流) |
| FFN 升维-激活-降维 | 2017 | 形状没动,中间的激活函数换了(见 6.3) |
| 激活函数 | 2017→2020 | 持续演化:ReLU → GELU → SwiGLU |
| 位置编码 | 2017→2021 | 持续演化:绝对位置 → RoPE / ALiBi |
| MHA → MQA / GQA / MLA | 2017→2023 | 持续演化(推理优化) |
| Dense FFN → MoE FFN | 2017→2023 | 持续演化(容量扩展) |
“骨架”基本没动,“细节”一直在动——这个分层是真实的:
- 骨架稳定 = 生态兼容、训练 pipeline 通用、模型可以无缝迁移
- 细节演化 = 持续涨点、推理效率提升、容量扩展
要说清楚:「骨架稳定」是观察,不是定律。 它真正意味着的是这 9 年里没有出现一个明显更好的 block 结构,而不是说 1:1 被证明最优(见 8.5)。骨架当然可以变,只是暂时没人拿出足够说服力的替代方案。
这和 Linux 内核“syscall ABI 尽量不动”、TCP“握手逻辑不动但拥塞控制持续演化”是同一类工程哲学:在更好的替代方案出现之前不动,不是因为动不了。
写给读者的话
如果你读完这篇文章,再回头看 transformer 的实现代码,大部分“看似多余”的矩阵乘法,都能讲出一个“为什么这样设计”的理由——只是这些理由分三个档次,强弱不同:
- 结构上必需的:残差一去,深度模型就训不动;因果掩码一去,训练和推理就脱节(见另一篇《Decoder-Only 大模型的推理过程详解》的 1.3 节)
- 数学上可证的: 必然让相似度对称,丢的是 个非对称自由度(见 2.4)
- 经验上没找到更好的:block 内 1:1 的配比至今是惯例而非定理(见 8.5)
- 看起来必要、实测冗余的:多头。96 个 encoder 自注意力头里剪掉 40% 都不掉点(见 1.7)——这一档比上一档更进一步,它连“等更好的论文”都不用等,答案已经有了,而且是否定的
分清楚这几档,比笼统说“每一处都有必要”有用得多——前两条你可以自己验,第三条只能等一篇论文来推翻,第四条则是提醒你:直觉给出的必要性,有时连方向都是反的。
不是“想加什么加什么”,而是“每一处要么有理由,要么有代价”。
附录:参考资源
论文
- Vaswani et al. 2017, “Attention Is All You Need” —— Transformer 原始论文;Post-LN、warmup 4000 步的出处
- Ba et al. 2016, “Layer Normalization” —— LayerNorm 原始论文(中心化 + 重标定两件事的出处,见 4.4)
- Zhang & Sennrich 2019, “Root Mean Square Layer Normalization”(NeurIPS 2019)—— RMSNorm 的来源,见 4.4;主张“减均值不降低隐藏状态或梯度的方差”
- Xiong et al. 2020, “On Layer Normalization in the Transformer Architecture”(ICML 2020)—— Pre-LN vs Post-LN 的理论解释,见 4.2、4.3;用平均场理论证明 Post-LN 在初始化时输出层附近梯度偏大
- Clark et al. 2019, “What Does BERT Look At? An Analysis of BERT’s Attention” —— Q / K / V 子空间可视化的关键证据
- Voita et al. 2019, “Analyzing Multi-Head Self-Attention” —— 用 LRP 给 head 排重要性并剪枝;结论是大部分 head 可删,不是 Q/K/V 谁更重要
- Michel et al. 2019, “Are Sixteen Heads Really Better than One?” —— Head 冗余性的硬证据,见 1.7:WMT encoder 自注意力 96 个头里逐个移除后只有 8 个造成统计显著的 BLEU 变化,其中约一半剪掉后 BLEU 反而上升;BERT 微调 MultiNLI 可剪 40%;cross-attention 剪超 60% 则崩
- Press et al. 2019, “Improving Transformer Models by Reordering their Sublayers” —— 子层顺序的 sandwich 结论(底层 attention、顶层 FFN 更优)
- Bhojanapalli et al. 2020, “Low-Rank Bottleneck in Multi-Head Attention” —— Head 维度瓶颈
- Shazeer 2020, “GLU Variants Improve Transformer” —— SwiGLU 的来源
- Kaplan et al. 2020, “Scaling Laws for Neural Language Models” —— 深度 / 宽度配比;§3.1 结论是形状参数影响很弱
- Hoffmann et al. 2022, “Chinchilla” —— 数据 vs 参数的最优比例
- Yang & Hu 2021, “Tensor Programs V” —— μP(最大更新参数化),超参跨规模迁移
- Tolstikhin et al. 2021, “MLP-Mixer” —— Attention 可被 MLP 替代的反例
- Lan et al. 2019, “ALBERT” —— Block 跨层共享
- Wang et al. 2022, “DeepNet: Scaling Transformers to 1,000 Layers” —— 深层残差流的方差累积与逐支路 、 修正
- Choromanski et al. 2020, “Performer” —— 核 attention 的代表
- Hu et al. 2018, “Squeeze-and-Excitation Networks” —— 通道注意力的 CNN 对应
工程资源
对照实现建议按这个顺序读,每一份都能对上本文的一节:
- LLaMA ·
modeling_llama.py—— 第 6 节到第 8 节的标准答案:LlamaMLP里的gate_proj/up_proj/down_proj就是本文的 、、,LlamaDecoderLayer就是 1 attn + 1 FFN 的 block - nanochat(Karpathy)
gpt.py—— 比 nanoGPT 更现代,也更贴本文:CausalSelfAttention里c_q/c_k/c_v三个投影分开(对应第 1、2 节)、Pre-LN 残差(第 3.3 节与第 4 节)、apply_rotary_emb、以及滑动窗口注意力。第 4 节的四个论据在它这里全能对上:norm()就是 4.4 节的 RMSNorm、Block.forward是 4.3 节那个 Pre-LN 写法、GPT.forward在lm_head前补了 final norm、CausalSelfAttention.forward里还有一句q, k = norm(q), norm(k)就是 4.5 节的 QK-Norm。但它的 MLP 是平方 ReLU 而不是 SwiGLU(见上面那段),所以第 7 节得去 LLaMA 那边看 - nanoGPT(Karpathy) —— 最好读的一份,
CausalSelfAttention和MLP(GELU + 4 倍隐藏)适合先读它建立整体印象,再去啃 LLaMA 的工程细节。缺点是它没有 RoPE、没有 KV cache,也没有门控 - HuggingFace Transformers ·
cache_utils.py—— 上一条modeling_llama.py就是这个仓库的实现。这里是 KV Cache 本身的代码:DynamicCache/StaticCache的接口与它们的取舍(DynamicCache每步torch.cat重新分配,省事但吃不了 CUDA Graph;延迟敏感的服务应该用StaticCache+torch.compile(mode="reduce-overhead"))
为什么第 7 节的例子是 LLaMA 而不是某个更小的实现
SwiGLU 的门控收益要靠 ablation 才看得出来,但这些 ablation 恰恰大多不在大模型上做。Shazeer 2020 自己就是在 12+12 层、 的 T5 编码器-解码器上扫的,真正在大规模上跑的只有少数几组。
自己从零写的实现为了控制复杂度,常常会选择平方 ReLU 之类的简单激活 —— 参数量同样是 ,但没有门控分支。
这不是假想:
nanochat的MLP就是c_fc: d → 4d、F.relu(x).square()、c_proj: 4d → d,正好是本文表里 ReLU FFN 那一行的 。想知道“把 SwiGLU 换成平方 ReLU 会怎样”,不用猜,去把nanochat的 MLP 换掉跑一遍就是。所以第 7 节的数值例子一律取自 LLaMA 的真实配置(、),而不是随手举的数字。
同系列
三篇一套,按这个顺序读最顺:
- 本文(2026-06-25)—— 第一本账:训练期为什么这样设计
- 《Decoder-Only 大模型的推理过程详解》(2026-04-09)—— 第二本账:运行期怎么把同一个模型跑得更快。本文 3.1 节引的因果掩码在它的第 1 节,1.5 节脚注里的 KV Cache 显存账在它的 4.3 节,7.5 节那条“精确等价”在它的 4.2 节
- 《同一块积木的两本账:Transformer Block 的训练期动机与运行期代价》(2026-10-09)—— 把两本账逐条对上:本文的每个设计在推理侧有没有账单、账单多大;反过来,KV Cache、GQA、RoPE、量化、投机解码各自是从本文的哪一条推出来的