The Math Behind LLMs · Part 11 of 16

The Math of Attention: Scaling, Masks, Jacobians and Low Rank

Attention derived by hand: why divide by √d, the mask as minus infinity, the softmax Jacobian, n² cost, GQA and MLA as factorizations, linear attention.

On this page

TL;DR

Attention is a soft dictionary lookup: every key matches a query a little, softmax turns the matches into weights, and the answer is a weighted average of the values. This part derives it with three tokens of four numbers each. The division by d\sqrt{d} falls out of Part 10’s variance rules, and the causal mask adds minus infinity because e−∞=0e^{-\infty} = 0. The softmax’s Jacobian is diag(p)−pp⊤\text{diag}(\mathbf{p}) - \mathbf{p}\mathbf{p}^{\top}, which FlashAttention’s backward pass uses without building it. The cost grows like n2dn^2 d; multi-head, grouped-query and latent attention are low-rank factorizations; and linear attention and Mamba avoid the n2n^2 by reordering a matrix product, at the price of a fixed-size memory.

Why it matters

Change the order of two matrix multiplications, and one attention head reading 128K tokens does 1,024 times less arithmetic: 8.6 billion operations instead of 8.8 trillion. Matrix multiplication is associative, so (AB)C=A(BC)(AB)C = A(BC), and Part 3 showed that the order you pick can change the cost enormously.

Standard attention can’t take that shortcut, because one function sits between the two products: softmax. It’s also why scores are divided by d\sqrt{d}, why the mask uses minus infinity, and why gradients can stall. Linear attention and state-space models like Mamba drop it to escape the cost, and lose something in the trade.

Fundamentals Part 5 explained attention in words and worked it on a four-token sentence. This part derives each piece, using variance from Part 10, softmax from Part 5, Jacobians from Part 7 and rank from Part 3.

The intuition

A Python dictionary is a hard lookup: hand it a key, it finds the one stored key that matches exactly, and returns that key’s value.

Attention makes the lookup soft. The query is scored against every stored key, softmax turns the scores into weights that add up to 1, and the answer is a blend of all the values, weighted by how well each key matched. When one key matches far better than the rest, it takes almost all the weight and the soft lookup becomes a hard one.

The analogy breaks in two places. A dictionary lookup fails when no key matches, while attention must hand out a full unit of weight even when no key fits, which is where the “attention sinks” in Fundamentals Part 5 come from. And every token is a query and a dictionary entry at the same time.

Step 1: soft lookup, by hand

From a hard lookup to weights

Write a hard lookup as a sum: the output is ∑jwjvj\sum_j w_j \mathbf{v}_j, where vj\mathbf{v}_j is the value stored at position jj and wjw_j is 1 for the matching key and 0 for the rest. To make it soft, let the weights grow with how well key kj\mathbf{k}_j matches query q\mathbf{q}. The dot product measures that match (Part 2), and softmax turns scores into positive weights that add up to 1 (Part 5). With a scale factor that Step 2 explains:

wj=softmaxj ⁣(q⋅kjd)w_j = \text{softmax}_j\!\left(\frac{\mathbf{q} \cdot \mathbf{k}_j}{\sqrt{d}}\right)

Here dd is the number of entries in each query and key.

Three tokens, four numbers each

Take three tokens with these queries, keys and values. A real model makes them by multiplying each token’s vector by three learned matrices (Fundamentals Part 5); these are picked for easy arithmetic.

TokenQueryKeyValue
1[1, 1, 0, 0][1, 0, 1, 0][1, 0, 1, 0]
2[0, 0, 0, 2][0, 1, 0, 1][0, 2, 0, 0]
3[2, 0, 2, 0][1, 1, 0, 0][0, 0, 0, 3]

Token 3’s query is [2, 0, 2, 0]. Its dot products with the three keys are 2+2=42 + 2 = 4, 00 and 22. With d=4d = 4, dividing by 4=2\sqrt{4} = 2 gives scores [2, 0, 1]. Their exponentials are 7.389, 1 and 2.718, which total 11.107, so the weights are 0.665, 0.090 and 0.245: the scores [2, 1, 0] from Part 5, in a different order.

The output is the weighted sum of the values:

0.665 × [1, 0, 1, 0] + 0.090 × [0, 2, 0, 0] + 0.245 × [0, 0, 0, 3]
= [0.665, 0.180, 0.665, 0.734]

Token 3 mostly gets token 1’s value, whose key lines up best with its query.

The matrix form

Stack the queries as the rows of a matrix QQ, and the keys and values as rows of KK and VV, each n×dn \times d for nn tokens. Then QK⊤QK^{\top} is the n×nn \times n grid of every query dotted with every key, and the whole calculation is equation (1) of Attention Is All You Need:

Attention(Q,K,V)=softmax ⁣(QK⊤d)V\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{QK^{\top}}{\sqrt{d}}\right) V

The softmax runs along each row. Here QK⊤QK^{\top} has rows [1, 1, 2], [0, 2, 0] and [4, 0, 2].

The causal mask as minus infinity

