Chapter 12 · Section 12.4 · 55 min

FlashAttention-2, FlashAttention-3, and practice

The first FlashAttention was 2 to 4 times faster than standard attention, and still slow. Its forward pass reached only 30 to 50% of an A100's peak FLOP/s, and its backward pass 25 to 35%, while a well-tuned matrix multiplication reaches 80 to 90% (Dao 2023). The algorithm of §12.3 had fixed the memory traffic. What remained was a question of scheduling, of which processor does which tile, in what order, and what it waits for.

This section follows the two sequels. FlashAttention-2 (Dao 2023) rearranged the work to spend less time on operations that are not matrix multiplications and to keep every SM busy, which doubled the speed. FlashAttention-3 (Shah et al. 2024) rebuilt the kernel around the H100's asynchronous hardware and its FP8 tensor cores. Then we turn to practice, the kernels you will actually call from PyTorch, how to batch sequences of different lengths without padding, why decoding a single token needs yet another variant, and a preview of the paged caches that serving engines feed it.

Most of this is GPU engineering, and the details live in CUDA. But each change follows from a cost that you can compute on paper, and that is what an interviewer will ask you to do. Throughout, a thread block processes a query block of BrB_r rows against key and value blocks of BcB_c rows, as in §12.3, and dd is the head width, the dkd_k of earlier chapters, written dd as in the FlashAttention papers.

Where the time went, three leaks

Profile the first FlashAttention on an A100 and three problems show up.

Non-matrix-multiply work is expensive. An A100 does 312 TFLOP/s of fp16 or bf16 matrix multiplication on its tensor cores, but only 19.5 TFLOP/s of other fp32 arithmetic. Each non-matmul FLOP costs 16 times as much as a matmul FLOP. The softmax's maximum, subtraction, exponential, sum, and the rescaling of the output are all non-matmul work, so even a small count of them takes a noticeable share of the time.

Not enough parallel work. The first FlashAttention gave each (sequence, head) pair to one thread block, which then looped over all the tiles. An A100 has 108 SMs. With a batch of 2 sequences and 16 heads there are only 32 thread blocks, and 70% of the GPU sits idle. Long sequences make it worse, because they force small batches to fit in memory.

Warps that must talk to each other. Inside a thread block, four or eight warps (groups of 32 threads, §11.1) share the tile. The first FlashAttention split the keys among the warps, so each warp computed part of every query's sum and the partial results had to be written to shared memory, synchronized, and added. Dao calls this the "split-K" scheme.

Each leak has a fix, and together they are FlashAttention-2.

FlashAttention-2, one fix per leak

Fewer non-matmul FLOPs

Two changes from §12.2 and §12.3 do it. First, keep the output unnormalized during the loop, rescaling O~\tilde{\mathbf{O}} only by em−mnewe^{\mathbf{m} - \mathbf{m}^{\text{new}}} and dividing by ℓ\boldsymbol{\ell} once at the end, instead of dividing the whole Br×dB_r\times d block at every step. Second, store only the log-sum-exp lse⁡=m+log⁡ℓ\operatorname{lse} = \mathbf{m} + \log\boldsymbol{\ell} for the backward pass, so each recomputed weight is one subtraction and one exponential, es−lse⁡e^{s - \operatorname{lse}}. Neither changes the result. Together with skipping the masked causal tiles, they remove most of the non-matmul work that is not strictly needed.

How much does what remains cost? Per tile, the two products do 4BrBcd4B_rB_cd FLOPs, and the softmax does a handful of operations per score, say 5, so 5BrBc5B_rB_c. Weighting the non-matmul operations 16 times more,

non-matmul timematmul time≈16⋅5BrBc4BrBcd=20d,\frac{\text{non-matmul time}}{\text{matmul time}} \approx \frac{16\cdot 5B_rB_c}{4B_rB_cd} = \frac{20}{d},

which is 31% for d=64d = 64 and 16% for d=128d = 128. In plain words, the exponentials and their bookkeeping are a sizable fraction of the work even though they are a tiny fraction of the FLOPs, and the fraction shrinks as the head width grows, because the products grow with dd and the softmax does not. Every non-matmul operation removed is worth sixteen matmul operations.

