Search Tech Journey

Find topics, journeys and posts

6-month learning plan115 / 130
back to blog
llmadvanced 50m read

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.

LLMsM13 · NLP & Transformers· Session 115 of 130 90 min

🎯 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.

You will be able to
  • 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

Why attention is expensive — the O(N²) receipt
🌍 Real world

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.

💻 Code world

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.

The three families in one line each
  • 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

  1. 2017
    Vanilla attention
    Vaswani et al. — quadratic in memory and compute. Cap = 512 tokens for BERT.
  2. 2020
    Sparse patterns
    Longformer (window + global), BigBird (sparse random + window). First real ≥4K-context models.
  3. 2020
    Linear attention wave
    Performer, Linformer, Reformer. Beautiful math, mixed real-world results — most got dropped by 2023.
  4. 2022
    FlashAttention v1 · Tri Dao
    Tile-based IO-aware exact attention. 2–4× faster. Suddenly 8K–32K contexts are cheap.
  5. 2023
    FlashAttention v2
    Better parallelism. Another 2× speedup. Now the default on H100.
  6. 2024
    State-space renaissance
    Mamba / Mamba-2 revive linear-cost sequence models with SSM math. RWKV also gains ground.
  7. 2024
    FlashAttention v3
    Uses 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

11
Tile the sequence into blocks

Split Q into blocks of ~64–128 rows; K and V into ~64-column blocks. A tile of Q × K fits in SRAM.

22
Load one Q tile, iterate over K/V tiles

For each K/V tile, compute the partial attention weights and partial output for this Q tile — all in SRAM.

33
Online softmax

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.

44
Write only the output back

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.

55
Backward pass

Recompute the tiles instead of storing them. Trades a small amount of extra FLOPs for enormous memory savings.

The three families side by side

Flash-Attention

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
Sparse attention

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
Linear attention

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)

Vanilla FP16
N² intermediates ≈ 32K × 32K × 32 heads × 2 bytes = 65 GB per layer. OOM on any single GPU.
OOM
Flash-Attention 2
N-sized intermediates ≈ 32K × 128 × 32 heads × 2 bytes = 256 MB per layer. Fits comfortably on an H100.
fits
Sliding window (w=4096)
N × w intermediates ≈ 32K × 4K × ... — 8× smaller than Flash exact, at the cost of losing long-range info beyond the window.
sparse
Mamba SSM
State size independent of N ≈ 32K × 16 = 512K per layer. Truly linear; different math family altogether.
linear

Common misconception
✗ What most people think

"FlashAttention is an approximation. It's faster because it skips some attention computation, so there must be a small accuracy cost."

✓ What is actually true

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.

Why the myth is so sticky

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.

Prove it to yourself

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 writes
From first principles
Start with the question

Why can softmax be computed in tiles at all? It needs a global normaliser over the whole row — that looks like it forbids streaming.

  1. 1
    Softmax must be computed as ezi−m/Σezj−m with m the row max, otherwise ez overflows in fp16 for logits above ~11.
    forced by · numerical stability requires subtracting the max; this is not optional at half precision
  2. 2
    Both 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
  3. 3
    But 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
  4. 4
    The 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
  5. 5
    Therefore 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

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.

Mental modelThe memory hierarchy is the algorithm

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.
🔔 Fires when you see

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.

The tradeoff

You need to support 128k-token context. Exact optimised attention, sparse/windowed attention, or retrieval over a short window?

