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:

First: words become numbers

A computer cannot read words. So before anything else, a language model turns text into tokens, and every token into a list of numbers.

Each token becomes a list of numbers (a vector). Real models use hundreds or thousands of numbers per token.cat+0.8-1.2+0.3+2.1…dog+0.7-1.0+0.5+1.9…mat-1.4+0.6+1.1-0.2…"cat" and "dog" get similar numbers:they mean similar things."mat" gets different numbers.
Every token becomes a vector. The model learns these numbers during training so that words with similar meanings get similar numbers.

From here on, "a token" and "the token's vector" mean the same thing: attention only ever works with the numbers.

The problem attention solved

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.

Before attention: the decoder sees one summary vectorWith attention: the decoder looks back at every inputLechatestnoirone vectorThe cat is …Everything must squeeze through one fixed-size state.Le0.06chat0.81est0.08noir0.05The cat …To write "cat", it looks mostly at "chat".
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.

The first attention equations

Their version works like this. The encoder leaves one vector hjh_j for each source word jj (they call these annotations). When the decoder is about to write target word ii, it does three things:

eij=a(si−1,hj)αij=exp⁡(eij)∑k=1Txexp⁡(eik)ci=∑j=1Txαij hje_{ij} = a(s_{i-1}, h_j) \qquad\qquad \alpha_{ij} = \frac{\exp(e_{ij})}{\sum_{k=1}^{T_x} \exp(e_{ik})} \qquad\qquad c_i = \sum_{j=1}^{T_x} \alpha_{ij}\, h_j

where:

  • si−1s_{i-1} is the decoder's current state (what it has written so far);
  • aa is a small learned network, the alignment model, that scores how well source word jj fits the next target word;
  • eije_{ij} is that score, and αij\alpha_{ij} is the score turned into a weight by softmax (the same softmax you will meet below);
  • TxT_x is the number of source words;
  • cic_i 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.

Two small tools: the dot product and softmax

Attention is built from two simple tools. It is worth meeting them first.

Tool 1: the dot product measures "how similar"

As an equation, for two vectors aa and bb with dd numbers each:

a⋅b=a1b1+a2b2+⋯+adbda \cdot b = a_1 b_1 + a_2 b_2 + \dots + a_d b_d

where a1a_1 is the first number of aa, b1b_1 the first number of bb, and so on.

For example, [1,2]⋅[3,4]=1×3+2×4=11[1, 2] \cdot [3, 4] = 1 \times 3 + 2 \times 4 = 11.

Why is this useful? Because it tells you how much two vectors point the same way:

same directiondot product: +0.95(big and positive)at a right angledot product: +0.00(zero)oppositedot product: -0.95(negative)Blue: a query. Orange: a key. Both have length 1, so the dot product is the cosine of the angle between them.
The dot product is big when two vectors point the same way, zero when they are at a right angle, and negative when they point in opposite directions.

So "is this word relevant to that word?" can become "is the dot product of their vectors big?"

Tool 2: softmax turns scores into percentages

Dot products can be any number: big, small, or negative. Attention needs weights instead: positive numbers that add up to 1, like percentages.

softmax⁡(si)=esies1+es2+⋯+esn\operatorname{softmax}(s_i) = \frac{e^{s_i}}{e^{s_1} + e^{s_2} + \dots + e^{s_n}}

where s1,…,sns_1, \dots, s_n are the scores, and e≈2.718e \approx 2.718. Raising ee to a power makes every score positive; dividing by the total makes them add up to 1.

scores (any size, can be negative)weights after softmax (add up to 1)+2.0cat0.64cat+1.0sat0.23sat+0.1on0.10on-1.0the0.03thesoftmaxBigger score → bigger weight. All weights are positive and sum to 1.
Softmax in action. Four scores, one of them negative, become four weights that are all positive and add up to 1.

Self-attention, step by step

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.

XtokensQ× WqK× WkV× WvQ Kᵀ / √dhow well each pair matchesmaskhide the futuresoftmaxrows sum to 1AA × V: a weighted mix of valuesZnew tokens
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:

  1. Score. Compare every query with every key using the dot product. A big score means "this key matches what I am looking for".
  2. Scale. Divide the scores by d\sqrt{d} (the square root of the vector length). The next section shows why.
  3. Hide the future. A model that writes left to right must not peek at words that come later, so those scores are blocked.
  4. Softmax. Turn each token's scores into weights that add up to 1.
  5. 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⁡ ⁣(QK⊤d+M)V\text{Attention}(Q, K, V) = \operatorname{softmax}\!\left(\frac{QK^\top}{\sqrt{d}} + M\right) V

