All posts

Mixture-of-Experts: Expert Parallelism, All-to-All Routing, and Auxiliary-Loss-Free Load Balancing

7 min read mixture-of-expertsdistributed-systemsload-balancingtransformersinference

Mixture-of-Experts: Expert Parallelism, All-to-All Routing, and Auxiliary-Loss-Free Load Balancing

Mixture-of-Experts (MoE) architectures achieve the representational capacity of dense models at a fraction of the compute cost. Instead of activating all parameters for every token, MoE layers route each token to a small subset of "expert" feed-forward networks. Mixtral 8x7B activates 2 of 8 experts per token, giving it 47B total parameters but only ~13B active per forward pass. DeepSeek-V3 pushes this to 256 experts with 8 active, reaching 671B total parameters with ~37B active.

The elegance is deceptive. Behind it lies a distributed systems problem: routing tokens across GPU boundaries in microseconds, keeping expert utilization balanced, and doing so without auxiliary losses that degrade model quality.

The Routing Problem

A standard MoE layer replaces the feed-forward network (FFN) in a transformer block:

# Simplified top-k routing
def moe_layer(x, num_experts=8, top_k=2):
    # Router: linear projection to expert scores
    router_logits = router_linear(x)  # [batch * seq_len, num_experts]
    scores = softmax(router_logits, dim=-1)
    
    # Select top-k experts per token
    top_k_scores, top_k_indices = scores.topk(top_k, dim=-1)
    top_k_scores = top_k_scores / top_k_scores.sum(dim=-1, keepdim=True)
    
    # Dispatch tokens to experts, combine outputs
    output = torch.zeros_like(x)
    for i, expert in enumerate(experts):
        mask = (top_k_indices == i).any(dim=-1)
        if mask.any():
            expert_out = expert(x[mask])
            # Weight by routing score
            output[mask] += expert_out * top_k_scores[mask, ...]
    return output

This loop-over-experts formulation is clean but hides the distributed reality. When experts live on different GPUs, the routing decision triggers all-to-all communication: tokens must physically move to the GPU hosting their assigned expert, get processed, then return.

Expert Parallelism and All-to-All Communication

With 256 experts distributed across 64 GPUs (4 experts per GPU), the forward pass of a single MoE layer requires:

  1. Router computation (local): Each GPU computes routing scores for its local tokens
  2. All-to-all dispatch: Tokens are sent to their assigned expert's GPU
  3. Expert computation (local): Each GPU runs its experts on received tokens
  4. All-to-all combine: Results are sent back to the originating GPU