A model that writes text mustn’t let token 1 read tokens 2 and 3, which come after it. So it adds a mask matrix MM to the scores before softmax, with 0 where looking is allowed and −∞-\infty where it isn’t:

M=[0−∞−∞00−∞000]M = \begin{bmatrix} 0 & -\infty & -\infty \\ 0 & 0 & -\infty \\ 0 & 0 & 0 \end{bmatrix}

Since e−∞=0e^{-\infty} = 0, a masked score adds nothing to the top or the bottom of the softmax fraction. The whole example in NumPy:

import numpy as np
np.set_printoptions(precision=3, suppress=True)

Q = np.array([[1., 1, 0, 0], [0, 0, 0, 2], [2, 0, 2, 0]])
K = np.array([[1., 0, 1, 0], [0, 1, 0, 1], [1, 1, 0, 0]])
V = np.array([[1., 0, 1, 0], [0, 2, 0, 0], [0, 0, 0, 3]])
d = Q.shape[1]

S = Q @ K.T / np.sqrt(d)                       # scaled scores, 3 x 3
M = np.triu(np.full((3, 3), -np.inf), k=1)     # -inf above the diagonal

def softmax_rows(X):
    E = np.exp(X - X.max(axis=1, keepdims=True))
    return E / E.sum(axis=1, keepdims=True)

P = softmax_rows(S + M)
print(P)
print(P @ V)
[[1.    0.    0.   ]
 [0.269 0.731 0.   ]
 [0.665 0.09  0.245]]
[[1.    0.    1.    0.   ]
 [0.269 1.462 0.269 0.   ]
 [0.665 0.18  0.665 0.734]]

Token 1 sees only itself, token 2 splits 0.269 and 0.731 between tokens 1 and 2, and the upper triangle is exactly 0.

Setting the masked scores to 0 wouldn’t work, because a score of 0 still gets weight e0=1e^0 = 1. Token 1’s row would become [0.5, 0, 0], whose softmax is [0.452, 0.274, 0.274], with more than half its attention on tokens it shouldn’t see.

A row with every entry masked would compute 0/00/0, which comes out as NaN (not a number). That’s one reason some code masks with a very large negative finite number instead: Hugging Face’s masking code uses torch.finfo(dtype).min, and The Annotated Transformer uses -1e9.

Step 2: why divide by the square root of d

The variance argument

Part 10 gave two rules about variance, the average squared distance from the mean. Rule one: multiplying by a constant aa multiplies variance by a2a^2. Rule two: for independent quantities, variances add.

Suppose the entries of a query and a key are independent random numbers with mean 0 and variance 1. One term qikiq_i k_i then has mean 0, and its variance is the average of qi2ki2q_i^2 k_i^2, which is 1×1=11 \times 1 = 1. The score is a sum of dd such terms, so by rule two:

Var(q⋅k)=d\text{Var}(\mathbf{q} \cdot \mathbf{k}) = d

Its spread, the square root of the variance, is d\sqrt{d}: about 11.3 for Llama 3.1 8B’s 128-number heads. Dividing by d\sqrt{d} multiplies the variance by 1/d1/d (rule one), back to 1 at any head size. This is footnote 4 of the 2017 paper, which assumes “the components of q and k are independent random variables with mean 0 and variance 1” and concludes that their dot product “has mean 0 and variance dkd_k.”

What unscaled scores do to softmax

The paper’s worry was that large scores push “the softmax function into regions where it has extremely small gradients.” To see how sharp softmax gets, give each of 500 random queries 1,024 random keys and measure the rows:

import numpy as np

rng = np.random.default_rng(0)
n, trials = 1024, 500                        # 1,024 keys per query, 500 queries

def softmax_rows(X):
    E = np.exp(X - X.max(axis=1, keepdims=True))
    return E / E.sum(axis=1, keepdims=True)

for d in [16, 64, 128, 256]:
    q = rng.standard_normal((trials, d))     # entries: mean 0, variance 1
    K = rng.standard_normal((n, d))
    S = q @ K.T                              # raw scores
    for name, X in [("raw", S), ("/sqrt(d)", S / np.sqrt(d))]:
        P = softmax_rows(X)
        H = -(P * np.log(P + 1e-300)).sum(axis=1)     # entropy of each row, in nats
        print(f"d={d:<4}{name:9} spread {X.std():5.2f}  top weight {P.max(axis=1).mean():.3f}"
              f"  entropy {H.mean():.2f}  rows with top > 0.99: {np.mean(P.max(axis=1) > 0.99):.0%}")
d=16  raw       spread  3.99  top weight 0.420  entropy 2.45  rows with top > 0.99: 1%
d=16  /sqrt(d)  spread  1.00  top weight 0.017  entropy 6.43  rows with top > 0.99: 0%
d=64  raw       spread  8.04  top weight 0.702  entropy 0.93  rows with top > 0.99: 9%
d=64  /sqrt(d)  spread  1.00  top weight 0.016  entropy 6.43  rows with top > 0.99: 0%
d=128 raw       spread 11.38  top weight 0.817  entropy 0.53  rows with top > 0.99: 23%
d=128 /sqrt(d)  spread  1.01  top weight 0.017  entropy 6.43  rows with top > 0.99: 0%
d=256 raw       spread 16.02  top weight 0.861  entropy 0.37  rows with top > 0.99: 39%
d=256 /sqrt(d)  spread  1.00  top weight 0.017  entropy 6.43  rows with top > 0.99: 0%

