Chapter 12 · Section 12.2 · 45 min

Online softmax

Someone reads you a long list of numbers, one at a time, and asks for the softmax of the whole list at the end. You may keep only a couple of numbers in your head. What do you keep?

The obvious answer is the running sum of esje^{s_j}, and in exact arithmetic it works. In floating point it overflows as soon as one score passes about 88 (in float32) or 11 (in float16), and the standard cure from §4.3, subtracting the maximum score first, seems to need the maximum before you start summing, which you only know at the end. So the textbook "safe" softmax reads the list three times, once for the maximum, once for the sum, and once to divide.

This section shows that two numbers are enough, a running maximum and a running sum that you rescale whenever the maximum changes. We derive the recurrence, prove it correct, extend it to the weighted sum of values that attention actually needs, and then find the property that makes FlashAttention possible. Partial softmax results computed on separate pieces of a row can be merged, exactly, in any order. That turns the softmax from a whole-row operation into one you can compute tile by tile, which is what §12.1 showed we need.

Three passes, and why that is a problem

Fix one row of scores s1,…,sns_1, \dots, s_n (for attention, n=Tkn = T_k, the number of keys, and sjs_j is one query's score against key jj). The safe softmax is

m=max⁡jsj,ℓ=∑j=1nesj−m,yj=esj−mℓ.m = \max_j s_j, \qquad \ell = \sum_{j=1}^{n} e^{s_j - m}, \qquad y_j = \frac{e^{s_j - m}}{\ell} .

Subtracting mm changes nothing mathematically, since the factor e−me^{-m} cancels between numerator and denominator, but it makes the largest exponent e0=1e^0 = 1, so nothing overflows and ℓ≥1\ell \ge 1. Each of the three quantities is a pass over the row. If the row lives in slow memory, that is three reads of every score plus one write of the output. Milakov and Gimelshein (2018) counted these as memory accesses per element. The unsafe version (no maximum) needs 3, two loads and a store. The safe version needs 4.

For one row of a vocabulary softmax that is a 33% tax. For attention it is worse than a tax. The scores are themselves expensive to produce (each is a dkd_k-dimensional dot product), and FlashAttention never wants to store them at all. Three passes would mean computing QK⊤\mathbf{Q}\mathbf{K}^\top three times.

Two numbers that can be updated

Here is the idea, on the four scores 2,5,1,72, 5, 1, 7. Keep a running maximum mjm_j and a running sum ℓj\ell_j of exponentials measured relative to the current maximum.

  • After s1=2s_1 = 2, the maximum is 2 and the sum is e2−2=1e^{2-2} = 1.
  • Then s2=5s_2 = 5 arrives and the maximum jumps to 5. The old sum was measured relative to 2. Relative to 5, the same term is e2−5=e2−2⋅e2−5e^{2-5} = e^{2-2}\cdot e^{2-5}, so we multiply the old sum by emold−mnew=e−3≈0.0498e^{m_{\text{old}} - m_{\text{new}}} = e^{-3} \approx 0.0498 and add the new term e5−5=1e^{5-5} = 1, giving 1.04981.0498.
  • s3=1s_3 = 1 does not change the maximum, so we just add e1−5≈0.0183e^{1-5} \approx 0.0183.
  • s4=7s_4 = 7 raises the maximum again. Multiply by e5−7e^{5-7} and add 1.

The cell runs exactly this and checks, at every step, that the running sum equals the sum it is supposed to equal.

Python · cell 1

The two columns on the right agree at every step, and at the end (m,ℓ)=(7,1.1446)(m, \ell) = (7, 1.1446) gives the softmax directly. Step through it yourself in the widget, including rows that you make up, before reading the proof.

How do a running maximum and a rescaled running sum give the exact softmax in one pass?
Scores arrive left to right, one block at a time. The dashed line is the running maximum. When it rises, the sum so far is multiplied by e to the old maximum minus the new one. The lower bars are the weights each score would get if the row ended now.
Loading interactive figure…

The recurrence, and a proof that it is right

Start from m0=−∞m_0 = -\infty and ℓ0=0\ell_0 = 0, and for j=1,…,nj = 1, \dots, n set

mj=max⁡(mj−1, sj),ℓj=ℓj−1 emj−1−mj+esj−mj.m_j = \max(m_{j-1},\, s_j), \qquad \ell_j = \ell_{j-1}\, e^{m_{j-1} - m_j} + e^{s_j - m_j} .

In plain words, whenever the maximum goes up, shrink everything you have summed so far by the factor the maximum went up by, then add the new term.

Theorem (Milakov and Gimelshein 2018). For every j≥1j \ge 1,

mj=max⁡i≤jsiandℓj=∑i=1jesi−mj.m_j = \max_{i\le j} s_i \qquad\text{and}\qquad \ell_j = \sum_{i=1}^{j} e^{s_i - m_j} .

Proof, by induction on jj. For j=1j = 1, m1=max⁡(−∞,s1)=s1m_1 = \max(-\infty, s_1) = s_1, and ℓ1=0⋅e−∞+e0=1=es1−m1\ell_1 = 0\cdot e^{-\infty} + e^{0} = 1 = e^{s_1 - m_1}. (Here e−∞=0e^{-\infty} = 0, and the product is 0⋅00\cdot 0, not an indeterminate form, because m1m_1 is finite.) Now suppose both statements hold for j−1j - 1. The first statement for jj is immediate, since the maximum of a set with one element added is the larger of the old maximum and the new element. For the second,

ℓj=(∑i=1j−1esi−mj−1)emj−1−mj+esj−mj=∑i=1j−1esi−mj+esj−mj=∑i=1jesi−mj,\ell_j = \Big(\sum_{i=1}^{j-1} e^{s_i - m_{j-1}}\Big) e^{m_{j-1} - m_j} + e^{s_j - m_j} = \sum_{i=1}^{j-1} e^{s_i - m_j} + e^{s_j - m_j} = \sum_{i=1}^{j} e^{s_i - m_j},

where the middle step uses eaeb=ea+be^{a}e^{b} = e^{a+b} to combine the exponents. So the statement holds for every jj, and in particular after the last score, softmax⁡(s)i=esi−mn/ℓn\operatorname{softmax}(\mathbf{s})_i = e^{s_i - m_n}/\ell_n. ■\blacksquare

Two bounds come for free, and they are why this is numerically safe, not only correct. Every term esi−mje^{s_i - m_j} is at most 1 (because mjm_j is the maximum) and at least one equals 1 (the maximum itself), so

1≤ℓj≤j,0<emj−1−mj≤1(j≥2).1 \le \ell_j \le j, \qquad 0 \lt e^{m_{j-1} - m_j} \le 1 \quad (j \ge 2) .

In plain words, the running sum never overflows (it is at most the number of scores), never underflows to zero (it is at least 1), and the rescaling factor only ever shrinks what is already there. (At j=1j = 1 the factor is e−∞=0e^{-\infty} = 0, which discards the empty initial sum.) Each rescaling multiplies the sum by a factor computed with a relative rounding error of about εmach\varepsilon_{\text{mach}}, so after kk rescalings the relative error grows at most like k εmachk\,\varepsilon_{\text{mach}}, the same order as the error of summing nn floating-point numbers in the first place.

The result is a softmax with two passes and 3 memory accesses per element instead of 4, a 1.33 times reduction. On a V100, Milakov and Gimelshein measured up to 1.3 times faster softmax than the safe version at large batch sizes. The cell compares the pass counts directly by wrapping the row in an object that counts how many times it is iterated.

Python · cell 2
Interview

The online normalizer in one pass

Implement online_normalizer(scores) returning (m, l), the maximum of the scores and ℓ=∑jesj−m\ell = \sum_j e^{s_j - m}, using exactly one pass over scores. The tests pass a one-shot iterator, so a second pass (or max(scores) followed by a loop) sees nothing. Scores may be huge, and some may be −∞-\infty (masked). For an empty input, or one where every score is −∞-\infty, return (-inf, 0.0) without producing NaN.

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

From weights to outputs

Attention does not need the softmax weights themselves. It needs their weighted sum of value vectors, o=∑jyjvj\mathbf{o} = \sum_j y_j \mathbf{v}_j, and that removes the second pass too.

Carry a third quantity, the unnormalized output o~j=∑i≤jesi−mjvi\tilde{\mathbf{o}}_j = \sum_{i\le j} e^{s_i - m_j}\mathbf{v}_i, a vector of width dvd_v. It is measured relative to the same running maximum as ℓj\ell_j, so it obeys the same recurrence,

o~j=o~j−1 emj−1−mj+esj−mj vj,o=o~nℓn.\tilde{\mathbf{o}}_j = \tilde{\mathbf{o}}_{j-1}\, e^{m_{j-1} - m_j} + e^{s_j - m_j}\,\mathbf{v}_j, \qquad \mathbf{o} = \frac{\tilde{\mathbf{o}}_n}{\ell_n} .

The proof is the same induction with vi\mathbf{v}_i riding along in every term. In plain words, the output is accumulated in one pass, the running sum is accumulated beside it, and one division at the end makes it a weighted average.

There is a second way to write it that keeps the output normalized at every step, oj=o~j/ℓj\mathbf{o}_j = \tilde{\mathbf{o}}_j / \ell_j. Dividing the recurrence by ℓj\ell_j gives

oj=ℓj−1 emj−1−mj oj−1+esj−mj vjℓj.\mathbf{o}_j = \frac{\ell_{j-1}\,e^{m_{j-1} - m_j}\,\mathbf{o}_{j-1} + e^{s_j - m_j}\,\mathbf{v}_j}{\ell_j} .

This is the form in the original FlashAttention (line 12 of Algorithm 1 in Dao et al. 2022). It does a division at every step, which is wasted work, and FlashAttention-2 switched to the unnormalized form for exactly that reason (§12.4). Both are exact.

The last thing worth keeping is the log-sum-exp of the scores, lse⁡(s)=log⁡∑jesj=mn+log⁡ℓn\operatorname{lse}(\mathbf{s}) = \log\sum_j e^{s_j} = m_n + \log \ell_n. It is one number per query, it can be computed from the two we already have, and it is all the backward pass will need to rebuild the weights (§12.3), since yj=esj−lse⁡(s)y_j = e^{s_j - \operatorname{lse}(\mathbf{s})}.

Python · cell 3

One query, one pass over the keys and values, memory of size dv+2d_v + 2 no matter how many keys there are. This is the observation behind Rabe and Staats (2021), who noted that attention for a single query therefore needs only constant memory in the sequence length.

Merging partial results

The recurrence processes one score at a time. FlashAttention processes a block of keys at a time, and different GPU thread blocks, or different GPUs, may process different blocks. What we need is a way to combine summaries of two pieces of a row.

Split the keys into two disjoint sets A\mathcal{A} and B\mathcal{B}, and summarize each with a triple measured relative to its own maximum,

mA=max⁡i∈Asi,ℓA=∑i∈Aesi−mA,o~A=∑i∈Aesi−mA vi,m_{\mathcal{A}} = \max_{i\in\mathcal{A}} s_i, \qquad \ell_{\mathcal{A}} = \sum_{i\in\mathcal{A}} e^{s_i - m_{\mathcal{A}}}, \qquad \tilde{\mathbf{o}}_{\mathcal{A}} = \sum_{i\in\mathcal{A}} e^{s_i - m_{\mathcal{A}}}\,\mathbf{v}_i ,

and the same for B\mathcal{B}. Define the merge ⊕\oplus by

(mA,ℓA,o~A)⊕(mB,ℓB,o~B)=(m,  ℓAemA−m+ℓBemB−m,  o~AemA−m+o~BemB−m),m=max⁡(mA,mB).(m_{\mathcal{A}}, \ell_{\mathcal{A}}, \tilde{\mathbf{o}}_{\mathcal{A}}) \oplus (m_{\mathcal{B}}, \ell_{\mathcal{B}}, \tilde{\mathbf{o}}_{\mathcal{B}}) = \big(m,\; \ell_{\mathcal{A}}e^{m_{\mathcal{A}} - m} + \ell_{\mathcal{B}}e^{m_{\mathcal{B}} - m},\; \tilde{\mathbf{o}}_{\mathcal{A}}e^{m_{\mathcal{A}} - m} + \tilde{\mathbf{o}}_{\mathcal{B}}e^{m_{\mathcal{B}} - m}\big), \quad m = \max(m_{\mathcal{A}}, m_{\mathcal{B}}).

In plain words, bring both summaries to the common reference mm, the larger of the two maxima, then add.

Claim. The merge of the summaries of A\mathcal{A} and B\mathcal{B} is the summary of A∪B\mathcal{A}\cup\mathcal{B}. The maximum part is clear. For the sum, ℓAemA−m=∑i∈Aesi−mAemA−m=∑i∈Aesi−m\ell_{\mathcal{A}}e^{m_{\mathcal{A}} - m} = \sum_{i\in\mathcal{A}} e^{s_i - m_{\mathcal{A}}}e^{m_{\mathcal{A}} - m} = \sum_{i\in\mathcal{A}} e^{s_i - m}, and likewise for B\mathcal{B}, so the merged sum is ∑i∈A∪Besi−m\sum_{i\in\mathcal{A}\cup\mathcal{B}} e^{s_i - m}. The output is identical with vi\mathbf{v}_i in every term. ■\blacksquare

Check it by hand on the row 2,5,1,72, 5, 1, 7 from before, split into halves. The first half {2,5}\{2, 5\} has summary (5,  e−3+1)=(5,1.0498)(5,\; e^{-3} + 1) = (5, 1.0498) and the second {1,7}\{1, 7\} has (7,  e−6+1)=(7,1.0025)(7,\; e^{-6} + 1) = (7, 1.0025). The common reference is 7, so the merged sum is 1.0498 e5−7+1.0025 e7−7=0.1421+1.0025=1.14461.0498\,e^{5-7} + 1.0025\,e^{7-7} = 0.1421 + 1.0025 = 1.1446, exactly the ℓ\ell that the one-score-at-a-time recurrence produced. The order in which the scores were seen made no difference.

Three properties follow, and each one is used later in the chapter.

  1. Associative and commutative. Merging the summaries of A\mathcal{A}, B\mathcal{B}, C\mathcal{C} in any grouping or order gives the summary of A∪B∪C\mathcal{A}\cup\mathcal{B}\cup\mathcal{C}, because union is associative and commutative. Milakov and Gimelshein state this for the (m,ℓ)(m, \ell) pair. So the pieces of a row can be combined left to right, in a balanced tree of depth log⁡2\log_2 of the number of pieces, or as they arrive from different processors. This is the structure of a parallel reduction (§11.4).
  2. An identity element. The empty set has summary (−∞,0,0)(-\infty, 0, \mathbf{0}), and merging with it changes nothing. In code the case where both maxima are −∞-\infty needs care, because e−∞−(−∞)e^{-\infty - (-\infty)} is NaN. That case is a fully masked block, and it happens constantly with causal masks.
  3. The one-step recurrence is a special case. Processing sjs_j is merging the summary of the prefix with the summary of the single score, (sj,1,vj)(s_j, 1, \mathbf{v}_j).
Python · cell 4

The same merge, in log-sum-exp form

Kernels usually return the normalized output oA=o~A/ℓA\mathbf{o}_{\mathcal{A}} = \tilde{\mathbf{o}}_{\mathcal{A}}/\ell_{\mathcal{A}} together with lse⁡A=mA+log⁡ℓA=log⁡∑i∈Aesi\operatorname{lse}_{\mathcal{A}} = m_{\mathcal{A}} + \log\ell_{\mathcal{A}} = \log\sum_{i\in\mathcal{A}} e^{s_i}, two things with a direct meaning. The merge is then

lse⁡=log⁡ ⁣(else⁡A+else⁡B),o=else⁡A−lse⁡ oA+else⁡B−lse⁡ oB.\operatorname{lse} = \log\!\big(e^{\operatorname{lse}_{\mathcal{A}}} + e^{\operatorname{lse}_{\mathcal{B}}}\big), \qquad \mathbf{o} = e^{\operatorname{lse}_{\mathcal{A}} - \operatorname{lse}}\,\mathbf{o}_{\mathcal{A}} + e^{\operatorname{lse}_{\mathcal{B}} - \operatorname{lse}}\,\mathbf{o}_{\mathcal{B}} .

The derivation is short. The weight that the union's softmax puts on all of A\mathcal{A} together is ∑i∈Aesi/∑i∈A∪Besi=else⁡A−lse⁡\sum_{i\in\mathcal{A}} e^{s_i} / \sum_{i\in\mathcal{A}\cup\mathcal{B}} e^{s_i} = e^{\operatorname{lse}_{\mathcal{A}} - \operatorname{lse}}, and within A\mathcal{A} the weights are exactly those of oA\mathbf{o}_{\mathcal{A}}. In plain words, the merged output is a convex combination of the two partial outputs, weighted by how much softmax mass each piece holds, and the two weights sum to 1. The first log is computed stably as max⁡+log⁡(else⁡A−max⁡+else⁡B−max⁡)\max + \log(e^{\operatorname{lse}_{\mathcal{A}} - \max} + e^{\operatorname{lse}_{\mathcal{B}} - \max}), NumPy's logaddexp.

This is the form used when the pieces are computed far apart. Flash-Decoding splits one query's keys across many thread blocks (§12.4), and ring attention splits them across GPUs (§12.7). Each piece reports (o,lse⁡)(\mathbf{o}, \operatorname{lse}), and one merge combines them.

Interview

Merge two partial softmax results

A partial result for one query over a subset of keys is a tuple (m, l, acc), with m the maximum score in the subset, l =∑esi−m= \sum e^{s_i - m}, and acc =∑esi−mvi= \sum e^{s_i - m}\mathbf{v}_i (a vector). The empty subset is (-inf, 0.0, zeros). Implement merge(a, b), returning the partial result of the union, and finalize(p), returning (o, lse), the normalized output and lse⁡=m+log⁡ℓ\operatorname{lse} = m + \log\ell. Merging must be exact, stable for huge scores, and never produce NaN, even when one or both inputs are empty.

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

Blocks, the bridge to FlashAttention

Combine the two ideas and you have the inner loop of FlashAttention. Hold a block of BrB_r queries. Stream the keys and values past them in blocks of BcB_c at a time. For each key block, compute the block of scores (a small matrix product), summarize it per query row, and merge the summary into the running one. In matrix form, with m,ℓ\mathbf{m}, \boldsymbol{\ell} vectors over the rows and O~\tilde{\mathbf{O}} a matrix, one step is

mnew=max⁡ ⁣(m, rowmax⁡(Sj)),P~j=eSj−mnew,ℓ←em−mnew⊙ℓ+rowsum⁡(P~j),O~←diag⁡ ⁣(em−mnew)O~+P~jVj,\mathbf{m}^{\text{new}} = \max\!\big(\mathbf{m},\, \operatorname{rowmax}(\mathbf{S}_j)\big), \quad \tilde{\mathbf{P}}_j = e^{\mathbf{S}_j - \mathbf{m}^{\text{new}}}, \quad \boldsymbol{\ell} \leftarrow e^{\mathbf{m} - \mathbf{m}^{\text{new}}}\odot\boldsymbol{\ell} + \operatorname{rowsum}(\tilde{\mathbf{P}}_j), \quad \tilde{\mathbf{O}} \leftarrow \operatorname{diag}\!\big(e^{\mathbf{m} - \mathbf{m}^{\text{new}}}\big)\tilde{\mathbf{O}} + \tilde{\mathbf{P}}_j\mathbf{V}_j ,

where the subtraction Sj−mnew\mathbf{S}_j - \mathbf{m}^{\text{new}} broadcasts each row's maximum across that row. In plain words, this is the scalar recurrence, run for all query rows at once, a block of keys at a time, with the rescaling applied once per block rather than once per key. The expensive parts, Sj=QKj⊤/dk\mathbf{S}_j = \mathbf{Q}\mathbf{K}_j^\top/\sqrt{d_k} and P~jVj\tilde{\mathbf{P}}_j\mathbf{V}_j, are matrix products that run on tensor cores. The rescaling is a few elementwise operations on a Br×BcB_r\times B_c tile that never leaves on-chip memory. You will write this loop in the next exercise, and §12.3 adds the outer loop over query blocks, the causal mask, and the backward pass.

Core

Attention with keys streamed in blocks

Implement blocked_attention(Q, K, V, block) for queries Q of shape (n, d_k), keys K of shape (T_k, d_k), and values V of shape (T_k, d_v), returning (O, lse) with O of shape (n, d_v) and lse of shape (n,), for unmasked scaled dot-product attention. You may touch K and V only through slices of at most block rows (the tests hand you objects that refuse anything bigger, including conversion to an array). Process one key block at a time with vectorized NumPy, keeping a running maximum, sum, and unnormalized output per query row.

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

Precision, and what can still go wrong

The online softmax was designed for stability, so it is worth seeing it rescue a computation that the naive formula ruins. In float16 the largest representable number is 65,504, so e12≈163,000e^{12} \approx 163{,}000 overflows (§4.3).

Python · cell 5

The naive version returns zeros and NaN, and the online version gets every weight to float16 precision, with a running sum of 1.79 that never came near the limit. In practice kernels keep m\mathbf{m}, ℓ\boldsymbol{\ell}, and O~\tilde{\mathbf{O}} in float32 even when Q\mathbf{Q}, K\mathbf{K}, and V\mathbf{V} are bf16 or FP8, because they are a few numbers per row and their accuracy determines the output's (§8.5).

Four details still matter in real code.

  • Fully masked blocks and rows. With a causal mask most key blocks above the diagonal are entirely masked for some rows. If a row's running maximum is still −∞-\infty after such a block, em−mnewe^{m - m^{\text{new}}} is e−∞−(−∞)e^{-\infty - (-\infty)}, NaN. Kernels either skip blocks that are fully masked (§12.3) or substitute 0 for an infinite maximum before subtracting. A row that is masked everywhere ends with ℓ=0\ell = 0 and must be handled explicitly, as in §4.4.
  • The maximum is a convenience, not a requirement. Any number at least as large as every score would prevent overflow. If you knew a bound in advance, for instance because queries and keys are normalized (QK-norm, §5.7), you could subtract it once and never rescale. The running maximum is used because it is the tightest bound available without looking ahead, and a loose bound risks underflowing every term to zero.
  • Kernels use base 2. GPUs compute 2x2^x in one special-function instruction, and ex=2xlog⁡2ee^x = 2^{x\log_2 e}. So production kernels fold the constant log⁡2e\log_2 e into the score scale 1/dk1/\sqrt{d_k} once, keep mm in the scaled units, and call exp2 everywhere, which saves a multiplication per score and changes nothing else, because every identity above holds for any base. The stored log-sum-exp is converted back to natural log at the end if the caller expects it.
  • Rescaling costs exponentials. Each block needs BrB_r extra exponentials for em−mnewe^{\mathbf{m} - \mathbf{m}^{\text{new}}} on top of the BrBcB_rB_c for the scores, a fraction 1/Bc1/B_c of extra work. That is negligible for arithmetic, but exponentials run on a slow special-function unit, which is why FlashAttention-2 and 3 work hard to keep that unit busy in parallel with the tensor cores (§12.4).
Research

Fused softmax and top-k in one pass

Beam search and sampling often need only the kk most likely tokens and their probabilities under the full softmax. Implement softmax_topk(scores, k) that makes one pass over scores (a one-shot iterator) and returns (idx, probs), two NumPy arrays of length min⁡(k,n)\min(k, n), with the indices of the kk largest scores in decreasing order of score (ties broken by the smaller index first) and their softmax probabilities normalized over all nn scores. Use memory proportional to kk, not nn. Milakov and Gimelshein (2018) fuse exactly this into one kernel.

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

In PyTorch

PyTorch does not expose the online softmax as a function, because it lives inside fused kernels. You can still write the block recurrence in a few lines of torch and check it against torch.softmax and against scaled_dot_product_attention, which runs the same idea in C++ on CPU and in FlashAttention on GPU.

🔥 In PyTorchfor a machine with PyTorch

Read more

Watch next

🎬 Flash Attention Runs Softmax One Tile at a Time · DataMListicWorks the rescaling of the online softmax through on real numbers, including merging two blocks.
🎬 How FlashAttention Accelerates Generative AI Revolution · Jia-Bin Huang · starts at 5:38The safe softmax and online softmax segments, with clear animations of the running statistics.