Causal Masked Attention
GPT-2
Medium
Attention with causal mask for autoregressive decoding
Independent study note. Written from the public paper and official code. This is not TensorTonic Plus and does not reproduce their exercises, starter code, or tests. For the official version, subscribe on TensorTonic.
Overview
GPT-2 is a left-to-right language model. Equation (1) in the paper factorizes a sequence as (\prod_i p(s_i\mid s_1,\ldots,s_{i-1})). Self-attention, left unmodified, lets position (i) read keys and values from (j>i), which would leak the token being predicted and make the factorization invalid. A causal (autoregressive) mask forbids that read.
The 2019 paper never writes the mask tensor. It inherits decoder-only attention from GPT / Vaswani: each position may attend to itself and the past, never the future. The official graph is the precise source. attention_mask builds a 0/1 band; mask_attn_weights applies it to already-scaled scores with a large negative offset rather than a hard zero, so softmax underflow sends blocked mass to ~0.
The same mask is written so it still works when the key/value sequence is longer than the query sequence. That is the KV-cache case: one new query attends over cached past keys plus the new key.
How it works
After the scaled product (W = QK^\top / \sqrt{d_k}), (W) has shape [batch, heads, nd, ns] — destination length by source length. Information is defined to flow from source to destination (mask_attn_weights comment in src/model.py).
attention_mask(nd, ns) builds a boolean matrix (M\in{0,1}^{n_d\times n_s}) with ones in the lower triangle, “counting from the lower right corner”:
M_{i,j}=\mathbf{1}\!\left[i \ge j - n_s + n_d\right],\qquad i=0\ldots n_d-1,\; j=0\ldots n_s-1.
When (n_d=n_s=n) (full forward, no cache) this is the familiar (i\ge j) lower-triangular mask, including the diagonal so a token may see itself. When keys have been concatenated with a past of length (n_s-n_d), the extra columns on the left are the cached past; the inequality still allows every query to see all cached keys and the current-step prefix, and still blocks the future relative to that query.
Scores are then
W \leftarrow W\odot M - 10^{10}\,(1-M)
in the mask’s dtype (tf.cast(1e10, w.dtype) in the official code). Softmax is over the source axis. Finite but huge negatives are used instead of (-\infty) so TPU/GPU softmax stays defined.
During incremental decoding, attn concatenates past keys/values along the sequence axis before multihead_attn. Queries have length 1 (the new token); keys/values have length (\mathrm{past}+1). The mask formula with (n_d=1), (n_s=\mathrm{past}+1) yields a single row of ones: the new token may see the entire cache and itself. No extra “generation mask” is required.
The paper’s contribution here is only the modeling statement (autoregressive (p(s_i\mid s_{<i})), context 1024). The index arithmetic, the (10^{10}) offset, and the past-concat contract are official-code details.
Official code
src/model.py: attention_mask, the nested mask_attn_weights, and the past concat in attn. attention_mask’s docstring notes it matches tf.matrix_band_part(tf.ones([nd, ns]), -1, ns-nd) but avoids a TPU-unfriendly path. sample.py drives generation using the present cache the attention block returns; it does not reimplement the mask.
Watch-outs
- Mask logits, not weights after softmax. Zeroing a softmax output does not renormalize the allowed positions and leaves residual leaked mass.
- Inclusive diagonal: (i=j) must stay visible. A strict (i>j) mask blinds every token to its own embedding.
- A naive
trilof size (n_d\times n_d) is wrong once (n_s\neq n_d). Cached decoding will either index-error or drop the past. - (-10^{10}) is large in fp16/fp32 but not (-\infty). Adding it twice, or using a small offset like (-1), either saturates or fails to suppress the tail.
Sources
- Paper: Language Models are Unsupervised Multitask Learners (Radford et al., 2019), §2 Eq. (1), §2.3
- Decoder mask: Vaswani et al., “Attention Is All You Need,” 2017
- Code: openai/gpt-2
src/model.py(attention_mask,mask_attn_weights)