Entropy (Part 6) measures spread, and ee raised to it is the effective number of choices. With scaling, every head size gives 6.43, about 620 keys’ worth of attention out of 1,024. Without it, at d=128d = 128, the entropy is 0.53, about 1.7 keys, and in 23% of rows a single key takes more than 99% of the weight. At d=256d = 256 that share reaches 39%.

The scaling removes the dependence on dd. Trained queries and keys don’t have unit-variance entries, and the model can learn larger or smaller ones, which acts like a learned temperature (Part 4). μP divides by dd instead, for reasons Part 10 covers.

Step 3: the softmax Jacobian

The full grid of rates

Part 5 stated how probability pip_i responds when score zjz_j moves. To derive it, write pi=ezi/Zp_i = e^{z_i} / Z, where Z=∑kezkZ = \sum_k e^{z_k} is the total. Raising zjz_j changes the top only if j=ij = i, and it always changes the bottom, at rate ezje^{z_j}. The quotient rule from Part 7 gives:

∂pi∂zj=δij eziZ−ezi ezjZ2=pi(δij−pj)\frac{\partial p_i}{\partial z_j} = \frac{\delta_{ij}\, e^{z_i}}{Z} - \frac{e^{z_i}\, e^{z_j}}{Z^2} = p_i(\delta_{ij} - p_j)

Here δij\delta_{ij} is 1 when i=ji = j and 0 otherwise: a probability rises with its own score at rate pi(1−pi)p_i(1 - p_i) and falls at rate pipjp_i p_j when another score rises. As a matrix:

J=diag(p)−pp⊤J = \text{diag}(\mathbf{p}) - \mathbf{p}\mathbf{p}^{\top}

where diag(p)\text{diag}(\mathbf{p}) puts the probabilities on the diagonal of a square matrix and pp⊤\mathbf{p}\mathbf{p}^{\top} is the grid of all products pipjp_i p_j.

For token 3’s weights from Step 1, p=[0.6652,0.0900,0.2447]\mathbf{p} = [0.6652, 0.0900, 0.2447]:

J=[0.2227−0.0599−0.1628−0.05990.0819−0.0220−0.1628−0.02200.1848]J = \begin{bmatrix} 0.2227 & -0.0599 & -0.1628 \\ -0.0599 & 0.0819 & -0.0220 \\ -0.1628 & -0.0220 & 0.1848 \end{bmatrix}

The top-left entry is 0.6652×0.3348=0.22270.6652 \times 0.3348 = 0.2227, and nudging the first score by 10−610^{-6} reproduces the first column to four decimals. The matrix is symmetric, with a positive diagonal and negative entries elsewhere: raising one score takes probability from all the others. Every row adds up to 0, which is Part 5’s shift invariance in derivative form, since raising all scores together changes nothing.

Using it without building it

Backpropagation (Part 8) never needs JJ itself, only JJ times an incoming gradient. If pˉ\bar{\mathbf{p}} is the gradient of the loss with respect to the weights, the gradient with respect to the scores is

zˉ=Jpˉ=p∘(pˉ−(p⋅pˉ))\bar{\mathbf{z}} = J\bar{\mathbf{p}} = \mathbf{p} \circ \big(\bar{\mathbf{p}} - (\mathbf{p} \cdot \bar{\mathbf{p}})\big)

where ∘\circ means multiply entry by entry. The pp⊤\mathbf{p}\mathbf{p}^{\top} part needs only the dot product p⋅pˉ\mathbf{p} \cdot \bar{\mathbf{p}}, so a row of nn scores costs about 3n3n operations instead of n2n^2.

Attention adds a second shortcut. The forward pass computes S=QK⊤/dS = QK^{\top}/\sqrt{d}, P=softmax(S)P = \text{softmax}(S) and O=PVO = PV. Given dOdO, the gradient of the loss with respect to the output, the backward pass in FlashAttention’s Appendix B.2 runs:

dV=P⊤dO,dP=dO V⊤dV = P^{\top} dO, \qquad dP = dO\, V^{\top} dSij=Pij (dPij−Di),Di=∑jPij dPijdS_{ij} = P_{ij}\,(dP_{ij} - D_i), \qquad D_i = \sum_j P_{ij}\, dP_{ij}

The second line is the formula above, applied to each row, and the trick is in DiD_i. Since dPij=dOi⋅vjdP_{ij} = dO_i \cdot \mathbf{v}_j, the sum is dOi⋅∑jPijvjdO_i \cdot \sum_j P_{ij}\mathbf{v}_j, and that sum is the output row OiO_i. So Di=dOi⋅OiD_i = dO_i \cdot O_i, a dot product of two vectors the backward pass already has. FlashAttention computes every DiD_i first, then rebuilds PP block by block from the saved log-sum-exp, and never stores the n×nn \times n grid. Checked against PyTorch:

