kernel tutorials / online safe softmax
Softmax as everyone first writes it overflows; the safe version costs three passes over the row; the online version gets that back down to one.
Softmax turns a row of scores into a row of probabilities: exponentiate every element, then divide each by the total, so the outputs are positive and sum to 1.
No single output can be produced until the denominator is known, and the denominator needs the entire row.
The naive formula dies on real inputs because grows absurdly
fast: is already infinity in fp32, and in fp16 the ceiling is
all the way down at . One score past the ceiling and the row is
inf/inf = NaN.
The fix rests on one observation: shifting every score by the same constant does not change the answer. Multiply the top and bottom of the fraction by and it cancels:
Pick , the row maximum, and every exponent becomes : every exponential lands in , and overflow is impossible by construction.
The price is that safe softmax is a three-pass algorithm, and the passes cannot overlap; each one needs the previous pass to have finished the whole row:
[2, 4, 3, 1]. Pass 2 sits idle until pass 1
has finished the entire row; each readout on the right only ticks while its own pass is moving.Three passes are harmless when the row is 32 wide and sitting in L1. In attention the row is a row of scores: one entry per key, so as long as the sequence. At 128K context that is a 128K-wide row per query, produced block by block out of , ideally consumed the moment each block exists and never stored. "First scan everything to find the max" forbids exactly that.
The three-pass structure exists only because pass 2 is scared of using a stale max. Online softmax walks the row once, carrying two running values: , the max seen so far, and , the sum of exponentials relative to that max. When the next score arrives:
The highlighted factor is the entire idea. Every term already inside has the form . If the max just rose from to , every one of those terms is now measured against the wrong reference, but they are all wrong by the same factor, because . So one multiply by retroactively corrects the whole accumulated sum at once. No stored term is ever revisited. When the max did not rise, , the factor is , and the update is a plain add.
The same walk over [2, 4, 3, 1], starting from
:
| Sees | New max | Correction | New sum |
|---|---|---|---|
| 2 | 2 | – | |
| 4 | 4 ↑ max rose | ||
| 3 | 4 | 1 | |
| 1 | 4 | 1 |
One pass, and the result is exactly the safe denominator: computing directly gives the same . The answer is identical, not an approximation.
For softmax by itself you still owe one more sweep at the end: the outputs need the final , so one pass becomes two, not one. The full win shows up in attention, which never needs the normalized scores individually: it only needs their weighted sum against . So keep a third running value, the accumulated output row , and rescale it by the same factor whenever the max rises, then divide by once at the very end. One pass total, and the score row is never stored anywhere. That is flash attention.
Look at the update rule again and notice it never cared that was a single score. Absorbing one element is just the special case of merging in a summary . Two summaries of two chunks of the row merge the same way:
This merge doesn't care what order the chunks arrive in, and merging three chunks gives the same answer however
you pair them up. An operation with those two properties is exactly what a reduction collective needs:
it is all_reduce with this merge in place of plain addition. So the row can be cut
into chunks, each chunk summarized independently, and the summaries combined pairwise, in a tree, or around a
ring.
That is the multichip payoff. When context gets long enough that the sequence dimension is
fractured across chips, each chip holds a slice of the keys, so for any query, each chip can only score its
own slice. The softmax denominator spans all of them. Gathering the raw score rows would move
seq_len numbers per query per chip; merging online-softmax summaries moves two scalars per query
(plus the running output row):
In a tensor-parallel attention block, attention is split by heads,
and a head never leaves its chip: each softmax row lives entirely in one place, so sdpa runs with
no collective attached. Fracture the sequence instead of the heads and softmax becomes the op that spans
chips. Online softmax is what keeps that affordable.
tt-metal's SDPA compute kernel is this page's math written in tiles: the running max and sum each get a
circular buffer (cb_max_A/B, cb_sum_A/B), the correction factor
has one of its own (cb_exp_max_diff), and the rescale step is a function named, fittingly,
correction_block. The ring variant runs the same walk across chips, where each step's "next chunk"
is another chip's slice of the sequence.
Source: state buffers, ttnn/cpp/ttnn/operations/transformer/sdpa/device/kernels/compute/sdpa.cpp:88 ·
rescale step, compute_common.hpp:706 (correction_block) ·
cross-chip walk, ring_joint_sdpa.cpp in the same directory