Now an entry off the diagonal. Its sign says which way goes when another score grows.
Backward through attention
How does the gradient get back through attention, and what must training keep in GPU memory for it?
Side trip · best after level 17 · The whole Transformer
A short test to skip this level
Solve these 6 questions on your own. Answer all of them correctly and the level counts as cleared with three stars, and every part of the page opens. Showing an answer doesn’t count.
Enter keeps the indent · Tab indents · Esc then Tab leaves the editor · ⌘/Ctrl + Enter runs
Warm-up2 questions from earlier levels
A quick review before you start. Optional. Nothing here locks the level.
This level uses the backward rules of U3 and the number formats of U2. If you skipped them, read them quickly first.
The full model trains with one line, loss.backward(). In U3 you built that line yourself for single numbers:
every operation knows its own backward rule, and the chain rule connects the rules.
Attention needs two rules that U3 didn’t have: one for softmax, and one for a matrix product.
This level writes them, takes one attention head backward by hand, and checks the result against numeric slopes. Then it counts what the backward pass costs: every value a backward rule needs must be kept in GPU memory from the forward pass until the backward pass uses it.
The level’s demo.py prints every number on this page. To run it, download it into a folder of its own
and run python demo.py there (setup).
1. Softmax backward
Softmax turns scores into probabilities :
Backward receives , the gradient of the loss with respect to each , and must return . The new part: every score moves every probability, because all of them share the same denominator (the sum under the fraction line). If grows, grows. and shrink, so the total stays 1.
So the slopes form a table. Row , column holds : how fast moves when moves. This table of all slopes is the Jacobian . For softmax it has a short formula:
- on the diagonal, ;
- off the diagonal, .
Both lines together: . Here is the table with on the diagonal and 0 elsewhere,
and (np.outer(p, p)) is the table of every product . Vectors are rows in this course, so is
(n, 1) @ (1, n) → (n, n).
Take . Then , the sum is 4, and .
Softmax backward: one Jacobian, or one short formula
Type new scores z or a new incoming gradient dp. Both ways of computing dz always give the same numbers.
| 0.50 | 0.25 | 0.25 |
| z0 | z1 | z2 | |
|---|---|---|---|
| p0 | 0.2500 | ? | ? |
| p1 | ? | ? | -0.0625 |
| p2 | ? | -0.0625 | ? |
| ? |
| ? |
| ? |
| ? | ? | ? |
| ? | ? | ? |
Go deeper Where the two formulas come from
Write , so . When moves, both and move, at the same rate . The rule for a fraction gives
When another score moves, only moves: .
One more fact about softmax: adding the same number to every score changes nothing. , and the factor appears in the numerator and in every term of the denominator, so it cancels.
From dp to dz
Each score moves every , so collects one part from each of them: .
This is the += of U3: a value used in several places gets the sum of all its parts.
When you put the formula for into the sum, the sum becomes short. is just , and is , where is one number for the whole row. So
where means entry by entry (* in NumPy). Subtract the one number from every entry of , then multiply by .
Check both ways on numbers, with and :
- the long way: column 0 of is , so ;
- the short way: , so .
I got stuck here Why not just build J and multiply?
It gives the same numbers, but it is much more work. With scores, has entries, and the short form needs about multiplications. In attention, each row of scores has one entry per token: with 1000 tokens, for one row has a million entries, and there are 1000 rows per head. The short form never builds .
In NumPy, and checked
In attention, softmax runs along each row of a table, so dp·p is needed once per row: a sum along the last axis.
A sum along an axis normally removes that axis: for a (2, 3) table, .sum(axis=-1) gives shape (2,).
The argument keepdims=True keeps the axis with size 1, so the result is (2, 1), one number per row.
Broadcasting can then subtract it from every entry of its own row.
How do you know a backward pass is right? Compare it with numeric slopes , as in level 6 and U5. A loss that makes the check easy is : its gradient at is exactly . The template does this check for you.
Enter keeps the indent · Tab indents · Esc then Tab leaves the editor · ⌘/Ctrl + Enter runs
In the lab, set the scores to . Then , and almost every entry of is 0. Change to anything: stays near 0. A softmax that is almost certain passes almost no gradient back. That is one reason level 14 divides the scores by : it keeps the scores small, so softmax is not this certain at the start of training.
2. One attention head, backward
The forward pass of one head (level 14) has three lines. Here are their shapes, with tokens:
| Forward | Shape |
|---|---|
S = Q @ K.T / √dₖ | (L, dₖ) @ (dₖ, L) → (L, L) |
A = softmax(S), each row | (L, L) |
O = A @ V | (L, L) @ (L, dᵥ) → (L, dᵥ) |
Backward handles the lines in reverse order, starting with the last one. It starts with dO, which has the shape of O.
Two rules do all the work:
- A matrix product (level 6):
dX = dY @ W.TanddW = X.T @ dY. - Softmax, each row: section 1’s formula, with in place of .
The figure shows the order. Every gradient has the shape of its value, which makes the transposes easy to check:
| Gradient | Comes from | Shape |
|---|---|---|
dV | O = A @ V | (L, dᵥ) |
dA | O = A @ V | (L, L) |
dS | A = softmax(S) | (L, L) |
dQ | S = Q @ K.T / √dₖ | (L, dₖ) |
dK | S = Q @ K.T / √dₖ | (L, dₖ) |
An example by hand
Two tokens, (so ), . The queries and keys are chosen so that every score is 0:
Every dot product of a row of with a row of is 0, so is all zeros and every weight in is 0.5. Each output row is the average of the rows of : . The gradient that arrives from the layers above is .
The product also gives : . Row 0 of says how the loss changes when token 0 gives more weight to each token. Now the softmax step, one row at a time.
The full score gradient is . Each row adds up to 0, like the rows of the Jacobian. The last step is the backward pass of . The forward pass divided the scores by , so both and are divided by too.
Now the keys. has one row per query and one column per key: is query 1 against key 0. Look at where key appears in .
I got stuck here Why does dK use dS.T, and dQ use dS?
. Query appears only in row of , so collects row of , each entry times its key:
that is dS @ K / √dₖ. Key appears only in column of , so collects column of . dS.T turns the columns into
rows, and dS.T @ Q / √dₖ does the same sum for the keys.
The shapes give a quick check on any line you write. Try it with sizes that are all different.
Go deeper With a batch, heads and a mask
A real model computes all of this for sequences and heads together. The tensors get two extra axes at the front:
(B, h, L, L) for S and A. Every line stays the same. Only .T must become .swapaxes(-1, -2), so that the batch and head axes stay where they are.
The causal mask needs no extra backward code. A blocked score became −∞, so its weight in is 0. Every entry of
dS = A * (…) is multiplied by its weight, so a blocked position gets exactly 0 gradient.
The whole head
Now write the backward pass in NumPy: five lines, in the order of the second table, with the rules you just used by hand. The template runs it on the example above, then checks every gradient against numeric slopes, on random numbers with 3 tokens, and .
Enter keeps the indent · Tab indents · Esc then Tab leaves the editor · ⌘/Ctrl + Enter runs
3. What backward must keep in GPU memory
Look at the five backward lines again. Each one uses values from the forward pass: A, V, K, Q.
Those values must still exist when the backward pass reaches them, so the forward pass keeps them. These kept values are called
saved activations. In your code they were the tuple saved.
So one head keeps Q, K, V and A, and the other layers keep what their own backward rules need (the input of every
matrix product, for example). Training uses GPU memory in two ways.
Per parameter: a fixed cost
Large models train in mixed precision (U2). The forward and backward passes use 16-bit numbers, bfloat16 (bf16 for short). The update is done on a 32-bit float32 copy (fp32 for short), so that small changes aren’t lost to rounding. With Adam (level 7), each parameter needs:
| Kept for every parameter | Format | Bytes |
|---|---|---|
| the weight used in forward and backward | bf16 | 2 |
| its gradient | bf16 | 2 |
| the master copy of the weight | fp32 | 4 |
| Adam’s m | fp32 | 4 |
| Adam’s v | fp32 | 4 |
That is 16 bytes per parameter, whatever the batch or the length.
Per token: activations
Saved activations grow with the data in the batch. Most of them have one row per token, shape (B, L, d_model): with , ,
d_model = 1000 and 2 bytes per number, one such tensor is 8 MB. A layer keeps several of them, for example the inputs
of its matrix products. The attention weights A are different: they have one number for every
pair of tokens, in every head. Their shape is (B, h, L, L).
How many (B, L, ·) tensors does one layer keep? Here is one pre-norm block (level 16), with an FFN of width 4 × d_model.
The table lists what its backward rules need, step by step, in a plain implementation:
| Kept for the backward pass | Width |
|---|---|
| the input of the first LayerNorm | d_model |
| the input of the Q, K, V products (the LayerNorm’s output) | d_model |
| Q, K and V | d_model each |
| the heads’ output, the input of the output product | d_model |
| the input of the second LayerNorm | d_model |
| the input of the FFN’s first product | d_model |
| the FFN’s hidden layer before ReLU | 4 × d_model |
| the FFN’s hidden layer after ReLU, the input of its second product | 4 × d_model |
d_model) tensor: a tensor of width 4 × d_model counts as 4. How many units?Now make the sequences longer and keep everything else.
The lab adds up everything training keeps, for one model. Move and watch which part grows fastest.
What training keeps in GPU memory, and what grows with L
Move B and L, or switch recomputation on. The model stays the same: 24 layers, width 1000, 8 heads, 300 million parameters. Each mark on the scale is 10 times the one before.
| Weights, gradients, Adam16 bytes × 300 million | 16 bytes × 300 million | 4.8 GB | 51% |
| Token activations16 × (B, L, d_model) × 24 layers | 16 × (B, L, d_model) × 24 layers | 3.1 GB | 33% |
| Attention weights(B, h, L, L) × 24 layers | (B, h, L, L) × 24 layers | 1.5 GB | 16% |
Token activations: the 16 tensors per layer that you counted above. On the scale, each boundary of the bar is the running total so far: the grey part always ends at 4.8 GB.
Go deeper Recomputation: more arithmetic, less GPU memory
The forward pass doesn’t have to keep everything. With recomputation, each layer keeps only its input. When the backward pass reaches that layer, it runs the layer’s forward pass again from the input, gets the values back, and uses them immediately.
In the example above, a layer then keeps one (4, 1000, 1000) input, 8 MB, instead of 64 MB for A alone plus everything else.
The values of that one layer exist again while the backward pass is inside it, so the peak is the kept inputs plus one layer’s values.
The cost is one more forward pass. A backward pass costs about twice a forward pass. So one training step goes from 1 + 2 = 3 units
of work to 3 + 1 = 4: about 33% more arithmetic, and much less GPU memory.
A kernel here is one program that runs on the GPU. A fused kernel does several steps in one program.
Fused attention kernels save even more GPU memory than recomputation. They keep Q, K, V, O and one number per row (from the softmax’s sum), but not A.
They recompute A in small blocks during the backward pass, so nothing of size is kept. A later part of the course builds this.
The softmax step still needs for each row, and it can come from O and dO alone:
, so .
In section 2’s example, row 0 gives , and .
Recap
a summary for when you finish the level
The key formulas and common mistakes appear here once you clear the level.
You can now
- Write the Jacobian of softmax, and compute dz with the short form p ⊙ (dp − dp·p) without building it.
- Take one attention head backward from dO to dQ, dK and dV, say the shape of every step, and check it against numeric slopes.
- Estimate the GPU memory of training: 16 bytes per parameter, plus saved activations that grow with B × L × d<sub>model</sub> and B × h × L².
Keep in mind
- , and the sum of every row of is 0
s = (dp * p).sum(-1, keepdims=True), thendz = p * (dp - s)- A matrix product goes backward as
dX = dY @ W.TanddW = X.T @ dY; every gradient has the shape of its value - Query is in row of the scores and key in column : dK needs the transpose of dS, dQ does not, and both keep the
- Mixed-precision Adam: 2 + 2 + 4 + 4 + 4 = 16 bytes per parameter
- One pre-norm block keeps about 16 tensors of shape (B, L, d<sub>model</sub>) for its backward pass, besides A
- Saved attention weights: B × h × L × L numbers per layer (in a plain implementation; a fused kernel keeps none)
Common mistakes
- Forgetting the 1/√dₖ in dQ and dK, or confusing the rows and the columns of dS.
- Counting only the bf16 weights (2 bytes each) for training: gradients, the master copy and Adam’s m and v make it 16.
Press ? for keyboard shortcuts