Auxiliary Balancing Loss › Implementation
Language AI HandbookAll chapters Auxiliary Balancing Loss › ImplementationOne section · Sections in this chapter8 / 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 the auxiliary balancing loss step by step. We'll create a module that can be integrated into any MoE training loop.
import torch
import torch.nn.functional as F
def compute_load_balancing_loss(
router_probs: torch.Tensor, expert_indices: torch.Tensor, num_experts: int
) -> torch.Tensor:
"""
Compute the auxiliary load balancing loss.
Args:
router_probs: Gate probabilities, shape (batch_size, seq_len, num_experts)
expert_indices: Selected expert indices, shape (batch_size, seq_len)
num_experts: Number of expert networks
Returns:
Scalar auxiliary loss
"""
# Flatten batch and sequence dimensions
batch_size, seq_len, _ = router_probs.shape
num_tokens = batch_size * seq_len
router_probs_flat = router_probs.view(-1, num_experts) # (N, E)
expert_indices_flat = expert_indices.view(-1) # (N,)
# Compute fraction of tokens routed to each expert (f_i)
# One-hot encode the expert selections and average
expert_mask = F.one_hot(expert_indices_flat, num_experts).float() # (N, E)
tokens_per_expert = expert_mask.sum(dim=0) # (E,)
f = tokens_per_expert / num_tokens # (E,)
# Compute fraction of router probability per expert (P_i)
P = router_probs_flat.mean(dim=0) # (E,)
# Load balancing loss: scaled dot product
load_balancing_loss = num_experts * (f * P).sum()
return load_balancing_lossThis function computes the exact formulation from the Switch Transformer paper. Let's verify it works correctly with a simple example.
import torch
import torch.nn.functional as F
# Create example data: 4 tokens, 3 experts
batch_size, seq_len, num_experts = 2, 2, 3
num_tokens = batch_size * seq_len
# Simulate router probabilities (softmax output)
torch.manual_seed(42)
router_logits = torch.randn(batch_size, seq_len, num_experts)
router_probs = F.softmax(router_logits, dim=-1)
# Select top expert for each token
expert_indices = router_probs.argmax(dim=-1)
# Compute loss and breakdown for inspection
loss = compute_load_balancing_loss(router_probs, expert_indices, num_experts)
router_probs_flat = router_probs.view(-1, num_experts)
expert_indices_flat = expert_indices.view(-1)
expert_mask = F.one_hot(expert_indices_flat, num_experts).float()
f = expert_mask.sum(dim=0) / num_tokens
P = router_probs_flat.mean(dim=0)
f_P_product = f * PRouter probabilities:
tensor([[0.3683, 0.2992, 0.3325],
[0.5215, 0.1348, 0.3438],
[0.8114, 0.0471, 0.1415],
[0.2484, 0.3246, 0.4271]])
Selected experts: [0, 0, 0, 2]
Load balancing loss: 1.3300
Fraction of tokens per expert (f): ['0.7500', '0.0000', '0.2500']
Average probability per expert (P): ['0.4874', '0.2014', '0.3112']
f * P products: ['0.3655', '0.0000', '0.0778']The loss is above 1.0 because expert 2 receives most tokens (high ) and also has high average probability (high ). With perfect balance, we'd expect and loss .
Handling Top-K Routing
When using top-2 or top-k routing, each token contributes to multiple experts' load. We need to adjust the computation to account for the fact that each token sends computation to experts rather than just one. The normalization factor changes accordingly, since the total number of expert assignments is rather than just .
import torch
def compute_load_balancing_loss_topk(
router_probs: torch.Tensor, expert_indices: torch.Tensor, num_experts: int
) -> torch.Tensor:
"""
Compute auxiliary loss for top-k routing.
Args:
router_probs: Gate probabilities, shape (batch_size, seq_len, num_experts)
expert_indices: Selected expert indices, shape (batch_size, seq_len, k)
num_experts: Number of expert networks
Returns:
Scalar auxiliary loss
"""
batch_size, seq_len, k = expert_indices.shape
num_tokens = batch_size * seq_len
router_probs_flat = router_probs.view(-1, num_experts) # (N, E)
expert_indices_flat = expert_indices.view(-1, k) # (N, k)
# Count how many times each expert is selected across all tokens
# Each token contributes k selections total
tokens_per_expert = torch.zeros(num_experts, device=router_probs.device)
for i in range(k):
expert_counts = torch.bincount(
expert_indices_flat[:, i], minlength=num_experts
).float()
tokens_per_expert += expert_counts
# Normalize by total selections (N * k)
f = tokens_per_expert / (num_tokens * k)
# Average router probability per expert
P = router_probs_flat.mean(dim=0)
return num_experts * (f * P).sum()# Example with top-2 routing
k = 2
_, topk_indices = router_probs.topk(k, dim=-1)
loss_topk = compute_load_balancing_loss_topk(
router_probs, topk_indices, num_experts
)Top-2 expert indices:
tensor([[[0, 2],
[0, 2]],
[[0, 2],
[2, 1]]])
Top-k load balancing loss: 1.0907The top-k loss is typically lower than top-1 because selecting two experts per token naturally spreads load more evenly.
Integrating with Training
The following training loop incorporates the auxiliary loss:
import torch
import torch.nn as nn
import torch.nn.functional as F
class MoELayer(nn.Module):
"""Simplified MoE layer with auxiliary loss tracking."""
def __init__(self, hidden_size: int, num_experts: int, expert_size: int):
super().__init__()
self.num_experts = num_experts
# Router network
self.router = nn.Linear(hidden_size, num_experts)
# Expert networks (simplified as linear layers)
self.experts = nn.ModuleList(
[
nn.Sequential(
nn.Linear(hidden_size, expert_size),
nn.GELU(),
nn.Linear(expert_size, hidden_size),
)
for _ in range(num_experts)
]
)
# Store auxiliary loss
self.aux_loss = None
def forward(self, x: torch.Tensor) -> torch.Tensor:
batch_size, seq_len, hidden_size = x.shape
# Compute router probabilities
router_logits = self.router(x) # (B, S, E)
router_probs = F.softmax(router_logits, dim=-1)
# Select top-1 expert
expert_indices = router_probs.argmax(dim=-1) # (B, S)
# Compute auxiliary loss
self.aux_loss = compute_load_balancing_loss(
router_probs, expert_indices, self.num_experts
)
# Route tokens to experts (simplified dense computation for clarity)
output = torch.zeros_like(x)
x_flat = x.view(-1, hidden_size)
output_flat = output.view(-1, hidden_size)
indices_flat = expert_indices.view(-1)
for i in range(self.num_experts):
mask = indices_flat == i
if mask.any():
output_flat[mask] = self.experts[i](x_flat[mask])
return output.view(batch_size, seq_len, hidden_size)import torch
import torch.nn.functional as F
def train_step(model, x, y, optimizer, aux_loss_coef=0.01):
"""Single training step with auxiliary loss."""
optimizer.zero_grad()
# Forward pass
output = model(x)
# Task loss (e.g., cross-entropy for language modeling)
task_loss = F.mse_loss(output, y) # Simplified example
# Collect auxiliary losses from all MoE layers
total_aux_loss = 0
for module in model.modules():
if isinstance(module, MoELayer) and module.aux_loss is not None:
total_aux_loss += module.aux_loss
# Combined loss
total_loss = task_loss + aux_loss_coef * total_aux_loss
# Backward and update
total_loss.backward()
optimizer.step()
return {
"task_loss": task_loss.item(),
"aux_loss": total_aux_loss.item()
if isinstance(total_aux_loss, torch.Tensor)
else total_aux_loss,
"total_loss": total_loss.item(),
}import torch
# Create model and run training step
hidden_size, num_experts, expert_size = 64, 4, 128
model = MoELayer(hidden_size, num_experts, expert_size)
# Random input and target
x = torch.randn(2, 8, hidden_size)
y = torch.randn(2, 8, hidden_size)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
metrics = train_step(model, x, y, optimizer, aux_loss_coef=0.01)Training metrics: task_loss: 1.0804 aux_loss: 1.0224 total_loss: 1.0906
The total loss is the sum of task loss and weighted auxiliary loss. The auxiliary loss is initially high (>1.0). This reflects the random initialization of the router.
Monitoring Expert Utilization
Tracking expert utilization during training helps diagnose load balancing issues. The key metrics to watch are the auxiliary loss value and the actual distribution of tokens across experts. The auxiliary loss is a differentiable proxy for this distribution, but the actual distribution is what matters for inference efficiency and model capacity utilization.
import torch
def compute_expert_utilization(
router_probs: torch.Tensor, expert_indices: torch.Tensor, num_experts: int
) -> dict:
"""Compute expert utilization statistics."""
num_tokens = expert_indices.numel()
expert_indices_flat = expert_indices.view(-1)
# Count tokens per expert
counts = torch.bincount(expert_indices_flat, minlength=num_experts).float()
fractions = counts / num_tokens
# Compute statistics
mean_fraction = 1.0 / num_experts # Expected under perfect balance
max_fraction = fractions.max().item()
min_fraction = fractions.min().item()
# Imbalance ratio: max/min (1.0 = perfect balance)
imbalance_ratio = max_fraction / max(min_fraction, 1e-8)
return {
"tokens_per_expert": counts.tolist(),
"fraction_per_expert": fractions.tolist(),
"max_fraction": max_fraction,
"min_fraction": min_fraction,
"imbalance_ratio": imbalance_ratio,
}import torch
import torch.nn.functional as F
# Generate more data to see utilization patterns
torch.manual_seed(123)
batch_size, seq_len, num_experts = 8, 32, 4
router_logits = torch.randn(batch_size, seq_len, num_experts)
router_probs = F.softmax(router_logits, dim=-1)
expert_indices = router_probs.argmax(dim=-1)
utilization = compute_expert_utilization(
router_probs, expert_indices, num_experts
)Expert utilization statistics: Tokens per expert: [77.0, 65.0, 62.0, 52.0] Fraction per expert: ['0.301', '0.254', '0.242', '0.203'] Max/min fractions: 0.301 / 0.203 Imbalance ratio: 1.48x
With random initialization, some imbalance naturally occurs. The auxiliary loss should push this ratio closer to 1.0 during training.
Implementation Notes
Several practical issues arise when integrating auxiliary losses into real training pipelines. First, when a model has multiple MoE layers, each layer computes its own auxiliary loss, and the total auxiliary contribution should sum across all layers. The pattern shown in the train_step function above handles this correctly by iterating over all modules and accumulating. In a transformer with MoE layers at every other block, a 24-layer model might have 12 MoE layers, each contributing an auxiliary loss. The coefficient therefore applies to the sum of all these contributions, which means the effective balancing pressure scales with the number of MoE layers. Some implementations divide by the number of MoE layers to normalize this effect.
Second, the auxiliary loss should not be included in validation metrics. When you compute validation perplexity to assess model quality, you should report only the task loss, not the total loss including balancing. This is because the auxiliary loss measures a training artifact (routing distribution), not the quality of the model's predictions. Including it would obscure real performance trends during model development.
Third, gradient clipping can interact unexpectedly with auxiliary losses. If you clip gradients by global norm, the auxiliary loss gradients are included in the norm computation. A large auxiliary loss during early training can dominate the gradient norm, causing excessive clipping of task loss gradients. Some practitioners compute gradient norms separately for the task and auxiliary components, or apply the auxiliary loss only after a warmup period when the model has begun to stabilize.