MiniMind 学习笔记 08:FeedForward 与 MoE,LLM Block 里的另一半计算

很多人在学习 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 就是模型“加工理解”的主要场所。

发表评论