import torch
torch.manual_seed(0)
n, d = 6, 4
Q, K, V = (torch.randn(n, d, dtype=torch.float64, requires_grad=True) for _ in range(3))
P = torch.softmax(Q @ K.T / d ** 0.5, dim=-1)
O = P @ V
dO = torch.randn(n, d, dtype=torch.float64)
O.backward(dO)                                 # PyTorch's own gradients

with torch.no_grad():
    dP = dO @ V.T
    D = (dO * O).sum(dim=1, keepdim=True)      # one number per row
    dS = P * (dP - D)
    dQ, dK, dV = dS @ K / d ** 0.5, dS.T @ Q / d ** 0.5, P.T @ dO
print([f"{(a - b).abs().max().item():.1e}" for a, b in [(dQ, Q.grad), (dK, K.grad), (dV, V.grad)]])
['2.2e-16', '2.2e-16', '0.0e+00']

When attention saturates

If one weight is 1 and the rest are 0, then diag(p)\text{diag}(\mathbf{p}) and pp⊤\mathbf{p}\mathbf{p}^{\top} are the same matrix and J=0J = 0, so no gradient reaches the scores or the query and key matrices behind them. Multiply Step 1’s scores [2, 0, 1] by 8, about what skipping the scale does at d=64d = 64: the top weight becomes 0.99966, and the top-left entry of JJ drops from 0.2227 to 0.000335, 664 times smaller. Step 2 found 23% of unscaled rows in that state at d=128d = 128.

Training can still grow scores until they saturate, so current models add the guards Part 10 described: QK-norm, which normalizes queries and keys, and soft-capping with tanh⁡\tanh.

Step 4: what n tokens cost

For one head, QK⊤QK^{\top} multiplies an n×dn \times d matrix by a d×nd \times n one: 2n2d2n^2 d floating-point operations (FLOPs) by Part 1’s rule. Multiplying the n×nn \times n weights by VV costs the same again, so a head costs 4n2d4n^2 d FLOPs and holds n2n^2 scores. That’s the O(n2⋅d)O(n^2 \cdot d) in the 2017 paper’s Table 1. With Llama 3.1 8B’s shapes (32 layers, 32 query heads of d=128d = 128, 8 key-value heads) with the per-head rows counting the full grid and the whole-model rows letting the causal mask skip about half the pairs:

Quantity8,192 tokens131,072 tokens
Scores per head, per layer67.1 million17.2 billion
Memory for those scores at 2 bytes134 MB34.4 GB
Same, all 32 heads of one layer4.3 GB1,100 GB
Score and value FLOPs, one head, one layer, full grid3.4 × 10^108.8 × 10^12
Attention scores and values, whole model1.76 × 10^134.50 × 10^15
Q, K, V and output projections, whole model2.20 × 10^133.52 × 10^14
Scores and values ÷ projections0.8012.8

Sixteen times the tokens gives 256 times the scores, while the projections, which multiply each token by fixed weight matrices, grow 16 times. Per token, the projections cost 2 FLOPs per weight. Llama’s four attention matrices hold 4096×(4096+1024+1024+4096)=41.94096 \times (4096 + 1024 + 1024 + 4096) = 41.9 million weights per layer, so 83.9 million FLOPs. A token at position tt costs 4t×40964t \times 4096 in scores and values across all heads. They’re equal at

t=dmodel+nkvdhead=4096+1024=5,120t = d_{\text{model}} + n_{\text{kv}} d_{\text{head}} = 4096 + 1024 = 5{,}120

where nkvn_{\text{kv}} is the number of key-value heads. Past position 5,120, a token spends more on attention than on its attention weights; over a whole prompt, where the average token sits halfway, the crossover is 10,240 tokens. Part 1 and Fundamentals Part 5 compare these terms with the rest of the model.

FlashAttention removes the memory problem by keeping the n×nn \times n grid in tiles on the chip (Part 5’s online softmax; Inference Part 7), but not the n2n^2 arithmetic.

Step 5: multi-head attention as low-rank pieces

The QK circuit

Write head hh‘s query and key as qi=WQhxi\mathbf{q}_i = W_Q^h \mathbf{x}_i and kj=WKhxj\mathbf{k}_j = W_K^h \mathbf{x}_j, where x\mathbf{x} is a token’s vector of dmodeld_{\text{model}} numbers and each WW is dhead×dmodeld_{\text{head}} \times d_{\text{model}}. The score is

qi⋅kj=xi⊤(WQh⊤WKh)xj=xi⊤WQKh xj\mathbf{q}_i \cdot \mathbf{k}_j = \mathbf{x}_i^{\top} \big(W_Q^{h\top} W_K^h\big) \mathbf{x}_j = \mathbf{x}_i^{\top} W_{QK}^h\, \mathbf{x}_j

