August 2026 · Technical
Handwritten Notes on Flash Attention
Attention is not slow because of the math. Multiplying and then multiplying by is a perfectly reasonable number of FLOPs for a modern GPU. It’s slow because the score matrix has to be written to HBM, read back for the softmax, written again, and read one more time for the multiply by . 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 , , 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.
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 HBM accesses instead of .
Page 2 — A softmax that never sees the whole row

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.

For an input vector , grab only the first block of size :
The subtraction in step 2 is the standard overflow guard. Every exponent is , so every entry of lands safely in and nothing blows up. It also means is baked into and , 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 and concatenate them into . The new max is free:

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:
The whole trick is one line of exponent arithmetic, drawn in the notes with a big :
The local max cancels itself out. Sum both rescaled halves and you get the denominator, which is step 6:
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 blocks with dim .” Carry 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.

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

The is there because a block of rows costs four -wide tiles at once: you are holding , , and simultaneously. Divide the budget four ways and that’s how many rows fit.
Line 2 sets up what stays in HBM:
- • — the output matrix, initialized to zero.
- • — the running normalizer, initialized to all 0.
- • — the running row-max vector, initialized to −∞ (so the first real max always wins).
Lines 3 and 4 do the tiling. splits into blocks of shape ; and each split into blocks of shape . follows ’s partition, and so do and — which, being -length vectors rather than matrices, are small enough to keep resident in SRAM without anyone noticing.
Who loops over what

The outer loop (line 5) walks over the , blocks; the inner loop (line 7) walks over the blocks. So a single pair is loaded into SRAM once and reused against every query block, while get cycled through underneath it. Every block that gets computed on-chip is written back to HBM as a slice of the output — never as part of an anything.
Line 9 is where that pays off. The score block is , and as the note in the corner says: we don’t need the , since it’s made of these tiny grids and we only ever handle one grid at a time.
The SRAM budget

Adding up what a single iteration needs resident on-chip:
The asymmetry between and is deliberate. and get the full column block size, because a wider means more reuse per load. But and are capped at rows — they fall back to to save resources, since making them taller buys nothing and costs SRAM. When the min collapses and , which tidies the budget to:
Page 3 — Why line 12 isn’t a hack

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

Line 11 updates the two per-row statistics. The new max is the running max against the current block’s max:
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 of the tile carries its own , and that row-local max is simultaneously the running max for that row of the full attention matrix. The bigger grid colors row : green for the key blocks already folded in, yellow for , the one currently on-chip. That color coding runs through the rest of the page.
The line itself

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

is the current output for key blocks — a finished softmax average over the keys seen so far, meaning it has already been divided by . Row of it is:
Multiplying by the diagonal matrix undivides it — each row is scaled by its own normalizer, which puts you back at the raw weighted sum:
That sum is still expressed relative to the old max . One scalar multiply fixes it, using the same cancellation from page 2:
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

holds the exponentiated scores for the current tile, each entry taken against the tile’s local max :
Multiplying by contracts over the keys in the tile, giving a -dimensional row:
Then the same one-scalar re-basing as before:
Green + yellow

Both terms are now sums of over disjoint sets of keys, in the same exponent base. Which means they simply add:
That is exactly the numerator of a softmax taken over every key seen so far. Divide by the new normalizer — the diagonal on the left — and you have the updated output block:
Which is the punchline of the page: 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

Start with what actually lives in HBM: plus the two vectors and of length :
Treating the head dimension as a constant (it’s typically 64 or 128, while runs into the thousands), that is linear in sequence length. Standard attention has an matrix sitting in there, so this is already the headline win.
Then the IO count, in four steps:
- • — number of blocks along one axis, since the block size comes from the SRAM budget.
- • — total iterations of the doubly-nested loop.
- • — data moved per iteration — one iteration's worth of Q, K, V, O tiles, which is a full SRAM's worth by construction.
- • — iterations times bytes per iteration.
Compare that to standard attention’s . The ratio is roughly , and on real hardware is on the order of 100 KB while is a few thousand — so the traffic drops by something like an order of magnitude. Note the direction of the : 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 , 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).