rumblr Work in progressWIP

● The AI Primer · Lesson 7 · Part 1: how the model works inside

The transformer block

the unit every modern LLM stacks

You'll be able to explain The block, a tiny GPT, parameter counts, mixture of experts

Members · open during launch 34 min12 figures and diagrams8 interactive
Guide is what to use and when. How it works builds it from scratch. Math & code adds the formulas and the Python.

The lesson in one minute

What you'll be able to explain

  1. A block is attention (tokens exchange information) then a feed-forward network (each token processes alone), each wrapped in a residual add and a layer norm (pre-norm).
  2. A GPT is: embed tokens and positions, run N blocks, normalize, and score every vocabulary entry with the (tied) embedding table.
  3. Parameters ≈ 12·L·d² + vocab·d; compute ≈ 2N per generated token and 6ND to train. MoE adds experts to grow parameters without growing per-token compute.

Level 1

The practitioner's guide

In one sentence

The transformer block (attention, then a feed-forward network that works on each token alone, each wrapped in a residual add and a normalization) is the unit every modern language model stacks, and reading a model's block count, width and expert layout off its card tells you its memory, its speed and its training cost before you download it.

When you need it

You never build a block; you meet its numbers. The day comes when you choose between a dense 70B model and one that calls itself "8x7B", size a GPU for a download, estimate what a fine-tune or a pretraining run will cost, pick an encoder or a decoder for an embedding or classification job, or open a config.json and need to turn num_hidden_layers, hidden_size, intermediate_size and num_local_experts into gigabytes and dollars. The rule of thumb behind all of it, from this lesson's gpt_param_count: parameters are about twelve square matrices per block plus the embedding table, 12 · L · d² + V · d. For GPT-2 small that gives 123,532,032, within 0.7% of the exact 124,439,808. When you call a hosted model by name, the vendor has already made these choices; the block then matters only as the reason you pay per token, and you can skip to the cost section.

Your options

The choices below are the ones a practitioner makes around the block: which family, dense or sparse, and how a parameter count becomes hardware. From the plainest to the most involved:

Option What it does What it gives you What it costs Where it lives
Encoder-only (BERT) Every token attends to every token, both directions One vector per token: classification, embeddings, tagging Cannot generate; a fixed maximum length Embedding and classifier models
Encoder-decoder (T5, the 2017 transformer) An encoder reads the source; a decoder writes the output while attending to it Translation and summarization with a clean split between reading and writing Two stacks to train and serve; rarely used for chat Sequence-to-sequence models
Decoder-only, dense (GPT, Llama, Claude) Each token sees only the past; every block's feed-forward network runs for every token Generation, chat, agents; the simplest to serve About 2N operations per generated token, and all N parameters in memory The model family you pick
Decoder-only, mixture of experts (Mixtral, DeepSeek-V3) Each block routes each token to k of E expert feed-forward networks Far more parameters per unit of compute: Mixtral 8x7B holds about 47B and runs about 13B per token; DeepSeek-V3 holds 671B and runs 37B Every expert must be loaded, so memory for all E and compute for k; a router and a balancing loss to keep experts busy The model family; num_local_experts, num_experts_per_tok
A smaller model trained longer The vendor picks N below the compute-optimal size and trains far past 20 tokens per parameter A model that is cheaper to serve forever: Llama 3 8B saw more than 15 trillion tokens More training compute up front, paid once by the vendor The card's training-token count
Quantization at load time Stores each parameter in fewer bits A 70B model at 4 bits is 35 GB instead of 140 GB and fits one 80 GB GPU Some quality loss, measured per model (primer.ml.inference) The serving stack

How to choose

Start from the job, then the hardware.

  • Generating text, chatting, calling tools: a decoder. Embeddings, classification, tagging: an encoder, or a decoder's final vectors (primer.ml.embeddings).
  • Dense or mixture of experts for a model you host: experts win when memory is plentiful and compute per token is the constraint (many concurrent users); dense wins when memory is tight, because Mixtral's 47B parameters must all be resident (about 94 GB at 16 bits) to run its 13B.
  • Sizing memory: parameters times bytes per parameter. 70B at 16 bits is 140 GB, more than one 80 GB GPU; at 4 bits it is 35 GB.
  • Estimating a training run: 6 · N · D. A 7B model on its compute-optimal 140 billion tokens costs 5.88 × 10²¹ operations, about 4,100 GPU-hours at a sustained 400 teraFLOP/s per GPU (primer.ml.pretraining).
  • Reading a config: blocks L, width d, feed-forward width (14,336 against 4,096 for Llama 3 8B, about 3.5×), vocabulary V, and the expert counts. With those you can reproduce the parameter count before downloading.
  • Whatever you pick, remember that the block is the same in all of them. The differences of kind are the attention mask and whether the feed-forward network is routed; everything else is size.