The queries and keys have disappeared, leaving one dmodel×dmodeld_{\text{model}} \times d_{\text{model}} matrix, WQKhW_{QK}^h, that scores any pair of token vectors directly. For Llama 3.1 8B it’s 4,096 by 4,096, 16.8 million entries built from two factors of 524,288 each. Part 3’s rule that a product’s rank is at most the smaller rank caps it at 128: only 128 of the 4,096 directions matter to this head’s scores.

On the value side, the head reads vj=WVhxj\mathbf{v}_j = W_V^h \mathbf{x}_j and its slice WOhW_O^h of the output projection writes back, so what reaches the residual stream is WOhWVhxj=WOVhxjW_O^h W_V^h \mathbf{x}_j = W_{OV}^h \mathbf{x}_j, again 4,096 by 4,096 with rank at most 128. Multi-head attention is a sum of 32 such pieces, and since 32×128=4,09632 \times 128 = 4{,}096, together they can cover the whole space.

Anthropic’s A Mathematical Framework for Transformer Circuits (Elhage et al., 2021) calls these the QK and OV circuits: “W_O W_V and W_Q^T W_K can always be thought of as individual, low-rank matrices,” and “keys, queries and value vectors are, in some sense, superficial.”

Checking a real model

Qwen2.5-1.5B has dmodel=1,536d_{\text{model}} = 1{,}536 and 12 query heads of 128, which share 2 key-value heads. Multiply out head 0’s two circuits in the first layer:

import glob, numpy as np
from safetensors import safe_open

path = glob.glob("hf/hub/models--Qwen--Qwen2.5-1.5B-Instruct/snapshots/*/model.safetensors")[0]
with safe_open(path, "pt") as f:
    get = lambda n: f.get_tensor(f"model.layers.0.self_attn.{n}.weight").double().numpy()
    Wq, Wk, Wv, Wo = get("q_proj"), get("k_proj"), get("v_proj"), get("o_proj")

dh = 128                                         # head 0 uses key/value head 0
W_QK = Wq[:dh].T @ Wk[:dh]                       # 1536 x 1536
W_OV = Wo[:, :dh] @ Wv[:dh]                      # 1536 x 1536
for name, W in [("W_QK", W_QK), ("W_OV", W_OV)]:
    s = np.linalg.svd(W, compute_uv=False)
    print(f"{name} {W.shape}: rank {np.linalg.matrix_rank(W)}, "
          f"singular values #1 {s[0]:.3g}, #128 {s[127]:.3g}, #129 {s[128]:.3g}")
W_QK (1536, 1536): rank 128, singular values #1 12, #128 0.208, #129 6.04e-15
W_OV (1536, 1536): rank 128, singular values #1 1.56, #128 0.0832, #129 1.06e-15

Singular values (Part 13) measure how much a matrix stretches each independent direction. Both matrices have exactly 128 nonzero ones; the 129th is rounding error. The real model adds two details: Qwen’s query and key projections have biases, and RoPE (Part 12) rotates queries and keys by position, so the score matrix depends on the distance between tokens, with rank at most 128 at every distance.

Step 6: GQA and MLA as factorizations

Grouped-query attention: shared right factors

In grouped-query attention (GQA), from Ainslie et al. (2023), groups of query heads share one key head and one value head. In circuit terms, the four heads of a Llama group have score matrices WQh⊤WKgW_Q^{h\top} W_K^{g} with the same right factor WKgW_K^g: each asks its own question of the same 128-number view of every token.

With one group it becomes multi-query attention (MQA), and with one group per head, ordinary multi-head attention. The cache holds 2×nkv×dhead2 \times n_{\text{kv}} \times d_{\text{head}} numbers per token and layer: 2,048 for Llama 3.1 8B’s 8 key-value heads, against 8,192 with 32, a factor of 4. Multi-query attention would cut it by 32, and Qwen2.5-1.5B’s 2 key-value heads for 12 query heads cut it by 6. Inference Part 2 turns these factors into bytes and users per GPU.

Multi-head latent attention: a low-rank key and value

DeepSeek’s multi-head latent attention (MLA), from DeepSeek-V2, factors keys and values through a small shared vector, the latent:

ct=WDKV ht,kth=WUKh ct,vth=WUVh ct\mathbf{c}_t = W_{DKV}\, \mathbf{h}_t, \qquad \mathbf{k}_t^h = W_{UK}^h\, \mathbf{c}_t, \qquad \mathbf{v}_t^h = W_{UV}^h\, \mathbf{c}_t

Here ht\mathbf{h}_t is token tt‘s vector, WDKVW_{DKV} projects it down to the latent ct\mathbf{c}_t, and WUKhW_{UK}^h and WUVhW_{UV}^h rebuild head hh‘s key and value. DeepSeek-V2 has a 512-number latent and 128 heads of 128. The map from a token to all 16,384 key numbers is WUKWDKVW_{UK} W_{DKV}, with an inner size of 512, so its rank is at most 512: MLA is a low-rank factorization, and the cache stores only the thin middle.

Associativity removes the need to rebuild keys. Head hh‘s score for the new token tt against cached token jj is

