A complete visual walkthrough — from embedding to generation — with interactive simulations and your full source code annotated.
A Transformer is a neural network architecture that processes sequences by letting every token "look at" every other token through a mechanism called self-attention. Unlike RNNs that read tokens one-by-one left-to-right, a Transformer sees the entire sequence in parallel during training — which makes it massively faster to train on GPUs.
The core insight is simple: to understand any word in a sentence, you need context from other words. Self-attention provides a learned, weighted way to gather that context. The word "bank" means different things in "river bank" vs "bank account" — attention figures out which other tokens to focus on.
A language model (LM) transformer is trained to predict the next token given all previous tokens. At inference time, it generates one token at a time in a loop — each new token feeds back in as input. This is called autoregressive generation, and it's where KV Cache becomes essential.
This is the full code we're explaining. Everything below — architecture diagrams, attention walkthrough, KV cache simulation — maps directly back to the classes and methods here.
Your DummyLM stacks four components into a clean pipeline. A token ID enters at the top and a probability distribution over the vocabulary exits at the bottom. Here's each piece:
Lookup table mapping each of the 1000 token IDs to a learnable 256-dimensional vector.
8 parallel attention heads (each 32-dim). Projects Q, K, V, computes scaled dot-product, merges heads, and projects output. This is where the KV Cache lives.
Two linear layers with GELU activation in between. Expands to 1024-dim hidden, then compresses back to 256. Adds non-linear representational power.
Final linear projection back to vocabulary size. Output logits are passed through argmax to select the most likely next token.
Self-attention answers: "for each token, how much should I attend to every other token?" It does this by projecting the input into three roles — Query (what am I looking for?), Key (what do I contain?), and Value (what information do I carry?) — then computing a compatibility score between each Query and all Keys.
Input x is projected through learned weight matrices WQ, WK, WV to produce Q, K, V. Each is shape (batch, seq, 256).
Each matrix is reshaped: (B, seq, 256) → (B, 8, seq, 32). Each head independently attends in its own 32-dim subspace, learning different relationship patterns.
If cached past keys/values exist, the new K and V are concatenated with the stored ones along the sequence dimension. Then the updated K, V are saved for next step. This is the key optimization — see Section 05.
scores = Q · KT / √32. Each query token gets a score against every key token. Division by √dk prevents dot products from growing too large, which would push softmax into saturated regions.
Lower-triangular mask sets future positions to −∞ before softmax. This ensures token i can only attend to tokens ≤ i — preserving the autoregressive property. During cached decoding with a single Q token, no mask is needed.
Softmax normalizes scores into attention weights (probabilities). The output is the weighted sum of Values: tokens with higher scores contribute more information.
Transpose back and reshape (B, 8, seq, 32) → (B, seq, 256). The 8 independent perspectives are concatenated into a single representation.
Final WO linear layer mixes information across heads and produces the attention block output, which gets added back via the residual connection.
During autoregressive generation, the model generates one token at a time. Without cache, every new step feeds the entire growing sequence back through the model — recomputing Q, K, V for all previous tokens. This is wasteful because the Keys and Values for past tokens never change (they only depend on the fixed past input, not the future).
The KV Cache exploits this by storing the K and V tensors from every previous step. On each new decode step, only the single new token is projected to get new Q, K, V. The new K, V are appended to the cache, and attention is computed with Q (1 token) against the full cached K, V (all tokens). The savings are enormous:
At step n, project all n tokens through WQ, WK, WV. Compute full n×n attention matrix. Total projection cost over N steps: O(N²).
At step n, project only 1 token. Compute 1×n attention row. Append to cache. Total projection cost: O(N). Memory cost: store K, V for all past tokens.
Click through each decode step to see what happens inside attention. Without cache: every token is recomputed (red). With cache: past K/V are stored (blue) and only the new token (orange) is computed.
Your test script generates 200 tokens with and without KV cache, then asserts identical outputs. The table below shows why caching is faster, and the bars show how much.
| Metric | Without Cache | With Cache |
|---|---|---|
| Q/K/V projections per step | All n tokens | 1 token only |
| Attention matrix size | n × n (full square) | 1 × n (single row) |
| Input to model per step | Full sequence (growing) | Last token only |
| Extra memory | None | O(n · d) per layer |
| Total projection ops (N tokens) | Σ(P+i) × 3 = O(N²) | P×3 + N×3 = O(N) |