Two tensors, the same shape, and not the same tensor
Split a head dimension the wrong way, catch it with a value comparison rather than a shape check, then compare a hand-written attention against PyTorch's own.
Attention is a sequence of explicit relationships between represented vectors, and almost every stage can be wrong while producing exactly the shape you predicted. Splitting the embedding across heads is the smallest example: a reshape gives the shape you wanted and not the arrangement of values you meant.
This program composes the chapter’s pieces into one run: the good and bad head split, a round trip that catches the bad one, a hand-written attention compared against PyTorch’s, and a future-token intervention.
Run it
Save this as attention_heads.py and run it with Python 3.11 or later. It
needs PyTorch: pip install torch, then python attention_heads.py.
"""Attention: which position is comparing with which, and how to prove it."""
import math
import torch
import torch.nn.functional as F
B, T, E, Nh = 2, 5, 12, 3
Dh = E // Nh
def split_heads(x, num_heads):
B, T, E = x.shape
return x.reshape(B, T, num_heads, E // num_heads).transpose(1, 2)
def merge_heads(x):
B, Nh, T, Dh = x.shape
return x.transpose(1, 2).reshape(B, T, Nh * Dh)
def manual_attention(q, k, v, allow):
scores = q @ k.transpose(-2, -1) / math.sqrt(q.shape[-1])
masked = scores.masked_fill(~allow, float("-inf"))
weights = torch.softmax(masked, dim=-1)
return weights @ v, weights
x = torch.arange(B * T * E).reshape(B, T, E)
good = split_heads(x, Nh)
bad = x.reshape(B, Nh, T, Dh)
print(f"good {tuple(good.shape)} bad {tuple(bad.shape)} shapes equal: {good.shape == bad.shape}")
print(f"same dtype: {good.dtype == bad.dtype} same values: {torch.equal(good, bad)}")
print(f"round trip, good: {torch.equal(merge_heads(good), x)} round trip, bad: {torch.equal(merge_heads(bad), x)}")
print("good head 0 of batch 0:")
print(good[0, 0])
print("bad head 0 of batch 0:")
print(bad[0, 0])
torch.manual_seed(0)
q, k, v = torch.randn(B, Nh, T, Dh), torch.randn(B, Nh, T, Dh), torch.randn(B, Nh, T, Dh)
allowed = torch.ones(T, T, dtype=torch.bool).tril()
mine, weights = manual_attention(q, k, v, allowed)
theirs = F.scaled_dot_product_attention(q, k, v, attn_mask=allowed, dropout_p=0.0)
print(f"manual vs SDPA, max abs diff: {(mine - theirs).abs().max():.2e}")
print(f"row sums, max error from 1: {(weights.sum(-1) - 1).abs().max():.2e}")
forbidden = weights.masked_select(~allowed)
print(f"forbidden weight max: {forbidden.abs().max():.1e}")
i = 2
v2 = v.clone()
v2[:, :, i + 1:] = torch.randn_like(v2[:, :, i + 1:])
y1 = manual_attention(q, k, v, allowed)[0]
y2 = manual_attention(q, k, v2, allowed)[0]
print(f"max |y1-y2| over positions 0..{i}: {(y1[:, :, :i + 1] - y2[:, :, :i + 1]).abs().max():.2e}")
print(f"max |y1-y2| over positions {i + 1}..: {(y1[:, :, i + 1:] - y2[:, :, i + 1:]).abs().max():.4f}")Expected outcome
good (2, 3, 5, 4) bad (2, 3, 5, 4) shapes equal: True
same dtype: True same values: False
round trip, good: True round trip, bad: False
good head 0 of batch 0:
tensor([[ 0, 1, 2, 3],
[12, 13, 14, 15],
[24, 25, 26, 27],
[36, 37, 38, 39],
[48, 49, 50, 51]])
bad head 0 of batch 0:
tensor([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19]])
manual vs SDPA, max abs diff: 2.38e-07
row sums, max error from 1: 1.19e-07
forbidden weight max: 0.0e+00
max |y1-y2| over positions 0..2: 0.00e+00
max |y1-y2| over positions 3..: 2.4035Shape, then values, then behaviour
A shape assertion cannot see the bad split: it has the right shape. The chapter’s cheapest test is a round trip on a deterministic tensor: splitting and merging heads must return the original exactly.
def split_heads(x, num_heads):
B, T, E = x.shape
assert E % num_heads == 0, f"E={E} not divisible by num_heads={num_heads}"
Dh = E // num_heads
return x.reshape(B, T, num_heads, Dh).transpose(1, 2)The printed heads show the difference in arrangement. The good head 0 owns the first four embedding dimensions of every position. The bad head 0 owns the first twenty numbers of the flat sequence.
The last two lines are the behavioural test. Replacing the value vectors
after i must not move the output at 0..i under a causal mask, and it does
move the output after i. A mask that looks causal is a claim; an output
that does not move is evidence.
Mechanism and limitations
Documented: the chapter shows two splits with identical shape, dtype and
element multiset but different values, proves that
merge_heads(split_heads(x)) is exact for the good split and not the bad
one, and compares a manual implementation with PyTorch’s own.
Observed: the run above. Both splits had shape (2, 3, 5, 4) and the same
dtype, but not the same values. The round trip was exact for the good split
and false for the bad one. The manual attention matched PyTorch’s to
2.38e-07, the forbidden weight was 0.0e+00, and the intervention moved
positions after i while leaving 0..i at zero.
Not shown. The program composes the chapter’s fragments into one run: it uses random Q, K and V instead of trained projections, and the intervention replaces value vectors rather than a trained model’s input. It checks one head count and one mask.
Canonical source , the chapter this example composes. Read the chapter .
Understand this example
Copy this prompt into your AI tool. No code runs here.
Apply this example
Copy this prompt into your AI tool. No code runs here.