File size: 12,585 Bytes
f664f3f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 | # 面试: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 闲置
|