Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
Prefix caching reuses KV state across requests that share a prompt prefix. It removes repeated work between requests, not the all-pairs work inside one uncached attention call. FlashAttention attacks the layer below that: it makes each dense attention kernel move far less data through GPU memory.
A coding assistant still compares every new prompt token with every earlier token while it prefills. Before looking at the number, predict what scales: each query-key pair contributes one saved score, so doubling sequence length squares the score storage. If the implementation writes the full score matrix to GPU RAM, an 8,192-token prompt at batch 8 with 32 heads in 16-bit floats needs about 32 GiB just for those scores.
FlashAttention computes the same attention operator, but it doesn't park the full score or probability matrix in HBM (High Bandwidth Memory, the large GPU RAM pool).[1] When attention IO is the bottleneck, that can raise throughput or let a longer context fit.
What is the main promise of FlashAttention?
Answer
It computes exact dense attention while avoiding the full score and probability matrices in HBM. The win is lower memory traffic and lower auxiliary memory, not a different attention formula.
The memory wall
Materialized attention first
Make the baseline explicit before looking at the optimization. In the scaled dot-product attention article, each query token scores every key token, normalizes those scores with softmax, and blends the corresponding value vectors.
Suppose you have only three prompt tokens from a failing-test message, and a head dimension of two:
- Query is a matrix (one row per token).
- Key and Value are also .
The score matrix is . That's nine numbers. Now make the prediction before scaling up: at 8,192 tokens, the same all-pairs layout is the memory problem.
For , , in 16-bit floating point (FP16):
That product is exactly bytes, which is 32 GiB or about 34 GB depending on whether you count in binary or decimal units. If an attention implementation saves this intermediate, it consumes a large part of an 80 GB A100 before counting model weights, other activations, or gradients.
Check that prediction with a small calculation before blaming model weights for an out-of-memory error:
1batch, heads, sequence, bytes_per_value = 8, 32, 8192, 2
2score_bytes = batch * heads * sequence * sequence * bytes_per_value
3
4print(f"score values: {batch * heads * sequence * sequence:,}")
5print(f"binary size: {score_bytes / 1024**3:.2f} GiB")
6print(f"decimal size: {score_bytes / 1000**3:.2f} GB")1score values: 17,179,869,184
2binary size: 32.00 GiB
3decimal size: 34.36 GBWhy does an 8,192-token attention matrix become huge even before model weights are counted?
Answer
Each head compares every query token with every key token, so storage grows with . With batch 8, 32 heads, 8,192 by 8,192 scores, and 2 bytes per FP16 score, the score matrix alone is about 34 GB (32 GiB).
GPU memory hierarchy
The score count explains capacity. Now follow those values across the memory hierarchy. A GPU isn't a flat memory space. It has layers:
- On-chip SRAM (static random-access memory) is the tiny, very fast scratchpad right next to the compute cores.
- HBM is the large pool of GPU memory (VRAM) that holds model weights and tensors.
- CPU DRAM is host memory outside the GPU, used only when data must leave the card entirely.
Memory hierarchy intuition: On-chip SRAM is a tiny, fast scratchpad beside the compute cores. HBM is much larger, but every revisit costs bandwidth. A materializing attention baseline keeps returning to HBM because the full score matrix doesn't fit in the scratchpad. FlashAttention keeps only active tiles there at a time.
In the original FlashAttention paper, the motivating A100 numbers are 192 KB of on-chip SRAM per streaming multiprocessor across 108 SMs (about 20 MB aggregate) at roughly 19 TB/s, versus 40-80 GB of HBM at 1.5-2.0 TB/s.[1] A kernel doesn't get that full 20 MB as one giant scratchpad: practical tile sizes are bounded by much smaller per-SM shared-memory and register budgets. Chip layouts change by generation, but the qualitative gap stays: on-chip memory is tiny and fast, off-chip memory is large and much slower to revisit.
| Memory tier | Typical role in attention | Capacity / bandwidth intuition |
|---|---|---|
| On-chip SRAM | Hold the current Q/K/V tiles and running softmax statistics | Tiny, but fast enough to reuse the same tile many times |
| HBM | Hold Q, K, V, O, model weights, and other activations | Much larger, but expensive to touch for every intermediate |
| CPU DRAM | Host memory outside the GPU | Larger still, but not suitable for the inner loop of an attention kernel |

