Mixture of Experts (MoE): Глубокий разбор архитектуры
llmmoearchitecturesparse-modelsefficiency
Введение
Mixture of Experts (MoE) — это архитектура нейронной сети, в которой каждый входной токен обрабатывается не всей моделью, а только несколькими специализированными "экспертами". Это позволяет создавать модели с миллиардами параметров, которые при инференсе используют лишь их малую часть.
Как работает MoE
Базовая структура
Input Token
│
┌────┴────┐
│ Router │
│ (Gating│
└────┬────┘
│
┌──────────┼──────────┐
↓ ↓ ↓
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Expert 1 │ │ Expert 2 │ │ Expert 3 │
│ (FFN-a) │ │ (FFN-b) │ │ (FFN-c) │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
└─────────────┼─────────────┘
│
┌──────┴──────┐
│ Combine │
│ (weighted) │
└──────┬──────┘
│
Output Token
Математика gating
import torch
import torch.nn as nn
import torch.nn.functional as F
class MoELayer(nn.Module):
"""Базовый MoE слой"""
def __init__(self, d_model: int, num_experts: int, top_k: int = 2):
super().__init__()
self.num_experts = num_experts
self.top_k = top_k
self.d_model = d_model
# Router (gating network)
self.gate = nn.Linear(d_model, num_experts)
# Experts (identical architecture, different weights)
self.experts = nn.ModuleList([
nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Linear(d_model * 4, d_model)
)
for _ in range(num_experts)
])
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
x: (batch, seq_len, d_model)
"""
batch, seq_len, d_model = x.shape
# Compute gating weights
logits = self.gate(x) # (batch, seq_len, num_experts)
gates = F.softmax(logits, dim=-1)
# Select top-k experts
gate_values, expert_indices = torch.topk(gates, self.top_k, dim=-1)
gate_values = F.softmax(gate_values, dim=-1) # Renormalize top-k
# Initialize output
output = torch.zeros_like(x)
# Route tokens to experts and compute
for i, expert in enumerate(self.experts):
# Find tokens routed to this expert
mask = expert_indices == i # (batch, seq_len, top_k)
if mask.any():
# Get the gate values for this expert
expert_gates = torch.where(
mask, gate_values,
torch.zeros_like(gate_values)
)
# Compute expert output
expert_output = expert(x) # (batch, seq_len, d_model)
# Weighted sum
output += expert_output * expert_gates.unsqueeze(-1)
return output
Типы MoE
1. Standard MoE (Dense Gating)
class StandardMoE(nn.Module):
"""Стандартный MoE с плотным gating"""
def __init__(self, d_model: int, num_experts: int, top_k: int = 2):
super().__init__()
self.top_k = top_k
self.gate = nn.Linear(d_model, num_experts)
self.experts = nn.ModuleList([
nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Linear(d_model * 4, d_model)
)
for _ in range(num_experts)
])
def forward(self, x):
logits = self.gate(x)
gates = F.softmax(logits, dim=-1)
# Top-k selection
top_gates, top_indices = torch.topk(gates, self.top_k, dim=-1)
top_gates = F.softmax(top_gates, dim=-1)
# One-hot encoding for efficient computation
one_hot = F.one_hot(top_indices, num_classes=self.num_experts)
one_hot = one_hot.permute(0, 2, 1).float()
combined_gates = torch.bmm(one_hot, top_gates.unsqueeze(-1)).squeeze(-1)
expert_outputs = torch.stack(
[expert(x) for expert in self.experts], dim=1
)
output = torch.sum(
expert_outputs * combined_gates.unsqueeze(-1),
dim=1
)
return output
2. Sparse MoE с load balancing
class SparseMoE(nn.Module):
"""Sparse MoE с жёстким ограничением на нагрузку"""
def __init__(self, d_model: int, num_experts: int, top_k: int = 1):
super().__init__()
self.top_k = top_k
self.num_experts = num_experts
self.gate = nn.Linear(d_model, num_experts)
self.aux_loss_fn = AuxiliaryLoadBalancingLoss()
self.experts = nn.ModuleList([
nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Linear(d_model * 4, d_model)
)
for _ in range(num_experts)
])
def forward(self, x):
batch, seq_len, d_model = x.shape
logits = self.gate(x)
gate_probs = F.softmax(logits, dim=-1)
top_k_probs, top_k_indices = torch.topk(gate_probs, self.top_k, dim=-1)
aux_loss = self.aux_loss_fn(gate_probs, top_k_indices)
output = torch.zeros_like(x)
for expert_idx, expert in enumerate(self.experts):
expert_mask = (top_k_indices == expert_idx)
if expert_mask.any():
expert_out = expert(x)
weights = top_k_probs[expert_mask].unsqueeze(-1)
output[expert_mask] += expert_out[expert_mask] * weights[expert_mask]
return output, aux_loss
3. GShard MoE
class GShardMoE(nn.Module):
"""GShard: MoE с capacity factor и balanced loss"""
def __init__(self, d_model: int, num_experts: int, top_k: int = 2,
capacity_factor: float = 1.0):
super().__init__()
self.top_k = top_k
self.num_experts = num_experts
self.capacity_factor = capacity_factor
self.gate = nn.Linear(d_model, num_experts)
self.experts = nn.ModuleList([
nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.GELU(),
nn.Linear(d_model * 4, d_model)
)
for _ in range(num_experts)
])
self.aux_loss_fn = GShardAuxLoss()
def forward(self, x):
batch, seq_len, d_model = x.shape
logits = self.gate(x)
gate_probs = F.softmax(logits, dim=-1)
# Top-k selection
top_k_probs, top_k_indices = torch.topk(gate_probs, self.top_k, dim=-1)
# Capacity: max tokens per expert
capacity = int((batch * seq_len * self.top_k) / self.num_experts * self.capacity_factor)
# Compute auxiliary loss for load balancing
aux_loss = self.aux_loss_fn(gate_probs, top_k_indices)
# Route tokens to experts with capacity constraint
output = torch.zeros_like(x)
importance = gate_probs.gather(-1, top_k_indices).max(dim=-1).values
for expert_idx, expert in enumerate(self.experts):
expert_mask = (top_k_indices == expert_idx)
if expert_mask.any():
# Apply capacity constraint
expert_tokens = expert_mask.sum(dim=-1)
if expert_tokens > capacity:
# Drop least important tokens
pass
expert_out = expert(x)
weights = top_k_probs[expert_mask]
output[expert_mask] += expert_out[expert_mask] * weights.unsqueeze(-1)
return output, aux_loss
Load Balancing
Auxiliary Loss для балансировки
class AuxiliaryLoadBalancingLoss(nn.Module):
"""Auxiliary loss для балансировки нагрузки между экспертами"""
def __init__(self):
super().__init__()
def forward(self, gate_probs: torch.Tensor, top_k_indices: torch.Tensor) -> torch.Tensor:
"""
gate_probs: (batch, seq_len, num_experts) - gating probabilities
top_k_indices: (batch, seq_len, top_k) - selected expert indices
"""
num_experts = gate_probs.shape[-1]
batch, seq_len = gate_probs.shape[:2]
# Fraction of tokens routed to each expert
# Create one-hot encoding
one_hot = F.one_hot(top_k_indices, num_classes=num_experts) # (batch, seq_len, top_k, num_experts)
token_fraction = one_hot.float().mean(dim=(0, 1)) # (num_experts, top_k)
token_fraction = token_fraction.mean(dim=1) # Average over top-k positions
# Fraction of probability mass for each expert
prob_fraction = gate_probs.mean(dim=(0, 1)) # (num_experts,)
# Ideal: uniform distribution
ideal = 1.0 / num_experts
# Load balancing loss
load_loss = torch.mean((prob_fraction - ideal) ** 2)
# Token fraction loss (encourage uniform token distribution)
token_loss = torch.mean((token_fraction - ideal) ** 2)
return load_loss + token_loss
MoE в реальных моделях
Switch Transformer
class SwitchTransformerBlock(nn.Module):
"""Switch Transformer block (MoE с top-1 gating)"""
def __init__(self, d_model: int, d_ff: int, num_experts: int,
num_heads: int, dropout: float = 0.1):
super().__init__()
# Self-attention (dense)
self.self_attn = nn.MultiheadAttention(d_model, num_heads, dropout=dropout)
self.norm1 = nn.LayerNorm(d_model)
# Switch FFN (sparse)
self.switch_ffn = SwitchFFN(d_model, d_ff, num_experts)
self.norm2 = nn.LayerNorm(d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
# Self-attention
attn_output, _ = self.self_attn(x, x, x)
x = x + self.dropout(attn_output)
x = self.norm1(x)
# Switch FFN
switch_output, aux_loss = self.switch_ffn(x)
x = x + self.dropout(switch_output)
x = self.norm2(x)
return x, aux_loss
class SwitchFFN(nn.Module):
"""Switch FFN: один эксперт на токен (top-1)"""
def __init__(self, d_model: int, d_ff: int, num_experts: int):
super().__init__()
self.gate = nn.Linear(d_model, num_experts)
self.experts = nn.ModuleList([
nn.Sequential(
nn.Linear(d_model, d_ff),
nn.GELU(),
nn.Linear(d_ff, d_model)
)
for _ in range(num_experts)
])
def forward(self, x):
logits = self.gate(x) # (batch, seq_len, num_experts)
probs = F.softmax(logits, dim=-1)
# Top-1 selection
indices = probs.argmax(dim=-1) # (batch, seq_len)
output = torch.zeros_like(x)
batch, seq_len, d_model = x.shape
for expert_idx, expert in enumerate(self.experts):
mask = (indices == expert_idx) # (batch, seq_len)
if mask.any():
output[mask] = expert(x[mask])
return output
Mixtral 8x7B
class MixtralMoE(nn.Module):
"""Mixtral 8x7B: MoE с Dense Top-2 gating"""
def __init__(self, d_model: int, num_experts: int = 8, top_k: int = 2):
super().__init__()
self.num_experts = num_experts
self.top_k = top_k
self.gate = nn.Linear(d_model, num_experts, bias=False)
self.experts = nn.ModuleList([
nn.Sequential(
nn.Linear(d_model, d_model * 4),
nn.SiLU(),
nn.Linear(d_model * 4, d_model)
)
for _ in range(num_experts)
])
def forward(self, x):
logits = self.gate(x) # (batch, seq_len, num_experts)
# Gating with softmax
scores = F.softmax(logits, dim=-1)
# Top-2 gating
top_scores, top_indices = torch.topk(scores, self.top_k, dim=-1)
# Normalize top-2 scores
top_scores = F.softmax(top_scores, dim=-1)
# Compute output
output = torch.zeros_like(x)
for expert_idx, expert in enumerate(self.experts):
# Find which tokens use this expert
mask = (top_indices == expert_idx) # (batch, seq_len, top_k)
if mask.any():
# Get the corresponding scores
expert_scores = torch.where(mask, top_scores, torch.zeros_like(top_scores))
# Sum over top-k dimension
expert_weight = expert_scores.sum(dim=-1, keepdim=True) # (batch, seq_len, 1)
# Compute expert output
expert_out = expert(x)
# Add weighted contribution
output += expert_out * expert_weight
return output
Сравнение производительности
| Модель | Параметры | Активные параметры | Experts | Top-K | Speedup |
|---|---|---|---|---|---|
| Dense Llama 3 8B | 8B | 8B | - | - | 1x |
| Switch Transformer 1B | 1B | 1B | 128 | 1 | ~1x |
| Mixtral 8x7B | 467B | 12.9B | 8 | 2 | ~3x |
| Grok-1 | 335B | 34B | 64 | 4 | ~5x |
| Qwen2.5-MoE | 32B | 3.7B | 64 | 2 | ~4x |
Преимущества и недостатки
Преимущества
- Масштабируемость — можно создавать модели с триллионами параметров
- Эффективность инференса — активна лишь малая часть параметров
- Параллелизм — эксперты легко распараллеливаются
- Специализация — эксперты учатся специализироваться на разных задачах
Недостатки
- Сложность обучения — проблемы с балансировкой нагрузки
- Communication overhead — всеэксперты должны быть доступны
- Непредсказуемость — разные токены используют разных экспертов
- Деградация — при плохой балансировке качество падает
Best Practices
- Используйте auxiliary loss для балансировки нагрузки
- Capacity factor > 1.0 — дайте запас для предотвращения drop токенов
- Top-2 gating — лучше Top-1 для стабильности
- Начинайте с малого числа экспертов — 4-8, затем масштабируйте
- Monitor expert utilization — следите за равномерностью загрузки
Заключение
MoE архитектура — это ключевой прорыв для создания больших эффективных моделей. Mixtral 8x7B и Qwen2.5-MoE показывают, что MoE позволяет получить преимущества больших моделей при затратах маленьких. Правильная реализация load balancing — ключ к успешному использованию MoE.