很多人在学习 Transformer 时会把注意力都放在 Attention 上。
但在现代 LLM 中,FeedForward / MLP 同样重要,甚至经常是参数量和计算量的大头。
MiniMind 的 block 里有一行:
self.mlp = FeedForward(config) if not config.use_moe else MOEFeedForward(config)
这说明每个 block 的第二个子层可能是:
普通 FeedForward 或 MoEFeedForward
这篇文章就看这两部分。
1. FeedForward 在 Block 中的位置
MiniMindBlock 的主流程是:
RMSNorm -> Self-Attention -> Residual -> RMSNorm -> FeedForward / MoE -> Residual
Attention 负责不同 token 之间的信息交互。
FeedForward 负责每个 token 自己的非线性加工。
也就是说:
Attention:横向看上下文。 FeedForward:纵向加工当前 token 表示。
2. MiniMind 的 FeedForward 结构
MiniMind 的普通 FFN 是 SwiGLU 风格:
down_proj(act(gate_proj(x)) * up_proj(x))
可以拆成三条线:
x
|------------------|
v v
gate_proj up_proj
| |
act |
| |
|------ multiply --|
|
v
down_proj
|
v
output
传统 FFN 常见结构是:
Linear -> activation -> Linear
SwiGLU 多了一个门控分支:
act(gate_proj(x)) * up_proj(x)
它让模型可以动态控制哪些通道被放大、哪些通道被抑制。
3. intermediate_size 和 MLP ratio
FFN 通常会先升维,再降维。
如果:
hidden_size = 768 intermediate_size ≈ 2432
那么过程是:
768 -> 2432 -> 768
这个中间维度和 hidden size 的比例常被称为 MLP ratio。
在很多 Transformer 结构中,MLP 中间层会比 hidden size 大很多。
原因是:
升维后,模型有更大的空间做非线性组合; 再降维回主干 hidden size,继续进入下一层。
4. FeedForward 是主要瓶颈吗
答案是:很多情况下是。
一个 block 里主要计算来自两块:
Attention FeedForward
Attention 的复杂度和序列长度强相关:
O(seq_len^2)
FeedForward 的复杂度和 token 数、hidden size、中间层维度强相关:
O(seq_len * hidden_size * intermediate_size)
在短序列或 decode 阶段,Attention 的二次项不一定总是最大,FFN 反而可能占据大量计算。
这也是为什么 MoE 主要替换 FFN,而不是替换 Attention。
5. MoE 的基本想法
MoE 是 Mixture of Experts。
普通 FFN 是:
所有 token 都走同一个 FFN。
MoE 是:
准备多个 FFN expert; 每个 token 只选择其中少数几个 expert; 把 expert 输出加权合并。
也就是:
token -> router -> top-k experts -> weighted sum -> output
这样模型总参数量可以变大,但每个 token 实际激活的 expert 数有限。
6. MiniMind 的 MOEFeedForward
MiniMind 中 MoE 的核心组件包括:
self.gate = nn.Linear(hidden_size, num_experts, bias=False)
self.experts = nn.ModuleList([
FeedForward(...)
for _ in range(num_experts)
])
gate 就是 router。
对于每个 token,它会输出每个 expert 的分数:
scores: [num_experts]
然后取 top-k:
topk_weight, topk_idx = torch.topk(scores, k=num_experts_per_tok)
如果:
num_experts = 4 num_experts_per_tok = 1
那么每个 token 只会选 1 个 expert。
7. router 是怎么训练的
router 的选择里有一个难点:
topk_idx 是离散选择,不可导。
但被选中的权重 topk_weight 来自 softmax scores,是连续可导的。
所以训练时:
被选中的 expert 会收到梯度; 被选中的 topk_weight 也会把梯度传回 gate; 未被选中的 expert 当前 token 不更新。
这意味着 router 会通过训练逐渐学会:
哪些 token 更适合交给哪个 expert。
但这个学习不是监督标签告诉它“该选专家 2”,而是来自最终语言模型 loss 的反向传播。
8. aux_loss:避免专家塌缩
如果只靠主 loss,router 可能会偷懒:
总是选择少数几个 expert。
这样会导致:
热门 expert 过载; 冷门 expert 训练不足; MoE 容量浪费。
MiniMind 中有一段:
load = F.one_hot(topk_idx, self.config.num_experts).float().mean(0)
self.aux_loss = (
load * scores.mean(0)
).sum() * self.config.num_experts * self.config.router_aux_loss_coef
可以直觉理解为:
load:实际被选中的 expert 分布。 scores.mean(0):router 平均想分给各 expert 的概率。 aux_loss:鼓励路由不要过度集中。
它是一种负载均衡损失。
9. 训练和推理时 expert 数能不能不一样
通常不建议随便改。
训练时如果设置:
num_experts_per_tok = 1
模型学到的就是每个 token 激活 1 个 expert 的行为。
推理时强行改成全部 expert 都打开,输出分布会变,计算量也会大幅增加。
从数学上看,如果所有 expert 都按权重加权参与,它有点像一个更大的条件化 MLP。
但它不是普通标准 MLP,因为:
每个 expert 有独立参数; router 决定组合权重; 训练过程中 expert 已经按稀疏激活方式分工。
所以不能简单认为“打开所有 expert 就等价于普通 MLP”。
10. 大规模 MoE 的工程设计
真实大规模 MoE 还会有更多设计:
| 机制 | 作用 |
|---|---|
| top-2 routing | 每个 token 选两个 expert,增强稳定性。 |
| router noise | 给 router 加噪声,鼓励探索,避免过早塌缩。 |
| capacity | 限制每个 expert 最多接收多少 token。 |
| dropless routing | 尽量不丢 token,提升训练稳定性。 |
| z-loss | 控制 router logits 过大。 |
| expert parallel | 不同 expert 放到不同 GPU,需要通信。 |
这些机制的核心目标都是:
让 MoE 既高效,又稳定,又能充分利用所有 expert。
11. 小结
FeedForward 和 MoE 可以这样理解:
FeedForward: 每个 token 都走同一个非线性网络。 MoEFeedForward: 每个 token 先由 router 选择 expert,再走少数 expert。
MoE 的价值是扩大参数容量,代价是路由训练、负载均衡和分布式通信更复杂。
如果说 Attention 让模型“看上下文”,那么 FeedForward / MoE 就是模型“加工理解”的主要场所。