Causal Masking
Causal masking hides every future token before attention weights are computed, so each position can only blend in itself and the tokens before it.
Why Does This Exist?
Start with self-attention. Each token builds three vectors: a query that asks, a key that advertises, and a value that carries content. It scores its query against every key, runs softmax, and blends the values. Left alone, that lookup sees the whole sequence at once, past and future together.
That full view breaks next-token training. Say you train a model to predict token 4 from the representation at token 3, but token 3 was allowed to attend to token 4. The model does not learn grammar or facts. It learns to copy the answer it already saw. Training loss looks excellent and means nothing. At generation time token 4 does not exist yet when you need to predict it, so the trick collapses the moment you use the model.
In our three-token example the raw future score is 5.0, larger than the true past scores 1.0 and 3.0. Without a mask the future would take 0.867 of the weight. The mask is what stops that theft.
This page covers the mask itself: the added matrix, the triangle it leaves, and the hand-computed weights. GPT is the decoder-only model built from this mask. Next-token prediction is the training job the mask keeps honest. BERT is the opposite choice, an encoder with no mask that reads both directions. Cross-attention is the decoder's other lookup, reading a separate sequence, so it needs no causal mask.
Think of It Like This
The mystery novel with glued pages
You read a mystery where every future chapter is glued shut. You can flip back and re-read any chapter you finished, and you can stare at the page you are on, but you cannot peel open what comes next. To guess the next page you only get the past plus the present.
That is the mask. Glued pages are the future positions set to negative infinity. Open pages are the allowed positions set to zero. Your guess blends only the open pages, the same way attention blends only unmasked values.
Where it stops: glue does not change the printed words, but attention rewrites each token from the tokens it was allowed to see. And a novel has one fixed future, while a model faces many possible next tokens at every step.
How It Actually Works
Attention takes three matrices. holds queries, one row per position asking what to look for. holds keys, one row per position advertising what it offers. holds values, the content you blend. is the width of one query or key vector, used only to keep dot products from growing with width. is the causal mask, an by grid for a sequence of length . names the query row and names the key column.
Here when and when . Adding leaves a score alone. Adding sends it to negative infinity, and , so softmax gives that position exactly zero weight. Because the lands before softmax, the surviving weights still sum to 1.
The mask rule in one grid
Keep position when the key sits at or before the query , block it when the key sits after the query. The kept cells form the lower triangle including the diagonal. The diagonal always stays open, so every token attends to itself. The whole upper triangle stays blocked, so no query ever sees a later key. The diagram draws this grid with the blocked cells in black and the worked row outlined.
Worked example: row two turns 1.0 and 3.0 into 0.119 and 0.881
Use three positions with scores already scaled, so you can follow the softmax by hand. The raw scores put 5.0 on the future, on purpose, to prove the mask wins over large values:
Add the mask. Future entries become :
Row one has one option. alone, so the weights are .
Row two is the worked row. Exponentiate the two survivors:
So row two gives . The future weight is exactly 0, not small. Without the mask the same row would softmax into , and the future would steal 0.867. That gap between 0.867 and 0 is the whole point.
Row three is fully visible. , , , sum , so the weights are . The diagram shows these same six numbers: scores 1.0 and 3.0 on the left, weights 0.119 and 0.881 on the right.
Why the triangle trains every position at once
The mask applies per row instead of cutting the input short. Position 2 predicts from positions 1 to 2, position 3 predicts from positions 1 to 3, and both predictions compute in the same forward pass. Every position supplies one fair next-token label, none of them peeked, so a 1,000-token page gives 1,000 training signals at once. That parallelism plus the no-peeking guarantee is why decoder-only models scaled so well. At generation time the same rule holds: each new token only ever sees what came before it.
Code
The point is the check: build the mask, add it before softmax, and confirm the future lands at exactly zero even though its raw 5.0 is the largest score.
import numpy as np
def softmax(z: np.ndarray) -> np.ndarray: e: np.ndarray = np.exp(z - np.max(z, axis=-1, keepdims=True)) return e / np.sum(e, axis=-1, keepdims=True)
def causal_mask(n: int) -> np.ndarray: upper: np.ndarray = np.triu(np.ones((n, n)), k=1) return np.where(upper == 1, -np.inf, 0.0)
scores: np.ndarray = np.array([ [2.0, 5.0, 5.0], [1.0, 3.0, 5.0], [1.0, 2.0, 3.0],])weights: np.ndarray = softmax(scores + causal_mask(3))print(np.round(weights, 3))# -> [[1. 0. 0. ]# [0.119 0.881 0. ]# [0.09 0.245 0.665]]Row one keeps all weight on itself. Row two splits 0.119 and 0.881 across the past only. The 5.0 future scores contribute nothing, even though they are the largest raw numbers in their rows.
Watch Out For
Zeroing weights after softmax instead of masking before
Softmax first, then setting future weights to zero, looks the same but breaks the math. Take row two raw . Correct masking gives summing to 1. Late zeroing softmaxes the full row into , then blanks the last entry to leave , which sums to 0.133.
Symptom: rows that sum well below 1, outputs that look muted, and a bug that survives casual inspection because the future entries do read zero. Fix: always add negative infinity to the scores before softmax, never touch the weights after.
Putting a causal mask on an encoder that needs full context
A causal mask is for generation, not for labeling a complete input. An encoder for sentiment, entailment, or span tagging should read left and right at once, the way BERT does.
Symptom: accuracy drops on jobs where the right context decides the label, like negation or coreference, and a bigger model does not fix it because the information was hidden by design. Fix: leave encoders unmasked, keep the causal mask in decoders, and check which stack you edited when you assemble a transformer from parts.
The Quick Version
- Causal masking adds negative infinity to every score where the key sits after the query, and zero elsewhere.
- After softmax those future positions get exactly zero weight, so row two turns scores 1.0 and 3.0 into weights 0.119 and 0.881.
- The allowed pattern is a lower triangle including the diagonal, so each token sees itself and every earlier token.
- Without the mask the 5.0 future score in row two would take 0.867 of the weight, letting training copy the answer.
- Masking must happen before softmax, or the surviving weights will not sum to 1.
- Causal masks belong in decoders that generate, while encoders that label complete inputs stay unmasked.