Lab 01 — Router, Load Balancing, Capacity & the MoE Layer
Build a complete Mixture-of-Experts layer in pure Python, then break it on purpose: watch the router collapse, watch tokens get dropped silently, and watch the auxiliary loss save it.
The problem
MoE is usually explained as "more parameters, same FLOPs." That framing is true and useless, because it omits every part that makes MoE hard: routers collapse by default, capacity buffers drop tokens without raising anything, and sharding experts across chips costs seconds of network time per forward pass.
This lab implements the mechanism and all three failure modes, so that "MoE" stops being a word and becomes something you have debugged.
What you build
| Group | Functions | The idea |
|---|---|---|
| Primitives | softmax, logsumexp, matvec, relu | Max-subtraction is the lesson, not a detail |
| Router | router_logits, route_token, route_batch | Top-k, gate renormalization, deterministic ties |
| Aux losses | load_balance_loss, router_z_loss, expert_utilization | What stops collapse, and the dashboard numbers |
| Capacity | expert_capacity, apply_capacity | Fixed buffers, dropping, padding |
| The layer | make_expert, expert_forward, moe_forward, total_training_loss | Full forward pass with shared expert and residual fallback |
| Accounting | moe_parameter_counts, expert_parallel_comm_bytes, comm_seconds | Total vs active; the communication wall |
| The failure | simulate_collapse | Rich-get-richer, and the aux loss preventing it |
Key concepts
| Concept | Why it is in this lab |
|---|---|
| Max-subtracted softmax | Router logits grow without bound; naive exp gives NaN and kills the run |
| Gate renormalization | Without it the layer output is silently attenuated by a per-token amount |
L_aux = E·Σ f_i·P_i | Discrete f supplies the signal, differentiable P supplies the gradient |
L_aux = 1.0 | The calibration point: exactly balanced. Larger is worse. |
| Router z-loss | Prevents bf16 overflow and softmax saturation — two distinct failures |
| Capacity factor | Trades dropped tokens against padded (wasted) compute. No free setting. |
| Silent dropping | A fully-dropped token rides the residual. No error. Quality quietly drops. |
| Shared expert | Structurally eliminates the fully-dropped case |
| Total vs active | Active → FLOPs (6ND); total → HBM. Off by 10–20× if swapped. |
| All-to-all cost | 2 collectives per layer under expert parallelism — the Flash 2.0 wall |
Files
| File | What it is |
|---|---|
| lab.py | Your implementation. Signatures, docstrings and validation contracts given. |
| solution.py | Reference. python solution.py runs an eight-part worked example. |
| test_lab.py | 53 tests: happy path, validation, boundaries, invariants, determinism. |
| requirements.txt | pytest only. Pure stdlib otherwise. |
Run
pytest test_lab.py -v # your lab.py — red until you implement
LAB_MODULE=solution pytest test_lab.py -v # the reference — must be green (53 passed)
python solution.py # the worked example
Where to start
softmax→ gettest_softmax_survives_huge_logitsgreen. Logits of 1000 must not produceNaN. This is the most common real-world MoE crash.route_token→test_renormalized_gates_sum_to_oneand itsrawcounterpart. Both behaviours are tested because both exist in the wild.load_balance_loss→ aim fortest_balanced_routing_gives_aux_loss_of_exactly_one. If you do not get exactly1.0, your formula is wrong. Rememberfdivides by total slots (tokens × k), not token count.apply_capacity→ first-come-first-served in token order; the tests depend on that determinism.moe_forward→ the residual fallback for fully-dropped tokens is the line that matters.simulate_collapse→ the money test.
The traps:
- Naive softmax →
NaNon large logits. Tested. - Dividing
fby token count instead of slot count → yourL_auxwill not be1.0. capacity_factor < 1.0must raise — it drops tokens even under perfect balance.top_k == n_expertsmust give sparsity exactly1.0. If not, your accounting is off.- Unstable tie-breaking in the router destroys reproducibility. Break by expert index.
Success criteria
-
LAB_MODULE=solution pytest test_lab.py -v→ 53 passed. -
Your
lab.pyreaches 53 passed. -
python solution.pyruns and you can explain all eight sections. -
You have seen
aux_weight=0.0collapse tomax/mean = 4.10and0.01hold at1.00. - You can state the capacity formula and predict a drop rate from an imbalance.
- You can convert an MoE's all-to-all traffic into seconds and say why it is fatal.
How this maps to the real stack
| This lab | The real thing | Where the miniature lies |
|---|---|---|
route_token / route_batch | Megatron-LM MoELayer, DeepSpeed-MoE, Mixtral's MoeBlock, JAX/Flax MoE | Real routers run fused on-device with the top-k in a kernel. Ours is a Python loop — same math, ~10⁶× slower. |
load_balance_loss | switch_load_balancing_loss in Megatron/DeepSpeed | Identical formula. Real implementations compute it per-device and all-reduce, which introduces subtleties about whether balance is global or per-shard — a genuine source of production bugs. |
apply_capacity | GShard/Switch dispatch-and-combine via one-hot matmuls or gather/scatter | Real dispatch builds a [E, capacity, d_model] tensor with one-hot matmuls so it stays differentiable and fused. Ours is a Python list walk. |
expert_forward | A grouped/batched GEMM over experts, or block-sparse kernels (MegaBlocks) | Real experts run as one batched matmul, not a loop. This is exactly where "missing kernels kill good ideas" bites — dropless MoE needs custom block-sparse kernels to exist at all. |
expert_parallel_comm_bytes | NCCL/all_to_all in Megatron; XLA collectives on TPU | Ours ignores topology (a torus vs a fat-tree changes the constant a lot), overlap, and compression. Directionally right, not a substitute for profiling. |
simulate_collapse | The real dynamic, observed on training dashboards | A crude ODE-ish model, tuned to show the qualitative behaviour. Real collapse depends on data ordering, init, and LR schedule. |
What is not a lie: the load-balance formula and its 1.0 calibration, the capacity
formula, the total-vs-active distinction, and the all-to-all byte count. Those are exact, and
those are what get asked about.
Extensions
- Implement expert-choice routing. Invert the assignment — each expert picks its top tokens. Balance becomes guaranteed by construction, no aux loss, no dropping. Then discover why it cannot be used for autoregressive decode.
- Implement auxiliary-loss-free balancing (DeepSeek-V3): maintain a per-expert bias added to router logits, nudged up or down by observed load. Compare final quality against the aux loss at matched balance — the claim is that the aux loss costs quality by forcing wrong routing, and you can measure that.
- Add a real gradient. Wire this into Phase 03's autograd (or PyTorch) and actually train a tiny MoE on a toy task. Watch the aux-loss coefficient matter.
- Model the pipelined-prefill fix. Extend
expert_parallel_comm_byteswith a pipelined alternative that shards layers instead of experts, and reproduce the order-of-magnitude improvement in the transcript dissection's Claim 12. - Sweep the capacity factor against real routing entropy. Generate routings at varying
levels of imbalance and plot drop rate vs
capacity_factor— that curve is the one you actually use to pick the hyperparameter.
Interview / resume bullets
- "Implemented a Mixture-of-Experts layer from first principles in pure Python — numerically stable top-k routing with gate renormalization, Switch-style load-balancing loss, ST-MoE router z-loss, capacity-factor dispatch with drop/pad accounting, shared experts, and expert-parallel communication cost modelling — verified by 53 tests covering boundary and determinism cases."
- "Reproduced and quantified the three canonical MoE failure modes: router collapse via rich-get-richer feedback (and its prevention by an auxiliary loss), silent token dropping through fixed capacity buffers, and the all-to-all communication wall of naive expert parallelism (32 GB and ~2.6 s per 8k-token prefill on a 60-layer model)."
- Interview-ready: "Active parameters for FLOPs, total parameters for memory. A 700B/52B MoE trains like a 52B model and stores like a 700B one — and confusing the two makes your capacity plan wrong by an order of magnitude."