Use this as a capacity pyramid, not a one-way data pipe. Compute wants tiles in SRAM, so ask what happens when the same tile is reused. A materializing baseline keeps bouncing the full score and probability matrices through HBM, while FlashAttention pays for local bookkeeping to avoid those round-trips.[1]
Materialized attention can become IO-limited
In a materializing baseline, the score and probability matrices are written to HBM and read again. A fused backend may already avoid those intermediates, so the useful comparison is FlashAttention versus the backend the system would otherwise execute, not versus every call named "attention."
Why is HBM traffic the bottleneck even though GPUs have enormous FLOPs?
Answer
Matrix math can run very fast once data is on-chip, but repeatedly writing and rereading score and probability matrices through HBM stalls the compute units. FlashAttention improves arithmetic intensity by reusing tiles in SRAM before writing only final outputs and row statistics.
The FlashAttention algorithm
Core idea: tiling + online softmax
The baseline keeps the attention formula, but its temporary intermediates live too long. FlashAttention combines three ideas instead of materializing the full attention matrix:
- Tiles the computation into blocks that fit in SRAM.
- Uses online softmax to compute exact softmax without seeing all values at once.
- Never materializes the full attention matrix in HBM.
This block-wise processing strategy avoids quadratic auxiliary score and probability storage. The input, output, and saved row-statistic tensors still scale with sequence length. Before looking at the schedule, predict what should disappear from HBM: temporary score and probability tiles, not the final output.
Trace two data lifetimes in the figure. A materializing baseline writes the full score matrix to HBM, reads it back for softmax, writes the probability matrix, and reads it again for the final multiply. FlashAttention streams small tiles through SRAM and saves only row-wise statistics.

The left path changes memory traffic by revisiting intermediates in HBM. The right path reuses small blocks in SRAM, then writes the output and compact row statistics. Same operator, different data lifetime.
Why doesn't tiling make FlashAttention approximate?
Answer
Tiling changes the order of computation, not the attention definition. Online softmax rescales previous partial sums whenever a new block changes the row maximum, so the final normalized output matches dense attention up to normal floating-point ordering differences.
How tiling fits in SRAM
Tiling changes data lifetime. Pull a block of , , and from HBM into SRAM, compute local scores there, and throw the score tile away. During forward execution the kernel writes final output and, for training, compact row statistics for backward. It doesn't write the full score or probability matrices.
Now the math problem appears. Softmax over a row needs a global max and a global sum. If SRAM holds one K/V tile at a time, you can't see the rest of the row yet. Online softmax is the fix that keeps the result exact.
Ask what must fit at the same time. Tile size is bounded by on-chip capacity. This simplified payload check counts four FP16 tile-shaped arrays (Q, K, V, and a partial output), while production kernels also budget for statistics, registers, and implementation overhead:
1block_rows, head_dimension, arrays, bytes_per_value = 128, 64, 4, 2
2payload_bytes = block_rows * head_dimension * arrays * bytes_per_value
3
4print(f"simplified tile payload: {payload_bytes / 1024:.0f} KiB")
5print("also budget: row statistics, registers, and kernel overhead")1simplified tile payload: 64 KiB
2also budget: row statistics, registers, and kernel overheadOnline softmax with a concrete example
Suppose SRAM fits only two scores at a time. A longer row can still be normalized if you keep a running max and denominator , then rescale whenever a later tile raises the max.
Standard softmax on three scores
Suppose one query token sees key scores .
- Find the max: .
- Exponentiate relative to the max: .
- Sum: .
- Normalize: .
A materializing baseline would store the full score matrix in HBM just to run that four-step process for every row.
Online softmax with two blocks
Now split the same scores into Block A and Block B . Predict Block B's job: it must contribute its weight without forcing us to retain Block A's raw scores.
Processing Block A:
- Local max: .
- Local denominator: .
- Local unnormalized numerator: .
Processing Block B:
- Local max: .
- New global max: .
- Rescale the old denominator: .
- Update the numerator: .
The final row output is , the same as dense softmax. We never held all three scores in the fast workspace at once. In this split the max didn't move, so the rescale factor is . Change Block B's score to and the next checkpoint makes the rescaling rule visible.

