Index Ai Academy
Phase 5 · Transformers · Scale & inference

Mixture of experts

Route each token to a few specialist sub-networks.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Many of the largest open models are built as a mixture of experts. DeepSeek-V3, Mixtral, Llama 4 and Qwen's biggest models all are. Several frontier labs are widely believed to use the idea too. It lets a model hold hundreds of billions of parameters but run each token through only a small share of them. That explains spec sheets that list two parameter counts. It also explains why some models are cheap per token but still need a lot of GPU memory.

What you'll be able to do

  • Explain how a mixture-of-experts layer routes each token to a few expert MLPs
  • Distinguish total parameters from active parameters, and what each costs
  • Train a small MoE layer and see why routing needs a load-balancing loss
  • Read MoE model specs such as '671B total, 37B active'

What it covers

  1. Many MLPs, a few at a time
  2. Reading real model specs
  3. Routing has to be learned, and kept fair
  4. Worked example: three balance scores
  5. Watch collapse happen
  6. What experts learn

Many MLPs, a few at a time

Picture a big restaurant kitchen with eight cooks. A host reads each order and sends it to the two cooks best suited to it. The other six cooks rest. The kitchen can make a huge range of dishes, but each order only costs two cooks' time.

A mixture-of-experts (MoE) layer works the same way inside a transformer block.

You met the block's MLP in the transformer block lesson. It is the small network that works on each token (a word or piece of a word) on its own. It holds about two-thirds of the block's parameters. An MoE layer swaps that one MLP for several MLPs, called experts. It adds a small router to pick between them.

  • The router is a single linear layer. For each token, it gives every expert a score. A softmax turns those scores into probabilities that add up to 1.
  • Top kk. Each token goes only to the kk experts with the highest probabilities. Here kk is a small whole number, often 1 or 2. Some models pick 8 out of 256.
  • Mixing. The chosen experts each produce an output. The layer adds them up, each one scaled by its router probability.

Attention stays as it was. Only the MLP changes.

In plain words: the router looks at a token and picks the few experts that suit it best. Only those experts do any work on that token.

Here's a map of the code below before you read it.

  • self.router is the linear layer that scores the experts.
  • self.experts is a list of 8 ordinary MLPs.
  • In forward, probs.topk(self.k) keeps the best k experts for each token. The loop then runs each expert only on the tokens that chose it.
  • The last lines count parameters. "Used per token" means the router plus k experts.
import torch
import torch.nn as nn

class MoE(nn.Module):
    def __init__(self, d, n_experts, k, hidden):
        super().__init__()
        self.k = k
        self.router = nn.Linear(d, n_experts, bias=False)
        self.experts = nn.ModuleList([nn.Sequential(nn.Linear(d, hidden), nn.GELU(), nn.Linear(hidden, d))
                                      for _ in range(n_experts)])

    def forward(self, x):                                   # x: (tokens, d)
        probs = torch.softmax(self.router(x), dim=-1)       # how well each expert suits each token
        weights, chosen_idx = probs.topk(self.k, dim=-1)    # keep the k best experts per token
        out = torch.zeros_like(x)
        for e, expert in enumerate(self.experts):
            for slot in range(self.k):
                chosen = chosen_idx[:, slot] == e           # tokens sending this slot to expert e
                if chosen.any():                            # the expert runs only on those tokens
                    out[chosen] += weights[chosen, slot].unsqueeze(-1) * expert(x[chosen])
        return out, chosen_idx

torch.manual_seed(0)
d = 32
moe = MoE(d, n_experts=8, k=2, hidden=4 * d)
x = torch.randn(64, d)                                     # 64 tokens
out, chosen_idx = moe(x)
print("output:", tuple(out.shape))
print("token slots per expert:", torch.bincount(chosen_idx.flatten(), minlength=8).tolist())

per_expert = sum(p.numel() for p in moe.experts[0].parameters())
router = moe.router.weight.numel()
total = sum(p.numel() for p in moe.parameters())
active = router + moe.k * per_expert
print(f"total parameters: {total:,}   used per token: {active:,} ({active / total:.0%})")
print(f"a single dense MLP of the same shape: {per_expert:,}")
It runs right here in your browser, with nothing to install.

