一句话:MoE 是对 Transformer 中 FFN 层的改进——把一个 FFN 替换成多个并行的"专家"FFN,每个 token 只激活其中少数几个专家,从而在不增加计算量的前提下大幅扩展模型参数量。
一、问题背景:Dense 模型扩参数的代价
标准 Transformer(Dense 模型)每个 token 都要经过所有参数:
参数量翻倍 → 计算量翻倍 → 训练/推理成本翻倍
想要更强的模型,就必须付出更高的计算代价。有没有办法让参数量和计算量解耦?
二、核心思想:条件计算(Conditional Computation)
不是每个 token 都需要用到所有知识,让不同的 token 走不同的"专家"路径。
MoE 的核心设计:
把 FFN 层替换成 N 个并行的专家 FFN
每个 token 由一个路由器(Router)决定激活哪 K 个专家
只有被选中的专家才参与计算,其余专家跳过
结果:
- 总参数量 = N 个专家的参数之和(很大)
- 每次计算量 = 只激活 K 个专家(很小)
- 参数量和计算量不再线性绑定
三、结构对比
Dense FFN(原始)
输入 x(形状:[seq_len, d_model])
↓
FFN(W_gate, W_up, W_down)
↓
输出(形状:[seq_len, d_model])
MoE FFN(替换后)
输入 x(形状:[seq_len, d_model])
↓
Router(路由器):为每个 token 计算各专家的得分,选出 Top-K
↓
并行调用被选中的 K 个专家 FFN
↓
按路由权重加权求和,合并输出
↓
输出(形状:[seq_len, d_model])← 形状和 Dense FFN 完全一致
注意:Attention 层不变,只有 FFN 层被替换成 MoE。
四、路由器(Router)的工作原理
Router 是一个简单的线性层,输出每个专家的"得分":
$$\text{scores} = \text{softmax}(x \cdot W_{\text{router}})$$
其中 $W_{\text{router}}$ 形状为 $[d_model, N_experts]$,输出 N 个专家的概率分布。
然后取 Top-K 个得分最高的专家:
import torch
import torch.nn as nn
import torch.nn.functional as F
# 构造最小示例:4 个 token,d_model=8,共 4 个专家,Top-2
torch.manual_seed(0)
N_tokens, d_model, num_experts, top_k = 4, 8, 4, 2
x = torch.randn(N_tokens, d_model) # 输入 token 向量
W_router = torch.randn(d_model, num_experts) # Router 权重矩阵
experts = [nn.Linear(d_model, d_model, bias=False) for _ in range(num_experts)]
# 第一步:Router 计算每个 token 对各专家的得分
scores = F.softmax(x @ W_router, dim=-1) # [4, 4]
# 第二步:选 Top-2 专家
topk_scores, topk_indices = scores.topk(k=top_k, dim=-1) # 各形状 [4, 2]
# 第三步:对 Top-K 权重重新归一化,使每个 token 的权重之和为 1
topk_scores = topk_scores / topk_scores.sum(dim=-1, keepdim=True)
# 第四步:对每个 token,加权求和被选中专家的输出
output = torch.zeros(N_tokens, d_model)
for k_pos in range(top_k):
expert_ids = topk_indices[:, k_pos] # 每个 token 在第 k_pos 位选的专家编号,形状 [4]
weights = topk_scores[:, k_pos] # 对应权重,形状 [4]
for token_i in range(N_tokens):
expert_id = expert_ids[token_i].item()
output[token_i] += weights[token_i] * experts[expert_id](x[token_i])
print(output.shape) # torch.Size([4, 8])
print(topk_indices) # 每个 token 选中的专家编号,如 tensor([[2,1],[0,3],[1,2],[3,0]])
五、完整代码示例(PyTorch)
import torch
import torch.nn as nn
import torch.nn.functional as F
class ExpertFFN(nn.Module):
"""单个专家:标准的两层 FFN"""
def __init__(self, d_model, d_ffn):
super().__init__()
self.w1 = nn.Linear(d_model, d_ffn, bias=False)
self.w2 = nn.Linear(d_ffn, d_model, bias=False)
def forward(self, x):
return self.w2(F.silu(self.w1(x)))
class MoELayer(nn.Module):
"""MoE 层:N 个专家 + Router,每个 token 激活 Top-K 个专家"""
def __init__(self, d_model, d_ffn, num_experts=8, top_k=2):
super().__init__()
self.num_experts = num_experts
self.top_k = top_k
# N 个独立的专家 FFN
self.experts = nn.ModuleList([
ExpertFFN(d_model, d_ffn) for _ in range(num_experts)
])
# Router:线性层,输出每个专家的得分
self.router = nn.Linear(d_model, num_experts, bias=False)
def forward(self, x):
batch_size, seq_len, d_model = x.shape
x_flat = x.view(-1, d_model) # [batch*seq_len, d_model]
# 路由:计算每个 token 对应各专家的得分
router_logits = self.router(x_flat) # [N_tokens, N_experts]
router_scores = F.softmax(router_logits, dim=-1) # 归一化为概率
# 选 Top-K 个专家
topk_scores, topk_indices = router_scores.topk(self.top_k, dim=-1)
# 对 Top-K 的权重重新归一化(使权重之和为 1)
topk_scores = topk_scores / topk_scores.sum(dim=-1, keepdim=True)
# 对每个 token,加权求和被选中专家的输出
output = torch.zeros_like(x_flat)
for expert_idx in range(self.num_experts):
# 找出哪些 token 选了这个专家
token_mask = (topk_indices == expert_idx).any(dim=-1)
if not token_mask.any():
continue
expert_input = x_flat[token_mask]
expert_output = self.experts[expert_idx](expert_input)
# 取该专家对应的权重
expert_weight = topk_scores[token_mask][
(topk_indices[token_mask] == expert_idx)
].unsqueeze(-1)
output[token_mask] += expert_weight * expert_output
return output.view(batch_size, seq_len, d_model)
# 验证
d_model, d_ffn = 512, 2048
moe = MoELayer(d_model=d_model, d_ffn=d_ffn, num_experts=8, top_k=2)
x = torch.randn(2, 16, d_model) # batch=2, seq_len=16
out = moe(x)
print(out.shape) # torch.Size([2, 16, 512])
六、负载均衡问题:专家塌陷
MoE 有一个严重的训练问题:专家塌陷(Expert Collapse)。
Router 一旦发现某个专家效果好,就会一直选它
→ 这个专家越训越强,其他专家越来越少被选
→ 最终退化成只有 1~2 个专家在工作,其余专家形同虚设
解决方案1:辅助损失(Auxiliary Loss)
在训练损失里加一项,惩罚负载不均衡:
$$\mathcal{L}{\text{aux}} = \alpha \cdot N \cdot \sum{i=1}^{N} f_i \cdot p_i$$
其中 $f_i$ 是专家 $i$ 被选中的频率,$p_i$ 是 Router 给专家 $i$ 的平均概率。鼓励每个专家被均匀使用。
解决方案2:无辅助损失的负载均衡(DeepSeek 方案)
DeepSeek-V3 提出了无辅助损失的方案:给每个专家加一个可学习的偏置项,动态调整各专家被选中的概率,不需要额外的损失函数。
七、DeepSeek-V3 的 MoE 设计
DeepSeek-V3(671B)是目前最具代表性的 MoE 大模型:
总专家数:256 个
每个 token 激活专家数:8 个(Top-8)
共享专家:1 个(每个 token 必选,保证基础能力)
总参数:671B
每次推理激活参数:~37B(约 5.5% 的参数参与计算)
共享专家是 DeepSeek 的创新:除了路由选出的 8 个专家,还有 1 个专家是所有 token 都必须经过的,用来保留通用知识,避免专家过度特化。
每个 token 的计算路径:
共享专家(必选 1 个)+ 路由专家(Top-8)= 共 9 个专家的输出加权求和
八、MoE 的优缺点
优点
| 优点 | 说明 |
|---|---|
| 参数量和计算量解耦 | 671B 参数但只激活 37B,推理速度接近 37B 模型 |
| 专家自然分工 | 不同专家会自发学习不同领域的知识 |
| 扩展性强 | 增加专家数量几乎不增加推理计算量 |
缺点
| 缺点 | 说明 |
|---|---|
| 显存需求高 | 所有专家的参数都要加载进显存,即使大多数不参与计算 |
| 训练不稳定 | 专家塌陷问题需要额外处理 |
| 通信开销大 | 分布式训练时,不同 token 路由到不同设备的专家,需要大量跨设备通信 |
| 推理延迟不均 | 不同 batch 里激活的专家不同,难以做静态优化 |
九、核心要点速查
| 问题 | 答案 |
|---|---|
| MoE 改的是哪一层? | FFN 层,Attention 层不变 |
| 专家是什么? | 独立的 FFN 子网络,结构和普通 FFN 一样 |
| Router 做什么? | 为每个 token 计算各专家得分,选 Top-K |
| 为什么参数多但计算少? | 每次只激活 K 个专家,其余专家跳过 |
| 最大的训练问题是什么? | 专家塌陷,需要负载均衡策略 |
| DeepSeek-V3 的规模? | 671B 总参数,每次激活约 37B(Top-8 + 1 共享专家) |
| 和 Dense 模型的本质区别? | Dense:所有参数都激活;MoE:条件激活,参数量≠计算量 |