Transformer Block 设计哲学:从『看似冗余』到『刚好够用』

作者 · kailfrong

写作日期: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,层归一化)?

每一处看起来都像“工程冗余”。但每当你真的把某一项删掉、改掉、合并掉,模型就会毫无悬念地退化。这不是代码的偶然,而是架构设计的内在必然。

这篇文章要做的事,就是从这些“看似冗余”的设计出发,把它们拆到零件级别,讲清楚每个零件为什么必须在那个位置。读完之后,你应该能自己回答:

  1. Q / K / V 三个矩阵各自承担什么不可替代的语义角色?
  2. 一次 attention 之后,一个 token 到底发生了什么不可逆的变换?
  3. FFN 内部那三个矩阵、那个“门控”、那个“先升维再降维”的设计,分别在解决什么问题?
  4. 业界为什么全都收敛到 1 : 1?这是被证明的最优解,还是工程惯例?
  5. 模型深度靠什么扩展——是 block 内部更厚,还是 block 数量更多?
  6. 多头到底在分什么工?为什么说“每个头都不可替代”这句话是错的?
  7. 归一化该放在子层前面还是后面?为什么主流模型几乎都改用了后一种?

母题预告:所有“看似冗余”的设计,背后都是同一个原则——让模型在数据里学到合适的“子空间分工”。这不是过度设计,是架构归纳偏置(inductive bias)和统计效率(statistical efficiency)的工程折中。

先看一眼要拆的到底是什么。绿色是本文 1–7 节逐个拆开的对象:

flowchart TD X["输入残差流 x"] -->|"1. norm(x)"| A["Attention<br/>横向:跨 token 借信息"] A -->|"2. 残差相加,再过一次 norm"| F["FFN / SwiGLU<br/>纵向:单位置内部加工"] F -->|"3. 残差相加"| Y["这一层的输出"] Y -->|"同样结构堆 L 层"| Z["final norm<br/>出栈前补一刀"] Z -->|"lm_head"| L["logits"] L --> S["采样 → 下一个 token"] style A fill:#d4edda,stroke:#28a745 style F fill:#d4edda,stroke:#28a745 style Z fill:#f8d7da,stroke:#dc3545 style S fill:#d4edda,stroke:#28a745

三处岔路,正好对应本文的三段:为什么 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
2
3
X[bank]  →  [bank, river, money, account, ...]       ← 静态语义
Q[bank] → [subject, of, is, the, ...] ← 句法意图
K[bank] → [financial, river, which, where, ...] ← 可被检索的方式

三个向量的最近邻几乎不重叠。如果它们来自同一个 、没有 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
2
3
4
5
# Post-LN(原始 Transformer):归一化作用在「支路输出 + 残差」的和上
x = LayerNorm(x + Sublayer(x))

# Pre-LN(GPT-2 之后的事实标准):归一化作用在子层的输入上
x = x + Sublayer(LayerNorm(x))

区别不在“有没有归一化”,而在归一化的是残差流本身,还是支路的输出:

  • 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
2
3
4
def forward(self, x, ve, cos_sin, window_size, kv_cache):
x = x + self.attn(norm(x), ve, cos_sin, window_size, kv_cache)
x = x + self.mlp(norm(x))
return x

而在 GPT.forward 里,block 循环结束、送进 lm_head 之前,同样补了一次 x = norm(x)。

4.4 RMSNorm:砍掉减均值

LayerNorm(Ba et al. 2016)做两件事:

  1. 中心化——减去特征维的均值
  2. 重标定——除以标准差,再乘一个学到的 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
2
# nanochat/gpt.py,CausalSelfAttention.forward 里
q, k = norm(q), norm(k) # QK norm

这一步压住的是 的幅度。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 不是信息混合的唯一方式。横向混合可以用全连接替代。

但有两个关键限制:

  1. MLP-Mixer 只能在视觉上打平 ViT——在语言任务上显著掉点
  2. 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
2
3
4
5
输入 x:  (B, T, 768)            ← batch, seq_len, d_model
W_1: (768, 3072) ← d → 4d(升维)
ReLU: 逐元素激活
W_2: (3072, 768) ← 4d → d(降维)
输出: (B, T, 768)

参数量:(不含 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 等人重新命名推广;现在两种叫法混用,看到哪个都不奇怪。

演化的核心方向:

  1. 从硬阈值到软阈值(ReLU → GELU → SiLU)—— 0 点从不可微变可微
  2. 从单分支到双分支(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
2
3
4
5
6
7
8
9
10
11
12
13
# === Step 1: 两条并行路径 ===
gate = x @ W_gate.T # (B, T, 4096) @ (4096, 11008) → (B, T, 11008)
up = x @ W_up.T # (B, T, 4096) @ (4096, 11008) → (B, T, 11008)

# === Step 2: 门控分支过 SiLU ===
# SiLU(x) = x * sigmoid(x)
gate_act = gate * torch.sigmoid(gate) # (B, T, 11008)

# === Step 3: element-wise 乘法(门控)===
hidden = gate_act * up # (B, T, 11008) → (B, T, 11008)

# === Step 4: 降维投影回 d ===
out = hidden @ W_down.T # (B, T, 11008) @ (11008, 4096) → (B, T, 4096)

数据形状变化:

1
2
3
4
5
6
7
8
9
10
11
x:        (B, T, 4096)         ← d
↓ W_gate
gate: (B, T, 11008) ← d_h
↓ SiLU
gate_act: (B, T, 11008) ← d_h
⊕ ← 门控
up: (B, T, 11008) ← d_h
↓ element-wise 乘
hidden: (B, T, 11008) ← d_h
↓ W_down
out: (B, T, 4096) ← d

7.4 水管类比:门控的直觉

把 11008 个通道想成 11008 根并行的水管:

1
2
3
4
5
6
7
8
9
10
11
x ──┬──→ W_gate → 算"每根管子的增益 g" ──→ SiLU ──→ g ∈ [-0.278, +∞) ──┐
│ │
│ ├──→ ⊙ ──→ 流量被增益调制
│ │
└──→ W_up → 算"每根管子的水流 u" ─────────────────────────────────┘
│
↓
W_down
│
↓
出口

核心思想:

  • 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)

数据流长这样:

flowchart TD X["token 的表示 x"] --> G["门控网络<br/>给每个专家打分"] G -->|"top-2"| P1["专家 1"] G -->|"top-2"| P2["专家 2"] G -->|"本轮没被选中"| PN["其余 6 个专家"] P1 --> W["按门控权重加权求和"] P2 --> W W --> Y["输出,形状仍是 (B, T, d)"] PN -.->|"参数照样常驻显存"| Z["推理侧账单"] G -.->|"token 要被送到对的地方"| Z style W fill:#fff3cd,stroke:#ffc107 style Z fill:#f8d7da,stroke:#dc3545

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、量化、投机解码各自是从本文的哪一条推出来的