Load Balancing › Measuring Load Balance in Practice

Michael BrenndoerferUpdated January 4, 202612 min read
Language AI HandbookAll chapters
Part 33 of 67Mixture of Experts
Chapter 243Load Balancing › Measuring Load Balance in Practice
Reading section 6 of 13 · Measuring Load Balance in Practice · 12 minSection 6 of 13 · 12 min

Book contents

Section 6 of 13 · 12 min read

Load Balancing › Measuring Load Balance in PracticeOne section · Sections in this chapter6 / 13 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's implement functions to compute load balance metrics and visualize expert utilization across training. The goal is to build the diagnostic toolkit you'll use when training and debugging your own MoE models. We'll start with the foundational measurements and then build up to simulations that show how imbalance evolves during training.

In[4]:
Code
import numpy as np
import torch

# Set random seed for reproducibility
torch.manual_seed(42)
np.random.seed(42)

We'll start by creating a simple gating network and generating routing decisions for a batch of tokens.

In[5]:
Code
import torch
import torch.nn as nn
import torch.nn.functional as F


class SimpleGatingNetwork(nn.Module):
    """A basic gating network for routing tokens to experts."""

    def __init__(self, input_dim, num_experts):
        super().__init__()
        self.gate = nn.Linear(input_dim, num_experts)
        self.num_experts = num_experts

    def forward(self, x):
        # x shape: (batch_size, seq_len, input_dim)
        logits = self.gate(x)  # (batch_size, seq_len, num_experts)
        probs = F.softmax(logits, dim=-1)
        return probs


# Create gating network
input_dim = 256
num_experts = 8
gating_net = SimpleGatingNetwork(input_dim, num_experts)

# Introduce realistic initialization bias so some experts are initially preferred
# (simulates unequal random init that seeds rich-get-richer dynamics)
with torch.no_grad():
    gating_net.gate.bias.data += torch.tensor(
        [1.8, -0.6, 1.2, -1.0, 0.9, -1.3, 0.4, -0.8]
    )

# Generate sample input
batch_size = 16
seq_len = 128
hidden_states = torch.randn(batch_size, seq_len, input_dim)

Now let's compute routing probabilities and expert assignments.

In[6]:
Code
# Get routing probabilities
with torch.no_grad():
    routing_probs = gating_net(hidden_states)  # (batch, seq, experts)

# Flatten to token-level view
flat_probs = routing_probs.view(-1, num_experts)  # (batch*seq, experts)

# Get top-1 expert assignments
expert_assignments = torch.argmax(flat_probs, dim=-1)  # (batch*seq,)
num_tokens = expert_assignments.shape[0]
Out[7]:
Console
Total tokens: 2048
Number of experts: 8
Routing probabilities shape: torch.Size([2048, 8])

The output confirms we have 2,048 tokens in total (16 sequences ×\times 128 length). The routing probability tensor shape matches our expectation, containing a probability distribution over the 8 experts for every token in the batch.

Computing Token Fractions

Let's compute the fraction of tokens routed to each expert. The function below implements the token fraction formula directly: for each expert, count the tokens assigned to it and divide by the total.

In[8]:
Code
def compute_token_fractions(expert_assignments, num_experts):
    """Compute the fraction of tokens assigned to each expert."""
    counts = torch.zeros(num_experts)
    for i in range(num_experts):
        counts[i] = (expert_assignments == i).sum()
    fractions = counts / len(expert_assignments)
    return fractions


token_fractions = compute_token_fractions(expert_assignments, num_experts)
Out[9]:
Console
Token fractions per expert:
  Expert 0: 0.697 ███████████████████████████
  Expert 1: 0.000 
  Expert 2: 0.189 ███████
  Expert 3: 0.000 
  Expert 4: 0.099 ███
  Expert 5: 0.000 
  Expert 6: 0.014 
  Expert 7: 0.000
Out[10]:
Visualization
Bar chart showing uneven token fractions across 8 experts under random initialization, with a dashed horizontal line marking the uniform baseline.
Token distribution across 8 experts with randomly initialized gating weights and initial biases added to simulate realistic early-training imbalance. Even before any gradient updates, E0 and E2 capture more than the uniform share, while the other experts remain below the uniform baseline (green dashed line).

