如果你把 Transformer block 拆开来看,多数注意力都给了「attention」那部分——QKᵀ、softmax、多头、causal mask、KV cache,每一个都能讲一整篇。
但 block 里还有另外一半,叫「前馈网络(Feed-Forward Network,FFN)」,看起来就是一个最普通的两层 MLP(Multi-Layer Perceptron),先把维度从 d 升到 4d,过一个 ReLU,再压回 d。代码三行写完。
很多教程到这里就一句带过:「然后是一个 position-wise feed-forward。」
这是个很大的浪费。
第一,FFN 占整个 Transformer 总参数量的大约三分之二——它不是边角料,是大头。
第二,从 2021 年开始,一系列可解释性研究(Geva、Anthropic、Olah 等)把 FFN 重新解读成「键值记忆(Key-Value Memory)」——也就是说,attention 是「检索接口」,FFN 才是模型存储事实和模式的地方。
第三,现代 LLM(LLaMA、PaLM、Qwen、Mistral、DeepSeek)几乎全部把原版的 ReLU FFN 换成了 SwiGLU——这是 2017 年到现在 Transformer 主结构上变化最大的一处。
第四,混合专家(Mixture of Experts,MoE)本质上就是把单个 FFN 换成 N 个 FFN 加路由,连 attention 都没碰过。后面我们专门讲 MoE 的那一篇(44 篇),所有改动都在 FFN 这块。
第五,做推理工程的人都知道,量化时 attention 还好说,FFN 才是真正难处理的——4d 这一层的激活值分布往往非常厚尾,INT8 一不小心就把模型搞坏。
所以这一篇我想做的事情是:把 FFN 当成一个值得写一万字的对象,而不是一行注释。
读完之后,你应该能做到:
- 看到
FFN(x) = max(0, xW₁ + b₁) W₂ + b₂这一行,立刻把每一项的形状、参数量、计算复杂度、它在 block 中的位置说清楚; - 解释清楚「为什么是 4 倍扩张比,而不是 2 倍或者 8 倍」;
- 用「键值记忆」这个视角重新看 FFN,理解为什么单个神经元能对应一个具体概念;
- 回答「SwiGLU 为什么把 d_ff 设成 8d/3 而不是 4d」这种面试题;
- 知道 FFN 在量化、稀疏化、MoE 中各自的角色;
- 大致理解 superposition(叠加)假设是怎么回事,以及为什么它对解释 FFN 很关键。
先把主公式贴出来,然后我们一节一节地拆它:
FFN(x) = max(0, x W₁ + b₁) W₂ + b₂
其中:
x ∈ ℝ^d:输入是 d 维向量(一个 token 的隐状态);W₁ ∈ ℝ^{d × d_ff}、b₁ ∈ ℝ^{d_ff}:把 d 升维到 d_ff;原论文 d_ff = 4d;W₂ ∈ ℝ^{d_ff × d}、b₂ ∈ ℝ^d:把 d_ff 压回 d;max(0, ·):逐元素 ReLU。
这是 2017 年原论文的写法。它有三个设计选择,每一个我们都要单独讲:两层而不是更多、4 倍扩张而不是其它倍数、逐位置(position-wise)而不是跨位置。
一、把 FFN 放回 block 里看
1.1 一个 Transformer block 长什么样
先回到上下文里。Transformer encoder 的一个 block 长这样(pre-LN 写法,现代主流):
x' = x + Attention(LN(x))
y = x' + FFN(LN(x'))
post-LN 写法(原论文):
x' = LN(x + Attention(x))
y = LN(x' + FFN(x'))
无论 LN 放哪儿,结构上有两块「子层」:先 attention,再 FFN,之间各夹一个残差连接(Residual Connection)和层归一化(Layer Normalization)。
如果你只看这两个子层做了什么事,会发现一个很清晰的分工:
- Attention 让一个 token 看到序列中其它 token,把上下文混进当前位置;它做的是「跨位置的信息流动」;
- FFN 拿到这个被混过上下文的 token 表示,对它做一个非线性变换,但不再看其它 token——它做的是「在每个位置内部的信息加工」。
写成大白话:attention 负责「token 之间通信」,FFN 负责「token 内部计算」。
这个分工乍一看好像 attention 是主力、FFN 是辅助,因为新闻里关注的都是 attention。但前面已经说过:FFN 占参数量的三分之二,attention 只占三分之一。从「容量」角度看,FFN 才是模型的主力。
1.2 为什么 attention 之后还要 FFN
这是个值得停下来想一想的问题。
理论上,self-attention 已经是一个「带参数的混合操作」——它有 W_Q、W_K、W_V、W_O 四个矩阵,足以做相当复杂的事情。为什么不直接堆叠 attention 层就行?为什么每层之后非要再夹一个 MLP?
答案藏在 attention 的结构里。
attention 的公式是 softmax(QKᵀ/√d_k) V。这里
Q、K、V 都是输入 x 的线性投影。也就是说,attention
输出的每一个分量,都是输入 x
各位置的线性组合——加权系数虽然是非线性算出来的,但
V 进入输出的方式是纯线性。
如果你只堆 attention 层,每一层输出都是上一层输入的「加权线性组合」。多层堆起来,仍然只是一个比较复杂的线性变换(再加上 softmax 那点非线性,主要影响的是混合权重,不影响表示空间的维度结构)。
要让模型学到真正的非线性变换——比如「这个表示要先映射到一个高维空间,在那里某些方向被压缩、某些方向被放大、某些方向被砍掉,再压回来」——你需要一个纯粹做非线性的子层。这就是 FFN 的角色。
它升维(拉到 4d)、过非线性(ReLU 或后来的 Swish)、再降维(压回 d)。这个「升-非线性-降」的三明治,是经典 MLP 的标准动作,被各种深度学习模型反复证明有效。
换个角度说:attention 是在表示空间内部做线性混合,FFN 是在表示空间和一个更高维空间之间来回。两者结合,才能既看到上下文,又能做复杂的非线性计算。
1.3 FFN 是 token 间无关的
再强调一次这条性质,因为后面会反复用到:FFN 是逐位置(position-wise)的。
意思是:对 batch 里的每一个 token,FFN 都用同一组参数 W₁、W₂、b₁、b₂ 独立做一次计算。token A 的 FFN 计算和 token B 的 FFN 计算之间没有任何信息交换。
这不是巧合,是有意设计。
第一,它和 attention 的「跨位置混合」形成互补。
第二,它让 FFN 的计算可以完美并行——每个 token 一份独立的矩阵向量乘,一万个 token 就是一万次相同形状的乘法,GPU 很喜欢这种结构。
第三,参数量与序列长度无关——FFN 的参数
d × 4d + 4d × d = 8d²,和 token 数 T
没关系。
第四,这种「位置不敏感」的属性,让 FFN 像一个「纯粹的查表函数」:你给它一个 d 维向量 x,它返回一个 d 维向量 y,不需要任何上下文。下面讲键值记忆视角时,这个属性是关键。
图里把整条流程画清楚了:输入 [B, T, d] → 升维到 [B, T, 4d] → ReLU → 降维到 [B, T, d]。每个位置独立处理,没有跨位置的算子。
二、为什么是「两层」与「4 倍扩张」
这是 FFN 最常被略过、却最值得讨论的两个超参数。
2.1 两层是经验上的甜蜜点
为什么不是一层 MLP?因为一层带激活的 MLP 等价于「一个非线性核函数」,表达能力有限——具体说,一层的隐层维度即便很大,也仍然只是「线性 → 非线性 → 线性输出」三步合一,没有非线性的复合。
为什么不是三层、四层?因为再加层,参数量翻倍,但实测增益边际很小。原论文当时尝试了几种方案,最终选 2 层。后来 GPT-2、GPT-3、LLaMA 都沿用 2 层,没人换过。
更深的原因可能与残差连接(Residual Connection)有关:在残差网络里,每个 block 学的是一个「小修正」,不需要每个子层本身做特别复杂的变换,深度由层数堆叠提供,而不是由单个 FFN 内部提供。
如果 FFN 内部已经是 3 层,再加上残差和 LN,每个 block 就有 5-6 个非线性层,反而可能让训练变难。
2.2 4 倍扩张比:原论文的消融
「为什么 d_ff = 4d?」这是个面试常问题,但真正讲到位的答案不多。
原论文 Attention Is All You Need(Vaswani et al., 2017)在 §5.4 的消融表里,对 d_ff 做过几组对照:
- d_ff = 1024(即 2d):BLEU 下降 0.4 左右;
- d_ff = 2048(即 4d):基线;
- d_ff = 4096(即 8d):BLEU 几乎不动,参数量翻倍。
也就是说,2 倍不够,8 倍边际收益太小,4 倍是性价比最高的点。
这个 4 倍扩张比的合理性,可以从另一个角度理解:FFN 第一层把 d 维向量映射到 4d 维空间。如果你把 4d 看成「特征字典」,每个字典条目对应一个被 ReLU 选择性激活的特征——4d 个条目大致够用,再多就稀疏到学不动。
但要注意:这个 4 倍是 2017 年在 d=512 的尺度上调出来的。后来不同尺度的模型,d_ff/d 这个比例其实有变动:
| 模型 | d_model | d_ff | 比值 |
|---|---|---|---|
| Transformer base (2017) | 512 | 2048 | 4.0 |
| Transformer big | 1024 | 4096 | 4.0 |
| BERT-base | 768 | 3072 | 4.0 |
| GPT-2 small | 768 | 3072 | 4.0 |
| GPT-3 175B | 12288 | 49152 | 4.0 |
| LLaMA-7B(SwiGLU) | 4096 | 11008 | 2.69(≈ 8/3) |
| LLaMA-65B(SwiGLU) | 8192 | 22016 | 2.69 |
| PaLM 540B(SwiGLU) | 18432 | 49152 | 2.67 |
ReLU FFN 一直是 4 倍。SwiGLU 改成 8/3 倍——但这不是「真的更窄」,而是因为 SwiGLU 多了一个矩阵,要把总参数量对齐到 8 d²。下面讲变体时会详细说。
2.3 d_ff 的选择不是孤立的
最后一个值得强调的点:d_ff 不能孤立看,要和 d_model、num_layers、num_heads 一起看。
总参数量 ≈
n_layers × (4 d² (attention) + 8 d² (FFN)) =
12 n_layers d²。
这是一个非常好用的估计公式。比如:
- GPT-3 175B:n_layers=96, d=12288 → 参数量 ≈ 12 × 96 × 12288² ≈ 173B(接近 175B,差额来自 embedding);
- LLaMA-7B:n_layers=32, d=4096 → ≈ 12 × 32 × 4096² ≈ 6.4B(实际 7B,差额来自 embedding 和 SwiGLU 多出的矩阵)。
记住这个 12 d² 的估计,对评估「这个模型有多大」「我能不能跑得起来」非常有用。
三、视角一:FFN 是逐位置的标量函数
我们已经反复说 FFN 是 position-wise 的,现在把这件事的形式化结果提出来。
把 FFN 看成一个函数 f: ℝ^d → ℝ^d,输入一个 d
维向量、返回一个 d 维向量。整个 FFN 子层做的事情是:
y[b, t, :] = f(x[b, t, :]) # 对每个 (b, t) 独立调用
注意 b 和 t 都不出现在 f 的定义里——f 只依赖参数 W₁、W₂、b₁、b₂,这些参数在所有位置共享。
这有几个有意思的推论:
推论 1:FFN 的输入分布是「每个 token 的隐状态」。
这点很容易被忽视。你训练 FFN 时,看到的不是「整句话」,而是大量被 attention 混合过的、独立的 d 维向量。两个不同句子在某个位置上的隐状态,对 FFN 来说是同一类输入——只要它们的 d 维表示落在相似的区域。
推论 2:FFN 学到的是「隐状态空间到隐状态空间」的映射。
这种映射的复杂度由 4d 这个隐藏层决定。如果 d=512、d_ff=2048,FFN 在内部有 2048 个「特征探测器」,每个探测器决定输入向量是否落在它响应的方向上。
推论 3:因为 FFN 不混 token,它对序列长度完全不敏感。
无论你输入 512 个 token 还是 32k 个 token,FFN 子层的参数和单 token 计算量完全一样。所以长序列推理的瓶颈通常不在 FFN,而在 attention(它的复杂度是 O(T²))。这是后面讲长上下文优化(41-43 篇)的一个关键背景。
推论 4:FFN 的计算可以无成本地并行。
GPU 可以把所有 (b, t) 的 FFN
计算合并成一次大的批量矩阵乘——[B*T, d] × [d, 4d] → [B*T, 4d],再
× [4d, d]。这是为什么 FFN
在工程上特别友好的原因。
3.1 一个直观的具象类比
我喜欢把 FFN 想象成一台「词向量加工厂」。
attention 子层把「上下文」装进了当前 token 的隐状态——它现在不再只是 “cat” 这个词的初始嵌入,而是「这句话里、在这个位置上、被前后文影响过的 cat 表示」。
FFN 接过这个加工过的表示,做一次复杂的非线性变换:「让这个向量在隐空间里走一段路」。这一步可能是把一个表示「澄清」(让它更接近某个概念)、可能是「转换」(把动词形态改了)、也可能是「事实查询」(输入 “Paris is the capital of”,输出方向上加上 “France” 的成分)。
具体它做了什么,要看下一节的「键值记忆」视角。
四、视角二:FFN 是关联记忆(Geva et al. 2021)
如果说前一节是「逐位置标量函数」的视角——把 FFN 当黑盒,问它的形状和性质——那么这一节要换一个完全不同的视角,把它打开看里面在干什么。
4.1 把 FFN 重写成内积+加权和
先做一点纯代数的重写。原公式:
FFN(x) = ReLU(x W₁ + b₁) W₂ + b₂
把 W₁ 看作 d_ff 个 d 维列向量(严格说是 W₁ 的列向量,或者把 W₁ 写成 [k₁ | k₂ | … | k_{d_ff}],每个 k_i 是 d 维):
W₁ = [k₁, k₂, ..., k_{d_ff}] 形状 d × d_ff
那么 x W₁ 的第 i 个分量就是
x · k_i——也就是输入 x 与 k_i 的内积。
加上 ReLU 和偏置:
a_i = ReLU(x · k_i + b_{1,i})
a_i 是一个标量,可以理解成「k_i 这个方向,被 x 激活了多少」。
再看 W₂。把 W₂ 拆成 d_ff 个 d 维行向量(这次是行):
W₂ = [v₁; v₂; …; v_{d_ff}] 形状 d_ff × d
那么:
y = a · W₂ + b₂ = Σ_i a_i v_i + b₂
也就是说,FFN 的输出,是 d_ff 个向量 v_i 的加权和,权重是 a_i。
这个写法立刻让人联想到一个东西:注意力公式。
Attention(Q, K, V) = softmax(QKᵀ/√d_k) V
attention 也是「先用 Q 和每个 K 算相似度,得到权重,再用权重加权 V」。
唯一的区别是:
- attention 的 K 和 V 是输入动态生成的(K = x W_K,V = x W_V);
- FFN 的 K 和 V 是参数(k_i 来自 W₁ 的列,v_i 来自 W₂ 的行),它们在训练后就固定了,不随输入变。
attention 的相似度用 softmax 归一化,FFN 用 ReLU 不归一化(多个激活可以同时打开)。
但形式上的对应关系非常清楚:FFN 是「带固定 K/V 的 attention」,或者反过来说,attention 是「带动态 K/V 的 FFN」。
4.2 Geva 等人的实证
这个视角不是凭空想出来的。Geva、Schuster、Berant、Levy 在 EMNLP 2021 的论文 Transformer Feed-Forward Layers Are Key-Value Memories 里,对 BERT 的每个 FFN 神经元做了系统的可视化研究。
他们做了什么呢?大致是:拿一个训练好的 BERT,把每一层 FFN 的 4d 个神经元逐个拿出来,看每个神经元在哪些输入下被强烈激活——也就是找出能把 a_i 激活到最大的那些训练样本。
结果非常震撼:
- 浅层(前 4 层)的神经元倾向于响应表层模式,比如「以 ed 结尾的词」「句首大写字母」「数字串」;
- 中层的神经元开始响应句法模式,比如「介词短语开头」「现在分词」「定语从句」;
- 深层(后 4 层)的神经元倾向于响应语义模式,比如「与音乐相关的句子」「描述法律事件」「关于动物的事实」。
也就是说,FFN 的每个神经元,确实在做某种「检索」——它有一个偏好的输入模式(k_i 编码),当输入 x 的 d 维表示与 k_i 方向接近时,a_i 被打开,对应的 v_i 被加到输出里。
4.3 这个视角有什么用
很多个用处。我挑几个最直接的。
第一,它解释了「事实回忆」是从哪儿来的。
一个语言模型能正确回答 “The capital of France is _”
是因为它在某个位置、某层 FFN 里,有一个神经元的 k
编码了「the capital of
这是 ROME(Locating and Editing Factual Associations in GPT,Meng et al., NeurIPS 2022)和 MEMIT 等模型编辑工作的理论基础。
第二,它解释了为什么 MoE 有效。
如果 FFN 是关联记忆,那一个大 FFN 就像一个大字典。当你想让模型容量翻倍时,可以扩大 d_ff——但 d_ff 已经很大了(数万),每个 token 都要遍历所有 d_ff 个 key 浪费太多。
MoE(Mixture of Experts)的想法是:做几个独立的 FFN(专家),每次只激活最相关的几个。这样总参数量可以放大几十倍,但单次计算只动用一小部分。后面 44 篇会详细讲。
第三,它解释了为什么 attention 量化容易、FFN 量化难。
attention 的 K/V 是动态生成的,分布相对均匀;FFN 的 W₁ 行向量(也就是 k_i)有非常明显的「明星神经元」——少数 k_i 在大量样本上被强烈激活,对应的激活值有长尾。INT8 量化的难点就在这里:用 256 个量化级覆盖一个长尾分布会丢精度。后面讲量化(48-50 篇)时这点会具体展开。
图里把 W₁ 的列当成 keys、W₂ 的行当成 values 画出来,并对照了 attention 公式的各项。直观上很容易看出两者的同构关系。
4.4 一个反思:FFN 真的「存」事实吗
需要泼一点冷水。
「FFN 是事实存储」是一个很流行的隐喻,但严格说,这个说法有它的边界。
第一,FFN 单个神经元不是「一个事实对应一个神经元」。Anthropic 在 Toy Models of Superposition(Elhage et al., 2022)中指出,神经网络往往把多个特征「叠加」(superposition)到同一个神经元上——尤其是当特征数量超过维度数时。所以你看一个神经元被「激活」,它可能同时编码三个不相关的概念。
第二,事实的回忆经常需要 attention 和 FFN 配合——attention 把相关上下文搬到当前位置,FFN 才能在该位置做 lookup。两者拆开看意义有限。
第三,模型的「事实」不是离散存储,而是分布式的——同一个事实可能被多个神经元、多层一起共同编码。改一个神经元只会让模型对这个事实的回忆稍稍偏移,不会完全忘掉。
但这些 caveat 不否定主结论:FFN 是模型存储「学到的内容」的地方,attention 是「调度逻辑」。
五、参数量的会计
我们前面已经多次提到「FFN 占总参数 2/3」。这一节把它算清楚。
5.1 单层参数
把一个 Transformer block 的可训练参数列出来(忽略 LN 和偏置,它们参数量极小):
Attention 子层:
- W_Q:d × d
- W_K:d × d
- W_V:d × d
- W_O:d × d
- 合计:4 d²
FFN 子层:
- W₁:d × 4d = 4 d²
- W₂:4d × d = 4 d²
- 合计:8 d²
单 block 合计:12 d²。其中 attention 占 4/12 = 33%,FFN 占 8/12 = 67%。
5.2 整个模型
把 block 重复 n_layers 次,再加 embedding:
总参数 ≈ n_layers × 12 d² + V × d
其中 V 是词表大小,V × d 是 token embedding 的参数量(output 的 LM head 通常和 input embedding 共享权重)。
代入几个具体模型验算:
Transformer base (2017):n_layers=6, d=512, V=37000 - block:6 × 12 × 512² = 18.9M - embedding:37000 × 512 = 18.9M - 合计:≈ 37.8M(论文公开值 65M——差额来自 encoder + decoder 各 6 层、cross-attention、各种偏置)
GPT-2 small:n_layers=12, d=768, V=50257 - block:12 × 12 × 768² = 85M - embedding:50257 × 768 = 38.6M - 合计:≈ 124M(与公开值 124M 完全吻合)
LLaMA-7B:n_layers=32, d=4096, V=32000,但用 SwiGLU(d_ff=11008,3 个矩阵) - attention:32 × 4 × 4096² = 2.1B - FFN:32 × 3 × 4096 × 11008 = 4.3B - embedding:32000 × 4096 = 0.13B - 合计:≈ 6.6B(公开值 6.7B,差额来自 RMSNorm 等少量参数)
注意 LLaMA 因为用 SwiGLU,FFN 参数比例更夸张:4.3 / 6.6 ≈ 65%——和原版相当,但绝对值大得多。
图里把单 block 参数分布画成饼图:FFN 那一块是 attention 的两倍。
5.3 这意味着什么
第一,模型瘦身的主要目标是 FFN。
剪枝(pruning)、低秩分解(low-rank factorization)、量化(quantization)、MoE,绝大多数是冲着 FFN 去的,因为它「肉最多」。
第二,FFN 也是显存大头。
训练时,激活值需要保存在显存里以便反传。FFN 的激活是 [B, T, 4d],比 attention 的 [B, T, d] 大 4 倍。所以 gradient checkpointing(重计算)经常优先重算 FFN。
第三,推理时 FFN 不能「缓存」。
KV cache 缓存的是 attention 的 K 和 V——它们随历史生成而累积,下一步只需要追加新 token 的 K/V,旧的复用。
但 FFN 是逐位置的,每生成一个新 token,都要把它过一遍完整的 FFN。所以 decode 阶段每步的计算量主要由 FFN 决定,这也是为什么 MoE 在 decode 时格外有意义——它把 FFN 的「每步必算」成本砍下来了。
六、现代变体:GLU、SwiGLU、GeGLU
6.1 ReLU 的弱点
原版 FFN 用 ReLU,公式干净、计算便宜。但它有一些工程上的麻烦:
- dead ReLU:训练过程中,某些神经元长期输入小于 0,永远输出 0、永远不更新。这部分 d_ff 等于浪费。
- 梯度不平滑:ReLU 在 0 处不可导(实际工程里给一个 sub-gradient),这种「硬转折」可能让大模型训练不稳定。
- 没有「门控」:ReLU 是「过或不过」的硬决策,缺少 LSTM 那种「打开多少」的连续门控信号。
针对这些问题,2016 年前后出现了几个替代品:GELU(Gaussian Error Linear Unit)、Swish(也叫 SiLU)、Mish 等。它们都是「平滑版的 ReLU」,在大模型上常见到。
GELU 的形式(精确版):
GELU(x) = x · Φ(x)
其中 Φ 是标准正态的累积分布函数。直观上,它把 ReLU 的「0/1 硬开关」换成了「按 x 的标准正态尾概率加权」。BERT、GPT-2、GPT-3 都用 GELU。
Swish(SiLU):
Swish(x) = x · sigmoid(x)
形式更简单,性质和 GELU 几乎一样,LLaMA 系列用的就是这个。
但更大的变化是引入「门控」结构。
6.2 GLU:门控线性单元
GLU(Gated Linear Unit)由 Dauphin 等人在 2017 年的 Language Modeling with Gated Convolutional Networks 中提出。后来 Shazeer 在 2020 年的短论文 GLU Variants Improve Transformer 中专门做了在 Transformer FFN 上的对照。
GLU 把 FFN 第一层从「一个矩阵乘 + 非线性」改成「两个矩阵乘相乘」:
FFN_GLU(x) = (x W₁ ⊙ σ(x V)) W₂
其中:
- W₁、V 都是 d × d_ff 的矩阵;
- σ 是 sigmoid,把 x V 压到 (0, 1),作为门控权重;
- ⊙ 是逐元素相乘;
- W₂ 是 d_ff × d,把结果压回 d。
直觉上:x W₁ 是「候选信息」,σ(x V) 是「这个信息要打开多少」。两者相乘,得到「按门控加权的信息」。
这比 ReLU 灵活——它可以学到「打开 30%」「打开 80%」这种连续的门控,而不是「打开 / 不打开」二选一。
6.3 SwiGLU:把 sigmoid 换成 Swish
Shazeer 的论文里给出了一组变体:
- ReGLU:σ → ReLU
- GeGLU:σ → GELU
- SwiGLU:σ → Swish
实测上 SwiGLU 和 GeGLU 表现最好,比 ReLU FFN 在同等参数量下 PPL 略低。LLaMA、PaLM、Qwen、DeepSeek、Mistral 等主流大模型基本都选了 SwiGLU。
SwiGLU 公式:
FFN_SwiGLU(x) = (Swish(x W_gate) ⊙ x W_up) W_down
注意这里有三个矩阵:W_gate、W_up、W_down(在 LLaMA 实现里就是这个命名)。
6.4 为什么 SwiGLU 的 d_ff 是 8d/3 而不是 4d
这是个常被问到、答得到位的人不多的问题。
ReLU FFN 的参数量是
d × 4d + 4d × d = 8 d²。
SwiGLU 有三个矩阵,每个是 d × d_ff(W_gate
和 W_up)或 d_ff × d(W_down),合计
3 × d × d_ff。
为了让 SwiGLU 和 ReLU FFN「同等参数量」做对比,需要
3 × d × d_ff = 8 d²,解得:
d_ff = 8d / 3 ≈ 2.667 d
所以 LLaMA-7B 的 d=4096,d_ff = 4096 × 8/3 ≈ 10923——实际工程取了 11008(向上取到 256 的整数倍,便于 GPU 算子对齐)。
如果你在论文或博客里看到「LLaMA 的 FFN 比 4 倍小」,那是因为没意识到 SwiGLU 多了一个矩阵——参数总量是没变的。
6.5 SwiGLU 真的更好吗
Shazeer 的原文里给了几条小实验,结论是 SwiGLU 在多个任务上比 ReLU FFN 好一点点(PPL 低 0.01-0.1 量级)。后来大模型的实测也支持这个结论。
但 Shazeer 自己写了一句很诚实的话:「我们不能给出一个清晰的解释,为什么这些变体有效。或许是 divine benevolence。」(divine benevolence = 神的眷顾,他原话开玩笑)
也就是说,现在没有一个清楚的理论解释 SwiGLU 为什么更好——只是工程上稳定地有效。这种「不知道为什么但有效」在深度学习里很常见。
图里把三种变体的公式、参数量、d_ff 取值放在一起对比。要点是:同等参数量下,SwiGLU 比 ReLU FFN 好;但代价是多一个矩阵(实现复杂、算子要适配)。
七、MoE:把 FFN 切成多个专家
混合专家(Mixture of Experts,MoE)整整一篇值得讲(44 篇会详讲),这里只点出它和 FFN 的关系。
7.1 单个 FFN → 多个专家 + 路由
标准 FFN:
y = FFN(x) # 单一参数共享给所有 token
MoE FFN:
gate_scores = router(x) # 给每个专家打分
top_k_experts = top_k(gate_scores) # 选出 k 个(通常 k=2)
y = Σ_{i in top_k} gate_scores[i] · FFN_i(x)
也就是说,MoE 用一个轻量的「路由网络」给每个 token 选 2 个(或 k 个)专家,只让这 2 个专家算 FFN,其它的不算。
如果有 8 个专家、k=2,那么 8 个专家的总参数量是单 FFN 的 8 倍,但每次推理只用其中 2/8 = 1/4 的计算(再加一点路由开销)。模型容量翻倍但计算不翻倍——这是 MoE 的核心卖点。
7.2 为什么 MoE 选 FFN 而不是 attention
理论上你可以做 MoE-attention(确实有论文尝试过),但实际几乎所有 MoE 工作都只对 FFN 做切分。原因:
- FFN 是参数大头(2/3),切它收益最大;
- FFN 是逐位置的,每个 token 路由独立,逻辑简单;
- attention 涉及跨 token 的混合,做 MoE 路由会破坏这种结构,工程上很麻烦;
- 「键值记忆」的视角下,FFN 切成多个专家很自然——每个专家是一个子记忆库。
7.3 现代 MoE 模型
- Switch Transformer(Google, 2021):1.6T 参数,但每 token 只激活 1 个专家
- GShard(Google, 2020):MoE 工程化的早期工作
- Mixtral 8x7B(Mistral, 2023):8 个 7B 专家,每 token 激活 2 个,等效 ~13B 推理成本但 47B 总参数
- DeepSeek-V3(2024):256 + 1 共享专家,路由策略更精细
这些细节 44 篇会展开。这里只要记住:MoE = FFN 的稀疏化版本。它没改 attention,只改 FFN。
八、可解释性:单个神经元在做什么
这一节把前面「键值记忆」的视角再往深推一层,进入可解释性研究的领域。
8.1 神经元可视化的早期工作
2018 年 Karpathy 的博客 The Unreasonable Effectiveness of Recurrent Neural Networks 里展示过:训练一个 LSTM 字符级语言模型,会有一个神经元专门追踪「我是不是在引号里」、另一个追踪「我是不是在 if 语句的条件部分」。这种「单个神经元学到一个具体功能」的现象,在 RNN 上就有人观察到了。
到了 Transformer 时代,OpenAI 在 2019 年的 GPT-2 微观分析(早期博客)和 Anthropic 后来的一系列工作(Olah、Elhage 等)把这件事推到了系统化研究。
8.2 Anthropic 的多 polysemantic 神经元
但实际研究中,很快发现一个问题:很多神经元是「多义的」(polysemantic)——你看它在某些样本上激活,会同时响应几个不相关的概念。
比如在一个 GPT-2 模型里,你可能找到一个神经元,它同时对「关于猫的句子」「金融术语」「动词过去时」都强烈激活。
这违反了「一个神经元对应一个概念」的简单假设。
Anthropic 在 Toy Models of Superposition(Elhage et al., 2022)中给出了一个解释:当模型要表示的特征数量超过维度数时,它会把多个特征「叠加」到同一个方向上——不是 d_ff 个特征用 d_ff 个独立方向,而是用稀疏结构(只有少数特征同时激活),把更多特征压缩到 d_ff 维空间里。
这种「叠加」(superposition)让单个神经元变成多义的,但也让模型容量变大——它实际能编码的特征数远超 d_ff。
8.3 字典学习与稀疏特征
Anthropic 后来的 Towards Monosemanticity(Bricken et al., 2023)和 Scaling Monosemanticity(Templeton et al., 2024)用稀疏自编码器(Sparse Autoencoder, SAE)从叠加表示中「解出」单义特征。
做法大致是:训练一个超宽的自编码器(比如 d=512 解到 32k 或 256k 维),强制激活稀疏。每个解出的特征更接近「一个清晰的概念」。
在 Claude 3 Sonnet 上的实验里,他们解出了几百万个单义特征,包括「金门大桥」「Python 列表推导式」「关于死亡的诗歌」等具体得吓人的概念。
这些研究的结论是:FFN 内部确实在做某种「特征字典」的工作,但字典条目和神经元不是一一对应——神经元是叠加表示,需要专门的工具才能解出。
8.4 这对工程意味着什么
可解释性目前还没有直接落到工程实践(除了 ROME 这类编辑工作)。但有几个值得关注的方向:
- 模型编辑:定位某个事实存在哪些神经元,然后改它(比如把「巴黎是法国首都」改成别的);
- 安全审计:找出模型里编码「危险概念」的神经元,看它们在什么时候被激活;
- 稀疏性利用:如果 SAE 能稳定解出稀疏特征,未来推理或许能只跑被激活的那一小部分。
九、Dropout、初始化与训练细节
9.1 dropout 放在哪里
原论文在两个位置加了 dropout(rate=0.1):
- 每个子层的输出(在加到残差之前);
- embedding + 位置编码之后。
具体到 FFN:
h = ReLU(x W₁ + b₁)
h = dropout(h) # 注:这一步在很多实现里有
y = h W₂ + b₂
y = dropout(y) # 子层输出 dropout
return x + y
不同实现的 dropout 位置略有差异——有的在 ReLU 之后再 dropout,有的不。fairseq、Tensor2Tensor、HuggingFace 各有微小差异,但总体效果相近。
现代大模型(GPT-3、LLaMA)已经基本不用 dropout 了——因为数据足够大,过拟合不再是主要矛盾。
9.2 初始化
Xavier/Glorot 初始化是默认选择:W 用 N(0, 2/(fan_in + fan_out)) 或类似分布。但 W₂ 因为是输出回 d,常常额外乘一个 1/√(2 n_layers) 的缩放因子(来自 GPT-2 / LLaMA 的实现),目的是让深层残差累积时方差不爆炸。
9.3 训练时 FFN 的常见踩坑
坑 1:bf16 下的数值不稳。
FFN 的 4d 中间激活在 ReLU 后某些值会非常大(极端值),bf16 表示范围窄,可能溢出或下溢。LLaMA 的训练日志里能看到「ffn_norm」这种额外的 LN 就是为了抑制 FFN 的激活范围。
坑 2:SwiGLU 的实现错误。
SwiGLU 公式是 Swish(x W_gate) ⊙ x W_up,注意
Swish 只作用于 W_gate 那一支。一个常见 bug 是把 Swish 错放到
W_up 上、或者两支都过 Swish——结果模型能训但效果略差。
坑 3:在量化时 FFN 是瓶颈。
INT8 / INT4 量化通常先量化 attention,最后才动 FFN。FFN 的 W₁ 和 W₂ 都是大矩阵,权重分布有重尾,量化误差容易累积。一些方案(GPTQ、AWQ)专门针对 FFN 做了校准。
十、推理时的工程考量
FFN 在推理阶段的特性,和训练时有些不一样,值得单独提。
10.1 不能缓存
KV cache 缓存的是 attention 的 K/V——它们一旦算出来,对未来步骤永远有效,可以累积存储,下一步只追加新 token 的 K/V、复用历史。
FFN 不一样。每生成一个新 token:
- 取这个 token 的隐状态(d 维);
- 过 W₁ → 4d;
- 过激活;
- 过 W₂ → d。
历史 token 的 FFN 计算结果对当前生成完全没用——它们已经写进了那些 token 的隐状态里,也已经传递过 attention,下一步 decode 不再需要它们的 FFN 输出。
所以 decode 时每一步都要完整跑一遍 FFN,没有 cache 可以复用。
10.2 decode 是带宽瓶颈
decode 阶段(生成新 token),每步只处理 1 个 token,但要把 W₁ 和 W₂ 这两个矩阵从 HBM 读到 SRAM。这两个矩阵很大(每个 4d²,加起来 8d²),比单 token 的计算量大得多。
也就是说,decode 阶段 FFN 是带宽瓶颈,不是算力瓶颈。
这个观察对推理优化非常关键:
- batch 多个请求一起算:摊薄权重读取成本;
- speculative decoding:用小模型猜测、大模型一次验证多个 token,把单步 1 token 变成单步 k token;
- MoE 路由:一次只读取被激活专家的权重,绕开「每步读完整 FFN」的成本。
具体优化策略 50 篇之后再展开。
10.3 prefill 阶段相反
prefill 阶段(处理 prompt),一次性给一长串 token(比如 1k 个)。这时 FFN 的计算量是 1k × 8d²,已经足够大,算力变成瓶颈而不是带宽——因为权重只读一次,被 1k 个 token 共享。
所以 prefill 和 decode 在 FFN 上的瓶颈完全不同:prefill 看 GPU 的 FLOPS,decode 看 HBM 带宽。一个推理服务的延迟分布往往两者都要分别建模。
十一、量化时为什么 FFN 难处理
前面提了几次,这一节展开说。
11.1 激活的厚尾问题
INT8 量化的本质是把一个浮点张量映射到 256 个量化级。如果张量分布是 N(0, 1) 这种钟形,256 个级别覆盖均匀、误差很小。
但 FFN 第一层的输出(即 4d 维的中间激活)有几个特点:
- ReLU 之后,所有负值变 0,正值保留——分布是「半正态」加一堆零;
- 训练后期,少数 k_i 的方向被反复激活,对应的 a_i 数值特别大——形成「重尾」(heavy-tail);
- 不同 batch、不同 token 之间,重尾的位置可能不同——很难找到一个全局缩放因子。
如果你用一个简单的 max scaling 把 [-127, 127] 映射回浮点:
scale = max(|x|) / 127
x_int8 = round(x / scale)
那个 max 可能是 4d 维里某一个极端激活,导致大多数其它值在量化时只占用了几个量化级——精度损失很大。
11.2 解决方案
SmoothQuant(Xiao et al., 2022):把激活的难度部分转移到权重上。如果激活的某个通道有重尾,让权重在这个通道上变小,激活变大,乘积不变——但激活的分布变得更平。
GPTQ(Frantar et al., 2022):对权重做逐列量化,保持低误差。
AWQ(Lin et al., 2023):识别「关键权重通道」(对应那些激活重尾的通道),保留它们高精度。
FP8 训练(H100 之后):用 FP8 而不是 INT8——表示范围更宽,重尾问题轻一些。
这些细节 48-50 篇会展开。这里只要记住:FFN 在量化里特别难,因为它激活有重尾、第一层 ReLU 后分布偏斜。
十二、一些看起来无关、其实很关键的细节
12.1 偏置 b₂ 经常被去掉
很多现代实现把 FFN 第二层的偏置 b₂ 去掉了:
self.w1 = nn.Linear(d, 4*d, bias=True)
self.w2 = nn.Linear(4*d, d, bias=False) # 注意 bias=False理由是后面紧跟着残差连接和 LN,b₂ 的作用会被 LN 的 β 参数吸收。去掉省一点参数,几乎不影响表现。LLaMA 系列把 W₁、W₂、W_gate、W_up 全部都去掉了 bias。
12.2 d_ff 取整对齐
为了 GPU kernel 友好,d_ff 通常向上对齐到某个倍数(128、256、甚至 512)。例如 LLaMA-7B 的 d=4096,理论 d_ff=8d/3=10923,实际取 11008(=43 × 256)。这种对齐对训练速度有显著影响,对模型性能几乎无影响。
12.3 FFN 的「数据吞吐」相对低
Chinchilla 的 scaling law 论文里提到一个细节:增加深度(n_layers)和增加宽度(d、d_ff)对模型表现的贡献有微妙差异。FFN 主要由 d 决定,但「足够深」也很关键——3-4 层的 Transformer 即使每层 FFN 很宽也学不动复杂任务。
12.4 共享 FFN 的尝试
有几篇论文尝试让多层共享同一组 FFN 参数(ALBERT 那一类)。结果:参数量降下来了,但表现也降下来。说明 FFN 在不同层学的东西确实不同——浅层 FFN 处理表层模式,深层 FFN 处理高级概念,硬强制它们用同一组参数会丢信息。
12.5 FFN 是多语言能力的载体之一
有研究发现,多语言模型(mBERT、XLM-R)的「语言识别」能力主要分布在 FFN 中——不同 FFN 神经元对不同语言敏感。这与「FFN 是关联记忆」的视角一致:每种语言的常用模式被存到了不同的 k_i 里。
十三、PyTorch 里的最小实现走查
我们已经从原理、变体、参数、可解释性的角度把 FFN 讲了一遍。这一节落到代码——把 FFN 写成 PyTorch 模块,逐行说明每一步在做什么。这个练习的价值是:你能验证前面所有定性叙述在数值上确实成立。
13.1 原版 ReLU FFN
最直白的实现,对应原论文:
import torch
import torch.nn as nn
class FFN(nn.Module):
def __init__(self, d_model: int, d_ff: int, dropout: float = 0.1):
super().__init__()
self.w1 = nn.Linear(d_model, d_ff, bias=True)
self.w2 = nn.Linear(d_ff, d_model, bias=True)
self.dropout = nn.Dropout(dropout)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: [B, T, d_model]
h = self.w1(x) # [B, T, d_ff]
h = torch.relu(h) # [B, T, d_ff]
h = self.dropout(h) # 中间 dropout(部分实现没有这行)
y = self.w2(h) # [B, T, d_model]
y = self.dropout(y) # 子层输出 dropout
return y读这段代码时,要注意几个看起来微小、其实重要的点。
第一,nn.Linear
的输入张量可以有任意前缀维度——它只在最后一维做矩阵乘。所以你不需要把
[B, T, d] 显式 reshape 成 [B*T, d] 再做矩阵乘,PyTorch
自动处理。这从工程上印证了「FFN 是
position-wise」——所有前缀维度都被当作独立样本。
第二,torch.relu
是一个完全没有参数的算子,逐元素操作。它对反向传播的影响是「输入小于
0 的位置,梯度直接置零」——这就是 dead ReLU 的来源。
第三,dropout 在两处都加了。如果你训练大模型,通常会把第一处去掉,只保留子层输出 dropout;如果做 fine-tune 大模型,dropout 可能整体设为 0。
第四,bias=True 是 PyTorch 默认值。LLaMA
系列把它改成 bias=False——前面提过,因为后面 LN
的 β 已经吸收了偏置作用。
13.2 SwiGLU 实现
class SwiGLU(nn.Module):
def __init__(self, d_model: int, d_ff: int):
super().__init__()
# d_ff 通常取 8*d_model/3 向上对齐到某个倍数
self.w_gate = nn.Linear(d_model, d_ff, bias=False)
self.w_up = nn.Linear(d_model, d_ff, bias=False)
self.w_down = nn.Linear(d_ff, d_model, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
gate = torch.nn.functional.silu(self.w_gate(x)) # Swish/SiLU
up = self.w_up(x)
h = gate * up # 逐元素门控
return self.w_down(h)注意点:
- 三个矩阵:
w_gate、w_up、w_down。w_gate上过 Swish/SiLU,w_up直通; *是逐元素相乘;- bias 全部去掉,跟 LLaMA 的实现一致;
d_ff的取值要按8 * d_model / 3计算,再向上对齐到 256 或 64 的整数倍。
13.3 用一个具体输入跑一遍
把 d_model = 4、d_ff = 16,做一次小尺度的前向,看看每一步的形状和数值:
torch.manual_seed(42)
ffn = FFN(d_model=4, d_ff=16, dropout=0.0)
x = torch.randn(1, 3, 4) # batch=1, T=3, d=4
print("x:", x.shape, x)
print("w1:", ffn.w1.weight.shape) # [16, 4],注意 PyTorch Linear 是 [out, in]
print("w2:", ffn.w2.weight.shape) # [4, 16]
h_pre = ffn.w1(x)
print("h_pre (before ReLU):", h_pre.shape) # [1, 3, 16]
h = torch.relu(h_pre)
print("h (after ReLU):", h.shape, "zeros:", (h == 0).float().mean().item())
y = ffn.w2(h)
print("y:", y.shape) # [1, 3, 4]如果把 (h == 0)
那个比例打出来,会发现大概一半的 h_pre 元素被 ReLU
砍掉——这是 ReLU 在 N(0, σ²) 输入下的预期(精确说是 50%
概率,因为对称分布的负半部分被丢)。
这个观察其实很关键:FFN
第一层有大约一半的「容量」在每个样本上是浪费的——被
ReLU 直接归零、不参与第二层的加权和。换句话说,FFN
的有效隐藏维度大约是
d_ff / 2 = 2d,这部分解释了为什么 SwiGLU 用
8d/3 的窄一点的 d_ff 也能匹配 4d 的 ReLU FFN——SwiGLU
的门控不会硬归零,「有效维度」更接近 d_ff 本身。
13.4 验证「逐位置」性质
写一个小实验:构造两个不同的 batch、不同的位置,但中间某一个位置的 x 值完全相同,看 FFN 输出是不是相同。
ffn.eval()
x_a = torch.randn(1, 3, 4)
x_b = x_a.clone()
x_b[0, 0, :] = torch.randn(4) # 把第 0 个位置改了
with torch.no_grad():
y_a = ffn(x_a)
y_b = ffn(x_b)
# 第 1、2 个位置的输入相同 → 输出应该相同
diff_pos1 = (y_a[0, 1, :] - y_b[0, 1, :]).abs().max()
diff_pos2 = (y_a[0, 2, :] - y_b[0, 2, :]).abs().max()
diff_pos0 = (y_a[0, 0, :] - y_b[0, 0, :]).abs().max()
print("diff at pos 1:", diff_pos1.item()) # 应该是 0
print("diff at pos 2:", diff_pos2.item()) # 应该是 0
print("diff at pos 0:", diff_pos0.item()) # 应该 > 0跑一遍会发现 pos 1、pos 2 的输出完全相同(只受自己位置的输入影响),pos 0 因为输入变了所以不同。这就是「FFN 是 position-wise」的可执行验证。
如果把它换成 attention,结论就不对——attention 子层的输出在每个位置都依赖所有位置,改任何一个位置的输入,所有位置的输出都会变。
十四、FFN 与 attention 的相互作用
把 FFN 单独讲完之后,回过头看它和 attention 的关系,能得到一些更细的洞察。
14.1 「先通信、再计算」的隐喻
最常见的隐喻是:attention 是「token 之间通信」,FFN 是「token 内部计算」。这个隐喻很好用,但需要小心一点——它给人一个错误印象,好像两者完全独立、可以拆开看。
实际上,attention 和 FFN 是紧密耦合的:
- attention 把上下文混到当前位置的隐状态里,这个混合后的隐状态正好是 FFN 的输入——也就是说,FFN 看到的「特征」已经是带上下文的特征;
- FFN 输出的隐状态又会进入下一层的 attention,作为下一轮 Q/K/V 的来源——也就是说,FFN 的非线性变换会影响下一层 attention 的行为。
所以严格说,每一层 attention 和 FFN 形成一个「混合 → 加工 → 混合 → 加工 → …」的链条。多层下来,每个 token 的隐状态既反映上下文信息,也反映多次非线性变换的结果。
14.2 「事实回忆」需要两者配合
举一个具体例子。模型要预测 “The capital of France is _” 后面的词。
理想情况下,模型应该回忆起「France 的首都是 Paris」这条事实。这件事是怎么在网络里发生的?
按 Geva 等人的研究,大致是这样:
早期层:attention 把 “France” 这个 token 的信息搬到位置 5(也就是 “is” 后面这个待生成位置)周围。具体说,attention 头会让位置 5 的 Q 与「France」的 K 强匹配,从而把 France 的 V 加到位置 5 的隐状态里。
中后期层:位置 5 的隐状态现在已经携带了「country=France」「question_type=capital」这种语义。FFN 的 k_i(W₁ 的列向量)里,有一个或几个专门响应「the capital of
」这种模式——它的 k 与位置 5 的隐状态内积很高,对应的 v 在 vocab 投影后倾向 Paris、Lyon、Marseille 这些法国城市的方向。 最后一层:经过几次 attention + FFN 加工,位置 5 的隐状态在 unembedding 矩阵投影后,Paris 的 logit 最高。
也就是说,「事实回忆」是 attention 和 FFN 联合完成的——attention 负责把相关上下文搬到当前位置,FFN 负责在当前位置做 lookup。少了任何一个,事实都回忆不出来。
ROME(Meng et al., 2022)做模型编辑,就是利用这个机制:他们定位「the capital of France」对应的 FFN 神经元,然后修改它们的 v_i,让这个事实从 “Paris” 变成 “Beijing”——之后模型在回答这个问题时确实会输出错误答案。这反向验证了「FFN 存事实」的视角。
14.3 attention 和 FFN 谁更「重要」
这是一个常被问、其实没标准答案的问题。
从消融实验看,把 attention 换成简单的卷积或 MLP-Mixer,模型表现会下降但不会完全垮——说明 attention 不是不可替代的。把 FFN 换成更窄的版本(比如 d_ff=d 而不是 4d),表现也会下降但能用——说明 FFN 也不是不可替代的。
但两者一起去掉,模型基本学不动。这说明 attention 和 FFN 是互补的,不是冗余的。
「重要性」这个问题更应该问:「在我的具体任务上,性能瓶颈在哪边?」
- 长上下文检索:attention 是瓶颈(它的 O(T²) 决定了能不能处理长序列);
- 知识密集型问答:FFN 是瓶颈(它的容量决定了能存多少事实);
- 推理 throughput:FFN 是瓶颈(decode 阶段每步都要算);
- 推理 latency(短输出):attention 和 FFN 都重要,但 prefill 阶段是 attention 主导。
十五、把 FFN 全部讲完之后再回头看一眼
我们花了非常长的篇幅讲一个表面上「两行代码就写完」的子层。这是值得的,因为 FFN 在 Transformer 里被严重低估了。
让我们用三句话做一个总结:
第一,FFN 是 Transformer 的「容量大头」——它占总参数的三分之二,模型能力的大部分都来自这里。剪枝、量化、MoE 的目标都是它。
第二,FFN 是模型存储「学到的内容」的地方——不是每个神经元一个事实,但通过叠加表示,它确实是知识的物理载体。attention 是调度器,FFN 是仓库。
第三,FFN 是 Transformer 唯一一处「位置无关」的子层——它逐位置独立处理,不混 token、不看上下文。这种性质让它工程友好、容易并行,但也让它在长上下文优化里显得格外突出(成本随 token 数线性增长,没法 cache)。
下一篇我们要回到原论文本身——讲清楚「Vaswani 他们到底是怎么把这个 Transformer 训出来的」:8 张 P100、12 小时、warmup 4000 步、label smoothing 0.1,所有这些细节当年是怎么调出来的。
十六、关键概念回顾(散文式)
回过头看这一篇,我们其实做了三件事。
第一件,是把 FFN 这个看似简单的子层「打开」:它是两层 MLP,先升维到 4d、过非线性、再压回 d。我们解释了为什么是 2 层、为什么是 4 倍、为什么逐位置——这三个设计选择都有原论文的实证依据,而且后来主流模型基本沿用没怎么改(除了把 4 倍改成更精巧的 SwiGLU 8/3 倍,那也只是因为多了一个门控矩阵,参数量其实一致)。
第二件,是引入「键值记忆」这个新视角:把 W₁ 的列看成 keys,W₂ 的行看成 values,FFN 就变成了「带固定 K/V 的 attention」——用输入向量与每个 key 算内积、ReLU 过一下作为权重、再加权求 values。这个视角的好处是把 FFN 和 attention 放到同一个数学框架下,让我们能用「检索接口 + 关联记忆」这套语言描述整个 Transformer。这也是 ROME、模型编辑、可解释性研究背后的理论基础。
第三件,是把 FFN 在工程上的影响讲透:它占参数 2/3、它在量化里最难、它在 decode 时是带宽瓶颈、它没法 cache、它是 MoE 的对象、它的激活有厚尾、它和 LN/dropout/初始化都密切相关。这些工程细节决定了你做 LLM 推理或微调时,绝大多数优化工作其实都在 FFN 这一块。
如果只让你记住一句话,那是:「Attention 让 token 之间通信,FFN 让 token 内部计算;attention 是接口,FFN 是仓库。」
十七、常见误解
下面几条是我读论文、跟人讨论时反复见到的误解,逐一指出来。
误解一:FFN 只是辅助子层,attention 才是核心。
错。从参数量看 FFN 是 attention 的两倍;从「模型存了什么知识」看 FFN 是事实和模式的载体;从 decode 推理成本看 FFN 是大头。它不是辅助,它是核心。
误解二:4 倍扩张比是某个理论推出来的。
错。它是 2017 年原论文消融出来的经验值,2 倍不够、8 倍边际收益小,4 倍是甜点。后来所有 ReLU FFN 都沿用,但这是「实证习惯」而不是「数学定理」。
误解三:SwiGLU 的 d_ff = 8d/3 比原版 4d 窄、参数量更少。
错。SwiGLU 多了一个门控矩阵,三个矩阵合计参数量正好等于 ReLU FFN 的两个矩阵——8/3 这个数字就是为了让总参数对齐。SwiGLU 不是更省参数,是用同等参数换更好性能。
误解四:FFN 的每个神经元对应一个具体概念。
只对一半。早期可视化工作确实发现一些「单义」神经元,但 Anthropic 的 superposition 假设和后续 SAE 工作表明,多数神经元是「多义」的——一个神经元同时编码多个不相关概念,需要专门工具(稀疏自编码器)才能解出真正的单义特征。
误解五:MoE 是把 attention 切成多个专家。
错。MoE 几乎全部是把 FFN 切成多个专家,attention 不动。这是因为 FFN 占参数大头、逐位置易切分、键值记忆视角自然支持「多个子记忆库」。
十八、下一步
下一篇 27|训练原论文 Transformer 把视角从「结构」切换到「训练过程」。我们会讲:
- 8 张 P100、12 小时是怎么训出来的;
- warmup_steps=4000 这个魔法常数为什么不能去掉;
- 学习率公式
lr = d^{-0.5} · min(step^{-0.5}, step · warmup_steps^{-1.5})怎么读; - label smoothing 0.1、dropout 0.1、Adam β₂=0.98 这些超参数是怎么调出来的;
- 现代大模型训练的配方和 2017 年比,到底变了什么。
再往后:
- 28|原论文实验结果:BLEU 28.4 是怎么测的、消融实验讲了什么、注意力可视化看到了什么;
- 29|Tokenization:为什么不是字、不是词、而是 BPE 子词;
- 30|预训练目标:BERT 的 MLM 与 GPT 的自回归,路线分歧从哪儿开始。
十九、参考文献
下面按相关度排序,列出本篇直接引用与延伸阅读,每条附一句话提示其在本篇中的角色。
- Vaswani, A. et al. “Attention Is All You Need.” NeurIPS 2017. FFN 原始定义、d_ff = 4d 的消融。
- Geva, M., Schuster, R., Berant, J., Levy, O. “Transformer Feed-Forward Layers Are Key-Value Memories.” EMNLP 2021. 「键值记忆」视角的开山论文。
- Geva, M. et al. “Dissecting Recall of Factual Associations in Auto-Regressive Language Models.” EMNLP 2023. 把事实回忆定位到具体 FFN 神经元。
- Meng, K. et al. “Locating and Editing Factual Associations in GPT (ROME).” NeurIPS 2022. 模型编辑工作,定位并改写 FFN 中的事实。
- Meng, K. et al. “Mass-Editing Memory in a Transformer (MEMIT).” ICLR 2023. ROME 的批量化扩展。
- Shazeer, N. “GLU Variants Improve Transformer.” arXiv:2002.05202, 2020. SwiGLU、GeGLU 等变体的对照实验。
- Dauphin, Y. et al. “Language Modeling with Gated Convolutional Networks.” ICML 2017. GLU 最早提出。
- Hendrycks, D., Gimpel, K. “Gaussian Error Linear Units (GELUs).” arXiv:1606.08415, 2016. GELU 激活的来源(BERT/GPT-2 使用)。
- Ramachandran, P., Zoph, B., Le, Q. V. “Searching for Activation Functions.” ICLR 2018 Workshop. Swish/SiLU 的来源。
- Elhage, N. et al. “Toy Models of Superposition.” Anthropic, 2022. 解释为什么神经元是多义的(叠加假设)。
- Bricken, T. et al. “Towards Monosemanticity: Decomposing Language Models With Dictionary Learning.” Anthropic, 2023. 用稀疏自编码器解出单义特征。
- Templeton, A. et al. “Scaling Monosemanticity: Extracting Interpretable Features from Claude 3 Sonnet.” Anthropic, 2024. SAE 在大模型上的应用。
- Shazeer, N. et al. “Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer.” ICLR 2017. MoE 的早期论文。
- Fedus, W., Zoph, B., Shazeer, N. “Switch Transformer: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity.” JMLR 2022. 1.6T 参数的 MoE。
- Jiang, A. Q. et al. “Mixtral of Experts.” arXiv:2401.04088, 2024. Mistral 的 MoE 工作。
- Touvron, H. et al. “LLaMA: Open and Efficient Foundation Language Models.” arXiv:2302.13971, 2023. SwiGLU 在大模型上的标准实现。
- Touvron, H. et al. “Llama 2: Open Foundation and Fine-Tuned Chat Models.” arXiv:2307.09288, 2023.
- Chowdhery, A. et al. “PaLM: Scaling Language Modeling with Pathways.” arXiv:2204.02311, 2022. 用 SwiGLU 的旗舰大模型之一。
- Brown, T. et al. “Language Models are Few-Shot Learners (GPT-3).” NeurIPS 2020. d=12288, d_ff=49152 的 4 倍 FFN 实例。
- Devlin, J. et al. “BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding.” NAACL 2019. d=768, d_ff=3072 的 BERT-base FFN 实例。
- Xiao, G. et al. “SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models.” ICML 2023. 处理 FFN 激活重尾的量化方案。
- Frantar, E. et al. “GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers.” ICLR 2023. 权重量化方案。
- Lin, J. et al. “AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration.” MLSys 2024. 关键通道保留的量化方案。
- Lan, Z. et al. “ALBERT: A Lite BERT for Self-supervised Learning of Language Representations.” ICLR 2020. 跨层共享 FFN 参数的尝试。
- Karpathy, A. “The Unreasonable Effectiveness of Recurrent Neural Networks.” Blog, 2015. 神经元功能可视化的早期工作。
← 上一篇:25|Layer Normalization | 下一篇:27|训练原论文 Transformer →
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Transformer 与注意力机制】05. 激活函数:让网络「弯下来」的非线性魔法
上一篇我们论证了一件事——纯线性的网络再深,也只是一个线性变换。把 $W2(W1\mathbf{x} + \mathbf{b}1) + \mathbf{b}2$ 展开就是 $W'\mathbf{x} + \mathbf{b}'$。线性的复合还是线性,这是线性代数的铁律。
【Transformer 与注意力机制】04. 函数与神经网络:从 y=f(x) 到一台可学习的拟合机器
如果你问我「神经网络到底是什么」,我会先把所有教材合上,然后给你一句朴素得近乎敷衍的话——神经网络就是一个函数。
【Transformer 与注意力机制】03 矩阵乘法的两种视角
把矩阵乘法掰开成两种等价但风格不同的视角——『行 × 列』的点积视角和『列的线性组合』视角,最终落到 QK^T 的形状分析。
【Transformer 与注意力机制】02 向量与点积的几何直觉
从二维平面上的箭头开始,把『向量、内积、夹角、相似度』这几个概念用几何方式串起来,最后落到注意力公式里那个 QK^T 的来历。