TL;DR: Keep a running max , a running normalizer and an unnormalized output, and rescale all three by whenever a new block raises the max: the final equals softmax attention exactly. That lets attention stream K and V through on-chip SRAM once, so the score matrix (128 MiB per head at 8K tokens in BF16) never touches HBM. FLOPs are unchanged; bytes moved fall, and attention is bound by bytes.
How to approach it
Lead with the obstacle: softmax needs the maximum and the sum over the whole row before any output can be written, which seems to force materializing every score. Then say the plan, which is to make the statistics mergeable so the row can be processed block by block. Clarify whether the interviewer wants the forward pass only, or also the backward pass, where the same trick reappears as recomputation.
A strong answer
Softmax attention for one query is with . Subtracting any constant from every leaves it unchanged, and choosing keeps every exponent at or below zero. That is not optional: overflows FP16 for , so real kernels keep these statistics in FP32 with the max subtracted.
Process the keys in blocks. After block , hold three things: the max so far , the normalizer , and the unnormalized output . When block arrives with block max :
and updates with the same scale factor. The proof is one line: every term already in is , and multiplying it by turns it into , exactly the term it would have been had the new max been known from the start. By induction the final state is the full-row sum referenced to the true max, and is exact, not approximate.
import numpy as np
def online_attention(q, K, V, block=128):
"""q: [d], K, V: [N, d]. One pass over K/V, O(d) state."""
m, l = -np.inf, 0.0
acc = np.zeros(V.shape[1])
for s in range(0, K.shape[0], block):
scores = K[s:s + block] @ q / np.sqrt(q.shape[0])
m_new = max(m, scores.max())
scale = np.exp(m - m_new) # 0 on the first block: exp(-inf)
p = np.exp(scores - m_new)
l = l * scale + p.sum()
acc = acc * scale + p @ V[s:s + block]
m = m_new
return acc / l
Against a direct softmax on 1,000 keys (deliberately not a multiple of the block), the maximum error is at ordinary score magnitudes and zero at magnitudes of , where a softmax without max subtraction returns NaN.
Now the cost. Naive attention writes the score matrix to HBM, reads it for the softmax, writes , and reads again for . At :
S per head = 8,192 x 8,192 x 2 B = 128 MiB
x 64 heads = 8 GiB per layer, written and read at least twice
H100 at 3.35 TB/s -> about 10 ms per layer in score traffic alone
softmax pass intensity: a few FLOPs per byte, far below the ridge of 295
FlashAttention tiles Q, K and V so a block of queries and a block of keys fit in an SM's shared memory (up to 228 KB per SM on H100), computes the block's scores, applies the recurrence in registers, multiplies by the V block, and moves on. Only Q, K, V and O cross HBM. The paper's bound is HBM accesses for SRAM of size , against for the naive kernel. The FLOPs are identical; for long sequences the bytes fall by a factor of roughly , and because the naive version was memory-bound, wall time follows bytes.
The reversal condition: at very short sequences, say under a few hundred tokens, the score matrix fits in cache anyway and launch overhead dominates, so fused attention gains little. FlashAttention matters most exactly where context grows.
What interviewers probe next
"What does the backward pass store?" Only the per-row log-sum-exp , one scalar per query. The backward kernel recomputes block by block from Q, K and that scalar. More FLOPs, far fewer bytes, and it wins for the same reason the forward pass does.
"What did FlashAttention-2 and -3 change?" FA2 parallelized over sequence length as well as batch and heads, and moved non-matmul work out of the inner loop, because FA1 left SMs idle at small batch. FA3 targets Hopper: warp-specialized producers issuing TMA loads, WGMMA tensor-core instructions, softmax overlapped with the next block's matmuls, and FP8 support. FlashAttention-4 (arXiv 2603.05451, March 2026) targets Blackwell, where tensor-core throughput grew faster than the exponential units, and computes part of the exponentials in software on FMA units.
"Batch 1, 128K context, decoding: does this kernel still parallelize?" Not over queries, since there is one. Split the KV length across thread blocks, run the recurrence on each split, and merge the partial triples with the same rescaling rule. That is split-KV, or FlashDecoding.
How it's scored
Weak: says FlashAttention "reduces attention complexity" or "approximates softmax", or cannot say why the max is subtracted.
Hire: writes the recurrence with the rescale factor, argues exactness, and explains that the saving is HBM traffic for the score matrix.
Strong Hire: gives the IO count, connects the overflow threshold to FP32 statistics, explains backward recomputation from the log-sum-exp, and generalizes the merge rule to split-KV decoding.
Common mistakes
- Rescaling but not the accumulator, which leaves the output wrong by a factor that depends on block order and only shows on inputs whose maxima arrive late.
- Initializing instead of , which silently breaks rows whose scores are all negative.
- Claiming fewer FLOPs. FlashAttention does the same multiplications, plus recomputation in the backward pass.
- Treating causal masking as a separate pass. Fully masked blocks are skipped before any loads, which is where causal attention gets close to 2x.