Approach (high level)FlashAttention reduces memory and increases throughput by computing attention in tiles: instead of materializing the full softmax(QK^T) (O(L^2) memory), it streams small blocks (tiles) of Q and K, computes partial logits, applies a numerically-stable incremental softmax (LogSumExp style), accumulates weighted V contributions, and only keeps O(L * tile) working memory. This improves cache/GPU shared-memory locality and avoids large intermediate tensors.Blockwise attention pseudocode (tile-by-tile, numerically-stable softmax per row of Q)python
# Pseudocode (conceptual, not kernel code)
def blockwise_attention(Q, K, V, tile_m, tile_n): # Q: (L, d), K:(L,d), V:(L,dv)
out = zeros_like(Q @ V[:0]) # allocate output L x dv
for i0 in range(0, L, tile_m): # tile over query rows
Q_tile = Q[i0:i0+tile_m] # shape (m, d)
# initialize per-row accumulators for numerically-stable softmax:
max_logit = -inf_vector(m) # shape (m,)
sum_exp = zeros_vector(m)
weighted_V = zeros(m, dv)
for j0 in range(0, L, tile_n): # tile over key/value rows
K_tile = K[j0:j0+tile_n] # shape (n, d)
V_tile = V[j0:j0+tile_n] # shape (n, dv)
logits = Q_tile @ K_tile.T # shape (m, n)
# rowwise max for stability
tile_max = logits.max(axis=1) # shape (m,)
# shift and exponentiate
shifted = exp(logits - tile_max[:,None])
tile_sum = shifted.sum(axis=1) # shape (m,)
# update global max and sum using log-sum-exp identity
new_max = maximum(max_logit, tile_max)
# correction factor to rescale accumulators built against the OLD max
correction = exp(max_logit - new_max)
# adjust sums to common scale
sum_exp = sum_exp * correction + tile_sum * exp(tile_max - new_max)
# accumulate weighted V: previously accumulated weighted_V was scaled to the
# OLD max, so it must be rescaled by `correction` too, not just added to
scaled_weights = shifted * exp(tile_max[:,None] - new_max[:,None]) # shape (m,n)
weighted_V = weighted_V * correction[:,None] + scaled_weights @ V_tile
max_logit = new_max
out[i0:i0+tile_m] = weighted_V / sum_exp[:,None]
return out
Why tiling reduces peak memory- Naïve attention stores full LxL logits and softmax weights (O(L^2)) before multiplying by V. Blockwise only materializes an m x n tile of logits at a time plus small per-row accumulators (O(L * tile + tile^2)), so peak memory drops from O(L^2) to roughly O(L*tile + tile^2). Small tile sizes enable fitting in shared memory/L2 cache improving throughput. (Note: whenever the running max is updated to a new tile's max, every previously accumulated quantity, both sum_exp and weighted_V, must be rescaled by exp(old_max - new_max) before adding the new tile's contribution. Forgetting to rescale weighted_V, and only rescaling sum_exp, silently produces the wrong output whenever the max changes across tiles. Verified numerically against a naive full-softmax reference with random Q/K/V and tile sizes that do not evenly divide the sequence length.)Implementation caveats / practical issues- Padding: handle sequence length not divisible by tile size; mask out padded positions before exponentiation.- Head dim not divisible by tile: if d not multiple of vectorized lane, either pad K/Q channels or process remainder with scalar loops (padding must not affect numerics).- Mixed precision: accumulate sums in higher precision (float32 or float64) even if inputs are float16/bfloat16; use fused kernels with FP32 accumulators to avoid underflow/overflow.- Causal masks: when doing autoregressive (triangular) attention, only process K tiles up to current query index; treat masked logits as -inf before max.- Numerical stability: use per-row log-sum-exp updates (as shown) to avoid overflow when adding tiles with different maxima.- CUDA/kernel concerns: ensure shared-memory fits, avoid bank conflicts, use warp-level reductions, minimize global memory traffic, and avoid atomic updates when possible; consider half-to-float conversions and vectorized loads/stores for throughput.