Module Ir.Online_softmax

The online-softmax attention rewrite (gh-ocannl-483): the first member of the algebraic-rewrite tier over lowered code (Rewrites), a pattern-directed substitution that CHANGES the computation where the schedule transforms only rearrange it.

The composed attention softmax (q * k^T) * v lowers to a max-reduction and a sum-reduction over the key axis with two elementwise nests between them, then a reduction over the key axis against v. Two of its intermediates are seq^2-shaped and get materialized: the scores are read by both reductions, and the probabilities are read once per value-width iteration of the final reduction. This pass rewrites four shapes, all gated by the config key online_softmax (default off, on in the approximate profile), the third and the fourth by a second key each:

What stays as it was in the two-pass form: the score reduction q * k^T keeps its own placement decision -- the recompute cap virtualize_max_inline_reduction decides whether it is replayed at its two read sites (the scan and the hoisted read) or stored once. Recomputing is flash attention's memory trade (no seq, seq buffer at all); storing was faster at every cell measured on cc and Metal (benchmarks/report-gh483-online-softmax.md, seq 128 to 1024), and a training step's backward reads the scores again through cross-routine splicing. (The block fold computes its scores into the tile from q and k and reads nothing else of the score chain, which it removes where nothing else reads it.)

The pass runs at lowering, ahead of the analyses (Rewrites.rewrite.apply, from Assignments.lower, over the raw lowered code), so the traced store and the placements see the rewritten routine and no analysis digest needs the knob: the code itself carries the decision (Code_borne). It is idempotent, as the tier's fixpoint requires: its output has no max-reduction nest left to match -- so no normalizer to anchor a backward or a fold on either -- and a hoisted reduction is no longer a single nest.

val enabled : unit -> bool

Whether the rewrite applies: the programmatic override if one is set, else the config key online_softmax.

val set_enabled : bool option -> unit

Programmatic override of the config key, for experiments and tests; None restores the key. Takes effect at the next lowering.

val backward_enabled : unit -> bool

Whether the fused backward applies, where the rewrite does: the programmatic override if one is set, else the config key online_softmax_backward. Inert while enabled is false.

val set_backward_enabled : bool option -> unit

Programmatic override of online_softmax_backward, like set_enabled.

val block : ?auto_block:int -> unit -> int

The block size of the single-pass fold (gh-ocannl-1003): the programmatic override if one is set, else the config key online_softmax_block; 0 (the default) keeps the two-pass form. Read only where the rewrite applies. auto selects the supplied auto_block (0 by default). A backend-free caller defaults to the conservative choice; numeric values and the programmatic override force their size regardless of device economics.

val set_block : int option -> unit

Programmatic override of online_softmax_block, like set_enabled.

val reassociable_local : Tnode.t -> bool

Whether a node is the fused backward's per-pair dp scope local (gh-ocannl-1124), whose value-width accumulation the default GPU schedule may give the lanes to compute together and reassociate: the license is online_softmax_backward's, an approximate-tier gate and the only thing minting it. The test is membership among the nodes the rewrite (or dprob_local) minted, not the node's namespace or label, which any client could reproduce. The schedule admits a preamble reduction only into such a local (Schedule.preamble_reduction), and the renderer's lane all-reduce (C_syntax.try_lane_all_reduce) renders only such a local.

val dprob_local : like:Tnode.t -> Ops.prec -> Tnode.t

The dp scope local the fused backward mints for the pair nests over like (virtual; memoized per like and precision, like every scalar the rewrite mints): the one node reassociable_local accepts. Exposed so that hand-built IR can exercise the schedule's preamble-reduction lanes on it -- and, since minting it is the license to reassociate, only under the gate that licenses that: raises Invalid_argument unless online_softmax_backward (or set_backward_enabled) is on.

val fold_tile_role : Tnode.t -> [ `Scores | `Numerator ] option

Whether a node is one of the block fold's per-row tiles, and which: the score tile or the output numerator. The scheduling side's cooperative rendering of the fold (Schedule.optop.Fold_mma) recognizes the fold by them.

val reset : unit -> unit

Drops the memoized scope-local nodes (the tier's session-reset hook; also run ahead of an accessibility snapshot). Sibling lowerings of one program mint the same nodes, which is what keeps them on one analysis-cache key; a reset only makes the next lowering mint afresh.

val rewrite : ?auto_block:int -> Low_level.t -> Low_level.t

The pass over raw lowered code (no gate: the tier consults enabled). Every normalizer pattern in the routine is rewritten; a routine without one is returned as is. The minted scope-local nodes carry memory-mode provenance "483:online-softmax-state", the fused backward's per-row D (a stored node: Never_virtual) "1002:fused-backward-row-dot"; the block fold's tiles (labelled block_scores and block_numerator) request no memory mode: the optimizer refuses them structurally (Non_virtual 148 for a write in a scan body, 147 for their inits ahead of it).