Does the LSTM solve the problem, or only push it further away?
| path from the last token to the first | 64 tokens | 256 tokens |
|---|---|---|
| plain RNN, weight scale 1 (measured in the simulator) | ||
| LSTM cell path, |
||
| attention | one step | one step |
| text | GPT-2 tokens |
|---|---|
| the cat climbed the | 4 |
| o gato subiu no | 6 |
| an English sentence of 16 words | 17 |
| its Portuguese version, 13 words | 25 |
GPT-2: 256 bytes + 50,000 merges + 1 special token = 50,257. "o gato subiu no" = o ␣g ato ␣sub iu ␣no. The space belongs to the next word: "the" is id 1169, "␣the" is 262.
Count the neighbouring pairs of a corpus, merge the most frequent one, write it down, repeat. To tokenize new text, replay the merges in order.
| after merge | pieces | how many |
|---|---|---|
| start: characters | t h e ␣ c a t ␣ c l i m b e d ␣ t h e |
19 |
1 t+h · 2 th+e · 3 ␣+the |
the ␣ c a t ␣ c l i m b e d ␣the |
14 |
4 ␣+c · 5 a+t · 8 ␣c+at |
the ␣cat ␣c l i m b e d ␣the |
10 |
14 e+d |
the ␣cat ␣c l i m b ed ␣the |
9 |
18 l+i · 38–40 → ␣climb |
the ␣cat ␣climb ed ␣the |
5 |
Toy corpus: 8 sentences ("the cat sat on the roof.", "the dog climbs on the sofa." …), 23 characters, 40 merges; "climbed" is not in it. The toy model keeps 12 tokens: the = 0, ␣the = 1, ␣cat = 2, ␣climb = 4, ed = 5 → [0, 2, 4, 5, 1].
A lookup in the embedding table
| token (id) | ||||
|---|---|---|---|---|
| 1 | the (0) |
[−0.5, −0.5, 0, 1] | [0, 0, 0, 0] | [−0.5, −0.5, 0, 1] |
| 2 | ␣cat (2) |
[1, −0.5, 0, −0.5] | [0.5, 0, −0.5, 0] | [1.5, −0.5, −0.5, −0.5] |
| 3 | ␣climb (4) |
[−0.5, 1, 0, −0.5] | [0, 0.5, 0, −0.5] | [−0.5, 1.5, 0, −1] |
| 4 | ed (5) |
[0, 0.5, −0.5, 0] | [−0.5, 0, 0.5, 0] | [−0.5, 0.5, 0, 0] |
| 5 | ␣the (1) |
[−0.5, 0, −0.5, 1] | [0, −0.5, 0, 0.5] | [−0.5, −0.5, −0.5, 1.5] |
Toy: V = 12, d = 4, 8 positions, weights picked by hand. GPT-2 small: E is 50,257 × 768 (38.6M parameters, 31% of the model), P is 1024 × 768, both learned. Llama has no P: RoPE rotates q and k instead.
The embedding layer gives "bank" a vector. Is it the same vector in both sentences?
stock = {"apple": 3, "pear": 7}
stock["pear"] # exact match → 7
A dictionary demands an exact match and returns one value.
Attention compares the question with every key, gives each a similarity score and returns a weighted average of all the values.
The average is a smooth function of the scores, so it can be differentiated — and the network learns, by gradient, what to ask and what to advertise.
X: one row per token, n tokens of d numbers · dk: the length of each query and key · softmax: escore divided by the row's sum.
Three projections of the same input, and WQ, WK, WV are the only learned parameters. That is why a token can ask for one thing, advertise another and deliver a third — a pronoun can query for "the noun I refer to" without being a noun.
"sat · bank · river", two dimensions picked by hand. The values read as
| token | key |
value |
|---|---|---|
| sat | ||
| bank | ||
| river |
The query of "bank" is
| token | ① score |
② |
③ |
③ weight |
④ |
|---|---|---|---|---|---|
| sat | 0.71 | 2.03 | 2.03 / 27.29 = 0.074 | ||
| bank | 2.12 | 8.34 | 0.306 | ||
| river | 2.83 | 16.92 | 0.620 | ||
| sum | 27.29 | 1 |
"bank" came in on the fence,
| tensor | shape | what it is |
|---|---|---|
| one vector per token | ||
| learned | ||
| learned | ||
| a query and a key per token | ||
| a value per token | ||
| a score for every pair — the |
||
| row |
||
| a new vector, with context, per token |
Lab, section 2: n = 6, d = 16, dk = 8, dv = 4 → A is 6 × 6, O is 6 × 4, 16·8 + 16·8 + 16·4 = 320 parameters — none of them depends on n. Our three lines match F.scaled_dot_product_attention to 1.2 × 10−7.
If
argmax, and no gradient.Why not divide by
One query against 16 keys, 200 rows; median over the rows.
| largest weight, with |
largest weight, without | dead rows, without | |||
|---|---|---|---|---|---|
| 4 | 0.21 | 0.26 | 0.37 | 0.27 | 0% |
| 64 | 0.22 | 0.27 | 0.94 | 0.054 | 17% |
| 1024 | 0.22 | 0.28 | 0.99999 | 0.000011 | 64% |
Largest weight: the maximum of a row of A (uniform would be 1/16 ≈ 0.06). ‖∂L/∂S‖: the size of the gradient reaching the row's scores. "Dead" = ‖∂L/∂S‖ below 10−3. Notebook, section 3.
Lab, section 4: queries × 100 → four of five rows print a largest weight of 1.0, and their gradients fall from 0.15–0.51 to between 10−34 and 7 × 10−4.
Added to the scores before the softmax, as
| position | sees | must predict |
|---|---|---|
| 1 | the |
␣cat |
| 2 | the␣cat |
␣climb |
| 3 | the␣cat␣climb |
ed |
| 4 | the␣cat␣climbed |
␣the |
| 5 | the␣cat␣climbed␣the |
␣roof |
␣cat and learn to copy.| mask | forbids | where |
|---|---|---|
| causal | looking at the future | every autoregressive decoder |
| padding | attending to <pad> filler |
any batch of variable-length sequences |
| sliding window | looking further back than |
Mistral, Gemma, hybrid stacks |
| document | crossing into another packed document | all modern pretraining |
A zero vector is not "nothing": its score is 0, not
Attention is permutation-equivariant: shuffle the tokens and the outputs shuffle with them. "dog bites man" = "man bites dog" (in the lab: a difference of
| scheme | mechanism | verdict |
|---|---|---|
| sinusoidal | fixed sin/cos added to the input | historical |
| learned absolute | one trainable vector per position | cannot pass the trained length |
| ALiBi | a linear distance penalty on the scores | largely superseded |
| RoPE | rotate Q and K by an angle ∝ position | universal |
| NoPE / hybrid | no signal in some layers | growing in long-context stacks |
Lab, section 6: the same q and k at positions (3, 1), (10, 8) and (1000, 998) give the same score, +0.033671; offsets 1 and 3 give −0.81 and +1.18.
| step | shape |
|---|---|
| split into heads | |
| scores, one |
|
| concatenate, then |
Four
B: sequences in the batch · n: tokens · h: heads, each with d/h dimensions.
Generating token
| term | what it is | Llama 3 8B |
|---|---|---|
| K and V | 2 | |
| layers | 32 | |
| key/value heads | 8 | |
| dimensions per head | 128 | |
| bytes per number | 16 bits | 2 |
| per token | 128 KiB | |
| tokens × sequences: 128k = 131,072 tokens, one user | 16 GiB |
| scheme | KV heads | cache | quality | 2026 |
|---|---|---|---|---|
| MHA | 1× | baseline | historical at long context | |
| MQA | 1 | measurably worse | where memory dominates | |
| GQA | ≈ MHA | the default | ||
| MLA | — | ~1/57 | reported ≥ MHA | DeepSeek-V2/V3 |
Decoding reads the whole cache to produce one token: it is bound by memory bandwidth, not FLOPs. GQA, quantized caches, PagedAttention, bigger batches — all attacks on bytes moved.
F.scaled_dot_product_attention.Lab, section 8, one layer with 8 heads: S takes 8 · 4096² · 4 bytes = 512 MiB at n = 4096 and the work grows 4× per doubling; on the lab's CPU the naive version took 239 ms and the fused kernel 74 ms.
SmolLM2-135M (Llama architecture: RoPE, GQA):
Colab notebook, ~2 h, CPU. Each section ends in a checkpoint with a check value.
Outline: (1) what recurrence could not do; (2) the idea: a soft dictionary, Q/K/V, one sum by hand; (3) the details that make it work: √d_k, masks, position, heads; (4) the cost: the KV cache and the O(n²). Closes with the Colab lab.
Defaults: 64 tokens, scale 1. RNN 5e-4, LSTM cell 4e-2. Drag to 256: 1e-14 and 2e-6. Both are straight lines on a log plot — exponential decay. The LSTM bought a better constant. Attention is flat at 1/n: small, but the same for every position.
Ask first: "how many tokens is 'the cat climbed the'?" Most say 4 words = 4 tokens; GPT-2 agrees here. Then the Portuguese line: 6 for 4 words, 25 for 13 words in the long sentence (1.92 per word against 1.06): more compute, less context, a bigger bill for the same content.
The unseen word is the point: "climbed" was never in the corpus and BPE writes it with two known pieces, ␣climb (from climb, climbs) and ed (from jumped, walked). Only 11 of the 40 merges touch the sentence. GPT-2's 50,000 merges include " climbed" as one token (19952).
Next through the merges: 19 characters, 5 pieces, ids [0, 2, 4, 5, 1]. Then the cases: "… roof." adds ␣roof and "."; "the bird climbed the roof." writes "bird" letter by letter (␣ b i r d, no id among the 12); the Portuguese sentence through the English tokenizer: 21 pieces.
X (5 x 4) is what attention receives. The same id always gets the same row: "bank" gets one vector in both sentences of the next slide. Making the vector depend on the neighbours is attention's job.
It starts at the numbers of the previous slide. Then: (a) value of river → [0, 1]: the verdict flips and no weight moves; (b) query → [1, 0]: the weight leaves river, 0.40/0.40/0.20; (c) query → 0: uniform, a plain average.
Read by row: where each token looks; every row sums to 1. The first three heads are patterns trained models really have (content, previous token, distance). Then: untick "divide by √d_k" and drag d_k up — the matrix goes binary. Then tick the causal mask: the upper triangle goes dark. Come back to the mask in four slides.
Variance 1/d_k: every score near zero, a near-uniform softmax that picks no one.
Lab, section 5: change the last token; without the mask all five rows change, with it only the last.
The lab's code (section 7, with the attention function of section 2), three tokens, d = 4, h = 2. Head 1 is the bank of slides 13-15: its row ends at [0.85, 0.15]; head 2 makes bank look at sat (0.77). Step line by line, then: "with the causal mask" - bank loses river and is back on the fence, [0.60, 0.40]; sat sees only itself. "a single head (h = 1)": same numbers, √4 = 2, bank splits 0.38/0.38/0.23 - one row cannot point at both. "all keys equal": every weight 1/3, a plain average.
Start at 8B, GQA, 32k, 8 requests: 34.4 GB of cache + 16 GB of weights, it fits. Switch to MHA: the cache grows 4× (137 GB) and does not fit. Back to GQA and up to 128k: 137 GB again. 70B or 405B: the weights alone (140 GB, 810 GB in 16 bits) do not fit on one 80 GB card.