The parameter count is the point. One expert has 8,352 parameters. The router has 256. So the whole layer holds 8 × 8,352 + 256 = 67,072 parameters. A token only uses the router and 2 experts: 256 + 2 × 8,352 = 16,960. That's about 25% of the layer.

So the layer holds 8 times the parameters of one MLP, but each token pays for only 2 experts. The work per token is about twice a single MLP's. The room to store patterns is eight times larger. That's the trade:

  • Total parameters set how much the model can store, and how much memory it needs.
  • Active parameters are the ones each token actually uses. They set the cost of each token.

An ordinary model, where every token uses every parameter, is called dense. An MoE model with 12B active parameters costs about as much per token as a 12B dense model.

(torch here is the course's in-browser PyTorch subset. Real systems group each expert's tokens far more efficiently than this loop does.)

Quick check
In the code above, 64 tokens each go to k = 2 experts. What do the 8 numbers in "token slots per expert" add up to?
  1. 128
  2. 16
  3. 64
  4. 8
Show the answer

128

Each of the 64 tokens fills 2 slots, one per chosen expert, so the counts add up to 64 × 2 = 128.

Reading real model specs

Here are three real MoE models. "Experts per layer" is how many experts each MoE layer has. "Used per token" is kk.

ModelExperts per layerUsed per tokenTotal parametersActive per token
Mixtral 8x7B (2023)8246.7B12.9B
DeepSeek-V3 (2024)256 + 1 shared8 + 1671B37B
Llama 4 Maverick (2025)128 + 1 shared1 + 1400B17B

DeepSeek-V3 and Llama 4 also have a shared expert. Every token uses it, on top of the experts the router picks.

Mixtral's name can mislead. "8x7B" sounds like 56B parameters, but the total is 46.7B. The experts only replace the MLPs. All eight share the same attention layers and embeddings, so those are counted once.

Try it now

This is how to read "671B total, 37B active". Memory follows the total, and compute follows the active number.

Quick check
A model card says '120B total parameters, 12B active'. What does that mean for running it?
  1. Only 12B parameters were trained
  2. It runs at the speed of a 120B dense model
  3. Compute per token is like a ~12B dense model, but all 120B parameters must be stored in memory
  4. It needs only enough memory for 12B parameters
Show the answer

Compute per token is like a ~12B dense model, but all 120B parameters must be stored in memory

Any token might be routed to any expert, so every expert must be loaded in memory. Each token only uses a few of them.

Routing has to be learned, and kept fair

Nobody tells the router which expert to pick. It learns, along with the rest of the model. Each expert's output is scaled by its router probability. So when training adjusts the weights, the router learns which experts helped with which tokens.

Go deeper How the router gets its training signal

The router's probabilities multiply the experts' outputs. That means the loss depends on them, and backpropagation can work out a gradient for the router's weights. If raising an expert's probability for a token would have lowered the loss, the router is nudged to raise it next time.

Left alone, though, routing tends to collapse. Think of a supermarket with four checkout lanes. One cashier happens to be quick on the first morning, so more shoppers join that lane. With all that practice, the cashier gets even quicker, and the lane gets even longer. The other cashiers stand idle.

Experts do the same. An expert that does well early gets more tokens. It improves faster, so it attracts even more. The others sit unused.

To stop this, MoE training adds a load-balancing loss. It's an extra score added to the training loss, and it's smallest when tokens are spread evenly across the experts. Here is the version from Google's Switch Transformer. It uses three symbols. Here they are, one at a time:

Quick check
What goes wrong without a load-balancing loss?
  1. Training gets faster but less accurate
  2. Every expert gets identical weights
  3. The model forgets its vocabulary
  4. Routing collapses: a few experts end up with almost all the tokens
Show the answer

Routing collapses: a few experts end up with almost all the tokens

An expert that does well early gets more tokens, improves faster and attracts more, like a supermarket with one busy lane.

  • EE is the number of experts.
  • fif_i is the fraction of tokens whose top choice is expert ii.
  • PiP_i is the router's average probability for expert ii, across all the tokens.
