S115 · Efficient Attention — Flash, Sparse, Linear
The three families of tricks that broke attention's O(N²) memory wall — Flash-Attention's tile-based recomputation, sparse patterns (BigBird/Longformer), and linear-time approximations. What each buys you, what it costs, and which one to reach for.
🎯 Explain, choose between, and enable the three families of efficient attention (Flash / Sparse / Linear) for a given context length and model.
Why this session exists
Standard attention is O(N²) in memory and compute — double your context length, quadruple your VRAM. That single fact capped every LLM at 2K tokens until 2022. Then Tri Dao shipped Flash-Attention, which is still exact O(N²) FLOPs but only O(N) memory, and the world moved to 8K/32K/128K contexts almost overnight. This session teaches you the three families of "make attention cheaper" tricks, when each matters, and how to actually turn them on in PyTorch / HuggingFace.
- Explain why vanilla attention is O(N²) in both time and memory and where the bottleneck lives (HBM ↔ SRAM traffic).
- Describe Flash-Attention's tile-based recomputation trick in one paragraph.
- Distinguish sparse (BigBird / Longformer / SW-attention) from linear (Performer / Linformer / RWKV) approximations.
- Enable Flash-Attention 2 in PyTorch (`SDPA` backend or `flash-attn` package) and verify it's active.
- Pick an attention family for a new problem — long-context RAG, streaming inference, edge deployment — with justification.
Prerequisites
- S110 · Attention & Transformers — the QK^T V pattern.
- S112 · BERT/GPT/T5 — you need the encoder/decoder mask shapes.
- Rough sense of GPU memory hierarchy (HBM vs SRAM vs registers).
(a) Intuition · 5 min
Imagine a meeting where every person must talk one-on-one with every other person once. Ten people = 45 conversations. A hundred people = 4,950. A thousand = ~500,000. The meeting doesn't just get longer — it gets quadratically longer.
The room also gets crowded: at any moment you have to remember what everyone has said (N conversation logs, each of length ~N). Memory grows quadratically too.
Attention has the same shape. Every token attends to every other token, so you compute an N×N similarity matrix, softmax it row-wise, and multiply by V. For N=8K that's a 256MB matrix in FP32 per attention head. On a 32-head model that's 8GB of intermediates — before you've done a single matrix multiply.
The three families of efficient attention are three different bets on how to shrink that receipt: Flash keeps the O(N²) FLOPs but hides them in fast SRAM. Sparse drops most cells to zero by construction. Linear reformulates the math so it never builds the N×N matrix at all.
- Flash-Attention (exact) — same math, streamed through GPU SRAM in tiles; O(N²) FLOPs but O(N) HBM memory. 2–8× wall-clock speedup. Default in every 2024+ LLM.
- Sparse attention (approximate) — only attend to a fixed pattern (window + global tokens): Longformer, BigBird, sliding-window. O(N·w) where w is the window. Great for 32K–1M contexts on encoder tasks.
- Linear attention (approximate) — reformulate softmax(QK^T)V using kernel tricks so cost is O(N) — Performer, Linformer, RWKV, Mamba (state-space). Cheap forever, but usually a small quality hit vs exact.
Timeline of the attention-efficiency race
- 2017Vanilla attentionVaswani et al. — quadratic in memory and compute. Cap = 512 tokens for BERT.
- 2020Sparse patternsLongformer (window + global), BigBird (sparse random + window). First real ≥4K-context models.
- 2020Linear attention wavePerformer, Linformer, Reformer. Beautiful math, mixed real-world results — most got dropped by 2023.
- 2022FlashAttention v1 · Tri DaoTile-based IO-aware exact attention. 2–4× faster. Suddenly 8K–32K contexts are cheap.
- 2023FlashAttention v2Better parallelism. Another 2× speedup. Now the default on H100.
- 2024State-space renaissanceMamba / Mamba-2 revive linear-cost sequence models with SSM math. RWKV also gains ground.
- 2024FlashAttention v3Uses Hopper (H100) FP8 asynchrony. 1.5–2× over v2. Still exact.
(b) Visual walkthrough · 15 min
The GPU memory hierarchy — the whole story lives here
Vanilla attention lives on HBM: it materialises the whole N×N score matrix, softmaxes it, writes it back, then reads it again for the V multiply. Each of those reads/writes crosses the 2 TB/s HBM boundary — and that traffic dominates the runtime.
Flash-Attention's trick in one picture
Split Q into blocks of ~64–128 rows; K and V into ~64-column blocks. A tile of Q × K fits in SRAM.
For each K/V tile, compute the partial attention weights and partial output for this Q tile — all in SRAM.
Track a running max and sum of exponentials as new tiles arrive, so you never need the full row at once to normalise. This is the mathematical trick that makes tiling exact.
The N×N score matrix never touches HBM. Memory drops from O(N²) to O(N). Wall clock drops 2–8× because HBM traffic was the bottleneck.
Recompute the tiles instead of storing them. Trades a small amount of extra FLOPs for enormous memory savings.
The three families side by side
Exact · 2–8× faster · O(N) memory
- Bit-identical to vanilla attention
- O(N²) FLOPs, O(N) HBM memory
- Available via PyTorch SDPA and flash-attn package
- Best default for any 2024+ LLM
Approximate · O(N·w) · window + global
- Longformer: sliding window + a few global tokens
- BigBird: window + random + global
- Great for encoder tasks with 16K–1M context
- Not fluent in generation — GPT variants are rare
Approximate · O(N) · kernel-trick math
- Performer / Linformer / Reformer historically
- Mamba / RWKV / SSMs today
- Small quality gap on standard benchmarks
- Winning again in 2024–26 for streaming inference
Which to reach for
What the numbers actually look like
Cost of a 32K-token forward pass on a 7B model (rough)
"FlashAttention is an approximation. It's faster because it skips some attention computation, so there must be a small accuracy cost."
FlashAttention is numerically exact — same result as the naive implementation, up to floating-point reassociation. It is faster because it never materialises the T×T score matrix in HBM: it tiles the computation, keeps tiles in on-chip SRAM, and uses an online-softmax recurrence to combine them. The win is memory-traffic, not arithmetic.
Because it sits in the same conversation as sparse and linear attention, which are approximations, and because "faster" in ML almost always means "cheaper approximation". The deeper reason the myth is sticky is a wrong mental model of what makes GPU code slow: people assume FLOPs. Attention is memory-bandwidth-bound, and reading/writing a T² matrix dominates the cost long before the multiplies do.
Confirm exactness, and see where the memory actually goes:
import torch, torch.nn.functional as F
B, H, T, D = 1, 4, 1024, 64
q, k, v = (torch.randn(B, H, T, D, device='cuda', dtype=torch.float16) for _ in range(3))
naive = torch.softmax(q @ k.transpose(-1,-2) / D**0.5, -1) @ v
with torch.backends.cuda.sdp_kernel(enable_flash=True, enable_math=False, enable_mem_efficient=False):
flash = F.scaled_dot_product_attention(q, k, v)
print((naive - flash).abs().max().item()) # ~fp16 rounding, not approximation
print('score matrix bytes:', B*H*T*T*2 / 1e6, 'MB') # what flash never writesWhy can softmax be computed in tiles at all? It needs a global normaliser over the whole row — that looks like it forbids streaming.
- 1Softmax must be computed as
ezi−m/Σezj−mwith m the row max, otherwiseezoverflows in fp16 for logits above ~11.forced by · numerical stability requires subtracting the max; this is not optional at half precision - 2Both m and the denominator are reductions over the entire row, so a naive implementation must see all T scores before emitting any output — hence materialise the row.forced by · a global reduction appears to require global visibility
- 3But observe the algebra: if you have a partial max m1 and partial sum s1 over one tile, and then meet a larger max m2, the old sum can be corrected in place by the factor
em1−m2.forced by · rescaling by the exponential of the max difference is exactly what changing the reference point does to every term - 4The same correction factor applies to the accumulated output
Σez−mv, because it is the same linear rescaling of every term in the sum.forced by · the output accumulator is a weighted sum with the same weights, so it rescales identically - 5Therefore one pass over key/value tiles suffices: carry
(m, s, o), and on each new tile update all three with the correction. No score is ever stored beyond its tile.forced by · the recurrence makes the global reduction associative and streamable
Therefore attention memory drops from O(T²) to O(T), exactly, with no approximation — the entire trick is that softmax normalisation is associative under max-rescaling.
And note what this predicts: the speedup should grow with sequence length, since the T² HBM traffic being eliminated grows faster than the O(T²) arithmetic that remains — and at short T the kernel launch overhead may make it no faster at all. It also predicts the backward pass must recompute scores rather than read them (they were never stored), trading a little extra arithmetic for a large memory saving. Both are exactly what the implementation does.
A GPU has a small, extremely fast scratchpad (SRAM, tens of KB per SM) and a large, comparatively slow main memory (HBM). Arithmetic is nearly free; moving bytes between the two is what costs time. Standard attention writes a T×T matrix to HBM and reads it back twice.
FlashAttention restructures the same math so every intermediate stays in SRAM and only Q, K, V and the output ever touch HBM. Nothing is approximated — the data simply never leaves the fast tier.
- Exact methods (FlashAttention, PagedAttention) change memory movement. Approximate methods (sparse, linear, low-rank) change the math. Never confuse the two categories.
- Attention is memory-bandwidth-bound at typical shapes. Optimise bytes moved, not FLOPs.
- At inference, the KV cache — not the score matrix — is the memory problem. It grows linearly with tokens and is the reason PagedAttention and GQA exist.
- Every approximate scheme sacrifices some pair of positions that can no longer interact directly. Ask which pair before adopting it.
Fire this model the moment you see: an OOM whose size scales with sequence length squared · a long-context feature request · low GPU utilisation with high memory traffic · a claim that some attention variant is "10× faster" · a serving system limited by batch size rather than compute.
You need to support 128k-token context. Exact optimised attention, sparse/windowed attention, or retrieval over a short window?
Always take the exact optimisations first — FlashAttention, GQA, paged KV — because they are free: no quality cost, no retraining, no new failure mode. Only after exhausting those should you accept an approximation, and then only against a measurement of what long-range interactions your workload actually uses.
The framing that matters: long context and retrieval are not competitors, they are different cost curves. Attention cost scales with what you put in the window; retrieval cost scales with what you index. Most production systems end up using both, with retrieval choosing what earns a place in the window.
(c) Hands-on · 25 min
Measure vanilla vs Flash-Attention on your GPU, then load a HuggingFace model with each backend and time inference. This is the "make it real" exercise.
"""attention_bench.py — measure vanilla vs Flash-Attention.
Requires torch >= 2.0 (SDPA), a CUDA GPU with compute capability >= 8.0
(Ampere/Hopper) for the Flash backend. Falls back to memory-efficient
on older GPUs.
"""
from __future__ import annotations
import time
import torch
import torch.nn.functional as F
from torch.nn.attention import SDPBackend, sdpa_kernel
assert torch.cuda.is_available(), "This bench needs a CUDA GPU."
DEVICE, DTYPE = "cuda", torch.float16
# ---------- 1. Synthetic Q,K,V tensors ----------
B, H, N, D = 2, 32, 4096, 128 # batch, heads, seq_len, head_dim
Q = torch.randn(B, H, N, D, device=DEVICE, dtype=DTYPE)
K = torch.randn(B, H, N, D, device=DEVICE, dtype=DTYPE)
V = torch.randn(B, H, N, D, device=DEVICE, dtype=DTYPE)
def bench(fn, warm=3, runs=10) -> float:
for _ in range(warm):
fn()
torch.cuda.synchronize()
t0 = time.perf_counter()
for _ in range(runs):
fn()
torch.cuda.synchronize()
return (time.perf_counter() - t0) / runs * 1000 # ms per iter
# ---------- 2. Vanilla (math) backend ----------
def vanilla():
with sdpa_kernel(SDPBackend.MATH):
return F.scaled_dot_product_attention(Q, K, V, is_causal=True)
# ---------- 3. Flash backend ----------
def flash():
with sdpa_kernel(SDPBackend.FLASH_ATTENTION):
return F.scaled_dot_product_attention(Q, K, V, is_causal=True)
# ---------- 4. Memory-efficient backend (falls back on older GPUs) ----------
def memeff():
with sdpa_kernel(SDPBackend.EFFICIENT_ATTENTION):
return F.scaled_dot_product_attention(Q, K, V, is_causal=True)
print(f"Sequence: B={B}, H={H}, N={N}, D={D}, dtype={DTYPE}")
try:
t_math = bench(vanilla)
print(f"vanilla MATH : {t_math:6.2f} ms/iter (baseline)")
except Exception as e:
print(f"vanilla MATH : OOM or error — {e.__class__.__name__}")
try:
t_flash = bench(flash)
print(f"FLASH_ATTENTION : {t_flash:6.2f} ms/iter"
f" ({t_math/t_flash:.1f}× faster)")
except Exception as e:
print(f"FLASH_ATTENTION : unavailable — {e.__class__.__name__}: {e}")
try:
t_eff = bench(memeff)
print(f"EFFICIENT_ATTENTION : {t_eff:6.2f} ms/iter"
f" ({t_math/t_eff:.1f}× faster)")
except Exception as e:
print(f"EFFICIENT_ATTENTION : unavailable — {e.__class__.__name__}")
# ---------- 5. Compare peak memory for one call each ----------
def peak_mem(fn) -> float:
torch.cuda.empty_cache()
torch.cuda.reset_peak_memory_stats()
fn()
return torch.cuda.max_memory_allocated() / 1024**3 # GB
print("\n--- Peak memory per attention call ---")
print(f"vanilla MATH : {peak_mem(vanilla):.2f} GB")
print(f"FLASH_ATTENTION : {peak_mem(flash):.2f} GB")
print(f"EFFICIENT_ATTENTION : {peak_mem(memeff):.2f} GB")
# ---------- 6. HuggingFace model with different attn implementations ----------
from transformers import AutoModelForCausalLM, AutoTokenizer
MODEL = "meta-llama/Llama-3.2-1B" # small enough for a single GPU
try:
tok = AutoTokenizer.from_pretrained(MODEL)
for impl in ("eager", "sdpa", "flash_attention_2"):
try:
m = AutoModelForCausalLM.from_pretrained(
MODEL, torch_dtype=DTYPE, attn_implementation=impl
).to(DEVICE).eval()
ids = tok("Hello world " * 500, return_tensors="pt").input_ids.to(DEVICE)
def gen():
with torch.no_grad():
m.generate(ids, max_new_tokens=32, do_sample=False)
print(f"\n{impl:<20}: {bench(gen, warm=1, runs=3):.1f} ms/iter")
del m; torch.cuda.empty_cache()
except Exception as e:
print(f"{impl:<20}: unavailable — {e.__class__.__name__}")
except Exception as e:
print(f"\n(HuggingFace section skipped: {e.__class__.__name__})")What each block does
Anatomy of the benchmark
Add this to the end of the script:
for N in [2048, 4096, 8192, 16384, 32768]:
Q = torch.randn(1, 32, N, 128, device=DEVICE, dtype=DTYPE)
K = torch.randn_like(Q); V = torch.randn_like(Q)
for name, fn in [("MATH", lambda: F.scaled_dot_product_attention(Q,K,V,is_causal=True))]:
try:
with sdpa_kernel(SDPBackend.MATH):
m = peak_mem(fn)
print(f"N={N:>6} vanilla={m:.2f} GB")
except Exception as e:
print(f"N={N:>6} vanilla=OOM")Watch the quadratic explosion. Then repeat with FLASH_ATTENTION and see the linear line.
(d) Production reality · 15 min
Serving 100K-token contexts naively meant O(N²) = 10^10 attention cells per layer per request. Memory alone for the score matrix in FP16 is 20 GB per attention head — impossible.
Anthropic (and every other lab) built on FlashAttention-2 plus paged KV-cache (vLLM-style) plus custom kernel work on Hopper GPUs. Result: 100K-token requests fit within a bounded VRAM budget and cost roughly linearly in context length.
Developer benchmarks Flash on N=128 (a tiny context) and finds it's slower than vanilla by 10%. Files bug reports. Meme circulates that 'Flash is overhyped'.
Below N ≈ 1024, tiling overhead dominates because attention is small enough to be compute-bound in the first place. Flash pays off exactly when the vanilla version is HBM-bound, which starts around N=1024–2048 and grows dramatically with N.
Fix: benchmark at your actual context length. If you serve N≥4K, Flash is a slam-dunk. If your workload is truly N<512, use SDPA MATH.
Research teams built sparse-attention variants (Longformer, BigBird, Sinkhorn, Reformer) to attack the 16K+ context ceiling. All achieved impressive academic benchmarks; almost none survived contact with production LLMs.
FlashAttention (2022) made exact attention cheap enough that sparse approximations became a solution to a problem that no longer existed for <100K contexts. Longformer / BigBird are still used for specialised long-doc encoders (legal, medical) but rarely in general chat models.
Where this shows up next
(e) Recall + stretch · 10 min
Explain-out-loud test
- Why is attention memory-bound instead of compute-bound?
- What does Flash-Attention change about the memory hierarchy?
- When would you reach for Sparse or Linear instead of Flash?
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.