Why twelve heads instead of one — and the single line of code that separates GPT from RoBERTa.
The attention from chapter 03 works, but it has a serious limitation: each row of the attention map sums to 1. That means attention is a fixed budget. If a token spends 70% looking at its syntactic antecedent, only 30% is left for everything else.
But a word takes part in several relations at once. Take "mordeu" (bit) in "o cachorro grande mordeu o carteiro ontem" (the big dog bit the mailman yesterday):
With a single head, these four needs compete for the same 100% budget. The model is forced to average across relations that are qualitatively different — and an average of different things is usually a bad thing.
Imagine investigating a crime with a single forensic expert who has to split their time between fingerprints, DNA, cameras and witnesses. They will do everything halfway. Now imagine twelve specialized experts, each with 100% of their time devoted to one lead, all working at the same time, and a final report that combines the twelve findings.
That is multi-head attention. Each head has its own 100% budget and its own specialty — learned on its own, with nobody assigning it.
The detail that makes the idea elegant: multi-head does not cost more. Instead of twelve heads of 768 dimensions each, the model splits the existing 768 dimensions into twelve slices of 64.
The full procedure:
The matrix c_proj in GPT-2) that lets information from head 3 influence the dimensions that head 7 uses.
GPT-2 small, 5-token sentence, 12 heads: h (5, 768) | c_attn (768 -> 2304) (5, 2304) a single matmul split into 3 Q,K,V: (5, 768) each reshape into heads Q,K,V: (12, 5, 64) <- 12 heads in parallel | attention per head (12, 5, 64) 12 independent 5x5 maps | concat (join) (5, 768) regroups the slices c_proj (768 -> 768) (5, 768) mixes the heads | output (5, 768) same shape as the input
Nobody assigns roles. The specializations emerge from training. Interpretability studies on GPT-2 and BERT have consistently found heads such as:
| Head type | What it does |
|---|---|
| Positional | always looks at the immediately preceding token, regardless of content. A very-short-term memory. |
| Syntactic | links verb↔subject, noun↔adjective, preposition↔complement. |
| Induction | the most fascinating one: detects that the sequence "A B" has already appeared and, on seeing "A" again, points to "B". It is the mechanism behind in-context learning. |
| Coreferential | links a pronoun to its antecedent — the head that solves the trophy case. |
| Null / "sink" | dumps almost all of its attention on the first token. It looks like a defect, but it is an escape mechanism: when there is nothing relevant to fetch, the head needs somewhere to spend its 100% without contaminating the result. |
We now reach the most important point of the entire course. Pay close attention to this section, because everything in Part III depends on it.
GPT-2 is trained to predict the next word. We give it "O Brasil é" and it must produce "gigante". But, to be efficient, training does not run one word at a time — it processes the whole sentence at once and demands all the predictions simultaneously:
Input: O Brasil é gigante pela
Target: Brasil é gigante pela própria
^ ^ ^ ^ ^
5 predictions demanded at the same time, in a single forward pass.
And here is the catastrophe: the attention from chapter 03 lets each token see all the others — including the ones that come after it. While trying to predict that "gigante" comes after "é", the token "é" is looking directly at "gigante".
The model would learn the trivial task of copying the answer. When it came time to generate real text, where the future does not exist, it would have no idea what to do. This is label leakage — the cardinal sin of supervised learning.
Before the softmax, add a mask matrix that puts −∞ in every future position:
That is the right question, and the answer is beautiful. Zeroing the score does not help — a score of 0 still becomes
The −∞ works because:
In implementations a very negative number is used (-1e9, or torch.finfo(dtype).min) to avoid NaN in floating-point arithmetic.
Attention map AFTER the mask (5 tokens). "." = forbidden.
O Brasil e gigante pela
O [0.99 . . . . ] <- sees only itself
Brasil[0.41 0.59 . . . ]
e [0.12 0.55 0.33 . . ]
gigante[0.08 0.31 0.19 0.42 . ]
pela [0.05 0.22 0.11 0.38 0.24 ] <- sees everything up to here
Lower triangular. Each row sums to 1. The future does not exist.
Adding this mask turns an encoder into a decoder. That is all. GPT-2 and RoBERTa share the same attention equation, the same Transformer block, almost the same tokenizer and nearly identical sizes. The architectural difference between "the model that generates text" and "the model that understands text" is a triangular matrix added before the softmax.
Everything else — the training objective, the output format, what each one is for — is a consequence of that choice. We will come back to this, in detail, in chapter 11.
The causal mask brings a free gift. Since token 5 never influences token 3, the
So, when generating text, there is no need to recompute the whole sentence for every word. You store
This is what makes autoregressive generation viable in practice — and RoBERTa, being bidirectional, cannot do this, because in it appending a token changes the representation of all the others.
import torch, math import torch.nn as nn import torch.nn.functional as F class AtencaoMultiCabeca(nn.Module): def __init__(self, d_model=768, n_cabecas=12, causal=True): super().__init__() self.h = n_cabecas self.d_k = d_model // n_cabecas # 64 self.causal = causal # <<< GPT=True, RoBERTa=False self.c_attn = nn.Linear(d_model, 3 * d_model) # Q,K,V fused self.c_proj = nn.Linear(d_model, d_model) # W^O def forward(self, x): B, n, d = x.shape # 1) a single matmul produces Q, K and V at once q, k, v = self.c_attn(x).split(d, dim=2) # 2) (B, n, 768) -> (B, 12, n, 64): separates the heads q = q.view(B, n, self.h, self.d_k).transpose(1, 2) k = k.view(B, n, self.h, self.d_k).transpose(1, 2) v = v.view(B, n, self.h, self.d_k).transpose(1, 2) # 3) scaled scores -> (B, 12, n, n) att = (q @ k.transpose(-2, -1)) / math.sqrt(self.d_k) # 4) ===== THE LINE THAT DEFINES THE ARCHITECTURE ===== if self.causal: futuro = torch.triu(torch.ones(n, n, dtype=torch.bool), diagonal=1) att = att.masked_fill(futuro, float("-inf")) # ================================================== att = F.softmax(att, dim=-1) y = att @ v # (B, 12, n, 64) # 5) regroups the heads and mixes y = y.transpose(1, 2).contiguous().view(B, n, d) return self.c_proj(y)
Read step 4 again. causal=True → GPT-2. causal=False → RoBERTa. Everything else is identical.
.venv\Scripts\python.exe lab\04_multihead_mask.py
causal=True and False, and shows the two attention matrices side by side. The visual difference is immediate.[1.0, 0, 0, …]. What does that imply for the usefulness of the first token? Does it explain the "sink" head?The primary sources for the concepts in this chapter. The numbers in the text were measured in the lab; the ideas come from here.
Technical terms here carry their machine-learning meaning. The course glossary shows that meaning next to the others the same words have in IT.
Attention moves information between positions. But it is essentially a weighted average — a linear operation.
Chapter 05 shows the missing pieces: where non-linear thinking comes in, and how 48 layers get stacked without training blowing up.