qth⊤kjh=qth⊤WUKh cj=(WUKh⊤ qth)⊤cj\mathbf{q}_t^{h\top} \mathbf{k}_j^h = \mathbf{q}_t^{h\top} W_{UK}^h\, \mathbf{c}_j = \big(W_{UK}^{h\top}\, \mathbf{q}_t^h\big)^{\top} \mathbf{c}_j

Moving the parentheses carries the query into the latent space. Since qth=WQhht\mathbf{q}_t^h = W_Q^h \mathbf{h}_t, the product WUKh⊤WQhW_{UK}^{h\top} W_Q^h can be multiplied out once, ahead of time. (DeepSeek-V2 compresses its queries too, so its WUQW^{UQ} plays the part of WQhW_Q^h.) In the paper’s words, “due to the associative law of matrix multiplication, we can absorb WUKW^{UK} into WUQW^{UQ}, and WUVW^{UV} into WOW^{O}.” A small check:

import numpy as np
rng = np.random.default_rng(0)
d, dc, H, dh, n = 64, 16, 4, 8, 10       # model width, latent, heads, head size, cached tokens

W_DKV = rng.standard_normal((dc, d))      # down-projection: h -> latent c (cached)
W_UK  = rng.standard_normal((H, dh, dc))  # per-head up-projection: c -> key
W_Q   = rng.standard_normal((H, dh, d))   # per-head query projection

C = rng.standard_normal((n, d)) @ W_DKV.T # cache: 16 numbers per earlier token
h_now = rng.standard_normal(d)            # the token being generated
for i in range(H):
    q = W_Q[i] @ h_now
    naive = (C @ W_UK[i].T) @ q           # rebuild 8-number keys, then score
    absorbed = C @ (W_UK[i].T @ q)        # 16-number query against the latents
    print(f"head {i}: max difference {np.abs(naive - absorbed).max():.1e}")
head 0: max difference 5.7e-13
head 1: max difference 4.5e-13
head 2: max difference 3.4e-13
head 3: max difference 2.3e-13

No key is built. The absorbed query has 512 numbers instead of 128, which costs multiply-adds, but decode is limited by memory reads (Inference Part 1).

Why RoPE gets in the way

Rotary position embedding (RoPE) multiplies each key by a rotation matrix RjR_j that depends on its position. On the rebuilt key, the score becomes qt⊤Rt⊤RjWUKcj\mathbf{q}_t^{\top} R_t^{\top} R_j W_{UK} \mathbf{c}_j, with a position-dependent matrix between the query and WUKW_{UK}, so no single matrix can be absorbed ahead of time. In DeepSeek-V2’s words, “a RoPE matrix related to the currently generating token will lie between WQW^Q and WUKW^{UK} and matrix multiplication does not obey a commutative law.”

The fix “decouples” position: each head’s query gets 64 extra numbers that carry RoPE, matched against one 64-number key shared by all heads and cached with the latent. That’s 512+64=576512 + 64 = 576 cached numbers per token per layer, against 2×128×128=32,7682 \times 128 \times 128 = 32{,}768 for multi-head attention with the same heads, 57 times less, or what GQA would store with 576/256=2.25576/256 = 2.25 groups. Part 12 derives why rotations behave this way.

Step 7: around the n² wall

Linear attention: reorder the product

Write one row of attention with a general similarity function sim\text{sim}:

oi=∑jsim(qi,kj) vj∑jsim(qi,kj)\mathbf{o}_i = \frac{\sum_j \text{sim}(\mathbf{q}_i, \mathbf{k}_j)\, \mathbf{v}_j}{\sum_j \text{sim}(\mathbf{q}_i, \mathbf{k}_j)}

Softmax attention uses sim=eq⋅k/d\text{sim} = e^{\mathbf{q} \cdot \mathbf{k} / \sqrt{d}}. Katharopoulos et al. (2020) used ϕ(q)⋅ϕ(k)\phi(\mathbf{q}) \cdot \phi(\mathbf{k}) instead, where ϕ\phi is applied to every entry and keeps similarities positive; their ϕ(x)=elu(x)+1\phi(x) = \text{elu}(x) + 1 is x+1x + 1 for positive xx and exe^x otherwise. Now ϕ(qi)\phi(\mathbf{q}_i) comes out of both sums:

oi=ϕ(qi)⊤∑jϕ(kj) vj⊤ϕ(qi)⊤∑jϕ(kj)\mathbf{o}_i = \frac{\phi(\mathbf{q}_i)^{\top} \sum_j \phi(\mathbf{k}_j)\, \mathbf{v}_j^{\top}}{\phi(\mathbf{q}_i)^{\top} \sum_j \phi(\mathbf{k}_j)}

In matrix form, the numerator is (ϕ(Q)ϕ(K)⊤)V=ϕ(Q)(ϕ(K)⊤V)\big(\phi(Q)\phi(K)^{\top}\big)V = \phi(Q)\big(\phi(K)^{\top}V\big). The left side builds an n×nn \times n grid; the right side builds a d×dd \times d matrix first, for about 4nd24nd^2 FLOPs instead of 4n2d4n^2d. For one head with d=128d = 128 at 128K tokens, that’s the opening’s 8.6 billion against 8.8 trillion, a factor of n/d=1,024n/d = 1{,}024.