Test the same invariant with scalar values, independent of any GPU kernel:
1import math
2
3scores_a, values_a = [1.0, 2.0], [10.0, 20.0]
4scores_b, values_b = [0.5], [40.0]
5
6def local_state(scores, values):
7 max_score = max(scores)
8 weights = [math.exp(score - max_score) for score in scores]
9 return max_score, sum(weights), sum(weight * value for weight, value in zip(weights, values))
10
11m_a, l_a, n_a = local_state(scores_a, values_a)
12m_b, l_b, n_b = local_state(scores_b, values_b)
13m = max(m_a, m_b)
14l = math.exp(m_a - m) * l_a + math.exp(m_b - m) * l_b
15n = math.exp(m_a - m) * n_a + math.exp(m_b - m) * n_b
16online = n / l
17
18all_scores = scores_a + scores_b
19all_values = values_a + values_b
20dense_weights = [math.exp(score - max(all_scores)) for score in all_scores]
21dense = sum(w * v for w, v in zip(dense_weights, all_values)) / sum(dense_weights)
22
23print(f"online output: {online:.6f}")
24print(f"dense output: {dense:.6f}")
25print(f"match: {abs(online - dense) < 1e-12}")1online output: 20.492649
2dense output: 20.492649
3match: TrueIf Block B had contained a score of instead of , what must happen to Block A's previous contributions?
Answer
They must be rescaled by because the global max changed from 2.0 to 3.0. Without that down-weighting, the denominator and numerator would mix values normalized against different maxima.
The general update rule
The two-block example scales to every row and every K/V tile. For each new block of scores and value vectors :
Where is the running max score (for numerical stability), is the running softmax denominator, is the running unnormalized numerator accumulator, and is the normalized output.
The rescaling terms keep previous results correct even though the max changed. This is the mathematical trick that eliminates the need for a second pass over the full row.[2]
The equations explain correctness. Loop order determines how often , , , , and row statistics cross HBM. A materializing baseline makes multiple round-trips for the full matrix; FlashAttention performs tile-local score and softmax work on-chip, writing output and compact saved statistics to HBM.
Pseudocode
Now connect the state update to a hardware schedule. The original FlashAttention paper loops over K/V tiles in the outer loop and writes each Q tile's running state back to HBM between inner steps.[1] FlashAttention-2 keeps a Q tile on-chip and streams K/V tiles past it, which cuts extra HBM traffic for and the row stats.[3] The sketch below follows that later loop nest.
Read it as a correctness sketch, not a production kernel. It takes , , and as nested Python lists and a block size. The last tile can be shorter than block_size; the test uses a block size that doesn't divide , so remainder tiles get exercised too.
1import math
2
3def matmul(a, b):
4 cols = len(b[0])
5 out = [[0.0] * cols for _ in range(len(a))]
6 for i, row in enumerate(a):
7 for k, aik in enumerate(row):
8 bk = b[k]
9 oi = out[i]
10 for j in range(cols):
11 oi[j] += aik * bk[j]
12 return out
13
14def transpose(matrix):
15 return [list(col) for col in zip(*matrix)]
16
17def scale_rows(matrix, factors):
18 return [[factor * value for value in row] for factor, row in zip(factors, matrix)]
19
20def add_matrices(left, right):
21 return [[x + y for x, y in zip(a, b)] for a, b in zip(left, right)]
22
23def flash_attention(Q, K, V, block_size):
24 n, d = len(Q), len(Q[0])
25 scale = 1.0 / math.sqrt(d)
26 O = [[0.0] * d for _ in range(n)]
27 for i0 in range(0, n, block_size):
28 Qi = Q[i0:i0 + block_size]
29 bq = len(Qi)
30 Oi = [[0.0] * d for _ in range(bq)]
31 li = [0.0] * bq
32 mi = [float("-inf")] * bq
33 for j0 in range(0, n, block_size):
34 Kj = K[j0:j0 + block_size]
35 Vj = V[j0:j0 + block_size]
36 Sij = [[value * scale for value in row] for row in matmul(Qi, transpose(Kj))]
37 m_local = [max(row) for row in Sij]
38 m_new = [max(old, local) for old, local in zip(mi, m_local)]
39 exp_old = [math.exp(old - new) for old, new in zip(mi, m_new)]
40 exp_new = [
41 [math.exp(score - new_max) for score in row]
42 for row, new_max in zip(Sij, m_new)
43 ]
44 Oi = add_matrices(scale_rows(Oi, exp_old), matmul(exp_new, Vj))
45 li = [
46 old_scale * old_l + sum(new_row)
47 for old_scale, old_l, new_row in zip(exp_old, li, exp_new)
48 ]
49 mi = m_new
50 for row_index, (row, denom) in enumerate(zip(Oi, li)):
51 O[i0 + row_index] = [value / denom for value in row]
52 return O
53
54def dense_attention(Q, K, V):
55 d = len(Q[0])
56 scale = 1.0 / math.sqrt(d)
57 scores = [[value * scale for value in row] for row in matmul(Q, transpose(K))]
58 output = []
59 for row in scores:
60 max_score = max(row)
61 weights = [math.exp(score - max_score) for score in row]
62 total = sum(weights)
63 probs = [weight / total for weight in weights]
64 output.append([
65 sum(prob * V[j][dim] for j, prob in enumerate(probs))
66 for dim in range(d)
67 ])
68 return output
69
70def fill(n, d, seed):
71 return [
72 [math.sin((i + 1) * 1.7 + (j + 1) * 0.9 + seed) for j in range(d)]
73 for i in range(n)
74 ]
75
76n, d = 8, 4
77Q, K, V = fill(n, d, 0.0), fill(n, d, 1.3), fill(n, d, 2.7)
78got = flash_attention(Q, K, V, block_size=3)
79expected = dense_attention(Q, K, V)
80max_diff = max(abs(a - b) for ra, rb in zip(got, expected) for a, b in zip(ra, rb))
81print(f"max difference: {max_diff:.2e}")
82print(f"match: {max_diff < 1e-12}")1max difference: 1.11e-16
2match: TrueOi stays an unnormalized numerator until every K/V tile for that Q block has landed. Then we divide by li. The check uses block_size=3, so the last tiles are remainder-sized, and the tiled loop still matches dense attention.
Common mistake: Don't normalize
Oiinside the inner loop. The final denominator isn't known until every K/V block has contributed. Early division locks in the wrong scale before a later tile can change the running max.
Production kernels implement the same algebra much more aggressively, while also handling batching, multiple heads, masks, and dropout.
In the pseudocode, why does Oi stay unnormalized until all K/V blocks for a Q tile are processed?
Answer
The final denominator li isn't known until every K/V block has contributed to the row. Normalizing early would lock in the wrong scale before later blocks can change the running max and denominator.
The backward pass: recomputation can win
Training creates a second memory question: backward needs score and probability information. A materializing implementation can save the massive attention matrices and from forward to compute gradients later. Those saved intermediates can become a major source of out-of-memory (OOM) errors.
FlashAttention solves this by recomputing the needed score and probability tiles during the backward pass instead of storing them all from the forward pass.[1] Because it saves compact row-wise softmax statistics instead, the saved attention state grows as rather than .
The alternative is deliberate recomputation: keep compact running statistics, reconstruct local blocks when backward needs them, and avoid storing the full matrix. That's FlashAttention's training trade-off.
Recomputation isn't free. It adds arithmetic in backward. The point of the FlashAttention paper is that, on the evaluated GPU workloads, avoiding much larger HBM reads and writes more than paid for that arithmetic cost.[1] Measure the trade-off on the model shape and hardware you deploy.
This calculator makes the saved-state difference concrete. It compares one FP16 score matrix with the FP32 softmax_lse tensor saved by the current flash-attention interface, not every tensor in training:
1batch, heads, sequence = 8, 32, 8192
2materialized_scores = batch * heads * sequence * sequence * 2
3softmax_lse = batch * heads * sequence * 4
4
5print(f"one saved score matrix: {materialized_scores / 1024**3:.2f} GiB")
6print(f"FP32 softmax LSE: {softmax_lse / 1024**2:.2f} MiB")
7print(f"size ratio: {materialized_scores / softmax_lse:,.0f}x")1one saved score matrix: 32.00 GiB
2FP32 softmax LSE: 8.00 MiB
3size ratio: 4,096x| Property | Materializing baseline | FlashAttention |
|---|---|---|
| Saved attention state for backward | Store and explicitly: | Store compact row-wise softmax statistics: |
| Backward strategy | Read large intermediates from HBM | Recompute local tiles from plus saved stats |
| Trade-off | Less recomputation, much higher memory | More recomputation, much lower memory |
Why can recomputing attention tiles during backward be faster than storing them during forward?
Answer
On modern GPUs, rereading huge saved matrices from HBM can cost more wall-clock time than recalculating small tiles from , , and . FlashAttention saves row-wise softmax statistics, then spends cheap compute to avoid expensive memory movement.
Complexity analysis
With the mechanism in hand, separate three quantities: mathematical work, extra attention state, and memory traffic. FlashAttention changes the IO complexity by tiling the computation. Let denote the amount of fast SRAM available to hold a tile's working set.
| Property | Materializing baseline | FlashAttention |
|---|---|---|
| Auxiliary attention memory | Materialize and : | Keep row stats and the current output tile: |
| FLOPs | (same) | |
| HBM reads/writes | ||
| Exact | Yes | Yes |

