← Back

August 2026 · Technical

Handwritten Notes on Flash Attention

Attention is not slow because of the math. Multiplying QKQ K^\top and then multiplying by VV is a perfectly reasonable number of FLOPs for a modern GPU. It’s slow because the N×NN \times N score matrix has to be written to HBM, read back for the softmax, written again, and read one more time for the multiply by VV. The GPU spends most of the wall clock waiting on memory while the tensor cores sit around doing nothing.

Flash Attention’s fix is to never materialize that matrix. It walks over blocks of QQ, KK, VV that fit in on-chip SRAM, and keeps a running softmax so it can produce the exact same output without ever holding a full row of scores in memory. Not an approximation — the same numbers, computed in a smarter order.

These are my handwritten notes working through the paper. The whole PDF is embedded below, and the rest of the post walks through it page by page.

Open Flash Attention.pdf ↗

Flash Attention.pdf — 4 sheets, ~2.5 MB. Sheet 1 is a cover page containing no attention whatsoever, so the walkthrough starts on page 2.

The short version

Three moving parts, and the notes hit them in order. Page 2 shows that softmax can be computed incrementally, then annotates Algorithm 1 and the SRAM budget that decides the block sizes. Page 3 grinds through the one line of the algorithm that looks like a hack — the output rescaling — and shows it isn’t. Page 4 counts the memory traffic and gets O(N2d2M1)O(N^2 d^2 M^{-1}) HBM accesses instead of Θ(N2)\Theta(N^2).

Page 2 — A softmax that never sees the whole row

Page 2 of the handwritten Flash Attention notes
Page 2online softmax, Algorithm 1, and the SRAM budget (click to open full size)

The obstacle to tiling attention is softmax. It needs a maximum over the whole row (for numerical stability) and a sum over the whole row (for the denominator), which is an inconvenient thing to need when you are deliberately looking at one block at a time. The top of this page fixes that by defining softmax through three running statistics instead of one pass.

Handwritten definition of the max, the exponentiated difference vector, and the denominator
Steps 1–4: the three pieces softmax is actually made of

For an input vector xRNx \in \mathbb{R}^N, grab only the first block of size BB:

m(x):=maxixif(x):=[ex1m(x)  exBm(x)]m(x) := \max_i\, x_i \qquad f(x) := \left[\, e^{x_1 - m(x)} \ \cdots \ e^{x_B - m(x)} \,\right]
(x):=if(x)isoftmax(x):=f(x)(x)\ell(x) := \sum_i f(x)_i \qquad \mathrm{softmax}(x) := \frac{f(x)}{\ell(x)}

The subtraction in step 2 is the standard overflow guard. Every exponent xim(x)x_i - m(x) is 0\leq 0, so every entry of f(x)f(x) lands safely in (0,1](0, 1] and nothing blows up. It also means mm is baked into ff and \ell, which is exactly the problem: a block computed against its own local max is, as the note bluntly puts it, “incorrect for softmax for now.”

Merging two blocks

Steps 5 and 6 are where it becomes an algorithm. Take two blocks x1,x2RBx^{1}, x^{2} \in \mathbb{R}^{B} and concatenate them into x=[x1,x2]R2Bx = [\,x^{1}, x^{2}\,] \in \mathbb{R}^{2B}. The new max is free:

Handwritten derivation of the merged max, merged exponent vector, and merged denominator
Steps 5–6: two blocks in, one correct softmax out
m(x)=m ⁣([x1 x2])=max ⁣(m(x1),m(x2))m(x) = m\!\left(\left[x^{1}\ x^{2}\right]\right) = \max\!\left(m(x^{1}),\, m(x^{2})\right)

The exponent vectors need a correction, and it’s a single scalar per block. Whatever a block computed against its own local max gets re-based to the global one:

f(x)=[em(x1)m(x)f(x1)em(x2)m(x)f(x2)]f(x) = \left[\, e^{m(x^{1}) - m(x)} f(x^{1}) \quad e^{m(x^{2}) - m(x)} f(x^{2}) \,\right]

