Skip to content

Mixture of Experts (MoE): Scaling to Trillions of Parameters

Overview

Mixture of Experts (MoE) is an architectural pattern where a model contains multiple expert networks and a router network that learns to dispatch different inputs to different experts. Only a subset of experts are active per token, enabling trillion-parameter models to run efficiently.

  • Foundational Paper: "Outrageously Large Neural Networks for Efficient Conditional Computation" (Shazeer et al., 2017)
  • Modern Scale: "Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity" (Lepikhin et al., 2021)
  • Key Insight: Use sparsity to scale parameter count without scaling computation
  • Impact: Can build 1.6T parameter models with less compute than 100B dense models
  • Adoption: Google Switch Transformer, Mistral MoE, Llama 3.1-405B

The Problem: Parameter vs Computation Trade-off

Dense Models

Traditional Scaling:
  - 7B model: 7B multiply-adds per token
  - 70B model: 70B multiply-adds per token
  - 1T model: 1T multiply-adds per token
  - Problem: Computation grows linearly with parameters!

Reality: Training 1T parameter model requires 10x compute
What if we could add parameters without adding compute?

The Insight

Modern observation:
"Not every token needs all parameters"

Question: 
Could we have 1T parameters, but only use 100B per token?

Answer:
Yes! With routing and expert selection.

Architecture:
  - 1000 experts of 1B parameters each (1T total)
  - Clever router learns which 2-4 experts to use per token
  - Only 2-4B parameters active per token
  - Total compute: Similar to 70B dense model
  - Total parameters: 1T (14x more!)

This is Mixture of Experts!

How MoE Works

Architecture

Input: (batch, seq_len, hidden_dim)
          ↓
- ┌─────────────────┐
    - Router Network  │ (simple: hidden_dim → num_experts)
  - ┘
          ↓
    Routing scores: (batch, seq_len, num_experts)
          ↓
    Top-k selection: Keep only top-2 or top-4 experts
          ↓
- ┌──────────┬──────────┬──────────┬──────────┐
          - Expert 1 │ Expert 2 │ Expert 3 │ Expert 4 │ (only active ones)
  - ┴──────────┴──────────┴──────────┘
         ↓           ↓           ↓           ↓
    Route subset of tokens to each expert
         ↓
- ┌─────────────────────────────────┐
    - Combine outputs (weighted sum)   │
  - ┘
         ↓
    Output: (batch, seq_len, hidden_dim)

Simple Example

class SimpleGatedMixtureOfExperts(nn.Module):
    def __init__(self, hidden_dim, num_experts, expert_dim, top_k=2):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k

        # Router: Maps hidden_dim → num_experts (scores)
        self.router = nn.Linear(hidden_dim, num_experts)

        # Experts: Each expert is a small FFN
        self.experts = nn.ModuleList([
            nn.Sequential(
                nn.Linear(hidden_dim, expert_dim),
                nn.ReLU(),
                nn.Linear(expert_dim, hidden_dim)
            )
            for _ in range(num_experts)
        ])

    def forward(self, x):
        # x shape: (batch, seq_len, hidden_dim)
        batch, seq_len, hidden_dim = x.shape

        # Flatten for routing
        x_flat = x.view(-1, hidden_dim)  # (batch*seq_len, hidden_dim)

        # Route
        router_scores = self.router(x_flat)  # (batch*seq_len, num_experts)
        router_probs = softmax(router_scores, dim=-1)

        # Select top-k experts
        top_k_probs, top_k_indices = torch.topk(router_probs, self.top_k, dim=-1)

        # Normalize top-k probabilities
        top_k_probs = top_k_probs / top_k_probs.sum(dim=-1, keepdim=True)

        # Apply experts
        output = torch.zeros_like(x_flat)
        for i in range(self.top_k):
            expert_idx = top_k_indices[:, i]  # Which expert for each token
            expert_prob = top_k_probs[:, i]   # Routing probability

            # Apply each expert to its tokens
            for e in range(self.num_experts):
                mask = (expert_idx == e)
                if mask.sum() > 0:
                    expert_out = self.experts[e](x_flat[mask])
                    output[mask] += expert_prob[mask].unsqueeze(-1) * expert_out

        # Reshape back
        output = output.view(batch, seq_len, hidden_dim)
        return output

# Usage:
moe = SimpleGatedMixtureOfExperts(
    hidden_dim=2048,
    num_experts=128,
    expert_dim=8192,
    top_k=2
)
output = moe(x)  # Only 2 experts active per token!

The Challenge: Load Balancing

The Problem

Naive MoE Issue:
  - Router learns to prefer certain experts
  - Token distribution becomes unbalanced
  - Example: 120/128 tokens go to Expert 1
  - 127 experts are mostly idle
  - Wasted computation!

Why it happens:
  - Gradient update makes router prefer rewarded experts
  - Positive feedback loop
  - Model converges to using only 1-2 experts
  - Defeats the purpose of 128 experts!

Result:
  - Dense model efficiency (compute grows with used experts)
  - Sparse model overhead (unused experts in memory)
  - Worst of both worlds!

Solutions: Load Balancing Losses

1. Auxiliary Loss (Switch Transformer approach)

