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 look at position 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 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 , key ), whether may attend to . You have used one already. In Exercise 3.6a a decoder query gave padded source positions a score of , 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 matches the key of position , a next-token head.
Now score it as a next-token predictor, with and without a mask that hides the future.
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 to attend to key exactly when . As a boolean matrix it is lower triangular, with True on and below the diagonal. As an additive mask it is
In plain words, before the softmax, every score that would let a position see its future is replaced by , and since , those weights are exactly zero.
Two details matter. The diagonal is allowed, since a position may always look at itself (the token at position is part of the input when predicting position ). 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 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 , the vector at position depends only on input tokens . In layer , attention at position mixes the values at positions , each of which depends only on tokens up to . The MLP, the normalization, and the residual connection act on each position separately (§5.1). So after layer , position still depends only on tokens up to . 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.
The causal mask is also what makes training efficient. One forward pass over a sequence of tokens produces predictions, each using exactly the context it would have at generation time, so every sequence yields 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).
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 see only the last w positions including itself, keys with . Mistral 7B uses this rule with (Jiang et al. 2023).
Padding masks
To run sequences of different lengths as one batch, we pad them to a common length 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 , the key-padding mask allows key of sequence exactly when . It has shape and is broadcast to , or 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 and anything plus is .
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.
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 valid lengths for right-padded sequences, return a (B, T, T) mask in which query of sequence may attend to key only if is a real token ( lengths[b]) and, if causal, . True means "may attend".
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 in every entry of a row, the softmax computes , which is , 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.
| choice | weights in an empty row | other rows | pitfalls |
|---|---|---|---|
| , plain softmax | NaN | exact | poisons the whole batch |
| large negative, such as | uniform over the masked keys (if filled in, see below) | exact, since underflows to 0 | overflows to in float16, bringing the NaN back (bfloat16 has float32's range, so it is safe there) |
the dtype's most negative value, finfo(dtype).min | uniform over the masked keys | exact | adding it to a negative score can overflow to (in float16, any score below about does), so use it with where (masked fill), not addition |
| "safe" softmax, rows with no allowed key return all zeros | zeros, so the output is zero | exact | needs 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 , which in float64 is almost exactly the softmax of the original scores, while in float32 the spacing between representable numbers near 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 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.
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.
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,
so the weights sum to less than 1 and the head can return (nearly) zero by making all its scores small compared with . With this is the "softmax plus one" proposed by Miller (2023). OpenAI's gpt-oss models (2025) learn a separate 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 for every query and whose value is the zero vector. The weight it absorbs, , is weight that no real token receives.
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 with and 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.
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 , 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 a document id , and allow query to attend to key only when
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 matrix for it. They take the document boundaries and skip the off-diagonal blocks, FlashAttention's variable-length interface of §12.4.
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 positions of a sequence whose keys span all positions so far, with . Query (counting from 0 within the chunk) sits at absolute position , so the causal rule becomes
In plain words, the triangle is anchored at the bottom right of the score matrix. For a single new token () 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 , 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.
| API | mask argument | True means |
|---|---|---|
F.scaled_dot_product_attention | attn_mask (bool) | may attend |
nn.MultiheadAttention | attn_mask, key_padding_mask (bool) | may not attend |
| Hugging Face models | attention_mask (0 or 1, per token) | 1 is a real token, 0 is padding |
| FlexAttention | mask_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
- Vaswani et al. (2017). Attention Is All You Need. Section 3.2.3 introduces masking out illegal connections in the decoder's self-attention.
- Phuong & Hutter (2022). Formal Algorithms for Transformers. Masks written as explicit matrices for encoder, decoder, and encoder-decoder models.
- Krell et al. (2021). Efficient Sequence Packing without Cross-contamination. How much padding costs, packing algorithms, and the masks and position ids that make packing exact.
- Jiang et al. (2023). Mistral 7B. A widely used model with sliding-window attention, and how stacked windows reach far back.
- Llama Team (2024). The Llama 3 Herd of Models. Section 3.2 describes the document mask used in pretraining.
- Xiao et al. (2023). Efficient Streaming Language Models with Attention Sinks. The discovery of attention sinks and why the first tokens matter.
- Miller (2023). Attention Is Off By One and OpenAI (2025), gpt-oss-120b and gpt-oss-20b Model Card. Letting heads attend to nothing, by a constant and by a learned sink.
- PyTorch documentation. scaled_dot_product_attention. The boolean mask convention and the alignment of
is_causal.