FLOPs stands for floating-point operations. The memory row refers to extra state created by the attention kernel itself, not the shared , , , and tensors that both approaches still need to hold.
FlashAttention doesn't reduce the asymptotic mathematical work: both paths still do operations. The win is avoiding score and probability transfers through HBM. Tile size, scheduling, datatype, and hardware still change observed speed.
Under the paper's SRAM model, tiling changes HBM reads and writes from for the materializing baseline to .[1] A larger usable SRAM budget lets each tile reuse more data before returning to HBM. This is an IO-model result, not a wall-clock guarantee: both paths still perform arithmetic, and kernel schedules determine how much of the bound appears in a benchmark.
Because FlashAttention computes the same dense operator (via online softmax), it doesn't change the model's attention rule. Different floating-point association can still cause small numeric differences.
Predict the curves before running the small scaling check: a score matrix should grow with the square of sequence length, while one row statistic per token should grow linearly.
These numbers distinguish score-matrix scaling from row-statistic scaling:
1base_sequence = 1024
2for sequence in [1024, 2048, 4096, 8192, 16384]:
3 materialized_relative = (sequence / base_sequence) ** 2
4 row_stats_relative = sequence / base_sequence
5 print(
6 f"{sequence:>5} tokens: materialized={materialized_relative:>5.0f}x, "
7 f"row-stats={row_stats_relative:>2.0f}x"
8 )11024 tokens: materialized= 1x, row-stats= 1x
2 2048 tokens: materialized= 4x, row-stats= 2x
3 4096 tokens: materialized= 16x, row-stats= 4x
4 8192 tokens: materialized= 64x, row-stats= 8x
516384 tokens: materialized= 256x, row-stats=16xWhich complexity changes with FlashAttention: FLOPs, auxiliary attention memory, or model quality?
Answer
Auxiliary attention memory drops from saved score/probability matrices to row statistics, and HBM IO drops sharply. FLOPs remain , and model quality doesn't change because the dense attention result is still exact.
Causal masking in FlashAttention
For autoregressive transformers, attention is causal: token can only attend to tokens . FlashAttention handles this efficiently without materializing a dense causal mask in HBM. The useful question is schedule-level: which K/V tiles are entirely past, on the boundary, or entirely future for a Q tile?
Block-level skipping
If a block of K/V tokens is entirely in the "future" relative to a Q block, the entire block multiplication is skipped. No compute is wasted on keys the query can't see.
Within-block masking
For blocks that straddle the causal boundary, FlashAttention applies the mask after computing scores but before the softmax update. Masked positions become .