The cell puts numbers on it for both GPUs, using each one's matmul rate and its rate for other arithmetic. (On the H100 the exponential alone is far slower than general fp32 arithmetic, a problem we return to with FlashAttention-3.)

Python · cell 1

Parallelism over the sequence

FlashAttention-2 uses the loop order of §12.3, query blocks outside, and makes each query block its own thread block. The launch grid becomes (number of query blocks) × (batch × heads), so a batch of 2 sequences of 8,192 tokens with 16 heads and Br=128B_r = 128 launches 64×32=2,04864\times32 = 2{,}048 thread blocks, enough to fill 108 SMs nineteen times over. The query blocks are independent (each has its own running statistics), so no communication is needed between them. Dao credits this loop order and the sequence-length parallelism to Phil Tillet's Triton implementation.

The cell compares the two launch grids for a few shapes on an A100, assuming one thread block per SM at a time and blocks of equal cost.

Python · cell 2

For a large batch of short sequences the two are equivalent, and FlashAttention-1 was designed for that regime. For long sequences and small batches, which is where attention costs the most, FlashAttention-2's grid fills the GPU and FlashAttention-1's does not.

The backward pass parallelizes the same way over key blocks, since dKj\mathrm{d}\mathbf{K}_j and dVj\mathrm{d}\mathbf{V}_j belong to one key block. The price is that many thread blocks add into the same dQi\mathrm{d}\mathbf{Q}_i, which is done with atomic additions in HBM, as in §12.3.

How many of the GPU's SMs does each FlashAttention version keep busy?
Each square is one streaming multiprocessor, shaded by how many thread blocks it runs. Blocks are handed out in waves of one per SM. Idle squares are wasted hardware.
Loading interactive figure…

Split the queries, not the keys

Within a thread block, FlashAttention-2 gives each warp a slice of the queries and lets every warp read all of the current key and value block. Now each warp owns complete rows. Its running maximum, sum, and output never need anyone else's, so the warps never exchange partial results through shared memory. In the language of §12.2, split-K forces a merge ⊕\oplus of partial softmax summaries across warps at every tile, and split-Q needs none, because a row's summary lives entirely in one warp.

What it bought

Dao (2023) reports about a 2 times speedup over FlashAttention, 1.7 to 3.0 times depending on the setting, reaching 50 to 73% of the A100's theoretical peak, with up to 73% in the forward pass and 63% in the backward pass. Training GPT-style models end to end, it reached up to 225 TFLOP/s per A100, 72% model FLOPs utilization. Block sizes are typically {64,128}×{64,128}\{64, 128\}\times\{64, 128\}, with 4 or 8 warps per thread block, tuned per head width, because larger blocks reduce shared-memory traffic until they run out of registers.

Warm-up

How many SMs does each version keep busy?

Model a GPU as n_sm SMs that each run one thread block at a time, in waves. Implement grid_utilization(batch, heads, T, Br, n_sm, version) returning (blocks, waves, utilization) for a forward pass, where version is "fa1" (one thread block per sequence and head) or "fa2" (one per query block, sequence, and head, with ⌈T/Br⌉\lceil T/B_r\rceil query blocks). waves is the number of rounds needed, and utilization is blocks / (waves * n_sm), the fraction of SM slots that do work, assuming every thread block takes the same time.

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

Decoding needs a different split

Generation breaks FlashAttention-2's parallelism. During decoding each sequence has a single new query (§6.5), so there is one query block per sequence and head, and the grid is batch × heads again. Dao, Haziza, Massa, and Sizov (2023) note that with a batch of one, FlashAttention "will use less than 1% of the GPU". Meanwhile each of those few thread blocks streams the entire KV cache, possibly hundreds of thousands of keys, by itself.

Flash-Decoding splits the keys instead. The cache is cut into chunks, each chunk is processed by its own thread block, which produces a partial output and its log-sum-exp, and a final small kernel merges the partials. The merge is exactly the log-sum-exp form from §12.2,

lse⁡=log⁡∑celse⁡c,o=∑celse⁡c−lse⁡ oc.\operatorname{lse} = \log\sum_{c} e^{\operatorname{lse}_c}, \qquad \mathbf{o} = \sum_{c} e^{\operatorname{lse}_c - \operatorname{lse}}\,\mathbf{o}_c .

