Gating Networks › Code Implementation
Language AI HandbookAll chapters Gating Networks › Code ImplementationOne section · Sections in this chapter7 / 11 ↓
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 a gating network from scratch to see these concepts in action.
import numpy as np
import torch
# Set random seed for reproducibility
torch.manual_seed(42)
np.random.seed(42)Basic Router Module
We start with the core router implementation. The router is a linear layer that projects token representations to expert scores.
import torch
import torch.nn as nn
import torch.nn.functional as F
class Router(nn.Module):
"""
Gating network for Mixture of Experts.
Routes tokens to top-k experts based on learned routing scores.
"""
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
# Single linear layer: projects from d_model to num_experts
# No bias term, common in production MoE implementations
self.gate = nn.Linear(d_model, num_experts, bias=False)
def forward(self, x: torch.Tensor):
"""
Compute routing weights for input tokens.
Args:
x: Input tensor of shape (batch_size, seq_len, d_model)
Returns:
routing_weights: Weights for selected experts (batch, seq, top_k)
selected_experts: Indices of selected experts (batch, seq, top_k)
router_probs: Full probability distribution (batch, seq, num_experts)
"""
# Compute routing logits
logits = self.gate(x) # (batch, seq, num_experts)
# Convert to probabilities via softmax
router_probs = F.softmax(logits, dim=-1)
# Select top-k experts
routing_weights, selected_experts = torch.topk(
router_probs, self.top_k, dim=-1
)
# Renormalize weights to sum to 1
routing_weights = routing_weights / routing_weights.sum(
dim=-1, keepdim=True
)
return routing_weights, selected_experts, router_probsLet's verify the router produces expected outputs:
# Create a router with 8 experts, selecting top-2
d_model = 256
num_experts = 8
router = Router(d_model, num_experts, top_k=2)
# Simulate a batch of token representations
batch_size = 2
seq_len = 4
x = torch.randn(batch_size, seq_len, d_model)
# Get routing decisions
routing_weights, selected_experts, router_probs = router(x)Input shape: torch.Size([2, 4, 256]) Routing weights shape: torch.Size([2, 4, 2]) Selected experts shape: torch.Size([2, 4, 2]) First token routing probabilities (all experts): [0.184 0.073 0.083 0.066 0.336 0.049 0.031 0.178] First token selected experts: [4, 0] First token weights for selected experts: [0.646 0.354] Weights sum: 1.000
The router produces probability distributions over all experts, then selects the top-2 and renormalizes their weights.
Routing Score Analysis
To understand router behavior, let's examine how routing probabilities distribute across experts for different inputs.
def analyze_routing_distribution(router, num_samples=1000, d_model=256):
"""
Analyze how the router distributes tokens across experts.
"""
# Generate random token representations
x = torch.randn(num_samples, 1, d_model)
with torch.no_grad():
_, selected_experts, router_probs = router(x)
# Count how often each expert is selected
expert_counts = torch.zeros(router.num_experts)
for i in range(router.num_experts):
expert_counts[i] = (selected_experts == i).sum().item()
# Compute mean probability assigned to each expert
mean_probs = router_probs.squeeze(1).mean(dim=0)
return expert_counts, mean_probs# Analyze an untrained router
counts, mean_probs = analyze_routing_distribution(router)Expert selection counts (out of 2000 selections): Expert 0: 265 ██████████████████████████████████████ Expert 1: 251 ████████████████████████████████████ Expert 2: 261 █████████████████████████████████████ Expert 3: 215 ██████████████████████████████ Expert 4: 241 ██████████████████████████████████ Expert 5: 227 ████████████████████████████████ Expert 6: 278 ████████████████████████████████████████ Expert 7: 262 █████████████████████████████████████ Mean routing probabilities: Expert 0: 0.126 ██████████ Expert 1: 0.126 ██████████ Expert 2: 0.128 ██████████ Expert 3: 0.120 █████████ Expert 4: 0.122 █████████ Expert 5: 0.122 █████████ Expert 6: 0.129 ██████████ Expert 7: 0.126 ██████████
With random initialization, the router distributes fairly evenly across experts. This is expected before training shapes the routing behavior.


