AI Grimoire
Sheet
statusstandard
difficultyintroductory
timeO(n²)
described2017
revised9d ago

Causal Masking

Attention is unordered and sees everything. A lower-triangular mask is what makes a decoder autoregressive, and what lets a single sequence supply n next-token predictions at once.

Standing

Current standardPart of the default recipe. Present in most frontier models trained today, and the thing a new design departs from rather than argues for.

judged as of 2026-09 · what the labels mean

Theory

Attention as written attends to everything. To model p(xtx<t)p(x_t \mid x_{<t}) the operation has to be prevented from reading forward, and the cheapest way to do that is to add -\infty to the logits it must not use.

Mij={0jij>iA=softmax ⁣(QKdk+M)M_{ij} = \begin{cases} 0 & j \le i \\ -\infty & j > i \end{cases} \qquad A = \softmax\!\left( \frac{QK^{\top}}{\sqrt{d_k}} + M \right)
eq. 1 — additive mask, applied before the softmax

The mask is additive rather than multiplicative because it has to act before normalisation. Zeroing entries of AA afterwards would leave the rows summing to less than one; adding -\infty to the logits removes the terms from the denominator as well.

What it buys

Every row of the masked attention matrix is a valid prediction context. A sequence of length nn therefore yields nn training examples from one forward pass, at the cost of one — this is the reason decoder-only pre-training is as sample-efficient as it is, and it is a property of the mask, not of the architecture.

L=1nt=1nlogpθ(xtx<t)L = -\frac{1}{n} \sum_{t=1}^{n} \log p_\theta(x_t \mid x_{<t})
eq. 2 — the loss every position contributes to

Variants worth knowing

The strictly triangular mask is the common case, not the only one. Document packing wants a block-diagonal mask so that concatenated documents cannot attend across the join. Prefix-LM wants a rectangular bidirectional region. Sliding-window attention is a band mask. All of them are the same mechanism with a different boolean array, which is why a kernel that takes an arbitrary mask is more useful than one with is_causal hard-coded — though the hard-coded one is faster, because it can skip whole tiles.

The off-by-one that will bite you

The mask allows jij \le i, not j<ij < i: a position attends to itself. Getting this wrong shifts the model by one token and produces a loss curve that looks plausible, converges, and generates nonsense. Check it by feeding the model a sequence and confirming that the logits at position tt are unchanged when position t+1t+1 is altered.

Implementation

python · torch ≥ 2.1
import torch
from torch import Tensor


def causal_mask(n: int, device: torch.device) -> Tensor:
    """True where attention is forbidden. Note `diagonal=1`: self is allowed."""
    return torch.ones(n, n, dtype=torch.bool, device=device).triu(diagonal=1)


def document_mask(doc_ids: Tensor) -> Tensor:
    """Causal *and* within-document, for packed training batches. [B, N] -> [B, 1, N, N]."""
    n = doc_ids.size(-1)
    causal = torch.ones(n, n, dtype=torch.bool, device=doc_ids.device).triu(1)
    cross = doc_ids[:, :, None] != doc_ids[:, None, :]
    return (causal | cross)[:, None]


def apply(logits: Tensor, mask: Tensor) -> Tensor:
    # A finite floor, so a fully masked row degrades to uniform rather than NaN.
    floor = torch.finfo(logits.dtype).min / 2
    return logits.masked_fill(mask, floor)

Build the mask once and cache it by length; regenerating a 4096×40964096 \times 4096 boolean tensor per step is a measurable fraction of a small model’s step time. Better still, pass is_causal=True to a fused kernel and never materialise it — the tiles above the diagonal are then skipped rather than computed and discarded, which is most of the point.

Related

References

[1]Vaswani et al. — Attention Is All You Need (2017)arXiv:1706.03762
[2]Radford et al. — Improving Language Understanding by Generative Pre-Training (2018)openai-gpt
[3]Raffel et al. — Exploring the Limits of Transfer Learning with T5 (2019)arXiv:1910.10683