Warmup Guide — Mixture of Experts From Scratch
How to read this. No prior knowledge of MoE, routing, or sparse models is assumed. Everything is built from a dense feed-forward layer upward: what it is, why MoE exists, how the mechanism works underneath, what it costs in production, and the misconception people carry. If you already know MoE, start at Chapter 5 and do not skip Chapter 7 or Chapter 9.
Table of Contents
- Chapter 1: The Problem MoE Solves
- Chapter 2: The Dense Feed-Forward Layer, First
- Chapter 3: The MoE Layer
- Chapter 4: The Router, In Full
- Chapter 5: Router Collapse — the Default Failure
- Chapter 6: The Router Z-Loss
- Chapter 7: Capacity, Dropping, and the Silent Quality Loss
- Chapter 8: Total vs Active Parameters
- Chapter 9: The Communication Wall
- Chapter 10: MoE Scaling Laws and "Running Out of Internet"
- Chapter 11: The Modern Refinements
- Lab Walkthrough
- Success Criteria
- Interview Q&A
- Tips & Takeaways
- References
Chapter 1: The Problem MoE Solves
The welded constraint
In a dense transformer, every parameter participates in every token. Want the model to know more? Add parameters. But now every token costs more to process — during training and forever afterward during serving.
From Phase 00: C = 6ND for training, 2N FLOPs per token for inference. Both scale linearly
with N. Capacity and cost are welded together.
That is a genuine problem, because the two things you want are in tension:
- Knowledge and capability want many parameters.
- Latency and serving cost want few parameters.
The observation MoE exploits
Here is the insight: not every token needs every parameter. The weights that help predict the next token in a Python function are largely not the weights that help with 18th-century French poetry. In a dense model, both sets fire for both inputs, and most of that computation is wasted on any individual token.
Conditional computation breaks the weld: store many parameters, but activate only a small, input-dependent subset per token.
DENSE MoE
────── ───
params = 8 units params = 64 units (8x the knowledge)
FLOPs = 8 units/token FLOPs = 8 units/token (same cost!)
memory = 64 units <- the bill arrives here
Why it exists historically
The idea is old (Jacobs et al., 1991). Shazeer et al. (2017) made it work at scale for
language, Switch Transformers (2021) simplified it to k = 1 and made it stable, GShard
(2020) built the distributed machinery, and by 2024 essentially every frontier model is an
MoE — including, as Feinberg notes, the Gemini 2.0 series.
The misconception
"MoE gives you a bigger model for free."
It gives you a bigger model for the same FLOPs. It costs you memory, communication, stability, dropped tokens, and data hunger. This entire warmup is an accounting of where that bill lands.
Chapter 2: The Dense Feed-Forward Layer, First
You cannot understand what MoE replaces without knowing what it replaces.
What it is
Each transformer block has two sub-layers: attention (which routes information between positions) and a feed-forward network / MLP (which transforms each position independently).
def dense_ffn(token, w_in, w_out):
"""Expand to d_ff, apply a nonlinearity, project back to d_model."""
hidden = matvec(w_in, token) # d_model -> d_ff (usually d_ff ~ 4*d_model)
activated = [max(0.0, h) for h in hidden]
return matvec(w_out, activated) # d_ff -> d_model
Why it is the target
From Phase 00's parameter count, for a typical block:
attention projections : 41,943,040 (24%)
MLP : 135,266,304 (76%) <- MoE replaces THIS
norms : 8,192 ( 0%)
Two reasons the MLP is the right target:
- It is where the parameters are — about three-quarters of the block.
- It is position-independent. Each token goes through the MLP alone, with no interaction between positions. So you can route each token to a different expert without breaking anything. Attention cannot be split this way, because it is precisely the part that mixes positions.
This is the single best reason MoE targets the MLP and not attention, and it is worth being able to say out loud in an interview.
The misconception
"The MLP is just a nonlinearity; attention is where the intelligence is."
Empirically, most factual knowledge lives in MLP weights — the model-editing literature (ROME, MEMIT) locates and edits facts specifically there. Attention decides what to look at; the MLP decides what to think about it.
Chapter 3: The MoE Layer
The structure
Replace the one MLP with E parallel MLPs ("experts") plus a small router.
token
│
┌────────▼────────┐
│ ROUTER │ E scores, one per expert
│ (d_model x E) │
└────────┬────────┘
│ softmax, take top-k, renormalize
┌────────┬───────┼───────┬────────┐
▼ ▼ ▼ ▼ ▼
E0 E1 E2 ... E7
· ✓ · · ✓ only 2 of 8 run
└───────┬────────┘
▼ weighted sum by gate
output
The forward pass, exactly
def moe_layer(token, router_weights, experts, top_k=2):
# 1. score every expert
logits = matvec(router_weights, token) # E scores
probs = softmax(logits)
# 2. pick the best k, renormalize their gates to sum to 1
chosen = sorted(range(len(probs)), key=lambda i: -probs[i])[:top_k]
total = sum(probs[i] for i in chosen)
gates = {i: probs[i] / total for i in chosen}
# 3. run ONLY those k, and combine by gate weight
out = [0.0] * len(token)
for i, g in gates.items():
contribution = dense_ffn(token, experts[i]["w_in"], experts[i]["w_out"])
out = [o + g * c for o, c in zip(out, contribution)]
return out
The k choice
k | Name | Trade-off |
|---|---|---|
| 1 | Switch routing | Cheapest. Sharpest specialization. Hardest to train — a single routing mistake has no backup. |
| 2 | The common default | Two experts give the gradient two paths and the token a fallback. Used by Mixtral, DeepSeek, most production models. |
| 4–8 | Fine-grained MoE | Used with many small experts (DeepSeek-V3 uses 8 of 256). More combinations, better specialization, more routing overhead. |
E | Dense | Sparsity ratio 1.0. You have built a dense model with extra steps. |
Why renormalize the gates
This is subtle and worth being precise about. Suppose the softmax gives expert 6 a probability of 0.575 and expert 4 a probability of 0.151, and you take the top 2.
- Renormalized: gates become
0.79and0.21, summing to 1. The layer output has a consistent scale regardless of how confident the router was. - Raw: gates stay
0.575and0.151, summing to0.726. The layer output is scaled down by 27% — and by a different amount for every token, depending on how much probability mass leaked to the experts you did not pick.
That varying scale interacts badly with the residual stream and layer norms. Almost all production implementations renormalize. The lab implements both so you can see the difference, and a test asserts it.
Chapter 4: The Router, In Full
What it is
The smallest and most important component. Just d_model × E parameters — for a model with
d_model = 7168 and E = 256, that is 1.8M parameters, versus ~44M for a single expert. It
is 0.04% of the layer and it decides everything.
Numerical stability is not optional here
def softmax(xs):
"""Max-subtraction is what stops exp() overflowing to inf."""
m = max(xs)
exps = [math.exp(x - m) for x in xs]
s = sum(exps)
return [e / s for e in exps]
Why this matters more for a router than elsewhere: router logits are unconstrained and
there is a positive feedback loop pushing them up (Chapter 5). In bf16, exp(90) already
overflows. A naive softmax gives NaN, the NaN propagates through the residual stream, and
your 40-day training run is dead. The lab tests this with logits of 1000.
Subtracting the max changes nothing mathematically —
softmax(x) = softmax(x - c)for any constantc— and changes everything numerically. A test asserts this shift-invariance.
Ties and determinism
Two experts can score identically (especially early in training, when weights are near zero).
If your tie-break is dict ordering or an unstable sort, the same input routes differently on
different runs, and you lose reproducibility — which you need for debugging a run that costs
$30M. Break ties by expert index, always.
Chapter 5: Router Collapse — the Default Failure
This is the chapter that matters most. A router left alone will collapse, and it is not a rare pathology — it is the natural dynamic.
The mechanism
expert i is randomly slightly better at step 0
│
▼
it wins slightly more tokens
│
▼
it receives more gradient, so it trains faster
│
▼
it becomes genuinely better
│
▼
it wins even more tokens ──────────┐
▲ │
└────────────────────────┘
RICH GET RICHER
Within a few thousand steps you have one expert doing everything and E − 1 dead ones. You
paid for E× the memory, E× the checkpoint size, and E× the sharding complexity, and you
are running a dense model.
The fix: an auxiliary load-balancing loss
def load_balance_loss(assignments, all_probs, n_experts):
"""Switch-style: L_aux = E * sum_i f_i * P_i
f_i = fraction of token-SLOTS routed to expert i (discrete -> NO gradient)
P_i = mean router PROBABILITY on expert i (continuous -> differentiable)
"""
total_slots = sum(len(a) for a in assignments)
f = [0.0] * n_experts
for row in assignments:
for e in row:
f[e] += 1.0 / total_slots
P = [0.0] * n_experts
for probs in all_probs:
for i, p in enumerate(probs):
P[i] += p / len(all_probs)
return n_experts * sum(fi * Pi for fi, Pi in zip(f, P))
Why the product f_i · P_i, and not something simpler?
This design is genuinely clever and the reasoning is a great interview answer.
You want to penalize imbalance in f — the actual token counts. But f comes from a
top-k operation, which is discrete: it has zero gradient almost everywhere, so you cannot
backpropagate through it.
P — the mean softmax probability — is differentiable, but on its own it is a weak signal:
the router could keep P uniform while its top-k choices remain lopsided.
Multiplying them gives you both: the loss is scaled by the real imbalance (f) while the
gradient flows through P. When expert i is over-subscribed, f_i is large, so the
gradient pushing P_i down is large.
The calibration point — memorize this
Under perfect balance, f_i = P_i = 1/E for every i:
$$ L_{\text{aux}} = E \sum_{i=1}^{E} \frac{1}{E}\cdot\frac{1}{E} = E \cdot E \cdot \frac{1}{E^2} = 1.0 $$
1.0 means perfectly balanced. Larger is worse. That single fact makes an MoE training
dashboard readable at a glance — and the lab has a test asserting exactly 1.0.
Measured in the lab:
balanced : L_aux=1.0000 max/mean=1.00
collapsed: L_aux=7.4400 max/mean=8.00 dead=7
Watching it happen
The lab simulates the feedback loop directly:
aux_weight=0.000: max/mean after 600 steps = 4.10 COLLAPSED
aux_weight=0.002: max/mean after 600 steps = 1.43 healthy
aux_weight=0.005: max/mean after 600 steps = 1.06 healthy
aux_weight=0.010: max/mean after 600 steps = 1.00 healthy
The coefficient is a real hyperparameter
L_total = L_task + alpha * L_balance + gamma * L_z
alpha ≈ 0.01 is the common value, and it is finicky:
- Too small → collapse. You lose the entire benefit of MoE.
- Too large → you force tokens to experts that are wrong for them, purely to satisfy the balance constraint. Quality drops. The router is now optimizing bookkeeping instead of prediction.
What to put on the dashboard:
max_over_meanutilization andL_balance. Ifmax_over_meanclimbs past ~3, intervene now — not at the next checkpoint. This is exactly the sort of thing the five-person Flash 2.0 rotation was watching for.
Chapter 6: The Router Z-Loss
What it is
$$ L_z = \frac{1}{T}\sum_{t} \left(\log \sum_i e^{z_{t,i}}\right)^2 $$
A penalty on the magnitude of the router logits, introduced in ST-MoE (Zoph et al., 2022).
def router_z_loss(all_logits):
return sum(logsumexp(row) ** 2 for row in all_logits) / len(all_logits)
Why it exists — two distinct failures
1. Numerical overflow. Router logits are unconstrained and the collapse dynamic pushes them
up. In bf16, exp(90) overflows. NaN in the router means NaN in the residual stream means
a dead run.
2. A saturated softmax has no gradient. This one is subtler and more damaging. If logits
are [100, 2, 1], the softmax is [1.0, 0.0, 0.0] to floating-point precision. The
derivative of softmax is p_i(δ_ij − p_j) — when p_i is 1 or 0, that is zero. The router
stops learning. Routing freezes into whatever pattern it happened to have, permanently.
The lab shows the magnitude growing sharply:
logit scale x 1.0 -> z_loss= 9.999
logit scale x 5.0 -> z_loss= 160.348
logit scale x20.0 -> z_loss= 2527.342
gamma ≈ 1e-3 is typical — small, because you are only trying to keep logits in a sane range,
not to shape the distribution.
The general lesson, which transfers well beyond MoE: whenever a small number of unconstrained logits control a discrete decision, add a magnitude penalty. The same reasoning shows up in attention (QK-norm) for the same reason.
Chapter 7: Capacity, Dropping, and the Silent Quality Loss
The constraint
Accelerators want fixed-shape tensors. You cannot allocate "however many tokens happened to route here" — the shape has to be known ahead of time. So each expert gets a fixed buffer:
$$ \text{capacity} = \text{capacity_factor} \times \frac{\text{tokens} \times k}{E} $$
The fraction is the count each expert would get under perfect balance. capacity_factor
(typically 1.0–2.0) is the safety margin for imperfect balance.
The two failure directions
capacity per expert ────────────────────────►
┌──────────────────────────────────────┐
│████████████████░░░░░░░░░░░░░░░░░░░░░░│ under-subscribed:
│ real tokens PADDING (waste) │ you compute on zeros
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│██████████████████████████████████████│▓▓▓▓ over-subscribed:
│ buffer full │DROP overflow is discarded
└──────────────────────────────────────┘
Measured in the lab on a realistically imbalanced batch:
capacity_factor=1.00: cap= 16 dropped= 12 ( 9.4%) padded= 12 buffer_util=90.6%
capacity_factor=1.25: cap= 20 dropped= 3 ( 2.3%) padded= 35 buffer_util=78.1%
capacity_factor=2.00: cap= 32 dropped= 0 ( 0.0%) padded=128 buffer_util=50.0%
There is no setting that avoids both. Going from 1.0 to 2.0 eliminates dropping and halves your effective compute utilization. That trade-off is the hyperparameter.
Why dropping is the dangerous one
Trace what happens to a dropped token in the lab's forward pass:
if not got_any:
# Every slot dropped: the token skips the FFN entirely and rides the
# residual. No error, no log line. This is the silent failure.
acc = list(token)
fully_dropped += 1
No exception. No warning. No log line. The token passes through the layer unchanged on the residual connection. Training continues. Your loss is very slightly worse than it should be, and nothing tells you why.
This is a genuine, live source of quality loss in production MoE models — and it is why
drop_rate belongs on the dashboard next to max_over_mean.
What a shared expert buys
A shared expert (DeepSeek-V3's design) runs for every token, unconditionally, in addition to the routed ones:
if shared_expert is not None:
acc = [a + c for a, c in zip(acc, expert_forward(shared_expert, token))]
Two things fall out:
- No token can ever be fully dropped. The failure mode above is structurally eliminated.
- The routed experts can specialize harder, because the shared expert absorbs the common, general-purpose transformation that every token needs. You are no longer forcing every expert to independently learn the basics.
The cost: that expert's FLOPs are paid on every token, always. It is a small, always-on tax that buys a large reduction in variance.
Chapter 8: Total vs Active Parameters
The arithmetic most commonly botched in modern LLM discussion, and it is off by 10–20×.
def moe_parameter_counts(d_model, d_ff, n_experts, top_k, shared_experts=0):
one_expert = 2 * d_model * d_ff
router = d_model * n_experts
total = router + (n_experts + shared_experts) * one_expert # -> MEMORY
active = router + (top_k + shared_experts) * one_expert # -> FLOPs
return {"total": total, "active": active, "sparsity_ratio": total / active}
The rule, and there are no exceptions:
| Question | Use |
|---|---|
How much compute to train? C = 6ND | active |
How much compute to serve? 2N/token | active |
| How much HBM do I need? | total |
| How big is the checkpoint? | total |
| How do I shard it? | total |
A 700B-total / 52B-active model trains like a 52B model and stores like a 700B one. Get this backwards and your capacity plan is wrong by an order of magnitude.
From the lab, at toy scale:
top_k=1: total= 2112 active= 320 sparsity=6.60x
top_k=2: total= 2112 active= 576 sparsity=3.67x
top_k=8: total= 2112 active= 2112 sparsity=1.00x
Note the last row: top_k == n_experts gives sparsity exactly 1.0. A dense model wearing an
MoE costume. The lab tests this boundary explicitly, because it is the sanity check that
proves your accounting is right.
Chapter 9: The Communication Wall
Now the cost that shaped Feinberg's Flash 2.0 story.
Why sharding is forced
All E experts must be resident in HBM. A 700B-parameter MoE in bf16 is 1.4 TB. A TPU v5e has
16 GB; an H100 has 80 GB. The model cannot fit on one chip, so experts are distributed
across chips — expert parallelism.
What that costs, per layer
Layer ℓ: token lives on chip 0
router says "expert 37", which lives on chip 4
──> send the activation to chip 4 [NETWORK]
chip 4 computes
──> send the result back to chip 0 [NETWORK]
Layer ℓ+1: router says "expert 12" (chip 1) ... [NETWORK] [NETWORK]
... for every one of ~60 layers.
Feinberg's description: "that token might live on the first TPU, but it needs to go to the last TPU." The collective is an all-to-all — every chip sends a different slice to every other chip — which is the most expensive collective there is, and its cost, he notes, "increases dramatically with N."
The arithmetic
def expert_parallel_comm_bytes(n_tokens, d_model, n_layers, top_k, bytes_per_elem=2):
per_hop = n_tokens * d_model * bytes_per_elem * top_k
return 2 * n_layers * per_hop # dispatch + combine, every layer
From the lab, for a realistic prefill:
8192 tokens, d_model=8192, 60 layers, top_k=2
bytes across the interconnect : 32.2 GB
time at 100 Gbps : 2.578 s
Two and a half seconds of pure network time before a single useful FLOP. For an interactive product with a sub-second budget (Phase 00's napkin math), that is fatal.
The fix, in one sentence
Stop sharding experts across chips. Shard layers instead, and stream chunks of the prompt through the resulting pipeline so the transfers overlap with computation. That is pipelined prefill, credited in the interview to Geng Yan, and it is what made an MoE Gemini Flash servable.
The full treatment — including why it works for prefill and not for decode — is in the transcript dissection, Claim 12.
The transferable lesson, and it is the real one: the fix was not a better kernel or a better model. It was changing which axis you shard along, chosen with knowledge of which phase of inference you are in. That is what "inference co-design" means in practice.
Chapter 10: MoE Scaling Laws and "Running Out of Internet"
Feinberg's slide states both halves:
"MoE scaling laws are better, but have implications for token hunger. We're running out of internet! ... Notice relative data hunger compared to dense! At same active param count and fixed 100B token training, MoE 64E improves on dense."
Decoded
The good half. For a fixed compute budget, an MoE reaches a lower loss than a dense model. The scaling law is strictly better. At equal active parameters and equal tokens, 64 experts beats dense. This is why everyone switched.
The bill. The compute-optimal D for an MoE is larger than for a dense model. MoE has
more capacity to fill, and filling capacity takes tokens. So MoE converts "we have compute"
into "we need more unique tokens" — and unique, high-quality tokens are the resource that is
actually scarce.
Hence: MoE trades a compute problem for a data problem. Which is a good trade right up until you run out of data, and then it is not.
That is the bridge to the rest of Feinberg's agenda — multimodal data, synthetic data, and the
data-constrained scaling law L(N, U, R) where U is unique tokens and R is repeats. He
notes that data work is where he spends "probably half my focus this year so far." The
pre-training lead's time goes to data, not architecture.
The reference for the routed-model scaling law is Clark et al., Unified Scaling Laws for Routed Language Models (2022). The data-constrained law is Muennighoff et al. (2023). Both are on his slide's resource list.
Chapter 11: The Modern Refinements
What production MoE looks like in 2024–2025, beyond the Switch baseline.
| Refinement | What it does | Why |
|---|---|---|
| Fine-grained experts | Many small experts (256) instead of few large ones (8), with higher k | More combinations of experts per token → better specialization at the same active parameter count |
| Shared experts | One or more experts that always run | Removes the fully-dropped failure mode; lets routed experts specialize harder (Chapter 7) |
| Auxiliary-loss-free balancing | A per-expert bias added to router logits, adjusted by observed load, instead of an aux loss | The aux loss damages quality by forcing wrong routing. A bias term balances load without adding a gradient that fights the task loss. (DeepSeek-V3) |
| Expert-choice routing | Invert it: each expert picks its top tokens, rather than each token picking experts | Balance is guaranteed by construction — no aux loss, no dropping. Cost: it needs the whole batch at once, so it does not work for autoregressive decode. |
| Dropless MoE | Variable-size expert buffers via block-sparse kernels | Eliminates dropping and padding entirely — at the price of needing custom kernels (which is Phase 11's territory, and a live example of "missing kernels kill good ideas") |
| Upcycling | Initialize MoE experts from a trained dense checkpoint | Skips the expensive early phase; the experts start from something that already works |
Notice the pattern across that table. Four of the six refinements exist to fix problems created by the two mechanisms in Chapters 5 and 7 — the auxiliary loss and the capacity buffer. Understand those two deeply and the rest of the literature reads as commentary.
Lab Walkthrough
Lab 01 — Router, Load Balancing, Capacity & the MoE Layer
Implement in this order:
softmax,logsumexp,matvec,relu. Gettest_softmax_survives_huge_logitspassing first — max-subtraction is the lesson, not a detail.router_logits,route_token,route_batch. Watch therenormalizeflag; both behaviours are tested. Break ties by expert index.load_balance_loss. Aim for the calibration point:test_balanced_routing_gives_aux_loss_of_exactly_one. If you do not get exactly1.0, the formula is wrong.router_z_loss,expert_utilization.expert_capacity,apply_capacity. Note thatcapacity_factor < 1.0must raise — it drops tokens even under perfect balance, which is never intended.moe_forward. The residual fallback for fully-dropped tokens is the important line.moe_parameter_counts,expert_parallel_comm_bytes,comm_seconds.simulate_collapse. The money test.
The traps:
- A naive softmax gives
NaNon large logits and the test will catch you. - The load-balance loss divides
fby total slots (tokens × k), not by token count. apply_capacityis first-come-first-served in token order — deterministic, and the tests depend on it.top_k == n_expertsmust give sparsity exactly1.0; if it does not, your router or parameter count is wrong.- Everything random goes through a seeded
random.Random(seed).
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. -
Balanced routing gives a load-balance loss of exactly
1.0in your code. -
You can explain why the aux loss multiplies
fbyPrather than penalizingfalone. -
You have watched a router collapse at
aux_weight = 0and recover at0.01. - You can state the capacity formula and the drop/pad trade-off from memory.
-
You can compute total vs active parameters and say which goes into
6ND. - You can compute all-to-all bytes for a realistic MoE and convert to seconds.
Interview Q&A
Q: What is a Mixture of Experts and what does it buy you?
Replace each transformer block's MLP with E parallel MLPs plus a small router that picks the
top-k per token. Parameters scale with E; FLOPs scale with k. So you get far more
capacity at the same compute per token. It targets the MLP rather than attention for two
reasons: the MLP is ~76% of the block's parameters, and it is position-independent, so routing
each token separately does not break anything — attention is precisely the part that mixes
positions and cannot be split this way.
Q: What breaks first when you train an MoE?
The router collapses. It is a rich-get-richer loop: one expert is randomly slightly better, so
it wins more tokens, so it trains more, so it becomes genuinely better, so it wins more. Within
a few thousand steps you have one live expert and E−1 dead ones — you paid for E× the memory
to run a dense model. The fix is an auxiliary load-balancing loss, L = E·Σ f_i·P_i, with a
coefficient around 0.01.
Q: Why does that loss multiply a token fraction by a probability?
Because f, the fraction of tokens routed to each expert, comes from a top-k, which is
discrete and has no gradient. P, the mean softmax probability, is differentiable but a weak
signal on its own — the router could keep P uniform while its actual choices stay lopsided.
Multiplying gives you a loss scaled by the real imbalance with gradient flowing through the
differentiable part. And it is calibrated: perfect balance gives exactly 1.0.
Q: What is capacity factor and what happens if you get it wrong?
Each expert gets a fixed buffer of cf × tokens × k / E slots, because hardware needs
fixed-shape tensors. Too low and tokens overflow and are dropped — silently: the token
skips the FFN, rides the residual, no error is raised, and your quality is quietly worse. Too
high and buffers are padded with zeros and you waste compute. There is no setting that avoids
both, so you tune it against measured drop rate. A shared expert that runs for every token
eliminates the fully-dropped case structurally.
Q: A 700B-parameter MoE with 8 of 256 experts active. How much compute to train it?
6ND with N = active parameters, not total. If active is ~52B, it trains like a 52B
dense model. But it stores like a 700B model — 1.4 TB in bf16 — which is what dictates your
sharding, your checkpoint size, and your HBM budget. Active for FLOPs, total for memory.
Q: Why is MoE hard to serve? All experts must be HBM-resident, so a large MoE cannot fit on one chip and you shard experts across chips. Then every layer, every token must be sent to whichever chip holds its expert and the result sent back — two all-to-all collectives per layer. For an 8k-token prefill on a 60-layer model that is tens of gigabytes and seconds of pure network time, which is fatal for an interactive product. The fix Google used for Flash 2.0 was to shard layers instead of experts and stream prompt chunks through the resulting pipeline, so the transfers hide behind computation. Notably it works for prefill — which is compute-bound and has thousands of tokens to chunk — and not for decode, which produces one token at a time and has nothing to overlap with.
Q: If MoE is strictly better per FLOP, why isn't everything an MoE? Three reasons. The memory and sharding complexity is real. The training instability is real — you have added a discrete decision to a system that otherwise has none. And the compute-optimal token count is larger for an MoE, so it converts a compute problem into a data problem — and unique high-quality data is the resource actually running out.
Q: What is the router z-loss for?
It penalizes the magnitude of router logits. Two failures it prevents: numerical overflow —
router logits are unconstrained, the collapse dynamic pushes them up, and exp() overflows in
bf16, putting NaN through the residual stream; and softmax saturation — once the softmax is
effectively one-hot, its derivative p(1−p) is zero, the router stops learning, and routing
freezes permanently.
Tips & Takeaways
Tips
- Put
max_over_meananddrop_rateon the dashboard. They are the two numbers that tell you an MoE is failing, and both fail silently otherwise. - Memorize
L_aux = 1.0means balanced. It makes the metric readable instantly. - Always ask "total or active?" the moment someone quotes an MoE parameter count.
- Break router ties deterministically. Reproducibility on a $30M run is not negotiable.
- Never write a softmax without max-subtraction. Especially in a router.
- When you see a new MoE paper, ask which of the two core problems it is fixing — balance or capacity. Most of the literature is one or the other.
- Compute the all-to-all bytes before you commit to an architecture, not after.
Takeaways
- Parameters scale with
E; FLOPs scale withk. That is MoE, entire. - MoE targets the MLP because it holds ~76% of the parameters and is position-independent.
- Renormalizing the top-k gates keeps the output scale consistent; not doing so silently attenuates the layer by a per-token amount.
- Routers collapse by default. The auxiliary loss is what prevents it, and
1.0is balanced. - The
f·Pproduct exists to get a gradient through a discrete decision. - The z-loss prevents overflow and softmax saturation — two different failures.
- Capacity trades dropping against wasted compute. Dropping is silent, which makes it worse.
- A shared expert eliminates fully-dropped tokens and lets the routed experts specialize.
- Active for FLOPs, total for memory. Off by 10–20× if you swap them.
- Naive expert parallelism costs seconds of network time. The fix is a different sharding axis, not a better kernel.
- MoE trades a compute problem for a data problem — and data is what is running out.
References
Foundational
- Jacobs et al., Adaptive Mixtures of Local Experts, 1991 — the original idea
- Shazeer et al., Outrageously Large Neural Networks: The Sparsely-Gated MoE Layer, 2017 — https://arxiv.org/abs/1701.06538
- Lepikhin et al., GShard: Scaling Giant Models with Conditional Computation, 2020 — https://arxiv.org/abs/2006.16668 (expert parallelism, capacity, the all-to-all)
- Fedus, Zoph & Shazeer, Switch Transformers, 2021 — https://arxiv.org/abs/2101.03961 (
k=1, the load-balance loss, capacity factor)
Stability and refinements
- Zoph et al., ST-MoE: Designing Stable and Transferable Sparse Expert Models, 2022 — https://arxiv.org/abs/2202.08906 (the router z-loss)
- Zhou et al., Mixture-of-Experts with Expert Choice Routing, 2022 — https://arxiv.org/abs/2202.09368
- Gale et al., MegaBlocks: Efficient Sparse Training with Mixture-of-Experts, 2022 — https://arxiv.org/abs/2211.15841 (dropless MoE via block-sparse kernels)
- Komatsuzaki et al., Sparse Upcycling, 2022 — https://arxiv.org/abs/2212.05055
Production models
- Jiang et al., Mixtral of Experts, 2024 — https://arxiv.org/abs/2401.04088
- DeepSeek-AI et al., DeepSeek-V3 Technical Report, 2024 — https://arxiv.org/abs/2412.19437 (fine-grained + shared experts, auxiliary-loss-free balancing)
Scaling
- Clark et al., Unified Scaling Laws for Routed Language Models, 2022 — https://arxiv.org/abs/2202.01169
- Muennighoff et al., Scaling Data-Constrained Language Models, 2023 — https://arxiv.org/abs/2305.16264
Primary source for this phase's framing
- Feinberg, Gemini Pretraining, Princeton, Apr 2025 — slides
- Developing Dev interview · video