# Moe Routing Alignment

> Prevents MoE router instability during RL training by recording and replaying inference-phase routing distributions back into training. Reduces training-inference routing divergence and KL divergence, enabling stable MoE RL scaling without sacrificing training speed.

- Skill: `adu2021/moe-routing-alignment` (Agent Skill)
- Install (CLI): `npx skillmds@latest add adu2021/moe-routing-alignment`
- Raw SKILL.md: https://api.skillmd.com/api/skills/adu2021/moe-routing-alignment/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: adu2021 (https://skillmd.com/u/adu2021)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/adu2021/moe-routing-alignment

---


# Rollout Routing Replay: Stabilizing Mixture-of-Experts RL

Mixture-of-Experts models with RL training suffer from routing instability: routers diverge between training and inference phases, causing catastrophic collapse. Rollout Routing Replay (R3) synchronizes routing behavior across both phases.

By recording inference routing and replaying those decisions during training, R3 prevents divergence and enables stable scaling.

## Core Concept

The core problem: **routers make different decisions during training vs. inference**, even with identical inputs. This divergence causes:
- Policy collapse in RL training
- Inconsistent expert utilization
- Degraded final model performance

R3 solves this by:
- Recording routing decisions during inference rollouts
- Replaying those same routing distributions during policy gradient training
- Synchronizing training and inference for the same states

## Architecture Overview

- Inference-phase routing distribution recording
- Training-phase replay mechanism to enforce learned routing
- KL divergence minimization between phases
- Efficient storage of routing patterns

## Implementation Steps

Record routing distributions during inference rollouts. When the model makes decisions in the environment, capture which experts were selected:

```python
class RoutingRecorder:
    def __init__(self, num_experts):
        self.num_experts = num_experts
        self.routing_history = []

    def record_routing(self, inputs, routing_logits):
        """Capture routing decisions during inference."""
        # Get expert selection distribution
        routing_probs = torch.softmax(routing_logits, dim=-1)

        # For each token, record which expert was selected
        selected_experts = torch.argmax(routing_probs, dim=-1)

        record = {
            'inputs': inputs.detach().cpu(),
            'routing_logits': routing_logits.detach().cpu(),
            'routing_probs': routing_probs.detach().cpu(),
            'selected_experts': selected_experts.detach().cpu()
        }

        self.routing_history.append(record)
        return record

    def get_routing_distribution(self):
        """Retrieve recorded routing distributions for training."""
        return self.routing_history

    def clear(self):
        """Clear history after training batch."""
        self.routing_history = []
```

Implement training with enforced routing replay. During policy gradient updates, use recorded routing to constrain router behavior:

```python
class RoutingReplayPG:
    def __init__(self, model, recorder, learning_rate=1e-4):
        self.model = model
        self.recorder = recorder
        self.optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

    def compute_loss_with_routing_replay(self, batch_inputs, recorded_routing, rewards):
        """Compute RL loss while enforcing routing consistency."""
        # Forward pass through model
        logits, routing_logits = self.model(batch_inputs)

        # Get current routing probabilities
        current_probs = torch.softmax(routing_logits, dim=-1)
        recorded_probs = recorded_routing['routing_probs']

        # Policy gradient loss
        action_log_probs = torch.log(current_probs.gather(-1, recorded_routing['selected_experts'].unsqueeze(-1)))
        pg_loss = -(action_log_probs * rewards).mean()

        # Routing consistency loss: penalize divergence from recorded routing
        kl_divergence = torch.nn.functional.kl_div(
            torch.log(current_probs + 1e-10),
            recorded_probs,
            reduction='batchmean'
        )

        # Total loss: PG + routing alignment
        total_loss = pg_loss + 0.5 * kl_divergence

        return total_loss, {
            'pg_loss': pg_loss.item(),
            'kl_div': kl_divergence.item(),
            'total_loss': total_loss.item()
        }

    def training_step(self, batch_inputs, recorded_routing, rewards):
        """Perform one gradient update with routing replay."""
        loss, metrics = self.compute_loss_with_routing_replay(
            batch_inputs, recorded_routing, rewards
        )

        self.optimizer.zero_grad()
        loss.backward()
        torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
        self.optimizer.step()

        return metrics
```

Implement the full training loop that alternates between inference (recording) and training (replaying):

```python
def train_moe_with_routing_replay(model, env, num_episodes=1000):
    """Main training loop with routing replay."""
    recorder = RoutingRecorder(num_experts=model.num_experts)
    pg_trainer = RoutingReplayPG(model, recorder)

    for episode in range(num_episodes):
        # Phase 1: Inference rollouts with routing recording
        episode_transitions = []
        for _ in range(batch_size):
            state = env.reset()
            done = False

            while not done:
                # Forward pass records routing
                with torch.no_grad():
                    action, routing_logits = model(state)
                    recorder.record_routing(state, routing_logits)

                state, reward, done, _ = env.step(action)
                episode_transitions.append((state, reward))

        # Phase 2: Training with recorded routing
        recorded_routing = recorder.get_routing_distribution()
        episode_reward = sum(r for _, r in episode_transitions)

        # Train on batch with enforced routing
        metrics = pg_trainer.training_step(
            batch_inputs=torch.stack([s for s, _ in episode_transitions]),
            recorded_routing=recorded_routing[0],  # Use first recorded distribution
            rewards=torch.tensor([r for _, r in episode_transitions])
        )

        recorder.clear()

        if episode % 100 == 0:
            print(f"Episode {episode}, Reward: {episode_reward:.2f}, "
                  f"KL Div: {metrics['kl_div']:.4f}")
```

## Practical Guidance

| Parameter | Recommendation |
|-----------|-----------------|
| KL divergence weight | 0.5-1.0 (balance PG loss and routing consistency) |
| Routing history retention | Single batch (memory-efficient) |
| Recording overhead | ~5-10% inference time |
| Batch size for replay | Same as inference batch |

**When to use:**
- MoE models with RL training (training collapse without R3)
- Scenarios where router stability is critical
- Large-scale models where divergence is pronounced
- Environments with exploration requirements

**When NOT to use:**
- Supervised fine-tuning only (routing already stable)
- Fixed routing strategies (no learnable router)
- Memory-constrained environments (recording adds overhead)

**Common pitfalls:**
- KL weight too high (suppresses learning new routing)
- Recording stale data across episodes (outdated routing patterns)
- Not clearing history between batches (information leakage)
- Mismatch between recorded batch size and training batch size

Reference: [Stabilizing MoE RL on arXiv](https://arxiv.org/abs/2510.11370)

