all_lessons/gpu_kernels/29 · FlashAttention backwardlesson 29 / 33

FlashAttention backward

The flagship of the training Part. The forward (lesson 12) never materialized the N² attention matrix — so the backward has to recompute it, block by block, while carrying a single per-row correction scalar and running the two-GEMM rule inside a streaming loop. It composes every idea in Part IV: recompute-don't-store, the online softmax statistic, cross-block gradient accumulation, and fp32 reductions.

Builds on
Forward FlashAttention & online softmax (lesson 12); the two-GEMM rule (26); recompute-the-stat and cross-block accumulation (28). If lesson 12 was the forward flagship of the serving track, this is its mirror.

The question this lesson answers

FlashAttention forward earned its speedup by not storing the N² scores. That looks like it should make the backward impossible — gradients need the attention weights. How do you compute dQ, dK, dV when the thing you'd differentiate was thrown away?

What the forward left behind

Recall the forward output for one query row: O = softmax(QKᵀ/√d)·V, computed with the online recurrence so that the only things written to HBM are:

That's O(N·d) of saved state, not O(N²). The N² probability matrix P = softmax(S) is gone. The backward's whole trick is that L is exactly enough to regenerate any block of P on demand: Pij = exp(Sij − Li), where Sij = (Qi·Kj)/√d is recomputed from Q and K. No global softmax pass needed — L already folded in the row max and normalizer.

The gradients, and the one scalar that makes them local

Given dO, the attention backward is:

dV = Pᵀ · dO dP = dO · Vᵀ dS = P ∘ (dP − D), where Di = Σj dOij·Oij (one scalar per row) dQ = (dS · K)/√d dK = (dSᵀ · Q)/√d

The pivotal term is Di = rowsum(dO ∘ O). It comes from the softmax Jacobian: differentiating softmax gives dS = P∘(dP − (P·dP)·𝟙), and the inner product P·dP per row equals rowsum(dO∘O) — a quantity you can compute in one cheap pass over dO and O before the main loop. Precomputing D (shape N, like L) means each block of dS is computable locally from that block's P, dP, and the row scalar Di — no second global reduction inside the loop. This is the same "hoist the per-row correction out of the loop" move as the online softmax, now on the backward side.

The loop structure: who accumulates cleanly, who needs atomics

Look at the reduction axes. dKj and dVj for a key/value block j sum over all query rows i. dQi for a query block i sums over all key blocks j. You can't make both the clean inner-loop accumulation at once. FlashAttention-2's backward chooses the outer loop over K/V blocks:

precompute D = rowsum(dO∘O) → one pass, shape N (like L) D[i] and L[i]: the two per-row scalars that make every block local outer loop: K/V block j inner loop: Q block i recompute S=QᵢKⱼᵀ/√d P=exp(S−Lᵢ) (regenerate) dS=P∘(dOᵢVⱼᵀ − Dᵢ) dKⱼ,dVⱼ += …accumulate in registerswrite once per j ✓ clean dQᵢ += dS·Kⱼ/√dcontributed every j →atomics OR separate dQ kernel never materializethe N×N P matrix —one block at a time Memory: saved state is O(N·d) [O, L, D], never O(N²). Compute: backward recomputes S, so it is ~2–2.5× the forward FLOPs. This is the recompute-for-memory trade (lesson 25) at its most valuable: at N=8192 the avoided P matrix is gigabytes.

The trade in one line

FlashAttention backward spends extra FLOPs (recomputing S and P) to avoid O(N²) memory. Because attention is bandwidth/memory-bound at long context and the saved matrix grows quadratically while the recompute grows only with the work you were already doing, the trade is overwhelmingly worth it — and it's the only way attention training fits at long sequence at all.

Worked numbers: why O(N²) was never an option

One head, head-dim d = 128, bf16. The materialized probability matrix is N²·2 bytes per head per layer:

Seq length NMaterialized P (per head)FlashAttn saved state O+L+D (per head)Ratio
2,0482048²·2 = 8.4 MB2048·128·2 + 2048·4·2 ≈ 0.54 MB~16×
8,1928192²·2 = 134 MB≈ 2.1 MB~64×
32,76832768²·2 = 2.1 GB≈ 8.5 MB~256×

Multiply by heads × layers and the materialized path is impossible past a few thousand tokens; the FlashAttention path scales linearly. The price is FLOPs: the backward's recompute of S makes attention backward ≈ 2.5× the forward attention FLOPs — paid gladly, because the alternative doesn't fit.

The traps

TrapSymptomFix
Forgetting to precompute Da global reduction sneaks into the inner loop; slow / wrong dScompute D = rowsum(dO∘O) in a pass before the main loop
Recomputing P without Lnumerically unstable exp; NaNsP = exp(S − L); L already carries the row max
dQ accumulation racenon-deterministic grads or wrong dQatomics (accept non-determinism) or the separate dQ-major kernel
bf16 dK/dV/dQ accumulatorsgrad drift at long contextfp32 accumulators, downcast on store (lesson 26)
Causal mask handling in backwardgradient leaks across the causal boundaryskip fully-masked (i,j) blocks; mask the diagonal block

Interactive · the N² you don't pay, the FLOPs you do

Sweep sequence length. The widget contrasts the materialized P matrix (quadratic) with FlashAttention's saved state (linear), and shows the recompute FLOP overhead you pay for it — the recompute-for-memory trade made visual.

FlashAttention backward: memory saved vs FLOPs spent

Materialized P per head = N²·2 bytes. FA saved state = N·d·2 (O) + N·4·2 (L,D). Backward FLOPs ≈ 2.5× forward attention (extra recompute of S).

What this sets up

You've now built the gradient of the hardest forward kernel by composing every Part IV idea. All these kernels produce one thing: gradients. The last lesson consumes them — the optimizer kernel — and closes the training step back to lesson 25's memory peak, with mixed precision and recomputation as the final levers.