def compute_load_balancing_loss(router_probs, top_k_indices, num_experts):
    """
    Encourages uniform distribution of tokens across experts

    Intuition:
    - Soft constraint: "Please use experts evenly"
    - Not hard constraint (allows natural specialization)
    - Auxiliary loss added to main loss
    """
    batch_size = router_probs.shape[0]

    # Importance: How much each token uses each expert
    # (batch*seq_len, num_experts)
    importance = router_probs.sum(dim=0)  # Sum across tokens

    # Load: How many tokens are routed to each expert
    load_one_hot = torch.nn.functional.one_hot(
        top_k_indices[:, 0], num_experts
    ).float()  # (batch*seq_len, num_experts)
    load = load_one_hot.sum(dim=0)  # (num_experts,)

    # Auxiliary loss encourages load ≈ importance
    # If load is uniform but importance is biased, loss pushes back
    loss = torch.mean(load * importance) * num_experts / batch_size
    return loss

# In training loop:
output = moe(x)
main_loss = compute_loss(output, target)
aux_loss = compute_load_balancing_loss(router_probs, top_k_indices, 128)
total_loss = main_loss + 0.01 * aux_loss  # Auxiliary loss term
total_loss.backward()

2. Expert Capacity

def apply_expert_capacity(top_k_indices, expert_capacity):
    """
    Hard constraint: Limit tokens per expert

    Intuition:
    - "Each expert can handle max_tokens tokens per batch"
    - Ensures load balancing through capacity constraint
    - Excess tokens overflow to second-choice expert
    """
    batch_size, seq_len = top_k_indices.shape[:2]
    num_tokens = batch_size * seq_len
    max_tokens_per_expert = expert_capacity * num_tokens

    # Track tokens assigned to each expert
    tokens_per_expert = torch.zeros(num_experts)

    for i in range(seq_len * batch_size):
        for k in range(top_k):
            expert = top_k_indices[i, k]
            if tokens_per_expert[expert] < max_tokens_per_expert:
                tokens_per_expert[expert] += 1
                break  # Token assigned to this expert
            # else: Try next expert in top-k

    return tokens_per_expert

# Typical capacity factor:
# capacity_factor = 1.25 means each expert handles 1.25x average
# Prevents concentration, ensures load balancing

MoE in Modern Models

Switch Transformers (2021)

Parameters: 1.6 Trillion
Architecture:
  - 2048 experts
  - Top-1 routing (only 1 expert per token!)
  - Reduced computation vs 1.6T dense
  - Quality comparable to 100-200B dense models

Performance:
  - Training: 4x faster than 1.6T dense
  - Inference: Similar cost to 100B dense
  - Quality: Near 1.6T dense model performance
  - Achievement: Massive parameter scaling with modest compute!

Mistral MoE & Llama MoE

Mistral Mixtral 8x7B:
  - 46.7B total parameters
  - 8 experts of 7B each
  - Top-2 routing
  - Sparse computation: ~13B FLOPs per token (vs 47B dense)
  - 5-10x speedup in inference vs dense 46B
  - Competitive with 70B models!

Llama 3.1 405B:
  - MoE variant in development
  - 16-32 experts
  - Mixture of dense + sparse blocks
  - Efficient scaling to trillion+ parameters

Pros and Cons

Advantages

✅ Massive Parameter Count
   - 1T+ parameters with reasonable compute
   - Each domain/task can have expert specialization

✅ Compute Efficiency
   - Only subset of experts active per token
   - Can use top-2 instead of computing all experts
   - 3-4x computation reduction vs dense

✅ Specialization
   - Different experts specialize in different domains
   - Emergent task-specific routing
   - Better generalization across diverse data

✅ Scaling Laws
   - Better compute-optimal frontier than dense models
   - Reach quality of larger dense models with less compute

Challenges

❌ Training Instability
   - Load imbalance causes training oscillations
   - Collapse to single expert without careful tuning
   - Requires specialized load balancing techniques

❌ Inference Complexity
   - Router overhead (overhead to decide which expert)
   - Expert memory: All experts loaded in memory
   - Not all hardware optimized for sparse operations

❌ Communication Overhead
   - Distributed training: Routing decisions across devices
   - All-reduce for distributed expert placement
   - Reduced efficiency compared to dense distributed training

❌ Generalization Concerns
   - Some evidence of worse generalization (earlier research)
   - Potential for expert collapse in specific domains
   - Hyperparameter sensitivity (top-k, capacity factor, loss weight)

When to Use MoE

Use MoE when:
✅ Parameter count is constraint
   - Want very large models but limited training compute
✅ Inference cost is constraint
   - Can deploy billion-token daily with modest resources
✅ Domain diversity is high
   - Multi-domain tasks benefit from specialization
✅ You have training compute budget
   - MoE training is complex; needs careful engineering

Avoid MoE when:
❌ Model size doesn't matter
❌ Inference latency is critical
   - Routing decision adds latency
❌ Distributed training not feasible
   - MoE requires careful distributed implementation
❌ You want simple, stable models
   - Dense models simpler to train/deploy

Key Takeaways

🌟 MoE enables 10-100x parameter scaling without proportional compute increase
⚖️ Trade-off: Sparse computation vs. training complexity
🎯 Load balancing is critical—improper routing kills efficiency
📊 Top-k routing (2-4 experts) performs better than top-1
🚀 Future of scaling: Dense + MoE hybrid architectures