FlashAttention-2 · forward + backward · Triton
The score matrix
never lands.
Attention at long context is slow because of memory movement, not because of the maths. These kernels do the same arithmetic as ordinary attention — more of it, in the backward pass — and still win, purely by never writing the N×N score matrix to GPU main memory.
- algorithm
- FlashAttention-2, forward and backward
- result
- exact — not an approximation
- precision
- fp16, bf16
- head dim
- 32, 64, 128
- masking
- causal and full, mask applied on the diagonal only
- tile sizes
- autotuned over BLOCK_M, BLOCK_N, warps, stages
- target
- NVIDIA SM 80+, CUDA PyTorch, Triton
- licence
- MIT
L, one float per row, survives.
Run it
Watch the inner loop
Every square is one BLOCK_M×BLOCK_N tile of the score matrix. Amber means the tile exists right now, on chip. Everything already computed is gone — it was never stored. Click any row to follow that program's softmax state.
keys / values — inner loop, BLOCK_N at a time →
queries — one program per BLOCK_M
The argument
Why skipping HBM is the whole speedup
Ordinary attention writes S = QKᵀ/√d to HBM, reads it back to
run softmax, writes P, and reads it once more to multiply by V.
That is Θ(N²) traffic across the slowest link in the machine, on top of the
Θ(Nd) the inputs actually need.
The hierarchy is lopsided enough that this dominates everything. On an A100, SRAM moves data roughly ten times faster than HBM, and there is about 192 KB of it per SM. A 8192×8192 fp16 score matrix is 128 MiB. It cannot live on chip, so every element makes several round trips over the slow link.
Tiling keeps the working set small enough to stay on chip for the entire inner loop. Counting HBM accesses rather than FLOPs: standard attention is Θ(N² + Nd), the tiled version is Θ(N²d²/M) where M is SRAM size. With d = 64 and M ≈ 100 KB, d²/M is about 1/25.
Note — the counterintuitive part
The backward pass does more arithmetic than the ordinary version, because
it recomputes P from the saved L instead of reading it back.
It is still faster. At these shapes attention is memory-bound, so trading FLOPs for
HBM traffic is a trade worth making.
| Level | Size | Bandwidth |
|---|---|---|
| SRAM (on chip) | ~192 KB / SM | ~19 TB/s |
| HBM (main memory) | 40–80 GB | ~1.5–2 TB/s |
| Implementation | Complexity |
|---|---|
| Standard attention | Θ(N² + Nd) |
| FlashAttention | Θ(N²d²/M) |
Work it out
What a shape actually costs
Nothing measured here — this is just the arithmetic of the tensor sizes, which is enough to show where the quadratic term takes over.
The catch there isn't
Tiling softmax is exact algebra
Softmax is not local: the denominator needs every score in the row, and a tile only ever sees part of one. The fix is to carry running statistics and correct them.
Keep m, the largest score seen so far, and l, the running
sum of exp(score − m). When a new tile arrives with a bigger max,
rescale everything already accumulated by
α = exp(m_old − m_new). That single factor corrects the sum and
the output accumulator together.
Every step is an algebraic identity, so after the last tile acc / l is
the same value ordinary attention produces — up to the floating point rounding the
reference incurs too. That is why the test suite compares against fp32 PyTorch with
dtype tolerances rather than approximation tolerances. The readout in the visualiser
above runs this comparison live.
FlashAttention-2 adds three things on top, all implemented here: defer the normalisation to one divide after the loop, put queries in the outer loop so the accumulator stays in registers, and split the grid over the sequence dimension so long context with small batch still fills the SMs. Full derivation in DERIVATION.md.
# running state, resident in registers m_i = -inf # biggest score so far l_i = 0 # running sum of exp(s - m_i) acc = 0 # output, not yet divided # for each K/V tile: m_ij = max(m_i, max(qk)) p = exp2(qk - m_ij) alpha = exp2(m_i - m_ij) # the correction l_i = l_i * alpha + sum(p) acc = acc * alpha + p @ v m_i = m_ij # once, after the loop (this is the FA-2 part) acc = acc / l_i L = m_i + log(l_i) # N floats, not N x N
exp2 rather than exp:
it is a single hardware instruction, so log2(e) is folded into the scale once,
before the loop.
Scope
What is and isn't in here
| Forward kernel, FA-2 style — normalise at the end, Q in the outer loop, grid split over the sequence | done |
| Backward kernels for dQ, dK, dV, rebuilding P from the saved L | done |
| Causal masking, with the mask applied only on the diagonal tile | done |
| fp16 and bf16 | done |
| Sequence lengths that aren't a multiple of the tile size | done |
| Autotuning over BLOCK_M / BLOCK_N / warps / stages | done |
autograd.Function plus a drop-in FlashSelfAttention module | done |
| Tests against fp32 PyTorch across shapes, dtypes and masking | done |
| Benchmark harness — speed, TFLOP/s, peak memory, OOM point | done |
| MQA/GQA, dropout, ALiBi, sliding window, variable-length batches | not yet |
| Hopper / FlashAttention-3 path | out of scope |
Measurements
Benchmarks
This table is generated from docs/results.json, which the
benchmark harness writes. It is empty until someone runs it on real hardware — there
are no numbers here from a machine the code wasn't run on.
reading results.json…
Source
Where things live
# needs an NVIDIA GPU (SM 80+), CUDA PyTorch, Triton. # Triton does not run on Windows -- Linux, WSL2 or Colab. pip install -r requirements.txt && pip install -e . # then from flash_attn_triton import flash_attn, FlashSelfAttention out = flash_attn(q, k, v, causal=True) # (B, H, N, D), fp16/bf16 attn = FlashSelfAttention(embed_dim=1024, num_heads=16, causal=True)
Sources
Papers this came from
The algorithm
- FlashAttention: Fast and Memory-Efficient Exact Attention with IO-AwarenessDao, Fu, Ermon, Rudra, Ré · NeurIPS 2022 · arXiv:2205.14135
- FlashAttention-2: Faster Attention with Better Parallelism and Work PartitioningDao · ICLR 2024 · arXiv:2307.08691
- FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precisionShah et al. · NeurIPS 2024 · arXiv:2407.08608 · Hopper only, not implemented
Why tiling softmax stays exact
- Online normalizer calculation for softmaxMilakov, Gimelshein · 2018 · arXiv:1805.02867
- Self-attention Does Not Need O(n²) MemoryRabe, Staats · 2021 · arXiv:2112.05682
Why memory movement is the bottleneck
- Data Movement Is All You Need: A Case Study on Optimizing TransformersIvanov et al. · MLSys 2021 · arXiv:2007.00072
- Roofline: An Insightful Visual Performance Model for Multicore ArchitecturesWilliams, Waterman, Patterson · CACM 2009
The compiler
- Triton: An Intermediate Language and Compiler for Tiled Neural Network ComputationsTillet, Kung, Cox · MAPL 2019