Chapter 4 · Section 4.4 · 50 min

Masks, causal and padding

Here is a language model that reaches perfect accuracy on its training data in a single step, and is completely useless. Its one attention head makes every position tt look at position t+1t+1 and copy the token it finds there. Asked to predict the next token, it reads it. Training loss goes to zero. Then you try to generate text, position t+1t+1 does not exist yet, and the model has nothing.

Nobody builds that head on purpose, but gradient descent will find it in an afternoon if you let it. A language model is trained on whole sequences in parallel, with every position predicting its successor at once (§3.1), and attention as we have defined it lets every position read every other. The fix is to forbid some connections. A mask says, for each pair (query ii, key jj), whether ii may attend to jj. You have used one already. In Exercise 3.6a a decoder query gave padded source positions a score of −∞-\infty, and Chapter 3 ended by noting that a decoder must also be kept from reading words it has not written yet. This section turns those two remarks into the masks every practitioner needs (causal, padding, and their relatives), shows how to combine them, what to do when a row is allowed to see nothing at all, and the conventions that trip people up when moving between libraries.

A model that reads the future learns nothing

Let's build the cheating head and watch it work. Tokens are one-hot ids concatenated with one-hot positions, as in Exercise 4.2b, but now the query of position tt matches the key of position t+1t+1, a next-token head.

Python · cell 1

Now score it as a next-token predictor, with and without a mask that hides the future.

Python · cell 2

Without a mask the head is a perfect predictor, 100% accuracy, because it copies the answer. With the causal mask it falls to 10%, chance level for 10 tokens, because the answer is out of reach. That 10% is the honest starting point, and everything a real model learns above it is genuine prediction.

The causal mask

The causal mask allows query ii to attend to key jj exactly when j≤ij \le i. As a boolean matrix it is lower triangular, with True on and below the diagonal. As an additive mask M∈RT×T\mathbf{M} \in \R^{T\times T} it is

