第 4 讲:Mixture of Experts (MoE)
1. MoE 的动机
Scaling Laws 告诉我们:更多参数 = 更好的性能。 但传统 dense model 的问题是:参数量增加意味着每个 token 的计算量也等比增加。 训练一个 1T 参数的 dense model 需要天文数字的计算资源。
Mixture of Experts (MoE) 提出了一个巧妙的解决方案: 增加参数量,但不等比增加每个 token 的计算量。 关键思想是:对于每个输入 token,只激活模型的一个子集(一部分 expert), 而不是使用全部参数。
核心权衡
MoE 模型的总参数量可能是 dense model 的 8-16 倍,但每个 token 的计算量(active parameters)只增加少量。例如 Mixtral 8x7B 有约 47B 总参数,但每个 token 只使用约 13B active parameters,计算成本接近一个 13B 的 dense model。
| 指标 | Dense 7B | MoE 8×7B(如 Mixtral) |
|---|---|---|
| 总参数量 | 7B | ~47B |
| Active parameters per token | 7B | ~13B |
| FLOPs per token | ~1x | ~2x |
| 显存需求 | ~1x | ~6-7x |
| 性能 | 基准 | 接近 dense 30B+ |
2. Expert 结构
在 MoE Transformer 中,只有 FFN 层被替换为 MoE 层, Attention 层保持不变(所有 token 共享)。每个 MoE 层包含多个 expert, 每个 expert 本质上就是一个独立的 FFN:
class Expert(nn.Module):
"""Each expert is simply a standard FFN."""
def __init__(self, d_model: int, d_ff: int):
super().__init__()
self.w1 = nn.Linear(d_model, d_ff, bias=False)
self.w_gate = nn.Linear(d_model, d_ff, bias=False)
self.w2 = nn.Linear(d_ff, d_model, bias=False)
def forward(self, x):
return self.w2(F.silu(self.w_gate(x)) * self.w1(x))
这些 expert 的结构完全相同,但参数不共享。 每个 expert 在训练过程中会自然地"专门化",学习处理不同类型的输入。
3. Router / Gating 机制
MoE 的核心问题是:对于每个输入 token,如何决定将它路由到哪些 expert? 这由 Router(也叫 Gating Network)决定。
3.1 Top-K Routing
最常用的路由策略是 Top-K Routing: Router 为每个 token 计算一个分数向量(每个 expert 一个分数), 然后选择分数最高的 K 个 expert。
class MoELayer(nn.Module):
def __init__(self, d_model, d_ff, num_experts, top_k):
super().__init__()
self.num_experts = num_experts
self.top_k = top_k
# Router: a simple linear layer
self.router = nn.Linear(d_model, num_experts, bias=False)
# Create expert pool
self.experts = nn.ModuleList([
Expert(d_model, d_ff) for _ in range(num_experts)
])
def forward(self, x):
"""
x: (B, T, d_model)
"""
B, T, d = x.shape
# Step 1: Compute router logits
router_logits = self.router(x) # (B, T, num_experts)
# Step 2: Top-K selection
top_k_logits, top_k_indices = router_logits.topk(
self.top_k, dim=-1
) # each (B, T, top_k)
# Step 3: Softmax over selected experts only
top_k_weights = F.softmax(top_k_logits, dim=-1) # (B, T, top_k)
# Step 4: Compute weighted sum of expert outputs
output = torch.zeros_like(x)
for i in range(self.top_k):
expert_idx = top_k_indices[:, :, i] # (B, T)
weight = top_k_weights[:, :, i:i+1] # (B, T, 1)
for e in range(self.num_experts):
mask = (expert_idx == e)
if mask.any():
expert_input = x[mask]
expert_output = self.experts[e](expert_input)
output[mask] += weight[mask] * expert_output
return output
Softmax Gating 的直觉
Router 的 softmax 输出可以理解为"置信度"——模型对每个 expert 的信任程度。Top-K 选择确保每个 token 只使用 K 个 expert,保持计算量可控。K 的典型值为 1 或 2。Mixtral 使用 top_k=2,即每个 token 选择 2 个 expert。
3.2 Router 的数学形式
完整的 routing 过程可以用以下公式描述:
y = Σi∈TopK gi(x) · Experti(x)
其中 TopK 操作将未被选中的 expert 的分数设为 -∞(在 softmax 之前), 从而只在选中的 expert 之间分配权重。
4. Load Balancing 问题
MoE 训练中最核心的挑战是 Load Balancing(负载均衡)。 如果不加干预,训练过程中很容易出现专家坍缩(expert collapse): 所有 token 都被路由到少数几个 expert,而其他 expert 完全不被使用。
4.1 为什么会发生专家坍缩?
这是一个正反馈循环:
- 某个 expert 偶然在初期表现略好
- 更多 token 被路由到这个 expert
- 这个 expert 获得更多梯度更新,进一步提升
- 其他 expert 因为缺少训练信号而停滞
- 最终只有少数 expert 活跃——失去了 MoE 的意义
4.2 Auxiliary Loss(辅助损失)
解决 load balancing 的标准方法是添加一个辅助损失函数, 惩罚不均衡的路由:
其中:
- fi:被路由到 expert i 的 token 比例(fraction of tokens)
- Pi:router 对 expert i 的平均概率
- α:辅助损失的权重系数(通常 0.01 左右)
def load_balance_loss(router_logits, top_k_indices, num_experts, alpha=0.01):
"""
router_logits: (B*T, num_experts) - raw router outputs
top_k_indices: (B*T, top_k) - selected expert indices
"""
num_tokens = router_logits.shape[0]
# f_i: fraction of tokens assigned to each expert
expert_counts = torch.zeros(num_experts, device=router_logits.device)
for k in range(top_k_indices.shape[1]):
expert_counts.scatter_add_(
0, top_k_indices[:, k],
torch.ones(num_tokens, device=router_logits.device)
)
f = expert_counts / (num_tokens * top_k_indices.shape[1])
# P_i: average router probability for each expert
router_probs = F.softmax(router_logits, dim=-1)
P = router_probs.mean(dim=0)
# Auxiliary loss: minimize when f and P are uniform
loss = alpha * num_experts * (f * P).sum()
return loss
直觉理解 Auxiliary Loss
当所有 expert 被均匀使用时(fi = 1/N, Pi = 1/N),辅助损失取最小值。这个损失会"推动"router 更均匀地分配 token,同时不会过强地干预 router 的自然学习。α 的选择很关键——太大会损害模型质量,太小则无法防止坍缩。
5. Token Dropping 策略
在分布式训练中,各 expert 分布在不同的 GPU 上。如果某个 expert 收到过多的 token, 会导致该 GPU 成为瓶颈(其他 GPU 必须等待它完成)。 Token Dropping 是一种强制负载均衡的策略:
- 为每个 expert 设定一个容量上限(buffer capacity):C = (N × top_k / E) × capacity_factor
- 超出容量的 token 被丢弃(dropped),直接使用 residual 连接的输入
capacity_factor通常设为 1.0-1.5
# Token dropping example
capacity = int(num_tokens * top_k / num_experts * capacity_factor)
for expert_idx in range(num_experts):
assigned_tokens = get_tokens_for_expert(expert_idx)
if len(assigned_tokens) > capacity:
# Drop excess tokens (lowest router score first)
scores = get_router_scores(assigned_tokens, expert_idx)
keep_indices = scores.topk(capacity).indices
dropped_indices = set(range(len(assigned_tokens))) - set(keep_indices.tolist())
# Dropped tokens use residual (skip this expert)
Token Dropping 的问题
Token dropping 在训练和推理之间引入了不一致性:训练时可能丢弃 token,但推理时通常不丢弃。这种 train-test mismatch 可能影响性能。一些新方法(如 Sinkhorn routing)试图在不丢弃 token 的情况下实现负载均衡。
6. 代表模型
6.1 Switch Transformer (Google, 2021)
- 使用 Top-1 routing:每个 token 只选一个 expert,最大化稀疏性
- 引入了 capacity factor 和 token dropping
- 最大规模达到 1.6T 参数
- 证明了 MoE 在 T5 架构上的有效性
6.2 GShard (Google, 2020)
- 使用 Top-2 routing
- 只替换 Transformer 中隔一层的 FFN 为 MoE(交替 dense 和 MoE)
- 在机器翻译任务上达到了 600B 参数
- 展示了 MoE 在多语言场景下的优势
6.3 Mixtral 8x7B (Mistral, 2024)
- 8 个 expert,Top-2 routing
- 总参数量约 47B,每个 token 使用 ~13B active parameters
- 在大多数 benchmark 上匹配或超过 LLaMA 2 70B
- 基于 Mistral 7B 架构,每层 FFN 被替换为 8 个 expert
- 开源模型,推动了 MoE 的广泛应用
| 模型 | 总参数 | Active 参数 | Expert 数 | Top-K |
|---|---|---|---|---|
| Switch-Base | 3.8B | 0.2B | 128 | 1 |
| GShard | 600B | — | 2048 | 2 |
| Mixtral 8x7B | 47B | 13B | 8 | 2 |
| DeepSeek-V2 | 236B | 21B | 160 | 6 |
7. MoE 训练的挑战
7.1 训练不稳定性
MoE 模型比同规模的 dense model 更容易出现训练不稳定(loss spikes, divergence)。 主要原因包括:
- Router 的离散选择:Top-K 选择引入了不可微分的操作,梯度只能通过被选中的 expert 传播
- Expert 间的竞争:router weights 的微小变化可能导致 token 路由的剧烈变化
- Load imbalance:即使有 auxiliary loss,某些 batch 仍可能出现极端不均衡
常用的缓解策略:
- 使用更小的 learning rate(比 dense model 小 2-10 倍)
- Router 使用 float32 精度(即使模型其他部分使用 BF16)
- 添加 router z-loss:
L_z = (1/N) * Σ log²(Σ exp(router_logits)),防止 router logits 过大 - Jitter noise:在训练时给 router logits 添加小噪声
7.2 通信开销
在分布式训练中,不同 expert 通常分布在不同 GPU 上(Expert Parallelism)。 每个 MoE 层需要两次 All-to-All 通信:
- Dispatch:将 token 发送到各自被分配的 expert 所在的 GPU
- Combine:将 expert 输出发回原始 GPU
All-to-All 的通信量与 batch_size × d_model 成正比。在大规模训练中, 通信开销可能占到总时间的 20-40%。优化通信(如通信计算重叠)是 MoE 系统的核心工程挑战。
# Simplified MoE distributed training flow
# (using Expert Parallelism across 8 GPUs, 8 experts)
# Forward pass:
# 1. Each GPU processes its local tokens through shared layers (Attention)
# 2. Router decides which expert each token goes to
# 3. All-to-All: dispatch tokens to expert GPUs
# GPU 0 sends tokens for Expert 3 to GPU 3, etc.
# 4. Each GPU processes tokens through its local expert(s)
# 5. All-to-All: combine results back to original GPUs
# 6. Continue with next layer
7.3 显存开销
虽然 MoE 的计算量接近 dense model,但显存需求远大于 dense model。 因为所有 expert 的参数都需要存储在内存中(即使每次只有一部分被使用)。 对于 Mixtral 8x7B,需要存储 47B 参数 + 对应的优化器状态, 这在单个 GPU 上是不可能的——必须使用模型并行策略。
8. 本讲总结
- MoE 的核心思想:增加参数量而不等比增加计算量,通过稀疏激活实现
- 每个 MoE 层包含多个独立的 FFN expert 和一个 Router
- Top-K routing + Softmax gating 是标准的路由策略
- Load Balancing 是 MoE 训练的核心挑战,通过 auxiliary loss 和 token dropping 缓解
- 代表模型:Switch Transformer (top-1), Mixtral (top-2, 8 experts)
- MoE 训练面临不稳定性、通信开销、显存压力三大挑战
- MoE 的计算效率优势使其成为构建超大规模模型的主流方案