MoE 的关键不是专家多,而是路由稳
MoE 的核心不是堆更多专家,而是用稳定路由把容量、计算、显存和通信重新做平衡。
大模型扩容时,最直接的办法是把 FFN 做大。FFN 是 Transformer 里真正吃参数的模块,也常被看作模型知识容量的重要来源。问题在于,稠密 FFN 有一个硬约束:每个 token 都要经过同一套参数。参数变大,单 token 的计算量也跟着变大。
MoE 想拆开的就是这根绑带。它不让每个 token 都访问全部 FFN 参数,而是准备一组专家 FFN,再用 Router 给每个 token 选择少数几个专家。总参数可以很大,单次激活的参数仍然保持较小。
这也是 MoE 最核心的工程判断:用稀疏激活扩大容量,用路由机制控制计算。它不是免费加参数,而是在显存、通信、训练稳定性之间重新做账。
MoE 替换的是 FFN 位置
MoE 不是挂在 Transformer 外面的附加系统。常见做法是把 Transformer Block 里的 FFN 子层替换为 MoE 层。Attention 仍然负责 token 间的信息交互,MoE 层负责在每个 token 的位置上做条件计算。
图:MoE 通常替换 Transformer Block 中的 FFN 子层
原生 Transformer 没有 MoE。MoE 是后来在 FFN 子层上的一种结构替换。理解这一点很重要,因为专家本身并不神秘,通常就是一组参数独立的 FFN。

图:MoE 的工程关键在于 Router 如何稳定地分配 token。
MoE 解决的是容量和计算的绑定
稠密模型的逻辑很简单:模型参数越多,容量越大;但每个 token 都要穿过全部参数,推理成本也同步上涨。
MoE 改成条件计算:
- 总参数很大:有很多专家,每个专家都是独立 FFN。
- 激活参数很小:每个 token 只交给 Top-K 个专家处理。
- 其他专家待命:它们占显存,但这一轮不参与该 token 的计算。
![mermaid-02.png]()
图:MoE 通过 Router 只激活少数专家
可以把稠密 FFN 看成一个全科医生,什么 token 都由同一个医生处理。MoE 更像专科医院:门口的 Router 负责分诊,每次只请最相关的几位专家出场。
一个 token 在 MoE 里怎么走
一个标准 MoE 层通常由两部分组成:
| 组件 | 职责 |
|---|---|
| Router / Gating Network | 给每个 token 对所有专家打分,选择 Top-K 专家 |
| Experts | 多个结构相同、参数独立的 FFN,负责实际变换 token 表示 |
对输入 token 向量 $x$,Router 先计算专家分数:
$$
g(x) = W_g \cdot x, \quad W_g \in \mathbb{R}^{N \times d_{model}}
$$
然后选出分数最高的 $K$ 个专家,并只在 Top-K 内做 Softmax:
$$p_i = \frac{\exp(g(x)i)}{\sum{j \in \mathrm{TopK}} \exp(g(x)_j)}, \quad i \in \mathrm{TopK}$$
最后把这些专家的输出加权合并:
$$
y = \sum_{i \in \mathrm{TopK}} p_i \cdot E_i(x)
$$
符号含义如下:
| 符号 | 含义 | 维度 |
|---|---|---|
| $x$ | 输入 token 向量 | $d_{model}$ |
| $W_g$ | Router 权重 | $N \times d_{model}$ |
| $g(x)$ | 各专家原始分数 | $N$ |
| $N$ | 专家总数 | 标量,如 8、64、256 |
| $K$ | 每个 token 激活的专家数 | 标量,如 1 或 2 |
| $E_i(x)$ | 第 i 个专家 FFN 的输出 | $d_{model}$ |
| $p_i$ | 第 i 个专家的合并权重 | 标量 |
Router 学的不是人类语义上的分类,而是这个 token 交给谁处理更合适。

