Chapter 8 · 18 min
An attention head by hand
Q, K, V, scaled dot product, causal mask, softmax. Build a self-attention head by hand and visualize what it attends to.
Up to chapter 7 we had a model that could fit functions on individual examples. Loss curves go down, MLPs solve XOR, do their job. The problem we haven't solved: the model has no way for a to look at other . The model in chapter 1 could only see the previous word; an applied to a fixed-size can only see what's in that single vector. There's no mechanism for context to flow.
Attention is that mechanism, and it has powered every state-of-the-art since 2017. The original paper called it "all you need", which was an exaggeration that proved approximately correct.
The mechanics are mechanical. Once you've run each piece and inspected the shapes, you can explain to a friend at a bar. We're going to do exactly that — four runnable cells that build a single head from the inside out, on a 5- toy sentence with 4-dimensional so the matrices fit on a screen. After that, you will save a minimal causal function locally.
The sentence: the cat sat on mat. Five , hand-crafted 4D .
1. Project to Q, K, V
A single head has three learned matrices: W_Q, W_K, W_V, each [d × d_head]. Multiplying the input X (shape [seq_len × d]) by each gives queries, keys, and values — three views of the same input, each computed by a different set of weights.
Conceptually:
- Queries ask "what am I looking for?".
- Keys advertise "this is what I have".
- Values carry "this is what I'll contribute if you pick me".
Write the for Q. (We use the same routine for K and V — once you've built it once, you're done.)
Your turn · JavaScript
The result is [seq_len × d_head], same as the input in our toy case. Each row is the query vector for one in the sentence.
2. Score every pair
Now ask: how much does every query care about every key? The standard answer is a dot product — large when two vectors point the same way, small when they're orthogonal, negative when they point opposite.
The result S is a [seq_len × seq_len] matrix. S[i][j] is "how relevant is j to i?". Row i is the row of scores from i's perspective.
Your turn · JavaScript
The heatmap shows the raw scores. The matrix is not symmetric in general — i asking about j is a different question than j asking about i, because they use different W_Q and W_K. That asymmetry is what lets express directional relations like "this verb is governed by this subject".
3. Scale and softmax
The scores aren't probabilities yet — they can be any sign, any magnitude. Two transformations turn them into a over , row by row:
- Scale every entry by
1/√d_k. Without this, the dot products of high-dimensional vectors would grow large enough that saturates and vanish during . - Softmax each row. Now each row sums to 1.
(Row-wise , applied independently to each row.)
Your turn · JavaScript
Look at row i in the heatmap. The cells in that row tell you, for i, how much of its updated representation will come from each other . If 2 has a strong value at column 4, the model thinks 4 is highly relevant as a source for 2's update. The row sums are 1 — every row is a real .
4. Mix the values
Last step: each 's output is the -weighted sum of value vectors. Token i takes a weighted average over all ' values, weighted by the row A[i].
In matrix form, output = A · V, shape [seq_len × d]. Same shape as the input X — the head reshuffles each 's representation by pulling in pieces of others.
Your turn · JavaScript
That's the full single-head computation. Five lines of math, four matrix operations. Stack the same machinery a few dozen times in a row (which is what the next chapters do), train it on a billion of text, and you get GPT.
Why this works
The mechanism isn't deep but the implications are. A single head can implement, depending on its trained weights:
- A copy — every attends to the previous one, output = previous 's value. (Useful for repetition.)
- A lookup — for every "the", attend to the noun that follows. (Common in language modeling.)
- A factor — for every verb, attend to its subject. (Long-range agreement.)
- A summary — every attends roughly equally to every other , averaging the sequence. (Useful for the final layer.)
The procedure ( on next- ) figures out which patterns the network needs. We never tell it. A modern has tens of these heads per layer and dozens of layers; each head specializes during .
5. Add causal attention locally
Create llm/attention.py:
"""Readable attention helpers before the PyTorch version."""
from __future__ import annotations
import math
Vector = list[float]
Matrix = list[Vector]
def dot(a: Vector, b: Vector) -> float:
return sum(x * y for x, y in zip(a, b))
def softmax(values: Vector) -> Vector:
m = max(values)
exps = [math.exp(v - m) for v in values]
total = sum(exps)
return [v / total for v in exps]
def matmul(x: Matrix, w: Matrix) -> Matrix:
columns = list(zip(*w))
return [[dot(row, list(col)) for col in columns] for row in x]
def causal_attention(x: Matrix, wq: Matrix, wk: Matrix, wv: Matrix) -> Matrix:
# [1]
q = matmul(x, wq)
k = matmul(x, wk)
v = matmul(x, wv)
scale = math.sqrt(len(k[0]))
out: Matrix = []
for i, query in enumerate(q):
# [2]
scores = [
dot(query, key) / scale if j <= i else -1e9
for j, key in enumerate(k)
]
# [3]
weights = softmax(scores)
# [4]
out.append([
sum(weight * value[d] for weight, value in zip(weights, v))
for d in range(len(v[0]))
])
return outRead it as four passes over the same sequence:
- [1]
q,k, andvare three learned views ofx. Same , different questions. - [2]
scorescomparesi's query against every key.j <= iis the : past and current are visible; future get-1e9. - [3]
softmax(scores)makes one fori. - [4] builds a weighted average of value vectors. That is the new representation for
i.
The important addition is the : i can only read 0..i. Without that, the model could cheat during next- by looking at the answer.
Recap
- Three projections of the same input — Q (queries), K (keys), V (values).
- Scores are pairwise dot products of queries and keys: how relevant is every other ?
- Scale and turn scores into a per .
- Output is a weighted sum of value vectors, weighted by .
- Your local project now has
llm/attention.pywith causal . - One head is a single information-routing pattern. Multiple heads (next chapter) let different routes coexist.
Going further
- The Illustrated Transformer by Jay Alammar — the most reproduced visual explanation of on the internet.
- Karpathy's "Let's build GPT from scratch" — two hours of live-coded .
- Step by Token, chapter 4 covers from the understanding angle.
- The reference math for each cell lives in
components/chapter/ch08/_shared.ts.
Next up: multi-head and residuals — why one head isn't enough, and the connection that lets us stack many.
Frequently asked questions
What do query, key and value mean in attention?
Three different views of the same input, each produced by its own learned matrix. The query asks "what am I looking for", the key advertises "this is what I have", and the value carries "this is what I will contribute if you pick me". Attention weights come from matching queries against keys; the content that actually moves is the values.
Why is the attention score divided by the square root of d_k?
Because the variance of a dot product grows linearly with its dimension. At d_k = 64 the raw scores land in the tens or hundreds, which pushes softmax into saturation: almost all the mass goes to one token and the gradient nearly vanishes. Dividing by √d_k restores unit-scale scores and keeps softmax in the regime where it can still learn.
What does the causal mask actually do?
It stops token i from attending to any token after it, by setting those scores to negative infinity before the softmax so they come out at exactly zero probability. Without it, a model trained on next-token prediction could simply read the answer. It is the one constraint that makes a transformer a decoder rather than an encoder.
Is the attention matrix symmetric?
No, and that matters. Token i asking about token j goes through W_Q and W_K, while j asking about i goes through the same matrices in the opposite roles — different computation, different score. That asymmetry is what lets attention express directional relations like "this verb is governed by this subject".