Even with random initialization and a biased setup to simulate early-training conditions, we can already see substantial imbalance emerging. E0 and E2 capture significantly more than their fair share in this deterministic sample, while the other experts remain underutilized. This is precisely the kind of initial asymmetry that, left unchecked, will amplify into collapse. Let's compute our load balance metrics to quantify this exactly.

In[11]:
Code
def compute_load_metrics(token_fractions):
    """Compute load balance metrics."""
    num_experts = len(token_fractions)

    # Load imbalance factor
    lif = num_experts * token_fractions.max().item()

    # Coefficient of variation
    mean_frac = 1.0 / num_experts
    std_frac = token_fractions.std().item()
    cv = std_frac / mean_frac

    # Entropy (normalized)
    probs = token_fractions + 1e-10  # Avoid log(0)
    entropy = -(probs * torch.log(probs)).sum().item()
    max_entropy = np.log(num_experts)
    normalized_entropy = entropy / max_entropy

    return {
        "load_imbalance_factor": lif,
        "coefficient_of_variation": cv,
        "normalized_entropy": normalized_entropy,
        "max_token_fraction": token_fractions.max().item(),
        "min_token_fraction": token_fractions.min().item(),
    }


metrics = compute_load_metrics(token_fractions)
Out[12]:
Console
Load Balance Metrics:
  Load Imbalance Factor: 5.578 (ideal: 1.0)
  Coefficient of Variation: 1.929 (ideal: 0.0)
  Normalized Entropy: 0.414 (ideal: 1.0)
  Max/Min Token Fraction: 0.697 / 0.000

The load imbalance factor above 1.0 indicates that the most popular expert receives more than its fair share of tokens. The normalized entropy below 1.0 shows that the distribution is not uniform. These numbers represent a model that has not even started training, and the biases we introduced are small compared to what develops during actual training runs. This gives you a sense of how aggressive the rich-get-richer dynamics can be.

Simulating Training Dynamics

To see how imbalance develops during training, let's simulate the rich-get-richer dynamics by iteratively biasing the gating network toward popular experts. This simulation compresses the feedback loop that plays out over real training runs into a controlled demonstration.

In[13]:
Code
def simulate_training_imbalance(
    num_experts, num_steps, reinforcement_strength=0.1
):
    """Simulate how expert preferences evolve during training without balancing."""
    # Start with uniform expert scores
    expert_scores = torch.zeros(num_experts)

    history = {"step": [], "fractions": [], "metrics": []}

    for step in range(num_steps):
        # Convert scores to probabilities
        probs = F.softmax(expert_scores, dim=0)

        # Sample token assignments based on probabilities
        num_tokens = 1000
        assignments = torch.multinomial(probs, num_tokens, replacement=True)

        # Count tokens per expert
        fractions = torch.zeros(num_experts)
        for i in range(num_experts):
            fractions[i] = (assignments == i).sum().float() / num_tokens

        # Record history
        history["step"].append(step)
        history["fractions"].append(fractions.clone())
        history["metrics"].append(compute_load_metrics(fractions))

        # Simulate rich-get-richer: boost scores of popular experts
        expert_scores += reinforcement_strength * (fractions - 1 / num_experts)

    return history


# Run simulation
history = simulate_training_imbalance(
    num_experts=8, num_steps=100, reinforcement_strength=0.4
)
Out[14]:
Visualization
Line plot showing 8 expert token fractions diverging from the uniform baseline over 100 training steps.
Token fractions for 8 experts over 100 training steps. After an initially noisy period near the uniform baseline (dashed), the lines separate as rich-get-richer dynamics take hold and a dominant expert accumulates progressively more tokens.
Line plot showing the Load Imbalance Factor trending upward with sampling noise over 100 training steps.
Load Imbalance Factor over 100 training steps. Despite batch-sampling noise, the metric trends upward from near 1.0 as routing becomes increasingly skewed.

The simulation demonstrates how quickly expert utilization diverges from uniform. After only 100 steps, the load imbalance factor has risen substantially, with some experts receiving several times their fair share of tokens while others receive almost none. In a real training run, this feedback loop operates over hundreds of thousands of gradient steps, giving it ample time to reach pathological extremes.

Visualizing Expert Collapse

Let's run a longer simulation with stronger reinforcement to observe expert collapse. We increase both the number of steps and the reinforcement strength to show what happens when no balancing mechanism intervenes.