图:Router 选择专家并加权合并输出
教学版 MoE 代码
真实工程会用批处理、分组 dispatch、专家并行和 All-to-All 通信优化。下面这段代码只保留核心逻辑,方便看清 Router、Top-K 和专家合并的关系。
import torch
import torch.nn as nn
import torch.nn.functional as F
class MoELayer(nn.Module):
def __init__(self, d_model, d_ff, num_experts, top_k=2):
super().__init__()
self.top_k = top_k
self.gate = nn.Linear(d_model, num_experts)
self.experts = nn.ModuleList([
nn.Sequential(
nn.Linear(d_model, d_ff),
nn.SiLU(),
nn.Linear(d_ff, d_model),
)
for _ in range(num_experts)
])
def forward(self, x):
logits = self.gate(x)
weights, idx = torch.topk(logits, self.top_k, dim=-1)
weights = F.softmax(weights, dim=-1)
out = torch.zeros_like(x)
for slot in range(self.top_k):
for e in range(len(self.experts)):
mask = idx[:, slot] == e
if mask.any():
out[mask] += (
weights[mask, slot:slot + 1] *
self.experts[e](x[mask])
)
return out
这段代码效率很低,但把 MoE 的核心动作写清楚了:先按 token 选专家,再让专家处理属于自己的 token,最后把结果加权写回。
Router 为什么是 MoE 的难点
专家只是 FFN,真正难的是 Router。
Router 要做离散选择:从 N 个专家里选 Top-K。可训练时又希望整个系统可导。这会带来两个问题:
- Top-K 截断本身不可导,路由如何学得更好。
- Router 很容易把 token 分给少数热门专家,造成负载不均衡。
负载不均衡是 MoE 的头号顽疾。训练早期,某些专家偶然被选得更多,它们得到更多训练,效果更好,于是又更容易被 Router 选中。最后少数专家被挤爆,大量专家几乎不工作。

图:路由不稳会造成专家过载和训练低效
结果很直接:冷门专家白占参数,模型实际容量缩水;热门专家变成计算瓶颈。MoE 训练必须主动干预负载分配。
路由方案的演进主线
MoE 路由技术一直在回答同一个问题:怎么既选得准,又分得匀。
| 方案 | 做法 | 好处 | 代价 |
|---|---|---|---|
| Noisy Top-K Gating | 打分时注入可学习噪声,再做 Top-K | 避免早期过快锁死热门专家 | 仍需配合额外均衡机制 |
| Aux Loss | 在主任务损失外加入负载均衡损失 | 经典、稳定、工程上常见 | 和主任务目标存在张力,超参难调 |
| Expert Choice Routing | 从专家视角选择 token,每个专家固定容量 | 天然负载均衡 | token 可能被多个专家选中,也可能没人选 |
| Loss-Free Balancing | 给每个专家加动态 bias,太忙调低、太闲调高 | 不引入额外主损失,选择前调节负载 | 仍需控制 bias 更新速率 |
Noisy Top-K:给冷门专家出场机会
早期 MoE 方案会在 Router 分数里加入噪声,让选择带一点随机扰动。这样可以避免模型在训练初期过早把流量锁死到少数专家上。
它解决的是探索问题:冷门专家至少有机会拿到 token,才可能被训练起来。
Aux Loss:用额外损失拉平负载
GShard、Switch Transformer 等方案常用辅助负载均衡损失。直觉形式是:
$$
L_{aux} = \alpha \cdot N \cdot \sum_{i=1}^{N} f_i \cdot P_i
$$
其中:
- $f_i$ 是实际被路由到专家 i 的 token 比例。
- $P_i$ 是 Router 分给专家 i 的平均概率。
- $\alpha$ 控制均衡惩罚强度。
负载越集中,这项损失越大,训练就会把 Router 往更均匀的方向拉。问题也在这里:辅助损失不是主任务本身,调大了可能伤效果,调小了又压不住不均衡。
Expert Choice:让专家反过来选 token
传统路由是 token 选专家。Expert Choice 反过来,让每个专家在容量范围内选择自己分数最高的 token。
它的好处是负载天然均衡,因为每个专家拿到固定容量。代价是 token 维度不再规整:同一个 token 可能被多个专家选中,也可能没有专家选。这个思路更依赖序列级视角,适用场景和工程实现都要单独评估。
Loss-Free:在选择前调节专家热度
DeepSeek-V3 等较新的方案试图摆脱 Aux Loss,用动态 bias 调节每个专家被选中的概率。Router 原始分数是 $s_i$,选择前变成:
$$
s_i'=s_i+b_i
$$
用 $s_i'$ 做 Top-K 选择,但最终合并权重仍基于原始分数 $s_i$。这样 bias 只改变选择,不直接污染前向语义。
bias 的更新规则可以写成:
$$
b_i \leftarrow b_i + \gamma \cdot sign(\overline{c_i} - c_i)
$$
其中:
- $c_i$ 是这一批里专家 i 收到的 token 数。
- $\overline{c_i}$ 是所有专家的平均 token 数。
- $\gamma$ 是 bias 更新速率。
某个专家太忙,bias 调低;太闲,bias 调高。它把负载均衡从损失函数里挪到路由选择前。
MoE 省的是 FLOPs,不是显存
MoE 最容易被误解的一点是省资源。它省的是计算量,不是参数显存。
一个 token 只激活 2 个专家,并不代表其他专家可以不加载。所有专家参数都要在显存里待命。一个总参数 200B、激活参数 30B 的 MoE,计算近似 30B 稠密模型,但显存占用接近 200B 量级。

