for d in (4, 64, 512):
_, jac_unscaled = attention_stats(d, scaled=False)
_, jac_scaled = attention_stats(d, scaled=True)
print(f'd = {d:3d}: Jacobian norm {jac_unscaled:.3f} (unscaled), '
f'{jac_scaled:.3f} (scaled)')Dive into Deep Learning · §10.2
Attention scoring and masking
dot-product scores · the 1/√d factor, measured · masked softmax · batched attention · from alignment to attention
Expand the Gaussian kernel’s exponent:
-\tfrac{1}{2}\|\mathbf{q} - \mathbf{k}_i\|^2 = \mathbf{q}^\top \mathbf{k}_i - \tfrac{1}{2}\|\mathbf{k}_i\|^2 - \tfrac{1}{2}\|\mathbf{q}\|^2.
The query term cancels in the softmax; the key-norm term cancels too when all key norms are equal. Keeping only the dot product is a modeling choice: a compatibility score that learned representations can shape freely.
For unit-variance entries, \operatorname{Var}(\mathbf{q}^\top\mathbf{k}) = d — scores grow with vector length for no semantic reason. Hence the Transformer’s scoring function:
a(\mathbf{q}, \mathbf{k}_i) = \mathbf{q}^\top \mathbf{k}_i / \sqrt{d}.
Random queries and keys, attention over 64 candidates, entropy of the weight distribution as d grows:
The softmax Jacobian \partial \boldsymbol{\alpha} / \partial \mathbf{a} = \mathrm{diag}(\boldsymbol{\alpha}) - \boldsymbol{\alpha}\boldsymbol{\alpha}^\top tends to zero as \boldsymbol{\alpha} approaches one-hot:
d = 4: Jacobian norm 0.258 (unscaled), 0.178 (scaled)
d = 64: Jacobian norm 0.218 (unscaled), 0.184 (scaled)
d = 512: Jacobian norm 0.103 (unscaled), 0.186 (scaled)
Padding — variable-length sequences share a minibatch:
Dive into Deep Learning
Learn to code <blank>
Hello world <blank> <blank>
Causality — a language model computes all positions in parallel, but query t must not see keys beyond t.
Same fix for both: overwrite invalid scores before the softmax.
Not with -10^6 (overflows float16), not with -\infty (a fully masked query → NaN). Use the dtype’s most negative finite value.
def masked_softmax(X, valid_lens):
"""Perform softmax operation by masking elements on the last axis."""
# X: 3D tensor, valid_lens: 1D or 2D tensor
if valid_lens is None:
return jax.nn.softmax(X, axis=-1)
shape = X.shape
if valid_lens.ndim == 1:
valid_lens = jnp.repeat(valid_lens, shape[1])
else:
valid_lens = valid_lens.reshape(-1)
mask = jnp.arange(shape[-1])[None, :] < valid_lens[:, None]
# Most negative finite score: exactly zero weight after the softmax,
# at any precision, without the NaN risk of literal -inf
X = jnp.where(mask, X.reshape(-1, shape[-1]), jnp.finfo(X.dtype).min)
weights = jax.nn.softmax(X, axis=-1)
# A fully masked query (no valid key) would be a uniform average over
# invalid values; zero those rows so no padded position leaks through
weights = jnp.where(mask.any(-1, keepdims=True), weights, 0.0)
return weights.reshape(shape)Valid lengths 2 and 3 — weights beyond the prefix are exactly zero, rows still sum to one:
Array([[[0.4922724 , 0.5077276 , 0. , 0. ],
[0.5997409 , 0.40025908, 0. , 0. ]],
[[0.3531402 , 0.19856434, 0.4482954 , 0. ],
[0.31042197, 0.2873157 , 0.40226236, 0. ]]], dtype=float32)
Per-query valid lengths work too:
Array([[[1. , 0. , 0. , 0. ],
[0.35672572, 0.2543091 , 0.38896522, 0. ]],
[[0.43679553, 0.5632045 , 0. , 0. ],
[0.2580636 , 0.2509437 , 0.258296 , 0.23269679]]], dtype=float32)
Query t sees keys 1, \ldots, t, so the valid lengths are (1, 2, \ldots, n). The pattern is lower triangular:
The general interface is a boolean tensor — entry (i, j): may query i see key j? Requirements compose by AND, broadcasting does the bookkeeping: padding (\textrm{batch}, 1, \textrm{keys}) ∧ causal (1, \textrm{queries}, \textrm{keys}).
The composite must still leave every query at least one valid key — masks that are harmless alone can intersect to an empty row.
Attention over a minibatch is two batched matrix multiplications and one softmax:
\mathrm{softmax}\left(\frac{\mathbf{Q}\mathbf{K}^\top}{\sqrt{d}}\right)\mathbf{V} \in \mathbb{R}^{n \times v}.
The implementation combines scoring, masking, dropout on the weights, and value pooling:
class DotProductAttention(nnx.Module):
"""Scaled dot product attention."""
def __init__(self, dropout, rngs=None):
rngs = nnx.Rngs(dropout=0) if rngs is None else rngs
self.dropout = nnx.Dropout(dropout, rngs=rngs)
# Shape of queries: (batch_size, no. of queries, d)
# Shape of keys: (batch_size, no. of key-value pairs, d)
# Shape of values: (batch_size, no. of key-value pairs, value dimension)
# Shape of valid_lens: (batch_size,) or (batch_size, no. of queries)
def __call__(self, queries, keys, values, valid_lens=None):
d = queries.shape[-1]
# Swap the last two dimensions of keys with keys.swapaxes(1, 2)
scores = queries@(keys.swapaxes(1, 2)) / math.sqrt(d)
attention_weights = masked_softmax(scores, valid_lens)
# NNX idiom: return (output, weights); PyTorch stores weights on self
return self.dropout(attention_weights) @ values, attention_weightsValid lengths (2, 6): the stored weights vanish beyond the second and sixth key.
Machine translation
In 2014 neural machine translation, one fixed vector connected the encoder and decoder RNNs, restricting the representation of long sentences. Bahdanau, Cho & Bengio: let the decoder state query all encoder states, one fresh summary per output token — “jointly learning to align and translate”.
Learned weights behave like soft alignments — monotone, except where the languages reorder words.
Decoder state and encoder states had different sizes, so the original score was a tiny MLP:
a(\mathbf{q}, \mathbf{k}) = \mathbf{w}_v^\top \tanh(\mathbf{W}_q \mathbf{q} + \mathbf{W}_k \mathbf{k}).
key_q, key_k, k1, k2, k3 = jax.random.split(jax.random.key(0), 5)
queries = jax.random.normal(key_q, (3, 20))
keys = jax.random.normal(key_k, (6, 2))
num_hiddens = 8
W_q = jax.random.normal(k1, (num_hiddens, 20)) / math.sqrt(20)
W_k = jax.random.normal(k2, (num_hiddens, 2)) / math.sqrt(2)
w_v = jax.random.normal(k3, (num_hiddens,)) / math.sqrt(num_hiddens)
features = jnp.tanh((queries @ W_q.T)[:, None, :]
+ (keys @ W_k.T)[None, :, :])
scores = features @ w_v
d2l.check_shape(scores, (3, 6))
jax.nn.softmax(scores, axis=-1)Array([[0.13390937, 0.3772766 , 0.17127123, 0.1027277 , 0.06276076,
0.15205443],
[0.19218282, 0.31524566, 0.14511336, 0.08953383, 0.07662669,
0.18129766],
[0.15669951, 0.3378386 , 0.15281527, 0.11713327, 0.09428051,
0.1412329 ]], dtype=float32)
DotProductAttention uses BMM, masked softmax, dropout, and a second BMM.