Causal masking is part of the tile schedule, not a dense mask matrix in HBM. Skipping future tiles saves work, but the exact speedup depends on sequence length, tile shape, and the kernel. The same skip rule also works for local windows: tiles completely outside the window never multiply.
You can audit causal tile decisions without any GPU code:
1tiles = 4
2counts = {"past": 0, "boundary": 0, "future": 0}
3
4for query_tile in range(tiles):
5 for key_tile in range(tiles):
6 if key_tile < query_tile:
7 decision = "past"
8 elif key_tile == query_tile:
9 decision = "boundary"
10 else:
11 decision = "future"
12 counts[decision] += 1
13
14print(counts)
15print(f"computed tiles: {counts['past'] + counts['boundary']} of {tiles * tiles}")1{'past': 6, 'boundary': 4, 'future': 6}
2computed tiles: 10 of 16How does FlashAttention avoid materializing a full causal mask?
Answer
The tile scheduler skips K/V blocks that are entirely in the future and applies an in-tile mask only for blocks that cross the causal boundary. The mask becomes part of the tiled kernel schedule instead of a dense matrix stored in HBM.
FlashAttention-2, FlashAttention-3, and FlashAttention-4
Once tiling and online softmax reduce large HBM transfers, the bottleneck moves into work partitioning and hardware pipelines. Each generation keeps the exact operator while changing that schedule for its target GPU.
FlashAttention-2[3] asks how to keep more independent work in flight:
- Parallelizes along sequence length as well as batch and heads, so occupancy stays higher when sequences are long and batch size is small.
- Splits across warps (groups of 32 GPU threads) instead of splitting /, which cuts shared-memory traffic inside a thread block.
- Reduces non-matmul FLOPs such as extra softmax rescales.
- On A100 it reaches 50-73% of theoretical peak (up to 73% forward and 63% backward in the paper body), about 2× FlashAttention-1, with kernel throughput up to 230 TFLOPs/s.[3]
On Hopper, the next question is whether data movement, matrix multiply, and softmax can overlap. FlashAttention-3[4] is that redesign:
- Overlaps TMA (Tensor Memory Accelerator) loads with WGMMA (Warpgroup Matrix-Multiply Accumulate) using warp specialization.
- Interleaves softmax with the next GEMM so the slow exponential sits under async matmul.
- Adds an FP8 forward path with block quantization.
- On H100, the paper reports 1.5-2.0× vs FlashAttention-2 in FP16 forward, up to 740 TFLOPs/s (75% utilization), and close to 1.2 PFLOPs/s in FP8.[4]
Blackwell changes that balance again: tensor-core throughput doubled while shared-memory bandwidth and exponential units didn't. FlashAttention-4[5] retunes the pipeline for Blackwell B200 and GB200.
The March 2026 paper reports up to 1,613 TFLOPs/s in FP16/BF16 on B200 (about 71% of peak), with up to 1.3× vs cuDNN 9.13 and 2.7× vs the paper's Triton baseline. Those are kernel numbers from that paper's setup, not guaranteed application speedups.
The implementation is CuTe-DSL embedded in Python, not CUDA C++ templates.
The tiled online-softmax core stays the same. Peak kernels still need hardware-aware scheduling. FlashAttention-1 through 3 are CUDA/CUTLASS-family kernels; FlashAttention-4 is CuTe-DSL. Triton is a productive way to write attention kernels and is the slower baseline in the FlashAttention-4 B200 comparison.
Most application teams should start with a framework SDPA API, verify which backend actually ran, and profile before writing a custom kernel.
What changed after FlashAttention-1?
Answer
The core exact tiled attention idea stayed the same. FlashAttention-2 improved work partitioning and reduced non-matmul overhead. FlashAttention-3 targeted Hopper features such as TMA, WGMMA, asynchronous overlap, and an FP8 forward path. FlashAttention-4 retuned the pipeline for Blackwell and implemented it in CuTe-DSL.
Measured performance
Training throughput
Treat benchmark rows as scoped evidence, not universal multipliers.
During model training, saving quadratic attention intermediates can sharply restrict the maximum sequence length a model can process. As sequence length increases, a materializing baseline may run out of memory even when a fused attention path can still fit.
Common mistake: Assuming FlashAttention is only a "long-sequence hack." The original paper reports a 15% end-to-end wall-clock speedup for BERT-large at sequence length 512 against the MLPerf 1.1 training speed record, so even a moderate evaluated sequence can benefit when attention IO matters.[1]
For each result below, keep workload, sequence length, hardware, and baseline attached to the number.
Compared with a baseline that saves full attention intermediates, FlashAttention's auxiliary attention memory grows linearly rather than quadratically. That can enable longer sequences and improve throughput even while a materializing baseline still fits in memory.[1][3]
| Source | Workload | Reported result |
|---|---|---|
| FlashAttention (2022) | BERT-large, sequence length 512 | 15% end-to-end vs MLPerf 1.1 speed record[1] |
| FlashAttention (2022) | GPT-2, sequence length 1K | 3× vs HuggingFace and Megatron-LM baselines[1] |
| FlashAttention (2022) | Long Range Arena, sequence length 1K-4K | 2.4× speedup[1] |
| FlashAttention-2 (2023) | GPT-style training on A100 | Up to 225 TFLOPs/s per GPU, 72% model FLOPs utilization[3] |
These rows answer different questions, so don't transfer one multiplier to another model or GPU. The shared mechanism is the memory path: once the attention kernel stops writing giant intermediates to HBM, longer-sequence training becomes more practical. Measure end-to-end throughput on your own shape.
Inference impact
During inference, FlashAttention helps most when the workload still looks like dense attention over many prompt tokens:
- Prefill phase benefits the most, because the model still performs full prompt self-attention. That work sits on the time to first token (TTFT) path. An 8,192-token coding-assistant prompt (shared repo guidelines plus a failing test log) still needs every prompt token to score every other prompt token before the first generated token.
- Decode phase benefits less from dense full-sequence FlashAttention alone, because each step introduces one new query token and reuses the KV cache, so weight streaming and other bottlenecks often dominate.
- Long prompts benefit more than short prompts, because avoiding a materialized score matrix matters more as grows.
FlashAttention has its biggest impact when attention itself is the bottleneck. That's usually training and prefill, not naive single-token decode over a contiguous cache.
Production decode uses a related but distinct kernel contract
Serving engines rarely keep one contiguous KV tensor per request. PagedAttention stores K/V in blocks and maps logical positions through a block table. Production decode therefore needs kernels that apply FlashAttention-style tiling and online softmax over non-contiguous KV blocks, beyond the training and prefill dense path.
Examples of that contract (names and APIs evolve):
flash_attn_with_kvcacheand related FlashAttention decode entry points that accept a packed or paged cache layout- FlashInfer and engine-specific paged attention kernels used by stacks such as vLLM
Those kernels are complementary to the PagedAttention allocator: paging decides where blocks live and how they are shared; the decode kernel decides how Q attends to those blocks without materializing full scores. GQA/MQA further shrinks bytes per token that those kernels read. Measure prefill TTFT and decode ITL on your engine; don't assume "FlashAttention = prefill only" or that a training FA kernel is a drop-in for multi-tenant decode.
This shape check shows why prefill creates far more score work per request than one decode step:
1prompt_tokens = 8192
2prefill_scores = prompt_tokens * prompt_tokens
3decode_scores = 1 * prompt_tokens
4
5print(f"prefill scores: {prefill_scores:,}")
6print(f"one decode step scores: {decode_scores:,}")
7print(f"ratio: {prefill_scores // decode_scores:,}x")1prefill scores: 67,108,864
2one decode step scores: 8,192
3ratio: 8,192xHardware compatibility
Generation names are not dispatch guarantees. The algorithmic idea is general, but the fastest kernels are hardware-specific. FlashAttention-2 describes better parallelism and work partitioning for modern GPUs. FlashAttention-3 is a Hopper-focused redesign that targets features such as TMA, WGMMA, and an FP8 forward path.[3][4] FlashAttention-4[5] retunes the pipeline for Blackwell B200 and GB200 GPUs and implements it in CuTe-DSL.
Availability in an application depends on its framework build, device, datatype, tensor shapes, and attention features. Treat backend selection as something to verify, not something to infer from the model name.
When should you expect FlashAttention to help most during inference?
Answer
Expect the biggest inference gain during prefill, especially for long prompts, because the model computes dense attention over the full prompt. Decode often benefits less because each step has one new query token and the KV cache or memory bandwidth elsewhere may dominate.
Using FlashAttention in practice
In modern deep learning frameworks, you rarely implement FlashAttention from scratch. PyTorch exposes torch.nn.functional.scaled_dot_product_attention (SDPA), which may choose an optimized CUDA implementation when the inputs and build support it. Its sdpa_kernel context manager lets you select permitted implementations while testing or profiling. Eligibility and fallback behavior depend on the installed PyTorch build, device, datatype, layout, and attention features, so consult the documentation for that build and measure the actual path.[6]
Keep three claims separate: the operator is correct, the requested backend was selected, and the workload got faster. The next checks test them in that order.
First check the causal operator on a tiny row, in plain Python. This is the math SDPA is supposed to implement, not a FlashAttention dispatch test. Query token 1 may see keys 0 and 1 only:
1import math
2
3scores = [1.0, 2.0, 0.5, 3.0]
4values = [10.0, 20.0, 40.0, 80.0]
5
6def weighted_sum(scores, values, allowed):
7 masked = [score if ok else float("-inf") for score, ok in zip(scores, allowed)]
8 max_score = max(masked)
9 weights = [
10 0.0 if score == float("-inf") else math.exp(score - max_score)
11 for score in masked
12 ]
13 total = sum(weights)
14 return sum(weight * value for weight, value in zip(weights, values)) / total
15
16causal = weighted_sum(scores, values, [True, True, False, False])
17full = weighted_sum(scores, values, [True, True, True, True])
18print(f"causal output: {causal:.6f}")
19print(f"full output: {full:.6f}")
20print(f"masking changed the blend: {causal != full}")1causal output: 17.310586
2full output: 58.029621
3masking changed the blend: TrueCausal masking changes the blend because the high score 3.0 is future context for query 1. PyTorch SDPA with is_causal=True is supposed to implement that rule. Whether it does so with a FlashAttention kernel is a separate, hardware-dependent question.
Once the math is right, test dispatch. On CUDA hardware, backend restriction is an availability probe, not a speedup proof. This snippet is unmarked because it needs a suitable installed CUDA build and GPU. Current PyTorch documents sdpa_kernel with SDPBackend.FLASH_ATTENTION as the way to request the fused FlashAttention implementation. If that is the only permitted backend and the inputs aren't eligible, PyTorch warns with the reasons and can raise RuntimeError; it doesn't silently switch to the math backend inside that restricted context.[6] PyTorch's SDPA tutorial demonstrates the same boundary by catching the error around a forced-backend call.[7] For a resilient serving path, catch the probe failure, record it, and make a second, unrestricted SDPA call so PyTorch can choose an available implementation.
1import torch
2import torch.nn.functional as F
3from torch.nn.attention import SDPBackend, sdpa_kernel
4
5Q = torch.randn(2, 16, 1024, 64, device="cuda", dtype=torch.float16)
6K = torch.randn(2, 16, 1024, 64, device="cuda", dtype=torch.float16)
7V = torch.randn(2, 16, 1024, 64, device="cuda", dtype=torch.float16)
8
9def attention_with_flash_probe(Q, K, V):
10 try:
11 with sdpa_kernel(SDPBackend.FLASH_ATTENTION):
12 output = F.scaled_dot_product_attention(
13 Q, K, V, is_causal=True, dropout_p=0.0
14 )
15 return output, "flash_probe_succeeded"
16 except RuntimeError as error:
17 print(f"FlashAttention unavailable: {error}")
18 print("Retrying with unrestricted SDPA backend selection.")
19 output = F.scaled_dot_product_attention(
20 Q, K, V, is_causal=True, dropout_p=0.0
21 )
22 return output, "automatic_fallback"
23
24output, dispatch_path = attention_with_flash_probe(Q, K, V)
25print("dispatch policy:", dispatch_path)This exception and retry are deliberate. Keep the warning visible while recording the failed FlashAttention probe; the second call uses PyTorch's normal backend selection. A returned dispatch_path describes policy, not proof of the kernel that executed. Record operator correctness, selected-kernel evidence, and a before/after measurement:
1verification = {
2 "requested_backend": "flash_attention",
3 "unavailable_policy": "retry_unrestricted_sdpa",
4 "operator_correctness_checked": True,
5 "profiler_shows_selected_kernel": False,
6 "latency_measured": False,
7}
8
9active = (
10 verification["operator_correctness_checked"]
11 and verification["profiler_shows_selected_kernel"]
12 and verification["latency_measured"]
13)
14print(f"unavailable policy: {verification['unavailable_policy']}")
15print(f"enough evidence to claim speedup: {active}")
16print("next check: capture backend/profiler output on target GPU")1unavailable policy: retry_unrestricted_sdpa
2enough evidence to claim speedup: False
3next check: capture backend/profiler output on target GPUFor example, current Hugging Face Transformers attention backends accept attn_implementation="flash_attention_2" or "flash_attention_3" as a load-time request. FlashAttention-2 still expects fp16 or bf16. The string is a request, not a profiler trace:
1import torch
2from transformers import AutoModelForCausalLM
3
4model = AutoModelForCausalLM.from_pretrained(
5 "your-org/your-supported-causal-lm",
6 dtype=torch.bfloat16,
7 attn_implementation="flash_attention_2",
8 device_map="auto",
9)If you set attn_implementation="flash_attention_2", what should you verify before assuming the speedup is active?
Answer
Check that your GPU, datatype, head dimension, mask pattern, framework version, and installed kernel package support the requested backend. Then profile or inspect backend diagnostics on that exact build before reporting a speedup.
Common mistakes
"FlashAttention is an approximation"
-
Symptom: You hear FlashAttention grouped with sparse or low-rank attention approximations and assume it drops some connections to save memory.
-
Cause: The word "efficient" often implies approximation in other contexts.
-
Fix: FlashAttention is exact. Thanks to the online softmax trick, it computes the same dense attention formula without using sparse or low-rank shortcuts. Numeric outputs can differ slightly from a reference implementation because floating-point operations are associated in a different order, but the mathematical operator is the same. If you need proof, run the tiled Python sketch above and check that the max difference is near zero.
"FlashAttention reduces the number of compute operations"
-
Symptom: You claim that FlashAttention cuts FLOPs.
-
Cause: It's natural to equate "faster" with "fewer operations."
-
Fix: The forward attention computation still has floating-point operations (FLOPs). The speedup comes from reduced memory operations (IO), not from changing dense attention into a cheaper mathematical operator. In training, the backward pass can perform more operations because it recomputes tiles. The win is that compute is cheap and memory movement is expensive.
"GPU memory is one big pool"
-
Symptom: You only compare total VRAM capacity and miss why attention still runs slowly on large GPUs.
-
Cause: HBM, on-chip SRAM, shared memory, and registers have very different capacity and bandwidth profiles.
-
Fix: Ask where each tensor lives and how often it crosses the HBM/SRAM boundary. FlashAttention wins because it keeps Q/K/V tiles and softmax state on-chip long enough to reuse them, then writes only the final output and row statistics back to HBM.
"IO complexity is the same as time complexity"
-
Symptom: You explain FlashAttention as if it changes attention from quadratic time to linear time.
-
Cause: The memory table and the FLOP table get mixed together.
-
Fix: Keep the dimensions separate. Dense attention still does quadratic compute in sequence length. FlashAttention reduces HBM reads and writes, so wall-clock time improves when the workload is memory-bound.
"Online softmax is optional bookkeeping"
-
Symptom: You tile attention but normalize each block independently.
-
Cause: The running max and denominator updates look like an implementation detail.
-
Fix: Online softmax is the correctness mechanism. The running max rescales old contributions when a later tile contains a larger score, and the running denominator keeps all blocks normalized against the same global row.
"FlashAttention is only useful for long sequences"
-
Symptom: You skip enabling it on short-context models.
-
Cause: The OOM headlines make FlashAttention look like a long-sequence-only tool.
-
Fix: FlashAttention supports long sequences by avoiding memory limits, and it can still speed up shorter sequences because it reduces HBM access. Dao et al. reported a 15% BERT-large speedup at 512 tokens against the MLPerf 1.1 speed record.[1]
"You have to write custom CUDA kernels to use it"
-
Symptom: You avoid FlashAttention because you assume it requires low-level GPU programming.
-
Cause: The original paper describes kernel-level details, which can give the impression that users must write CUDA.
-
Fix: Use a framework SDPA API or supported model integration, then check backend selection and measure on the target GPU. A request flag is configuration, not proof that an optimized kernel ran.