In[15]:
Code
# Simulate collapse with stronger dynamics
collapse_history = simulate_training_imbalance(
    num_experts=8, num_steps=200, reinforcement_strength=0.3
)
Out[16]:
Visualization
Stacked area chart showing token distribution collapsing to one dominant expert over 200 steps.
Expert collapse dynamics visualized via stacked area chart. Over 200 training steps, the token distribution (colored areas) concentrates entirely onto one dominant expert, while the remaining seven experts receive zero tokens. This irreversible endpoint represents wasted capacity: the model pays for 8 experts while benefiting from 1.

The stacked area chart shows expert collapse in action. By the end of training, nearly all tokens are routed to just one or two experts. The other experts, despite consuming memory and having trainable parameters, contribute nothing to model outputs. Notice how the collapse happens in stages: initially, some experts grow while others shrink, then the dominant experts gradually squeeze out all competitors until the final collapsed state is reached.

Analyzing Router Entropy

Router entropy provides another view into load balance. Let's examine how the softmax output distribution changes as collapse progresses. Entropy is particularly valuable because it begins declining earlier than other metrics. This provides a leading indicator of collapse before it becomes obvious in the token fraction plots.

In[17]:
Code
def compute_router_entropy(expert_scores):
    """Compute entropy of the router's softmax output."""
    probs = F.softmax(expert_scores, dim=0)
    entropy = -(probs * torch.log(probs + 1e-10)).sum()
    return entropy.item()


def simulate_with_entropy(num_experts, num_steps, reinforcement_strength=0.2):
    """Track router entropy during simulated training."""
    expert_scores = torch.zeros(num_experts)
    entropies = []

    max_entropy = np.log(num_experts)

    for step in range(num_steps):
        # Record entropy
        entropy = compute_router_entropy(expert_scores)
        entropies.append(entropy / max_entropy)  # Normalized

        # Sample and reinforce
        probs = F.softmax(expert_scores, dim=0)
        assignments = torch.multinomial(probs, 1000, replacement=True)
        fractions = torch.zeros(num_experts)
        for i in range(num_experts):
            fractions[i] = (assignments == i).sum().float() / 1000
        expert_scores += reinforcement_strength * (fractions - 1 / num_experts)

    return entropies


entropy_history = simulate_with_entropy(
    num_experts=8, num_steps=150, reinforcement_strength=0.5
)
Out[18]:
Visualization
Line plot showing normalized entropy declining from 1.0 toward 0 over 150 training steps.
Normalized router entropy over 150 training steps. The metric declines from maximum entropy (uniform distribution, green dashed line) toward zero, giving an early warning signal of collapse before it becomes irreversible. The shaded area below the curve shows the cumulative loss of routing diversity.

The entropy plot clearly shows the progression toward collapse. Starting at maximum entropy (uniform routing), the entropy stays near its ceiling during the early noisy phase and then falls rapidly as the router concentrates probability mass on fewer experts. This metric provides an early warning signal: entropy begins dropping before token fractions show obvious skew. By monitoring entropy during training, you can detect the onset of imbalance and intervene before the situation becomes irreversible.

Out[19]:
Visualization
Line plot showing the Load Imbalance Factor rising over training steps as expert collapse progresses.
Load Imbalance Factor during expert collapse. The metric rises above 1.0 as some experts handle far more tokens than the uniform baseline, showing worsening imbalance.
Line plot showing the Coefficient of Variation increasing over training steps during expert collapse.
Coefficient of Variation during expert collapse. CV increases from near zero as token distribution spreads unevenly across experts, which reflects growing routing disparity.
Line plot showing Normalized Entropy declining from 1.0 toward 0 over training steps as routing concentrates.
Normalized Entropy during expert collapse. Entropy declines from maximum (uniform routing) toward zero as the routing distribution becomes increasingly concentrated on a few experts.

Looking at all three metrics together, we can see how they provide complementary information. The Load Imbalance Factor rises sharply once the dominant expert clearly separates from the pack, which makes it good for detecting moderate to severe imbalance. The Coefficient of Variation is sensitive to diffuse imbalance where many experts deviate from their ideal load, even if no single expert is dramatically dominant. Normalized Entropy provides the earliest warning signal, declining smoothly from the very first steps as the distribution begins to concentrate.