Core optimizations
Online softmax
Softmax needs a row max and a sum. Online softmax keeps running statistics so you can process the row in tiles and still get the exact result.
Softmax looks like it needs the whole row at once: each entry is exp(x_i) divided by the sum of exp(x_j) over the row. If that were the only algorithm, you would have to store every score before normalizing. Online softmax keeps a running max and a running sum so you can eat the row in tiles.
Concept
Softmax turns a row of scores into a probability distribution. The max is subtracted first so exp does not overflow. Online softmax (Milakov and Gimelshein, and related work) updates that max and sum as tiles arrive, and rescales anything already accumulated.
The big idea
You are totaling a stack of receipts in batches. Whenever a new batch has a larger tax rate than you assumed, you rescale the old total before adding the new one. You never needed every receipt on the table.
The analogy breaks on floating point. Different addition order means results are not bit-identical to a naive kernel. They are mathematically the same softmax. Production stacks accept the tiny difference.
Walking through it
For each tile of scores:
m_new = max(m_old, max(tile))
rescale the running numerator by exp(m_old - m_new)
add exp(tile - m_new)
update the running sum of exp
At the end, divide the numerator by the running sumThe only resident state is the tile plus (max, sum, partial output). You never needed the full row.
import math
xs = [1.0, 3.0, 2.0, 0.0]
m = max(xs)
naive = [math.exp(x - m) / sum(math.exp(y - m) for y in xs) for x in xs]
run_m, run_s, num = float("-inf"), 0.0, []
for tile in ([1.0, 3.0], [2.0, 0.0]):
m_new = max([run_m, *tile])
scale = math.exp(run_m - m_new) if run_m != float("-inf") else 0.0
run_s = run_s * scale + sum(math.exp(x - m_new) for x in tile)
num = [n * scale for n in num]
num.extend(math.exp(x - m_new) for x in tile)
run_m = m_new
online = [n / run_s for n in num]
print([round(v, 6) for v in naive])
print([round(v, 6) for v in online])Output:
[0.087144, 0.643914, 0.236883, 0.032059]
[0.087144, 0.643914, 0.236883, 0.032059]That identity is what lets FlashAttention stream K and V tiles without writing S to HBM.
Masks belong inside the tiled loop. Causal scores for future positions, and padded keys, must not enter the running sum, or they enter as -inf. Getting this wrong corrupts the distribution. It is worse than a slow kernel.
Common mistakes
- Thinking "online" means approximate. The algebra is exact softmax. Only the addition order changes.
- Forgetting to rescale when the max grows. Old numerators are then in the wrong units.
- Letting padded keys into the running sum. The model then attends to padding.
Check yourself
- What three pieces of state does online softmax keep between tiles? Answer: The running max, the running sum of exp, and the partial weighted output.
- Why rescales when a later tile has a bigger max?
Answer: Earlier
exp(x - m_old)values must becomeexp(x - m_new), which is a multiply byexp(m_old - m_new).
Key takeaways
- Softmax needs a row max and a sum, not the whole row resident at once.
- When the running max changes, rescale what you already have.
- The result matches naive softmax up to floating-point associativity.
- This is the math that makes tiled, IO-aware attention possible.
Further learning
Online normalizer calculation for softmaxThe Softmax function is ubiquitous in machine learning, multiple previous works suggested faster alternatives for it. In this paper we propose a way to compute classical Softmax with fewer memory accesses and hypothes…arXiv.org
Next, FlashAttention uses this inside an SRAM tile loop.
Related
FlashAttention
Why standard attention is HBM-bound, how tiling plus online softmax computes exact attention in on-chip SRAM, and what FlashAttention-2/3 and decode variants change.
Why attention is HBM-bound
Standard attention writes an n by n score matrix to HBM. Bytes grow with n squared, so long sequences leave tensor cores idle.
Self-attention
The operation at the core of every modern language model. Each position builds a weighted average of the others, with weights set by query-key matches.