In plain words, each chunk reports what it found and how much softmax mass it holds, and the final output is the mass-weighted average of the chunks' outputs. This is split-K again, the scheme FlashAttention-2 removed from inside a thread block, now used across thread blocks because at decode time there is nothing else to parallelize over.

Here is the merge at work on one decoding step, with the keys split into eight chunks that could each run on a different SM.

Python · cell 3

On CodeLlama-34B, Dao and colleagues measured up to 8 times faster generation for very long sequences, with the attention operation itself up to 50 times faster than FlashAttention. The same reduction is what the serving engines that decode many sequences at once build on, with the cache stored in fixed-size pages, as the next subsection describes.

Interview

Split-KV decoding

Implement Flash-Decoding for one query. partial_attention(q, K, V) returns (o, lse) for one chunk of keys, with (zeros, -inf) for an empty chunk. combine(outs, lses) merges a list of partial results into the final (o, lse), stably and without NaN when some chunks are empty. split_kv_decode(q, K, V, n_splits) splits the keys into n_splits contiguous chunks as evenly as possible (np.array_split), computes the partials independently, and combines them. Scores are scaled by 1/dk1/\sqrt{d_k}.

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

Paged KV caches, a preview of serving

How a serving system schedules hundreds of requests and manages their memory belongs to continuous batching and paged memory management, a later subject in its own right. One idea from it changes the attention kernel, though, so we preview it here. A serving engine decodes hundreds of sequences at once, and it does not know in advance how long each one will grow. Reserving a contiguous buffer of the maximum length for every sequence wastes most of the memory. Kwon et al. (2023) measured that earlier serving systems used only about 20 to 40% of their KV cache memory for actual tokens, with the rest lost to over-reservation and fragmentation.

PagedAttention, the kernel behind vLLM, borrows the operating system's answer. The cache is a pool of fixed-size pages (blocks of 16 tokens' keys and values by default) that can sit anywhere in HBM, and each sequence has a block table that maps its logical blocks (tokens 0 to 15, 16 to 31, and so on) to physical pages. A new page is taken from the free list only when the sequence's last page fills, so at most one partly filled page per sequence is wasted, under 4% of the cache in their measurements. Sequences that share a prompt can share its pages and copy one only when they would write to it. With the memory freed for larger batches, vLLM reported 2 to 4 times the throughput of earlier systems at the same latency.

The attention kernel barely changes. Instead of reading keys at consecutive addresses, it looks up each block's page in the table and reads the tile there. That is a gather at page granularity, so each page is still one contiguous, coalesced read, and everything else (the online softmax, and the split over keys of Flash-Decoding) is untouched. The flash-attn package's flash_attn_with_kvcache accepts a block table for exactly this purpose. Admission, preemption, eviction, prefix caching, and how pages are scheduled across a batch are the serving side of the story, and they are left to the treatment of serving.

Interview

Decoding through a block table