where:

  • QQ, KK, VV are the queries, keys and values of all the tokens, stacked as rows;
  • QK⊤QK^\top is every query's dot product with every key (the ⊤^\top, "transpose", just flips KK so the multiplication lines up);
  • dd is the length of each key vector;
  • MM is the causal mask: 00 where looking is allowed, −∞-\infty 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:

A tiny example you can check

Here is the same thing in a few lines of Python, using PyTorch:

python
import math, torch

def 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.

plain text
attention weights (rows = query token, columns = key token):
     The  1.00  0.00  0.00  0.00
     cat  0.25  0.75  0.00  0.00
     sat  0.31  0.26  0.42  0.00
    down  0.08  0.29  0.09  0.54
   row sums: [1.0, 1.0, 1.0, 1.0]
TheThe → The: 1.001.00catcat → The: 0.250.25cat → cat: 0.750.75satsat → The: 0.310.31sat → cat: 0.260.26sat → sat: 0.420.42downdown → The: 0.080.08down → cat: 0.290.29down → sat: 0.090.09down → down: 0.540.54Thecatsatdowneach row: where one token looksempty cells: the future (masked)every row sums to 1
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.

Why divide by √d?

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 dd 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\sqrt{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 dSpread of the dot productsTop weight without ÷√dTop weight with ÷√d
164.00.5760.107
648.00.7960.108
25616.00.8960.107
1,02432.00.9500.108
0.000.250.500.751.0016642561,024without ÷√d, 16: 0.58without ÷√d, 64: 0.80without ÷√d, 256: 0.90without ÷√d, 1,024: 0.95with ÷√d, 16: 0.11with ÷√d, 64: 0.11with ÷√d, 256: 0.11with ÷√d, 1,024: 0.11without ÷√d: 0.95with ÷√d: 0.11vector size d (log scale)largest weight in a 64-key softmax
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\sqrt{d}: 4, 8, 16, 32. Without the division, at d=1,024d = 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\sqrt{d} cancels the growth: the top weight stays near 0.107 at every size.

Where the √d comes from (the math in four lines)

Suppose every number in qq and kk is random, with mean 0 and variance 1, and all are independent. Look at one term of the dot product, qikiq_i k_i:

E[qiki]=E[qi] E[ki]=0⋅0=0,Var⁡(qiki)=E[qi2] E[ki2]=1⋅1=1\mathbb{E}[q_i k_i] = \mathbb{E}[q_i]\,\mathbb{E}[k_i] = 0 \cdot 0 = 0, \qquad \operatorname{Var}(q_i k_i) = \mathbb{E}[q_i^2]\,\mathbb{E}[k_i^2] = 1 \cdot 1 = 1

The dot product adds dd such independent terms, and variances of independent terms simply add up:

Var⁡(q⋅k)=∑i=1dVar⁡(qiki)=d⟹spread(q⋅k)=d\operatorname{Var}(q \cdot k) = \sum_{i=1}^{d} \operatorname{Var}(q_i k_i) = d \qquad\Longrightarrow\qquad \text{spread}(q \cdot k) = \sqrt{d}

So dividing by d\sqrt{d} brings the spread back to exactly 1, whatever the vector length:

Var⁡ ⁣(q⋅kd)=dd=1\operatorname{Var}\!\left(\frac{q \cdot k}{\sqrt{d}}\right) = \frac{d}{d} = 1

where E\mathbb{E} means "average value" (expected value) and Var⁡\operatorname{Var} means variance. That is exactly what my measurement showed: spreads of 4, 8, 16, 32 for dd = 16, 64, 256, 1,024, which are d\sqrt{d}.

The transformer paper gives the same reasoning, in one sentence and one footnote:

Many heads instead of one

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.

xhead 1: own Wq, Wk, WvA1·V1head 2: own Wq, Wk, WvA2·V2head 3: own Wq, Wk, WvA3·V3head 4: own Wq, Wk, WvA4·V4concatenateall heads× WoEach head learns its own pattern; the output projection blends them back into one vector per token.
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.

As equations:

headi=Attention(XWq(i),  XWk(i),  XWv(i))\text{head}_i = \text{Attention}(X W_q^{(i)},\; X W_k^{(i)},\; X W_v^{(i)}) MultiHead(X)=Concat(head1,…,headh) Wo\text{MultiHead}(X) = \text{Concat}(\text{head}_1, \dots, \text{head}_h)\, W_o

where:

  • XX holds the vectors of all the tokens;
  • Wq(i),Wk(i),Wv(i)W_q^{(i)}, W_k^{(i)}, W_v^{(i)} are head ii's own query, key and value matrices;
  • hh is the number of heads;
  • Concat places the heads' results side by side into one long vector per token;
  • WoW_o is the output matrix that mixes the heads back together.

Each head works with shorter vectors, so hh heads cost about the same as one big head:

dhead=dmodelhfor example5128=64d_{\text{head}} = \frac{d_{\text{model}}}{h} \qquad\text{for example}\qquad \frac{512}{8} = 64

where dmodeld_{\text{model}} is the length of each token's vector and hh the number of heads. The original transformer used exactly these numbers: 512 per token, 8 heads, 64 per head. Total work is h×dhead=dmodelh \times d_{\text{head}} = d_{\text{model}}, 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

Proof that it is right

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-16
our MHA vs torch.nn.MultiheadAttention:     max |diff| = 1.7e-16

The largest difference is 1.7×10−161.7 \times 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.

What real attention heads learn

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:

attention sink (L11 H7)TheThe → The: 1.00catcat → The: 1.00cat → cat: 0.00satsat → The: 1.00sat → cat: 0.00sat → sat: 0.00onon → The: 1.00on → cat: 0.00on → sat: 0.00on → on: 0.00thethe → The: 1.00the → cat: 0.00the → sat: 0.00the → on: 0.00the → the: 0.00matmat → The: 1.00mat → cat: 0.00mat → sat: 0.00mat → on: 0.00mat → the: 0.00mat → mat: 0.00becausebecause → The: 1.00because → cat: 0.00because → sat: 0.00because → on: 0.00because → the: 0.00because → mat: 0.00because → because: 0.00itit → The: 1.00it → cat: 0.00it → sat: 0.00it → on: 0.00it → the: 0.00it → mat: 0.00it → because: 0.00it → it: 0.00waswas → The: 1.00was → cat: 0.00was → sat: 0.00was → on: 0.00was → the: 0.00was → mat: 0.00was → because: 0.00was → it: 0.00was → was: 0.00tiredtired → The: 1.00tired → cat: 0.00tired → sat: 0.00tired → on: 0.00tired → the: 0.00tired → mat: 0.00tired → because: 0.00tired → it: 0.00tired → was: 0.00tired → tired: 0.00.. → The: 1.00. → cat: 0.00. → sat: 0.00. → on: 0.00. → the: 0.00. → mat: 0.00. → because: 0.00. → it: 0.00. → was: 0.00. → tired: 0.00. → .: 0.00Thecatsatonthematbecauseitwastired.previous token (L8 H7)TheThe → The: 1.00catcat → The: 0.95cat → cat: 0.05satsat → The: 0.01sat → cat: 0.98sat → sat: 0.00onon → The: 0.01on → cat: 0.02on → sat: 0.96on → on: 0.00thethe → The: 0.01the → cat: 0.00the → sat: 0.08the → on: 0.91the → the: 0.00matmat → The: 0.09mat → cat: 0.00mat → sat: 0.00mat → on: 0.18mat → the: 0.72mat → mat: 0.02becausebecause → The: 0.15because → cat: 0.00because → sat: 0.00because → on: 0.00because → the: 0.08because → mat: 0.74because → because: 0.03itit → The: 0.00it → cat: 0.00it → sat: 0.00it → on: 0.00it → the: 0.00it → mat: 0.01it → because: 0.98it → it: 0.00waswas → The: 0.01was → cat: 0.00was → sat: 0.00was → on: 0.00was → the: 0.00was → mat: 0.00was → because: 0.20was → it: 0.78was → was: 0.00tiredtired → The: 0.07tired → cat: 0.00tired → sat: 0.01tired → on: 0.00tired → the: 0.00tired → mat: 0.00tired → because: 0.00tired → it: 0.31tired → was: 0.59tired → tired: 0.01.. → The: 0.12. → cat: 0.00. → sat: 0.00. → on: 0.00. → the: 0.00. → mat: 0.00. → because: 0.00. → it: 0.00. → was: 0.07. → tired: 0.78. → .: 0.04Thecatsatonthematbecauseitwastired.pronoun → noun (L5 H5)TheThe → The: 1.00catcat → The: 0.75cat → cat: 0.25satsat → The: 0.01sat → cat: 0.97sat → sat: 0.01onon → The: 0.16on → cat: 0.74on → sat: 0.06on → on: 0.04thethe → The: 0.31the → cat: 0.57the → sat: 0.06the → on: 0.05the → the: 0.01matmat → The: 0.62mat → cat: 0.28mat → sat: 0.02mat → on: 0.02mat → the: 0.00mat → mat: 0.05becausebecause → The: 0.24because → cat: 0.41because → sat: 0.20because → on: 0.09because → the: 0.01because → mat: 0.02because → because: 0.03itit → The: 0.01it → cat: 0.83it → sat: 0.07it → on: 0.04it → the: 0.00it → mat: 0.02it → because: 0.00it → it: 0.01waswas → The: 0.09was → cat: 0.29was → sat: 0.25was → on: 0.15was → the: 0.03was → mat: 0.13was → because: 0.04was → it: 0.02was → was: 0.01tiredtired → The: 0.07tired → cat: 0.78tired → sat: 0.05tired → on: 0.03tired → the: 0.00tired → mat: 0.04tired → because: 0.01tired → it: 0.01tired → was: 0.00tired → tired: 0.00.. → The: 0.44. → cat: 0.14. → sat: 0.09. → on: 0.02. → the: 0.00. → mat: 0.01. → because: 0.06. → it: 0.00. → was: 0.00. → tired: 0.00. → .: 0.22Thecatsatonthematbecauseitwastired.
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:

SentenceWordWhere it looked mostWeight
The dog chased the ball because it was bored.itdog0.90
The trophy did not fit in the suitcase because it was too big.ittrophy0.62
My sister bought a new car and she loves driving it.shesister0.86
The engineers fixed the server after it crashed twice.itserver0.40
The scientist published the paper because she was proud of it.shescientist0.78
The children ate the cake because they were hungry.theychildren0.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.

The same patterns, found by researchers

These findings are not special to Qwen. Two well-known papers found the same kinds of heads in other models.

The cost that every other part tries to cut

Attention has two costs, and both grow with the length of the text:

  1. Work. Every token compares itself with every earlier token. Twice as much text means four times as many comparisons.
  2. 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\text{KV memory per token} = 2 \times \text{layers} \times \text{heads} \times d_{\text{head}} \times \text{bytes per number}

where the 2 counts one key and one value, and dheadd_{\text{head}} is the length of each head's vectors. For Llama 2 7B that is 2×32×32×128×2=524,2882 \times 32 \times 32 \times 128 \times 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:

PartIdeaWhat it cuts
2MQA, GQA, MLAmemory: each token stores fewer or smaller keys and values
3sliding-window and sparse attentionwork and memory: each token looks at fewer tokens
4linear attention, Gated DeltaNet, hybridsboth: most layers stop keeping a growing memory at all

How attention changed everything

It is hard to overstate what these two papers started. A short timeline of what happened next:

YearWhat happenedRole of attention
2014Bahdanau et al.: attention for translationa helper for an RNN translator
2017Vaswani et al.: the transformerattention replaces the RNN entirely
2018BERT and GPTtransformers pre-trained on huge amounts of text
2020Vision Transformer (ViT)images cut into patches and treated as tokens
2021 to 2022AlphaFold 2, Whisperattention for protein structures and for speech
2022 onwardChatGPT, Llama, Qwen, Gemma, DeepSeekevery large chatbot is a stack of attention layers

Use cases: where you meet this today

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).

The translation results that started it all:

Summary

  • 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\operatorname{softmax}(QK^\top/\sqrt{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−161.7 \times 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 dd, so dividing by d\sqrt{d} brings it back to 1.
  • The transformer won because attention needs no sequential steps (fast, parallel training), at the price of O(n2)O(n^2) 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 transformers
python part1_attention.py      # writes results/part1.json
python part1_coref_check.py    # the six pronoun sentences

Reading a real model's attention weights takes one setting:

python
from transformers import AutoModelForCausalLM, AutoTokenizer
tok = 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_ids
attentions = model(ids, output_attentions=True).attentions   # one tensor per layer: (batch, heads, tokens, tokens)

References

  1. D. Bahdanau, K. Cho, Y. Bengio. Neural Machine Translation by Jointly Learning to Align and Translate. 2014.
  2. A. Vaswani et al. Attention Is All You Need. NeurIPS 2017.
  3. G. Xiao et al. Efficient Streaming Language Models with Attention Sinks. ICLR 2024.
  4. K. Clark, U. Khandelwal, O. Levy, C. Manning. What Does BERT Look At? An Analysis of BERT's Attention. 2019.
  5. Qwen Team. Qwen2.5-0.5B.