GPT-2 · RoBERTa Lab · Part I — The foundation
04

Multi-head
and the causal mask

Why twelve heads instead of one — and the single line of code that separates GPT from RoBERTa.

Chapter 04 of 15 Prerequisite: chapter 03

1 The problem with a single head

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.

Analogy

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.

2 How it works: divide, don't multiply

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.

dk = dmodelh = 76812 = 64 h = number of heads. Each head works in a 64-dimensional subspace. The total cost is identical to that of one 768-dimensional head — only the internal organization changed.

The full procedure:

headi = Attention(hWiQ, hWiK, hWiV)
MultiHead(h) = Concat(head1, …, headh) WO Concatenates the 12 outputs of 64 dimensions, recovering 768. Then passes through WO (768×768, learned), which mixes the contributions — it is the one that decides how to combine the twelve findings.

The matrix WO is essential and frequently overlooked. Without it, the twelve heads would sit in sealed compartments, each writing into its own 64 dimensions and never talking to each other. It is WO (called c_proj in GPT-2) that lets information from head 3 influence the dimensions that head 7 uses.

The dimensions, in practice

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

What the heads actually learn

Nobody assigns roles. The specializations emerge from training. Interpretability studies on GPT-2 and BERT have consistently found heads such as:

Head typeWhat it does
Positionalalways looks at the immediately preceding token, regardless of content. A very-short-term memory.
Syntacticlinks verb↔subject, noun↔adjective, preposition↔complement.
Inductionthe 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.
Coreferentiallinks 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.

3 The causal mask — the line that defines everything

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.

The problem

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.

The solution: forbid it explicitly

Before the softmax, add a mask matrix that puts −∞ in every future position:

Mij = 0   if j ≤ i  (past or present: allowed) −∞  if j > i  (future: forbidden)
A = softmax(QK⊤√dk + M)

Why −∞ and not zero?

That is the right question, and the answer is beautiful. Zeroing the score does not help — a score of 0 still becomes e0 = 1 in the softmax, a perfectly respectable weight. And zeroing the weight after the softmax would break the sum-to-1.

The −∞ works because:

exp(−∞) = 0 The weight becomes exactly zero before normalization. So the softmax denominator also ignores those positions, and the rows keep summing to exactly 1 — just distributed only over the past. Mathematically clean, no patching.

In implementations a very negative number is used (-1e9, or torch.finfo(dtype).min) to avoid NaN in floating-point arithmetic.

The triangular shape

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.
The most important sentence of the course

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 practical consequence: the KV cache

The causal mask brings a free gift. Since token 5 never influences token 3, the K and V vectors of earlier positions never change when a new token is appended.

So, when generating text, there is no need to recompute the whole sentence for every word. You store K and V for everything that has already passed (the KV cache) and compute only the new token. Generation drops from O(n²) per token to O(n).

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.

4 The complete code

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.

5 In the lab

.venv\Scripts\python.exe lab\04_multihead_mask.py
  1. Prints the triangular mask in ASCII, so you can see its shape.
  2. Runs the same input with causal=True and False, and shows the two attention matrices side by side. The visual difference is immediate.
  3. Proves the leakage: changes a token at the end of the sentence and measures how much the representation at the start changed. In causal mode: zero. In bidirectional mode: quite a lot. This is the test that demonstrates causality instead of asserting it.
  4. Portrait of the 12 heads of layer 0 in the real GPT-2: for each head, computes the average distance it looks at and the entropy of its distribution. You will identify the positional heads (distance ≈ 1) and the "sink" heads (extremely low entropy, everything on token 0).
  5. Measures the gain from the KV cache by generating 50 tokens with and without it.

Exercise 4 — predict before running

  1. In a causal mask with n=1024, what fraction of the n² positions is wasted on −∞? (Roughly half — and no common GPU takes advantage of this. Why do you suppose nobody optimizes it?)
  2. The first token of a causal sequence can only look at itself. Its attention row is always [1.0, 0, 0, …]. What does that imply for the usefulness of the first token? Does it explain the "sink" head?
  3. If you applied the mask after the softmax (zeroing the future weights), what would break?
  4. Hard: GPT-2 XL has 25 heads and dmodel=1600. So dk = 64. Every GPT-2 keeps dk = 64, varying only the number of heads. Why that constant? (Think about √dk and the expressiveness of a subspace.)
References

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.

  1. Vaswani, A. et al. (2017). Attention Is All You Need. NeurIPS 2017. arXiv:1706.03762
  2. Pope, R. et al. (2023). Efficiently Scaling Transformer Inference. MLSys 2023. arXiv:2211.05102

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.