FlashAttention derived
We now have both halves of an idea. §12.1 showed that standard attention is slow because it writes the matrices and to HBM and reads them back, and that a kernel which read , , once and wrote once would be compute-bound. §12.2 showed that the softmax, the step that seemed to need a whole row at once, can be computed a block at a time with exact corrections.
Put them together and you get FlashAttention (Dao et al. 2022). This section derives it in full. We build the forward pass tile by tile, with the causal mask and the block skipping it allows. We work out what the forward pass must leave behind for the backward pass (one number per query) and derive the backward pass that recomputes everything else. Then we prove the IO complexity theorem, HBM accesses for SRAM of size , and its matching lower bound. Along the way we write the whole algorithm in NumPy with a counter on every byte that crosses between HBM and SRAM, and check it against naive attention to machine precision.
The picture, a grid of tiles
Fix one head. The queries are cut into row blocks of rows each, and the keys and values into column blocks and of rows each, so and . (Here a subscript on a bold capital is a block of rows, not a single row.) The score matrix becomes a grid of tiles,
FlashAttention visits the tiles one at a time. For each tile it loads the blocks it needs into SRAM, computes there, uses it to update a running result, and throws it away. The tile never travels to HBM. Choose the block sizes so that a query block, a key block, a value block, the tile, and the running output all fit in one SM's SRAM, which on an A100 means blocks of 64 or 128 rows for head widths of 64 or 128.
Two orders of the loops are possible, and both are used. The original FlashAttention put the key and value blocks in the outer loop. FlashAttention-2 swapped them, putting the query blocks outside, which is simpler and parallelizes better (§12.4). We use the FlashAttention-2 order for the forward pass and come back to the original when we prove the theorem.
The forward pass
With the query block held in SRAM, the inner loop streams key and value blocks past it and runs the block form of the online softmax from §12.2. Keep, for the rows of the block, a running maximum , a running sum , and an unnormalized output , initialized to , , and .
- Load from HBM into SRAM.
- For each key block, , do the following.
- Load and into SRAM.
- Compute the tile (a matrix product on tensor cores).
- Update the maximum, .
- Exponentiate, , with each row's maximum subtracted from that row.
- Rescale and accumulate, and , then set .
- Write and to HBM.
In plain words, each query block makes one sweep over all the keys and values, keeping a few numbers per row that are corrected each time a larger score shows up, and writes its finished output exactly once.
Why is the result exactly attention? Step 2 is the merge of §12.2 applied, for each row, to the summary of keys and the summary of key block . By the merge claim proved there, after the last block each row holds the summary of all its keys, and with the row maximum. Dividing gives , which is row of . Nothing is approximated. FlashAttention is exact attention, computed in a different order, and its output differs from the naive one only by floating-point rounding.
Here it is in NumPy, with the HBM counter from §12.1 on every load and store. The outer loop is over query blocks, the inner loop over key blocks, and the comments match the steps above.
The outputs match naive attention to about , and so do the log-sum-exps. The traffic tells the story. Each of the query blocks reads all of and once, so the count is
1.18 million values here against 4.46 million for standard attention. And nothing of size was ever stored.
The causal mask, and the tiles you never visit
A causal mask (§4.4) lets query see keys . In the tile grid that splits the tiles into three kinds.
- Below the diagonal, every key in the tile is at or before every query, so the tile is computed with no mask at all.
- On the diagonal, the tile crosses , so it is computed and then masked with above the diagonal.
- Above the diagonal, every key is after every query, so every weight would be zero. The tile is skipped entirely, with no load, no product, and no exponential.
That last line is the block skipping that makes causal FlashAttention cost about half of non-causal. In the run above, 72 of the 128 tiles were visited. With square blocks per side the visited fraction is out of , approaching one half. A naive implementation, by contrast, computes the full matrix and throws half of it away (§4.7).
There is one subtlety. The skip test compares the first key of the block with the last query of the block, j0 > rows[-1], because a tile can be skipped only if every one of its entries is masked. Comparing first with first, an off-by-one that is easy to make when , silently drops keys that some rows should see.
Tiled FlashAttention forward with causal block skipping
Implement flash_attention_forward(Q, K, V, Br, Bc, causal=False) returning (O, lse, tiles) for single-head scaled dot-product attention, where Q is (T_q, d_k), K is (T_k, d_k), V is (T_k, d_v), O is (T_q, d_v), lse is the length- vector of over the allowed keys, and tiles is the number of tiles whose scores you computed. Use query blocks of Br rows in the outer loop and key blocks of Bc rows in the inner loop, with the online softmax, and never form a matrix larger than one tile. With causal=True (only used when ), query may attend to keys , and you must skip every tile that lies entirely above the diagonal. Blocks need not divide the lengths.
What the forward pass leaves behind
Standard attention keeps for the backward pass, numbers per head. FlashAttention keeps the output , which the layer returns anyway, and one extra number per query row, the log-sum-exp
That is enough, because every attention weight can be rebuilt from it and its score,
In plain words, the log-sum-exp is the softmax's normalizer in log space. Subtract it from a score and exponentiate, and you have the weight, with no maximum and no sum needed. And since , the exponent is never positive, so the reconstruction cannot overflow.
The original FlashAttention stored and separately. FlashAttention-2 stores only their combination (Dao 2023), half the statistics and one fewer operation per weight in the backward pass. Either way the extra memory is per head instead of , which is Theorem 1 of Dao et al. (2022), FLOPs and memory beyond the inputs and output.
For one layer of Llama 3 8B at 8,192 tokens, the difference is between storing , numbers or 4 GiB in bf16, and storing the log-sum-exps, numbers or 1 MiB in float32. That factor of is why FlashAttention made long-context training possible on its own, even setting its speed aside.
The backward pass, derived
Recall the backward pass of attention from §4.3. With , , , , and upstream gradient ,
with , the dot product of row of with row of . Each of these is written in terms of matrices. The job is to rewrite every one in terms of tiles, so that the backward pass, like the forward pass, never holds more than one tile at a time. Three observations do it.
Observation 1, the weights can be recomputed tile by tile. By the reconstruction above, the tile of weights is
with block of the stored log-sum-exps subtracted from each row. It needs only , , and , and it equals the forward pass's weights exactly (up to rounding), not approximately.
Observation 2, the row terms need no row of . The softmax backward needs , a sum over a whole row. §4.3 showed that this equals , because . So is computed before the main loop, in one pass over and , and each tile then needs only its block .
Observation 3, every product splits into a sum over tiles. With the tile of score gradients
block matrix multiplication gives
To check the tile formula for , look at one entry. involves only entry of and and the row term , and , so restricting to block and to block gives exactly the tile formula. The three sums are the definition of a matrix product, written block by block. In plain words, the backward pass is the forward pass's tile loop run again, recomputing each tile of weights from the stored log-sum-exps, and adding each tile's contribution into the three gradients.
Which loop goes outside? and are sums over query blocks, and a sum over key blocks. FlashAttention-2 puts the key blocks outside, so and accumulate in SRAM and are written once, and lets the contributions to go to HBM as additions. On a GPU, different thread blocks handle different key blocks at the same time, so their additions to the same use atomic adds (Dao 2023). In our sequential NumPy version that is a read, an add, and a write. Atomic additions land in whatever order the thread blocks finish, and floating-point addition is not associative, so can differ in its last bits from one run to the next. The flash-attn package offers a deterministic backward pass (deterministic=True) that fixes the order at some cost in speed, which is worth turning on when you need bitwise reproducible training or are hunting a numerical bug.
The gradients agree with the naive formulas to about , with and without the causal mask, and the backward pass's traffic is a few dozen times , with no term. Causal block skipping works in the backward pass exactly as in the forward pass.
The price of recomputation
The backward pass now does five tile products instead of four. Besides , , , and , it recomputes . Counting FLOPs per product, a training step costs forward plus backward instead of , about 17% more arithmetic. Dao et al. measured 13% more FLOPs (75.2 against 66.6 GFLOPs) for GPT-2 medium, and a 5.7 times shorter runtime, because the arithmetic was never what took the time.
This is selective recomputation, the same trade as activation checkpointing (§10.1), applied to the one activation that is quadratic in length. Here it is a pure win, because recomputing a tile on chip is cheaper than reading it from HBM.
FlashAttention backward from the stored log-sum-exp
Implement flash_attention_backward(Q, K, V, O, dO, lse, Br, Bc, causal=False) returning (dQ, dK, dV), the gradients of single-head scaled dot-product attention, given the inputs, the forward output O, the upstream gradient dO, and the forward pass's per-row log-sum-exp lse. You are not given . Recompute each tile of weights from lse, compute once, loop over key blocks outside and query blocks inside, skip fully masked causal tiles, and never form a matrix larger than one tile.
The IO complexity theorem
Now we can say precisely how much FlashAttention saves. Use the external memory model of §12.1, with SRAM holding numbers, and write for the head width and , to match the paper's statement.
Theorem (Dao et al. 2022, Theorem 2). For , standard attention requires HBM accesses, and FlashAttention requires .
The proof is a count. We give it for the loop order of the paper's Algorithm 1, which puts the key and value blocks outside, and then note why the other order gives the same answer.
Block sizes. Algorithm 1 sets and . Then everything that must be on chip at once fits in a constant multiple of . The key and value blocks take , the query block and its output take , and the tile takes . (The cap exists only to keep the tile small.)
Counting. The outer loop runs over the key blocks. Each key and value block is loaded once, in total. Inside, for each key block, the algorithm sweeps every query block, loading , the partial output , and its statistics and , and writing back , , . One sweep therefore moves values, and there are sweeps, so
Substituting, the second term is about . The first term, , is at most of the same order, because means . So the total is .
In plain words, the number of HBM accesses is the number of passes over the inner operand times the size of that operand. The number of passes is the length divided by the outer block size, and the outer block can hold about rows. So the traffic is passes of values each, .
The FlashAttention-2 order swaps the roles. The query blocks are outside, with about rows, and each makes one pass over and , so the traffic is , again , with the bonus that is written once rather than read and written in every sweep. That is the formula our NumPy code measured. The backward pass obeys the same bound (Theorem 5 of the paper), by the same count over its tile loop.
How big is the saving? The ratio of standard to FlashAttention traffic is about , up to the constants. The plot shows it for and . With an SM's worth of SRAM, around numbers, FlashAttention moves an order of magnitude less. But notice the left of the plot. When is near , the blocks are so thin that FlashAttention rereads and more times than standard attention reads its matrices, and it loses. FlashAttention needs SRAM much larger than , so wide heads cost it more, and each GPU generation's larger shared memory helps.
Count the HBM accesses of both loop orders
Implement two functions that return exact HBM access counts (numbers moved, as integers) for self-attention with tokens, head width (), and SRAM of numbers, ignoring the initialization of outputs.
fa1_accesses(T, d, M), the paper's Algorithm 1, with and key and value blocks outside. Each key and value block is read once. For each key block, every query block's , , , is read and , , written.fa2_accesses(T, d, M), the FlashAttention-2 order, with and query blocks outside. is read once, and once per query block, and and written once.
No exact algorithm does better
Is the best possible? Proposition 3 of the paper says there is no algorithm that computes exact attention with HBM accesses for every in . The proof takes the largest allowed SRAM, . There the bound is , and any algorithm must at least read its inputs, numbers, so nothing can be asymptotically smaller at that end of the range. This is a weak form of optimality, since it pins down only one value of . A lower bound that holds for each separately is left by the paper as future work. In practice the question is moot, because at realistic sizes FlashAttention is already compute-bound.
The theorem also explains why tiling attention looks like tiling a matrix multiply. Both sweep one operand once per block of the other, so both move about or values, a total that falls as fast memory grows (§11.5).
What it bought, measured
The FlashAttention paper reports these results on A100 GPUs, all for exact attention.
| measurement | result |
|---|---|
| GPT-2 medium attention, forward and backward | HBM traffic 40.3 GB to 4.4 GB, runtime 41.7 ms to 7.3 ms |
| attention kernel alone, GPT-2 | up to 7.6 times faster than the PyTorch implementation |
| BERT-large training (sequence 512) | 15% faster end to end than the MLPerf 1.1 training speed record |
| GPT-2 training (sequence 1K) | up to 3 times faster than Hugging Face, and 1.7 to 1.8 times faster than Megatron-LM |
| Long Range Arena (sequences 1K to 4K) | 2.4 times faster |
| memory | up to 20 times less than exact attention baselines, linear in sequence length |
| Path-X (sequence 16K) and Path-256 (64K) | 61.4% and 63.1% accuracy, the first Transformers to do better than chance |
The last row is the one people forget. Memory linear in meant models could be trained on much longer sequences, and longer context improved quality. GPT-2 small trained with 4K context reached a perplexity of 17.5 on OpenWebText against 18.2 at 1K, and still trained 30% faster than Megatron-LM's 1K model.
Dropout, block-sparsity, and other real-kernel details
Real kernels handle a few more things, each of which fits the tile structure.
- Dropout on the weights. Storing a dropout mask would defeat the purpose. The forward pass instead saves the state of the pseudo-random number generator, and the backward pass regenerates exactly the same mask tile by tile (Appendix B of the paper).
- Block-sparse attention. If a mask is known at block granularity, tiles marked empty are skipped, exactly as causal tiles are. Proposition 4 of the paper gives HBM accesses, with the fraction of nonzero blocks. This is the hardware view of the sparse patterns in §12.6.
- Other score modifications. Biases such as ALiBi (§5.5), logit soft-capping, sliding windows, and document masks are all functions of and the score, computed inside the tile. PyTorch's FlexAttention lets you write such a function in Python and compiles it into a FlashAttention-style kernel (§12.4).
Block-sparse FlashAttention
Implement block_sparse_attention(Q, K, V, B, block_mask) for self-attention with divisible by the block size B, where block_mask is a boolean array of shape (T // B, T // B) and entry says whether query block may attend to key block (all keys in the block, no finer masking). Return (O, lse, tiles). Visit only the allowed tiles. A query block whose row of block_mask is all False attends to nothing; give its rows O = 0 and lse = -inf without any NaN.
The kernel, in Triton
On a GPU the NumPy loops become a kernel. Each program (a thread block) owns one query block of one head, which is exactly our outer loop, and the inner loop runs inside it. Triton, the subject of §11.6, lets us write it at the level of tiles, with tl.dot for the tile products on tensor cores. The forward kernel below is complete for contiguous inputs of shape (batch * heads, T, D). It cannot run in your browser, and production kernels add the details that make it fast (software pipelining, exp2 in place of exp, autotuned block sizes, and a separate path for the diagonal tiles), but every line maps to a step of the algorithm.
Notice the launch grid, one program per query block and per head. Parallelizing over query blocks, not only over batch and heads, is one of FlashAttention-2's central changes, and the reason it keeps the GPU busy when the batch is small (§12.4).
In PyTorch
To see the whole algorithm as a differentiable operation, wrap the tiled forward and backward passes in a torch.autograd.Function. The forward saves only , , , , and the log-sum-exps, and the backward recomputes the tiles. gradcheck compares it with numerical derivatives, and the comparison with scaled_dot_product_attention checks both passes against PyTorch's own.
Read more
- Dao et al. (2022). FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. The algorithm (Section 3 and Appendix B), the IO theorem and lower bound (Section 3.2, Appendix C), and the measurements.
- Dao (2023). FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning. Algorithms 1 and 2 are the clearest statement of the forward and backward passes with stored log-sum-exp.
- Tillet, Kung & Cox (2019). Triton: An Intermediate Language and Compiler for Tiled Neural Network Computations. The tile-level language in which most FlashAttention variants are first prototyped.
- Aggarwal & Vitter (1988). The Input/Output Complexity of Sorting and Related Problems. The external memory model behind IO complexity, with matching lower bounds.
- Triton tutorials (2024). Fused Attention. A production-quality FlashAttention-2 forward and backward pass in Triton, worth reading line by line after this section.