# 面试: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`) ```python 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`) ```python 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 梯度同步死锁。 ### 解决方案 ```python 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`) ```python 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`) ```python 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`) ```python 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`) ```python 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 归一化 ``` ### 为什么用线性投影? 1. **简单高效**:只增加 hidden_size × num_experts 参数 2. **可训练**:通过反向传播学习路由策略 3. **无 bias**:减少参数量,避免过拟合 ### 为什么用 softmax? 1. **概率分布**:确保所有 expert 的权重和为 1 2. **可微分**:支持反向传播 3. **top-k 选择**:方便选择概率最高的 expert > 面试点:如果用其他归一化方法会怎样?→ softmax 是最常用的选择,其他方法(如 L1 归一化)可能训练不稳定 --- ## Q7. MoE 的训练稳定性 ### 问题 MoE 训练比 Dense 更难,因为: - Router 容易坍缩 - Expert 负载不均衡 - 梯度不稳定 ### 本仓库的解决方案 1. **辅助损失**:鼓励负载均衡 2. **死 expert 梯度保持**:保持 DDP 通信闭环 3. **Expert 初始化**:用 Dense FFN 的权重初始化 ### Expert 初始化(`src/core/mlp.py:20-25`) ```python 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 的参数到显存。 ### 解决方案 1. **Expert 并行**:不同 expert 放在不同 GPU 上 2. **Expert 卸载**:将不常用的 expert 卸载到 CPU 3. **Expert 量化**:降低 expert 的精度 ### 本仓库的推理实现(`src/core/mlp.py:35-45`) ```python 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 的场景 1. **大规模模型**:参数量大但计算量小 2. **多任务学习**:不同 expert 可以学习不同任务 3. **稀疏激活**:每 token 只过部分 expert ### 不适合 MoE 的场景 1. **小规模模型**:参数量增加但计算量不变,性价比低 2. **密集计算**:每 token 需要所有参数 3. **低延迟推理**:需要加载所有 expert,延迟高 > 面试点:什么时候应该用 MoE?→ 模型规模大(>10B),且需要提升容量而不增加计算成本 --- ## Q11. MoE vs 其他高效方法 ### 对比 | | MoE | LoRA | 蒸馏 | |---|---|---|---| | 原理 | 稀疏激活 | 低秩分解 | 知识迁移 | | 参数量 | 大 | 小 | 小 | | 计算量 | 小 | 小 | 小 | | 适用场景 | 大规模模型 | 微调 | 压缩模型 | ### 本仓库的组合使用 - **MoE**:提升模型容量 - **LoRA**:微调时减少参数量 - **蒸馏**:压缩模型大小 > 面试点:MoE 和 LoRA 可以一起用吗?→ 可以,MoE 提升容量,LoRA 减少微调参数,两者互补 --- ## Q12. MoE 的负载均衡指标 ### 指标定义 1. **Expert 负载**:每个 expert 被选中的比例 2. **负载方差**:expert 负载的方差,越小越均衡 3. **最大负载**:被选中次数最多的 expert ### 本仓库的辅助损失(`src/core/mlp.py:44-46`) ```python 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 负载不均衡 ### 解决方案 1. **Dropout**:在 router 输出上加 dropout 2. **Label Smoothing**:对 router logits 做 label smoothing 3. **Warmup**:训练初期逐渐增加 router 的学习率 ### 本仓库的实现(`src/core/mlp.py:31-32`) ```python 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 选择 ```python topk_probs, topk_indices = torch.topk(router_probs, self.num_experts_per_tok, dim=-1) ``` ### 为什么用 Top-k? 1. **简单高效**:只选择概率最高的 k 个 expert 2. **可微分**:支持反向传播 3. **可控稀疏度**:通过 k 控制稀疏程度 ### 其他选择策略 1. **Random**:随机选择 expert 2. **Threshold**:选择概率超过阈值的 expert 3. **Gumbel-Softmax**:用 Gumbel-Softmax 采样 > 面试点:为什么不用 Random?→ Random 不可微分,无法反向传播 --- ## Q15. MoE 的 Expert 初始化 ### 问题 Expert 初始化不当会导致: - 路由偏向某个 expert - 训练不稳定 ### 本仓库的解决方案(`src/core/mlp.py:20-25`) ```python 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 闲置