Two paths from the same Q, K and V, each n tokens by d numbers, to the same n by d output. Softmax attention multiplies Q by K-transpose first, giving an n by n grid of scores, adds the mask, takes the softmax of each row, and multiplies the weights by V, with cost that grows with n squared. Linear attention multiplies phi of K, transposed, by V first, giving a d by d state, then multiplies phi of Q by that state, with cost that grows with n.Two paths from the same Q, K and V, each n tokens by d numbers, to the same n by d output. Softmax attention multiplies Q by K-transpose first, giving an n by n grid of scores, adds the mask, takes the softmax of each row, and multiplies the weights by V, with cost that grows with n squared. Linear attention multiplies phi of K, transposed, by V first, giving a d by d state, then multiplies phi of Q by that state, with cost that grows with n.

Swipe to pan the diagram, or tap it to zoom.

The same three matrices, multiplied in two orders. Softmax between the first two products forces the expensive order.

Softmax attention must take the top path, because, as the paper notes, the exponential of a dot product doesn’t split into a dot product of finite-size pieces.

It’s a recurrence

With a causal mask, row ii only sums over j≤ij \le i, so running totals work:

Si=Si−1+ϕ(ki) vi⊤,zi=zi−1+ϕ(ki)S_i = S_{i-1} + \phi(\mathbf{k}_i)\, \mathbf{v}_i^{\top}, \qquad \mathbf{z}_i = \mathbf{z}_{i-1} + \phi(\mathbf{k}_i) oi=ϕ(qi)⊤Siϕ(qi)⊤zi\mathbf{o}_i = \frac{\phi(\mathbf{q}_i)^{\top} S_i}{\phi(\mathbf{q}_i)^{\top} \mathbf{z}_i}

The state is a d×dd \times d matrix SS and a dd-vector z\mathbf{z}, the same size for 10 tokens or 10 million. That’s a recurrent neural network (RNN), hence the paper’s title and its observation that “any transformer layer with causal masking can be written as a model that, given an input, modifies an internal state and then predicts an output.” It reported generation “up to 4000x faster” on long sequences. A check:

import numpy as np
rng = np.random.default_rng(0)
n, d = 512, 16
Q, K, V = (rng.standard_normal((n, d)) for _ in range(3))

def phi(x):                                  # elu(x) + 1, always positive
    return np.where(x > 0, x + 1, np.exp(x))

# Quadratic form: build the n x n grid, mask the future, normalize each row
A = np.tril(phi(Q) @ phi(K).T)
out_quadratic = (A @ V) / A.sum(axis=1, keepdims=True)

# Recurrent form: a d x d state and a d-vector, updated once per token
S, z = np.zeros((d, d)), np.zeros(d)
out_recurrent = np.zeros((n, d))
for i in range(n):
    S += np.outer(phi(K[i]), V[i])
    z += phi(K[i])
    out_recurrent[i] = phi(Q[i]) @ S / (phi(Q[i]) @ z)

print("max difference:", np.abs(out_quadratic - out_recurrent).max())
print("numbers kept: grid", n * n, " state", d * d + d)
max difference: 4.440892098500626e-16
numbers kept: grid 262144  state 272

What a fixed-size state forgets

Store nn random key-value pairs as S=∑jkjvj⊤S = \sum_j \mathbf{k}_j \mathbf{v}_j^{\top} and look one up with kj⊤S\mathbf{k}_j^{\top} S. You get vj\mathbf{v}_j back, plus every other value weighted by its key’s overlap with kj\mathbf{k}_j: about 1/d1/\sqrt{d} each (Part 2), but n−1n - 1 of them. We stored and recalled random pairs with d=64d = 64, and did the same with softmax attention:

Pairs storedLinear state (numbers)Softmax cache (numbers)Recall, linearRecall, softmax
164,0962,0480.911.00
644,0968,1920.691.00
2564,09632,7680.451.00
1,0244,096131,0720.241.00

Recall is the cosine between the value retrieved and the value stored; the linear column follows 1/1+n/d1/\sqrt{1 + n/d}. Softmax keeps every pair, so its memory grows with nn and its recall holds. Jelassi et al. (2024) proved that fixed-state models “are fundamentally limited by their fixed-size latent state” when copying, and found pretrained Transformers “dramatically outperform state space models at copying and retrieving information from context.”

State-space models and Mamba

A state-space model updates a hidden state h\mathbf{h} with a linear recurrence:

ht=Aˉ ht−1+Bˉ xt,yt=C ht\mathbf{h}_t = \bar{A}\, \mathbf{h}_{t-1} + \bar{B}\, x_t, \qquad y_t = C\, \mathbf{h}_t

