Learn: The Quadratic Bottleneck & IO-Aware Attention
What you're learning
The one idea that made long context tractable: naive attention is memory-bound, not compute-bound — it wastes its time moving the score matrix through HBM, not computing it. The fix isn’t fewer FLOPs, it’s fewer bytes. By the end you’ll be able to derive the online (streaming) softmax recurrence — the mathematical heart of FlashAttention — and explain why it drops HBM traffic from to while computing exactly the same output.
First lesson of Tier 2. Assumes Tier 1: 01-scaled-dot-product-attention (SDPA, the wall, FLOP counting), 02-multi-head-attention-and-the-block, and 03-positional-information. Vocabulary follows the attention-glossary.
This is the algorithm angle, not the kernel angle
This lesson is about why attention is memory-bound and the math that fixes it. The hardware/kernel treatment — warp specialization, TMA/wgmma overlap, FA-2/3/4 generations, roofline analysis — lives in 06-memory-wall-and-flashattention. We deliberately don’t repeat it here; cross-links point you there for the systems details.
1. Learning map
graph TD A["Recap: attention core is<br/>O(s²·d) FLOPs (lesson 01)"] --> B["But wall-clock ≠ FLOPs:<br/>it materializes s×s S,P in HBM"] B --> C["Arithmetic intensity ~ d,<br/>far below the ridge point<br/>⇒ MEMORY-BOUND"] C --> D["Realization: fix bytes moved,<br/>not FLOPs done (IO-awareness)"] D --> E["Obstacle: softmax needs a<br/>GLOBAL row normalizer"] E --> F["Online softmax:<br/>running m, ℓ, o + rescale α"] F --> G["Compute row output<br/>block-by-block, never form s×s"] G --> H["HBM traffic: O(s²) → O(s)<br/>same FLOPs, long context unlocked"] H --> I["Kernel/hardware details:<br/>see GPU lesson 06"] style B fill:#a44,color:#fff style C fill:#a44,color:#fff style F fill:#4a4,color:#fff style H fill:#44a,color:#fff
2. Intuition: attention is memory-bound, not compute-bound
Lesson 01 left you with the headline: the attention core costs FLOPs — quadratic in sequence length . Double the context, quadruple the compute. That’s the wall, and it’s real. But it’s only half the story, and not even the binding half on modern hardware.
Here’s the realization. Count what the naive implementation actually does to memory when it runs as three library calls (S = Q@K.T/√d, P = softmax(S), O = P@V):
- — a matmul that reads () and writes the full score matrix to HBM.
- — an elementwise+reduction kernel that reads back (), does a handful of FLOPs per element, and writes back ().
- — a matmul that reads () and , writes ().
The matrix is round-tripped through HBM roughly four times (write , read , write , read ) — bytes moved — while carrying only FLOPs split across separate kernels. The softmax in the middle is the smoking gun: it’s pure elementwise work with arithmetic intensity FLOP/byte, so it runs at HBM bandwidth and does nothing but shuttle the score matrix. Its entire runtime is “time to read + time to write .”
So the attention block, as launched, is bottlenecked by moving the score matrix, not by the tensor cores computing it. Its effective arithmetic intensity collapses toward (tens to ~128) — far below the hardware’s ridge point (~296 FLOP/byte on an H100).1 Attention has plenty of FLOPs; it just can’t feed them because it’s starved on bandwidth. That’s what “memory-bound” means.2
The one-sentence version
Naive attention is slow not because it does too much math, but because it writes an matrix to slow memory only to immediately read it back. The win is to never materialize that matrix in HBM — same FLOPs, a fraction of the bytes.
There’s a second, equally fatal consequence of materializing : capacity. Holding an matrix costs HBM storage. At that’s ~128 MiB per head in BF16; at it’s ~32 GiB2 — larger than an entire H100’s 80 GB if you keep a few around. That storage wall is, quite literally, what capped context length before 2022.
The fix is IO-awareness: design the algorithm around minimizing HBM reads/writes, the way tiled GEMM is designed around SRAM reuse. Compute attention block-by-block, keeping each block’s scores on-chip, producing, consuming, and discarding them before they ever reach HBM. The obstacle is softmax: its normalizer is a sum over the entire row, which seems to demand seeing the whole row at once. Section 3 shows why it doesn’t.
Quiz (retrieval): Attention genuinely does FLOPs — a lot of compute. Why is it nonetheless memory-bound in practice, and which sub-step is the culprit?
Answer
Because the FLOPs and the bytes don’t live in the same kernel. The FLOPs are in the two matmuls, but running the op as three separate kernels forces the score/probability matrix through HBM four times (write , read , write , read ) — that’s traffic with only a handful of FLOPs attached per byte. The effective arithmetic intensity of the block collapses toward , far below the ridge point, so wall-clock is set by HBM bandwidth, not tensor-core throughput. The softmax is the culprit: it’s an elementwise/reduction kernel () that both sits on the memory roof and forces the matrix to be materialized so a later kernel can read it back. (Note lower head dim makes it worse — intensity shrinks.)
3. The math: online (streaming) softmax
We want to compute, for a single query row with scores (here ) and value vectors , the softmax-weighted output
where subtracting the row max is the standard safe-softmax trick to avoid overflow of (every exponent becomes ). The obstacle for streaming: both and the denominator depend on the whole row, but we want to sweep over blocks of the key/value sequence, seeing only one block at a time and keeping off HBM.
The trick (Milakov & Gimelshein, 2018)3 is to maintain a small running state and correct it whenever a later block reveals a larger max. Track three quantities after processing blocks :
- — running max of all scores seen so far;
- — running denominator, referred to the current max ;
- — running unnormalized output accumulator, also referred to .
Initialize . For block let . The recurrence is:
After the last block, normalize once: .
Why the correction factor ?
The old accumulators were built with exponentials referred to the old max . When the max jumps to , every previously stored term must be re-referenced to the new max. For a single term:
Multiplying the whole running sum by the scalar applies this correction to every stored term at once — that’s why a single multiply fixes the entire accumulator. If the new block doesn’t raise the max (), then and nothing changes. And because , the exponent is , so — a shrink factor, never an overflow.
The invariant that makes it exact
At every step the recurrence maintains the exact partial softmax over the prefix seen so far:
By induction (base case trivial; the inductive step is exactly the rescale algebra above), after the final block equals full softmax attention bit-for-bit up to floating-point associativity. FlashAttention is exact, not an approximation — you can drop it into any model with zero accuracy change. That is the crucial selling point that separates it from the sparse/linear-attention approximations.
Because and are just a running scalar and a running -vector, each block’s scores can be thrown away the instant they update the state — they never need to be stored. That is the mechanical reason the matrix never reaches HBM. The state is per query row, independent of .
Quiz (derive it): Justify the rescale factor from first principles. What breaks if you omit it, and why can never overflow?
Answer
Justification. After block the accumulators store exponentials referred to the then-current max: and likewise . When block reveals a larger max, the running max updates to , and every stored term must be re-referenced: . Multiplying the whole accumulator by does this to all stored terms simultaneously, restoring the invariant ; then the new block’s terms (already at the new reference) are added.
If omitted: old terms stay scaled by while new terms use — you’d sum exponentials with inconsistent reference points, so numerator and denominator no longer correspond and is wrong (unless the max happened never to change).
No overflow: . Every too, since . Safe-softmax stability is preserved, now incrementally.
Quiz (retrieval): Why does maintaining let you avoid ever writing the score matrix to HBM? How large is the running state per query row?
Answer
Softmax’s only global dependency on the row is through the max and the denominator . The online recurrence carries both as running scalars, plus a running unnormalized output vector . Once a block’s scores have been folded into , they carry no further information and can be discarded immediately — so a block of scores is produced, consumed, and thrown away on-chip, never written out. The state is one scalar , one scalar , and one -vector : per query row, independent of sequence length . That’s the whole reason the matrix never needs to exist in HBM.
4. Illustration: tiled block-by-block attention
For each query block , stream over the key/value blocks , keeping only the running state on-chip. The scores are never assembled.
flowchart LR subgraph HBM["HBM (slow, large) — only linear-in-s traffic crosses here"] Qi["Q_i block"] KV["K_1..K_T, V_1..V_T"] Oi["O_i (written once)"] end subgraph SRAM["SRAM / on-chip — loop t = 1..T"] S1["1. score tile<br/>S_it = Q_i K_tᵀ/√d<br/>(stays on-chip, never to HBM)"] S2["2. update state<br/>m ← max(m, m̃_t)<br/>α = e^(m_old−m)<br/>ℓ ← αℓ + Σ e^(S−m)<br/>o ← αo + Σ e^(S−m) V_t"] S3["3. after last block<br/>O_i = o / ℓ"] end Qi --> S1 KV --> S1 S1 --> S2 S2 -->|"next K/V block"| S1 S2 --> S3 S3 --> Oi
Figure 1 — For each query block, FlashAttention streams over the K/V blocks. Each score tile is formed, consumed to update the running state, and discarded — all on-chip. Only (in) and (out) cross HBM. The matrix is never materialized.
Before vs. after, in HBM traffic:
Figure 2 — Naive attention’s HBM traffic is dominated by four round-trips of the score/probability matrix (quadratic in ). The tiled, online-softmax algorithm never materializes those matrices, leaving only the linear-in- traffic of reading and writing . This collapse is the entire win.
The tiling reorders the softmax sum — associativity, not identity
“Bit-for-bit exact” in the invariant means up to floating-point associativity. Streaming accumulates the exponential sums in a different order than a single-pass softmax, so results can differ in the last bit or two. This is the ordinary non-associativity of float addition (the same caveat as any tiled reduction), not an approximation of attention — accuracy is unaffected in practice.
5. FLOPs & memory: same math, a fraction of the bytes
The crucial accounting, side by side. Let = batch, = sequence length, = head dim.
| Naive (3 kernels) | IO-aware / tiled | |
|---|---|---|
| Attention-core FLOPs | (same — forward) | |
| HBM traffic | (four round-trips) | (Q,K,V in, O out) |
| Peak HBM footprint of | running state / row | |
| Bound by | HBM bandwidth (memory-bound) | near tensor-core peak (compute-bound) |
The headline: the FLOP count does not change. Online softmax is not a cheaper algorithm in arithmetic — it’s the same multiply-adds. What changes is the bytes moved: HBM traffic drops from quadratic to linear in , and the storage footprint vanishes. Because the op was memory-bound, cutting the bytes is what cuts the wall-clock — the original paper measured ~9× fewer HBM accesses and 2–4× wall-clock speedup on GPT-2, computing identical outputs.2
Why this unlocked long context — the scaling, not the constant
Naive traffic is ; tiled traffic is . So the advantage grows with context: double and naive’s traffic 4בs while the tiled version’s only 2בs. Equally important, the storage of the score matrix — the capacity wall that once OOM’d long sequences — is gone. Same FLOPs, linear bytes: that combination is what turned 128k+ token context from impossible to routine.
"Fewer FLOPs" is the wrong instinct here
Faced with the wall, the tempting move is to approximate attention to cut FLOPs (sparse, low-rank, linear attention). Those change the math and can cost accuracy. FlashAttention shows a different, often better lever: keep the exact math, cut the bytes. On a memory-bound op, bytes — not FLOPs — are what you’re actually paying for. (The backward pass even adds FLOPs via recomputation and still wins — see 06-memory-wall-and-flashattention §5.3.)
For the hardware mechanics that turn this IO complexity into realized TFLOP/s — SRAM tiling specifics, the exact IO bound, warp specialization, and the FA-2/FA-3/FA-4 generations — see the systems lesson 06-memory-wall-and-flashattention. FlashAttention-24 in particular re-derives the recurrence to defer per-block rescaling to a single final divide, cutting non-matmul work; that’s an optimization of the exact recurrence in §3.
Try it: watch the two curves diverge
Open the interactive Attention FLOPs & KV-cache calculator and set , one head, . Sweep from 2k → 8k → 32k → 128k. Watch two things: (1) prefill FLOPs grow ~quadratically — that cost is the same for naive and IO-aware attention; (2) the score-matrix footprint (the thing naive attention writes to HBM) explode as , while the IO-aware version holds only state per row. The gap between “FLOPs you must do” and “bytes you must move” is exactly the memory-bound story of §2.
Quiz (retrieval): Online softmax computes the identical attention output. So where does the speedup come from, and by what factor does HBM traffic grow when you 4× the context?
Answer
The speedup is not from doing less arithmetic — the forward FLOP count is unchanged at . It comes entirely from moving fewer bytes: the score/probability matrix is never written to or read from HBM, so mandatory traffic drops from to . Since the op was memory-bound, less traffic ⇒ less wall-clock (~9× fewer HBM accesses, 2–4× faster on GPT-22). On 4×-ing the context: naive HBM traffic scales as , so 16×; tiled traffic scales as , so 4×. The gap between them widens with — which is exactly why long context became feasible.
6. Primary source & where to go deeper
- FlashAttention (primary): Dao, Fu, Ermon, Rudra, Ré, “FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness” — arXiv:2205.14135 (NeurIPS 2022).2 Read §3 (the tiling + online-softmax algorithm) and the IO-complexity analysis. This is the paper that framed attention as an IO problem.
- Online softmax (the recurrence itself): Milakov & Gimelshein, “Online normalizer calculation for softmax” — arXiv:1805.02867 (2018).3 The single-pass running-max + running-denominator trick, four years before FlashAttention adapted it.
- Kernel/hardware angle (companion): 06-memory-wall-and-flashattention — roofline, SRAM tiling, recomputation backward, and FA-2/3/4.
Ask your teacher
Stuck on the induction that makes online softmax exact, the role of , or the FLOPs-vs-bytes accounting? Ask — I’m your teacher for this course and can go deeper on any piece, hand-run the recurrence on a new example, or build you a harder problem.
7. Practice
Problem 1: HBM traffic, naive vs. IO-aware
One head, , , BF16 (2 bytes). Ignore causal masking. (a) Estimate naive attention’s HBM traffic from the score-matrix round-trips. (b) Estimate the IO-aware ideal (Q,K,V read once, O written once). (c) Give the ratio and state how it scales with .
Solution.
(a) The matrix is written (), read (softmax), written (), read () — 4 round-trips: bytes MiB. Input/output traffic bytes is ~1.6% — negligible beside the term.
(b) No traffic; read once, write once: bytes MiB. (Real tiling adds bounded K/V re-reads, giving the paper’s ; still far below .)
(c) less traffic here. Scaling: naive is , IO-aware is — so the ratio grows ; the advantage widens with context. (And the naive 512 MiB is 512 MiB you must hold — the old capacity wall.)
Problem 2: hand-run the online-softmax recurrence
A query attends to 4 keys in two blocks of two. Post-scaled scores: , . Scalar values . Run the recurrence tracking and verify it equals the direct softmax-weighted sum.
Solution. Init: .
Block 1 (): , . . .
Block 2 (): , . Rescale then add (for ) and (for ): . .
Normalize: .
Direct check (global max 4): weights , sum ✓. Weighted value sum ✓. ✓. Block 2’s larger max triggered the rescale that corrected block 1’s contribution.
8. What’s next
- 05-mqa-gqa-mla (next) — the other attention memory problem. FlashAttention fixed training/prefill HBM traffic; at autoregressive inference the binding constraint is the KV cache (linear in but read every step). MQA/GQA/MLA shrink it by sharing or compressing keys/values. Different bottleneck, same “move fewer bytes” philosophy.
- 03-positional-information (back) — revisit how positions enter attention. Position schemes (especially RoPE) must be applied inside the fused kernel, block-by-block, since the scores never materialize — a concrete constraint the IO-aware algorithm imposes on everything upstream.
Further threads: linear attention & SSMs (change the algorithm to escape entirely, trading exactness for asymptotics — the opposite bet to FlashAttention), and the kernel-level continuation in 06-memory-wall-and-flashattention.
Reference: attention-glossary | Topic: attention-mechanisms | Filed: 2026-09-10
References
Footnotes
-
NVIDIA H100 Tensor Core GPU — BF16 arithmetic-intensity ridge point ≈ 296 FLOP/byte (≈ 989 TFLOP/s tensor-core ÷ ~3.35 TB/s HBM3). See NVIDIA H100 architecture whitepaper; the ridge-point figure and its use in the attention roofline are detailed in 06-memory-wall-and-flashattention. ↩
-
[established] Tri Dao, Daniel Y. Fu, Stefano Ermon, Atri Rudra, Christopher Ré, “FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness” — arXiv:2205.14135 (NeurIPS 2022). arxiv.org/abs/2205.14135. Source of the IO-aware framing, tiling + online-softmax algorithm, the exactness result, the ~9× fewer HBM accesses and 2–4× wall-clock speedup on GPT-2, and the storage/capacity wall. ↩ ↩2 ↩3 ↩4 ↩5
-
[established] Maxim Milakov, Natalia Gimelshein, “Online normalizer calculation for softmax” — arXiv:1805.02867 (2018). arxiv.org/abs/1805.02867. The single-pass streaming-softmax recurrence (running max + running denominator with rescale) that FlashAttention adapts. ↩ ↩2
-
[established] Tri Dao, “FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning” — arXiv:2307.08691 (ICLR 2024). arxiv.org/abs/2307.08691. Restructures the recurrence to defer per-block output rescaling to a single final normalization, cutting non-matmul work; work partitioning / warp scheduling covered in the GPU-systems lesson. ↩