The whole trick is one line of exponent arithmetic, drawn in the notes with a big \Downarrow:

em(x1)m(x)exim(x1)  =  exim(x)e^{\,m(x^{1}) - m(x)} \cdot e^{\,x_i - m(x^{1})} \;=\; e^{\,x_i - m(x)}

The local max cancels itself out. Sum both rescaled halves and you get the denominator, which is step 6:

(x)=em(x1)m(x)(x1)  +  em(x2)m(x)(x2),softmax(x)=f(x)(x)\ell(x) = e^{\,m(x^{1}) - m(x)}\,\ell(x^{1}) \;+\; e^{\,m(x^{2}) - m(x)}\,\ell(x^{2}), \qquad \mathrm{softmax}(x) = \frac{f(x)}{\ell(x)}

And since the merge takes two blocks and returns something with the same shape as a block, you can just keep going — “logic continues to N/BN/B blocks with dim BB.” Carry (m,)(m, \ell) as state, fold in one block at a time, and the exact softmax falls out at the end. Two scalars per row is the entire price.

Algorithm 1, annotated

The middle of the page is the algorithm box from the paper, with a reminder in the margin that it assumes a batch size of 1 — real implementations add batch and head dimensions on top, which changes nothing conceptually and everything about the indexing.

Algorithm 1 FlashAttention, as printed in the paper
Algorithm 1, straight from the paper

Line 1 picks the block sizes from the SRAM capacity MM, and this is the line worth staring at:

Bc=M4d,Br=min ⁣(M4d,  d)B_c = \left\lceil \frac{M}{4d} \right\rceil, \qquad B_r = \min\!\left(\left\lceil \frac{M}{4d} \right\rceil,\; d\right)
Handwritten notes on the ceiling term, HBM residents, and the block partitions
Why the 4d, and what lives in HBM

The 4d4d is there because a block of rows costs four dd-wide tiles at once: you are holding QQ, KK, VV and OO simultaneously. Divide the budget four ways and that’s how many rows fit.

Line 2 sets up what stays in HBM:

  • ORN×dO \in \mathbb{R}^{N \times d}the output matrix, initialized to zero.
  • RN\ell \in \mathbb{R}^{N}the running normalizer, initialized to all 0.
  • mRNm \in \mathbb{R}^{N}the running row-max vector, initialized to −∞ (so the first real max always wins).

Lines 3 and 4 do the tiling. QQ splits into Tr=N/BrT_r = \lceil N/B_r \rceil blocks of shape Br×dB_r \times d; KK and VV each split into Tc=N/BcT_c = \lceil N/B_c \rceil blocks of shape Bc×dB_c \times d. OO follows QQ’s partition, and so do \ell and mm — which, being BrB_r-length vectors rather than matrices, are small enough to keep resident in SRAM without anyone noticing.

Who loops over what

Diagram of the outer loop over K and V blocks and the inner loop over Q blocks, with copies into SRAM
Outer loop over K/V (red), inner loop over Q (blue)

The outer loop (line 5) walks over the KK, VV blocks; the inner loop (line 7) walks over the QQ blocks. So a single Kj,VjK_j, V_j pair is loaded into SRAM once and reused against every query block, while Qi,Oi,i,miQ_i, O_i, \ell_i, m_i get cycled through underneath it. Every block that gets computed on-chip is written back to HBM as a Br×dB_r \times d slice of the output — never as part of an N×NN \times N anything.

Line 9 is where that pays off. The score block is Sij=QiKjRBr×BcS_{ij} = Q_i K_j^\top \in \mathbb{R}^{B_r \times B_c}, and as the note in the corner says: we don’t need the N×NN \times N, since it’s made of these tiny grids and we only ever handle one grid at a time.

The SRAM budget

