Attention, Part 1: Self-Attention and Multi-Head Attention
The one idea every modern AI language model is built on, explained from zero: why attention was invented, how a word decides which other words to look at, why the scores are divided by √d, how several heads work together, and what real attention heads inside Qwen2.5 actually learn. Every claim checked in code.
Every AI chatbot you have used (ChatGPT, Gemini, Llama, Qwen, Gemma, DeepSeek) is built around one idea called attention. It is the part of the model that lets each word look at the other words around it and decide which ones matter.
This series explains attention from zero, and then every important variation of it that today's models use. No background is assumed. Whenever a technical word appears, there is a box like this one that explains it:
Before 2014, translation models read a sentence one word at a time and squeezed everything into a single vector, a kind of running summary. Then they wrote the translation from that one summary.
The problem: one fixed-size summary has to hold the whole sentence, whether it has five words or fifty. Long sentences get blurry.
Left: the old way squeezes the whole sentence into one vector. Right: with attention, the model looks back at every input word and decides how much each one matters right now (the weights shown are illustrative).
Here is how the paper that introduced attention describes that problem, in its very first section:
In 2014, Bahdanau, Cho and Bengio had a simple idea: let the model look back at every word, every time it writes a word, and learn how much to pay attention to each one. To write "cat" it looks mostly at "chat"; to write "black", mostly at "noir". That weighting is attention.
Their version works like this. The encoder leaves one vector hj for each source word j (they call these annotations). When the decoder is about to write target word i, it does three things:
si−1 is the decoder's current state (what it has written so far);
a is a small learned network, the alignment model, that scores how well source word j fits the next target word;
eij is that score, and αij is the score turned into a weight by softmax (the same softmax you will meet below);
Tx is the number of source words;
ci is the context vector: a weighted mix of all the source words, made fresh for every target word.
The paper also drew the weights as pictures, and they show something nobody had programmed:
In 2017 the paper Attention Is All You Need went further. It threw the word-by-word reading away and made attention the main way words share information. That design is called the transformer, and every modern chatbot is one. When the words of one text attend to each other, it is called self-attention.
Now we can build attention. It answers one question for every token: of the tokens I am allowed to see, which ones should I take information from, and how much?
Each token's vector is turned into three new vectors, each made by multiplying it by a different grid of numbers:
a query (Q): what am I looking for?
a key (K): what do I contain? (used for matching)
a value (V): what will I hand over if I am chosen?
A library is a good picture. Your query is the question you bring. Every book has a key (its title and topic) and a value (its contents). You compare your question with every title, then read mostly from the books that match best.
The self-attention pipeline. Each token makes a query, a key and a value. Every query is compared with every key, the future is hidden, softmax turns the scores into weights, and the weights mix the values.
The steps:
Score. Compare every query with every key using the dot product. A big score means "this key matches what I am looking for".
Scale. Divide the scores by d (the square root of the vector length). The next section shows why.
Hide the future. A model that writes left to right must not peek at words that come later, so those scores are blocked.
Softmax. Turn each token's scores into weights that add up to 1.
Mix. Each token's new vector is the weighted average of the values.
All five steps fit in one equation, the most important equation in this series:
Attention(Q,K,V)=softmax(dQK⊤+M)V
where:
Q, K, V are the queries, keys and values of all the tokens, stacked as rows;
QK⊤ is every query's dot product with every key (the ⊤, "transpose", just flips K so the multiplication lines up);
d is the length of each key vector;
M is the causal mask: 0 where looking is allowed, −∞ where it is not.
This is the equation from Attention Is All You Need (Vaswani et al., 2017), the paper that introduced the transformer. Here it is in the original:
Here is the same thing in a few lines of Python, using PyTorch:
python
import math, torchdef self_attention(x, Wq, Wk, Wv, causal=True): """x holds one vector per token. Returns the attention weights A and the new vectors Z.""" Q, K, V = x @ Wq, x @ Wk, x @ Wv # 1. make queries, keys and values scores = Q @ K.T / math.sqrt(K.shape[-1]) # 2. score every pair, divided by sqrt(d) if causal: # 3. hide the future T = x.shape[0] future = torch.triu(torch.ones(T, T, dtype=torch.bool), diagonal=1) scores = scores.masked_fill(future, float("-inf")) A = torch.softmax(scores, dim=-1) # 4. scores -> weights; each row adds up to 1 return A, A @ V # 5. mix the values
I ran it on four tokens, The cat sat down, with random weights. Here are the attention weights it printed: each row is one token, showing how much it looks at each token in the columns.
The same weights as a picture. Each row is one token deciding where to look. The empty cells are the future, which is hidden. Every row adds up to 1.
Three things to notice:
The can only look at itself, so its row is a single 1.00. The causal mask is working.
Every row adds up to exactly 1. A token can choose where to look, but its total attention is always 100%.
There is one number for every pair of tokens. 4 tokens make 16 numbers. 1,000 tokens make 1,000,000, in every head of every layer. Remember this: it is the cost that the whole rest of this series is about.
Step 2 looks like a small detail. It is not: leave it out and the model cannot learn properly. Here is why, with measurements.
A dot product adds up d multiplications. If the numbers are random, adding more of them makes the total swing more widely: the typical size of a dot product grows like d.
Softmax is very sensitive to big numbers. If one score is much bigger than the others, softmax gives it almost all the weight, and the model effectively stops looking at anything else.
I measured it: 10,000 random queries, each scored against 64 random keys, for four vector lengths.
Vector length d
Spread of the dot products
Top weight without ÷√d
Top weight with ÷√d
16
4.0
0.576
0.107
64
8.0
0.796
0.108
256
16.0
0.896
0.107
1,024
32.0
0.950
0.108
The biggest weight in a softmax over 64 keys. Without dividing by √d, it climbs towards 1 as the vectors get longer, so attention collapses onto a single token. Dividing by √d keeps it steady.
The spread grows exactly as d: 4, 8, 16, 32. Without the division, at d=1,024 one random key already takes 95% of the attention before the model has learned anything. When softmax is that lopsided, its learning signals (gradients) become tiny and training stalls. Dividing by d cancels the growth: the top weight stays near 0.107 at every size.
The dot product adds d such independent terms, and variances of independent terms simply add up:
Var(q⋅k)=i=1∑dVar(qiki)=d⟹spread(q⋅k)=d
So dividing by d brings the spread back to exactly 1, whatever the vector length:
Var(dq⋅k)=dd=1
where E means "average value" (expected value) and Var means variance. That is exactly what my measurement showed: spreads of 4, 8, 16, 32 for d = 16, 64, 256, 1,024, which are d.
The transformer paper gives the same reasoning, in one sentence and one footnote:
One attention calculation learns one pattern. But a word often needs several kinds of information at once: the word just before it, the subject of the sentence, the matching bracket in some code. So models run several attention calculations side by side.
Multi-head attention. Each head has its own matrices and its own attention pattern. The results are joined together and mixed by one more matrix, called Wo.
Wq(i),Wk(i),Wv(i) are head i's own query, key and value matrices;
h is the number of heads;
Concat places the heads' results side by side into one long vector per token;
Wo is the output matrix that mixes the heads back together.
Each head works with shorter vectors, so h heads cost about the same as one big head:
dhead=hdmodelfor example8512=64
where dmodel is the length of each token's vector and h the number of heads. The original transformer used exactly these numbers: 512 per token, 8 heads, 64 per head. Total work is h×dhead=dmodel, the same as one head of full size.
python
class MultiHeadAttention(torch.nn.Module): def __init__(self, d_model, n_heads): super().__init__() self.h, self.d = n_heads, d_model // n_heads # number of heads, numbers per head self.Wq = torch.nn.Linear(d_model, d_model, bias=False) self.Wk = torch.nn.Linear(d_model, d_model, bias=False) self.Wv = torch.nn.Linear(d_model, d_model, bias=False) self.Wo = torch.nn.Linear(d_model, d_model, bias=False) def forward(self, x): # x: (batch, tokens, d_model) B, T, _ = x.shape split = lambda t: t.view(B, T, self.h, self.d).transpose(1, 2) # cut into h heads q, k, v = split(self.Wq(x)), split(self.Wk(x)), split(self.Wv(x)) scores = q @ k.transpose(-2, -1) / math.sqrt(self.d) # one score grid per head mask = torch.triu(torch.ones(T, T, dtype=torch.bool, device=x.device), 1) A = torch.softmax(scores.masked_fill(mask, float("-inf")), -1) heads = A @ v return self.Wo(heads.transpose(1, 2).reshape(B, T, self.h * self.d)) # join and mix
Code you write yourself is only trustworthy if it agrees with a trusted version. PyTorch has two built-in attention functions, so I gave them exactly the same weights as the class above and compared the results:
plain text
our MHA vs F.scaled_dot_product_attention: max |diff| = 1.7e-16our MHA vs torch.nn.MultiheadAttention: max |diff| = 1.7e-16
The largest difference is 1.7×10−16, that is 0.00000000000000017. That is the smallest rounding error a computer can make with these numbers: the three versions compute exactly the same thing.
Random weights show how attention works. To see what it does, we have to look inside a trained model. I used Qwen2.5-0.5B, a small open model with 24 layers and 14 heads in each layer: 336 heads in total.
I gave it this sentence and read out the attention weights of every head:
The cat sat on the mat because it was tired.
Three kinds of head stood out:
Three real heads from Qwen2.5-0.5B. Left: almost every token looks at the first token (an attention sink). Middle: every token looks at the token just before it. Right: many tokens, including "it", look back at "cat". Hover over a cell to see its weight.
1. Attention sinks. In 68% of all heads, more than half of the attention goes to the very first token, whatever it is.
2. A previous-token head. Layer 8, head 7 puts on average 84% of each token's attention on the token just before it: the clean diagonal in the middle picture. Heads like this help with copying and spotting patterns.
3. A pronoun head. In layer 5, head 5, the word "it" puts 83% of its attention on "cat", the thing "it" refers to. One sentence could be luck, so I tested the same head on six sentences it had never seen:
Sentence
Word
Where it looked most
Weight
The dog chased the ball because it was bored.
it
dog
0.90
The trophy did not fit in the suitcase because it was too big.
it
trophy
0.62
My sister bought a new car and she loves driving it.
she
sister
0.86
The engineers fixed the server after it crashed twice.
it
server
0.40
The scientist published the paper because she was proud of it.
she
scientist
0.78
The children ate the cake because they were hungry.
they
children
0.72
Six out of six. One honest note: in every one of these sentences, the right word is also the main noun near the start, so this head might be finding "the main noun" rather than truly understanding who "it" is. Either way, nobody programmed this. It appeared on its own during training.
Attention has two costs, and both grow with the length of the text:
Work. Every token compares itself with every earlier token. Twice as much text means four times as many comparisons.
Memory. When the model writes a reply one token at a time, it keeps every earlier token's keys and values in memory so it does not have to recompute them.
With plain multi-head attention, every head in every layer stores a key and a value for every token. The memory per token is:
KV memory per token=2×layers×heads×dhead×bytes per number
where the 2 counts one key and one value, and dhead is the length of each head's vectors. For Llama 2 7B that is 2×32×32×128×2=524,288 bytes, or 512 KiB per token. At its full 4,096-token length, one conversation needs 2 GiB just for this memory.
The transformer paper itself lists this cost, comparing attention with the older layer types:
Every idea in the rest of the series cuts one of these two costs:
Part
Idea
What it cuts
2
MQA, GQA, MLA
memory: each token stores fewer or smaller keys and values
3
sliding-window and sparse attention
work and memory: each token looks at fewer tokens
4
linear attention, Gated DeltaNet, hybrids
both: most layers stop keeping a growing memory at all
Everything in this part runs, almost unchanged, inside products you use:
Translation. The original use case. Attention lets the model align words across languages, as Figure 3 of Bahdanau et al. showed above.
Chatbots and writing assistants. ChatGPT, Gemini, Llama, Qwen and DeepSeek are stacks of the multi-head attention built in this part (plus the efficiency tricks of Parts 2 to 4). Each word they write is one round of exactly this attention over the conversation so far.
Search and understanding. Encoder transformers such as BERT read a query and documents and match their meaning, not just their words. The heads Clark et al. found (objects, determiners, coreference) are part of why this works.
Code assistants. The same attention links a variable to where it was defined, or a closing bracket to its opening one, exactly like the previous-token and pronoun heads above.
Long conversations. Attention sinks matter in practice: some systems that keep a chat going for a very long time keep the first few tokens in memory for exactly this reason (Part 3 tests it).
Models turn text into tokens and tokens into vectors (lists of numbers).
Attention was invented so a model could look back at every word instead of squeezing a sentence into one summary.
Self-attention: each token makes a query, a key and a value. Queries are compared with keys (dot products), softmax turns the scores into weights, and the weights mix the values: softmax(QK⊤/d+M)V.
The causal mask stops tokens from seeing the future; every row of weights adds up to 1.
Dividing by √d keeps scores from growing with vector length. Without it, the top weight reached 0.95; with it, 0.107.
Multi-head attention runs several heads side by side. Our version matched PyTorch's to 1.7×10−16.
Real heads specialise: in Qwen2.5-0.5B, 68% are attention sinks, one follows the previous token (84%), and one links pronouns to nouns (6 out of 6 test sentences). BERT (Clark et al.) and Llama-2 (Xiao et al.) show the same patterns.
The √d comes from simple statistics: the dot product of two random vectors has variance d, so dividing by d brings it back to 1.
The transformer won because attention needs no sequential steps (fast, parallel training), at the price of O(n2) work.
Attention's work grows with the square of the text length, and its memory (the KV cache) with the length. Parts 2 to 4 are about cutting both.
Run it yourself
The script that produced every number above is code/attention/part1_attention.py, and the six-sentence test is part1_coref_check.py. They need Python with torch and transformers, and download Qwen2.5-0.5B (about 1 GB) the first time.
bash
pip install torch transformerspython part1_attention.py # writes results/part1.jsonpython part1_coref_check.py # the six pronoun sentences
Reading a real model's attention weights takes one setting:
python
from transformers import AutoModelForCausalLM, AutoTokenizertok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B")model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B", attn_implementation="eager")ids = tok("The cat sat on the mat because it was tired.", return_tensors="pt").input_idsattentions = model(ids, output_attentions=True).attentions # one tensor per layer: (batch, heads, tokens, tokens)