Chapter 01The state grows without bound

GPT-2, and the price of perfect recall

Softmax attention has a property nothing later in this series can match: it never forgets, never overwrites, and never confuses two facts. It gets that by keeping one memory slot per token — so the memory grows for as long as you keep talking. Everything after this chapter is an attempt to keep the property and lose the bill.

The entire model, in nine lines

GPT-2 is decoder-only: one stack of identical blocks, each token allowed to look only at itself and everything before it. The top-level forward pass is genuinely this short.

# idx: (B, T) token ids.  pos: (T,) positions 0..T-1
tok_emb = self.transformer.wte(idx)   # (B, T, n_embd) — what the token is
pos_emb = self.transformer.wpe(pos)   # (T, n_embd)    — where it sits
x = self.transformer.drop(tok_emb + pos_emb)
for block in self.transformer.h:      # 12 identical blocks
    x = block(x)
x = self.transformer.ln_f(x)
logits = self.lm_head(x)              # (B, T, vocab_size)
return logits

Two embedding tables, added together. GPT-2 learns its positional embeddings as a lookup table indexed by absolute position — a design decision that later models abandon, but it is not what this series is about. The interesting part is what happens inside block.

class Block(nn.Module):
    def forward(self, x):
        x = x + self.attn(self.ln_1(x))   # mix across tokens
        x = x + self.mlp(self.ln_2(x))    # mix across channels
        return x

Two lines, and they do genuinely different jobs. The attention line is the only place in the whole model where information moves between token positions. The MLP line processes each position independently. Every architecture in this series changes the first line and leaves the second alone until chapter 7.

Both lines are written as x = x + f(x). That plus sign is the residual stream: a running sum that every layer reads from and adds to. It looks like plumbing. In chapter 7 it becomes the target of an architectural change of its own.

What attention actually computes

Strip away the batching and the heads and attention is a weighted average of value vectors, where the weights come from how well each key matches the current query:

\[ o_t \;=\; \sum_{i \le t} w_{ti}\, v_i, \qquad\quad w_{ti} \;=\; \frac{\exp\!\big(q_t \cdot k_i / \sqrt{d}\big)}{\sum_{j \le t} \exp\!\big(q_t \cdot k_j / \sqrt{d}\big)} \]

q, k, v are the three linear projections of the same hidden state; i ≤ t is the causal mask; √d keeps the dot products from saturating the exponential at large head width.

The softmax does exactly two things: it forces the weights to be non-negative, and it makes them sum to one. That is what makes \(o_t\) an average rather than an arbitrary linear combination. Hold onto those two requirements — chapter 2 satisfies them a completely different way.

And notice where the exponential sits. It is applied to the dot product, after \(q_t\) and \(k_i\) have already met. This is the single most consequential structural fact in the whole series, for two opposite reasons.

The hinge

Because \(\exp\) is applied after the interaction, and \(\exp\) grows without bound, one key can take nearly all the weight. Attention can be almost one-hot: find me the single token that said "Bratislava". That is exact retrieval, and it is why softmax attention is so hard to beat.

Because \(\exp\) is applied after the interaction, \(\exp(q\cdot k)\) cannot be factored into something-about-\(q\) times something-about-\(k\). Every query must be evaluated against every key, individually. That is the entire cost of this architecture.

THE ATTENTION MATRIX AT SEQUENCE LENGTH N = 12 keys k₁ … k₁₂ queries q₁ … q₁₂ At decode step 12, this row is the only thing you need. Every earlier row was computed on a previous step and then discarded. what you must keep K V 2 × N × d numbers
The N² matrix you compute, and the N vectors you keep. Attention scores form an N×N lower-triangular matrix, but at any single decode step only the bottom row is used. The row is cheap. What is not cheap is that computing it requires every one of the N key vectors and N value vectors — so those, and only those, have to survive between steps. That is the KV cache.

Why the KV cache exists, and what it buys

Autoregressive decoding produces one token at a time. Feed in the prompt, take the logits at the last position, sample a token, append it, and go again. The observation that makes this affordable is small and slightly boring: appending a token does not change the key and value vectors of any earlier token. Each \(k_i\) and \(v_i\) depends only on hidden state \(i\), which depends only on tokens \(1..i\). Adding token \(t+1\) at the end leaves all of them untouched.

So keep them. That store is the KV cache: for every layer, for every head, the \(k\) and \(v\) vectors of every token seen so far.

The saving is larger than it looks. Run the numbers on generating \(N\) tokens:

StrategyWork at step tTotal over N stepsState carried
Recompute everythingfull forward over t tokens → \(O(t^2 d)\)\(O(N^3 d\,/\,3)\)none
KV cacheone query against t keys → \(O(t\,d)\)\(O(N^2 d\,/\,2)\)\(2Nd\) per layer

The cache turns cubic decoding into quadratic decoding. It is not an optimisation, it is the thing that makes generation possible at all. And in exchange you now carry a block of memory that grows by a fixed amount with every token you emit, forever.

The constraint this creates

The KV cache grows in \(O(N)\), and every decode step must read all of it. Not once per sequence — once per generated token. The state is unbounded, and the bandwidth cost of touching it is proportional to how much you have already said.

How big does it actually get?

GPT-2 small: 12 layers, 12 heads, \(n_{\text{embd}} = 768\), so \(d = 64\) per head and \(12 \times 64 = 768\) per layer. Two tensors (K and V), two bytes each in fp16:

\[ \underbrace{2}_{K,\,V} \times \underbrace{12}_{\text{layers}} \times \underbrace{768}_{n_{\text{embd}}} \times \underbrace{2\ \text{bytes}}_{\text{fp16}} \;=\; 36{,}864\ \text{bytes per token} \;\approx\; 36\ \text{KiB} \]

36 KiB per token, for a model whose entire weight set is 248 MB in fp16. Which means:

Context lengthKV cachevs. the model's own weightsComment
1,024 (GPT-2's actual limit)37.7 MB0.15×Fine. This is why nobody worried in 2019.
16,384604 MB2.4×The cache is now bigger than the model.
131,0724.83 GB19.5×You are storing 20 copies of the model, to hold one conversation.

Scale that up and it stops being a curiosity. A 70B-class model with plain multi-head attention — 80 layers, 8192 hidden — carries 2.62 MB of cache per token. At 100k context that is 262 GB, for a single sequence, on top of the 140 GB of weights. This is the pressure that produced grouped-query attention, then multi-head latent attention (chapter 7), and eventually the entire linear-attention branch of this series.

The bottleneck is bandwidth, not arithmetic

It is tempting to read \(O(N)\) work per step as "attention decode is slow because of the FLOPs." It isn't. Look at the ratio of arithmetic to memory traffic.

To attend over one cached position you read its \(k\) and \(v\) — \(2d\) numbers, so \(4d\) bytes in fp16 — and you do one dot product plus one scaled accumulation, about \(4d\) floating-point operations. The arithmetic intensity is therefore

\[ \frac{4d \ \text{FLOPs}}{4d \ \text{bytes}} \;=\; 1\ \ \text{FLOP per byte.} \]

A modern accelerator wants somewhere in the region of a few hundred FLOPs per byte to keep its matrix units busy — an H100 pairs roughly 500–1000 TFLOP/s of tensor throughput with about 3.35 TB/s of HBM bandwidth. At an intensity of 1, the arithmetic units are idle essentially all the time. Every cached byte is read, used once, and thrown away.

Why this matters for everything that follows

If decode were compute-bound, the fix would be better kernels. It is bandwidth-bound, so the only real fix is to have less state to read. That reframing is what makes a fixed-size state attractive even when it is mathematically weaker than attention — and it is why FlashAttention, which removed the need to materialise the N×N score matrix in HBM during training, did not make this problem go away. FlashAttention fixed the training memory. The decode-time cache is a separate bill.

The property worth preserving

Before dismantling it, be precise about what softmax attention gives you, because the next five chapters are all measured against it.

One slot per token
Attention never has to decide what to evict, because nothing ever shares a slot. Token 4,000 and token 40,000 sit in separate rows of the cache and cannot corrupt each other. There is no capacity limit, so there is no eviction policy.
Sharp, content-addressed retrieval
The unbounded exponential lets one score dominate all the others. The model can genuinely look up one earlier token. Every approximation in this series trades some of this away.
Position-independent addressing
Retrieval quality does not degrade with distance. A key 100,000 tokens back competes on equal terms with one from the previous word — assuming positional encoding does not interfere.

The baseline config, for reference:

vocab_size: int = 50304   # GPT-2's 50257, padded to a multiple of 64
n_layer:    int = 12
n_head:     int = 12
n_embd:     int = 768      # → 124M parameters, → 22,580 of these fit in Kimi K3
What has to change

Nothing about the quality of softmax attention is the problem. The problem is that its state has one slot per token and every decode step reads all of them. To get a state that does not grow, you have to break the thing that forbids factorisation — the exponential sitting on top of the dot product. That is chapter 2, and it costs you exactly the property that made softmax sharp.