Handwritten SRAM budget: 2(B_r d) + 2(B_c d) + B_r B_c
Everything that has to fit on-chip at once

Adding up what a single iteration needs resident on-chip:

2(Brd)Qi, Oi  +  2(Bcd)Kj, Vj  +  (BrBc)QiKj\underbrace{2\,(B_r \cdot d)}_{Q_i,\ O_i} \;+\; \underbrace{2\,(B_c \cdot d)}_{K_j,\ V_j} \;+\; \underbrace{(B_r \cdot B_c)}_{Q_i K_j^\top}

The asymmetry between BrB_r and BcB_c is deliberate. KK and VV get the full column block size, because a wider BcB_c means more reuse per load. But QQ and OO are capped at dd rows — they fall back to dd to save resources, since making them taller buys nothing and costs SRAM. When BcdB_c \leq d the min collapses and Br=BcB_r = B_c, which tidies the budget to:

4Bcd  +  BcBc4\,B_c \cdot d \;+\; B_c \cdot B_c

Page 3 — Why line 12 isn’t a hack

Page 3 of the handwritten Flash Attention notes
Page 3unpacking the output update, term by term (click to open full size)

Line 12 is the line that makes people squint. It rescales the output block you already computed by a ratio of exponentials and a pair of diagonal matrices, and it is not at all obvious that what comes out the other side is a correct softmax-weighted average. This whole page is the check.

First, the running statistics

Handwritten line 11 with grid diagrams showing green processed blocks and the yellow current block
Line 11: green is everything processed so far, yellow is the block in hand

Line 11 updates the two per-row statistics. The new max is the running max against the current block’s max:

minew=max ⁣(mi,m~ij)RBrm_i^{\mathrm{new}} = \max\!\left(m_i,\, \tilde m_{ij}\right) \in \mathbb{R}^{B_r}
inew=emiminewirescaled sum from past green blocks  +  em~ijminew~ijrescaled from the new yellow\ell_i^{\mathrm{new}} = \underbrace{e^{\,m_i - m_i^{\mathrm{new}}}\,\ell_i}_{\text{rescaled sum from past green blocks}} \;+\; \underbrace{e^{\,\tilde m_{ij} - m_i^{\mathrm{new}}}\,\tilde\ell_{ij}}_{\text{rescaled from the new yellow}}

The two grids in the margin are the whole mental model. The small one makes the point that the max is taken per row of the block, not over the block as a whole — row ii of the tile carries its own mim_i, and that row-local max is simultaneously the running max for that row of the full attention matrix. The bigger grid colors row i=2i=2: green for the key blocks j=1,2j = 1, 2 already folded in, yellow for j=3j = 3, the one currently on-chip. That color coding runs through the rest of the page.

The line itself

