Autotunetinygrad-style beam search over Ir.Schedule transforms, timed on the real device (docs/proposals/schedule-ir-optops.md; the search-harness half of the OptOps port). tune is a drop-in replacement for Context.compile: it compiles candidate schedules through the ?lowered_transform seam, times each on the context's device, and returns the routine of the fastest one. Every candidate (and the winner replay) derives from a hermetic copy of the one base lowering captured at the start — each candidate compile's own fresh lowering is ignored, because timing runs settle tensor-node value bounds and later lowerings can fold guards or re-segment fission differently, silently corrupting digest comparisons and replays. Winning schedules are persisted, in the structurally-rebindable saved form of Ir.Schedule_cache, to a disk cache keyed by the code's canonical digest and the backend, so a re-run of the same program skips the search (cross-process replay is guarded by digest equality against that process's own base lowering).
The candidate space:
Ir.Schedule.default_gpu. On GPU backends the serial baseline (and any candidate that degenerates to it) is enumerated but never dispatched: with no hardware dimension bound the whole routine runs in one work-item, which cannot win and whose cost is unbounded — hours of uninterruptible dispatch on a device shared with the display (gh-ocannl-532). It keeps its role as the search's starting point for menu moves and as the code every candidate derives from; it just carries no measurement. On CPU backends it runs at full single-core speed and is timed as before. Every such refusal enters the report's decline census under Not_dispatched_key (gh-ocannl-543): candidates_timed alone cannot distinguish a GPU search whose serial candidates were all refused from one whose candidate space was empty. Beam rounds expanding an incumbent that was never dispatched propose only the moves that can bind a hardware dimension (Tensorize, placement retypes); the rest are pruned before compile and counted in the same census. Separately, the baseline's compile is protected like any candidate's (gh-ocannl-533): a typed rejection — a large softmax/cross-entropy head whose unscheduled form exceeds the HIP scratch budget is the case in hand — declines the baseline (baseline_declined in the report, and its own key in the census rather than a Not_dispatched refusal, which would assert a reason that is not the one) and the search proceeds on the scheduled candidates, which fission and placement promotion routinely bring back within budget. Only the base lowering itself is indispensable: a failure before it is captured has no search to run and propagates.Ir.Schedule.fission_scheduled) with per-segment schedules — the same preset sweep per segment, and beam rounds that extend one segment at a time. `Zeros segments keep the default zero-expansion; `Solo segments stay unscheduled. A fissioned winner is cached with its whole segmentation — each segment's kind, length and pre-schedule canonical digest, and its schedule, zero expansions included (Ir.Schedule_cache.saved_segment, gh-ocannl-1164) — and replays by cutting there (Ir.Schedule.fission_segmented's replay) rather than by re-segmenting under the current policy: a segmentation that no longer fits, or a segment whose digest differs from the saved one, fails the replay into a re-search. One seed uses the config-default thresholds, reproducing the untuned default pipeline exactly — so the winner is never worse than not tuning, even on launch-overhead-bound workloads where every aggressive preset loses to it; its measured time is surfaced as the report's default_ms reference (gh-ocannl-552). Each preset is additionally seeded in a privatized variant (extend_with_privatize): per segment, every materialized read-modify-write accumulator is contracted into a per-thread register tile (Ir.Schedule.optop.Privatize) over its serial reduction loop where the op's preconditions permit — a routine-local accumulator beats a device-memory RMW, and on Metal it sidesteps the volatile scalar-RMW workaround.Tensorize targeting simdgroup_matrix/tensor cores, both unstaged (one full-reduction Tile_mma block) and cooperatively staged through shared tiles (lane-aware Stage) — Stage-only by design, Privatize would move the accumulator into thread-space the MMA loads cannot address. On the C backends the tensorized whole-triple and Grid-split-row forms are seeded regardless of limits.mma — their Tile_mma renders as the register-tiled vector micro-kernel. Seeding matters because the beam cannot reach these compositions incrementally: a bare Tensorize from the serial baseline loses its round and is discarded before Grid retypes could join it. The sketches are seeded whole-routine and per fission segment: on a fissionable computation, the fission segmentation is enumerated once and the sketch pipelines are instantiated for each segment where a matmul site is detected (keyed by the segment's pre-schedule digest), the remaining segments keeping the default preset. GPU segments can carry a covering per-cell zero companion, mapped with the site's geometry; Privatize forwards its seed when its scope owns the whole reduction. Whole-node zeros retained by the policy (including CPU zeros) remain separate, and the accumulator init-load observes them. Tile_mma loads its accumulator fragment before the reduction. On GPU backends the segmentation is additionally enumerated under Ir.Schedule.fission_scheduled's arity_cuts (finer) mode (gh-ocannl-574): a segment carrying a companion that cannot follow its site's full arity — the lm_head GEMM with its max-logits row reduction — has every seed of the shared segment decline on companion coverage, and the finer cut frees the site into its own kernel; segments whose digest is new versus the coarse segmentation seed fine-flagged singles, one composite recombines the fine keys' best-timed singles (coarse-timed bests staff the digest-identical segments); a fine winner's cached segmentation is the fine one, so its replay needs no record of the mode.detect_conv), the implicit-GEMM pipeline — the packing Stage serving as im2col, the micro-kernel the ordinary Tile_mma. On the C backends: serial and Grid-parallel flavors, the latter adopting the default preset's aligned whole-segment Grid geometry on merged segments (lenet's conv+bias/relu+pooling). On GPU backends with an mma capability: the staged flavor — outer output loops Grid-typed, both slices staged through cooperative shared tiles at the kernel-window anchor, the accumulator fragment resident across the window (gh-ocannl-480). Strided rows (stride-2 stems and downsample blocks) are seeded on both legs since the compacting Stage (gh-ocannl-502) packs the strided window densely.split_reduce_sites — an rmw accumulation, or the gh-466 Set_dynamic scatter, whose target has little output parallelism while a long serial reduction loop feeds it: bias and weight gradients of convolutions, softmax denominators, skinny split-K GEMMs), the deterministic two-pass split reduction (Ir.Schedule.Split_reduce) with a few num_blocks values as the tunable. The prelude applies whole-routine before fission: the per-block partials edge it mints is exactly the materialized cross-nest edge kernel fission cuts at, so the two passes compile as separate kernels with the event chain supplying the grid-wide synchronization the combine needs, and each segment then gets the default preset (the block loop parallelizes pass 1). Sites are seeded as singles plus one composite recombining each site's best-timed num_blocks. A split winner persists as prelude + post-prelude per-segment schedules; note the numerics pin — the combine tree is a function of the schedule, so retuning may change low bits (see the schedule-cache docs).Tile_mma renders as the register-tiled vector micro-kernel (gh-ocannl-469). The loop enumeration descends into the Local_scope bodies the accumulation mints of Unroll ~materialize:true / Partition create (gh-ocannl-666, matching Schedule.rewrite_loop's reach since gh-ocannl-639), so a materialized unroll or partition does not hide the segment/inner loops from later rounds; scope-nested loops are proposed only for the ops that survive scope nesting — Tensorize's hardware lane loop does not, so its triples stay statement-level.Caveats (v1):
repeats until ~25 ms of accumulated per-launch samples — not queued-batch wall. Queued timing can consequently spend hundreds of milliseconds on its timed batches and calibration; time_routine documents the exact dispatch bounds. On sub-millisecond kernels a min-of-3 is launch-jitter roulette and can crown the wrong candidate. Static indices are bound to the midpoint of their declared ranges during timing and restored afterwards.Implementation note: the structured half of the candidate space — matmul/conv site detection, the composed schedule pipelines those sites parameterize, and the refinement trees whose leaves are the seed lists — lives in sketch_families.ml and is included here (gh-ocannl-580). Sketch_families' interface bounds that include to the construction contracts consumed here; the family entry points below (sketch_params, detect_conv, matmul_sketch_tree, sketch_schedule, sketch_path_traffic_floor, …) are defined there, and sketch_seed_params is the composition the search enumerates.
type sketch_params = {sk_gpu : Base.bool;sk_mma : Base.bool;sk_simd : Base.int;sk_bm : Base.int;sk_bn : Base.int;sk_bk : Base.int;sk_tm : Base.int;sk_tn : Base.int;sk_hoist : Base.bool;sk_grid : Base.bool;sk_pack_rest : Base.bool;sk_conv : Base.bool;sk_epilogue : Base.bool;sk_batch_grid : Base.bool;sk_batch_inner : Base.bool;sk_coalesce : Base.bool;sk_swizzle : Ir.Low_level.swizzle_kind Base.option;sk_depth : Base.int;sk_pack_prec : Ir.Ops.prec Base.option;sk_tile : Ir.Register_tile.t Base.option;}Parameters of one matmul-sketch seed candidate; see Sketch_families.sketch_params for field documentation. Exposed for tests (the seeding pre-filter of gh-ocannl-479 and the mixed grid-outermost shape of gh-ocannl-473 are asserted on directly).
type matmul_site = {m_i : Ir.Indexing.symbol;m_j : Ir.Indexing.symbol;m_k : Ir.Indexing.symbol;The innermost contraction loop — the one a pipeline's k-split divides; its extent is m_nk.
m_ni : Base.int;m_nj : Base.int;m_nk : Base.int;m_ko : (Ir.Indexing.symbol * Base.int) Base.list;Contraction loops enclosing m_k, in nest order (gh-ocannl-683): a multi-axis contraction — attention's out projection, whose weight carries two input axes — is a k-loop lowering has already split, and every pipeline treats these as k-block loops above the one its own k-split mints. Empty on single-axis contractions.
m_bo : (Ir.Indexing.symbol * Base.int) Base.list;m_bi : (Ir.Indexing.symbol * Base.int) Base.list;m_row_axis : Base.int;m_d : Ir.Tnode.t;m_a : Ir.Tnode.t;m_b : Ir.Tnode.t;m_zeroed : Base.bool;m_tb : Base.bool Base.option;m_fma : Base.bool;}A recognized matmul accumulation site; see the implementation's field docs. Exposed for tests (gh-ocannl-683 asserts on the contraction nest directly).
val detect_matmul : Ir.Low_level.t -> matmul_site Base.optionRecognize a matmul accumulation nest: a perfectly nested all-serial accumulation whose contraction loops are the innermost suffix absent from the accumulator's index map, every other loop owning a distinct accumulator axis, with the 2-D tile roles assigned per classify_matmul's operand rules and everything else batch. Reads off the extracted access relations like detect_conv, with the same legality_crosscheck soak. Exposed for tests.
val detect_matmul_canonical :
Ir.Low_level.optimized ->
(matmul_site * Ir.Schedule.schedule * Ir.Low_level.optimized) Base.optionThe matmul site every seeding and schedule-construction entry point uses (gh-ocannl-1183): the site, the enabling-interchange prefix every schedule built from it carries, and the code it was detected on. The prefix is empty, and the code the argument, for a site detect_matmul finds; otherwise it is an adjacent-Swap chain, each Op_legal, sinking the contraction loops of the first accumulation-of-a-product statement below its write loops (the backward contractions backprop lowers with the contraction loops outermost or interleaved), and the code is the interchanged one. None when the plain matcher fails and detect_conv succeeds: the conv family keeps its segments. Exposed for tests.
type conv_axis = {cx_o : Ir.Indexing.symbol;Output spatial symbol (a plain iterator of the output).
*)cx_no : Base.int;cx_k : Ir.Indexing.symbol;Kernel-window symbol (read by the kernel, not the output).
*)cx_nk : Base.int;cx_stride : Base.int;cx_dilation : Base.int;cx_offset : Base.int;Padding offset on the input access (<= 0 for padded convs).
}type conv_site = {c_loops : Ir.Indexing.symbol Base.list;c_outer : (Ir.Indexing.symbol * Base.int) Base.list;c_kernel : Ir.Indexing.symbol Base.list;c_axes : conv_axis Base.list;c_row : Ir.Indexing.symbol;c_nrow : Base.int;c_oc : Ir.Indexing.symbol;c_noc : Base.int;c_red : Ir.Indexing.symbol;c_nred : Base.int;c_d : Ir.Tnode.t;c_a : Ir.Tnode.t;c_b : Ir.Tnode.t;c_zeroed : Base.bool;c_fma : Base.bool;}A recognized convolution accumulation site (gh-ocannl-493); see the implementation's field docs. Exposed for tests.
val detect_conv : Ir.Low_level.t -> conv_site Base.optionRecognize a convolution accumulation nest: the output written at plain distinct iterators, one operand carrying affine components that mix an output symbol with a kernel-window symbol (the projections carry the strides, dilations, and padding offsets), the other operand reading the kernel window, exactly one out-channel and one reduction-channel symbol, with the out-channel at the output's last axis and a conv axis at its second-to-last (the implicit-GEMM row). Reads off the extracted access relations (Ir.Low_level.affine_accesses — the gh-494 artifact the op-legality oracle also consumes); under config legality_crosscheck the retained procedural matcher runs alongside and any divergence raises. Exposed for tests.
Boundary (gh-ocannl-912): rank-generic — any number of batch axes and of spatial axes (1-D, 2-D, 3-D) beyond the row become outer loops — but it refuses every SINGLETON axis, because lowering drops extent-1 loops and indexes the axis at a literal Fixed_idx: a batch of one or an output spatial extent of one (the output is no longer written at plain iterators), a single input channel or a k-by-1 window (the kernel is no longer read at plain iterators; with one input channel no reduction-channel loop remains for Tensorize (row, oc, ic)). A channel row of several axes is refused too (exactly one reduction channel). An all-singleton window (1x1, any stride) is a plain GEMM: detect_matmul owns it. Refused sites get no matmul or conv sketch seeds (the preset and split-reduce candidates are unaffected); test/operations/conv_detection_boundary is the witness.
val matmul_launch_geometry :
matmul_site ->
sketch_params ->
Ir.Schedule.launch_geometryThe launch geometry a GPU matmul seed will have, predicted from the parameters alone (gh-ocannl-709) — the grid's row-block .y extent and folded batch .z product, and the workgroup's register-split extents — so seeding can consult the same Ir.Schedule.launch_geometry_excess the pre-driver gate does and never propose a candidate the device would refuse. Ir.Schedule.unknown_launch_geometry for parameters that name no block geometry (every CPU pipeline: the C backends render annotated loops serially). Exposed for tests, which cross-check the prediction against an applied schedule's Ir.Low_level.launch_dims — a prediction that drifted from what the builders emit would withhold legal candidates.
val conv_launch_geometry :
conv_site ->
sketch_params ->
Ir.Schedule.launch_geometryThe launch geometry a GPU convolution seed will have (gh-ocannl-739): the site's outer output loops followed by the blocked flavor's row-block loop in grid nest order, and the tensorization lane as the sole workgroup loop. This lets convolution seeding consult Ir.Schedule.launch_geometry_excess before compiling a candidate. The prediction is a lower bound on the applied schedule's geometry; tests derive a real conv site through Ir.Schedule.fission_scheduled and cross-check every GPU seed against Ir.Low_level.launch_dims. Ir.Schedule.unknown_launch_geometry for CPU parameters.
val mma_format_triples :
a_prec:Ir.Ops.prec ->
b_prec:Ir.Ops.prec ->
d_prec:Ir.Ops.prec ->
(Ir.Backend_intf.mma_input_format
* Ir.Backend_intf.mma_input_format
* Ir.Backend_intf.mma_input_format)
Base.listThe (operand, operand, accumulator) format triples a site with these storage precisions resolves to, in preference order — the key the typed seeds look up in mma_format_tiles and mma_staged_layouts. Single-precision operands read as tf32 first only under Ir.Numerics.t.tf32_matmuls. Exposed for tests, which derive the storage precisions of an advertised triple from it rather than restating the format table.
val mma_tile_for_precisions_in_scope :
Ir.Backend_intf.mma_capability ->
scope:Ir.Backend_intf.mma_emission_scope ->
a_prec:Ir.Ops.prec ->
b_prec:Ir.Ops.prec ->
d_prec:Ir.Ops.prec ->
(Base.int * Base.int * Base.int) Base.optionThe advertised intrinsic tile a site with these storage precisions seeds in this emission scope under the current policy, None where the descriptor withholds it (no advertised format triple, or a wide-accumulator lifetime the scope lists exclude). Exposed for tests: it is the descriptor's verdict as seeding reads it, which cuda_mma_cutoffs compares against the arm resolver's floors at every cutoff (gh-ocannl-1214).
val sketch_seed_params :
is_gpu:Base.bool ->
is_cpu:Base.bool ->
limits:Ir.Backend_intf.hardware_limits ->
Ir.Low_level.optimized ->
sketch_params Base.listThe matmul-sketch seeds proposed for the given lowering: parameterized instantiations of the composed pipelines with dividing tile sizes, pre-filtered against rules that statically imply a declined rendering (gh-ocannl-479) — on GPU backends: the (operand, operand, accumulator) format tile advertised by limits.mma.mma_format_tiles, including policy-enabled TF32 and excluding combinations the backend supports at one accumulator width but not the other (gh-ocannl-545: CUDA's bf16 has no wmma accumulator of its own); on the C backends: operand-precision uniformity (f32/f64), the fused accumulation form, micro-kernel column extent at least one vector of lanes (limits.simd_vector_bytes), and transposed-B storage for shapes that read B in place. Builder preconditions the schedule builders settle identically for every tile completion — the zero-expansion row-axis rule, and the GPU sketches' companion-coverage rule per fusion flavor — also pre-filter here, as the family tree's construction-time refutations (gh-ocannl-577): a seed list never proposes a candidate that statically must fail its build. For the matmul family this list is Ir.Schedule_space.leaves of matmul_sketch_tree, epilogue twins included. Exposed for tests.
val coalesce_prefix :
Ir.Low_level.optimized ->
Ir.Schedule.schedule Base.optionSee Sketch_families.coalesce_prefix (gh-ocannl-1165). Exposed for tests.
val coalesced_seed_params :
is_gpu:Base.bool ->
is_cpu:Base.bool ->
limits:Ir.Backend_intf.hardware_limits ->
Ir.Low_level.optimized ->
sketch_params Base.listThe coalesced tuner branch (gh-ocannl-1165), which tune seeds beside sketch_seed_params's candidates, whole-routine and per fission segment: see Sketch_families.coalesced_seed_params. Its winners persist as ordinary saved schedules whose leading Ir.Schedule.Coalesce ops name their loops structurally.
val tensorized_capability_refutation :
is_gpu:Base.bool ->
is_cpu:Base.bool ->
limits:Ir.Backend_intf.hardware_limits ->
a_prec:Ir.Ops.prec ->
b_prec:Ir.Ops.prec ->
d_prec:Ir.Ops.prec ->
Base.string Base.optionWhy the backend and the configuration withhold the tensorized family from a matmul site with these operand and destination storage precisions, before any geometry or site structure is consulted — None when they do not (gh-ocannl-1115). matmul_sketch_tree refutes its tensorized branch with exactly this witness. On GPU: routine logging, an mma lane wider than the workgroup, no advertised format tile; on CPU, the register tiling's shape-independent rules: a usable vector file, two lanes at the compute precision, uniform vector-capable compute precisions, routine logging. Exposed so a test claims a tensorized seed's PRESENCE exactly where the seeder's own capability judgment admits it, never gating on the seed list under test.
module Family_decision : sig ... endval matmul_sketch_tree :
is_gpu:Base.bool ->
is_cpu:Base.bool ->
limits:Ir.Backend_intf.hardware_limits ->
Ir.Low_level.optimized ->
(Family_decision.t, sketch_params) Ir.Schedule_space.tree Base.optionThe matmul sketch family as a refinement tree (gh-ocannl-514 phase 1): decision levels — epilogue fusion, pipeline, packing shape, geometry, twins — whose lazily-refined choices depend on earlier commitments, and whose Ir.Schedule_space.leaves are exactly the family's sketch_seed_params, in enumeration order. None when no matmul site is detected. The root level is the Fuse_epilogue flavor (gh-ocannl-613): unfused first, then fused — refuted with the fusion recognizer's reason (Schedule.fuse_epilogue_witness) when the site's output feeds no fusable tail — so the twins enumerate after every unfused leaf and each flavor's construction-time verdicts are its own (gh-ocannl-577: companion coverage can pass fused where it fails unfused). Statically-decidable builder preconditions refute at the pipeline level — above the geometry menus and the tile lattice, so a family whose every completion must fail its candidate build never expands (gh-ocannl-577). The conv family factors the same way as a follow-up. Exposed for tests and as the phase-4 search driver's entry into the family space.
The exclusion witness marking the tile-size lattice branches beyond the curated geometry menus (gh-ocannl-514 phase 5): binary interval refinements over the staged tile sizes — every intrinsic-tile multiple of the row block crossed with every staged depth block — whose boxes carry corner-judged verdicts (a workgroup-memory floor at the most favorable corner refutes the whole box pre-expansion). Ir.Schedule_space.leaves never enumerates an excluded branch, so the tuner's seed lists are unchanged by the lattice's existence.
val lift_geometry_lattice :
(Family_decision.t, sketch_params) Ir.Schedule_space.tree ->
(Family_decision.t, sketch_params) Ir.Schedule_space.treeLift every geometry-lattice exclusion — the branches whose decision is Family_decision.Geometry Lattice, identified by that datum rather than by the exclusion's prose witness — preserving the laziness of everything else; lifted branches remain subject to legality (the box refutations), and other exclusions stay excluded. model_default's family search applies this under config model_default_geometry_lattice. Exposed for tests.
val sketch_path_traffic_floor :
limits:Ir.Backend_intf.hardware_limits ->
Ir.Low_level.optimized ->
Family_decision.path ->
Base.intThe certain-traffic increment (bytes) of a family-tree decision path (gh-ocannl-514 phase 5): traffic every completion below the path moves beyond the schedule-invariant Ir.Cost_model.completion_floor, read off the path's committed staging decisions as data (Family_decision, gh-ocannl-591; the decision also says which pipeline minted it, so the caller's backend kind is not needed alongside the path) — a committed staged geometry contributes its operand tiles' distinct-cell footprints exactly as Ir.Cost_model.analyze charges them on every leaf (in-kernel GPU stages read and write; CPU packed panels only under in-kernel serial packing — a hoisted panel replaces the original operand's reads rather than adding traffic), and a lattice box contributes its most favorable (smallest-tiles) corner priced at the same per-format intrinsic tile the lattice is built from, so the increment is monotone in refinement. 0 when no matmul site is detected or nothing is certain. Composed with the floor's legs, this is what makes the family bound non-uniform across the tree — the schedule-invariant floor differentiates only placements (phase 3), the increments differentiate the sketch-geometry subtrees. Detection runs once at partial application; the returned closure is cheap per path. Exposed for tests.
val sketch_schedule :
accum_prec:(Ir.Ops.prec -> Ir.Ops.prec) ->
p:sketch_params ->
Ir.Low_level.optimized ->
Ir.Schedule.scheduleThe composed pipeline a seed parameterizes, built against the given lowering (the site is re-detected). Raises Invalid_argument when no site is detected or the parameters do not fit the segment. accum_prec is the rendering backend's accumulator resolution ((Context.codegen_capabilities ctx).accum_prec): the scalar pipelines' Privatize mints its tile at it (Ir.Schedule.privatize, gh-ocannl-1116). Exposed for tests (the pad-composition seeding of gh-ocannl-485 is executed directly).
val extend_with_privatize :
accum_prec:(Ir.Ops.prec -> Ir.Ops.prec) ->
static_indices:Ir.Indexing.static_symbol Base.list ->
Ir.Schedule.schedule ->
Ir.Low_level.optimized ->
Ir.Schedule.scheduleThe privatized preset extension used by the fissioned candidates: appends a Schedule.privatize ~accum_prec ~target ~over for every materialized read-modify-write accumulator detected in the schedule's application to the segment — over being the outermost enclosing Serial loop whose symbol the access vector does not mention and whose subtree contains no hardware-typed loop. Each proposal is validated by try-applying the grown schedule against a hermetic copy of the segment (proposals violating the op's preconditions are dropped), so the result always applies cleanly where the input schedule does. Exposed for tests.
type sr_site = {sr_axis : Ir.Indexing.symbol;The reduction loop to split.
*)sr_target : Ir.Tnode.t;The accumulated node.
*)sr_red : Base.int;The reduction loop's extent.
*)sr_out : Base.int;The target's cell count — the site's whole output parallelism.
*)sr_cost : Base.int;Estimated segment cost: the accumulation statement's trip count (the product of every enclosing loop extent) — the serial work a split could recover. The ranking key (gh-ocannl-541).
*)sr_dynamic : Base.bool;The gh-466 scatter form (Set_dynamic).
sr_swaps : (Ir.Indexing.symbol * Ir.Indexing.symbol) Base.list;The gh-ocannl-537 enabling interchange: (outer, inner) Swaps applied in order before the Split_reduce, each hoisting an accumulation-cell loop outside sr_axis. Empty when the site is splittable as lowered.
}A reduction-dominated accumulation site eligible for Ir.Schedule.Split_reduce seeding (gh-ocannl-484 task 3); see the implementation's field docs. Exposed for tests.
val split_reduce_sites :
?static_indices:Ir.Indexing.static_symbol Base.list ->
Ir.Low_level.optimized ->
sr_site Base.listThe split-reduce seeding sites of the given lowering: rmw accumulations (and gh-466 Set_dynamic scatters) whose target has at most a few thousand cells while a serial reduction loop of substantial extent feeds it — little output parallelism, lots of splittable reduction work. Per site the largest-extent enclosing serial loop that passes the hermetic Ir.Schedule.op_legality probe of the corresponding Split_reduce is chosen (the op's own recognizer decides the pinning discipline), so every returned site is seedable as proposed.
Every detected site is returned, ranked by descending estimated segment cost (sr_cost, the accumulation's trip count) — gh-ocannl-541: the earlier sr_red / sr_out integer-division ratio zeroed every large-output site, and the in-detection cap then silently excluded (and, once gh-537 made more sites reachable, evicted) exactly the sites carrying the most serial work. The candidate-volume cap now lives in tune (max_split_reduce_sites / config autotune_split_reduce_max_sites), which records evicted sites in the decline census.
A candidate rejected only because the accumulation cell's loops sit inside the reduction loop — how OCANNL lowers conv bias/weight gradients, where nothing else in the schedule space reaches the dominant segment — is re-probed after the enabling loop interchange (Ir.Schedule.split_reduce_hoist names the loops, each Swap confirmed Op_legal on the code it acts on); the chain is recorded in sr_swaps and replayed by the candidate's prelude. static_indices only reaches the interchange probe's Sched.apply. Exposed for tests.
gh-ocannl-685: spend a cap round-robin across named categories instead of as a prefix over their concatenation, returning the survivors (in the original category order, so an under-cap input comes back unchanged) and the per-category drop counts. One proposal per category per round, a category that runs out stops taking turns: every non-empty category is represented before any gets a second, and unused share spills to the others without imposing a ranking. Used for menu's per-unit action cap, whose list is a category-ordered concatenation and NOT ranked -- a plain prefix there starved every category after the first outright. Exposed for tests.
The beam-round action menu over one unit's transformed code — registry must resolve its loop binders, including symbols minted by the schedule applied so far (the search harness builds it with Ir.Schedule_cache.to_saved over that schedule): dividing serial Splits, Swaps of perfect serial pairs, Unrolls, Retype-Vectorized on innermost loops, and Tensorize role permutations, each proposal vetted by Ir.Schedule.op_legality (proven-illegal ones are pruned before they cost a candidate compile; Op_unknown proceeds to compile-and-time). The loop enumeration descends into accumulation-minted Local_scope bodies and treats binder-sharing mint copies as one decision (gh-ocannl-666). It descends virtualization's inlined computations too, but a loop reached through one draws the Retype-Vectorized proposal ALONE (Ir.Low_level.scope_mint, gh-ocannl-687): that is the one category with a renderer built for the shape (an inlined reduction's Set_local accumulation is what C_syntax.try_vectorize_reduce recognizes, and the enclosing loop -- whose body holds a Local_scope -- cannot be explicitly vectorized at all), while Splits, Swaps and Unrolls there are up to eight descriptors per loop that nothing proposed before gh-666 and that displace proposals for the main nest.
The per-unit action cap is shared across the categories by share_cap rather than spent in category order (gh-ocannl-685). admits filters proposals BEFORE that cap, so the budget is shared over moves the caller can use rather than over moves it is about to discard -- the beam passes its GPU dispatchability rule here, since an incumbent binding no hardware dimension can only be expanded through a move that binds one. It may record its refusals. Exposed for tests.
type decline_summary = {key : Ir.Schedule_outcome.rejection_key;count : Base.int;sample_details : Base.string Base.list;}Aggregate of candidate declines sharing one stable key. Details retain at most the first three distinct diagnostics and are never part of the key.
type terminal_failure = {phase : Ir.Schedule_outcome.phase;candidate : Base.string Base.option;detail : Base.string;}type abandonment = {ab_timed : Base.int;How many admitted timings the verdict was taken after: the search's beam width, the depth at which its first beam is full.
*)ab_best_ms : Base.float;This search's best after its first ab_timed admitted timings.
ab_incumbent_ms : Base.float;The incumbent's best after its first ab_timed — the same depth, not its final best.
ab_ratio : Base.float;The trailing ratio ab_best_ms exceeded, against ab_incumbent_ms.
}Why a search was abandoned by its caller's ?abandon rule (gh-ocannl-1110).
type outcome = | SearchedA search ran and completed: candidates were proposed and compiled or timed, leaving the process loaded with their modules and buffers.
*)| Search_died of terminal_failureA search ran and terminated on the carried fatal failure. The counters hold the work it had reached, and best_ms is a mid-search measurement of the search context: no routine was compiled from the caller's context for it, so it is never shippable (gh-ocannl-550).
| Cache_replayA cached winner replayed; no search ran in this process. The census is then empty except for a declined baseline: the base compile precedes the lookup, so its rejection is real information about this process on this device even though nothing was searched.
*)| Abandonment_replay of abandonmentCached timings under this call's search shape still satisfy its current ?abandon rule. No search ran, the counters describe only the base compile, and tune raises Search_abandoned without returning a routine.
| Search_disabledNothing was searched and there was nothing to replay: config autotune_search=false (the reproducible profile, gh-ocannl-559) with no chosen cache, or a chosen cache that missed. The caller gets the untuned default compile — the same code Context.compile would have produced. Every counter is zero and every time infinity; a declined baseline can still populate declines.
| Pre_search_failure of terminal_failureThe call failed before (or instead of) the search proper — a base compile that failed before its lowering was captured, a fatal baseline link, a fatal cache replay, a baseline timing failure, or an untuned fallback compile of a search-less call — and raised. It still reports (gh-ocannl-550), carrying whatever census the call had reached, so a caller attributing arms by arrival order (the positional ?report of Train.tune_placements) gets a slot for it.
| Abandoned of abandonmentThe caller's ?abandon rule stopped the search once its first timed candidates trailed the incumbent's at the same depth by more than the rule's ratio (gh-ocannl-1110), and tune raised Search_abandoned. Not a failure: the counters hold the work it reached, best_ms is a measurement of the search context, nothing was compiled for the caller and clean measured prefixes are cached for a later Abandonment_replay.
What the call did about searching (gh-ocannl-677). The states are mutually exclusive and each carries exactly its own data, so a consumer matches instead of re-deriving: "it searched" is Searched | Search_died _ and nothing else — in particular it is not not cache_hit, the reading that costs a benchmark harness every tuned cell under the reproducible profile, where a call reports having neither searched nor replayed. Spelled as four independent booleans until gh-ocannl-677, where two consumers mis-derived the state in one PR.
Note the arithmetic: five constructors for what reads as four outcomes, because "nothing searched" is two — a deliberate no-search that ships the untuned default and returns, and a failure before the search that reports and then raises. The old encoding could not tell them apart either, it just did not say so.
type timing_mode = | IsolatedOne launch followed by a host synchronization: the number is the latency of a lone dispatch, kernel plus one submit/sync round trip. What every schedule crowned before gh-ocannl-755 was ranked by, and the right objective for a workload that really does dispatch one kernel and wait for it.
*)| QueuedA calibrated number of launches dispatched back to back with one synchronization, divided by the count: the round trip is amortized and the number is what the kernel sustains inside a stream that already has work in it. The default, because that is what a training step presents — it queues every kernel of a layer and synchronizes at the end, so no kernel in it pays a round trip of its own.
*)type timing_sample = {per_launch_ms : Base.float;The quantity the sampling budget accumulates and the minimum ranks.
*)contention_ms : Base.float;Raw wall time used only for contention detection. Under Queued this is the whole batch before division by its depth, so a fixed host stall is not divided out of the signal.
}type timing_result = {ms : Base.float;contended : Base.bool;At least half the sample window was more than 2x slower than its minimum. This is a refusal signal: the window mostly measured host stalls, so the autotuner does not rank or cache the number (gh-ocannl-855). Dispersion only — a window whose minimum is non-positive or non-finite is a clock that resolved nothing, a separate fact that admitted_timing_ms refuses on the number itself (gh-ocannl-888). Not set for unbatched's refusal, which has its own reason (gh-ocannl-1098).
Because they are separate, not contended is HALF a proof of usability: a consumer deciding whether to keep a number asks admitted_timing_ms, never this field. Reading it directly is for saying something about contention itself — a diagnostic, a test that excuses a claim under host load.
unbatched : Base.bool;A CUDA/HIP queued calibration measured no batch within its target for a candidate whose single launch owed it one (gh-ocannl-1096), even at the rescue probe below its shallowest over-target batch (gh-ocannl-1098): only an isolated depth-1 reading was left, and the readings cannot tell a stall from a queue threshold, so the reading is refused exactly as a contended one is. The flag names what calibration MEASURED, not a diagnosis: host load stalling every batched probe reads the same. It is kept apart from contended because the two call for different follow-ups -- a no-batch refusal that persists across idle reruns is a threshold's signature, which a count mixed into the contention verdict hides. A refusal like contended: admitted_timing_ms admits neither.
samples : Base.int;The number of samples behind ms and contended, for diagnostics and exact dispatch accounting.
}val admitted_timing_ms : timing_result -> Base.float Base.optionThe shared admission gate for the consumers that RANK: candidate selection, the calibration rows, the roofline consistency check, cache attribution. Returns a positive finite ms only for an uncontended result; a refused or degenerate result is None. Deliberately NOT the gate for queued_batch_depth, which chooses a scale rather than consuming a measurement (gh-ocannl-888).
What a candidate's timing is a measurement of (gh-ocannl-755), selected by config autotune_timing.
The two are different objectives and they do not crown the same candidate. On gfx1151 the submit/sync round trip is ~50-60 us while the fastest candidates at the gpt2_mini out-projection shape run in 60-70 us, so Isolated reads about 2x the steady-state cost there — and the offset varies from candidate to candidate with the block count and the per-launch queue work (39-86 us over that site's ten seeded geometries, and up to 45 us of spread within a single run), which is what lets two candidates 5-8 us apart in steady state swap places — measured, in 2 of 8 runs. Re-measured on the same device at the same site (gh-ocannl-833, 2026-09-27) the offset had shrunk to 0-6 us (Isolated at most 1.08x Queued), and the crown still moved in 3 of 4 site-runs, Queued keeping the batched-throughput winner in all 4: the offset's candidate-to-candidate variation, not its size, is what reorders a close pair. The same site-runs moved the isolated crown in 3 of 4 on M4 Max Metal (offset 42-143 us) and in 0 of 4 on CUDA sm_120 (a uniform 3.8-5.6 us against a 10-18% gap between the top two), and Queued crowned the batched winner in every site-run on all three backends. Consequently a best_ms measured under Isolated is not a throughput number and must not be compared with a batched per-kernel figure; under Queued it is, up to the batch's residual ~1% of round trip.
type report = {outcome : outcome;What this call did about searching. The counters below say how much work that state got through; they never identify it — several are zero in more than one state.
*)candidates_timed : Base.int;Including the serial baseline where it was dispatched — on GPU backends it is not (gh-ocannl-532), and neither is any other candidate that binds no hardware dimension. So this count is not comparable across a CPU and a GPU backend: every serial-form candidate the CPU backends legitimately time is refused on GPU, and the refusals are counted in declines under Not_dispatched_key instead (gh-ocannl-543).
timings_contended : Base.int;Baseline and candidate timing windows refused because host contention dominated their samples or the clock result was non-positive/non-finite (gh-ocannl-855). Zero on cache replay and search-disabled calls, which time nothing in this process. Lets a completed search distinguish transient measurement refusal from structural candidate declines and retry when appropriate. Counts WINDOWS: a refused digest is dropped from the dedup set so an equivalent seed can retry it, so one candidate refused twice contributes two. Sound evidence for "was this search's measurement set complete?", and for nothing narrower — see candidates_contended and default_refused for the per-candidate facts. Includes the timings_unbatched windows.
timings_retried : Base.int;Baseline and candidate timing calls that started a fresh contention retry (Metal queued timing only). Counts each discarded first window once, whether its retry was admitted, refused or failed. Independent of timings_contended, which counts final refusals only. Zero on cache replay and search-disabled calls. Excludes diagnostic control timing.
timings_unbatched : Base.int;Of timings_contended, the windows refused because queued calibration measured no batch within its target (unbatched, gh-ocannl-1098) rather than for dispersion or a degenerate clock. Not a diagnosis -- a stall on every batched probe reads the same as a queue threshold the rescue probe could not get under -- but a count that persists across idle reruns is the threshold's signature, which the contention count alone hides. Every refusal still makes the measurement set incomplete.
candidates_contended : Base.int;Distinct candidate digests whose timing window was refused and which no later equivalent seed managed to time — the population timings_contended over-counts (Codex P2 on PR #608). This is the count that composes with candidates_timed and the Not_dispatched_key declines into "how many distinct candidates did this search reach", which a sum over refused windows cannot answer.
default_refused : Base.bool;The untuned-default seed's own digest had a refused timing window. Separates the two reasons default_ms can be None on a completed search: the load refused the reference's measurement, or the default seed was never proposed or never attributed — the gh-ocannl-552 regression, which a report-wide refusal count cannot distinguish from the first. False on cache replay and search-disabled calls.
candidates_failed : Base.int;Candidates rejected by op preconditions, hardware limits, or backend compilation — the serial baseline included (gh-ocannl-533) — plus detected seed sites declined before proposal (split-reduce sites evicted by max_split_reduce_sites, gh-ocannl-541) and candidates refused as unparallelized on a GPU backend (gh-ocannl-532), which the decline census records so a previously-proposed site — or a candidate space the backend's execution model empties — never stops being proposed silently.
baseline_declined : Base.bool;The serial baseline's own compile was rejected with a typed cause and the search ran on the scheduled candidates alone (gh-ocannl-533): baseline_ms is then infinity and the rejection is in declines like any candidate's. The HIP scratch validator declining the unscheduled serial form of a large softmax/cross-entropy head at Backend_link is the case this exists for — before it was contained, that one rejection ended the search.
declines : decline_summary Base.list;Candidate rejections aggregated by stable cause key. Their counts sum to candidates_failed. Cache-entry replay failures are excluded.
rounds_run : Base.int;Beam-expansion rounds actually executed (0 = seeds only).
*)beam_width : Base.int;The beam width the search resolved (?beam_width, else config autotune_beam_width): how many of the fastest candidates each round expands, and the admitted-timing depth an ?abandon rule decides at. 0 in no_search_report's template, which tune fills. With rounds_run, what a configured autotune_rounds / autotune_beam_width actually reached — a caller passing ~rounds:0, as the benchmark runners do, runs no round whatever the configuration says (gh-ocannl-1137).
sketch_candidates : Base.int;Whole-routine matmul-sketch instantiations seeded (0 when no matmul micro-kernel was detected or no tile sizes divide the extents), after the model pre-filter when one is active (keep_fraction < 1). Deterministic given the computation, backend, and configuration.
epilogue_sketch_candidates : Base.int;Of sketch_candidates, the fused-epilogue twins (gh-ocannl-486): seeded when the site's output feeds an eligible elementwise tail (Schedule.can_fuse_epilogue) — each sketch is then proposed both unfused and with Schedule.Fuse_epilogue appended, so the tuner measures the one-kernel fused form against the fissioned two-kernel form.
fiss_sketch_candidates : Base.int;Per-fission-segment sketch candidates seeded (0 when segmentation neither splits the computation nor exposes a changed lowering, or no segment contains a compatible site). GPU zero expansion can expose a sketchable singleton with a per-cell zero companion. Includes the finer-segmentation (arity_cuts, gh-ocannl-574) singles on GPU backends. Deterministic given the computation and backend.
fiss_sketch_timed : Base.int;Of the seeded per-fission-segment sketch candidates, those that compiled and were actually timed (not rejected by op preconditions or hardware limits, not deduplicated by digest). Includes completed timing windows refused as unusable; timings_contended identifies those verdicts. Includes coarse and fine recombination windows, so it can exceed fiss_sketch_candidates.
fiss_sketch_composite : [ `Ineligible
| `Singles_refused
| `Proposed
| `Refused
| `Timed ];Coarse recombination's own outcome (excluding the finer-fission composite): `Ineligible means no search, a decision not yet reached, or fewer than two usable coarse singles. `Singles_refused refines that absence when every missing segment key has a refused window and no admitted single. `Proposed means at least two usable singles staffed a proposal, but it did not complete a timing window; `Refused and `Timed record that proposal's refused or admitted window. Window outcomes are published before post-admission callbacks can raise, including in partial reports.
fiss_sketch_playoff_timed : Base.int;The composite playoff's windows (gh-ocannl-1166), counted here and NOT in fiss_sketch_timed. A per-segment single is timed against every other segment's untuned preset, a backdrop several times the composite's time, so near-tie singles are re-ranked inside the faster timed composite: per keyed segment, up to playoff_width singles within playoff_margin of the segment's best single take its place (constants in autotune.ml); changing either bumps Schedule_cache.entry_version. Includes refused windows, like fiss_sketch_timed.
fiss_sketch_playoff_swaps : Base.int;Of fiss_sketch_playoff_timed, the alternates that ran faster than the incumbent composite and replaced it, so later segments' alternates build on them.
split_reduce_candidates : Base.int;Split-reduce seeds (gh-ocannl-484 task 3): one candidate per split_reduce_sites site within the max_split_reduce_sites cap and eligible num_blocks value — the two-pass deterministic split reduction applied whole-routine before fission, each resulting segment getting the default preset. Deterministic given the computation and backend; 0 when no reduction-dominated site is detected. Sites the cap evicts appear in declines under Seed_evicted_key "split_reduce".
split_reduce_timed : Base.int;Of the split-reduce candidates (the per-site singles and the recombined multi-site composite), those that compiled and were actually timed. Includes completed timing windows refused as unusable.
*)split_reduce_composite_eligible : Base.bool;Whether at least two split-reduce sites supplied usable best-timed singles, making the multi-site composite eligible for proposal. False on cache replay and search-disabled calls.
*)split_reduce_composite_timed : Base.bool;Whether the eligible multi-site split-reduce composite compiled and reached a timing window. A completed window refused as unusable still counts: the field pins candidate reachability, independently of whether its timing verdict was usable. False when the composite was ineligible, on cache replay, and on search-disabled calls.
*)mma_candidates : Base.int;Candidates whose label promises a tensorized (Schedule.Tensorize) pipeline that the search put through candidate compile: whole-routine and per-fission-segment sketch seeds, the cross-segment recombination composite, composite-playoff alternates, and beam-expansion candidates. Counted at the same point as mma_timed, so the two always describe the same population.
fiss_mma_candidates : Base.int;Of mma_candidates, candidates built from per-fission-segment MMA sketches (including their recombination composites and composite-playoff alternates). This is counted at candidate compile, not enumeration, so it detects a fission MMA family that disappeared or never reached compilation without borrowing evidence from a whole-routine candidate.
mma_timed : Base.int;Of mma_candidates, those that compiled and were actually timed (dedup'd duplicates excluded — an identical candidate was already timed). Includes completed timing windows refused as unusable; timings_contended identifies those verdicts. mma_candidates > 0 with mma_timed = 0 means the search never reached a timing window for a tensorized pipeline at all, the state gh-ocannl-521 recorded for every GPU backend: candidates are cheap to enumerate and were being rejected in bulk at candidate compile.
model_scored : Base.int;Sketch candidates the analytic cost model scored during the seed pre-filter (gh-ocannl-491); 0 when the pre-filter is off (keep_fraction >= 1) or nothing was scoreable (e.g. no envelope constants).
model_pruned : Base.int;Of model_scored, the candidates dropped before compilation and timing. Candidates without model coverage are never counted here — they are always kept.
bound_pruned : Base.int;Candidates the measured-incumbent bound pruning skipped before compile (gh-ocannl-514 phase 4b, config autotune_bound_pruning): their schedule-invariant roofline floor met the best measured time so far — an admissible fathom, so no pruned candidate could have won. Counted apart from model_pruned (the keep-fraction pre-filter).
fissioned : Base.bool;The winning candidate compiles as multiple fissioned kernels; false when nothing was timed.
baseline_ms : Base.float;The unscheduled serial baseline's measured time, or infinity when it was not dispatched: on a GPU backend an unparallelized candidate is never run (gh-ocannl-532 — the whole routine in one work-item, unbounded in cost and uninterruptible), so it has no measurement and cannot win. Also infinity when baseline_declined.
default_ms : Base.float Base.option;The untuned default pipeline's measured time (gh-ocannl-552): the config_thresholds fissioned-preset seed reproduces Ir.Schedule.maybe_default_schedules exactly, so this is the schedule the user gets without tuning — the reference baseline_ms cannot provide on GPU backends, where it is infinity. Attributed by digest, so it is present even when the seed deduplicated against an identical earlier candidate (the timed serial baseline included, on CPU backends whose config thresholds leave the code unparallelized). On a completed search with default_ms = Some d, best_ms <= d by construction — the seed is in the pool — and the margin between them is the value tuning added (the question gh-ocannl-491 asks). The attribution honors the scheduling gates: with automatic scheduling inactive (Ir.Schedule.automatic_schedule_active) the untuned default is the unscheduled serial form and this field reports the baseline's measurement (so None on GPU, where that form is never dispatched); with config schedule_fission=false no candidate reproduces the whole-routine config-thresholds default and this is None. Also None when the seed failed to compile, was refused as unparallelized on GPU (gh-ocannl-532), or on a cache hit whose entry predates this field or was written under a config that shaped a different default pipeline (Ir.Schedule.default_schedule_fingerprint mismatch).
best_ms : Base.float;The winner's measured time, or infinity when nothing was timed at all — every candidate failed and the baseline was not dispatched (or was declined). In that case no cache entry is stored and the returned routine is the untuned default compile, not the serial baseline.
It is a reading of timing_mode, and which one is not recorded anywhere in the report or the cache entry, so a number carried across processes is only comparable to another taken under the same setting. Under the default Queued it is a per-launch steady-state time and can be compared with a batched per-kernel figure (up to the batch's residual ~1% of round trip). Under Isolated it is a lone dispatch's latency, kernel plus one submit/sync round trip: on gfx1151 that is a 26-105% inflation over the same candidate's steady-state cost at the gpt2_mini projection shapes, varying per candidate, and the number must NOT be read as a throughput figure (gh-ocannl-755). The same caution applies to baseline_ms, default_ms and mma_best_ms, which are the same instrument's readings; ratios between them are safe, since all four were taken under one setting within one search.
timing : timing_mode;The timing_mode every time in this report was measured under (gh-ocannl-755), including the times a Cache_replay carries: the objective is a cache-key component, so an entry this call could look up was measured under this call's objective. Never absent: tune resolves the objective before it constructs any report, and no_search_report takes it as an argument for the same reason.
It is here because nothing else records it. A consumer storing a best_ms in an artifact, or comparing one across processes, otherwise has to reconstruct the objective from ambient configuration — which a caller's explicit ?timing need not match, and which a later reader of the artifact does not have at all.
best_label : Base.string;The crowned candidate's spec label — the same string the autotune_log lines carry (e.g. "F_sketch[mma-gpu 16x32x32 ep]"). "baseline" when no candidate beat the serial baseline, and "" exactly when nothing was timed — including the states that time nothing by construction, which say so in outcome rather than through this string. Which candidate won is otherwise recoverable only by matching best_ms against the log's per-candidate times (gh-ocannl-546).
best_tensorized : Base.bool;The crowned schedule contains a Schedule.Tensorize — read off best_schedule, so this is what the winner is, not what its label promised. This is the fact a caller needs to state that a search's shipping artifact uses tensor cores; per-search mma_timed answers only whether one was measured.
It says what the schedule asked for. What the emission delivered is best_tensorization; the two disagreeing is exactly the false "tensorized" timing.
best_tensorization : Ir.C_syntax.tensorization Base.option;How the crowned candidate's Tile_mma statements actually rendered (gh-ocannl-626), read off its compiled routine's Context.routine.mma rather than re-derived by bracketing the census global: Tensorized when at least one tensor-core / SIMD-register-tile emission happened, Scalar_fallback when every emitted Tile_mma declined to the lane-0 scalar path, Not_requested when codegen emitted no Tile_mma at all.
None exactly when there is no crowned candidate to consult (nothing was timed, or the call never searched) — a report that consulted no census must not read as tensorized, so the absence is a distinct value and not a default of Not_requested.
Against best_tensorized this closes the reporting contract of gh-ocannl-545: a schedule that asked and got scalar code is best_tensorized = true with Scalar_fallback, one that asked and emitted nothing is best_tensorized = true with Not_requested, and a genuinely tensorized artifact is best_tensorized = true with Tensorized.
best_mma_statements : Base.int;Tile_mma statements the crowned candidate emitted, and of those, how many rendered as the lane-0 scalar fallback (best_mma_scalar_fallbacks). The pair keeps the reporting contract's distinction (gh-ocannl-545): best_tensorized with best_mma_scalar_fallbacks > 0 is a schedule that carries a Tensorize and executes scalar code, and best_tensorized with best_mma_statements = 0 is one that emitted no tensorized statement at all. A genuinely tensorized artifact is best_tensorized with best_mma_statements > 0 and best_mma_scalar_fallbacks = 0.
best_mma_scalar_fallbacks : Base.int;mma_best_ms : Base.float;The best timed tensorized candidate's time, infinity when none was timed (gh-ocannl-546). Against best_ms this is the margin by which tensorization won or lost this search, which is the difference between "the tensorized pipeline is uncompetitive here" and "it lost inside measurement noise". Where this field carries a measurement at all — Float.is_finite mma_best_ms, the only state in which comparing it to best_ms is meaningful — best_tensorized implies mma_best_ms <= best_ms, the winner being a member of the population it minimizes over. An infinite mma_best_ms is the absence of a measurement and stands under any best_tensorized: a replay of an entry written before the field existed (see the cache paragraph below) reports a tensorized winner with a finite best_ms and no family time at all, and asserting the implication unconditionally would call that legacy replay inconsistent.
It does not imply equality, and neither does not best_tensorized imply mma_best_ms >= best_ms (gh-ocannl-716). A beam round is accepted only when it improves on the incumbent by at least min_progress (1%); a round that improves by less is rejected with the incumbent still crowned, yet its candidates were timed, so a tensorized one among them can have lowered mma_best_ms below best_ms by up to that band. Callers reading the margin as a profitability ratio (family_profit_of_report) are unaffected — a ratio just under 1.0 means the family tied the winner inside the beam's own tolerance — but a caller asserting the strict inequality is asserting a coin toss, which is how it reached a CI failure.
Its population is structural — timed candidates whose schedule contains a Schedule.Tensorize — and therefore differs from mma_timed's label-promised one, in both directions. A beam move can append a Tensorize to a saved or preset incumbent, producing a candidate that is tensorized while its label promises nothing (so it counts here and not in mma_timed); conversely a labeled candidate whose applied schedule carries no Tensorize counts in mma_timed and not here. Same choice as best_tensorized, for the same reason: what shipped is a property of the schedule.
On a cache hit it is the STORING search's measurement, exactly as best_ms and baseline_ms there are (gh-ocannl-579): this process timed nothing, but the counters are what describe this call, while the times describe the program — under a key regime that already makes those two replayable. Without that the flip chain's profitability term would rank the decision surface one way on the cold run that measured the family and the other way on every warm-cache run after it. infinity when the storing search timed no tensorized candidate, and for entries written before the field existed.
best_schedule : Ir.Schedule_cache.saved_schedule;The winner's schedule; for a fissioned winner, the concatenation of the per-segment schedules (informational). Empty when nothing was timed.
*)source_digest : Base.string;The digest of the base lowering this call tuned (gh-ocannl-1022): Ir.Schedule_cache.digest of the placement-aware canonical form the base compile captured inside its lowered_transform, in the search's lineage (timing_ctx when one was given). It is the digest the call's schedule entry is stored and looked up under, and what Train.tune_placements records as a placement decision's outcome_digest — what the shipped search actually tuned, rather than what a re-lowering says it would have.
Carried by every report the call emits once the capture has happened: completed searches, cache replays, dead searches, a search-less call's cache miss, and the pre-search failures that follow the base compile. "" exactly when the call never reached a base lowering: a search-less call with no cache directory (which skips the base compile), and a failure before the base compile's transform ran (a failure after it, at codegen or link, carries the digest of the lowering it had captured).
best_steps : (Base.int * Base.float) Base.list;The best-so-far as a step function of the admitted timings (gh-ocannl-1110): (n, ms) says the search's best became ms at its n-th admitted timing (the dispatched baseline is the first), ascending in n and descending in ms, so best_after reads the best at any depth. What Train.tune_placements' flip chain abandons a hopeless flip against. A Cache_replay carries the storing search's steps, like its times, when that search had this call's shape (Ir.Schedule_cache.trajectory); empty when nothing was timed, for an entry older than the field, and for one stored under another shape.
}val no_search_report : timing:timing_mode -> reportThe report of a tune call that never searched (config autotune_search=false, gh-ocannl-559, and no cache entry to replay): outcome = Search_disabled, every counter zero, every time infinity, best_label empty, best_tensorization = None and source_digest = "". The caller gets the untuned default compile. Also the base the pre-search failure reports are built on, with outcome replaced and whatever census the call had reached filled in.
timing is the objective the call resolved, which is all that distinguishes one of these from another: it names what the (absent) times would have been measured under, and keeps report.timing a plain field rather than an option every consumer of a real report would have to unwrap. A caller synthesizing a report — a test building a search outcome to feed family_profit_of_reports, say — passes whichever objective its scenario is about.
A search's cost record, for a search that may be killed before it reports: with config autotune_progress=true (default false), or for one call its ~progress:true, tune and Train.tune_placements write single lines to stderr, each flushed as it is written, of the form
autotune-progress: wall_s=<s> event=<event> <key>=<value> ...
wall_s is seconds since the process started (this module's initialization); string values are OCaml-quoted (%S), so a line splits on spaces outside quotes, and a time that was never measured prints as none. The events, with the keys each carries beyond wall_s and event:
search_start (tune): routine, elapsed_s, backend, device, search, beam, rounds.phase (tune, one per phase with a known size): routine, elapsed_s, phase (seeds or round<k>), candidates (the phase's total), timed, attempts, compile_s, timing_s, best_ms, best.candidate (tune, as EVERY candidate attempt starts): routine, elapsed_s, phase (seeds, recombine, playoff or round<k>), tried=<k>/<total> (the ordinal of the attempt starting within its phase; ? when the phase's total is not known up front), timed, attempts (this one included), compile_s, timing_s, best_ms, best (all as of the attempt's start), and attempt, the starting candidate's label. A search killed inside a candidate is therefore inside the one its last candidate line names. The rate is bounded by the attempts themselves, not by a clock: a time bound would leave a window in which the candidate being worked on is unnamed.stage (tune): routine, elapsed_s, stage, attempts, compile_s, timing_s, written BEFORE each step outside the candidates that can block for long: base_compile, cache_replay, baseline_timing, seed_enumeration (the lowerings the seeds are derived from), winner_compile, untuned_default_compile, untuned_control (under autotune_log or a ?log only). From Train.tune_placements, stage alone: placement_store (the decision problem's lowering and replay check) and flip_surface. Together with candidate and arm_start this is the rule the stream keeps: every step that can block is named by a line written before it, so a killed search is inside the step its last line names.search_done (tune, exactly once per call, with its report): routine, elapsed_s, outcome (outcome_name), timed, contended, timings_retried, unbatched, failed, rounds, attempts, compile_s, timing_s, best_ms, best.arm_start / arm_done (Train.tune_placements, around each arm's, flip's or replayed placement's search): arm; a flip's carry flip=<k>/<budget>; arm_done adds result (ok, failed or abandoned), best_ms and elapsed_s. An abandoned flip's best_ms is its best at the verdict's depth, followed by that depth as after and the incumbent's best there as incumbent_ms (gh-ocannl-1110).flips_start / flips_done (Train.tune_placements): candidates and budget; flips_done adds measured, pruned, abandoned (of the measured), improved (whether the chain beat the A/B winner) and best_ms.elapsed_s is seconds since that search (or arm) started; compile_s and timing_s are the parts of it spent in candidate compiles and in candidate timing windows, attempts the candidates attempted so far (bound-pruned, declined, deduplicated and timed alike), counting one whose fatal failure ended the search.
Whether the progress lines are on in this domain: the ?progress of the innermost enclosing tune or Train.tune_placements call that passed one, else config autotune_progress.
Whether the autotune: diagnostics (and the untuned-default control they gate) are on in this domain: the ?log of the innermost enclosing tune or Train.tune_placements call that passed one, else config autotune_log.
with_streams ?log ?progress f runs f with log_enabled and progress_enabled overridden by whichever of the two is given, in the calling domain only, restoring the enclosing settings on return or exception. It is how tune and Train.tune_placements scope their arguments; an omitted argument leaves the enclosing setting in place.
Writes one progress line (prefix and wall_s added) when progress_enabled; the format supplies event=... and the rest.
A time in milliseconds as progress lines print it: %.4f, or none when not finite.
Starts a clock; the returned function reads the seconds since.
val outcome_name : outcome -> Base.stringThe stable one-word name of an outcome state — "searched", "search-died", "cache-replay", "search-disabled", "pre-search-failure", "abandoned" — for logs, JSON records and test goldens.
val terminal_failure : report -> terminal_failure Base.optionThe fatal failure that ended the call, from whichever of the two failing states carried it; None otherwise. A projection over outcome, not a re-derivation of it: "did this call fail" spans two states, and every caller that ranks or attributes arms asks exactly that — an arm carrying one is never the shipped arm, whatever its pre-failure best_ms says (gh-ocannl-550). The failure's phase is the one it carries — where the call actually died (at link, at launch, at sync), not where the report was assembled.
tune reports exactly once per call, on every path that does any work. Argument validation is the exception, and deliberately so: an incompatible timing_ctx is a precondition violation detected before anything happens, not an outcome of a search, and reporting it would attribute a phase to a call that never reached one.
val model_score :
static_indices:Ir.Indexing.static_symbol Base.list ->
limits:Ir.Backend_intf.hardware_limits ->
Ir.Low_level.optimized ->
Ir.Schedule.schedule ->
Base.float Base.optionThe analytic cost model's ranking score of a candidate schedule (gh-ocannl-491, the selection half): Ir.Schedule.apply on a hermetic copy, Ir.Cost_model.analyze, then the roofline lower-bound seconds under the envelope constants — limits' advisory peak_flops / peak_memory_bandwidth, each overridable by config model_peak_flops / model_peak_memory_bandwidth (calibrated per-machine values beat the class constants). None — no model coverage — when the schedule fails to apply, the code is opaque to the extraction (its counts may under-estimate, so ranking on them could prune the true winner), or no envelope constant is present. A ranking score, not a runtime prediction. Exposed for tests.
val envelope_legs :
limits:Ir.Backend_intf.hardware_limits ->
(Base.float * [ `Config | `Backend ]) Base.option
* (Base.float * [ `Config | `Backend ]) Base.optionThe envelope constants model_score scores against — (peak_flops, peak_memory_bandwidth), FLOP/s and bytes/s — each with where it came from: `Config for a model_peak_flops / model_peak_memory_bandwidth override, `Backend for limits' advisory class constant. None for a leg neither provides (the C backends carry no class constant). Exposed so that a report scoring a measured kernel against the envelope reads the very constants the model ranks with, and can say whose they are (gh-ocannl-1006).
val model_prefilter :
keep_fraction:Base.float ->
('a * Base.float Base.option) Base.list ->
('a * Base.float Base.option) Base.listThe order-preserving pre-filter over model-scored candidates: keeps every unscored (None) candidate — the no-coverage exemption: never dropped, only measured — plus the best ceil (keep_fraction * n) of the n scored ones (at least one; ties at the cutoff are all kept, so the outcome is independent of enumeration order). The identity when keep_fraction >= 1. Exposed for tests.
val placement_enablement :
limits:Ir.Backend_intf.hardware_limits ->
static_indices:Ir.Indexing.static_symbol Base.list ->
base:Ir.Low_level.optimized ->
allmat:Ir.Low_level.optimized ->
Base.Set.M(Ir.Tnode).t * Base.Set.M(Ir.Tnode).tThe enablement prior of the placement levels (gh-ocannl-514, the gh-ocannl-558 lesson): given the default-policy lowering and its all-materialized specialization, classify the flip candidates by whether their placement decides a sketch family's expressibility — computable from the seeders' own site classification, before any compile. Returns (enablement, disablement):
enablement: operand/destination nodes of an mma-eligible matmul site (the backend advertises a format tile and the wide-f16 policy's required scope for the site's unstaged form, whole-routine or per-fission-segment — the granularity the seeders detect at) that exists in the all-materialized lowering but has no eligible counterpart under default placements. Materializing such a node is what makes the tensorized family expressible — the flip changes the feasible set, not just the objective, which the per-node recompute-cost bound has no term for (on gh-558's mlp_wide/hip/bf16, cost ranking buried the family-unlocking cast twins below four no-op `Inline flips and a budget-5 chain found nothing).disablement: operand/destination nodes of sites already eligible under default placements. An `Inline flip of a node in either set can only move away from an eligible site — rank_flip_candidates demotes those.Empty on backends without an mma capability. A classified fission failure degrades to whole-routine detection; the classification is a ranking prior, never a legality fact.
type family_profit = | UnmeasuredNo completed search timed a tensorized candidate (mma_best_ms infinite), so nothing is known about the family's speed here — including the gh-ocannl-521 state (many seeded, none surviving candidate compile), which is a fact about candidate compilation, not about the family. Read from mma_best_ms rather than mma_timed: the latter counts LABEL-promised candidates, and a beam-appended Tensorize on a saved incumbent measures the family without promising it in a label.
| Pays of Base.floatThe best timed tensorized candidate (mma_best_ms, structural rather than label-keyed) was within the profit margin of the search's best, at this ratio of it (<= 1. when the family won outright).
| Loses of Base.floatIt lost by more than the margin, at this ratio of the search's best. On gh-514's metal/f16 mlp_wide cell the ratio is ~12 (mma_best 79-92 ms against 7.5 ms).
Parse config tune_flip_profit_margin (gh-ocannl-579): a ratio of at least 1.0. Anything else — unparseable, non-finite, or below 1.0, which would demote a family that WON — raises Utils.User_error rather than falling back to the default, since a run that asked for a profitability policy it also made impossible should not quietly get a different one. Exposed for tests.
Train.tune_placements' flip chain runs a full tune per tried flip, and on gh-719's approximate CUDA gpt2_mini cell two flips that ended 9-15x behind the arm they refined took 4747 s of a 9145 s search. The rule that cuts them is a comparison at EQUAL DEPTH: a flip is abandoned once its best after its first k admitted timings trails the incumbent's best after the incumbent's own first k by more than trailing_ratio. Against the incumbent's FINAL best the same rule would abandon every flip, the profitable ones included: that cell's arm A itself sat at 80.6 ms — 11.7x its final 6.862 ms — for 207 of its 209 timed candidates, and only the last two (the recombination composites) brought it down. At equal depth the two flips trailed by 2.0x and 1.7x, and arm B (the materialize-all specialization, the other direction a flip can take) by 1.07x.
exception Search_abandoned of abandonmentRaised by tune when its ?abandon rule decides, including from cached evidence. Its report (Abandoned or Abandonment_replay) has already been delivered, and every candidate this call compiled released.
best_after steps k: the best after the first k admitted timings of the search whose report.best_steps is steps — its final best when it timed fewer than k, infinity when it timed nothing.
val abandon_verdict :
abandon_rule ->
k:Base.int ->
steps:(Base.int * Base.float) Base.list ->
abandonment Base.optionSome exactly when best_after steps k exceeds trailing_ratio times the incumbent's best_after at the same k — strictly, so a flip trailing by exactly the ratio keeps searching — and the incumbent timed something by then. tune decides once, as soon as it has k = beam_width admitted timings (the depth at which its first beam is full), before its next attempt. Exposed for tests.
The trailing ratio the flip chain abandons at: config tune_flip_profit_margin, squared (1.5625 at the default 1.25). The margin is the gap the chain already calls a measured loss for a family; squared, a flip must trail by that much once more than the rest of its search could be granted to catch up. That separates gh-719's hopeless flips (2.0x, 1.7x at equal depth) from arm B's 1.07x with room on both sides. margin overrides the configured one; a malformed configured value raises Utils.User_error as flip_profit_margin_of_string does.
val family_profit_of_report : ?margin:Base.float -> report -> family_profitval family_profit_of_reports :
?margin:Base.float ->
report Base.list ->
family_profitWhat completed searches measured about the tensorized family's profitability on this device (gh-ocannl-579): mma_best_ms against best_ms, compared to config tune_flip_profit_margin. A report with any timings_contended contributes Unmeasured: its admitted minima cover an incomplete candidate set and cannot safely price the family. Over several reports the most favourable evidence wins — the expressibility prior is deleted only by evidence that contradicts it, never by the absence of a confirmation. A failing arm's report counts: its timings are measurements of the family even though its best_ms is not shippable. Exposed for tests and for Train.tune_placements, which derives it from the placement A/B's two arm reports.
val family_profit_summary : family_profit -> Base.stringA log-line phrase naming the evidence and its ratio.
val effective_flip_ordering :
ordering:[ `Cost | `Enablement | `Profitable ] ->
profit:family_profit ->
[ `Cost | `Enablement ]The ordering `Profitable resolves to under the given evidence: `Enablement when the family is unmeasured or competitive, `Cost when it is measured to lose here. Both of the prior's classes go at once, because the promotion of family-unlocking flips and the demotion of family-breaking ones are the same bet on the same family. Exposed for tests.
val rank_flip_candidates :
ordering:[ `Cost | `Enablement | `Profitable ] ->
?profit:family_profit ->
enablement:Base.Set.M(Ir.Tnode).t ->
disablement:Base.Set.M(Ir.Tnode).t ->
Ir.Low_level.flip_candidate Base.list ->
Ir.Low_level.flip_candidate Base.listDeduplicate (by Tn.uid, keep-first), drop refused alternatives (Ir.Low_level.fa_refused, gh-ocannl-1093: each replays to the materialized placement) and the nodes left with none, and rank the decision surface. `Cost is the legacy recompute-cost-descending order (the gh-555 chain's, kept as the evaluation baseline); `Enablement sorts family-unlocking `Materialize flips (enablement members) first and family-breaking `Inline flips (members of either set) last, cost-descending within each class. The unit ranked is the candidate (gh-ocannl-1017): each node's alternatives are sorted by that order and the node ranks where its best-ranked alternative does, so a top-N prefix is N nodes. `Profitable (gh-ocannl-579) is `Enablement weighed against profit per effective_flip_ordering — the prior models expressibility, and on a device where the family it unlocks is measured hopeless, promotion is pure opportunity cost that displaces the winning flip out of a small budget. profit defaults to Unmeasured (so a caller with no measurements, such as model_default, gets the prior). Config tune_flip_ordering selects the default ordering. Exposed for tests.
val placement_floor_withheld :
Ir.Low_level.flip_candidate Base.list ->
Base.boolWhether placement_surface withholds ps_floor_ms over these candidates: when one of them is not materialized by default (fc_default) and offers no `Materialize alternative — a node an earlier routine of the lineage left virtual and this routine footprint-scoped, which nothing here writes (gh-ocannl-616). The all-materialized specialization the floor is read from keeps that node's scratch, so the floor would not lower-bound its inline completion. Decided by the default reading (gh-ocannl-1017), not by which alternatives exist: a cap-materialized node with no smaller footprint offers the same lone `Inline and leaves the floor sound. Exposed for tests.
type placement_surface = {ps_candidates : Ir.Low_level.flip_candidate Base.list;Deduplicated, ranked per rank_flip_candidates under config tune_flip_ordering: one candidate per node, its alternatives in rank order, none of them refused.
ps_ordering : [ `Cost | `Enablement ];The ordering ps_candidates actually came out in — with tune_flip_ordering=profitable (the default) this is where the measured evidence landed, so a log line or a test can say which prior ranked the surface rather than which one was configured.
ps_profit : family_profit Base.option;The evidence that decided ps_ordering, and None when the configured ordering is unconditional (cost or enablement) and no evidence was consulted — which is also why such a run never reads tune_flip_profit_margin and cannot fail on a malformed one.
ps_enablement : Base.Set.M(Ir.Tnode).t;ps_disablement : Base.Set.M(Ir.Tnode).t;ps_floor_ms : materialized:Ir.Tnode.t Base.list -> Base.float Base.option;The roofline floor (ms) of a partial placement vector: every completion in which materialized holds costs at least this much — Ir.Cost_model.completion_floor on the all-materialized specialization with the other candidates' placements open, under the same envelope constants as model_score. Monotone in materialized (commitments only narrow open_placement), so it is a sound branch-and-bound fathom: in the tuned regime, a flip whose floor meets the best measured time cannot win and is skipped without spending budget (the admissible direction — the bound already exceeds the incumbent's measurement). None when no envelope constant is present, or when placement_floor_withheld.
}The placement decision surface prepared for search (gh-ocannl-514): the per-node inline/materialize levels of the joint placement x sketch x fission space.
val placement_surface :
?name:Base.string ->
?ordering:[ `Cost | `Enablement | `Profitable ] ->
?evidence:report Base.list ->
Context.t ->
Ir.Assignments.comp ->
Ir.Indexing.unit_bindings ->
placement_surfaceRead and rank the decision surface from ctx (analyze-only — two hermetic lowerings via Context.lowered_for_decisions, sharing the gh-560 analysis cache; no backend codegen, no effect on ctx). name names the computation for those lowerings exactly as Context.compile's does, and is what makes this work for a comp carrying no Ir.Assignments.t.Block_comment (gh-ocannl-669). ordering defaults from config tune_flip_ordering (profitable). evidence is the completed searches `Profitable weighs the enablement prior against (family_profit_of_reports, reported as ps_profit): Train.tune_placements' flip refinement passes the placement A/B arms' reports, while model_default's placement search (config model_default_placements) passes none — it measures nothing, so the prior stands there. Under an unconditional ordering the evidence is not derived at all, so such a run never reads tune_flip_profit_margin.
type model_choice = {mc_label : Base.string;"default", or the winning candidate's spec label (matching the autotune_log labels).
mc_model_ms : Base.float Base.option;The winner's roofline lower bound in ms — a ranking score, not a runtime prediction; None when selection did not run.
mc_scored : Base.int;Model evaluations that produced a score (the default pipeline included; the fissioned flow also scores per segment).
*)mc_skipped : Base.int;Model evaluations without coverage, excluded from the ranking.
*)mc_rejected : Base.int;Candidates excluded from the ranking because their scheduled form fails Ir.Low_level.validate_parallel: the model cannot see that a schedule will not compile, and rates the tensorized families best, so on a backend where those are unbuildable it used to crown one and then degrade to the default (gh-ocannl-522).
}Config model_default_schedule: recipe-level untuned compiles (Train.to_routine, Train.run_once, the benchmark runners) route through model_default instead of Context.compile.
val compile_advisory :
?name:Base.string ->
?on_fallback:(Base.exn -> Base.unit) ->
?fallback_if:(Base.unit -> Base.bool) ->
(Ir.Low_level.optimized -> Ir.Low_level.optimized Base.list) ->
Context.t ->
Ir.Assignments.comp ->
Ir.Indexing.unit_bindings ->
Context.t * Context.routineContext.compile_outcome with advisory provenance and the given lowered_transform, falling back to a plain Context.compile — the ordinary default pipeline — for a classified compiler rejection, including validation in backend codegen. Fatal failures propagate without retrying. on_fallback is called with the public rendering of the cause when fallback fires. fallback_if (default: always) is consulted first, for transforms that may themselves have degraded to the default pipeline — false re-raises the original exception, backtrace included, instead of duplicating a compile that has nothing to fall back to. For advisory transforms only: a failure of the default pipeline itself propagates. name names the routine, exactly as Context.compile's does, and reaches the fallback compile too. See model_default (gh-ocannl-519).
val model_default :
?name:Base.string ->
?report:(model_choice -> Base.unit) ->
Context.t ->
Ir.Assignments.comp ->
Ir.Indexing.unit_bindings ->
Context.t * Context.routineA drop-in for Context.compile that raises the untuned floor (gh-ocannl-491 task 3): inside the compile's own transform seam, the untuned default pipeline and the sketch families (whole-routine, and per-fission-segment substitutions when the default fissions) are scored with the roofline model, and the model-argmin schedule is applied — zero measurement, one backend compile. Advisory by construction: a candidate without model coverage is never picked over the default, ties go to the default, and missing envelope constants, a disabled default annotator (Ir.Schedule.automatic_schedule_active), or any classified scoring, application, backend validation, or compilation failure fall back to the ordinary default pipeline — the reported model_choice then says "default". Fatal failures propagate without retrying. Once the compile is on that pipeline there is nothing left to fall back to, so its failures propagate as they would from Context.compile, without a duplicate attempt. Unlike tune, nothing is executed and no cache is involved — results depend only on the computation, backend, and envelope constants.
Being a drop-in for Context.compile includes its name (gh-ocannl-669): it names the routine and every artifact of this compile — the model pick's, the fallback's, and the hermetic lowerings of the placement search alike — and, as there, a comp carrying no Ir.Assignments.t.Block_comment requires it.
val set_test_bindings : Context.routine -> Base.unitBinds representative values for timing runs: ranged static indices at range / 2, and gh-490 symbolic extents at their upper bound range (the schedule-cache identity is extent-value-independent, so the single tuned entry is measured at the maximum). Unranged bindings are left at their current values. Exposed for tests and custom timing harnesses.
The wall time, in milliseconds, one Queued batch aims for (10 ms). Exposed so a test can state a batch-wall bound against the policy's own target rather than a copy of it.
The wall budget, in milliseconds, on one CUDA/HIP queued calibration's batch probes (gh-ocannl-1098): eight target-sized twelve-minimum probes. Once the probes' summed wall reaches it, no further validation, confirmation or retry starts and the calibration ends unresolved, for the wall-bounded fallback to decide from what was measured; only the one rescue probe may follow. Exposed so a test states the budget against the policy's own value.
The minima one queued calibration batch probe takes when its wall stays within twice the target (12); an over-target probe may stop at three. Exposed so a dispatch bound is stated against the policy's own count.
The most batch probes one CUDA/HIP queued calibration dispatches (9): the provisional probe, four validations, a confirmation, its stall retry, a sampled shallower crossing, and the rescue. Enforced alongside queue_calibration_wall_ms: once all but the last have started, only the rescue may follow. Exposed so time_routine's dispatch maximum is stated against it.
The maximum number of queued depth validations (4), including measured doublings toward an unmeasured cap. Exposed so synthetic timing fixtures hold convergence against its probe budget.
How far past the deepest batch it probed a CUDA/HIP queued calibration may settle, as a multiple of that depth (2, one doubling; gh-ocannl-1100). An affine projection or a linear scale can land far past every measured batch, where a queue cost may jump unseen; the settled depth is capped here after calibration ends. A projection to an unmeasured cap in depth validation uses its remaining probes to walk toward it by this factor (gh-ocannl-1144). Exposed so a test states the bound against the policy's own factor.
Queue-memory bound selected by canonical backend name: 2048 for CUDA/HIP, and the historical 200 for cc/Metal. Exposed with the neighboring pure calibration seams so the backend scoping of gh-ocannl-892 is pinned without running a full search.
val queued_batch_depth : timing_result -> Base.intHow many launches Queued puts in one batch, given an estimate of what one launch plus one synchronization costs. Aims at ~10 ms of wall time per batch, capped at 2048 launches and floored at 1 — so a routine at or above the target is batched at depth 1 and measured exactly as Isolated measures it, and a microsecond routine cannot mint an unbounded batch. A non-positive or NaN estimate is a clock that resolved nothing rather than a zero-cost kernel and batches at the cap; an infinite one floors at 1, the far end of the scale the policy is for. Total, over every float and every verdict: a positive subnormal estimate saturates rather than raising. Exposed because those two boundaries are what a regression would cross silently: a depth stuck at 1 turns a queued search back into an isolated one.
contended is deliberately unread (gh-ocannl-888). The 2x-majority rule judges a ~10 ms batch, while this estimate is one dispatch plus one synchronization, whose dispersion on a GPU is the round trip's own tail; refusing on it starved every search on both GPU backends. A depth is not a measurement — an overestimate only shortens the batch, an underestimate is capped — and a deeper batch is the remedy for that dispersion, so the refusal belongs downstream in the timed loop, where the window judged IS a batch.
val refine_queued_batch_depth :
single_ms:Base.float ->
probe_depth:Base.int ->
probe_ms:Base.float ->
Base.int * Base.floatRefines a provisional queued depth from the synchronized single-launch minimum and the wall minimum of a batch at probe_depth. Returns the final depth and its estimated whole-batch wall. The two observations separate fixed synchronization cost from marginal launch cost, so even a shallow provisional batch reaches the ~10 ms contention scale rather than counting a fraction of the fixed cost against every launch. A probe exactly at the target keeps its depth; one that overshoots is interpolated back inside the measured bracket. An unresolved pair requests a doubled retry and returns nan for the wall rather than jumping to the cap or labeling the shallow probe as a cap-wall estimate; repeated unresolved batch pairs eventually reach the 2048-launch memory cap, which calibrate_and_time then bounds by the deepest depth measured within the target (gh-ocannl-1096). Exposed as the deterministic policy seam for tests.
val refine_queued_batch_depth_between :
base_depth:Base.int ->
base_ms:Base.float ->
probe_depth:Base.int ->
probe_ms:Base.float ->
Base.int * Base.floatThe same affine refinement between two batch observations. An already-target-sized base_ms is retained only when the pair's inferred fixed component is below the target and no more negative than a quarter of the larger of the target and base_ms (the noise tolerance matching the 25% confirmation step, relative to the base's wall since gh-ocannl-1098 so that a slow candidate's slightly superlinear pair still fits), and only when the base is the fit's first target crossing; a resolved base further over the target projects to that crossing instead, shallower than the base. This is the depth-separated confirmation that keeps a shared fixed stall or a physically invalid fit from selecting a shallow final depth while preserving legitimate submit/sync overhead. Otherwise an unresolved pair requests a deeper retry and reports a nan wall. Exposed as the deterministic validation-policy seam for tests.
The depth of the one rescue probe a CUDA/HIP calibration makes before refusing a candidate whose single launch owed it a batch but whose calibration settled at depth 1 (gh-ocannl-1098), from every (depth, minimum wall) it measured: the shallowest batch over the target, projected linearly down to the target, at least 2 and strictly shallower than that batch; None when no depth lies between. Exposed so a test can tell the rescue from the probes before it.
val sample_min :
repeats:Base.int ->
sample:(Base.unit -> timing_sample) ->
timing_resultPure sampling-policy seam used by calibration and the timed loop (gh-ocannl-855). Takes at least max 16 repeats samples; after that floor is met, tops up until their accumulated per_launch_ms reaches ~25 ms or 64 samples have been taken. Thus repeats > 64 deliberately overrides the top-up limit. Reports contended when at least half the raw contention_ms samples exceed their minimum by 2x — dispersion only; a minimum that is non-positive or non-finite is refused by admitted_timing_ms instead. Exposed so tests can inject a deterministic clock. Equal to window_result (sample_window ~repeats ~sample ()).
val sample_window :
?prior:timing_sample Base.list ->
repeats:Base.int ->
sample:(Base.unit -> timing_sample) ->
Base.unit ->
timing_sample Base.listsample_min's loop, returning the window itself, oldest first. prior (default empty) is a window already taken, which the loop resumes under the same stopping rule rather than restarting (gh-ocannl-1074): a resumed window stops exactly where one uninterrupted window over the same samples would, so it takes no fresh sample when prior already meets the floor and the top-up budget.
val window_result : timing_sample Base.list -> timing_resultA window's reading: its minimum per_launch_ms, its sample count, and the dispersion-only contended verdict sample_min documents.
val search_measurements_cacheable :
nothing_timed:Base.bool ->
timings_contended:Base.int ->
Base.boolPure cache-policy seam (gh-ocannl-855). A search result is cacheable only when at least one candidate was timed and no timing window was refused as unusable. The current call may still ship its best usable candidate, but an incomplete measurement set must be retried by a later cache-cold search.
val timing_string : timing_mode -> Base.stringThe mode's canonical spelling (isolated / queued) — what a cache key's "timing" component carries, what a report's mode is rendered as, and the round trip of timing_of_setting.
val timing_of_setting : Base.string -> timing_modeParses the autotune_timing spelling (isolated / queued, case- and space-insensitive); raises Invalid_argument on anything else.
val time_routine :
?on_retry:(Base.unit -> Base.unit) ->
?tag_failures:Base.bool ->
?phase:Base.string ->
timing:timing_mode ->
repeats:Base.int ->
Context.t ->
Context.routine ->
timing_resulton_retry defaults to a no-op and observes each fresh contention retry as it starts. The tuner's own instrument, exposed so a harness can rank candidates by exactly what a search ranks them by (gh-ocannl-755) rather than by a re-derivation of it. Binds test values (set_test_bindings) and restores the routine's bindings afterwards, runs one warmup, then minimizes the per-launch time over at least 16 and repeats timed runs, topping up until ~25 ms of per-launch samples has accumulated (up to 64 runs unless the repeats floor is larger) so that a host stall cannot collapse the min-of-N. Under ~timing:Queued a "run" is a whole batch of dispatches whose depth is calibrated per candidate to ~10 ms of wall, capped at 2048 on CUDA/HIP and at the historical 200 on cc/Metal, and floored at 1. cc/Metal retain the historical synchronized-single estimate. On CUDA/HIP that estimate instead seeds a provisional depth; when that is above 1, a provisional queued probe separates fixed synchronization from marginal launch cost and selects the depth whose affine wall estimate reaches the target. The batch probes take the minimum of twelve runs: unlike ranked timing, they only choose scale and are already milliseconds long. Since gh-ocannl-1098 both are wall-budgeted: a probe stops at three minima once its own wall passes two target-sized probes', and once the probes' summed wall reaches queue_calibration_wall_ms no further probe starts and the calibration ends unresolved. Up to four further probes validate the depth; a resolved crossing shallower than the batches it was fitted from is sampled and checked like an interpolation. The first target-sized batch is confirmed at a 25% deeper depth (clamped to and measured at the cap) and retained only when the pair's inferred fixed component is below the target. A confirmation over 2x its supported base is retried once before an unresolved pair selects the cap. Such a no-verdict fallback is then wall-bounded (gh-ocannl-1096): it never goes deeper than the deepest depth any calibration batch was measured at within the ~10 ms target, and is depth 1 when none was -- the only bound sound for any cost that grows with depth, since the readings that left the fits unresolved cannot tell a host stall from a queue threshold. So a slow candidate whose fits never resolve is timed at depth 1, not in 2048-launch batches, while a fast one whose deeper probes stalled still batches; one that measured no batch within the target at all first takes one rescue probe below its shallowest over-target batch, projected linearly to the target (gh-ocannl-1098) -- where a queue-threshold or superlinear cost reads within the target -- and is refused as unbatched only when that also reads over it, rather than timed as an isolated depth-1 reading. An unresolved single/probe pair retries at double depth, and the next affine fit uses the two batch observations so an inflated single window cannot force the cap. If the bounded loop first reaches the target on its last probe, the interpolated target depth is still sampled and checked against the measured overshoot. A non-monotone confirmation scales from the deeper measured batch, never the earlier suspect crossing. When a clean pair's fixed component already fills the target, its marginal slope selects a depth carrying ~10 ms of launch work instead of accepting a shallow stalled window or jumping to the cap. After four noisy but resolved underestimates, calibration keeps the latest affine projection rather than jumping to a 20--30 ms cap batch that would blunt the 2x contention threshold. None of these projections settles deeper than queue_depth_projection_factor times the deepest batch the calibration probed (gh-ocannl-1100): past it a queue cost can jump where nothing measured. A CUDA/HIP routine slower than the target is confirmed by a depth-2 probe, stays at depth 1, and is measured identically in both modes. When singles below the target owe batching, a resolved pair starting at depth 2 whose marginal launch cost fits the target and whose fixed term is below it retains that measured batch even if its fixed term puts the fitted depth-1 wall above the target (gh-ocannl-1184). It neither returns to the isolated singles nor refuses an ordinary integer-depth boundary; unresolved or over-target marginal fits keep their refusal. Whenever a queued call settles at depth 1 its timed window resumes the calibration's synchronized singles (sample_window's prior) rather than timing a fresh one (gh-ocannl-1074): at depth 1 they are samples of the very quantity the window measures, taken under the same stopping rule, so the loop dispatches only what the caller's repeats floor asks beyond the calibration's sixteen. The calibration always yields a depth; the result of the timed loop reports when most of ITS samples were stalled, and the tuner refuses such a candidate measurement rather than ranking and caching it (gh-ocannl-888). Since the budget is per-launch rather than batch wall, queued timing can spend up to max 64 repeats batches on a fast candidate; max_timing_runs bounds the top-up beyond the caller's requested floor. Metal queued timing retries a contention-refused window once at the same depth with fresh samples (gh-ocannl-1060). The retry faces the same admission rule; only its outcome reaches the search's refusal accounting. Other backends and isolated timing take no retry.
phase labels the call's on_batch_decision (gh-ocannl-1199); none by default.
With ~tag_failures:true the pre-dispatch validation, the launches and the synchronization are wrapped in their Ir.Schedule_outcome phases, which is what lets a caller's Ir.Schedule_outcome.protect attribute a failure to the phase it happened in; without it they propagate raw. Timing dispatches the routine repeatedly against live buffers, so an accumulating routine must be timed on a scratch lineage (see tune's ?timing_ctx) if its inputs matter afterwards. Queued raises how many such dispatches happen. For repeats <= 64, the maxima are 65 under Isolated and, under Queued, 352321 on CUDA/HIP, 12865 on cc or 25665 on Metal. The CUDA/HIP bound includes warmup, 64 single-launch calibration runs, at most queue_calibration_max_probes (nine) queue_batch_probe_runs-sample calibration probes (the provisional probe, four validations, a confirmation, its stall retry, a sampled shallower crossing, and the rescue) and 64 timed batches at the cap; cc/Metal have no batch probes. In general the queued bounds are 65 + 2048 * (108 + max 64 repeats) on CUDA/HIP and 65 + 200 * max 64 repeats on cc, twice the timed part on Metal for its one retry. Thus a routine whose values grow per run reaches larger ones. That is a fact about the scratch buffers, not about the measurement: the cap bounds each in-memory queue while the ~25 ms budget accumulates per-launch samples, and a candidate's time is not what it accumulated.
Observation seam for the timing tests (gh-ocannl-851), called by each time_routine call with the batch depth it settled on — after calibration, before the timed loop; Isolated always reports 1. The negative control for a twice-divided queued reading needs the depth the call ACTUALLY used: the call recalibrates independently, so re-applying queued_batch_depth to an estimate taken outside it guesses wrong exactly on the busy runners the control must survive. calibration_samples retains its historical label for source compatibility; its value is zero for Isolated and the actual number of launches spent on single-launch, provisional-batch and depth-validation calibration for Queued. The default is a no-op and no configuration selects it.
val calibrate_and_time :
retry_contended:Base.bool ->
timing:timing_mode ->
repeats:Base.int ->
queue_depth_cap:Base.int ->
batch:(Base.int -> Base.float) ->
timing_resulttime_routine after its warmup — the calibration and the timed loop, seams included — with the device reduced to batch depth, which must dispatch depth launches back to back, synchronize once and return the wall in milliseconds. queue_depth_cap is queue_depth_cap_for_backend's value for the backend being modelled; it also selects between the CUDA/HIP affine calibration and the historical cc/Metal single estimate. Exposed so a test can drive the whole timing policy on an injected clock and count its launches exactly (gh-ocannl-1074). retry_contended is retry_contended_window_for_backend's value for the backend being modelled; it enables one retry only under Queued, never Isolated.
val calibrate_and_time_with_retry_observer :
on_retry:(Base.unit -> Base.unit) ->
phase:Base.string Base.option ->
retry_contended:Base.bool ->
timing:timing_mode ->
repeats:Base.int ->
queue_depth_cap:Base.int ->
batch:(Base.int -> Base.float) ->
timing_resultcalibrate_and_time with the per-call observer that drives search accounting. on_retry runs once when a fresh contention retry starts, before on_timing_retry and before any retry dispatch. phase is reported as batch_decision's. The original helper remains a no-op-observer, phase-less wrapper for source compatibility.
The backend whose contended queued windows get one immediate retry: Metal only.
A discarded contention window, immediately before its one fresh retry. Counts the batches sampled and those reused from calibration, so extra dispatches can be accounted for. on_timed_window describes only the returned window; a recovered first window is not a candidate refusal. Default no-op, no configuration key selects it.
val on_timed_window :
(samples:Base.int ->
reused:Base.int ->
wall_ms:Base.float ->
median_wall_ms:Base.float ->
Base.unit)
Base.refObservation seam for the timing tests (gh-ocannl-994), called by each time_routine call once its timed loop has finished, with the number of batches that loop ran — counted by the loop rather than restated from its result, so a test can hold the two against each other — and their summed wall. It is the window the returned minimum was taken over, and so the window a bound on that minimum belongs against: the whole call's wall also holds the warmup and the calibration's synchronized singles, which on a backend whose host round trip is two orders of magnitude above an amortized launch are a comparable share of the call to every timed dispatch put together. A mean over the whole call is therefore diluted by construction, and a host stall in the untimed part moves it without moving the reading. median_wall_ms is the median of the same batch walls, reported alongside the sum because this window's mean is still a stalled minority's to move: sample_min declares contended on a majority of the window exceeding twice its floor, so a bound that must hold whenever the claim is not bypassed belongs on a statistic a minority cannot move — which the median is over exactly the regime the contention rule leaves to it. reused is how many of the window's samples the loop did not dispatch itself: a Queued call that settles at depth 1 resumes the calibration's synchronized singles as its window (gh-ocannl-1074), and those launches are already counted in on_batch_depth's calibration_samples, so the loop's own launches are depth * (samples - reused); it is 0 at every other depth and under Isolated. The default is a no-op and no configuration selects it.
type calibration_probe_role = | Provisional_probeAt the depth the synchronized singles' estimate provisionally picks.
*)| Validation_probeAt a depth an affine fit projected, checked against its base.
*)| Confirmation_probeOne step deeper than a target-sized batch, the depth-separated confirmation.
*)| Stall_retry_probeThe confirmation repeated once at the same depth, after it read as a stall.
*)| Crossing_probeAt a fit's target crossing, below a measured over-target batch.
*)| Rescue_probeAt rescue_depth, before refusing a candidate that measured no batch within the target; charged to no budget.
The branch of a CUDA/HIP Queued calibration that started a batch probe. The longest path queue_calibration_max_probes counts is one provisional probe, four validations, a confirmation, its stall retry, a sampled shallower crossing and the rescue.
type calibration_probe = {role : calibration_probe_role;depth : Base.int;runs : Base.int;wall_ms : Base.float;The probe's summed wall over its finite positive batches: what it charged to queue_calibration_wall_ms.
min_ms : Base.float;The minimum batch wall, the probe's reading.
*)}One batch probe of a calibration, as it ended.
val on_calibration_probe : (calibration_probe -> Base.unit) Base.refObservation seam for the timing tests (gh-ocannl-1119), called once per CUDA/HIP calibration batch probe, in dispatch order, as the probe ends. A test that sees only the batch calls of calibrate_and_time can reconstruct probes only from runs of same-depth batches, which merges a stall retry into the confirmation it repeats and counts probes as a lower bound; this reports each probe with the branch that started it. The cc/Metal calibration has no batch probes, and Isolated no calibration. The default is a no-op and no configuration selects it.
type fit_verdict = | Fit_unorderedThe probe was not deeper than the base: no slope, a doubled retry.
*)| Fit_unreadableA reading was not finite and positive: a doubled retry.
*)| Fit_nonpositive_marginalThe slope was not positive: unresolved, a doubled retry.
*)| Fit_negative_fixedThe fixed term was more negative than the noise tolerance allows: unresolved.
*)| Fit_fixed_dominatedThe fixed term alone fills the target: a depth carrying one target of marginal work.
*)| Fit_base_crossingThe base is the fit's first target crossing and is kept.
*)| Fit_boundary_floorThe crossing is shallower than the measured depth-2 base, which is kept anyway: singles owed a batch, the fixed term is below the target and the marginal work fits it (gh-ocannl-1184).
*)| Fit_shallower_crossingThe crossing, shallower than an over-target base.
*)| Fit_deeper_crossingThe crossing, deeper than an under-target base (an unmeasured depth).
*)What one affine fit of a CUDA/HIP Queued calibration concluded about its pair of batch minima: which branch of the refinement produced the depth it returned.
type affine_fit = {base_depth : Base.int;probe_depth : Base.int;fixed_ms : Base.float;The fitted synchronization term; nan when no slope was resolved.
marginal_ms : Base.float;The fitted per-launch term; nan when the readings gave no slope.
verdict : fit_verdict;}One affine fit of a calibration, as the refinement computed it.
type batch_settle = | Isolated_objective| Single_estimateThe cc/Metal policy: the synchronized singles' estimate alone.
*)| Singles_reach_targetThe singles reached the target and the provisional depth-2 pair confirmed depth 1.
*)| Measured_batchA resolved fit confirmed a depth a probe (or the singles) measured.
*)| Affine_projectionA resolved fit's projection to a depth no probe measured.
*)| Wall_scaledA linear scale from the deepest measured batch, the fit being unresolved.
*)| Cap_measuredThe cap, measured by a probe.
*)| Cap_unresolvedThe cap, wanted by an unresolved monotone pair, unmeasured.
*)| Budget_exhaustedThe probe wall or count budget ended the calibration unresolved, at the depth in hand.
*)Which exit of the calibration chose the depth, before the post-calibration bounds (batch_decision's bounded_from, fallback_from, rescue) adjusted it.
The call's admission verdict, in admitted_timing_ms's terms; a refusal names the first of unbatched, contended, an unreadable reading that holds.
type batch_decision = {phase : Base.string Base.option;The caller's phase: for a tune candidate, the search phase as its autotune_progress lines name it (seeds, recombine, playoff for the composite playoff of gh-ocannl-1166, or round<k>); None for the baseline, the untuned control and any other caller. Carried by the call, not read from shared state, so concurrent searches never label each other's decisions.
timing : timing_mode;depth : Base.int;cap : Base.int;The call's queue_depth_cap.
target_ms : Base.float;estimated_wall_ms : Base.float Base.option;The CUDA/HIP calibration's whole-batch wall estimate at depth, nan when unresolved; None under Isolated and the cc/Metal policy.
settle : batch_settle;fit : affine_fit Base.option;The calibration's latest affine fit, if it made one.
*)cap_directed_probes : Base.int;Validation probes whose depth a fit wanting the unmeasured cap directed: measured doublings toward the cap (gh-ocannl-1144), as against ordinary affine projections.
*)bounded_from : Base.int Base.option;The depth the exit chose, when queue_depth_projection_factor times the deepest probe bounded it (gh-ocannl-1100).
fallback_from : Base.int Base.option;The depth before the wall-bounded fallback shortened an unresolved outcome (gh-ocannl-1096).
*)rescue : (Base.int * Base.float) Base.option;The rescue probe's depth and minimum, when one ran (gh-ocannl-1098); depth equals it exactly when it read within the target.
probes : Base.int;probe_wall_ms : Base.float;budget_spent : Base.bool;Whether that budget was spent when the calibration ended.
*)admission : batch_admission;retried : Base.bool;Whether a contended first window was discarded for one fresh retry.
*)}One timing call's depth decision and admission verdict, from the policy's own metadata.
val on_batch_decision : (batch_decision -> Base.unit) Base.refTrace-only seam (gh-ocannl-1199), called once per time_routine / calibrate_and_time call that returns, after its on_timed_window, with the decision the call took: the fitted fixed and marginal costs, the exit that chose the depth, the bounds that moved it, the probe budget left and the admission verdict, so a consumer such as the benchmarks' BENCH_TIMING_TRACE never re-derives the calibration policy. Observing it changes nothing measured or ranked. The default is a no-op and no configuration selects it.
Fault-injection seam for the containment tests (gh-ocannl-550), called with each candidate's label just before its compile — including the baseline's, which is a candidate (gh-ocannl-533); that one is called inside the base compile's transform, so a fault injected there is classified like any other and surfaces as the pre-search failure of a search that never started. The default is a no-op and no configuration selects it; raising from it terminates the search the way an uncontainable failure does — the Search_died report (carrying its failure) is emitted to ?report and the exception propagates out of tune, which is what Train.tune_placements must survive without losing the other arm's winner. Not a production seam: candidate failures that a backend can attribute are contained without it (see declines).
Fault-injection seam for the pre-dispatch containment tests (gh-ocannl-564), called with a routine's name inside the Ir.Schedule_outcome.phase.Preflight region of its timing run, just before Context.check_launch_bindings. Raising from it classifies exactly as a real per-candidate validation failure does: for a candidate a contained decline under Unclassified_key (Preflight, _); for the baseline the propagating pre-search failure a baseline timing failure always is.
It exists because even the per-candidate trigger — an out-of-range static binding — needs a candidate whose static ranges differ from its siblings' to arise naturally, which the preset families do not supply. The lineage-wide triggers cannot be reached through it at all any more: Context.check_lineage_runnable runs outside this region (gh-ocannl-569), so injecting one of their exceptions here exercises the containment machinery with a realistic payload but does not mirror where a real one is now raised. Default a no-op; no configuration selects it.
Observation seam for the containment tests (gh-ocannl-898), called with a routine's name each time a timing run's window yields an admitted measurement — the moment the search's candidates_timed accounting grows, for the dispatched baseline and candidates alike. timed_so_far is that accounting's own value after this admission — what a report cut at this instant would state as candidates_timed — passed so a precondition of the form "the arm under injection has timed N candidates" pins the tuner's number rather than maintaining a second counter that can drift from it. on_candidate_preflight is upstream of the window's verdict and not a substitute: under the Queued objective a preflighted run can still be refused (a contended window, a degenerate clock reading; gh-ocannl-855) and then times nothing, so counted in preflights such a precondition fires, on a loaded device, on an arm whose report says it timed nothing. Default a no-op; no configuration selects it.
Config autotune_search (default true): what tune searches under when its caller passes no ?search.
The schedule cache directory a tuning call uses, or "" for none: cache_dir when given, else config autotune_cache_dir — except that with the search off only a CHOSEN directory (passed, or set at some config source) counts, the built-in default being treated as no cache (gh-ocannl-559). tune resolves its directory through this, and so does Train.tune_placements for the placement-decision store it keeps in the same directory (gh-ocannl-786), so the two open the same directory under the same rule.
val tune :
?name:Base.string ->
?search:Base.bool ->
?beam_width:Base.int ->
?rounds:Base.int ->
?repeats:Base.int ->
?timing:timing_mode ->
?seed_block_sizes:Base.int Base.list ->
?cache_dir:Base.string ->
?keep_fraction:Base.float ->
?max_split_reduce_sites:Base.int ->
?timing_ctx:Context.t ->
?abandon:abandon_rule ->
?log:Base.bool ->
?progress:Base.bool ->
?report:(report -> Base.unit) ->
Context.t ->
Ir.Assignments.comp ->
Ir.Indexing.unit_bindings ->
Context.t * Context.routineLike Context.compile, but returns the empirically fastest of the searched schedule candidates. The returned context/routine come from an ordinary sibling compile of ctx, so execution-dependency tracking behaves as if the winning compile were the only one. Raises like Context.run would (e.g. uninitialized inputs) — tune in the same state you would run in.
"Like Context.compile" includes its name (gh-ocannl-669) — without which a comp that carries no Ir.Assignments.t.Block_comment but that Context.compile can name (as Parallel's collective routines are named) could be compiled but not tuned. The schedule cache deliberately does not see it: a cache key is Ir.Schedule_cache.cache_key over the canonical lowering, the backend, the numerics and codegen tags, and the worker pool — and the name reaches only codegen's artifact and kernel naming, never the lowering. Two identical computations under different names therefore share one tuned entry, which is what the cache is for; naming a routine neither invalidates a crown nor mints a private one.
Memory (gh-ocannl-550): every candidate this searches is Context.released as soon as it stops being a beam survivor, so the search's live working pools and contexts are bounded by beam_width rather than by candidates attempted. The bound holds across contained failures and across dedups — a deduplicated candidate has still paid for a compile and a link — and the returned routine is the only artifact left when tune returns. Nothing about the returned value changes: it is the same live context and routine as before, from the same lineage.
The bound is on working pools, and that qualifier is load-bearing: Context.release cannot free per-device constants, and a hoisted Stage candidate mints a fresh packed constant per application. A search that seeds those (the CPU hoist sketches) therefore still grows one constant pool per such candidate — measured on cc at 1 -> 109 constant pools over 181 candidates, against working pools held within 2-6. Bounding it needs an eviction rule inside the shared constant cache, which is gh-ocannl-565's subject; pinned as far as it can be by test/operations/autotune_candidate_release.
val on_candidate_callback :
([ `Timed | `Calibration ] ->
candidate_ms:Base.float ->
incumbent_ms:Base.float ->
Base.unit)
Base.refDefault-no-op fault-injection seam immediately before each post-admission callback. The times are the admitted measurements the search ranks, after on_candidate_measured; a strictly larger candidate_ms selects a nonwinner while an already measured incumbent exists.
val on_candidate_measured :
(label:Base.string ->
digest:Base.string ->
Base.float ->
Base.float)
Base.refMeasurement seam (gh-ocannl-1027), called once per ADMITTED timing window — the dispatched baseline's (label "baseline") and each candidate's — with the candidate's label, its schedule digest and the admitted time in milliseconds. The returned time replaces the reading before the admission gate, so it is what the search ranks, records and reports: the beam, the best-so-far incumbent, on_candidate_callback, report.best_ms/baseline_ms, the calibration rows and the schedule cache's stored times. A refused window (contended, or a degenerate clock reading) never reaches it unless the test explicitly opts into with_uncontended_test_windows; degenerate readings still never reach it. A returned time that is not finite and positive refuses the window the way a degenerate reading does. It must return rather than raise: it runs outside the search's containment boundaries. It exists so a test about the nonwinner paths can pin the ranking — make the first admitted window the fastest and every later one slower — instead of depending on the machine's timings. The default is the identity and no configuration selects it.
Explicit test-only scope (gh-ocannl-1156) for scripted measurements: clear the physical window's contention flag before candidate admission and on_candidate_measured. Invalid physical or scripted readings and unresolved queued batching still refuse admission. Calibration and dispatch remain real. Restores the previous mode on return or exception, including for nested scopes. No configuration selects it; normal searches retain production contention refusal. Like the other process-wide test hooks, scopes must not run concurrently.
val apply_measurement_seam :
label:Base.string ->
digest:Base.string ->
timing_result ->
timing_resultCandidate-window admission and scripted measurement, shared by real searches and deterministic opposing controls. Production refuses contended windows before calling on_candidate_measured; with_uncontended_test_windows explicitly opts a scripted fixture out of that refusal only.
The label on_candidate_measured receives for the candidate a search on backend_name attributes as the untuned default — the one whose refused window sets report.default_refused and whose time is report.default_ms. Some "baseline" with automatic scheduling inactive, where the default is the serial form — None there on a GPU backend, which never dispatches it (gh-ocannl-532); the default-reproducing config_thresholds seed's label with it active and fissioning; None when it is active without fission, where no candidate reproduces the default. tune's own attribution reads this, so the two cannot disagree. One case it cannot foresee: a default seed whose code equals the serial baseline's dedups against it, and is timed only under the label "baseline".