Exact attention (FlashAttention + GQA + paging)
+ you gain no quality loss whatsoever — any token can attend to any token, which is what long-context benchmarks actually test; and it is a drop-in change with no retraining
− you pay compute still grows quadratically, so prefill latency on a 128k prompt is large and irreducible; and the KV cache grows linearly, capping concurrency hard
pick when context is long but requests are few, or correctness on arbitrary long-range references is the product requirement
Sparse / sliding-window attention
+ you gain compute becomes near-linear in sequence length, so prefill on very long inputs becomes tractable and memory per sequence drops sharply
− you pay some token pairs can no longer interact in one layer; whether that matters depends entirely on the task, and the failures are silent — long-range retrieval degrades without any error; usually requires training or fine-tuning with the pattern
pick when the dependency structure is genuinely local or you have measured that your task's long-range needs are rare — logs, code files, time series
Retrieval into a short window
+ you gain cost is independent of corpus size, so it scales to gigabytes rather than 128k tokens; retrieved chunks are inspectable and citable, which is an operational advantage attention can never give you
− you pay quality is bounded by the retriever — anything not retrieved does not exist; and it fails on tasks needing global synthesis over the whole document rather than a few relevant passages
pick when the corpus exceeds any plausible context window, or the queries are lookup-shaped rather than synthesis-shaped
What a senior engineer actually does

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

sdpa_kernel(SDPBackend.X)
PyTorch's context manager that forces a specific attention backend. Without it, SDPA picks the fastest available for your shapes.
backend
is_causal=True
Tells SDPA to apply the lower-triangular mask (decoder-style). The Flash backend uses this to skip computing the upper triangle entirely.
mask
torch.cuda.synchronize()
Necessary before timing because CUDA calls are async — without sync, you measure launch time, not run time.
timing
reset_peak_memory_stats + max_memory_allocated
The clean way to measure per-op memory. Flash should show N× less than MATH for large N.
memory
attn_implementation='flash_attention_2'
HuggingFace's flag for using the flash-attn package directly (needs pip install flash-attn). Higher throughput than SDPA on Hopper.
hf
Try itSee the O(N²) memory wall in action

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.

💡 Hint · Loop N over [2048, 4096, 8192, 16384, 32768] and re-run peak_mem for vanilla vs Flash. Vanilla will OOM at some point; Flash keeps going. Plot the memory curves — vanilla is quadratic, Flash is linear.

(d) Production reality · 15 min

War story Anthropic · Claude 100K context launch· 2023100K-token context per request
🔥 What broke

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.

🧯 The fix

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.

🎓 Lesson to steal
The 100K → 200K → 1M context race that started in 2023 was almost entirely a kernel-engineering race. Flash was the first ingredient; every lab has since layered ring-attention, KV-cache tricks, and sparse rescoring on top.
War story Common failure mode — 'Flash-Attention is slow!'reported weekly on GitHub
🔥 What broke

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'.

🧯 The fix

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.

🎓 Lesson to steal
Efficient-attention gains are non-uniform. Always measure at your production shapes. The 'X is faster than Y' claim only holds in a shape range.
War story Longformer / BigBird era — 2020–2022every long-doc research project
🔥 What broke

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.

🧯 The fix

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.

🎓 Lesson to steal
Sometimes engineering solves what algorithms were trying to. If your problem is 'my quadratic thing is too expensive', try 'make the quadratic thing faster' before 'replace it with a linear approximation'. Flash was the whole reason sparse-attention research quieted down.

Where this shows up next

Efficient attention unlocks every long-context feature
S117 · RAG Chunking
Big contexts change the 'chunk small' calculus — with 128K context you can just dump a whole PDF.
S119 · Vector Databases
Long context reduces (but doesn't eliminate) the need for retrieval; RAG stays because context is still money.
S120 · LLM Agents
Long histories of tool calls + reasoning require Flash-Attention-scale contexts; without it agents die at hop 5.
S124 · LLM Serving
vLLM / TGI / TensorRT-LLM all rely on Flash + paged KV. Serving efficiency is 90% attention efficiency.
S125 · Multimodal
Vision-language models blow up sequence length (images become thousands of tokens); efficient attention is non-negotiable.
S128 · Cost & Sustainability
Every 2× attention speedup halves inference cost — direct margin impact.

(e) Recall + stretch · 10 min

Quick recall · click to reveal
★ = stretch question

Explain-out-loud test

  1. Why is attention memory-bound instead of compute-bound?
  2. What does Flash-Attention change about the memory hierarchy?
  3. 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.