Handwritten line 12, with the green and yellow terms labelled
Line 12, split into a green term and a yellow term
Oidiag ⁣(inew)1(diag(i)emiminewOi  +  em~ijminewP~ijVj)O_i \leftarrow \mathrm{diag}\!\left(\ell_i^{\mathrm{new}}\right)^{-1}\left( \textcolor{#6aa84f}{\mathrm{diag}(\ell_i)\, e^{\,m_i - m_i^{\mathrm{new}}} O_i} \;+\; \textcolor{#c8a532}{e^{\,\tilde m_{ij} - m_i^{\mathrm{new}}}\, \tilde P_{ij} V_j} \right)

Read it as three moves: undo the old normalization, put both halves on a common exponent base, then re-normalize with the new denominator. The rest of the page checks the green and yellow terms separately.

Green: rewinding the old output

Handwritten breakdown of the green term, showing diag(l_i) undividing O_i
diag(ℓᵢ) undoes the division, e^(mᵢ − mᵢⁿᵉʷ) re-bases the exponents

OiRBr×dO_i \in \mathbb{R}^{B_r \times d} is the current output for key blocks j=1,2j = 1, 2 — a finished softmax average over the keys seen so far, meaning it has already been divided by i\ell_i. Row kk of it is:

Oi[k]=1i[k][rawo1, , rawod]O_i[k] = \frac{1}{\ell_i[k]}\left[\, \mathrm{raw}\,o_1,\ \ldots,\ \mathrm{raw}\,o_d \,\right]

Multiplying by the diagonal matrix diag(i)RBr×Br\mathrm{diag}(\ell_i) \in \mathbb{R}^{B_r \times B_r} undivides it — each row is scaled by its own normalizer, which puts you back at the raw weighted sum:

diag(i)[k]Oi[k]=[rawo1, , rawod],rawc=past keys peskpmiVpc\mathrm{diag}(\ell_i)[k] \cdot O_i[k] = \left[\, \mathrm{raw}\,o_1,\ \ldots,\ \mathrm{raw}\,o_d \,\right], \qquad \mathrm{raw}_c = \sum_{\text{past keys } p} e^{\,s_{kp} - m_i}\, V_{pc}

That sum is still expressed relative to the old max mim_i. One scalar multiply fixes it, using the same cancellation from page 2:

emiminewpeskpmiVpc  =  peskpminewVpce^{\,m_i - m_i^{\mathrm{new}}} \sum_{p} e^{\,s_{kp} - m_i}\, V_{pc} \;=\; \sum_{p} e^{\,s_{kp} - m_i^{\mathrm{new}}}\, V_{pc}

So the green term is the accumulated numerator over all past keys, rewritten in the new exponent base. No information lost, nothing approximated.

Yellow: the block in hand

Handwritten breakdown of the yellow term, P_ij times V_j
The current block, re-based the same way

P~ijRBr×Bc\tilde P_{ij} \in \mathbb{R}^{B_r \times B_c} holds the exponentiated scores for the current tile, each entry taken against the tile’s local max m~ij\tilde m_{ij}:

P~ij[k]=[esk1m~ij, esk2m~ij, , eskBcm~ij]\tilde P_{ij}[k] = \left[\, e^{\,s_{k1} - \tilde m_{ij}},\ e^{\,s_{k2} - \tilde m_{ij}},\ \ldots,\ e^{\,s_{k B_c} - \tilde m_{ij}} \,\right]

Multiplying by VjRBc×dV_j \in \mathbb{R}^{B_c \times d} contracts over the BcB_c keys in the tile, giving a dd-dimensional row:

(P~ijVj)[k]=[q=1Bceskqm~ijVq1,  q=1Bceskqm~ijVq2,  ]\left(\tilde P_{ij} V_j\right)[k] = \left[\, \sum_{q=1}^{B_c} e^{\,s_{kq} - \tilde m_{ij}} V_{q1},\ \ \sum_{q=1}^{B_c} e^{\,s_{kq} - \tilde m_{ij}} V_{q2},\ \ \ldots \,\right]

Then the same one-scalar re-basing as before:

em~ijminew(q=1Bceskqm~ijVq1)=q=1BceskqminewVq1e^{\,\tilde m_{ij} - m_i^{\mathrm{new}}}\left( \sum_{q=1}^{B_c} e^{\,s_{kq} - \tilde m_{ij}} V_{q1} \right) = \sum_{q=1}^{B_c} e^{\,s_{kq} - m_i^{\mathrm{new}}} V_{q1}

Green + yellow

Handwritten sum of part A and part B, and the final normalization
Two partial sums over disjoint key sets, in the same base — so they just add

Both terms are now sums of esminewVe^{\,s - m_i^{\mathrm{new}}} V over disjoint sets of keys, in the same exponent base. Which means they simply add:

A[k]=past peskpminewVp,B[k]=qeskqminewVq\text{A}[k] = \sum_{\text{past } p} e^{\,s_{kp} - m_i^{\mathrm{new}}} V_{p}, \qquad \text{B}[k] = \sum_{q} e^{\,s_{kq} - m_i^{\mathrm{new}}} V_{q}
A+B=p+qesk,allminewVall    RBr×d\text{A} + \text{B} = \sum_{p\,+\,q} e^{\,s_{k,\text{all}} - m_i^{\mathrm{new}}}\, V_{\text{all}} \;\in\; \mathbb{R}^{B_r \times d}

That is exactly the numerator of a softmax taken over every key seen so far. Divide by the new normalizer — the Br×BrB_r \times B_r diagonal on the left — and you have the updated output block:

[111Br]×(A+B)  =  Oinew    RBr×d\begin{bmatrix} \tfrac{1}{\ell_1} & & \\ & \ddots & \\ & & \tfrac{1}{\ell_{B_r}} \end{bmatrix} \times \left(\text{A} + \text{B}\right) \;=\; O_i^{\mathrm{new}} \;\in\; \mathbb{R}^{B_r \times d}

Which is the punchline of the page: OiO_i is correct after every iteration, not just the last one. It’s always the exact attention output restricted to the keys processed so far, which is why the loop can stop caring about everything it has already thrown away.

Page 4 — Counting the memory traffic

Page 4 of the handwritten Flash Attention notes
Page 4space and HBM-access complexity (the rest of the sheet is blank) (click to open full size)

Start with what actually lives in HBM: Q,K,V,ORN×dQ, K, V, O \in \mathbb{R}^{N \times d} plus the two vectors \ell and mm of length NN:

total HBM memory=4Nd+2N        O(N)\text{total HBM memory} = 4Nd + 2N \;\;\Longrightarrow\;\; O(N)

Treating the head dimension dd as a constant (it’s typically 64 or 128, while NN runs into the thousands), that is linear in sequence length. Standard attention has an N×NN \times N matrix sitting in there, so this is already the headline win.

Then the IO count, in four steps:

  • T=NB=NM/4d=4NdMT = \frac{N}{B} = \frac{N}{M/4d} = \frac{4Nd}{M}number of blocks along one axis, since the block size comes from the SRAM budget.
  • Tr×Tc16N2d2M2T_r \times T_c \approx \frac{16 N^2 d^2}{M^2}total iterations of the doubly-nested loop.
  • B4d=MB \cdot 4d = Mdata moved per iteration — one iteration's worth of Q, K, V, O tiles, which is a full SRAM's worth by construction.
  • 16N2d2M2M=16N2d2M\frac{16 N^2 d^2}{M^2} \cdot M = \frac{16 N^2 d^2}{M}iterations times bytes per iteration.
HBM accesses=O ⁣(N2d2M1)\text{HBM accesses} = O\!\left(N^2 d^2 M^{-1}\right)

Compare that to standard attention’s Θ(Nd+N2)\Theta(Nd + N^2). The ratio is roughly d2/Md^2 / M, and on real hardware MM is on the order of 100 KB while d2d^2 is a few thousand — so the traffic drops by something like an order of magnitude. Note the direction of the MM: bigger SRAM means fewer memory accesses, because bigger blocks mean more reuse per load. This is the rare complexity result where the constant factor of your hardware shows up in the exponent’s neighborhood and you’re glad about it.

What I took away

The thing worth remembering isn’t the algebra, it’s the framing. Flash Attention doesn’t reduce the number of operations at all — it does more arithmetic than standard attention, since every block re-scales work that was already done. It wins anyway, because on a modern GPU the arithmetic is nearly free and the memory movement isn’t.

The softmax decomposition on page 2 is what makes that trade legal. Everything after it is bookkeeping: carry (m,)(m, \ell), re-base when the max moves, divide at the end. Once you’ve seen it, the same running-statistics pattern shows up everywhere — Welford’s algorithm for variance, streaming log-sum-exp, half of what a distributed reduce does.

Attachment

Flash Attention.pdf ↗

The original handwritten notes, all four sheets, unedited. Based on FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness (Dao, Fu, Ermon, Rudra, Ré, 2022).