The all-to-all is the bottleneck. For a batch of 4096 tokens at hidden dimension 7168 (DeepSeek-V3's config), each dispatch moves ~4096 * 7168 * 2 bytes = ~56MB across the network. With NVLink bandwidth of ~900 GB/s between GPUs in a node, this takes ~62 microseconds intra-node. Cross-node over InfiniBand at 400 Gbps, latency jumps to hundreds of microseconds.

The standard optimization is capacity factor: each expert has a fixed buffer size (tokens * capacity_factor / num_experts). Tokens that overflow an expert's buffer are dropped. GShard introduced this with capacity_factor=2.0, meaning each expert can handle at most 2x its fair share. This enables static memory allocation and efficient batched computation but introduces token dropping as a failure mode.

# Capacity-bounded expert dispatch (GShard style)
expert_capacity = int(num_tokens * capacity_factor / num_experts)
for expert_id in range(num_experts):
    assigned = tokens_for_expert[expert_id]
    if len(assigned) > expert_capacity:
        # Drop overflow tokens (they get zero expert output)
        assigned = assigned[:expert_capacity]
    expert_buffers[expert_id] = pad_to_capacity(assigned)

The Auxiliary Loss Problem

Without intervention, routers collapse: they learn to send all tokens to the same few experts, leaving others idle. The standard fix is an auxiliary load-balancing loss:

L_aux = α * num_experts * Σᵢ (fᵢ * pᵢ)

Where fᵢ is the fraction of tokens routed to expert i, and pᵢ is the mean routing probability for expert i. This loss penalizes uneven distributions. Switch Transformer, Mixtral, and most MoE models use variants with α between 0.01 and 0.1.

The problem: auxiliary loss directly conflicts with model quality. It forces the router to spread tokens more uniformly than is optimal for the task. Some experts should legitimately receive more tokens (e.g., a "code expert" seeing a code-heavy batch). The auxiliary loss coefficient becomes a hyperparameter that trades model quality against utilization, and tuning it is notoriously sensitive.

DeepSeek-V3's Auxiliary-Loss-Free Routing

DeepSeek-V3 (December 2024) introduced a routing mechanism that achieves balanced utilization without any auxiliary loss term. The key insight: instead of penalizing imbalance in the loss function, add a bias term to expert scores and dynamically adjust it based on observed load.

Each expert maintains a bias bᵢ added to its routing score:

# Auxiliary-loss-free routing
def route_with_bias(x, expert_biases):
    router_logits = router_linear(x)
    # Add per-expert bias for routing decision only
    biased_logits = router_logits + expert_biases  
    top_k_indices = biased_logits.topk(top_k, dim=-1).indices
    
    # IMPORTANT: use original (unbiased) scores for weighting
    original_scores = softmax(router_logits, dim=-1)
    top_k_scores = original_scores.gather(-1, top_k_indices)
    return top_k_indices, top_k_scores

# Bias update (after each batch)
def update_biases(expert_biases, expert_loads, target_load, gamma=0.001):
    for i in range(num_experts):
        if expert_loads[i] > target_load:
            expert_biases[i] -= gamma
        else:
            expert_biases[i] += gamma

The genius is separating the routing decision from the output weighting. Biases influence which expert receives the token but not how much that expert's output contributes. This means:

  • No gradient interference: The main training loss backpropagates through the original (unbiased) scores, so model quality is never directly penalized for balance
  • Dynamic adaptation: Biases increase for underloaded experts and decrease for overloaded ones, naturally balancing load
  • No hyperparameter sensitivity: The bias update rate γ is far less sensitive than α in auxiliary loss, since biases converge to whatever values achieve balance

Empirically, DeepSeek-V3 reports that auxiliary-loss-free routing improves benchmark scores by 0.3-0.8% compared to the same architecture with standard auxiliary loss, while maintaining expert utilization within 5% of uniform.

Shared Expert and Fine-Grained Routing

DeepSeek-V3 adds another innovation: shared experts. Of the 257 total experts, 1 is "shared" (always activated for every token) and 256 are "routed" (8 selected per token). The shared expert captures common patterns that all tokens need, reducing pressure on the router to replicate basic capabilities across many experts.

Additionally, DeepSeek-V3 groups experts into segments for its routing topology. With 256 experts across 64 nodes, expert placement is segment-aware: tokens prefer experts on the same node first (intra-node all-to-all is 10x faster), with cross-node routing only when the best local expert scores significantly below a remote one.

# Segment-aware routing (simplified)
def segment_route(x, local_experts, remote_experts, threshold=0.1):
    all_scores = compute_scores(x, local_experts + remote_experts)
    local_scores = all_scores[:len(local_experts)]
    remote_scores = all_scores[len(local_experts):]
    
    # Prefer local unless remote is significantly better
    best_local = local_scores.max()
    best_remote = remote_scores.max()
    
    if best_remote > best_local + threshold:
        # Worth paying cross-node latency
        return select_from(remote_experts, remote_scores)
    return select_from(local_experts, local_scores)

Implications for Inference

At inference time, MoE routing creates irregular memory access patterns. Unlike dense models where every GPU processes the same computation path, MoE inference has data-dependent routing that makes batching complex:

  • Token imbalance across experts: Some experts get many tokens in a batch, others get few, leading to GPU utilization skew
  • KV cache interaction: Tokens must return to their original position after expert processing for correct attention computation
  • Speculative decoding incompatibility: Draft models must predict routing decisions, not just next tokens

Systems like MegaBlocks (2023) address this with block-sparse operations that avoid padding: instead of fixed-capacity buffers, they use sparse matrix multiplication to process variable numbers of tokens per expert efficiently on GPU.

When to Choose MoE

MoE architectures are most compelling when:

  • Training compute budget is fixed but you want more parameters (MoE scales parameters at constant FLOPs)
  • Inference throughput matters more than latency (batched serving amortizes all-to-all overhead)
  • Domain heterogeneity is high (experts can specialize on different data distributions)

They are less suitable when single-request latency is critical (all-to-all adds 100-500μs per layer), memory is constrained (all parameters must be loaded even if sparsely activated), or when the deployment target is a single GPU where expert parallelism provides no benefit.

The trajectory is clear: GPT-4, Gemini 1.5, Mixtral, DeepSeek-V3, and Arctic all use MoE. The systems challenges of efficient routing, balanced utilization, and communication overlap define the frontier of large-scale model serving infrastructure.