Read FlashAttention from first principles: exact tiled attention, online softmax, GPU memory traffic, kernel dispatch, version history, and production trade-offs.
Personalize this lesson
Adapt explanations and teaching visuals to your background and preferred voice.
In full dense self-attention, each query compares with every key. Causal decoders mask future positions, but the number of allowed query-key pairs still grows quadratically. The arithmetic is familiar, but the GPU has a second problem: where each intermediate lives while that arithmetic runs. FlashAttention keeps exact attention while changing the order and location of those reads and writes.
This chapter reads the Dao-AILab implementation as an engineering project. It connects original papers to the source tree, works through a two-tile softmax by hand, then identifies when a production system should use FlashAttention, a framework backend, or a fallback. The earlier scaled dot-product attention lesson supplies the formula. Here the focus is the memory traffic around it.
Scaled dot-product attention forms a score for every query-key pair. For sequence length , that is scores per head before multiplying by values. A baseline implementation can write scores, read them for softmax, write probabilities, read them for the value multiply, and finally write the output. Each round trip crosses HBM (high-bandwidth memory), the large GPU memory pool.
HBM has high capacity and high aggregate bandwidth, but an on-chip SRAM (static random-access memory) workspace is much smaller and faster to revisit. GPU arithmetic units can perform many fused multiply-adds while a kernel waits for HBM transactions. Kernel analysis therefore needs both FLOPs and bytes crossing the chip boundary.
The original FlashAttention paper describes an A100-era gap between aggregate on-chip SRAM and HBM capacity and bandwidth.[1] Exact values depend on the GPU and the available per-kernel budget. Treat those paper-era values as an explanation of the design pressure, not as a current hardware specification.
| Intermediate | Shape for one head | Baseline lifetime | FlashAttention lifetime |
|---|---|---|---|
| Query, key, value | each | Read from HBM as needed | Streamed into SRAM tiles |
| Scores | Written and reread in HBM | One tile, then discarded | |
| Probabilities | Often written and reread | Never materialized globally | |
| Output | Written to HBM | Written once after tile loop | |
| Row max and normalizer | each | Optional saved buffers | Compact state, saved for backward |
The asymptotic score calculation stays FLOPs. The memory that must remain live for intermediate scores changes from quadratic to linear in sequence length. That distinction is why "memory efficient" doesn't mean "less attention math."
Suppose a training batch has 8 sequences, 32 heads, sequence length 8,192, and FP16 scores. One dense score tensor would need:
That is only scores. Probabilities, model weights, activations, gradients, and allocator fragmentation still need space. The number is a sizing exercise, not a benchmark, and it assumes an implementation actually materializes that tensor.
1batch, heads, sequence, bytes_per_value = 8, 32, 8192, 2
2score_bytes = batch * heads * sequence * sequence * bytes_per_value
3print(f"score values: {batch * heads * sequence * sequence:,}")
4print(f"binary size: {score_bytes / 1024**3:.2f} GiB")Expected output is score values: 17,179,869,184 and binary size: 32.00 GiB. A fused kernel can avoid this allocation, but the pair count still matters for compute and data movement.
FlashAttention chooses a query tile and walks over key/value tiles . A tile is small enough for on-chip SRAM. The kernel computes there, applies mask and scale, updates a row-wise softmax state, and discards before loading the next tile.
The control flow is easier to see than the CUDA code. Each row of carries three pieces of state:
m_i and \ell_i are running softmax statistics from which is formed. They are small vectors, one entry per query row, not matrices.
The diagram's last edge is a loop in the kernel: the next tile reuses the same query tile and softmax state. Only after the final key tile does the kernel write the output row to HBM.
Normal softmax appears to need every score before it can compute a denominator. The online algorithm instead merges two partial summaries. Let a previous tile have state and the new score tile have row scores . Compute:
Then shift both old and new contributions to the same maximum:
For values , update the numerator and normalize at the end:
The exponential shift keeps exponents at or below one. When a later tile contains a larger score, the old accumulator is rescaled exactly instead of being thrown away. Milakov and Gimelshein describe this streaming normalizer, which FlashAttention uses as part of its tiled exact computation.[2]
Use one query row and four keys split into two tiles. The first tile has scores and values . The second has scores and values . We omit the common scale in this arithmetic so the state update stays readable.
For tile one, and . Its numerator is:
So . That output is provisional because key tile two hasn't been seen.
Tile two contains a larger score, so . The old state must be rescaled by :
The new numerator is:
The final result is . Computing ordinary softmax over all four scores gives the same values up to rounding. No score row had to survive between tiles.
The official repository keeps Python wrappers and GPU kernels in separate layers.[3] The public flash_attn_func interface accepts tensors shaped like (batch, sequence, heads, head_dim), normalizes optional arguments, and calls a compiled forward operator. The wrapper also exposes variable-length and key/value-cache paths.
The CUDA path registers custom PyTorch operators, returning output, row-wise softmax LSE, an optional probability or dropout-mask buffer, and RNG state. The LSE tensor is compact: it has one value per query row and head. It gives backward a stable summary without saving every probability.
The source has a ROCm branch selected by FLASH_ATTENTION_TRITON_AMD_ENABLE. On CUDA, the wrapper imports the compiled flash_attn_2_cuda extension. On ROCm, it can route to an AIter Triton implementation. This dispatch is part of the product: a fast algorithm still needs a kernel matching the device, dtype, head dimension, and layout.
1def choose_path(device: str, head_dim: int, use_rocm_triton: bool) -> str:
2 if device == "rocm" and use_rocm_triton:
3 return "aiter-triton"
4 if device == "cuda" and head_dim <= 256:
5 return "flash-attn-cuda"
6 return "framework-fallback"
7
8print(choose_path("cuda", 128, False))
9print(choose_path("cuda", 320, False))The expected output is flash-attn-cuda followed by framework-fallback. The snippet models a guard, not the full runtime selector. Real dispatch also checks GPU capability, dtype, sequence lengths, masks, dropout, and compiled extension availability.
Training needs gradients for , , and . A materializing implementation can save the full probability matrix for backward, but that repeats the quadratic memory problem. FlashAttention saves , , , output , row-wise LSE, and any required random-number state, then recomputes score and probability tiles during backward.
For each tile, backward reconstructs the same shifted probabilities in SRAM, uses incoming gradient , and accumulates , , and . Recomputing scores costs additional FLOPs and HBM reads, but it avoids storing and loading a full matrix. This is a classic memory-compute trade-off, not a free speedup.
FlashAttention-2 adds work partitioning changes so more thread blocks can process long sequences and less serial work sits in each block.[4] The repository exposes a deterministic option for backward. Deterministic backward uses more memory and can be slower, while forward remains deterministic according to the project interface.[3]
FlashAttention isn't limited to unmasked full-sequence attention. The same tile loop can skip or alter score elements before softmax.
Autoregressive decoders can't read future tokens. A causal tile masks positions above the diagonal, treating them as before softmax. The wrapper aligns causal masks for unequal query and key lengths, which matters during decode when a short query attends to a long prefix.
The window_size=(left,right) argument limits each query to nearby keys. Local attention reduces the number of allowed scores for long contexts, but it isn't automatically faster for every shape. Tile occupancy, mask shape, and GPU generation decide whether the work reduction outweighs branch and scheduling overhead.
Training batches often contain sequences with different lengths. The varlen interface uses cumulative sequence lengths (cu_seqlens_q and cu_seqlens_k) so padding doesn't force every row to the longest sequence. This changes indexing and launch metadata, not the online-softmax invariant.
Decode systems may store K/V in fixed-size physical pages and map each request through a block table. The implementation accepts a page table for this mode. FlashAttention handles the attention math and page lookup; a serving engine still owns allocation, eviction, and request scheduling. Don't confuse the kernel's paged path with a complete KV-cache manager.
Multi-query attention (MQA) shares one K/V head across query heads. Grouped-query attention (GQA) shares K/V heads in groups. Passing fewer K/V heads than Q heads reduces cache traffic, provided the query-head count is divisible by the K/V-head count. This is an architectural choice with quality and bandwidth trade-offs; FlashAttention's interface supports it but doesn't choose the model's head layout.
| Mode | What changes | Main production question |
|---|---|---|
| Causal | Mask future keys | Does mask alignment match query and cache lengths? |
| Local | Keep a bounded window | Does the window preserve task quality and tile occupancy? |
| Varlen | Pack unequal sequences | Are cumulative lengths and max lengths correct? |
| Paged KV | Indirect K/V pages | Who owns page allocation and eviction? |
| MQA/GQA | Fewer K/V heads | Is cache bandwidth saved without unacceptable quality loss? |
Each release combines an algorithmic idea with a hardware target. Paper benchmark numbers are snapshots from specific GPU, dtype, sequence, and baseline conditions. They shouldn't be copied into a service SLO.
| Generation | Primary source | Hardware or software focus | Core change |
|---|---|---|---|
| FA1 | FlashAttention paper, NeurIPS 2022[1] | A100-era CUDA | IO-aware tiling and exact online softmax avoid materialized intermediates. |
| FA2 | FlashAttention-2 paper, ICLR 2024[4] | A100/Ampere CUDA | Better parallelism, work partitioning, and lower non-matmul overhead. |
| FA3 | FlashAttention-3 paper, 2024[5] | Hopper H100/H800 | Asynchrony, warp specialization, and low-precision paths match Hopper features. |
| FA4 | FlashAttention-4 paper, 2026[6] | Hopper and Blackwell | Algorithm and kernel pipelining co-design for asymmetric hardware scaling. |
The repository README describes FA3 as a beta path with H100/H800 and CUDA requirements, while FA4 is exposed through a CuTeDSL package for Hopper and Blackwell.[3] Read those release notes as compatibility guidance. A paper name in a citation doesn't mean every installed wheel has that generation enabled.
The team is best understood as an open research-and-engineering project. The Dao-AILab repository carries the implementation, tests, build scripts, and issue history; the papers identify Tri Dao and collaborators as authors. Downstream framework maintainers contribute integrations and backend work. That split explains why paper ideas, compiled kernels, and framework dispatch evolve on different schedules.
| Field | Current project fact |
|---|---|
| Origin | The Stanford-centered FlashAttention paper names Tri Dao, Daniel Y. Fu, Stefano Ermon, Atri Rudra, and Christopher Ré.[1] |
| Stewardship | Dao AI Lab maintains the public repository, and its root author file names Tri Dao.[3][7] |
| Contributor model | The project uses maintainer-led GitHub issues and pull requests. Framework teams contribute separate dispatch and integration work downstream.[3] |
| Source license | BSD-3-Clause for the pinned source snapshot.[8] |
| Commercial boundary | FlashAttention is a kernel implementation, not a hosted model service. A framework, API, or product that embeds it keeps its own support and licensing terms. |
| Asset boundary | The repository doesn't license model weights or datasets, and its CUDA or ROCm dependencies retain their own notices. |
PyTorch exposes scaled dot-product attention (SDPA), which can select among flash, memory-efficient, and math implementations based on input and device constraints.[9] A model library should call the framework API when it wants portability and let the dispatcher pick a valid backend. It should call the Dao-AILab package when it needs a feature or kernel version that the framework path doesn't provide.
The repository usage page lists integrations or adoption examples in PyTorch, Transformers, DeepSpeed, Megatron-LM, diffusion systems, and protein-structure models.[3] Those examples cover two broad workloads:
Use the paper or a local benchmark for any speed claim. The measured gain depends on sequence length, batch shape, head dimension, dtype, dropout, mask, GPU generation, compiler version, and baseline backend. "Flash" is not a guarantee that a tiny or unusual tensor wins.
| Dimension | Strength | Cost or limit |
|---|---|---|
| Exactness | Same attention definition, aside from normal floating-point ordering | Doesn't reduce quadratic score FLOPs for dense attention |
| Memory | No global score/probability allocation; linear-size saved state | Tile workspace and output still consume memory |
| Training | Recompute can unlock longer context or larger batch | Backward does extra work and deterministic mode may cost more |
| Hardware | Specialized kernels exploit SRAM, tensor cores, and async pipelines | Build and dispatch are GPU, CUDA/ROCm, dtype, and shape specific |
| Features | Causal, local, varlen, paged, MQA, and GQA paths exist | Each feature combination has its own support and performance envelope |
| Ecosystem | PyTorch and model libraries can dispatch to compatible kernels | A framework may select another backend or fall back to math |
The most important weakness is an operational one: a kernel can be correct but still be the wrong choice for a workload. Small batches, unsupported head dimensions, CPU or Apple MPS runs, unusual masks, and missing compiled extensions all need explicit fallback behavior.
The following pure-Python check compares a full stable softmax with a two-tile streaming implementation. It uses one query row and scalar values so every intermediate can be inspected. This is a teaching model, not a GPU benchmark.
1import math
2
3scores = [1.0, 2.0, 3.0, 0.0]
4values = [10.0, 0.0, 5.0, 9.0]
5
6def full(scores, values):
7 m = max(scores)
8 weights = [math.exp(x - m) for x in scores]
9 total = sum(weights)
10 return sum(w * v for w, v in zip(weights, values)) / total
11
12def tiled(tile_scores, tile_values):
13 m = float("-inf")
14 total = 0.0
15 numerator = 0.0
16 for current_scores, current_values in zip(tile_scores, tile_values):
17 tile_max = max(current_scores)
18 new_m = max(m, tile_max)
19 old_scale = 0.0 if m == float("-inf") else math.exp(m - new_m)
20 tile_weights = [math.exp(x - new_m) for x in current_scores]
21 total = old_scale * total + sum(tile_weights)
22 numerator = old_scale * numerator + sum(w * v for w, v in zip(tile_weights, current_values))
23 m = new_m
24 return numerator / total
25
26print(f"full: {full(scores, values):.6f}")
27print(f"tiled: {tiled([[1.0, 2.0], [3.0, 0.0]], [[10.0, 0.0], [5.0, 9.0]]):.6f}")1full: 4.379542
2tiled: 4.379542Both lines print full: 4.379542 and tiled: 4.379542 to six decimals. Try moving score 3.0 into the first tile. The result should stay the same, which checks that state merging is independent of the tile boundary.
Before enabling a FlashAttention backend, record the exact workload and hardware:
PyTorch's SDPA documentation is a good first check for backend eligibility.[9] The repository tests are the next check for a direct package integration. A benchmark should compare the backend that production would otherwise choose, not a deliberately slow baseline.
If a release upgrades CUDA, PyTorch, GPU generation, or FlashAttention, rerun correctness and performance checks. Keep a fallback path in the service. A request should fail over to a framework implementation or a safe math path with an observable metric, rather than silently return wrong output or crash because a niche shape missed a kernel specialization.
⚠️ Common mistake: Treating "no quadratic allocation" as "constant-time attention." FlashAttention still computes dense query-key pairs, and long-context decode can remain dominated by KV-cache reads and scheduler effects.
Answer every question, then check your score. Score 75% or higher to mark this lesson complete.
6 questions remaining.
FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness.
Dao, T., Fu, D. Y., Ermon, S., Rudra, A., & Ré, C. · 2022 · NeurIPS 2022
Online normalizer calculation for softmax.
Milakov, M. & Gimelshein, N. · 2018
FlashAttention
Dao-AILab · 2026
FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning.
Dao, T. · 2023 · ICLR 2024
FlashAttention-3: Fast and Accurate Attention with Asynchrony and Low-precision.
Shah, J., Bikshandi, G., Zhang, Y., Thakkar, V., Ramani, P., & Dao, T. · 2024
FlashAttention-4: Algorithm and Kernel Pipelining Co-Design for Asymmetric Hardware Scaling
Ted Zadouri, Markus Hoehnerbach, Jay Shah, Timmy Liu, Vijay Thakkar, Tri Dao · 2026 · arXiv
FlashAttention Authors
Dao AI Lab · 2026
FlashAttention BSD 3-Clause License
Dao AI Lab · 2026
torch.nn.functional.scaled_dot_product_attention
PyTorch Contributors · 2026
Questions and insights from fellow learners.