What it costs

Three currencies: memory, compute and, for experts, the gap between the two.

  • Memory. Parameters × bytes. GPT-2 ran from 124 million (12 blocks, width 768) to 1.56 billion (48 blocks, width 1,600); Llama 3 runs from 8B (32 blocks, width 4,096) through 70B (80 blocks, width 8,192) to 405B (126 blocks, width 16,384), each with 8 key-value heads. The feed-forward network is always the largest share, and the embedding table shrinks from 32% of GPT-2 small to 5% of XL as blocks multiply (param_breakdown).
  • Compute. About 2N operations per generated token (a 7B model: 1.4 × 10¹⁰, 14 GFLOPs) and about 6 · N · D to train (GPT-3: 3.15 × 10²³, matching the paper's 3.14 × 10²³). Llama 3 405B took 3.8 × 10²⁵ operations over 15.6 trillion tokens on up to 16,000 H100 GPUs, and DeepSeek-V3 reports 2.788 million H800 GPU-hours over 14.8 trillion tokens.
  • Experts. This lesson's 8 experts of width 16 hold 17,152 parameters while one token touches 4,384: the whole point, and the whole catch. You buy knowledge with memory and pay compute only for what each token uses.

What breaks

  • Reading "8x7B" as 56B or as 7B. It is neither: about 47B to load, because only the feed-forward networks are multiplied by eight (attention and embeddings are not), and about 13B to run per token. Size the GPU for the first number and the latency for the second.
  • Experts that starve. Even an untrained router is lopsided: in this lesson's run experts 3 and 5 receive 75 of 256 tokens each while expert 2 receives 51, and in training the imbalance compounds because busy experts improve and attract more traffic. The balancing loss is the fix; Hugging Face's Mixtral config keeps it on with router_aux_loss_coef at 0.001. Leave it on when you fine-tune an expert model.
  • Normalizing after instead of before. The 2017 layout put the norm after each sub-layer; pre-norm, with the norm before and the residual path untouched, trains more stably (Xiong et al., 2020) and is what modern models use. If you assemble blocks yourself, copy the modern order.
  • A stack with no skip path. Replace the residual add with plain replacement and a hundred blocks cannot train; the spec checks that a block with both sub-layers switched off returns its input unchanged.
  • The wrong mask for the job. An encoder cannot generate, and a decoder's per-token vectors only ever saw the past, which is why embedding models are usually encoders.
  • Forgetting the embedding table. For a small model it is not a rounding error: 38.6 million of GPT-2 small's 124 million parameters, 32% of the total.

In the wild

GPT-2 ships in the four sizes counted above, and this lesson's count lands on its exact 124,439,808 once the attention biases are included. Llama 3's herd (8B, 70B, 405B) uses RMSNorm, SwiGLU feed-forward networks and grouped-query attention inside the same block. Mixtral 8x7B and DeepSeek-V3 are the reference mixture-of-experts models, and the Switch Transformer paper (Fedus, Zoph and Shazeer, 2021) introduced the balancing loss built here. Hugging Face configs name the block's numbers directly: num_hidden_layers, hidden_size, intermediate_size, num_attention_heads, num_key_value_heads, vocab_size and, for experts, num_local_experts and num_experts_per_tok. The 6 · N · D rule comes from Kaplan et al. (2020) and the 20-tokens-per-parameter rule from Hoffmann et al. (2022). BERT is the canonical encoder and T5 the canonical encoder-decoder. The papers are linked at the end of the lesson.

Go deeper

Level 2 builds a block on a two-number token, normalizes (1, 2, 3, 4) by hand, runs one number through GELU, assembles a 27,328-parameter GPT, draws the three families' masks, counts GPT-2 to the exact parameter, routes a token through a mixture of experts with its balancing loss, and derives the 2N and 6N rules. If you only needed to read a model card or size a machine, you are done.

Level 2

How it works, from scratch

Level 2 assembles the block one piece at a time, starting with the picture of a team that meets, then works alone.

Chapter 1

The block: a meeting, then desk work

Move the two corrections yourself first; the residual connection will then read as what it is.

Everyday picture A team works in rounds. Each round starts with a meeting, where everyone listens to everyone else and takes notes on what's relevant to them (that's attention). Then comes desk work: each person goes back to their own desk and thinks through their notes alone, without talking to anyone (that's the feed-forward network). Nobody throws away their old notes; they only add to them (the residual connection). A large model runs dozens of these rounds: GPT-2 small has 12, big models have 80 or more.

Tiny worked example Take one token whose vector is x = (1, 2). The meeting produces a correction (0.1, −0.3); adding it gives (1.1, 1.7). Desk work then adds (−0.2, 0.4), giving (0.9, 2.1). The token's vector has been edited twice, never replaced.

Figure 1 · Diagram

Reading it: follow the main line straight down the left. The input is first normalized (rescaled, section 2) and fed to attention. Attention's output does not replace the input: the "Add" circle adds it to the original, which arrives by the side arrow that skips the whole step. The same pattern repeats for the feed-forward network. Those two skip arrows are the residual connections. Because every step only adds a correction, the signal (and during training, the gradient) has an unobstructed highway through a hundred blocks. This layout, with the norm before each sub-layer, is called pre-norm; it trains more stably than the 2017 original, which normalized after.

The math and the code

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the token vectors, one row per token (the "residual stream") (1, 2) for one token
"replace the left side with the right side", as in code: x = x + ...
layer normalization (section 2)
multi-head attention from primer.ml.attention: the meeting returns (0.1, −0.3)
the feed-forward network (section 3): desk work returns (−0.2, 0.4)
add the correction to the vector, number by number

In words: "add what the meeting found to each token's notes, then add what each token worked out alone."

With the numbers: (1, 2) + (0.1, −0.3) = (1.1, 1.7); then (1.1, 1.7) + (−0.2, 0.4) = (0.9, 2.1). TransformerBlock.__call__ is exactly these two lines.

Level 3: in Python
x = [1.0, 2.0]
# what Attn(LN(x)) returned
attn = [0.1, -0.3]
# x ← x + Attn(LN(x))
x = [x_j + a_j for x_j, a_j in zip(x, attn)]
[round(x_j, 1) for x_j in x]  # → [1.1, 1.7]
# what FFN(LN(x)) returned
ffn = [-0.2, 0.4]
# x ← x + FFN(LN(x))
x = [x_j + f_j for x_j, f_j in zip(x, ffn)]
[round(x_j, 1) for x_j in x]  # → [0.9, 2.1]

In code: TransformerBlock holds one primer.ml.attention.MultiHeadAttention, one FeedForward and the two norms' learned gains and biases.

Why it matters The block's output has the same shape as its input, so blocks stack like Lego. And because the input is never overwritten, switching both sub-layers off gives back the input unchanged (a scenario in the spec), which is why very deep stacks train at all.

Chapter 2

Layer normalization: grading each token on its own curve

Everyday picture A teacher grading on a curve: subtract the class average from each score, then divide by how spread out the scores are. After that, "2 above average" means the same thing in every class. LayerNorm does this to the numbers inside one token's vector, so no token's numbers can drift huge or tiny as they pass through dozens of blocks.

Tiny worked example Normalize (1, 2, 3, 4). The mean (average) is 2.5. The variance (average squared distance from the mean) is (2.25 + 0.25 + 0.25 + 2.25) / 4 = 1.25, so the spread (standard deviation, its square root) is 1.118. Result: (−1.342, −0.447, 0.447, 1.342). Multiply the input by 100 and the result is identical.

Figure 2 · Diagram

Reading it: two fixed steps, then one learned step. Centring and dividing force every token's numbers to average 0 with spread 1; the learned gain and bias then let the model pick whatever scale each feature actually needs. RMSNorm (used by Llama and most recent models) skips the centring box and divides by the root-mean-square instead: cheaper, and it works as well.

The math and the code

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
one token's vector (1, 2, 3, 4)
how many numbers it has 4
a counter over those numbers, 1 to
the -th number
"add up the following for = 1, 2, ..., "
(mu) the mean 2.5
(sigma squared) the variance 1.25
square root; is the spread 1.118
(epsilon) a tiny number (1e-5) so we never divide by zero 0.00001
(gamma, beta) learned gain and bias per feature 1 and 0 at the start

In words: "subtract the token's average from each of its numbers, divide by the token's spread, then rescale and shift each feature by learned amounts."

With the numbers: : (1 − 2.5) / √(1.25 + 0.00001) = −1.5 / 1.118 = −1.342, then × 1 + 0 = −1.342. layer_norm and rms_norm implement both variants.

Level 3: in Python
import math
x = [1, 2, 3, 4]
d = len(x)
# μ = (1/d) Σ x_j
mu = sum(x) / d
# σ² = (1/d) Σ (x_j − μ)²
sigma2 = sum((x_j - mu) ** 2 for x_j in x) / d
mu, sigma2  # → (2.5, 1.25)
# learned gain and bias start at 1 and 0
eps, gamma, beta = 1e-5, 1.0, 0.0
[round(gamma * (x_j - mu) / math.sqrt(sigma2 + eps) + beta, 3) for x_j in x]  # → [-1.342, -0.447, 0.447, 1.342]

Why it matters Without normalization, activations drift layer after layer until training blows up or stalls. It normalizes per token (not per batch, unlike the BatchNorm used in image networks), so it works for any batch size and sequence length.

Chapter 3

The feed-forward network: desk work

Everyday picture Back at their desk, each person spreads their notes out over a desk four times wider than the notebook, underlines what matters, and writes a short summary back into the notebook. Nobody talks to anyone.

Tiny worked example With model width 8, the network widens each token to 32 numbers, applies GELU to each, and narrows back to 8. Parameters: 8·32 + 32 + 32·8 + 8 = 552. GELU on single numbers: GELU(1) = 0.841, GELU(10) = 10.0, GELU(−3) = −0.004, GELU(0) = 0.

Figure 4 · Diagram

Reading it: a token's vector goes in on the left, alone. It is widened by a matrix multiply, passed through a smooth on/off switch (GELU) number by number, and narrowed back. The same weights are applied to every token separately, so this step never mixes tokens; it transforms each one using what attention already gathered. Two thirds of each block's parameters live here (8d² of every 12d²); counting the embedding table too, that is 45% of GPT-2 small and 63% of GPT-2 XL (chapter 6). Much of a model's factual knowledge is thought to be stored in these weights.

The math and the code A matrix multiply turns a list of numbers into a list of numbers: each output number is the dot product of with one column of (multiply matching numbers, add them up).

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
one token's vector 8 numbers
first weight matrix, widens 8 × 32
first bias, added after widening 32 numbers
second weight matrix, narrows 32 × 8
second bias 8 numbers
any single number fed to GELU 1
"hyperbolic tangent": an S-shaped curve from −1 to +1 = 0.664
pi, 3.14159...
"approximately equals": this tanh form is a fast stand-in for exact GELU

In words: "widen the token with a matrix multiply, softly switch off negative numbers, then narrow it back with another matrix multiply."

With the numbers: GELU(1) = ½ · 1 · (1 + tanh(0.798 · 1.045)) = ½ · (1 + tanh(0.834)) = ½ · (1 + 0.683) = 0.841. FeedForward and gelu are the code.

Level 3: in Python
import math
def gelu(z):
    return 0.5 * z * (1 + math.tanh(math.sqrt(2 / math.pi) * (z + 0.044715 * z ** 3)))
[round(gelu(z), 3) for z in (1, 10, -3, 0)]  # → [0.841, 10.0, -0.004, 0.0]
d = 8
# W1 (8 × 32), b1, W2 (32 × 8), b2
d * 4 * d + 4 * d + 4 * d * d + d  # → 552

Figure 3 · Drawn from the lesson's code

−4 −3 −2 −1 0 1 2 3 4 input to the activation 0 1 2 3 4 output GELU: a smooth ReLU ReLU: max(0, x) GELU

GELU tracks ReLU far from zero but bends smoothly through it and dips slightly below zero for small negatives

Reading it: the x-axis is the number going into the activation and the y-axis is what comes out. ReLU (grey) is a hard hinge: zero for every negative input, the identity for positives. GELU (blue) follows the same shape far from zero but bends smoothly around it and dips slightly below zero for small negatives. The smooth bend means the gradient never jumps, which is part of why transformers train well with it. (Many newer models use SwiGLU, a gated cousin of the same idea.)

Why it matters Without a nonlinearity like GELU, the two matrix multiplies would collapse into one, and stacking layers would add nothing.

The GELU curve above, with your own number on it, and the desk's parameter count as the width changes.

Chapter 4

A tiny GPT, end to end

Everyday picture An assembly line. Token ids go in one end, get turned into vectors, pass through a row of identical workstations (the blocks), get a final polish (the last norm), and come out the far end as a score for every word in the dictionary.

Tiny worked example A model with a 50-token vocabulary, width 32, 2 blocks and 16 positions. The ids [3, 1, 4, 1, 5] become a 5 × 32 grid, pass through both blocks as 5 × 32, and come out as a 5 × 50 grid of logits (raw scores): row 5 scores every possible 6th token. It has 27,328 parameters.

Figure 5 · Diagram

Reading it: the top-left path says what each token is, the bottom-left path where it is (see primer.ml.positional); their sum enters the blocks. Every block keeps the 5 × 32 shape. At the end, each token's final vector is dot-producted with every row of the token table, giving one score per vocabulary entry. The dotted line marks weight tying: the table that turned ids into vectors on the way in is reused to score them on the way out, saving vocab × width parameters.

The math and the code

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
the vectors leaving the last block 5 × 32
after the final layer norm 5 × 32
the token embedding table, one row per vocabulary entry 50 × 32
transposed: rows become columns 32 × 50
logits score of every vocabulary entry at every position 5 × 50

In words: "score each candidate next token by how well its embedding lines up with the model's final vector."

With the numbers: row 5 of the logits holds 50 scores, one per token id; softmax turns them into next-token probabilities (see primer.ml.big_picture). TinyGPT.__call__ is this pipeline. Shrink it to a width of 2 and a vocabulary of 3 to check by hand: a last position whose normalized vector is LN(h) = (1, −1), against table rows (1, 0), (0, 1) and (−1, 1), scores 1·1 + (−1)·0 = 1, then −1 and −2: token 0 lines up best.

Level 3: in Python
# LN(h) for the last position
ln_h = [1.0, -1.0]
# one row per vocabulary entry
E = [[1.0, 0.0], [0.0, 1.0], [-1.0, 1.0]]
# LN(h) Eᵀ: a dot product with each row
[sum(h_k * e_k for h_k, e_k in zip(ln_h, row)) for row in E]  # → [1.0, -1.0, -2.0]

In code: TinyGPT.hidden runs everything up to the final norm, and TinyGPT.n_params counts the 27,328 weights.

Why it matters This is the whole forward pass of GPT-2, Llama and Claude-style models in miniature. Real models differ in size, not in kind.

The assembly line with sliders on it, and the hand check you can drag.

Chapter 5

Three families: encoder, decoder, encoder-decoder

See the one choice that separates the families before reading about them.

Everyday picture An editor reads the whole page before commenting on any sentence (encoder). A storyteller tells the story word by word and can't peek at words they haven't said yet (decoder). A translator reads the whole source sentence, then writes the translation word by word (encoder-decoder).

Tiny worked example With 3 tokens, an encoder's attention may use all 9 (query, key) pairs; a decoder only the 6 on or below the diagonal. In the spec, changing token 5 changes token 1's output in an encoder block, and leaves it untouched in a decoder block.

Figure 7 · Diagram

Reading it: the three boxes differ only in who may attend to whom. The blocks inside are the same. That one choice of mask decides what the model is good at: seeing everything suits understanding tasks, and seeing only the past is what makes generation possible.
Type Example Attention Best for
Encoder-only BERT Bidirectional Classification, embeddings, NER
Decoder-only GPT, Claude, Llama Causal Generation, chat, agents
Encoder-decoder T5, original transformer Both Translation, summarization

Figure 6 · Drawn from the lesson's code

0 2 4 key (token looked at) 0 1 2 3 4 5 query (token looking) encoder (bidirectional) 0 2 4 key (token looked at) 0 1 2 3 4 5 query (token looking) decoder (causal) 0.0 0.2 0.4 0.6 0.8 1.0 attention weight

The encoder spreads attention over the whole grid; the decoder's upper-right triangle is empty, and only the last row matches

Reading it: both panels are attention weights from one block on the same 6 input vectors; rows are the token looking, columns the token looked at, darker is more weight. On the left (encoder) weight is spread over the whole grid. On the right (decoder) the upper-right triangle is empty, so nothing reads the future. Only the bottom row is identical in both panels: the last token sees everything either way, so the mask changes nothing for it. Every other decoder row differs, because its weights are shared out over only the tokens it may see: the first token, seeing only itself, puts all of its weight (1.0) there.

In code: TransformerBlock with its causal flag on is a decoder block and with it off an encoder block; mask_patterns runs one of each on the same input to draw the figure.

Why it matters Embedding models (primer.ml.embeddings) are usually encoders; chat models and agents are decoders.

Chapter 6

Counting parameters

Everyday picture Counting the bricks in a Lego model from its blueprint, without opening the box.

Tiny worked example For width d, one block holds: attention 4·d² (W_q, W_k, W_v, W_o), feed-forward 8·d² (two d × 4d matrices), plus small bias and norm terms. With d = 32: 12 · 1,024 = 12,288, plus 160 (biases) plus 128 (norms) = 12,576 per block. Two blocks, plus tables of 50 × 32 and 16 × 32 and a final norm of 64, gives 27,328. The same recipe on GPT-2 small (d = 768, 12 blocks, 50,257 tokens, 1,024 positions) gives 124,402,944. GPT-2 also puts a bias on its attention projections, which the tiny model leaves out: 4·d = 3,072 more per block, 36,864 in all, and that brings it to exactly its published 124,439,808.

Figure 9 · Diagram

Reading it: almost all of a block's weight sits in two places, attention's four square matrices and the feed-forward's two wide ones. Everything else is a rounding error at large width, which is where the famous 12·L·d² rule comes from. Embeddings are added once, not per block.

The math

Level 3: the formula and its symbols

Symbols

Symbol Meaning here GPT-2 small
total parameters 124,439,808
number of blocks (layers) 12
model width 768
times : the size of one square matrix 589,824
vocabulary size 50,257
the token embedding table 38,597,376

In words: "about twelve square matrices per layer, plus the embedding table."

With the numbers: 12 · 12 · 589,824 = 84,934,656, plus 38,597,376 = 123,532,032, about 0.7% under the exact 124,439,808 (which also counts positions, biases and norms; see gpt_param_count).

Level 3: in Python
L, d, V = 12, 768, 50_257
# 12·L·d² and V·d
blocks, table = 12 * L * d ** 2, V * d
print(f"{blocks:,} + {table:,} = {blocks + table:,}")  # → 84,934,656 + 38,597,376 = 123,532,032
# the share it leaves out
round((124_439_808 - (blocks + table)) / 124_439_808, 3)  # → 0.007

Figure 8 · Drawn from the lesson's code

small medium large XL GPT-2 size 0 200 400 600 800 1000 1200 1400 1600 parameters (millions) Where the parameters live 124M 355M 774M 1,558M embeddings attention feed-forward norms

Feed-forward is always the largest share; embeddings fall from 32% of GPT-2 small to 5% of XL

Reading it: each bar is one GPT-2 size, split by where the parameters live. Feed-forward (red) is always the largest block of the stack. In the small model, embeddings (grey) are almost a third of everything, because a 50k-row table is big next to 12 narrow layers. As models grow, the embeddings stay fixed while layers multiply, so their share shrinks to about 5% in XL, and the 12·L·d² term dominates.

In code: param_breakdown splits a GPT-2-shaped model's count into embeddings, attention, feed-forward and norms for the figure, and TransformerBlock.n_params counts one real block's weights.

Why it matters Parameters × bytes per parameter is the memory a model needs just to load (see primer.ml.inference for the arithmetic).

Chapter 7

Mixture of Experts (MoE): a triage desk

Run the triage desk first: four scores in, two experts out.

Everyday picture A hospital triage desk. Instead of one general practitioner seeing every patient, the desk sends each patient to the two most relevant specialists out of eight. The hospital employs eight doctors' worth of expertise, but each patient only takes up two doctors' time.

Tiny worked example A router scores one token against 4 experts: (2.0, 1.0, 0.5, −1.0). Keep the top 2 (experts 0 and 1) and softmax just those two: e² / (e² + e¹) = 7.39 / 10.11 = 0.731, and 0.269. The token's output is 0.731 × expert 0's output + 0.269 × expert 1's output; experts 2 and 3 never run for this token.

Figure 11 · Diagram

Reading it: the router is a tiny linear layer that scores the token against every expert. Only the two best-scoring experts run, and their outputs are blended by the renormalized scores (solid arrows); the others are skipped entirely (dotted). An MoE layer replaces the feed-forward network inside a block; attention is unchanged.

The math and the code

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
one token's vector
the router's weights, width × number of experts
the router scores (2.0, 1.0, 0.5, −1.0)
the positions of the largest scores experts {0, 1} for k = 2
just those scores (2.0, 1.0)
expert 's gate: softmax over the kept scores 0.731, 0.269
expert (a feed-forward network) applied to
add up over the chosen experts only two terms

In words: "score every expert, keep the best k, and blend those experts' outputs by their softmaxed scores."

With the numbers: y = 0.731 · E₀(x) + 0.269 · E₁(x). With 8 experts of width 16, the layer holds 8 × 2,128 + 128 = 17,152 parameters, but one token touches only 2 × 2,128 + 128 = 4,384. MixtureOfExperts and top_k_gates are the code. Mixtral 8x7B works the same way: about 47B parameters in total, about 13B active per token.

Level 3: in Python
import math
r, k = [2.0, 1.0, 0.5, -1.0], 2
# TopK(r)
top_k = sorted(range(len(r)), key=lambda i: r[i], reverse=True)[:k]
top_k  # → [0, 1]
exps = [math.exp(r[i]) for i in top_k]
# g: softmax over the kept scores only
[round(e / sum(exps), 3) for e in exps]  # → [0.731, 0.269]
d, n_experts = 16, 8
# one expert is one feed-forward network
expert = d * 4 * d + 4 * d + 4 * d * d + d
# W_r
router = d * n_experts
# one, all held, touched per token
expert, n_experts * expert + router, k * expert + router  # → (2128, 17152, 4384)

A router left alone tends to play favourites, overloading some experts while others starve. Training adds a small load-balancing loss (Switch Transformer):

Level 3: the formula and its symbols

Symbols

Symbol Meaning here Balanced example Collapsed example
number of experts 4 4
share of routing slots that went to expert ¼ each (1, 0, 0, 0)
expert 's average router probability ¼ each ≈ (1, 0, 0, 0)
the penalty added to the training loss 1.0 ≈ 4.0

In words: "number of experts times the sum, over experts, of traffic share times average router probability."

With the numbers: balanced: 4 · (4 · ¼ · ¼) = 1.0, the minimum. Collapsed onto one expert: 4 · (1 · 1) = 4.0, the maximum (load_balancing_loss).

Level 3: in Python
n = 4
def balance(f, P):
    # n Σ f_i P_i
    return n * sum(f_i * P_i for f_i, P_i in zip(f, P))
# every expert gets a quarter
balance([0.25] * n, [0.25] * n)  # → 1.0
# everything goes to expert 0
balance([1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0])  # → 4.0

Figure 10 · Drawn from the lesson's code

0 1 2 3 4 5 6 7 expert 0 10 20 30 40 50 60 70 tokens routed to it (of 256, top-2) An untrained router plays favourites perfectly even (64 each)

An untrained router is lopsided: experts 3 and 5 get 75 tokens each, expert 2 only 51, against an even 64

Reading it: each bar counts how many of 256 tokens an untrained router sent to each of 8 experts (top-2, so 512 slots in all); the dashed line is the perfectly even 64. Experts 3 and 5 get 75 tokens each while expert 2 gets only 51: even random routing is lopsided, and in training the imbalance compounds because favoured experts improve and attract more traffic. The balancing loss pushes the bars back towards the line.

In code: MixtureOfExperts.n_params and MixtureOfExperts.active_params give the 17,152 and 4,384 above, and MixtureOfExperts.tokens_per_expert counts the bars in the figure.

Why it matters MoE lets a model hold far more knowledge (parameters) for the same compute per token, which is why many frontier models use it. The price is memory: every expert must be loaded even though few run per token.

Chapter 8

Compute arithmetic: 2N to generate, 6N to train

Everyday picture Every parameter does one multiply and one add for every token that passes through it, like a toll booth that charges two coins per car.

Tiny worked example A 7B-parameter model generating one token: 2 × 7e9 = 1.4e10 operations (14 GFLOPs). Training a 70B model on a trillion tokens: 6 × 70e9 × 1e12 = 4.2e23 operations.

Figure 12 · Diagram

Reading it: inference only runs the forward pass, so it pays 2 per parameter per token. Training runs the same forward pass, then a backward pass (primer.ml.neural_net) that costs about twice as much, because it computes gradients both for the weights and for the activations. 2 + 4 = 6.

The math

Level 3: the formula and its symbols

Symbols

Symbol Meaning here In the example
number of parameters 7e9 / 70e9 / 175e9
number of training tokens 1e12 / 300e9
compute in FLOPs (floating-point operations: one multiply or one add)
in 7e9 "times 10 to the power": 7e9 = 7,000,000,000

In words: "generating costs two operations per parameter per token; training costs six per parameter per training token."

With the numbers: GPT-3: 6 × 175e9 × 300e9 = 3.15e23 FLOPs, matching the roughly 3.14e23 its paper reports (training_flops, inference_flops_per_token).

Level 3: in Python
def C_infer(N):
    # per generated token
    return 2 * N
def C_train(N, D):
    # over all D training tokens
    return 6 * N * D
print(f"{C_infer(7e9):.2g}  {C_train(70e9, 1e12):.2g}  {C_train(175e9, 300e9):.3g}")  # → 1.4e+10 4.2e+23 3.15e+23

Why it matters These two lines let you estimate GPU-hours, serving cost and training budgets on the back of an envelope.

Test yourself

8 questions

Answer each one out loud or on paper before you open it. If you can explain it, you know it.

Question 1What do attention and the feed-forward network each contribute?Think it through, then reveal

Attention mixes information across tokens; the feed-forward network transforms each token independently and holds most of the parameters (and, it's thought, much of the factual knowledge).

Question 2Why residual connections?Think it through, then reveal

Each sub-layer adds a correction instead of replacing its input, so signal and gradients have a direct path through very deep stacks.

Question 3Pre-norm vs. post-norm?Think it through, then reveal

Pre-norm normalizes before each sub-layer and leaves the residual path untouched; it trains more stably, so modern models use it.

Question 4Why LayerNorm rather than BatchNorm in transformers?Think it through, then reveal

It normalizes within each token, so it doesn't depend on batch size or sequence length.

Question 5Roughly how many parameters does a model with 32 layers of width 4096 have, not counting embeddings?Think it through, then reveal

12 · 32 · 4096² ≈ 6.4 billion.

Question 6Encoder-only vs. decoder-only?Think it through, then reveal

Encoders attend bidirectionally and suit classification and embeddings; decoders attend causally and generate text.

Question 7What does Mixture of Experts buy you, and what does it cost?Think it through, then reveal

More total parameters at the same compute per token, since each token runs only k experts. It costs memory (all experts loaded), routing complexity and load-balancing.

Question 8Estimate the compute to train a 7B model on 2 trillion tokens.Think it through, then reveal

6 × 7e9 × 2e12 = 8.4e22 FLOPs.

Primary sources

The papers behind this lesson

Vaswani et al. (2017), Attention Is All You Need.

Introduced the transformer block: attention plus feed-forward, residuals and layer norm, stacked.

Read on rumblr →The paper ↗
Ba, Kiros & Hinton (2016), Layer Normalization.

Normalized within each example instead of across the batch, the variant transformers use.

Read the annotated companion →The paper ↗
Zhang & Sennrich (2019), Root Mean Square Layer Normalization.

Dropped the mean-centring for a cheaper norm now used by most LLMs.

The paper ↗
Hendrycks & Gimpel (2016), Gaussian Error Linear Units (GELUs).

The smooth activation used in GPT-2 and BERT.

The paper ↗
Xiong et al. (2020), On Layer Normalization in the Transformer Architecture.

Explained why pre-norm trains more stably than post-norm.

The paper ↗
Devlin et al. (2018), BERT.

The canonical encoder-only model.

The paper ↗
Fedus, Zoph & Shazeer (2021), Switch Transformers.

Scaled Mixture of Experts and introduced the load-balancing loss built here.

The paper ↗
Jiang et al. (2024), Mixtral of Experts.

An open top-2-of-8 MoE model: about 47B parameters, about 13B active per token.

The paper ↗
Kaplan et al. (2020), Scaling Laws for Neural Language Models.

Popularized the 6·N·D compute estimate.

Read the annotated companion →The paper ↗

Researcher's shelf

Further reading

  • The Annotated Transformer (Harvard NLP): https://nlp.seas.harvard.edu/annotated-transformer/
  • The Illustrated Transformer (Jay Alammar): https://jalammar.github.io/illustrated-transformer/
  • Andrej Karpathy, Let's build GPT (video): https://www.youtube.com/watch?v=kCc8FmEb1nY
  • Karpathy's nanoGPT: https://github.com/karpathy/nanoGPT
  • Hugging Face LLM course, How do Transformers work?: https://huggingface.co/learn/llm-course/chapter1/4
  • Hugging Face blog, Mixture of Experts Explained: https://huggingface.co/blog/moe

About this lesson. This is the illustrated edition of a lesson from the open-source AI Primer. Its text, figures and numbers are generated from the Primer's source at commit 048aeaa, so the two always agree: the explanation, the code that builds it and the tests that prove it.