balance=E∑i=1Efi Pi\text{balance} = E \sum_{i=1}^{E} f_i \, P_i

The ∑\sum sign (sigma) means "add up, for every expert ii from 1 to EE".

In plain words: for each expert, multiply its share of the tokens by its average probability. Add those up, then multiply by the number of experts. The result is 1 when tokens are spread evenly. It rises toward EE when everything goes to one expert.

Try it with 4 experts. If every expert gets a quarter of the tokens and a quarter of the probability, each expert adds 0.25 × 0.25 = 0.0625. Four of those make 0.25, and 4 × 0.25 = 1. If every token goes to expert 0 with probability 1, only expert 0 adds anything: 1 × 1 = 1. Then 4 × 1 = 4, the worst score.

In Python, the sum is a loop over the experts:

E = 4
f = [0.25, 0.25, 0.25, 0.25]
P = [0.25, 0.25, 0.25, 0.25]
print(E * sum(fi * pi for fi, pi in zip(f, P)))   # 1.0
Go deeper Why this score gets the router to spread out

The counts fif_i come from hard choices, so they can't pass a gradient back to the router. The average probabilities PiP_i can. The loss is bigger when an expert with a large share of the tokens also gets a high probability. So training lowers the probability of overloaded experts, and tokens drift toward the quieter ones.

Quick check
Two experts. Every token's top choice is expert 0, and the router's average probabilities are 0.9 and 0.1. What does this print?
E = 2
f = [1.0, 0.0]
P = [0.9, 0.1]
print(E * sum(fi * pi for fi, pi in zip(f, P)))
  1. 1.0
  2. 2.0
  3. 0.9
  4. 1.8
Show the answer

1.8

Only expert 0 adds anything: 1.0 × 0.9 = 0.9. Multiply by E = 2 to get 1.8. That's close to the worst score of 2, because the tokens have piled onto one expert.

Worked example

Worked example: three balance scores

Four experts, three situations. First, a perfectly even spread. Second, everything sent to expert 0. Third, half the tokens go to expert 0 and half to expert 1, and the router puts 0.4 probability on each of those and 0.1 on the other two. Predict each score before you run it.

def balance(f, P):
    E = len(f)
    return E * sum(fi * pi for fi, pi in zip(f, P))

print(balance([0.25, 0.25, 0.25, 0.25], [0.25, 0.25, 0.25, 0.25]))
print(balance([1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0]))
print(balance([0.5, 0.5, 0.0, 0.0], [0.4, 0.4, 0.1, 0.1]))
In the lesson you can change this and run it.

Even spread: each expert adds 0.25 × 0.25 = 0.0625. Four of them make 0.25, and 4 × 0.25 = 1.0.

All on expert 0: only expert 0 adds anything, 1 × 1 = 1. Times 4 gives 4.0, the worst possible.

Two busy experts: experts 0 and 1 each add 0.5 × 0.4 = 0.2. The idle experts add 0, because their share of tokens is 0. So the sum is 0.4, and 4 × 0.4 = 1.6. That's between the best and the worst, so the loss still pushes training to use experts 2 and 3.

Watch collapse happen

Here's a small experiment. The inputs come from four groups. Each group needs a different function to get the right answer. A 4-expert MoE learns them, sending each token to its single best expert (top-1, as in Switch Transformer). It trains twice: once without the balance loss, once with it.

In the code, balance_weight sets how much the balance loss counts. A weight of 0 switches it off. For each group, the code prints how many of its tokens went to each of the 4 experts.

import torch
import torch.nn as nn

