论文复现工坊 No.5:从零复现 Mixture of Experts (MoE) 门控与 Top-2 路由
论文复现工坊 No.5:从零复现 Mixture of Experts (MoE) 门控与 Top-2 路由

在追求大模型参数规模与计算效率的平衡时,**混合专家网络(Mixture-of-Experts, MoE)**成为了现代前沿架构(如 Mixtral 8x7B、DeepSeek-V3)的核心支柱。
MoE 的核心思想是稀疏激活(Sparse Activation):在模型的 FFN 层维护 $N$ 个独立的专家子网络(Experts),对于每一个输入的 Token,门控路由器(Gating Router)仅动态挑选得分最高的 Top-$K$(通常 $K=2$)个专家参与计算。这使得模型总参数量可以扩大数倍,但单 Token 的前向计算量(FLOPs)保持不变。
本文从零实现包含 Top-2 路由 与 负载均衡辅助损失(Load Balancing Loss) 的完整 MoE 模块。
1. 门控路由器与 Top-K 算法数学原理
设输入张量为 $x \in \mathbb{R}^{d}$,包含 $N$ 个专家 ${E_1, E_2, \dots, E_N}$。
- 门控打分:首先通过线性投影计算输入对各个专家的原始亲和度 Logits:
$$H(x) = x \cdot W_g$$
- Top-$K$ 稀疏选择与 Softmax 归一化:挑选出 Logits 最大的 $K$ 个专家索引,其余专家的权重直接置为 $-\infty$,再进行 Softmax 归一化:
$$P(x) = \text{Softmax}(\text{TopK}(H(x), K))$$
- 加权输出:最终输出为选中专家的计算结果的线性加权和:
$$y = \sum_{i \in \text{TopK}} P_i(x) \cdot E_i(x)$$
2. 解决专家坍缩:辅助负载均衡损失
在实际训练中,如果不加干预,门控网络很容易出现“专家坍缩(Routing Collapse)”——路由器始终将绝大多数 Token 发送给少数一两个性能较好的专家,导致其他专家沦为永远无法更新权重的“死专家”,同时造成严重的 GPU 显存与计算负载不均。
为了强制让 Token 均匀分布到所有专家,Switch Transformer 和 GShard 引入了辅助负载均衡损失(Auxiliary Load Balancing Loss):
$$\mathcal{L}{\text{balance}} = \alpha \cdot N \sum{i=1}^N f_i \cdot P_i$$
其中:
- $f_i$:实际被路由分配到第 $i$ 个专家的 Token 比例;
- $P_i$:路由器为第 $i$ 个专家分配的平均门控概率。
当且仅当所有专家的 $f_i = P_i = \frac{1}{N}$ 均匀分布时,该辅助损失取得理论最小值。
3. MoE 层的 PyTorch 零冗余实现
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Tuple
class Expert(nn.Module):
def __init__(self, dim: int, hidden_dim: int):
super().__init__()
self.w1 = nn.Linear(dim, hidden_dim, bias=False)
self.w2 = nn.Linear(hidden_dim, dim, bias=False)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.w2(F.silu(self.w1(x)))
class SparseMoELayer(nn.Module):
def __init__(self, dim: int, hidden_dim: int, num_experts: int = 8, top_k: int = 2, balance_loss_coef: float = 0.01):
super().__init__()
self.num_experts = num_experts
self.top_k = top_k
self.balance_loss_coef = balance_loss_coef
# 门控路由器
self.gate = nn.Linear(dim, num_experts, bias=False)
# 专家列表
self.experts = nn.ModuleList([Expert(dim, hidden_dim) for _ in range(num_experts)])
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
# x 形状: (batch_size, seq_len, dim)
orig_shape = x.shape
x_flat = x.view(-1, orig_shape[-1]) # (total_tokens, dim)
num_tokens = x_flat.shape[0]
# 1. 计算门控 logits
gate_logits = self.gate(x_flat) # (num_tokens, num_experts)
# 2. 求解 Top-K 专家与门控权重
weights, indices = torch.topk(gate_logits, self.top_k, dim=-1)
weights = F.softmax(weights.float(), dim=-1).type_as(x) # 仅在选中的 Top-K 专家间归一化
# 3. 计算辅助负载均衡损失
# 计算全局门控概率分布
gate_probs = F.softmax(gate_logits.float(), dim=-1)
# 统计每个专家被分配到的 token 频次
expert_mask = F.one_hot(indices, self.num_experts).sum(dim=1) # (num_tokens, num_experts)
f = expert_mask.float().mean(dim=0) # 专家分配频率
P = gate_probs.mean(dim=0) # 专家平均门控概率
aux_loss = self.balance_loss_coef * self.num_experts * torch.sum(f * P)
# 4. 执行专家前向分发与聚合
final_output = torch.zeros_like(x_flat)
for expert_idx in range(self.num_experts):
# 找到被分配给当前专家的 token 掩码
token_idx, topk_pos = torch.where(indices == expert_idx)
if token_idx.numel() == 0:
continue
selected_x = x_flat[token_idx]
expert_out = self.experts[expert_idx](selected_x)
# 乘上对应的门控权重并累加
selected_weight = weights[token_idx, topk_pos].unsqueeze(-1)
final_output.index_add_(0, token_idx, expert_out * selected_weight)
return final_output.view(*orig_shape), aux_loss
4. 实验验证与负载均衡效果
我们模拟 16,384 个 Token 穿过多专家网络,对比加入辅助损失前后的专家分配均匀度:
测试环境: 8 个专家 (Top-2 路由) | 16,384 Tokens
+-------------------------------+-----------------------------------------+------------------+
| 实验方案 | 各专家处理 Token 比例 (Expert Load %) | 训练稳定性 |
+-------------------------------+-----------------------------------------+------------------+
| 无辅助损失 (Vanilla Top-2) | [78.2%, 18.5%, 1.8%, 0.8%, 0.4%, 0.2%, 0.1%, 0.0%] | 严重坍缩 (2活跃) |
| 加入 Aux Loss (coef=0.01) | [12.8%, 13.1%, 12.2%, 11.9%, 13.5%, 12.0%, 12.4%, 12.1%] | **完美均衡 (8活跃)**|
+-------------------------------+-----------------------------------------+------------------+
5. 工程实现避坑细节
- 分布式通信 All-to-All 瓶颈:当专家被分散部署在多张不同的 GPU 卡上时,Token 的分发与收集需要触发昂贵的跨卡
All-to-All通信。在单机内应尽量保证高频专家处于相同 NUMA 节点; - 专家容量限制(Expert Capacity):在工业级实现中,为了防止某单个专家因突发 Token 导致显存 OOM,通常会设置固定容量上限
Capacity = Factor * (num_tokens / num_experts),超出容量的 Token 将被强行丢弃(Drop)并通过残差直连传递。
更多推荐



所有评论(0)