图:Router 选择专家并加权合并输出
所以 MoE 是拿显存换算力。
多卡训练里,All-to-All 是大头
大规模 MoE 往往使用专家并行。专家分布在不同 GPU 上,一个 token 被路由到哪个专家,不一定在当前卡。于是每个 MoE 层都可能发生两次通信:
- 把 token 发到目标专家所在 GPU。
- 专家计算完,再把结果发回来。
![mermaid-06.png]()
图:多卡 MoE 训练里的通信开销来自 token 分发和回收
在大集群里,All-to-All 通信经常比专家 FFN 计算更难优化。MoE 的稀疏计算收益,必须和路由、dispatch、combine、跨卡通信一起算。
稠密 FFN 和 MoE 的工程账
| 维度 | 稠密 FFN | MoE |
|---|---|---|
| 总参数容量 | 较小 | 很大 |
| 单 token 激活参数 | 全部 | Top-K 少数专家 |
| FLOPs | 随参数同步增加 | 可保持相对较低 |
| 显存占用 | 参数量多大就加载多大 | 所有专家常驻,显存压力高 |
| 通信开销 | 较低 | 专家并行下 All-to-All 显著 |
| 训练稳定性 | 相对稳定 | Router、负载波动、loss spike 都要处理 |
| 适用场景 | 中小规模或简单部署 | 超大容量、追求单位计算性价比 |
MoE 的价值不是让模型无成本变大,而是在总容量和单次计算之间打开一个新的折中空间。
常见误区
| 问题 | 判断 |
|---|---|
| MoE 是多个模型投票吗 | 不是。它是模型内部的条件计算结构,专家通常是多个 FFN |
| 参数多,计算就一定多吗 | 不一定。关键看每个 token 激活多少专家 |
| 专家会变成数学专家、代码专家吗 | 不一定。专家可能有分工,但未必符合人类标签 |
| MoE 推理一定更快吗 | 不一定。还要算路由、分发、合并和跨卡通信 |
| Expert 越多越好吗 | 不是。专家多会增加容量,也会增加路由和通信复杂度 |
| Top-K 越大越好吗 | 不是。K 变大可能增强表达,也会提高计算和通信成本 |
| 每层都要换成 MoE 吗 | 不一定。可以只替换部分 Transformer Block 的 FFN 子层 |
真正要带走的判断
MoE 的核心不是专家越多越好,而是三个问题能否同时被控制住:
- 容量能否扩大:用多个 FFN 专家提供更大的总参数空间。
- 计算能否稀疏:每个 token 只激活少量专家。
- 路由能否稳定:热门专家不过载,冷门专家不饿死,跨卡通信可承受。
Router 是 MoE 的灵魂,也是它最难工程化的部分。Noisy Top-K、Aux Loss、Expert Choice、Loss-Free Balancing 这些方案,本质都在处理同一组矛盾:选择要准,负载要均衡,训练目标不能被过度干扰。
MoE 值得用,是因为它把参数容量和单 token 计算量解耦了;MoE 难用,是因为它把问题转移到了显存、通信、路由和稳定性上。理解这笔账,比记住“混合专家”这个名字更重要。
推荐阅读
Prime Agent 把 Coding Agent 从工具调用推向可恢复工作流
Agent Team 真正缺的不是更多 Agent,而是同步协议
DeepSeek Harness 为什么能热换模型:插件依赖、事件日志与回滚机制
DeepSeek Harness:Agent 自我改进之前,先让 Harness 可观察、可组合、可回滚




浙公网安备 33010602011771号