Mij={0j≤i−∞j>i,A=softmax⁡ ⁣(QK⊤dk+M).M_{ij} = \begin{cases} 0 & j \le i \\ -\infty & j > i \end{cases}, \qquad \mathbf{A} = \operatorname{softmax}\!\left(\frac{\mathbf{Q}\mathbf{K}^\top}{\sqrt{d_k}} + \mathbf{M}\right).

In plain words, before the softmax, every score that would let a position see its future is replaced by −∞-\infty, and since e−∞=0e^{-\infty} = 0, those weights are exactly zero.

Two details matter. The diagonal is allowed, since a position may always look at itself (the token at position tt is part of the input when predicting position t+1t+1). And the mask goes on the scores, before the softmax, so the remaining weights renormalize among the allowed keys. Zeroing weights after the softmax without renormalizing leaves rows that sum to less than 1. (Zeroing and then renormalizing gives the same result as the −∞-\infty mask, but costs an extra pass.)

Why is this enough to stop all cheating, through every layer of a deep model? By induction. Suppose that after layer ℓ\ell, the vector at position ii depends only on input tokens 1,…,i1, \dots, i. In layer ℓ+1\ell+1, attention at position ii mixes the values at positions j≤ij \le i, each of which depends only on tokens up to j≤ij \le i. The MLP, the normalization, and the residual connection act on each position separately (§5.1). So after layer ℓ+1\ell+1, position ii still depends only on tokens up to ii. The input embeddings satisfy the claim, so every layer does. Let's verify the claim numerically, by changing a future token and checking that no past output moves.

Python · cell 3

The causal mask is also what makes training efficient. One forward pass over a sequence of TT tokens produces TT predictions, each using exactly the context it would have at generation time, so every sequence yields TT training examples at once. This is teacher forcing (§3.5), now done in parallel instead of one step at a time. And because the mask matches what a position can see during generation, the keys and values computed for earlier positions never change as new tokens arrive, which is what makes the KV cache possible (§6.5).

Core

Prefix-LM and sliding-window masks

Two more rules from the callout above, as boolean (T, T) masks with True meaning "may attend". prefix_lm_mask(T, P) lets the first P positions (the prompt) attend to each other in both directions, while every later position stays causal and sees the whole prompt. sliding_window_mask(T, w) is causal and lets query ii see only the last w positions including itself, keys jj with i−w<j≤ii - w < j \le i. Mistral 7B uses this rule with w=4,096w = 4{,}096 (Jiang et al. 2023).

Hidden tests, hints, and AI help, from $1.99 a month.

Padding masks

To run sequences of different lengths as one batch, we pad them to a common length TT with a PAD token (§2.5). This is the mask of Exercise 3.6a, now for every query of a batch at once. Padding is filler, and no real token should attend to it, or the output would depend on how much padding happened to be added. So we mask every key position that holds padding. For a batch with valid lengths ℓ1,…,ℓB\ell_1, \dots, \ell_B, the key-padding mask allows key jj of sequence bb exactly when j<ℓbj < \ell_b. It has shape (B,T)(B, T) and is broadcast to (B,Tq,T)(B, T_q, T), or (B,h,Tq,T)(B, h, T_q, T) with heads, by inserting axes.

What about padded queries, the rows belonging to pad positions? They compute some output, and we ignore it. Their predictions are excluded from the loss with a loss mask (for example ignore_index in PyTorch's cross-entropy). This is a separate mask with a separate job. The attention mask decides who sees whom. The loss mask decides which positions are graded. Mixing them up is a common bug. Masking prompt tokens out of the loss during fine-tuning, for instance, is a loss mask, and the prompt must stay visible to attention (§14.3).

Masks combine by logical AND, since a connection is allowed only if every rule allows it. With additive masks, the same thing is a sum, because 0+0=00 + 0 = 0 and anything plus −∞-\infty is −∞-\infty.

Python · cell 4

The last rows of sequence 2 are padded queries. They attend to the two real keys, produce an output nobody will use, and cause no harm. With right padding, as here, every query has at least one allowed key, because key 0 is always real and always in the past. That is not true of every layout, and the exceptions are where NaNs come from.

Warm-up

Causal and padding masks for a batch

Implement make_mask(T, lengths=None, causal=True). With lengths=None, return a (T, T) boolean mask (all True if causal is false). With lengths, a list of BB valid lengths for right-padded sequences, return a (B, T, T) mask in which query ii of sequence bb may attend to key jj only if jj is a real token (j<j < lengths[b]) and, if causal, j≤ij \le i. True means "may attend".

Hidden tests, hints, and AI help, from $1.99 a month.

Attention to nothing

Some rows of a mask can end up entirely False. Here are three ways it happens in practice.

  • Left padding. Batched generation usually pads on the left, so that all sequences end at the same position and new tokens can be appended in a column. With a causal mask, a pad position at the very start may attend only to keys at or before it, and those are all padding. Its row is empty.
  • Masks built from several rules, such as padding inside packed sequences or a sliding window that has slid past every real token.
  • Cross-attention to an empty source, for example an image-conditioned model run on a batch where one example has no image.

With −∞-\infty in every entry of a row, the softmax computes e−∞−(−∞)e^{-\infty - (-\infty)}, which is eNaNe^{\text{NaN}}, and the row becomes NaN. One NaN row is enough. It contaminates every later layer through the values, and the loss and the gradients become NaN everywhere. Implementations guard against this in one of four ways.

choiceweights in an empty rowother rowspitfalls
−∞-\infty, plain softmaxNaNexactpoisons the whole batch
large negative, such as −109-10^9uniform over the masked keys (if filled in, see below)exact, since e−109e^{-10^9} underflows to 0−109-10^9 overflows to −∞-\infty in float16, bringing the NaN back (bfloat16 has float32's range, so it is safe there)
the dtype's most negative value, finfo(dtype).minuniform over the masked keysexactadding it to a negative score can overflow to −∞-\infty (in float16, any score below about −16-16 does), so use it with where (masked fill), not addition
"safe" softmax, rows with no allowed key return all zeroszeros, so the output is zeroexactneeds an explicit check, but gives the cleanest semantics

The large-negative options never produce NaN, but they quietly make an empty row attend to the very positions it was told to ignore. With a masked fill every blocked score becomes the same number, so the row is exactly uniform. With an additive mask the row becomes softmax⁡(s−109)\operatorname{softmax}(\mathbf{s} - 10^9), which in float64 is almost exactly the softmax of the original scores, while in float32 the spacing between representable numbers near 10910^9 is 64, so the scores all round to the same value and the row is again uniform. Either way the row ignores its mask. That is harmless when the row's output is discarded (a padded query) and wrong when it is not. The safe softmax is the most honest. An empty row means "there is nothing to look at", and a zero output says exactly that. Recent versions of PyTorch return zeros for fully masked rows in scaled_dot_product_attention (we checked the math and CPU kernels of version 2.14), while a bare torch.softmax over a row of −∞-\infty still returns NaN. This behavior has changed across versions and can differ between kernels, so test the one you deploy. Exercise 4.4b asks you to write the safe version.

Python · cell 5
Interview

A numerically safe masked softmax

Implement masked_softmax(S, mask) for scores S of shape (..., n) and a boolean mask broadcastable to it (True means "may attend"), normalizing over the last axis. Requirements, all tested. Masked entries are exactly 0. Rows with at least one allowed entry sum to 1 and match the ordinary softmax over the allowed entries. Rows with no allowed entry are all zeros, not NaN and not uniform. It is stable for scores in the thousands. Do not let NumPy compute -inf - (-inf) anywhere.

Hidden tests, hints, and AI help, from $1.99 a month.

When a head wants to attend to nothing

An empty row is a mask's way of saying "nothing to see". But even with every key allowed, a head often has nothing it wants. Think of a head that tracks the subject of a verb, sitting at a punctuation mark. Softmax gives it no way to abstain, because the weights must sum to 1. Trained models find a workaround. They learn to dump the unwanted weight on a token that carries little information, most often the first token of the sequence. Xiao et al. (2023) named these attention sinks, and showed that the first few tokens receive a large share of attention in many layers of models such as Llama 2, regardless of what those tokens are. Removing the sink tokens from the cache during streaming generation makes quality collapse, which is how they discovered how much the model relies on them.

If abstaining is useful, why not make it explicit? Add a constant to the softmax denominator,

aij=esijeb+∑lesil,a_{ij} = \frac{e^{s_{ij}}}{e^{b} + \sum_{l} e^{s_{il}}},

so the weights sum to less than 1 and the head can return (nearly) zero by making all its scores small compared with bb. With b=0b = 0 this is the "softmax plus one" proposed by Miller (2023). OpenAI's gpt-oss models (2025) learn a separate bb for every head, which their model card describes as letting a head pay no attention to any token. Equivalently, a sink is an extra key whose score is bb for every query and whose value is the zero vector. The weight it absorbs, eb/(eb+∑lesil)e^b/(e^b + \sum_l e^{s_{il}}), is weight that no real token receives.

Interview

Attention with a learned sink

Implement sink_attention(Q, K, V, sink, mask=None) for one layer of heads, with Q of shape (h, T_q, d_k), K of shape (h, T_k, d_k), V of shape (h, T_k, d_v), one sink logit per head in sink of shape (h,), and an optional boolean mask broadcastable to (h, T_q, T_k) (True means "may attend"). The weights are aij=esij/(eb+∑lesil)a_{ij} = e^{s_{ij}}/(e^{b} + \sum_l e^{s_{il}}) with sij=qi⊤kj/dks_{ij} = \mathbf{q}_i^\top\mathbf{k}_j/\sqrt{d_k} and bb the head's sink logit, so each row sums to less than 1. Return (O, A). It must be stable for scores and sink logits in the thousands, give masked keys exactly zero weight, and return a zero output, not NaN, for a row whose keys are all masked.

Hidden tests, hints, and AI help, from $1.99 a month.

Packed sequences, a preview

Padding wastes compute, and real documents vary so much in length that padding a batch to its longest member can waste most of every matrix multiply. Krell et al. (2021) found that padding can make up as much as half of all tokens in common NLP datasets. The standard fix is packing, concatenating several documents into one row of length TT, separated by end-of-text tokens, so that every row is full. §6.1 builds that pipeline and measures the waste. Here we need only the mask it calls for.

Without one, the first token of document 2 can attend to all of document 1, which it has nothing to do with. Many pretraining runs accept that cross-contamination (GPT-3 relied on the end-of-text token alone, Brown et al. 2020). The exact fix is a block-diagonal mask. Give token ii a document id gig_i, and allow query ii to attend to key jj only when

gi=gjandj≤i.g_i = g_j \quad\text{and}\quad j \le i .

In plain words, every document gets its own causal triangle, and the triangles never touch.

With this mask each document's outputs are identical to what you would get by running it alone, as the cell checks. (With absolute position embeddings the position ids must also restart at each document, a detail §6.1 handles.) The Llama 3 report describes training with exactly this mask (Llama Team 2024), and production kernels never build the T×TT\times T matrix for it. They take the document boundaries and skip the off-diagonal blocks, FlashAttention's variable-length interface of §12.4.

Python · cell 6
Which keys may each query see, and what happens to a row that may see none?
Rows are queries and columns are keys. Combine causal, padding, document, and window rules. Click a token to start a new document there. Shading shows the attention weights for random scores.
Loading interactive figure…

Causal masks when queries and keys have different lengths

So far the score matrix was square. During generation with a KV cache (§6.5), the queries are the last TqT_q positions of a sequence whose keys span all TkT_k positions so far, with Tq≤TkT_q \le T_k. Query ii (counting from 0 within the chunk) sits at absolute position Tk−Tq+iT_k - T_q + i, so the causal rule becomes

query i may attend to key j  ⟺  j≤i+(Tk−Tq).\text{query } i \text{ may attend to key } j \iff j \le i + (T_k - T_q).

In plain words, the triangle is anchored at the bottom right of the Tq×TkT_q\times T_k score matrix. For a single new token (Tq=1T_q = 1) the row is all True, which is why plain decoding with a cache needs no mask at all. Here is a trap worth knowing now. torch.nn.functional.scaled_dot_product_attention with is_causal=True anchors the triangle at the top left when Tq≠TkT_q \ne T_k, so query 0 sees only key 0. (PyTorch provides torch.nn.attention.bias.causal_lower_right for the other alignment, and FlashAttention uses bottom-right alignment.) §6.5 builds the cache and processes prompts in chunks with this mask.

In PyTorch

The same masks in PyTorch. Note how three popular APIs disagree about what True means.

🔥 In PyTorchfor a machine with PyTorch
APImask argumentTrue means
F.scaled_dot_product_attentionattn_mask (bool)may attend
nn.MultiheadAttentionattn_mask, key_padding_mask (bool)may not attend
Hugging Face modelsattention_mask (0 or 1, per token)1 is a real token, 0 is padding
FlexAttentionmask_mod(b, h, q_idx, kv_idx)returns True where attention is allowed

For document masks and other structured patterns, PyTorch's FlexAttention lets you describe the mask as a function and compiles a kernel that skips fully masked blocks. A block-diagonal causal mask looks like this. (It is shown, not run, because it needs torch.compile to be fast.)

Read more

Watch next

🎬 Let's build GPT: from scratch, in code, spelled out. · Andrej Karpathy · starts at 42:13The lower-triangular trick, building the causal mask from a running average of the past, one version at a time.
🎬 Attention in transformers, step-by-step | Deep Learning Chapter 6 · 3Blue1Brown · starts at 11:08The masking segment, which shows why later tokens must not influence earlier ones during training.