Chapter 6 of 8
The full block, and a hard proof of causality
Now you put it all together. A transformer block has two sub-layers wrapped in the normalization and residuals from the last chapter: causal self-attention, where tokens share information, followed by a small MLP, where each token thinks on its own. It takes a T by C tensor in and returns a T by C tensor out, which is exactly why you can stack it into a deep model. This is the LEGO brick of every GPT, and you built a version of it in Course 1.
What is new here is how you prove it is causal. Course 1 checked that the attention weight matrix was lower-triangular, which is good evidence but only evidence. This chapter proves causality the way a scientist would, by intervention. You run the block, then reach in and CHANGE a token at a future position, and re-run. If the block is genuinely causal, the outputs at every earlier position must not move by a single bit, because an earlier token cannot be allowed to see a later one. That intervention test catches leaks a triangular-matrix check can miss: a stray bias, a wrong transpose, a normalization that accidentally mixes across time. It is the difference between believing your model is causal and knowing it.
What is new here is how you prove it is causal. Course 1 checked that the attention weight matrix was lower-triangular, which is good evidence but only evidence. This chapter proves causality the way a scientist would, by intervention. You run the block, then reach in and CHANGE a token at a future position, and re-run. If the block is genuinely causal, the outputs at every earlier position must not move by a single bit, because an earlier token cannot be allowed to see a later one. That intervention test catches leaks a triangular-matrix check can miss: a stray bias, a wrong transpose, a normalization that accidentally mixes across time. It is the difference between believing your model is causal and knowing it.
the block
x = x + attn(LN(x)); x = x + mlp(LN(x))
attention lets tokens share information, the MLP lets each token compute on its own, both with a norm and a residual
The lab: read it, then run it
#!/usr/bin/env python3
"""
LAB TF6: The full block, and a hard proof of causality.
Course 1 built a transformer block and checked the attention weight matrix was
lower-triangular. That is good, but here you prove causality the way a scientist
would: by INTERVENTION. Assemble the whole block (pre-norm, causal attention,
residual, MLP, residual), then reach in and CHANGE a token at a future position.
If the block is truly causal, the outputs at every EARLIER position must not move
by a single bit, while the changed position and later ones do move. That
intervention test catches leaks a triangular-matrix check can miss (a stray bias,
a wrong transpose, a normalization that mixes across time).
You verify shape (T x C in, T x C out, so blocks stack) and run the intervention.
Run: python3 modules/academy-content/labs/transformers/tf6-block-causality.py
"""
import sys
import math
import torch
import torch.nn as nn
import torch.nn.functional as F
torch.manual_seed(23) # reproducible
T, C = 6, 8
class CausalAttention(nn.Module):
def __init__(self):
super().__init__()
self.q = nn.Linear(C, C, bias=False)
self.k = nn.Linear(C, C, bias=False)
self.v = nn.Linear(C, C, bias=False)
self.register_buffer("tril", torch.tril(torch.ones(T, T)))
def forward(self, x):
q, k, v = self.q(x), self.k(x), self.v(x)
scores = q @ k.transpose(-2, -1) / math.sqrt(C)
scores = scores.masked_fill(self.tril == 0, float("-inf"))
return F.softmax(scores, dim=-1) @ v
class MLP(nn.Module):
def __init__(self):
super().__init__()
self.fc = nn.Linear(C, 4 * C)
self.proj = nn.Linear(4 * C, C)
def forward(self, x):
return self.proj(F.gelu(self.fc(x)))
class Block(nn.Module):
"""Pre-norm transformer block: the exact unit every GPT stacks."""
def __init__(self):
super().__init__()
self.ln1, self.ln2 = nn.LayerNorm(C), nn.LayerNorm(C)
self.attn, self.mlp = CausalAttention(), MLP()
def forward(self, x):
x = x + self.attn(self.ln1(x)) # tokens share information (causally)
x = x + self.mlp(self.ln2(x)) # each token thinks on its own
return x
block = Block().eval()
x = torch.randn(T, C)
# STEP 1: shape in == shape out, so blocks stack into a deep model.
with torch.no_grad():
out = block(x)
shape_ok = out.shape == (T, C)
print("STEP 1: block maps %s -> %s (stackable): %s" % (
tuple(x.shape), tuple(out.shape), "YES" if shape_ok else "NO"))
# STEP 2: intervention. Perturb the token at a FUTURE position j and re-run.
j = 3
x2 = x.clone()
x2[j] = torch.randn(C) # completely change the token at position j
with torch.no_grad():
out2 = block(x2)
# STEP 3: measure how much each position's output moved.
delta = (out - out2).abs().max(dim=-1).values # (T,) per-position max change
print("STEP 3: perturbed token at position %d, per-position output change:" % j)
for i, d in enumerate(delta.tolist()):
tag = "past " if i < j else ("CHANGED" if i == j else "future")
print(" t%d %s change=%.3e" % (i, tag, d))
# STEP 4: the invariant. Positions BEFORE j must be untouched (a token cannot see
# the future); position j and after must react.
past_frozen = delta[:j].abs().max().item() < 1e-6
future_reacts = delta[j:].abs().max().item() > 1e-4
print("STEP 4: earlier tokens unchanged: %s | changed+later tokens reacted: %s" % (
"YES" if past_frozen else "NO", "YES" if future_reacts else "NO"))
ok = shape_ok and past_frozen and future_reacts
print("")
print("CAUSAL MASK BLOCKS FUTURE TOKENS: %s" % ("YES" if ok else "NO"))
if not ok:
sys.exit(1)
print("")
print("Editing the future left the past bit-for-bit identical. That is causality,")
print("proven by intervention, and it is what lets a decoder predict left to right.")
Lab (read-only)
tf6-block-causality.pyAssemble the full block, verify it preserves shape, then perturb a future token and prove every earlier output stays bit-for-bit identical.
Proves: CAUSAL MASK BLOCKS FUTURE TOKENS: YES
The intervention was decisive. You changed the token at position three completely, and the outputs at positions zero, one, and two did not move by even a floating-point bit, a change of exactly zero. Position three and everything after it reacted, as they must. That is causality demonstrated, not assumed: information flows strictly left to right, so a token can only ever be built from itself and its past. This is the property that makes next-token prediction sound, because when the model predicts token four it genuinely has not seen token four or beyond. And the block preserved its T by C shape, so you can stack a dozen of these and build the real thing. You now understand the internals cold. The last two chapters step back to the big picture: how models improve with scale, and why this architecture won.
Check your understanding
1. What are the two sub-layers of a transformer block?
2. Why must the block map T by C to T by C?
3. How did the lab prove causality?
4. Why does causality make next-token prediction sound?