面试:MoE 深度
本仓库
src/core/mlp.py的 MoE 实现,覆盖路由机制、负载均衡、训练技巧
0. MoE 架构图
输入 x
│
▼
┌─────────────┐
│ Router │ softmax gate → top-k 选择
└──────┬──────┘
│
▼
┌──────────────────────────────┐
│ Expert 0 │ Expert 1 │ ... │ 每个 Expert 是独立的 FFN
└──────────────────────────────┘
│
▼
加权求和 → 输出
Q1. MoE 的稀疏指什么?
核心思想
每 token 只过 top-k 个专家(默认 k=1),参数量大但单步计算量小。
本仓库实现(src/core/mlp.py:20-49)
class MOEFeedForward(nn.Module):
def __init__(self, config):
self.num_experts = config.num_experts # 默认 4
self.num_experts_per_tok = config.num_experts_per_tok # 默认 1
# 每个 expert 是独立的 FeedForward
self.experts = nn.ModuleList([
FeedForward(config) for _ in range(self.num_experts)
])
# Router: 将 hidden_size 映射到 num_experts
self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False)
def forward(self, x):
# 1. 计算路由概率
router_logits = self.gate(x) # (batch, seq_len, num_experts)
router_probs = F.softmax(router_logits, dim=-1)
# 2. Top-k 选择
topk_probs, topk_indices = torch.topk(router_probs, self.num_experts_per_tok, dim=-1)
# 3. 加权求和
output = torch.zeros_like(x)
for i, expert in enumerate(self.experts):
mask = (topk_indices == i).any(dim=-1)
if mask.any():
expert_output = expert(x[mask])
output[mask] += topk_probs[mask] * expert_output
return output
面试点:为什么每 token 只过 1 个专家?→ 计算量小,但参数量大,适合大规模模型
Q2. 为什么需要 aux_loss?
问题:Expert 坍缩
如果不加约束,router 可能只选少数专家,导致:
- 大部分专家闲置
- 参数利用率低
- 训练不稳定
解决方案:辅助损失(src/core/mlp.py:44-46)
def aux_loss(router_probs, topk_indices):
# 1. 计算每个 expert 的负载
load = (topk_indices == torch.arange(num_experts).view(1, 1, -1)).float().sum(dim=[0, 1])
# 2. 计算每个 expert 的平均路由概率
scores = router_probs.mean(dim=[0, 1])
# 3. 辅助损失:鼓励负载均衡
aux_loss = (load * scores).sum() * num_experts * coef # coef=5e-4
return aux_loss
为什么用 load * scores?
- load:实际被选中的次数
- scores:平均路由概率
- 乘积:鼓励两者一致,即负载高的 expert 路由概率也高
面试点:如果不用 aux_loss 会怎样?→ Expert 坍缩,大部分专家闲置,参数利用率低
Q3. 死 Expert 梯度保持(src/core/mlp.py:42-43)
问题
在 DDP 分布式训练中,如果某个 expert 完全没被选中,它的参数就不会有梯度,导致 DDP 梯度同步死锁。
解决方案
class MOEFeedForward(nn.Module):
def forward(self, x):
# ... top-k 选择 ...
# 死 expert 梯度保持技巧
y[0, 0] += 0 * sum(p.sum() for p in self.experts.parameters())
return y
为什么用 0 * sum(p)?
- 让未被选中的 expert 仍然出现在计算图中
- 保持 DDP 通信闭环
- 实际值为 0,不影响 loss
面试点:为什么不用
.requires_grad = True?→ 那只是让参数可训练,但不会出现在计算图中,0 * sum(p)才能保证计算图连通
Q4. 本仓库 MoE 怎么切换?
Block 中的切换(src/core/block.py:14)
class Block(nn.Module):
def __init__(self, config):
# 根据 config.use_moe 切换 FeedForward / MOEFeedForward
self.mlp = FeedForward(config) if not config.use_moe else MOEFeedForward(config)
aux_loss 累加(src/core/mlp.py:44-46)
def forward(self, x):
# ... 前向传播 ...
# aux_loss 在 MLP.forward 内累加
if self.training and self.config.use_moe:
aux_loss = aux_loss(router_probs, topk_indices)
else:
aux_loss = 0
return output, aux_loss
主模型汇总(src/models/lm/model.py:31-33)
class LM(nn.Module):
def forward(self, x, ...):
aux_loss = 0
for layer in self.layers:
x, layer_aux_loss = layer(x, ...)
aux_loss += layer_aux_loss
return x, aux_loss
面试点:aux_loss 为什么在 MLP.forward 内累加?→ 每层独立计算,避免跨层依赖
Q5. MoE vs Dense 的参数量对比
参数量计算
假设:
- hidden_size = 768
- intermediate_size = 3840
- num_experts = 4
Dense FFN:
- 参数量 = 2 × 768 × 3840 = 5.9M
MoE FFN:
- 参数量 = 4 × 2 × 768 × 3840 = 23.6M
- 每 token 计算量 = 2 × 768 × 3840 = 5.9M(只过 1 个 expert)
对比
| Dense | MoE | |
|---|---|---|
| 参数量 | 5.9M | 23.6M |
| 每 token 计算量 | 5.9M | 5.9M |
| 显存占用 | 低 | 高 |
| 表达能力 | 低 | 高 |
面试点:MoE 为什么适合大规模模型?→ 参数量大但计算量小,可以提升模型容量而不增加计算成本
Q6. Router 的设计选择
本仓库的 Router(src/core/mlp.py:31-32)
class MOEFeedForward(nn.Module):
def __init__(self, config):
self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False)
def forward(self, x):
router_logits = self.gate(x) # 线性投影
router_probs = F.softmax(router_logits, dim=-1) # softmax 归一化
为什么用线性投影?
- 简单高效:只增加 hidden_size × num_experts 参数
- 可训练:通过反向传播学习路由策略
- 无 bias:减少参数量,避免过拟合
为什么用 softmax?
- 概率分布:确保所有 expert 的权重和为 1
- 可微分:支持反向传播
- top-k 选择:方便选择概率最高的 expert
面试点:如果用其他归一化方法会怎样?→ softmax 是最常用的选择,其他方法(如 L1 归一化)可能训练不稳定
Q7. MoE 的训练稳定性
问题
MoE 训练比 Dense 更难,因为:
- Router 容易坍缩
- Expert 负载不均衡
- 梯度不稳定
本仓库的解决方案
- 辅助损失:鼓励负载均衡
- 死 expert 梯度保持:保持 DDP 通信闭环
- Expert 初始化:用 Dense FFN 的权重初始化
Expert 初始化(src/core/mlp.py:20-25)
class MOEFeedForward(nn.Module):
def __init__(self, config):
# 每个 expert 用相同的初始化
self.experts = nn.ModuleList([
FeedForward(config) for _ in range(self.num_experts)
])
面试点:为什么用相同的初始化?→ 避免初始路由偏向某个 expert,让训练更稳定
Q8. MoE 的推理优化
问题
推理时,每个 token 只过 1 个 expert,但需要加载所有 expert 的参数到显存。
解决方案
- Expert 并行:不同 expert 放在不同 GPU 上
- Expert 卸载:将不常用的 expert 卸载到 CPU
- Expert 量化:降低 expert 的精度
本仓库的推理实现(src/core/mlp.py:35-45)
def forward(self, x):
# 推理时只过 1 个 expert
if not self.training:
topk_probs, topk_indices = torch.topk(router_probs, 1, dim=-1)
output = torch.zeros_like(x)
for i, expert in enumerate(self.experts):
mask = (topk_indices == i).any(dim=-1)
if mask.any():
output[mask] = expert(x[mask])
return output
面试点:为什么推理时不需要 aux_loss?→ 推理时不需要反向传播,aux_loss 只在训练时使用
Q9. MoE 的显存占用
参数量
假设:
- hidden_size = 768
- intermediate_size = 3840
- num_experts = 4
- 精度 = fp16(2 bytes)
Dense FFN:
- 显存 = 5.9M × 2 bytes = 11.8MB
MoE FFN:
- 显存 = 23.6M × 2 bytes = 47.2MB
KV Cache
MoE 不影响 KV Cache,因为 KV Cache 只与注意力层相关。
面试点:MoE 为什么显存占用高?→ 参数量大,需要加载所有 expert 的参数
Q10. MoE 的适用场景
适合 MoE 的场景
- 大规模模型:参数量大但计算量小
- 多任务学习:不同 expert 可以学习不同任务
- 稀疏激活:每 token 只过部分 expert
不适合 MoE 的场景
- 小规模模型:参数量增加但计算量不变,性价比低
- 密集计算:每 token 需要所有参数
- 低延迟推理:需要加载所有 expert,延迟高
面试点:什么时候应该用 MoE?→ 模型规模大(>10B),且需要提升容量而不增加计算成本
Q11. MoE vs 其他高效方法
对比
| MoE | LoRA | 蒸馏 | |
|---|---|---|---|
| 原理 | 稀疏激活 | 低秩分解 | 知识迁移 |
| 参数量 | 大 | 小 | 小 |
| 计算量 | 小 | 小 | 小 |
| 适用场景 | 大规模模型 | 微调 | 压缩模型 |
本仓库的组合使用
- MoE:提升模型容量
- LoRA:微调时减少参数量
- 蒸馏:压缩模型大小
面试点:MoE 和 LoRA 可以一起用吗?→ 可以,MoE 提升容量,LoRA 减少微调参数,两者互补
Q12. MoE 的负载均衡指标
指标定义
- Expert 负载:每个 expert 被选中的比例
- 负载方差:expert 负载的方差,越小越均衡
- 最大负载:被选中次数最多的 expert
本仓库的辅助损失(src/core/mlp.py:44-46)
def aux_loss(router_probs, topk_indices):
load = (topk_indices == torch.arange(num_experts).view(1, 1, -1)).float().sum(dim=[0, 1])
scores = router_probs.mean(dim=[0, 1])
aux_loss = (load * scores).sum() * num_experts * coef
return aux_loss
面试点:如何衡量负载均衡?→ 用辅助损失的值,越小越均衡
Q13. MoE 的 Router 训练技巧
问题
Router 容易过拟合,导致:
- 路由策略不稳定
- Expert 负载不均衡
解决方案
- Dropout:在 router 输出上加 dropout
- Label Smoothing:对 router logits 做 label smoothing
- Warmup:训练初期逐渐增加 router 的学习率
本仓库的实现(src/core/mlp.py:31-32)
class MOEFeedForward(nn.Module):
def __init__(self, config):
self.gate = nn.Linear(config.hidden_size, config.num_experts, bias=False)
def forward(self, x):
router_logits = self.gate(x)
router_probs = F.softmax(router_logits, dim=-1)
# 没有使用额外的技巧,依赖辅助损失
面试点:Router 为什么容易过拟合?→ Router 参数少,容易记住训练数据的路由模式
Q14. MoE 的 Expert 选择策略
Top-k 选择
topk_probs, topk_indices = torch.topk(router_probs, self.num_experts_per_tok, dim=-1)
为什么用 Top-k?
- 简单高效:只选择概率最高的 k 个 expert
- 可微分:支持反向传播
- 可控稀疏度:通过 k 控制稀疏程度
其他选择策略
- Random:随机选择 expert
- Threshold:选择概率超过阈值的 expert
- Gumbel-Softmax:用 Gumbel-Softmax 采样
面试点:为什么不用 Random?→ Random 不可微分,无法反向传播
Q15. MoE 的 Expert 初始化
问题
Expert 初始化不当会导致:
- 路由偏向某个 expert
- 训练不稳定
本仓库的解决方案(src/core/mlp.py:20-25)
class MOEFeedForward(nn.Module):
def __init__(self, config):
# 每个 expert 用相同的初始化
self.experts = nn.ModuleList([
FeedForward(config) for _ in range(self.num_experts)
])
为什么用相同的初始化?
- 避免初始路由偏向某个 expert
- 让训练更稳定
- 所有 expert 从同一起点开始学习
面试点:如果用不同的初始化会怎样?→ 路由可能偏向某个 expert,导致其他 expert 闲置