What a mixture of experts really is
A mixture of experts holds many parallel sub-networks and sends each token to only a few of them, so the model can carry huge knowledge while doing small work per word.
- 12 min read
- 3 reading levels
- Updated
Read these first
On this page 9
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
The short answer
A mixture of experts is a model of many small sub-models, and each word visits only a couple.
The hospital you have queued in
A big government hospital has thirty departments. Orthopaedics, cardiology, eye, skin, and so on.
You arrive with a swollen ankle. At the front desk someone glances at you and sends you to orthopaedics. You do not walk through all thirty departments.
The hospital is enormous. Your visit is small. That is exactly the trade a mixture of experts makes.
What problem this solves
More knowledge in a model means more stored numbers. More stored numbers means more work for every word, and more cost for every user, forever.
The two used to be locked together. A bigger model was always a slower model.
A mixture of experts breaks the lock. Add a hundred departments and the hospital knows more. Each patient still visits one department, so each visit costs the same.
ordinary layer mixture of experts
-------------- ------------------
one block of numbers 32 blocks of numbers
every word uses all of it each word uses 4 of them
knowledge: fixed knowledge: 32 blocks worth
work/word: all of it work/word: 4 blocks worthWho does the sending
A small piece called the router looks at each word and picks which departments it should visit. It is tiny — a fraction of the size of one department.
Nobody tells the router what the departments should specialise in. It works that out during training, together with the departments themselves.
And what it learns is often not what people expect. Departments rarely split by topic like "medicine" or "law". They split by patterns in the text that are harder to name.
The catch that surprises everybody
You still need every department standing there, staffed, with the lights on. A hospital cannot conjure up cardiology when a heart patient walks in.
So the memory cost is the whole model. Every expert must be loaded, even the ones this word will not use.
That is the real trade. Less work per word, the same memory. These models are cheap to run and expensive to hold. It is why they suit big shared servers and are awkward on a laptop.
Which part gets replaced
Only one part of each layer becomes a set of experts: the thinking block that sits after attention.
The attention part is untouched. Everything you learned about attention still applies. A mixture-of-experts model is an ordinary transformer with one component swapped.
Where you have already seen this
- Mixtral, DeepSeek, Qwen and gpt-oss, all open models you can download.
- Almost every large hosted assistant. The design is close to universal at the top end.
- Any model described as "671 billion parameters, 37 billion active".
Remember this
- Many parallel sub-networks called experts, and a small router choosing a few per word.
- Knowledge grows with the number of experts. Work per word does not.
- Memory does not shrink. Every expert has to be loaded.
What to learn next
- The router — how the choice is made, and why it is hard to train.
- Active parameters vs total parameters — reading model names honestly.
- Transformers — the architecture an MoE layer plugs into.
Developer — Code and libraries.
Setup
pip install torchWritten against PyTorch 2.5.1, Python 3.10. CPU, instant.
A mixture-of-experts layer, complete
import torch, torch.nn as nn, torch.nn.functional as F
torch.manual_seed(0)
d_model, d_ff, n_exp, top_k, n_tok = 16, 32, 8, 2, 6
class MoE(nn.Module):
def __init__(self):
super().__init__()
self.router = nn.Linear(d_model, n_exp, bias=False)
self.experts = nn.ModuleList(
nn.Sequential(nn.Linear(d_model, d_ff), nn.GELU(), nn.Linear(d_ff, d_model))
for _ in range(n_exp))
def forward(self, x):
logits = self.router(x) # one score per expert per token
probs = F.softmax(logits, dim=-1)
weight, idx = probs.topk(top_k, dim=-1) # keep the best few only
weight = weight / weight.sum(-1, keepdim=True) # renormalise the survivors
out = torch.zeros_like(x)
for slot in range(top_k):
for e in range(n_exp):
hit = idx[:, slot] == e
if hit.any(): # this expert runs only if chosen
out[hit] += weight[hit, slot, None] * self.experts[e](x[hit])
return out, idx, weight
moe = MoE()
x = torch.randn(n_tok, d_model)
out, idx, weight = moe(x)
print("token -> experts chosen (and their weights)")
for t in range(n_tok):
picks = ", ".join(f"E{idx[t,s].item()} {weight[t,s]:.2f}" for s in range(top_k))
print(f" token {t}: {picks}")
one_expert = sum(p.numel() for p in moe.experts[0].parameters())
total = sum(p.numel() for p in moe.parameters())
active = one_expert * top_k + moe.router.weight.numel()
print(f"\nparameters in one expert : {one_expert:,}")
print(f"total parameters in the layer: {total:,}")
print(f"used for any single token : {active:,} ({active/total:.1%})")
print("\nsame layer, more experts, same work per token")
print(f"{'experts':>8}{'total params':>15}{'active params':>15}{'fraction':>10}")
for n in (8, 64, 256, 512):
tot = n * one_expert + d_model * n
act = top_k * one_expert + d_model * n
print(f"{n:>8}{tot:>15,}{act:>15,}{act/tot:>10.2%}")
print("\nreal models (from each config.json on the Hub)")
print(f"{'model':<24}{'experts':>8}{'top-k':>7}{'shared':>8}")
for nm, e, k, s in [("Mixtral-8x7B-v0.1", 8, 2, 0), ("DeepSeek-V3", 256, 8, 1),
("Qwen3-30B-A3B", 128, 8, 0), ("Qwen3-Next-80B-A3B", 512, 10, 1),
("gpt-oss-20b", 32, 4, 0)]:
print(f"{nm:<24}{e:>8}{k:>7}{s:>8}")token -> experts chosen (and their weights)
token 0: E5 0.53, E7 0.47
token 1: E7 0.60, E0 0.40
token 2: E3 0.66, E5 0.34
token 3: E7 0.58, E0 0.42
token 4: E6 0.57, E7 0.43
token 5: E6 0.60, E4 0.40
parameters in one expert : 1,072
total parameters in the layer: 8,704
used for any single token : 2,272 (26.1%)
same layer, more experts, same work per token
experts total params active params fraction
8 8,704 2,272 26.10%
64 69,632 3,168 4.55%
256 278,528 6,240 2.24%
512 557,056 10,336 1.86%
real models (from each config.json on the Hub)
model experts top-k shared
Mixtral-8x7B-v0.1 8 2 0
DeepSeek-V3 256 8 1
Qwen3-30B-A3B 128 8 0
Qwen3-Next-80B-A3B 512 10 1
gpt-oss-20b 32 4 0Reading the output
Different tokens went to different experts. Token 2 chose E3 and E5; token 5 chose E6 and E4. That divergence, on an untrained router with random weights, is only routing noise. In a trained model it is the mechanism the whole design rests on.
Expert 7 appears in four of six tokens. Experts 1 and 2 appear in none. With a random router this is chance. In training, this exact pattern becomes self-reinforcing and is the failure mode of the entire technique — see load balancing and expert collapse.
The scaling table is the whole argument. From 8 experts to 512, total parameters grow 64x while active parameters grow 4.5x — and that growth is entirely the router, which must produce one score per expert. The expert compute per token is fixed at top_k experts, forever.
Real models sit far along that curve. Qwen3-Next-80B-A3B activates 10 of 512 experts. Naming a model "80B-A3B" is the industry's shorthand: 80 billion total, 3 billion active.
The loop is teaching code, not production code
for slot ... for e ... runs top_k * n_exp iterations with a boolean mask each time. It is readable and it is slow. Production kernels sort tokens by expert assignment once and then run one grouped matrix multiply per expert.
transformers uses a MixtralTopKRouter that computes softmax then topk then renormalises, exactly as above, and hands indices to a fused MixtralExperts module. The routing logic is what you see here; the dispatch is not.
What is and is not replaced
An MoE transformer layer is:
x -> attention (unchanged, still GQA or MLA) -> MoE feed-forward -> outAttention stays dense. Everything in Fast Attention and Long Context applies unchanged to an MoE model. Some models keep the first few layers dense: DeepSeek-V3's first_k_dense_replace: 3 means layers 0 to 2 use an ordinary feed-forward block, because routing is unstable in the earliest layers.
Common mistakes
Expecting a memory saving. Every expert is resident. A 671B-parameter model needs 671B parameters' worth of memory whatever its active count. MoE trades memory for compute, in that direction only.
Expecting interpretable specialisation. Published analyses find experts specialising by token identity, syntax and position far more than by human topic. Do not promise a "legal expert".
Forgetting to renormalise the top-k weights. Without weight / weight.sum(), output magnitude varies with how confident the router was, and training becomes unstable.
Comparing an MoE and a dense model by parameter count. Mixtral-8x7B has around 47B parameters and about 13B active. It behaves like neither a 47B nor a 13B dense model. Compare on measured quality and measured cost, never on the headline number.
Assuming any training framework handles it. MoE needs balancing losses, capacity handling and expert-parallel communication. Naive data-parallel training of the code above will run, and will collapse onto a few experts.
Try it yourself
Set top_k = 8 with n_exp = 8. Every expert now runs for every token, which is a dense layer with extra steps. Compare the active-parameter fraction. Then set top_k = 1 and see how much the output changes when one router decision flips.
What to learn next
- The router — how the choice is made, and why it is hard to train.
- Active parameters vs total parameters — reading model names honestly.
- Transformers — the architecture an MoE layer plugs into.
Researcher — Mathematics and papers.
Definition
Replace the feed-forward sublayer with $N$ experts ${E_i}$ and a gating function $G$:
$$ y = \sum_{i=1}^{N} G(x)_i \, E_i(x) $$
$G(x) \in \mathbb{R}^{N}$ is sparse: at most $k \ll N$ entries are non-zero, so only $k$ experts are evaluated. Shazeer et al. (2017), Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer (arXiv:1701.06538) introduced the sparse form for LSTM language models, reaching 137B parameters at a time when dense models were three orders of magnitude smaller.
The dense ancestor is Jacobs, Jordan, Nowlan and Hinton (1991), Adaptive Mixtures of Local Experts, where all experts are evaluated and the gate produces a convex combination. Sparsity is the modern contribution; the decomposition is thirty-five years old.
Cost model
Let $P_e$ be parameters per expert, $P_{\text{other}}$ the attention and embedding parameters, and $L_{\text{moe}}$ the number of MoE layers.
$$ P_{\text{total}} = P_{\text{other}} + L_{\text{moe}} N P_e, \qquad P_{\text{active}} = P_{\text{other}} + L_{\text{moe}} (k P_e + d N) $$
The $dN$ term is the router. FLOPs per token track $P_{\text{active}}$; memory tracks $P_{\text{total}}$. The design freedom is that $N$ enters memory linearly and compute only through the negligible router term.
Clark et al. (2022), Unified Scaling Laws for Routed Language Models fit scaling laws in which routed models follow a dense-equivalent parameter count that grows sublinearly in $N$, with returns diminishing beyond roughly 64 experts at the scales they studied. Later work with fine-grained experts pushed that ceiling substantially — see shared and fine-grained experts.
Lineage
| Year | Work | Contribution |
|---|---|---|
| 1991 | Jacobs et al. | Dense mixture of local experts |
| 2017 | Shazeer et al. | Sparse top-$k$ gating, noisy gating, load-balancing loss |
| 2020 | GShard (Lepikhin et al., arXiv:2006.16668) | Top-2 routing, expert capacity, automatic sharding at 600B |
| 2021 | Switch Transformer (Fedus, Zoph, Shazeer, arXiv:2101.03961) | Top-1 routing, simplified balancing loss, 1.6T parameters |
| 2022 | ST-MoE (Zoph et al., arXiv:2202.08906) | Router z-loss, stability and fine-tuning recipes |
| 2024 | Mixtral 8x7B (Jiang et al., arXiv:2401.04088) | First widely used open sparse model |
| 2024 | DeepSeekMoE (arXiv:2401.06066) | Fine-grained experts plus shared experts |
| 2024 | DeepSeek-V3 (arXiv:2412.19437) | 671B total, 37B active; auxiliary-loss-free balancing |
What experts learn
The evidence contradicts the marketing. Mixtral's own paper reports no clear topic-level specialisation, and finds strong consecutive-token routing locality instead: adjacent tokens are frequently sent to the same expert. Switch Transformer and ST-MoE report specialisation by shallow features — punctuation, numbers, conjunctions, proper nouns.
The mechanistic reading is that experts partition the representation space, not the subject space, and that the router is closer to a learned hash than to a triage nurse. Anyone designing evaluation around expected expert semantics should read those sections first.
Where the difficulty actually is
The forward pass above is fifteen lines. Everything hard about MoE is elsewhere:
- Balance. Nothing in the objective prevents one expert taking everything. Load balancing.
- Discreteness.
topkhas zero gradient with respect to unselected experts, so the router learns only from experts it already chose. The router. - Capacity. Fixed-size expert buffers mean tokens can be dropped. Expert capacity.
- Communication. Experts live on different devices, and every token must travel. Expert parallelism.
- Fine-tuning. MoE models overfit more readily than dense models of equal active size; ST-MoE devotes a section to it.
Each of those is a lesson in this section, and together they are the reason MoE took seven years from Shazeer's paper to become the default at the frontier.
What to learn next
- The router — how the choice is made, and why it is hard to train.
- Active parameters vs total parameters — reading model names honestly.
- Transformers — the architecture an MoE layer plugs into.