Skip to content
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.
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

labs/transformers/tf6-block-causality.py
#!/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.py

Assemble 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. 1. What are the two sub-layers of a transformer block?

  2. 2. Why must the block map T by C to T by C?

  3. 3. How did the lab prove causality?

  4. 4. Why does causality make next-token prediction sound?