Here xtx_t is the input at step tt, yty_t the output, and Aˉ\bar{A}, Bˉ\bar{B} and CC are matrices; Aˉ\bar{A} sets how much of the old state survives each step. Older designs kept them fixed. Mamba (Gu and Dao, 2023) computes them from the current token, with Aˉ=exp⁡(ΔA)\bar{A} = \exp(\Delta A) and an input-dependent step size Δ\Delta, so the model can “selectively propagate or forget information along the sequence length dimension depending on the current token.” The paper’s Theorem 1 shows the simplest case is a gate, ht=(1−gt) ht−1+gt xth_t = (1 - g_t)\, h_{t-1} + g_t\, x_t, with gtg_t between 0 and 1 computed from the token: near 1 overwrites the memory, near 0 ignores the token.

Linear attention is this kind of recurrence with Aˉ=1\bar{A} = 1: nothing is ever forgotten. Mamba’s authors put the trade-off this way: “a fundamental problem of sequence modeling is compressing context into a smaller state,” and attention “is both effective and inefficient because it explicitly does not compress context at all.” Mamba reported 5 times the generation throughput of a Transformer of similar size, and the recall table shows the cost.

In the real world

  • PyTorch’s scaled_dot_product_attention uses scale_factor = 1 / math.sqrt(query.size(-1)) by default, and its reference code builds the causal mask with masked_fill_(..., float("-inf")). Its enable_gqa option shares key-value heads by copying them with repeat_interleave.
  • Gemma 2 27B doesn’t divide by the square root of its head size. Its config has head_dim 128 but query_pre_attn_scalar 144 (4,608 divided by 32 heads), which Hugging Face’s Gemma 2 code turns into self.scaling = config.query_pre_attn_scalar**-0.5, a division by 12 instead of 11.3. It also caps scores with attn_logit_softcapping 50.
  • With Step 3’s DiD_i, FlashAttention’s backward pass “can also be computed with O(n)O(n) extra memory,” and DeepSeek-V2 reports that MLA’s 576-number cache comes with performance “stronger than MHA.”
  • Production models mix attention with recurrences. AI21’s Jamba uses “a : m = 1 : 7,” one attention layer for every seven Mamba layers, with a 4 GB cache at 256K tokens (Inference Part 11). Qwen3-Next-80B-A3B’s model card lists the layout “12 * (3 * (Gated DeltaNet -> MoE) -> 1 * (Gated Attention -> MoE))”: 36 of its 48 layers use Gated DeltaNet, a gated linear-attention recurrence. Only 12 keep a cache, which by our arithmetic (2 key-value heads of 256) is 12×2×2×256=12,28812 \times 2 \times 2 \times 256 = 12{,}288 numbers per token, a quarter of what 48 attention layers would store.

Common misconceptions

  • “Dividing by √d stops the numbers from overflowing.” Subtracting the max handles overflow (Part 5). The scaling controls sharpness: without it, at d=128d = 128, 23% of random rows put over 99% of their weight on one key, where the Jacobian is nearly zero.
  • “The causal mask multiplies future scores by zero.” A score of 0 still gets weight e0=1e^0 = 1: zeroing token 1’s future scores in Step 1 left 55% of its attention on tokens it can’t see. Adding minus infinity drops those terms entirely.
  • “Backpropagating through attention means multiplying by each row’s n × n Jacobian.” That would cost n3n^3 per head. The row gradient is p∘(pˉ−p⋅pˉ)\mathbf{p} \circ (\bar{\mathbf{p}} - \mathbf{p} \cdot \bar{\mathbf{p}}), and attention needs only one extra dot product per row, Di=dOi⋅OiD_i = dO_i \cdot O_i.
  • “MLA saves compute.” It saves memory: 576 cached numbers per token per layer instead of 32,768. Its absorbed queries have 512 numbers instead of 128, so scoring each cached token takes more multiply-adds.
  • “Linear attention is a cheaper approximation of the same thing.” It uses a different similarity and a fixed-size memory. In Step 7’s test, its recall fell to 0.24 with 1,024 stored pairs; softmax stayed at 1.00.

Key takeaways

  • Attention is a soft dictionary lookup: score the query against every key, divide by the square root of the head size, softmax, and average the values. The causal mask adds minus infinity, so future tokens get exactly zero weight.
  • Query-key dot products have variance d. Dividing by the square root of d brings it back to 1 and keeps softmax from collapsing onto one key, where its Jacobian, diag p minus p p transpose, goes to zero.
  • The backward pass never builds that Jacobian. Each row needs one extra number, the output gradient dotted with the output, which lets FlashAttention skip the n by n grid.
  • Each head scores tokens with a model-width matrix of rank at most the head size. Grouped-query attention shares the key factor; latent attention factors keys and values through a small latent and uses associativity to skip rebuilding them.
  • Softmax forces queries times keys first, which costs n squared. Linear attention and Mamba multiply keys by values first and keep a fixed-size state: fast, but forgetful, so production models mix the two.

What’s next

RoPE kept getting in the way here, putting a rotation between query and key and breaking MLA’s absorb trick. Part 12, Rotations, complex numbers and RoPE, builds rotation matrices from the plane up, shows why rotating both vectors makes the score depend only on the distance between tokens, and works out the math behind stretching a model’s context window.

Further reading

Diagram

Scroll to zoom · drag to pan · double-click to zoom in · Esc to closePinch to zoom · drag to pan · double-tap to zoom in · tap outside to close