Implement a paged KV cache for one sequence and one head. The pools k_pages and v_pages have shape (n_pages, page_size, d_k), and block_table is a Python list of page ids, so token ii of the sequence lives in page block_table[i // page_size] at slot i % page_size. Only the first seq_len tokens are valid, and the unused slots of the last page hold garbage.

  • append_token(k_pages, v_pages, block_table, seq_len, k, v, free_pages) writes the key and value of token seq_len in place, first taking a new page with free_pages.pop(0) and appending it to block_table when the sequence's pages are full (including when it has none). Return the new length, seq_len + 1.
  • gather(pages, block_table, seq_len) returns the (seq_len, d_k) matrix of the valid rows, in token order.
  • paged_decode(q, k_pages, v_pages, block_table, seq_len) returns (o, lse) for one query q against the sequence's keys, with scores scaled by 1/dk1/\sqrt{d_k}. Read the pool only through the block table, one page at a time, and merge the pages with the online softmax.
Hidden tests, hints, and AI help, from $1.99 a month.

FlashAttention-3, built for asynchrony

FlashAttention-2 reaches only about 35% of an H100's peak (Shah et al. 2024). The H100 changed the rules in three ways that a kernel written for the A100 does not exploit.

  • Asynchronous tensor cores. The WGMMA instruction runs a matrix multiply for a warpgroup (four warps) and returns immediately. The warps can do other work while the tensor cores compute.
  • An asynchronous copy engine. The Tensor Memory Accelerator (TMA) moves tiles between HBM and shared memory on its own, without threads computing addresses.
  • FP8 tensor cores at twice the bf16 rate, about 1,979 dense TFLOP/s.

And it made one old problem much worse. Shah et al. compute it. The H100 SXM5 has 989 TFLOP/s of fp16 matrix multiply but only 3.9 TFLOP/s of special functions such as the exponential. In the forward pass with d=128d = 128, a tile does 4BrBcd=512 BrBc4B_rB_cd = 512\,B_rB_c matmul FLOPs and BrBcB_rB_c exponentials, so there are 512 times more matmul FLOPs, but the exponential is 256 times slower. If they ran one after the other, the exponentials would take half as long as the products. In FP8 the products get twice as fast and the exponentials do not.

The answer is to run them at the same time, and FlashAttention-3 has three techniques for it.

  1. Warp specialization. Some warps become producers that only issue TMA loads of Kj\mathbf{K}_j and Vj\mathbf{V}_j into a circular buffer of shared memory. The rest are consumers that only compute. Producers stay a few tiles ahead, so consumers rarely wait for memory. Registers are reallocated toward the consumers, since a producer needs almost none.
  2. Ping-pong between warpgroups. With two consumer warpgroups, the kernel schedules them so that while warpgroup 1 does the softmax of its tile on the special-function units, warpgroup 2 runs its matrix products on the tensor cores, and then they swap.
  3. Pipelining inside a warpgroup. Within one warpgroup, the softmax of tile jj overlaps with the product QiKj+1⊤\mathbf{Q}_i\mathbf{K}_{j+1}^\top for the next tile, which is issued asynchronously before the softmax starts. This takes extra registers to hold two tiles of scores at once.

In the paper's ablation (one fixed problem size with d=128d = 128), the full kernel ran at 661 TFLOP/s, 582 without the softmax-matmul pipelining, and 570 without warp specialization.

FP8, and the outlier problem

FP8 (the e4m3 format has 4 exponent bits and 3 mantissa bits, with a largest value of 448) doubles throughput but has very little precision and range, and attention's inputs have outliers, a few coordinates of Q\mathbf{Q} and K\mathbf{K} much larger than the rest. FlashAttention-3 uses two tricks.

Block quantization. Instead of one scale per tensor, keep one scale per block of Q\mathbf{Q}, K\mathbf{K}, and V\mathbf{V}, the blocks the kernel loads anyway, so an outlier only inflates the scale of its own block.

Incoherent processing. Multiply Q\mathbf{Q} and K\mathbf{K} by the same random orthogonal matrix R\mathbf{R} before quantizing. Since RR⊤=I\mathbf{R}\mathbf{R}^\top = \mathbf{I},

(QR)(KR)⊤=QRR⊤K⊤=QK⊤,(\mathbf{Q}\mathbf{R})(\mathbf{K}\mathbf{R})^\top = \mathbf{Q}\mathbf{R}\mathbf{R}^\top\mathbf{K}^\top = \mathbf{Q}\mathbf{K}^\top ,

so the scores are unchanged in exact arithmetic, while each outlier's magnitude is spread over all dd coordinates. FlashAttention-3 uses R\mathbf{R} = a Hadamard matrix times a random diagonal of ±1\pm1, which costs O(dlog⁡d)O(d\log d) per vector with the fast Walsh-Hadamard transform and can be fused with the rotary embedding. The idea comes from quantizing weights in QuIP (Chee et al. 2023).

On a test with deliberately injected outliers (each entry N(0,1)\mathcal{N}(0,1), plus an independent N(0,100)\mathcal{N}(0, 100) term for 0.1% of entries), FP8 FlashAttention-3 had a root mean squared error of 9.1×10−39.1\times10^{-3}, against 2.4×10−22.4\times10^{-2} for a baseline FP8 attention with per-tensor scaling, 2.6 times lower. In that ablation incoherent processing did most of the work. Without it the error was back to 2.4×10−22.4\times10^{-2}, while without block quantization it was 9.3×10−39.3\times10^{-3}. There is one more practical wrinkle. FP8 matrix multiplies on Hopper accept operands only in one memory layout, so the kernel transposes tiles of V\mathbf{V} in shared memory and reshuffles the registers that hold P\mathbf{P} before the second product.

What it bought

FlashAttention-3 is 1.5 to 2.0 times faster than FlashAttention-2 on H100 in fp16, reaching up to 740 TFLOP/s (75% of peak), and close to 1.2 PFLOP/s in FP8. The backward pass is 1.5 to 1.75 times faster. These are kernel-level numbers on one GPU. The lesson that generalizes is that on modern hardware a kernel is fast when its memory traffic, its tensor-core work, and its other arithmetic all overlap, and the designer's job is to arrange the dependencies so they can.

And on Blackwell

The pattern continues with each GPU generation. FlashAttention-4 (2025) targets NVIDIA's Blackwell GPUs, and public accounts of it come mainly from its open-source code and its authors' talks, so treat the details as provisional. Two of its reported ideas follow directly from this section and §12.2. It computes part of the exponentials in software on the ordinary floating-point units, writing 2x2^x as a power of two with an integer exponent times 2f2^f for the fractional part ff, approximated by a cubic polynomial, to take load off the special-function units. And it rescales the accumulator only when the running maximum has grown by enough to threaten overflow, which reportedly cuts the number of rescalings about tenfold. That shortcut is exact. As §12.2 pointed out, any reference point works as long as ℓ\boldsymbol{\ell} and O~\tilde{\mathbf{O}} share it, so a stale maximum only makes some exponentials somewhat larger than 1, which fp32 accumulators absorb, and the final division by ℓ\boldsymbol{\ell} cancels the choice.

Core

When do the exponentials catch up with the matrix products?

Per tile, the forward pass does 4BrBcd4B_rB_cd matmul FLOPs and BrBcB_rB_c exponentials. Implement exp_to_matmul_time(d, matmul_flops, exp_flops) returning the ratio of the time the exponentials take to the time the products take (each at its own peak rate), and overlap_speedup(d, matmul_flops, exp_flops) returning the speedup of a kernel that overlaps the two perfectly, max⁡(tmm,texp⁡)\max(t_{\text{mm}}, t_{\exp}), over one that runs them back to back, tmm+texp⁡t_{\text{mm}} + t_{\exp}. Rates are in operations per second. Then use them, in a comment, to explain why the problem is worse in FP8.

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

Using it, the real way

You will rarely write an attention kernel. You will call one, and the details of how you call it decide whether you get FlashAttention or something much slower.

In PyTorch the entry point is torch.nn.functional.scaled_dot_product_attention(query, key, value, attn_mask=None, dropout_p=0.0, is_causal=False, scale=None, enable_gqa=False), with tensors shaped (batch, heads, T, d_k). It chooses among several backends, which you can list and restrict with the torch.nn.attention.sdpa_kernel context manager and the SDPBackend enum.

backendwhat it iswhen it is chosen
FLASH_ATTENTIONFlashAttention-2 kernels (and a fused CPU kernel of the same design)fp16 or bf16 on a supported GPU, no arbitrary attn_mask
EFFICIENT_ATTENTIONthe memory-efficient kernel from xFormers, also tiled with an online softmaxsupports an additive attn_mask, more dtypes
CUDNN_ATTENTIONNVIDIA's cuDNN fused attentionrecent GPUs and cuDNN versions
MATHthe three-kernel reference implementation of §12.1anything else, and for debugging

Three details trip people up.

  • Passing a mask can cost you the kernel. FlashAttention kernels support causal masks, sliding windows, and variable lengths natively, but not an arbitrary dense mask. Passing attn_mask for something that is_causal=True could express may silently move you to a slower backend. Check with sdpa_kernel([SDPBackend.FLASH_ATTENTION]), which raises an error instead of falling back.
  • is_causal is aligned to the top left. With 4 new queries and 128 keys, is_causal=True lets the first query see only key 0, which is wrong for chunked prefill or decoding with a cache. Use torch.nn.attention.bias.causal_lower_right there (§4.4 derives the bottom-right rule). The flash-attn package's causal=True is aligned bottom-right, the opposite convention.
  • Grouped-query attention. enable_gqa=True (PyTorch 2.5 and later) accepts fewer key-value heads than query heads and shares them in groups, without copying K\mathbf{K} and V\mathbf{V} (§12.5).

For masks and score modifications that SDPA cannot express, PyTorch 2.5 added FlexAttention. You write the mask as a Python predicate on (b, h, q_idx, kv_idx), or a score modification as a function of the score and those indices, and torch.compile generates a FlashAttention-style kernel. create_block_mask evaluates the predicate ahead of time to find which 128 by 128 tiles are entirely masked, so the kernel skips them, which is block-sparse FlashAttention (Exercise 12.3d) with the sparsity pattern written as code.

🔥 In PyTorchfor a machine with PyTorch

The printed block mask shows which 128 by 128 tiles the kernel would visit. With a 64-token window, only the diagonal tiles and the ones just below them survive.

The standalone flash-attn package exposes the kernels directly, with tensors in (batch, T, heads, d_k) layout, and adds options SDPA lacks. The block below shows the three calls you will meet in model code. It needs a CUDA GPU.

Variable-length batches without padding

Real batches hold sequences of different lengths. Padding them to the longest wastes compute on pad tokens, and a padding mask (§4.4) only stops the pads from mattering, not from costing. With lengths L1,…,LBL_1, \dots, L_B, padded attention computes B⋅(max⁡bLb)2B\cdot(\max_b L_b)^2 scores per head, while the useful work is ∑bLb2\sum_b L_b^2. For lengths 1,000, 3,000, and 96 that is 27 million scores for 10 million useful ones.

The variable-length ("varlen") kernels instead take the sequences packed end to end into one tensor of shape (total_tokens, heads, d_k), plus a vector of cumulative offsets cu_seqlens =[0,L1,L1+L2,… ]= [0, L_1, L_1 + L_2, \dots] (int32, of length B+1B + 1). Sequence bb occupies rows cu_seqlens[b] to cu_seqlens[b + 1]. Mathematically this is attention with the block-diagonal document mask of §4.4. The kernel never visits the tiles between different sequences, because each thread block works within one sequence's offsets, so the cost is ∑bLb2\sum_b L_b^2 and there is no mask to build at all. This is also how packed pretraining with document masking is run at scale, and how encoders such as ModernBERT drop padding entirely (§7.1).

PyTorch's equivalent is a nested tensor with the jagged layout, which SDPA accepts directly. Here is the same idea on a CPU.

🔥 In PyTorchfor a machine with PyTorch
Core

Variable-length attention from packed sequences

Implement make_cu_seqlens(lengths) returning the int32 offsets [0,L1,L1+L2,… ][0, L_1, L_1 + L_2, \dots], and varlen_attention(q, k, v, cu_seqlens, causal=False) for packed self-attention, where q, k, v have shape (total, heads, d_k) and sequence bb is rows cu_seqlens[b]:cu_seqlens[b + 1]. Return the output, shape (total, heads, d_k). Tokens must attend only within their own sequence, and with causal=True only to earlier or equal positions of it. Loop over sequences if you like, but vectorize over heads and tokens within a sequence, and never build a (total, total) mask.

Hidden tests, hints, and AI help, from $1.99 a month.
versiontargetkey ideasreported speed
FlashAttention (2022)A100tiling, online softmax, recomputation2 to 4 times standard attention, 25 to 40% of peak
FlashAttention-2 (2023)A100fewer non-matmul FLOPs, parallel over sequence, split-Q within a blockabout 2 times FA1, 50 to 73% of peak
Flash-Decoding (2023)A100split-KV across thread blocks, log-sum-exp mergeup to 8 times faster long-sequence generation
FlashAttention-3 (2024)H100warp specialization, ping-pong and pipelining, FP8 with incoherent processing1.5 to 2.0 times FA2, up to 740 TFLOP/s fp16, about 1.2 PFLOP/s FP8

Read more

Watch next

🎬 Flash Attention 2.0 with Tri Dao (author)! | Discord server talks · Aleksa Gordić - The AI EpiphanyTri Dao walks through the FlashAttention-2 changes and answers detailed questions about the kernel.
🎬 Lecture 36: CUTLASS and Flash Attention 3 · GPU MODEJay Shah, first author of FlashAttention-3, explains how it is built with CUTLASS on Hopper, including the warpgroup overlapping.