Visualizing Routing Entropy
Routing entropy measures how "decisive" the router is. Low entropy means the router strongly prefers specific experts; high entropy means uncertainty across many experts.
def compute_routing_entropy(router_probs):
"""
Compute entropy of routing distributions.
Higher entropy = more uniform, lower = more concentrated.
"""
# Avoid log(0)
probs = router_probs.clamp(min=1e-10)
entropy = -torch.sum(probs * torch.log(probs), dim=-1)
return entropy
# Compute entropy for our sample
_, _, router_probs = router(x)
entropies = compute_routing_entropy(router_probs)# Maximum possible entropy (uniform distribution over 8 experts)
max_entropy = np.log(num_experts)Maximum entropy (uniform over 8 experts): 2.079 Mean observed entropy: 1.904 Entropy ratio (observed/max): 91.56%
An untrained router produces near-maximum entropy, showing uniform routing. Training should reduce this as the router learns to make more decisive choices.
Simulating Trained Router Behavior
Let's simulate what happens when a router learns domain-specific patterns. We'll create synthetic "domain" embeddings and train the router to recognize them.
This is a supervised toy classification setup: we generate synthetic "domains" as Gaussian clusters and directly train the router to predict the domain label. If the clusters are well separated (large prototype_scale) and noise is small (noise_std), a linear gate can drive logits to large magnitudes, making softmax saturate and the loss appear to drop to ~0 in just a few optimization steps.
To keep the curve informative (and connect to the stability discussion above), the demo below uses overlapping domains and adds a small router z-loss term (z_loss_coef) to discourage runaway logits.
# Create synthetic domain representations
# Imagine these are averaged representations from different document types
num_domains = 4
prototype_scale = 1.0
noise_std = 0.8
batch_size_per_domain = 64
# NOTE: If you crank up prototype_scale and reduce noise_std, this becomes
# an almost trivial supervised classification problem and the loss can drop
# to ~0 in just a few optimization steps (softmax saturation).
domain_prototypes = torch.randn(num_domains, d_model) * prototype_scale
# Create a router with one expert per domain
domain_router = Router(d_model, num_domains, top_k=1)
# Simulate "labeled" training: each sample comes from a known domain
# We'll train the router to route domain samples to corresponding experts
z_loss_coef = 1e-2
optimizer = torch.optim.Adam(
domain_router.parameters(), lr=0.005, weight_decay=1e-4
)
losses = []
for epoch in range(200):
epoch_loss = 0
for domain_idx in range(num_domains):
# Generate samples from this domain (prototype + noise)
samples = (
domain_prototypes[domain_idx]
+ torch.randn(batch_size_per_domain, d_model) * noise_std
)
samples = samples.unsqueeze(1) # Add seq dimension
# Train on logits directly for numerical stability
logits = domain_router.gate(samples).squeeze(1) # (batch, num_domains)
labels = torch.full(
(logits.shape[0],),
domain_idx,
dtype=torch.long,
device=logits.device,
)
# Cross-entropy: encourage routing to the correct expert
nll = F.cross_entropy(logits, labels)
# Router z-loss: discourages runaway logits (keeps softmax from saturating)
z = torch.logsumexp(logits, dim=-1)
z_loss = (z**2).mean()
loss = nll + z_loss_coef * z_loss
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch_loss += loss.item()
losses.append(epoch_loss / num_domains)
The router quickly learns to distinguish domain-specific patterns. Let's verify it routes correctly:
# Test routing accuracy on new samples
correct = 0
total = 0
domain_accuracies = []
with torch.no_grad():
for domain_idx in range(num_domains):
samples = (
domain_prototypes[domain_idx] + torch.randn(100, d_model) * 0.3
)
samples = samples.unsqueeze(1)
_, selected, _ = domain_router(samples)
selected = selected.squeeze()
# Calculate accuracy for this domain
domain_acc = (selected == domain_idx).float().mean().item()
domain_accuracies.append(domain_acc)
correct += (selected == domain_idx).sum().item()
total += 100Routing accuracy: 100.0% Per-domain accuracy: Domain 0: 100.0% Domain 1: 100.0% Domain 2: 100.0% Domain 3: 100.0%
The trained router successfully learns to route tokens from different domains to appropriate experts.

Router Weight Visualization
We can visualize the router's learned prototypes to understand what patterns it captures.
# Extract router weights for visualization
weights = domain_router.gate.weight.detach().numpy()
Each row shows a distinct pattern, representing the "prototype" that input must match to route to that expert. The router learns to position these prototypes to distinguish between domain-specific representations.