class MoE(nn.Module):
    def __init__(self, d, n_experts, hidden):
        super().__init__()
        self.n = n_experts
        self.router = nn.Linear(d, n_experts, bias=False)
        self.experts = nn.ModuleList([nn.Sequential(nn.Linear(d, hidden), nn.GELU(), nn.Linear(hidden, d))
                                      for _ in range(n_experts)])

    def forward(self, x):                                   # top-1 routing, as in Switch Transformer
        probs = torch.softmax(self.router(x), dim=-1)
        weight, choice = probs.max(dim=-1)
        out = torch.zeros_like(x)
        for e, expert in enumerate(self.experts):
            chosen = choice == e
            if chosen.any():
                out[chosen] += weight[chosen].unsqueeze(-1) * expert(x[chosen])
        f = torch.bincount(choice, minlength=self.n).float() / len(x)
        balance = self.n * (f * probs.mean(dim=0)).sum()
        return out, choice, balance

def experiment(balance_weight):
    torch.manual_seed(0)
    centers = torch.randn(4, 8) * 3                         # four groups of inputs...
    maps = torch.randn(4, 8, 8)                             # ...each with its own target function
    group = torch.arange(400) % 4
    x = centers[group] + torch.randn(400, 8) * 0.5
    y = (x.unsqueeze(1) @ maps[group]).squeeze(1) / 3
    moe = MoE(8, n_experts=4, hidden=16)
    opt = torch.optim.Adam(moe.parameters(), lr=1e-2)
    for step in range(301):
        out, choice, balance = moe(x)
        error = ((out - y) ** 2).mean()
        loss = error + balance_weight * balance
        opt.zero_grad()
        loss.backward()
        opt.step()
    print(f"balance weight {balance_weight}: error {error.item():.3f}, balance {balance.item():.2f}")
    for g in range(4):
        print(f"  group {g} → tokens per expert {torch.bincount(choice[group == g], minlength=4).tolist()}")

experiment(0.0)
experiment(1.0)

Both runs learn to send each group to one expert. Routing by what the input looks like appears on its own, with no labels.

Without the balance loss, one expert ends up handling two groups while another gets nothing. The error is higher. With the balance loss, each group gets its own expert and the balance score is 1.00. The error is lower, because no expert is wasted.

In real models, balance matters for speed too. Experts usually live on different GPUs. An overloaded expert makes all the others wait.

Go deeper Capacity limits and bias balancing

Systems cap how many tokens one expert accepts per batch. That cap is the expert's capacity. Tokens over the cap are sent elsewhere or dropped. Newer models such as DeepSeek-V3 balance the load a different way. They adjust small bias numbers in the router instead of adding a loss.

Quick check
Why do experts that start out a little better tend to take over when there is no balance loss?
  1. The router is told in advance which expert is best
  2. Attention sends all tokens to the first expert
  3. Experts that get fewer tokens are deleted during training
  4. They get more tokens, so they train more and improve faster, which wins them even more tokens
Show the answer

They get more tokens, so they train more and improve faster, which wins them even more tokens

It's a rich-get-richer loop, like the busy checkout lane. The balance loss pushes back toward an even spread.

What experts learn

The name suggests a "marketing expert" and a "coding expert". Studies of real MoE models find something less tidy. In Mixtral's paper, routing showed no clear split by subject such as math, biology or philosophy. It depended more on grammar and the type of token. The same expert was often picked for tokens that come one after another.

So experts are a cheap way to add capacity. They aren't a team of specialists you can call on by name.

The trade-offs

  • Cheaper compute per token for a given total size. Generation is faster and cheaper. Training also gets more capacity for each unit of computing work (each training FLOP).
  • More memory. Every expert must be loaded, because any token might pick any expert. Serving needs as much GPU memory as a dense model of the total size.
  • Harder training and serving. You have to balance the load and pass data between GPUs that hold different experts. MoE models also have training instabilities that dense models don't have.

For marketers: when you compare models by price and speed, MoE explains why a model with a huge parameter count can still be fast and inexpensive per token. What you pay for at run time is mostly the active parameters.

Practice what you just read

  • 4 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Why do MoE models add a load-balancing loss?

  1. Without it, routing tends to pile tokens onto a few experts, wasting the others and slowing training
  2. To make every token use every expert, so that nothing the model has learned goes unused during generation
  3. To reduce the model's memory use, since unused experts can be removed once training has balanced the rest
  4. To keep the attention weights balanced, so that no single token ends up dominating what the others can see

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson