Module Ir.Backend_intf

The interface types for backends

The shared backend-interface types: the user-facing API (Backend, routine, buffer_loc) together with the interface pieces the implementation layers assemble from (marked implementation-facing where applicable). Implementation-only components live in Backend_impl.

type buffer_loc = {
  1. pool_id : Base.int;
  2. offset : Base.int;
}

A backend-agnostic, deterministic per-device buffer location: a pool_id into the device's backend-private pool_id -> 'base pool table, plus a byte offset within that pool. The concrete backend pointer (Metal.Buffer.t / CUdeviceptr / void*) lives only in that private table -- it never appears in any type of this shared interface -- so buffer_loc (pure integers) is stable across runs, diffable, and meaningful in logs and .expected files. Phase-1 policy is one pool per tnode at offset = 0, byte-for-byte equivalent to per-tnode allocation. An alias (future work) is the parent's { pool_id; offset = offset + delta }.

val buffer_loc_of_sexp : Sexplib0.Sexp.t -> buffer_loc
val sexp_of_buffer_loc : buffer_loc -> Sexplib0.Sexp.t
val compare_buffer_loc : buffer_loc -> buffer_loc -> Base.int
val equal_buffer_loc : buffer_loc -> buffer_loc -> Base.bool
type ctx_buffers = buffer_loc Base.Map.M(Ir.Tnode).t
val sexp_of_ctx_buffers : ctx_buffers -> Sexplib0.Sexp.t
type upload_tenant = {
  1. tenant : Tnode.t;
  2. tenant_offset : Base.int;
    (*

    Where the tenant's buffer starts in the arena's pool.

    *)
  3. bump : Base.int;
    (*

    The arena's bump pointer just after the tenant's allocation.

    *)
}

One allocation in an upload_arena. A tenant is a LOCATION, not a node: sibling upload leaves can each hold the same node, in different arenas or at different offsets of one, so whether a value holds a tenant is decided by holds_tenant, never by the node alone (gh-ocannl-1328).

val sexp_of_upload_tenant : upload_tenant -> Sexplib0.Sexp.t
type upload_arena = {
  1. arena_pool_id : Base.int;
  2. arena_mode : Tnode.memory_mode Base.option;
    (*

    The residency hint the arena's slab was allocated with; only nodes with the same hint share it.

    *)
  3. capacity : Base.int;
  4. mutable tenants : upload_tenant Base.list;
    (*

    Newest first. Retirement trims the tail until it reaches a tenant still held by a surviving pool owner.

    *)
}

A working pool that host uploads and device copies of not-yet-allocated nodes are bump-packed into (gh-ocannl-1125), so that loading many parameters with Context.set_values before any routine links them costs a few pools rather than one each -- Metal publishes the per-routine limit as field-hardware_limits.max_bound_pools.

val sexp_of_upload_arena : upload_arena -> Sexplib0.Sexp.t
val holds_tenant : ctx_buffers -> upload_arena -> upload_tenant -> bool

Whether the buffer map places the tenant's node at the tenant's own location. Holding the node elsewhere -- a sibling leaf's upload of the same node -- is not holding the tenant: that value is no registered owner of the arena's pool, so extending the arena from it would put its upload in a pool another leaf's release frees, and a trim stopping at it would strand the tail.

type upload_arenas = {
  1. mutable arenas : upload_arena Base.list;
  2. owners_mutex : Stdlib.Mutex.t;
  3. pool_owners : (Base.int, ctx_buffers Base.ref Base.list) Base.Hashtbl.t;
    (*

    Current maps of upload-lineage leaves owning each pool. References let a linear upload update its owner without adding one; forks retain independently. Lifecycle descendants have a separate table and retain the existing leaf-release precondition.

    *)
}

Upload arenas shared by an upload lineage. Sibling leaves share the pool ownership table, while each has its own release flag. A compile starts fresh arenas.

val sexp_of_upload_arenas : upload_arenas -> Sexplib0.Sexp.t
val fresh_upload_arenas : unit -> upload_arenas
val upload_arena_violations : upload_arenas -> Base.string Base.list

gh-ocannl-1462: the ownership invariant of a lineage's upload arenas, as violations (empty when it holds). Every tenant a retained arena still lists must be held, at its own location (holds_tenant), by a registered owner of the arena's pool; a retained arena with no tenant, or with no owner list, is a violation too. Why every tenant and not only the newest: only upload_slot appends, and only from a value holding the arena's last tenant; ctx_buffers maps only grow, so a holder of a tenant holds every tenant below it; and retirement trims from the newest end down to a tenant a surviving owner holds. The caller holds owners_mutex and asks at a point of committed ownership (after a release), never with an upload in flight: an arena appended by a transfer that has not registered its owner yet is not a violation.

exception Backend_unavailable of {
  1. backend : Base.string;
  2. detail : Base.string;
}

Device discovery established that this backend cannot be used on this machine: its library is not linked in, or the driver reports no devices. This is deliberately narrow — it is the only failure Context.auto treats as "try the next backend" (gh-ocannl-536 landing step 5). A driver that is present but fails to initialize is not this: that is a real problem with a real installation, and silently selecting another backend would hide it.

type mma_input_format =
  1. | Mma_f32
    (*

    Genuine f32 multiply-accumulate (Metal simdgroup_float8x8).

    *)
  2. | Mma_tf32
    (*

    f32 storage computed with a 10-bit mantissa (CUDA wmma precision::tf32, sm_80+). Not a storage precision — data lives in memory as ordinary f32; only tensor-core loads truncate. Gated by Numerics.t.tf32_matmuls.

    *)
  3. | Mma_f16
  4. | Mma_bf16
  5. | Mma_fp8_e5m2
    (*

    OCANNL's single fp8 today (Ops.Fp8_prec, e5m2). An e4m3 constructor slots in here when the precision exists (gh-ocannl-481 item 2); descriptor entries are keyed per operand pair, so mixed e5m2×e4m3 combinations need no interface change.

    *)

Element formats tensor-core instructions accept for their multiplicand operands, and (reusing the same constructors) for their accumulator. This is deliberately NOT Ops.prec: formats like tf32 have no byte layout of their own, so they must never appear as a tensor node's storage precision.

val sexp_of_mma_input_format : mma_input_format -> Sexplib0.Sexp.t
val equal_mma_input_format : mma_input_format -> mma_input_format -> Base.bool
type mma_staged_layout =
  1. | Mma_swizzled_b128
    (*

    Low_level.swizzle_kind.Swizzle_b128: the CUDA inline-PTX mma.sync arms read it with ldmatrix.sync.aligned.m8n8 or swizzle-aware byte gathers where ldmatrix.b16 cannot form the register. Metal banks too but has no ldmatrix analogue; a later simdgroup-era entry would reuse this type.

    *)

A physical layout the backend's tensor-core loads can consume for a cooperatively staged operand tile, beyond the plain row-major one (gh-ocannl-481 item 3, D3).

val sexp_of_mma_staged_layout : mma_staged_layout -> Sexplib0.Sexp.t
val equal_mma_staged_layout : mma_staged_layout -> mma_staged_layout -> Base.bool
type mma_emission_scope =
  1. | Mma_per_statement
    (*

    One Low_level.t.Tile_mma emitted by the backend's mma_syntax hook. In a staged schedule this form reloads and stores d at every outer k block.

    *)
  2. | Mma_fragment_scope
    (*

    A persistent accumulator emitted by mma_fragment_syntax, loaded before and stored after the enclosing serial reduction.

    *)

Where a tensor-unit rendering keeps its accumulator. The distinction is observable whenever a reduction is split into outer k blocks: Mma_per_statement crosses only the intrinsic's own k extent, while Mma_fragment_scope crosses the enclosing serial reduction.

val mma_emission_scope_of_sexp : Sexplib0.Sexp.t -> mma_emission_scope
val sexp_of_mma_emission_scope : mma_emission_scope -> Sexplib0.Sexp.t
val compare_mma_emission_scope : mma_emission_scope -> mma_emission_scope -> Base.int
val equal_mma_emission_scope : mma_emission_scope -> mma_emission_scope -> Base.bool
val all_of_mma_emission_scope : mma_emission_scope Base.list
type mma_capability = {
  1. mma_simd_width : Base.int;
    (*

    Threads cooperating in one tile-MMA instruction (CUDA warp / Metal simdgroup width).

    *)
  2. mma_tile : Base.int * Base.int * Base.int;
    (*

    The canonical intrinsic tile shape (m, n, k) (8×8×8 for Metal simdgroup_matrix, 16×16×16 for CUDA wmma), used where schedule construction has no typed operand site; a Low_level.t.Tile_mma's block extents must be multiples of the tile of the format actually emitted. Typed matmul/conv sketch seeds use mma_format_tiles below.

    *)
  3. mma_format_tiles : ((mma_input_format * mma_input_format * mma_input_format) * (Base.int * Base.int * Base.int)) Base.list;
    (*

    Per (a-operand, b-operand, accumulator) format intrinsic tile shapes, for formats whose tile diverges from mma_tile as well as the ones matching it (e.g. CUDA fp8 16×8×32, tf32 16×16×8). Typed autotune seeds use the matching entry for divisibility; whether a given call ultimately emits is still decided by the backend's mma_syntax hook plus the Numerics policy.

    The accumulator format is part of the key because it is NOT free to choose: the operand pair that a backend supports against an f32 accumulator is generally not the pair it supports against a narrow one. CUDA is the case that made this explicit (gh-ocannl-545): nvcuda::wmma pairs bf16 operands with a float accumulator only, so keying on the operands alone made the autotuner seed — and time, and rank — 36 candidates per arm on a uniformly-bf16 network that every one of them rendered as the lane-0 scalar fallback.

    *)
  4. mma_f16_wide_acc_scopes : mma_emission_scope Base.list;
    (*

    The emission scopes in which the backend's uniform-f16 arm — an f16-storage destination with f16 operands — holds the accumulator in f32 and converts once at that scope's d boundary, as Numerics.fp16_mode.Fp16_wide requires (gh-ocannl-680, gh-ocannl-836).

    CUDA sm_80+ advertises both: Mma_per_statement through its inline-PTX mma.sync.m16n8k16 arm, and Mma_fragment_scope since gh-ocannl-925 through nvcuda::wmma float accumulator fragments whose f16 d crosses a converted boundary addressed by a coordinate table (wmma's element order is unspecified, so no fragment-to- fragment copy). HIP advertises both scopes since gh-ocannl-789 (rocWMMA's (f16, f16, f32) fragments and converted boundary), and Metal advertises both since gh-ocannl-837 (mixed-type simdgroup_multiply_accumulate plus a thread_elements() boundary copy). Withholding only the unsupported scope preserves legal tensorized schedules without letting an outer k split introduce extra f16 narrowing boundaries.

    *)
  5. mma_bf16_wide_acc_scopes : mma_emission_scope Base.list;
    (*

    The bf16 twin of mma_f16_wide_acc_scopes: the emission scopes in which the backend's uniform-bf16 arm holds the accumulator in f32 and converts once at that scope's d boundary, as Numerics.bf16_mode.Bf16_wide requires (gh-ocannl-838). HIP advertises both scopes (rocWMMA's (bf16, bf16, f32) fragments behind the same converted boundary as the wide f16 arm); CUDA sm_80+ both (its inline-PTX m16n8k16 arm, whose accumulate is f32 in hardware, per statement, and since gh-ocannl-1063 that arm's per-lane registers held across the outer reduction); Metal both (a simdgroup_float8x8 accumulator over bfloat operand fragments behind the wide-f16 arm's thread_elements() boundary, gh-ocannl-923).

    *)
  6. mma_staged_layouts : ((mma_input_format * mma_input_format * mma_input_format) * mma_staged_layout) Base.list;
    (*

    Format triples whose cooperatively staged operand tiles the backend can read in a non-row-major layout, and which layout (gh-ocannl-481 item 3, D3). Autotune's staged mma sketches seed a swizzled twin per staged seed exactly for the advertised triples — the tuner, not a heuristic, then decides whether the bank-conflict fix beats the plain tile.

    Keyed by format triple for the same reason as mma_format_tiles, and pre-filtered for the same reason (gh-ocannl-479): eligibility is per operand AND per orientation, and the orientation the staged sketches mint is each role's own. CUDA's fp8 arm, for instance, feeds A from ldmatrix in that orientation; B's four bytes per register are strided, so its row-major staged tile uses swizzle-aware byte gathers instead (gh-ocannl-1073). Both operands tensorize, allowing the advertised swizzled twin. Empty everywhere the question does not arise.

    *)
  7. mma_pipeline_depths : Base.int Base.list;
    (*

    Software-pipelining depths beyond the unpipelined 1 that autotune's staged mma/conv sketches propose as twins of each staged seed (Schedule.Stage ~pipeline_depth, gh-ocannl-487) — a list, not a flag, so the search has a dimension. The portable double-buffered rendering is backend-generic, but a depth is advertised only where the arm has been validated on hardware: Metal ([2], phase 1, portable form) and CUDA ([2] on sm_80+, phase 2, the cp.async arm); HIP stays empty until its LDS async-copy arm lands. Empty on CPU backends (cooperative staging is not renderable there).

    *)
}

Tensor-core capability descriptor (docs/proposals/tensorize-mma.md §6). Which operand precisions are supported is decided per call by the backend's mma_syntax hook (the emission is the source of truth); this record carries what schedule construction needs.

val sexp_of_mma_capability : mma_capability -> Sexplib0.Sexp.t
val equal_mma_capability : mma_capability -> mma_capability -> Base.bool
type timing_identity = {
  1. device_signature : Base.string;
  2. toolchain_signature : Base.string Base.option;
}

Concrete device capabilities and optional observed toolchain metadata for timing evidence. Separate from conservative construction limits. Include model/throughput properties or stable physical identity, not just an ordinal. Toolchain metadata is intentionally partial: its absence does not disable caching or claim that all driver/runtime/compiler inputs are represented.

val sexp_of_timing_identity : timing_identity -> Sexplib0.Sexp.t
val equal_timing_identity : timing_identity -> timing_identity -> Base.bool
type hardware_limits = {
  1. max_bound_pools : Base.int Base.option;
    (*

    Maximum number of distinct device-memory pools one routine may bind. None when the backend imposes no pool binding budget. Counts pools, not tensor nodes: host uploads and device copies into one context share arenas. Metal enforces this at link time.

    *)
  2. max_threads_per_workgroup : Base.int Base.option;
    (*

    Upper bound on the number of threads in one workgroup (CUDA thread block / Metal threadgroup); None when the backend imposes no limit (the C backends render annotated loops serially).

    *)
  3. max_workgroup_memory_bytes : Base.int Base.option;
    (*

    Capacity in bytes of the workgroup-shared memory (CUDA __shared__ / Metal threadgroup); None when the backend imposes no limit.

    *)
  4. max_thread_private_bytes : Base.int Base.option;
    (*

    The per-work-item budget for one kernel's declared scratch arrays: the largest private (stack) frame the backend's compiler accepts, less a reserve for what else lands on the frame (gh-ocannl-1313). The kernel's routine-scoped scratch, the function-scope arrays of its Local-placed nodes (Schedule.thread_private_bytes), is checked against it pre-driver by Schedule.check_hardware_limits_classified, as Schedule_outcome.Thread_scratch. Without the check, a kernel over the limit reaches the device compiler, which refuses it untyped after compiling the whole kernel: HIP's hiprtc took 52 s to report stack frame size (360544) exceeds limit (262136) on the serial base form of a GPT-2 training step, whose 89 Local arrays declare 360452 bytes, and under strict failure classification that ended the search (outcome pre-search-failure). HIP: LLVM AMDGPU's frame limit of 262136 bytes, on every AMD part seen (gfx1102, gfx1151), less a 1 KiB reserve (that kernel's frame held 92 bytes beyond its arrays; spills add more), independent of the post-link scratch-budget check that HIP applies to what does compile. None where unmeasured, which checks nothing, and on the C backends. Omitted from the record's sexp when None, so adding it moved no other backend's schedule-cache key.

    *)
  5. max_workgroup_dims : (Base.int * Base.int * Base.int) Base.option;
    (*

    Per-dimension upper bounds on the launch's workgroup shape — the caps on .x, .y and .z of Low_level.launch_dims' block, in that order. Beside, not instead of, max_threads_per_workgroup: that one caps the product, and the two are not the same fact. CUDA's maxThreadsDim is (1024, 1024, 64) — the .z component is 16x smaller than the product cap — so a workgroup of 2 x 2 x 128 has a perfectly legal 512-thread product and is still an invalid launch configuration (gh-ocannl-679). Workgroup slots are capped at 3 and the innermost binds .x, so the outermost annotated loop's extent lands on .z directly; no fold is involved.

    Unlike max_grid_yz this carries all three bounds rather than one shared one, because here the dimensions genuinely differ: on CUDA .z is the odd one out, while HIP (queried max_threads_dim) and Metal (maxThreadsPerThreadgroup's three components) report three equal values. None on the C backends, which render annotated loops serially.

    A tuple rather than an int array, on two independent grounds. It is the shape mma_tile already uses for a 3-D quantity here; and it is immutable, which this record needs from every field it has. The GPU backends memoize their hardware_limits behind a lazy and Context.hardware_limits returns that record itself, so one mutable cell anywhere in it would let a caller deriving tighter limits for a custom schedule write through into the process-wide singleton — after which compiles reject legal kernels or admit illegal ones, with nothing to point at. A tuple also makes the arity exactly the three Workgroup slots, so no reader has to bounds-check a length the type does not promise.

    Enforced pre-driver by Schedule.check_hardware_limits_classified (as Schedule_outcome.Workgroup_x_extent / _y_ / _z_extent); Schedule.default_gpu and Schedule.zero_expansion clamp their block size against the .x entry too, and check the .y/.z entries a widened workgroup reaches, so the gate is a backstop rather than the first line of defence.

    *)
  6. max_grid_yz : Base.int Base.option;
    (*

    Upper bound on each of the launch's .y and .z grid dimensions: the row-block count (grid.(1)) and the folded batch-extent product (grid.(2), the dimension Grid slots >= 2 fold onto — gh-ocannl-643, Low_level's hardware-axis section comment). One field rather than two because it is one hardware fact: CUDA and HIP cap gridDim.y and gridDim.z at the same 65535 while gridDim.x is 2^31-scale (on CUDA an architectural constant, unlike the queried per-device limits above; HIP queries it, conservatively as the smaller of the two components). What a caller does about an excess differs per dimension, and that distinction lives in the typed cause instead (Schedule_outcome.Grid_y_extent vs. Grid_z_extent). None when the backend imposes no such limit (Metal's threadgroups-per-grid dimensions are not 16-bit, and the C backends render annotated loops serially). Both dimensions are checked pre-driver by Schedule.check_hardware_limits_classified; the autotune batch-grid twins also consult the .z reading at seeding so an over-cap candidate is never proposed.

    *)
  7. mma : mma_capability Base.option;
    (*

    Tile-MMA units (simdgroup_matrix / tensor cores); None when the backend has none wired — Tile_mma statements then render their scalar fallback.

    *)
  8. simd_vector : Vector_file.t;
    (*

    The vector register file of the C backends' explicit vector-extension renderings (Vectorized loops, the register-tiled Tile_mma micro-kernel): its width in bytes and its register count, one value so that neither can be stated without the other (gh-ocannl-1399). Vector_file.none when the backend does no such rendering (GPU backends bind hardware axes instead). Carried here so schedule construction (autotune's seeding pre-filter, gh-ocannl-479) can statically rule out candidates the renderer must decline, e.g. a micro-kernel column extent below one vector's lane count.

    The count is 32 on AArch64 and on AVX-512 targets (at every width, since AVX-512VL gives the narrower forms the same 32), 16 on other x86 targets (SSE, AVX2) -- the target the kernels are compiled for, probed under their flags. It is not a function of the width: a 16-byte file is 32 registers on NEON and 16 on SSE, and Register_tile.rn_cap fits the register tile's columns to the count (gh-ocannl-1285), so the seeding and the renderer must read the same file.

    *)
  9. peak_flops : Base.float Base.option;
    (*

    Advisory peak arithmetic throughput in FLOP/s (single-precision, FMA counted as two), the hardware envelope of the analytic cost model (gh-ocannl-491): rough documented constants or cheap device queries — the model ranks candidate schedules, it does not predict runtimes. Never gates compilation and never overrides a measured timing; None when the backend offers no estimate. Known single-precision bias (gh-ocannl-575): a pure-fp16 kernel on a native_fp16_arithmetic target has twice this ceiling, so its roofline flops leg over-estimates — harmless for ranking because a site's candidates all share one policy-resolved compute precision (footprint widths, by contrast, are exact: they come off each node's own storage precision).

    *)
  10. peak_memory_bandwidth : Base.float Base.option;
    (*

    Advisory peak main-memory bandwidth in bytes/s, the other leg of the roofline envelope (gh-ocannl-491). Same contract as peak_flops — advisory, rough, never load-bearing for correctness; None when the backend offers no estimate — with one bias requirement (gh-ocannl-578): the value must be a class ceiling, at least what any machine of the backend's class can sustain. Streaming kernels with exact byte counts are routine now (the calibration pass, packed initializations), and each one achieving more than the advisory trips the gh-514 agreement warning — while under autotune_bound_pruning an understated leg over-prunes. Overstatement only loosens an advisory bound; calibrated model_peak_* overrides beat these wherever fidelity matters.

    *)
  11. cache_capacity_bytes : Base.int Base.option;
    (*

    Untimed (see limits_key): the cache capacity of the cost model's ranking score (gh-ocannl-1253, Ir.Cost_model.ranking_seconds) — once a kernel's working set, per-block global read bytes times resident blocks, exceeds it, global reads are charged per block rather than as distinct cells. A FITTED constant of that proxy, not the device's L2 size: the working set sums per-block footprints that resident blocks share, so the value that separates cache-resident from DRAM-bound kernels sits well above any physical cache. Per-class advisories on CUDA and HIP, and a fitted one on the C backends (gh-ocannl-1687, Schedulers.cpu_mma_limits), overridable by config model_cache_capacity; None elsewhere: no DRAM tier (see cache_bandwidth). Read only by the untimed choosers, never by schedule construction, codegen or timing — so it is not in the schedule-cache key (limits_key).

    *)
  12. max_resident_threads : Base.int Base.option;
    (*

    Untimed: how many threads the whole device keeps resident at once (multiprocessors times threads per multiprocessor), which divided by a kernel's workgroup size is the ranking score's resident-block count. None where unknown: every block then counts as resident. Same readers and same cache identity as cache_capacity_bytes.

    *)
  13. cache_bandwidth : Base.float Base.option;
    (*

    Untimed: the rate, in bytes/s, at which the cache serves re-reads within capacity — the ranking score's second tier (gh-ocannl-1253): cross-block re-reads of a kernel whose working set fits cache_capacity_bytes are charged at this rate rather than for free; it is the requested-rate plateau of the capacity sweep below the capacity (tools/calibrate_cache_capacity.exe). Overridable by config model_cache_bandwidth; None where unmeasured, which leaves re-reads within capacity uncharged. Same readers and same cache identity as cache_capacity_bytes.

    *)
  14. issue_rate : Base.float Base.option;
    (*

    Untimed: the effective scalar operation rate, in operations/s (summary.flops' units, an FMA counting two), of a register-blocktiled kernel, its operand staging included — the ranking score's compute leg for scalar work (gh-ocannl-1442, Ir.Cost_model.ranking_seconds), where peak_flops stays the bound's ceiling; accepted Tile_mma work is priced at mma_rate. The slope of tools/calibrate_issue_rate.exe's sweep of the seeded weight-gradient sketch — or, where fill_threads is set, the full-fill rate its launch sweep fits jointly with it (gh-ocannl-1479): the two are one calibration, and a pre-fill rate under a fill model double-counts the calibration kernel's partial fill. Read as at most peak_flops. Overridable by config model_issue_rate; None where unmeasured, which prices scalar work at peak_flops as before. Same readers and same cache identity as cache_capacity_bytes.

    *)
  15. dram_bandwidth : Base.float Base.option;
    (*

    Untimed: the DRAM rate kernels achieve, in bytes/s — the ranking score's DRAM tier and distinct-byte charge (gh-ocannl-1442), where peak_memory_bandwidth stays the bound's class ceiling. The STREAM fit of tools/calibrate_bandwidth.exe. Read as at most peak_memory_bandwidth. Overridable by config model_dram_bandwidth; None where unmeasured, which prices DRAM traffic at peak_memory_bandwidth as before. Same readers and same cache identity as cache_capacity_bytes.

    *)
  16. mma_rate : Base.float Base.option;
    (*

    Untimed: the effective rate, in operations/s (an FMA counting two), of Tile_mma work the backend's mma_accepts hook accepts — the ranking score's compute leg for work on the matrix units (gh-ocannl-1495, Ir.Cost_model.ranking_seconds). A FITTED constant of the emitted tensorized kernels, operand staging and shared-memory loads included, not the units' datasheet peak: the geometric mean of the rates the staged tensorized leaves of a site achieve (benchmarks/gh1442_agreement.py --fit-mma). Read as at most peak_flops. Overridable by config model_mma_rate; None where unmeasured, which prices tile-MMA work at peak_flops as before. Same readers and same cache identity as cache_capacity_bytes.

    *)
  17. device_gather_rate : Base.float Base.option;
    (*

    Untimed: how many per-lane operand gather instructions a lane group issues per second from DEVICE memory — the rate of the ranking score's device-gather term (gh-ocannl-1495, Ir.Cost_model.device_operand_gathers): a Tile_mma operand the arm gathers element by element straight from global memory (an unstaged inline-PTX operand) costs its count over this rate, at the Tile_mma contexts' fill. Its unit is the operand-load count's (executions per lane group times instructions per lane). Shared-memory gathers, ldmatrix and whole-fragment loads are not counted. A FITTED constant: the rate that makes a site's unstaged tensorized leaves' excess over mma_rate come out as measured (benchmarks/gh1442_agreement.py --fit-mma). Overridable by config model_device_gather_rate; None where unmeasured, which adds no term. Same readers and same cache identity as cache_capacity_bytes.

    *)
  18. multiprocessors : Base.int Base.option;
    (*

    Untimed: the device's multiprocessor (SM / CU / GPU core) count — the P of the ranking score's launch fill (gh-ocannl-1479, Ir.Cost_model.context_fill). Queried on CUDA and HIP; on Metal, which publishes none, read from the IORegistry's gpu-core-count (gh-ocannl-1638; None on a non-AGX GPU); None elsewhere. Overridable by config model_multiprocessors. The launch fill applies only when both this and fill_threads resolve. Same readers and same cache identity as cache_capacity_bytes.

    *)
  19. fill_threads : Base.int Base.option;
    (*

    Untimed: the threads per multiprocessor at which a register-blocktiled kernel's scalar issue saturates — the t of the launch fill (gh-ocannl-1479). A FITTED constant of the launch sweep of tools/calibrate_issue_rate.exe, jointly with issue_rate, which it normalizes (the rate is then a full-fill rate); not the physical residency. Per-class advisories where measured, overridable by config model_fill_threads; None elsewhere, which leaves the compute leg as before. Same readers and same cache identity as cache_capacity_bytes.

    *)
  20. transaction_rate : Base.float Base.option;
    (*

    Untimed: how many excess 32-byte memory sectors a device serves per second -- the rate of the ranking score's memory-transaction term (gh-ocannl-1600, Ir.Cost_model.memory_transactions): per launch block (its lane groups of simdgroup_width lanes sharing what L1 serves), the sectors a global access touches beyond the compact count for the same distinct bytes, at the access's own context fill, added to the compute leg. What orders a layout whose lanes read contiguous cells against a twin whose lanes stride. A FITTED constant (benchmarks/gh1600_txn_fit.py), per device class. Overridable by config model_transaction_rate; None where unmeasured, which adds no term and keeps the coalesced layouts out of model_default's candidate set. Same readers and same cache identity as cache_capacity_bytes.

    *)
  21. workgroup_bytes_per_multiprocessor : Base.int Base.option;
    (*

    Untimed: the workgroup-shared memory one multiprocessor (SM / WGP) holds for all its resident blocks -- the capacity of the ranking score's occupancy factor (gh-ocannl-1600, Ir.Cost_model.occupancy): a kernel declaring W workgroup bytes in blocks of T threads keeps min (capacity / W) (threads per multiprocessor / T) blocks resident, and its compute leg is divided by the fraction of the multiprocessor's thread slots they fill (the threads per multiprocessor are max_resident_threads over multiprocessors). Also the second half of model_default's gate on the coalesced layouts, beside transaction_rate: their deep-K tiles are told apart only by it. The physical value where advisory (HIP's RDNA wave32 parts: 128 KiB of LDS per WGP, the unit HIP counts as a multiprocessor); None where the occupancy does not explain the measured order (CUDA, gh-ocannl-1600) or is unmeasured. Overridable by config model_workgroup_bytes_per_multiprocessor. Same readers and same cache identity as cache_capacity_bytes.

    *)
  22. native_fp16_arithmetic : Base.bool;
    (*

    Whether 16-bit float arithmetic executes natively at twice f32's lane count (gh-ocannl-516: ARMv8.2-FP16, AVX512-FP16). false covers both "no _Float16 on this target" and the middle case that matters for ranking: the type exists and computes correctly, but the compiler implements it by promoting to float, so the lane count does not double and candidates must not be seeded as if it did. Whether the type exists at all is a separate, purely textual question the emitted C answers for itself (HAS_NATIVE_FLOAT16); this field is about throughput.

    Always false on the GPU backends, whose 16-bit story is their native types and tensor-core shapes rather than a CPU vector width.

    *)
  23. simdgroup_width : Base.int Base.option;
    (*

    The SIMD-group (warp) width the backend's warp-shuffle renderings assume -- its C_syntax configuration's warp_size: 32 on Metal, CUDA and HIP (whose shuffles pass an explicit width of 32). None where kernels render no shuffles (the C backends). The default GPU schedule retypes a cooperative lane reduction only at a multiple of this width, at most lane_all_reduce_simdgroups of them (gh-ocannl-1124, gh-ocannl-1168). Where a transaction_rate is set, also the execution width the memory-transaction count groups lanes by (gh-ocannl-1600): a backend sets that rate only on parts that execute this width (HIP: the RDNA wave32 parts).

    *)
  24. lane_scalar_recompute_cheap : Base.bool;
    (*

    Device economics (gh-ocannl-1124): whether scalar work every lane of a lane geometry recomputes redundantly -- the per-pair preamble a lane nest repeats in each of its lanes (the fused attention backward's p, dp, ds) -- is cheap relative to a thread's serial walk over the channel loop that the plain plan pays it once for. Measured: true on Metal (M4 Max) and CUDA (RTX, rog-nv), whose dK and dQ nests on lanes cut the D1 training step by 2-9%; false on HIP (gfx1151), where the same lanes cost 7-20% against the plain dK plan. false -- today's behaviour, the plain plan -- wherever unmeasured, and on the C backends, which run no lane geometry. Read by the default GPU schedule's gpu_lane_preamble_reduction = auto and its gpu_serial_lanes = auto (gh-ocannl-1167): where false, the stage-1 lanes of gh-ocannl-1003 are still taken in a kernel that admits them (the forward value pass won on gfx1151), but are no reason to cut a kernel (the fused backward's dV cut from dK for them lost to the merged plain kernel).

    *)
  25. lane_all_reduce_simdgroups : Base.int;
    (*

    Device economics (gh-ocannl-1168): the most simdgroups a cooperative lane all-reduce is worth spanning. At one simdgroup the all-reduce is a register butterfly; across k > 1 it stages each simdgroup's partial in workgroup-shared memory between two barriers per (query, key) pair, which may eat the lanes' win -- so a head wider than simdgroup_width (GPT-2's 64) gets lanes only where that was measured to pay. 1 (one simdgroup, the gh-ocannl-1124 rule) wherever unmeasured, and on the C backends, which run no lane geometry. Measured: 4 on Metal (M4 Max; the D1 training step at head width 64 0.97x / 0.92x / 0.82x at seq 128 / 512 / 1024, at width 128 0.69x at seq 512, against the plain plan: benchmarks/gh1133_cells.sh d1ar4 vs d1ar1); 4 on CUDA (RTX, rog-nv; at head width 64 0.84x / 0.52x / 0.41x at seq 128 / 512 / 1024, at width 128 0.37x at seq 512, the same A/B, gh-ocannl-1256). Read by the default GPU schedule's gpu_lane_all_reduce_simdgroups = auto.

    *)
  26. online_softmax_auto_block : Base.int;
    (*

    Automatic key-block size for online_softmax_block=auto (gh-ocannl-1171); 0 selects the two-pass rewrite. Metal and CPU keep 16 from the gh-ocannl-1003 block sweep (benchmarks/report-gh1003-block-fold.md). CUDA's tuned f32 transformer measured 4.49 ms at 16 versus 2.44 ms at 0 with tf32 retained (gh-ocannl-1194); CUDA and unmeasured devices conservatively select 0. HIP gfx1151 unified measured 7.22 ms at 16 versus 4.72 ms at 0; other HIP and unmeasured targets conservatively select 0. Explicit numeric sizes bypass this policy. Consumed before lowering analyses, so the resolved choice is in the code digest.

    *)
  27. worker_pool_tag : Base.string Base.option;
    (*

    Compact signature of the worker pool timings execute on (w8P, w24, ...), filled by the CPU backends from the pool-uniformity policy (gh-ocannl-530). Enters the autotune disk-cache key the way the numerics tag does: schedules crowned on one pool do not transfer to another, so a policy flip or a different external pinning must re-tune rather than replay. None (GPU backends) leaves the cache key unchanged.

    *)
  28. codegen_tag : Base.string Base.option;
    (*

    Compact signature of this backend's codegen configuration: the settings the backend consults when rendering and compiling a kernel, which are therefore invisible to the canonical digest of the lowered code (gh-ocannl-572). Same contract as worker_pool_tag — it enters the autotune disk-cache key, so a knob flip re-tunes instead of replaying a winner crowned in another codegen regime, which is the hazard gh-ocannl-568 measured at 5.9x. Fill it from resolved values, not raw settings: what "auto" resolves to is a per-machine fact, and crowns do not transfer across machines either. None where the backend has no such knobs.

    *)
}
val sexp_of_hardware_limits : hardware_limits -> Sexplib0.Sexp.t

Equality of the (a-operand, b-operand, accumulator) format triple that keys field-mma_capability.mma_format_tiles and field-mma_capability.mma_staged_layouts. All three components are part of the key, for the reason those fields document (gh-ocannl-545): which operand pair a backend supports is a function of the accumulator it is paired against.

val advertises_mma_format : hardware_limits -> a:mma_input_format -> b:mma_input_format -> d:mma_input_format -> bool

Whether the backend's tensor-core unit advertises an intrinsic tile for this (a, b, d) format triple -- false when it advertises the triple's operands only against another accumulator, and false when the backend has no such unit at all (mma = None).

The typed way to interrogate the MMA seam (field-hardware_limits.mma, gh-ocannl-822): callers ask the descriptor rather than re-deriving the mma_format_tiles lookup, which is easy to open-code with the accumulator dropped from the key. Whether a given call ultimately emits is still decided per call by the backend's mma_syntax hook plus the Ir.Numerics policy; this answers what schedule construction and its tests can know statically.

val advertises_mma_format_in_scope : hardware_limits -> policy:Numerics.t -> scope:mma_emission_scope -> a:mma_input_format -> b:mma_input_format -> d:mma_input_format -> Base.bool

Whether the advertised triple is available in this accumulator scope under an explicit numerics policy. This is a descriptor judgment, not a promise that a particular statement emits: geometry, strides, address spaces and logging can still make the backend decline it. Mma_tf32 describes f32 operand storage; it requires tf32_matmuls. Uniform narrow triples additionally require the matching wide-accumulator scope when that policy is selected.

type mma_arm = {
  1. arm_name : Base.string;
    (*

    The arm's own marker, as its rendering comment carries it (e.g. wmma-tf32, mma-bf16): which arm, not only what it accumulates at.

    *)
  2. arm_accumulator : Ops.prec;
    (*

    The precision the arm's accumulator resides at.

    *)
  3. arm_floor : Base.int Base.option;
    (*

    The device-architecture floor the arm itself checks before emitting, in the backend's own numbering (CUDA compute capability). None where the backend gates tensor units at the device level only (hardware_limits.mma).

    *)
}

The tensor-unit arm a backend's mma emission resolves one storage-precision triple to, in one emission scope, under the current numerics policy (gh-ocannl-1153). What accum_prec cannot say: an arm the policy chooses whose operands are consumed in a format no storage precision names — CUDA's tf32 arm reads f32 storage as tf32, and accumulates at exactly the f32 that accum_prec already reports for f32 with the policy off.

type mma_operand_place = [ `Device | `Shared | `Thread ] * [ `Plain | `Swizzled_b128 ]

Where a tile-MMA input operand lives and how it is laid out, as the emission hooks see it: the address space the renderer classifies the node into (workgroup-shared, device-resident, or a per-thread array) and its physical layout (`Swizzled_b128 for a Low_level.Swizzle_b128 staged tile). What field-codegen_capabilities.mma_operand_loads decides the load path by.

type mma_fragments = {
  1. mf_tile : Base.int * Base.int * Base.int;
    (*

    The intrinsic tile (tm, tn, tk) the arm emits: an a fragment is tm x tk, a b fragment tk x tn.

    *)
  2. mf_reloads : Base.bool;
    (*

    true: both fragments are loaded inside the m/tm x n/tn x k/tk tile loops, so each is reloaded for every tile of the other operand's free axis (CUDA's inline-PTX arms); false: each fragment loads once per reduction step and is reused across that axis (CUDA's wmma arms).

    *)
  3. mf_fragment_loads : Base.bool;
    (*

    true: each fragment not loaded through ldmatrix comes in whole, by one cooperative matrix load (CUDA's wmma load_matrix_sync); false: by per-lane element gathers (the inline-PTX arms).

    *)
}

An mma arm's fragment structure, as its emission loops load operands (gh-ocannl-1254).

type codegen_capabilities = {
  1. supports_f64 : Base.bool;
    (*

    Whether the backend dialect can represent f64 tensor storage. This is explicit rather than an exception probe against typ_of_prec.

    *)
  2. compute_prec : Ops.prec -> Ops.prec;
    (*

    The resolved compute precision for a storage precision, from the same C_syntax_config.compute_prec function code generation uses.

    *)
  3. accum_prec : Ops.prec -> Ops.prec;
    (*

    The resolved accumulator precision for a storage precision, from the same C_syntax_config.accum_prec function code generation uses.

    *)
  4. asynchronous_staging_copy : Base.bool;
    (*

    Whether eligible pipelined staging copies use a dialect-specific asynchronous copy arm. A portable synchronous depth-2 pipeline is not this capability.

    *)
  5. compiler_contraction_off : Base.unit -> Base.bool;
    (*

    Whether the backend's kernel compile pins contraction off, from the same C_syntax_config.compiler_contraction_off code generation reads (gh-ocannl-1457): then a Tile_mma fallback, and the register tile, keep the serial loop's two roundings. Read per call, since the settings behind it are mutable within a process.

    *)
  6. mma_arm : a_prec:Ops.prec -> b_prec:Ops.prec -> d_prec:Ops.prec -> scope:mma_emission_scope -> mma_arm Base.option;
    (*

    The arm the backend's mma_syntax (for Mma_per_statement) or mma_fragment_syntax (for Mma_fragment_scope) selects for these storage precisions, derived from the same combination table those hooks consult; None where the table has no arm for them. Only the precision- and policy-level choice: extents, strides, address spaces, layouts and the device can still make a call decline.

    *)
  7. mma_operand_loads : a_prec:Ops.prec -> b_prec:Ops.prec -> d_prec:Ops.prec -> scope:mma_emission_scope -> ta:Base.bool -> tb:Base.bool -> a:mma_operand_place -> b:mma_operand_place -> Base.bool * Base.bool;
    (*

    Which of the a and b operands the backend's mma arm for these storage precisions, in this emission scope, loads with a warp-cooperative matrix load (ldmatrix on CUDA) rather than per-lane gathers, given each operand's place and whether it is stored transposed (gh-ocannl-1254). The very rule the emission and its rendering census apply (CUDA: Cuda_mma.operand_loads over Cuda_mma.arm_ldmatrix_operands), so a cost model reading it reads the decision the kernel makes. Like mma_arm, only the arm- and operand-level choice: a call the hook declines on extents or the device renders the scalar fallback whatever this says. (false, false) on a backend without such a load.

    *)
  8. mma_accepts : d_prec:Ops.prec -> a_prec:Ops.prec -> b_prec:Ops.prec -> ta:Base.bool -> tb:Base.bool -> m:Base.int -> n:Base.int -> k:Base.int -> fragment_target:(Base.int * mma_operand_place) Base.option -> d:(Base.int * mma_operand_place) -> a:(Base.int * mma_operand_place) -> b:(Base.int * mma_operand_place) -> Base.bool;
    (*

    Whether the backend's mma hook accepts a statement with these storage precisions, orientations, extents and, per operand, leading dimension and place — asked of the hook itself (a C backend's C_syntax_config.mma_syntax, which decides acceptance without the operand addresses), so it holds every condition the emission checks: layouts, spaces, strides, extents, the device's floor (gh-ocannl-1254). With fragment_target — the backing target of a persistent accumulator, its leading dimension and place — the question is the fragment-scope rendering, in the renderer's order: the enclosing mma_fragment_syntax hook accepting that target and the operands, then the update-only statement (d's own leading dimension) against the fragment array. false on a backend without the hook.

    *)
  9. mma_operand_fragments : a_prec:Ops.prec -> b_prec:Ops.prec -> d_prec:Ops.prec -> scope:mma_emission_scope -> mma_fragments Base.option;
    (*

    How the backend's mma arm for these storage precisions, in this emission scope, brings its operands into registers: its intrinsic tile and whether it reloads each operand fragment for every tile of the other's free axis (gh-ocannl-1254). The arm's own description, read by the cost model's operand-load count, which has no count where this is None: no arm, or a backend that does not describe its emission (the default).

    *)
  10. mma_kernel_workgroup_bytes : triples:(Ops.prec * Ops.prec * Ops.prec) Base.list -> Base.int;
    (*

    Static workgroup-shared bytes a kernel declares beyond the staged tiles for its tile-MMA emission scopes, given their (d, a, b) storage triples: the backend's C_syntax_config.mma_kernel_workgroup_bytes, which it derives from the same predicate as the kernel-scope declaration (C_syntax_config.mma_kernel_prologue). A per-KERNEL term, not a per-scope one: non-zero only for Metal's converted destination boundary, whose coordinate table is declared once per kernel however many scopes read it (gh-ocannl-1205, gh-ocannl-1255). Schedule.check_hardware_limits_classified adds it once to the staged tiles.

    *)
  11. workgroup_allocation_alignment : Base.int;
    (*

    The granule the device's allocator rounds each workgroup-shared array up to, in bytes: the backend's C_syntax_config.workgroup_allocation_alignment. Metal rounds every threadgroup array to 4 bytes (measured on an M4 Max: two odd-length half tiles whose exact bytes total the limit are refused, the driver requesting 4 bytes more, gh-ocannl-1226), so Schedule.workgroup_memory_bytes rounds each allocation it counts up to this before summing (gh-ocannl-1330); 1 where the sum of exact sizes is what the device allocates.

    *)
  12. lane_all_reduce_workgroup_bytes : extent:Base.int -> prec:Ops.prec -> Base.int;
    (*

    Static workgroup-shared bytes one lane all-reduce site declares for its per-simdgroup partials (gh-ocannl-1168): a reduction of extent lanes into a scope local residing at prec, rendered across more than one simdgroup. The renderer (C_syntax.try_lane_all_reduce) decides by this same field, so the two cannot drift; 0 where the site renders within one simdgroup, serially, or on a backend without shuffles, barriers or shared declarations. Only the backend-level half: whether a site may stage partials at all is Low_level.lane_all_reduce_sites' structural predicate, which Schedule.workgroup_memory_bytes reads beside it.

    *)
  13. warp_reduce_workgroup_bytes : extent:Base.int -> prec:Ops.prec -> Base.int;
    (*

    Static workgroup-shared bytes one warp-shuffle reduction into a cell declares for its per-warp partials (gh-ocannl-1452): a Workgroup_reduce accumulation of extent lanes whose shuffles stage at the accumulator residency prec, rendered across more than one warp (C_syntax.try_warp_reduce's wred_partials_ array, one slot per warp). The renderer checks its declaration against this same field, so the two cannot drift; 0 where the reduction fits one warp, or where the multi-warp form cannot render (no shuffles, barriers or shared declarations, an extent not a power-of-two count of at most warp-size whole warps, a narrow residency) — the renderer refuses those loudly, so no kernel declares them. Only the backend-level half: which statements are such reductions is Low_level.warp_reduce_cell_sites' structural predicate, which Schedule.workgroup_memory_bytes reads beside it.

    *)
}

Stable code-generation facts callers need before compiling. Actual rendering decisions stay on the compiled routine's censuses.

val no_codegen_capabilities : codegen_capabilities

Conservative implementation-facing defaults for missing and mock backends. A real C-family backend derives this record from its Ir.C_syntax.C_syntax_config.

val codegen_capabilities_fingerprint : codegen_capabilities -> Base.String.t

A stable, exhaustive rendering of a capability record under the CURRENT numerics policy: its flags, each precision-resolution function tabulated over every precision (Ops.all_precs), and its mma arm table over every storage triple and emission scope (gh-ocannl-1153). It is what a numerics mode resolves to on this backend (gh-ocannl-1117) — the mode itself is fingerprinted by Schedule_cache.numerics_tag, but what Bf16_auto means on HIP changed in gh-ocannl-1051 with the mode unchanged, and a winner tuned under the old resolution would have replayed. Tabulating the function codegen calls, rather than naming the predicate behind it, makes the cache identity move exactly when the resolution does, on every backend, with nothing to add by hand. The record pattern names every field, so a field added to codegen_capabilities is a compile error here until it is rendered (warning 9).

mma_operand_loads, mma_accepts and mma_operand_fragments are not rendered either (gh-ocannl-1254): which operands load through ldmatrix, and how often, is a fixed function of the arm, which the mma row below already renders, and of each operand's transposition, space and layout, which are the schedule's own content and so already in its canonical digest. Rendering them would add no discrimination, only re-tunes.

The fields deliberately not rendered are mma_kernel_workgroup_bytes, workgroup_allocation_alignment, lane_all_reduce_workgroup_bytes and warp_reduce_workgroup_bytes (the same argument holds for all four): it decides whether a schedule FITS, never what a kernel computes, and it is a function of the storage triples, the numerics policy and the backend, which the key already carries (Schedule_cache.numerics_tag and the backend identity), so rendering it would add no discrimination. An emitter change that grows the scratch is caught without it: a replayed winner passes the same Schedule.check_hardware_limits_classified gate as a fresh candidate, so one that no longer fits is refused there as a typed cause, which autotune answers by re-searching (gh-ocannl-572: over-keying is a defect too).

val no_hardware_limits : hardware_limits

Backend capability facts (gh-ocannl-1723)

What the scheduler, the autotuner, the schedule cache and codegen ask about a backend, stated by the backend rather than read off its name. Static: answering touches no driver, unlike hardware_limits (per device, queried from the driver) and codegen_capabilities (per configuration). Every backend binds one of the record literals below as its Backend_device_common.backend_facts -- the installed implementation and the missing-toolkit placeholder (Lowered_backend_missing) alike, which is why the literals live here, in the layer both link: a test on a cc box models Metal or CUDA scheduling through the placeholder's facts. No literal is a { x with ... } update, so a new fact is a compile error until every backend states it.

type grid_execution =
  1. | Hardware_grid
    (*

    Grid loops bind the device's hardware indices (workgroups and threads): the GPU default presets and sketch family, zero segments as their own kernels, and code binding no hardware dimension running the whole routine on one thread (gh-ocannl-532).

    *)
  2. | Host_pool
    (*

    Grid loops render as fork-join dispatches over the host CPU worker pool: the CPU default preset, zero segments as memset, the register-tiled matmul rendering.

    *)

Where a routine's Grid loops execute. Exactly one of the two by construction: a caller phrased if binds_hardware_grid ... else ... cannot take the CPU branch for a backend that is neither (gh-ocannl-706).

val equal_grid_execution : grid_execution -> grid_execution -> Base.bool
type queued_timing =
  1. | Queued_legacy
    (*

    Batches capped at the historical depth 200; the key spells the objective bare (queued).

    *)
  2. | Queued_deep
    (*

    Batches capped at depth 2048, so a sub-microsecond device kernel still amortizes the host round trip; the policy is versioned, and the key spells its generation (queued-v<N>, Schedule_cache.queued_objective_version).

    *)

The queued timing objective's policy (Autotune's queued batches, and the cache key's timing component).

val equal_queued_timing : queued_timing -> queued_timing -> Base.bool
type backend_facts = {
  1. grid_execution : grid_execution;
  2. workgroup_shared_placement : Base.bool;
    (*

    A node can be placed in workgroup-shared memory (the shared epilogue placement of Schedule's Fuse_epilogue): the premise of Schedule.may_keep_off_device. CPU codegen rejects a non-empty workgroup_shared.

    *)
  3. seeds_untuned_default : Base.bool;
    (*

    The untuned compile seeds its plan (Autotune.seeded_default, gh-ocannl-1183): the backends that plan's before/after was measured on. Also scopes the seeded plan's numerics gate in Schedule_cache.codegen_tag (gh-ocannl-1405).

    *)
  4. queued_timing : queued_timing;
  5. retries_contended_window : Base.bool;
    (*

    A queued timing window refused as contended is retried once: the device scheduler's transient bursts (Metal).

    *)
  6. host_load_attributable : Base.bool;
    (*

    A dispersed timing window is attributed through the host-load signal rather than refused outright (gh-ocannl-1558): the device's co-tenants are host processes, which host CPU accounting sees.

    *)
  7. node_ident_prefix : Base.string;
    (*

    The dialect's tensor-node identifier prefix (C_syntax_config.node_ident_prefix, gh-ocannl-1338): "t_" where the vendor headers declare global names outside the reserved namespace.

    *)
  8. source_extension : Base.string;
    (*

    The extension, dot included, of the routine source a backend writes as its build_files/ artifact: what each backend's artifact writer appends, and what test/config/config_reads.ml's code_extension -- linked by tests that link no backend -- is held equal to by test/operations/marker_backend_vocabulary.

    *)
  9. blends_value_selects : Base.bool;
    (*

    The dialect renders a value select that Low_level.speculable_select admits as a branchless blend, evaluating every arm (C_syntax_config.select_blend_syntax, gh-ocannl-1622, gh-ocannl-1695): cc and HIP, at the float compute precisions their blends cover. CUDA's ptxas if-converts the short-circuiting spelling itself and Metal keeps ?:. The renderers derive their blend from this fact, and the inline fan-in guard reads it before codegen (gh-ocannl-1775): on a blending backend a speculable Where is charged the union of its arms' loads, elsewhere its wider arm.

    *)
}
val equal_backend_facts : backend_facts -> backend_facts -> Base.bool
val binds_hardware_grid : backend_facts -> Base.bool
val renders_on_host_pool : backend_facts -> Base.bool
val facts_inconsistencies : backend_facts -> string Base.List.t

The relations between facts no backend may break, as the violations found ([] when consistent): workgroup-shared placement, the seeded untuned plan (a GPU sketch), the deep queued policy and the contended-window retry (device-dispatch timing) all presuppose a hardware grid, and a dialect prefixes its node identifiers exactly when its vendor headers are a GPU toolchain's.

val cc_facts : backend_facts

The C backends, cc and multidev_cc alike: both render on the host worker pool.

val cuda_facts : backend_facts
val hip_facts : backend_facts
val metal_facts : backend_facts

Cache identity of the device description (gh-ocannl-1253)

The schedule cache hashes the device description into its key (Schedule_cache.codegen_tag), so that two devices of one backend that construct, render or time candidates differently never share a winner. Not every field is such a fact: the ranking score's capacity and residency steer only the untimed choosers, and a stored winner is a measured schedule whose compilation and speed they do not change — keying on them would only force re-searches (gh-ocannl-572: over-keying is a defect too). So every field has a class, the way configuration keys have Utils.config_key_classification:

The classification IS limits_key: it destructures the whole record with no ; _, so a new field is a compile error (warning 9) until it is classified — projected into Limits_key.t or discarded as Untimed. test/operations/limits_field_classes holds the rest: toggling an Untimed field leaves the key unchanged and toggling a Key field changes it, the projection renders byte-identically to the full record with its Untimed fields absent (so no stored key moved when they were added), and no Untimed field is read outside its readers.

module Limits_key : sig ... end
val limits_key : hardware_limits -> Limits_key.t

The projection the schedule cache keys on, and the classification itself: every Key field is copied, every Untimed field is bound to _.

type untimed_limits = {
  1. cache_capacity : Base.int Base.option;
  2. resident_threads : Base.int Base.option;
  3. cache_rate : Base.float Base.option;
  4. scalar_rate : Base.float Base.option;
    (*

    field-hardware_limits.issue_rate.

    *)
  5. dram_rate : Base.float Base.option;
    (*

    field-hardware_limits.dram_bandwidth.

    *)
  6. matrix_rate : Base.float Base.option;
    (*

    field-hardware_limits.mma_rate.

    *)
  7. gather_rate : Base.float Base.option;
    (*

    field-hardware_limits.device_gather_rate.

    *)
  8. core_count : Base.int Base.option;
    (*

    field-hardware_limits.multiprocessors.

    *)
  9. saturation_threads : Base.int Base.option;
    (*

    field-hardware_limits.fill_threads.

    *)
  10. sector_rate : Base.float Base.option;
    (*

    field-hardware_limits.transaction_rate.

    *)
  11. workgroup_capacity : Base.int Base.option;
    (*

    field-hardware_limits.workgroup_bytes_per_multiprocessor.

    *)
}

The Untimed fields' readings, the one accessor their readers go through (test/operations/limits_field_classes refuses a direct read elsewhere).

val untimed_limits : hardware_limits -> untimed_limits
val minimal_mma_capability : mma_capability

A base for { minimal_mma_capability with ... } literals, so adding a field to mma_capability touches one record instead of every mock and CPU descriptor: a 32-wide simdgroup with the 16×16×16 canonical tile, and no format tiles, wide-accumulator scopes, staged layouts or pipelining depths advertised.

type device_dump = {
  1. group : Base.string;
    (*

    The group atom naming the dump, e.g. "cuda_devices".

    *)
  2. devices : (Base.string * Base.Sexp.t) Base.list Base.list;
    (*

    One (key, value) assoc per device, in ordinal order.

    *)
}
val sexp_of_device_dump : device_dump -> Sexplib0.Sexp.t
val parse_static_properties : Base.Sexp.t -> device_dump Base.option

Reads a Backend_device_common.static_properties dump per its contract (gh-ocannl-710), or answers None when the sexp is not a device dump at all.

The contract, which every backend that enumerates devices honors and this function is the single reader of:

  • The dump is (<group> <entry> ...): an atom naming the group, then the entries. <group> ends in "_devices" -- and is <backend name>_devices -- exactly when the dump enumerates devices, so a dump that describes something else (an unlinked backend's (<backend>_missing (error ...)), see Lowered_backend_missing) is distinguishable without guessing.
  • Every entry of a _devices dump is a device, and is Sexp.message-shaped: the atom device followed by (key value) pairs, ONE nesting level -- no list-of-pairs wrapper around the pairs. There is one entry per device, in ordinal order.
  • Every device carries at least device_name and device_ordinal, and all the devices of one dump carry the same keys, so an entry indexes uniformly and two machines' dumps diff line by line.
  • The device COUNT is not a child of its own: it is the number of entries. Neither is any other backend-level fact -- a child that is not a device is what made a generic reader reproduce the backend's num_devices as a second, fictitious device (gh-ocannl-710).

A dump violating the shape answers None rather than a partial reading: a reader that invents structure is worse than one that says it does not recognize the shape. Uniform keys and the ordinal sequence are contract too, but are not enforced here -- they are what test/operations/static_properties_contract.ml checks against each backend's real dump.

val simd_lane_ladder : vector_bytes:Base.int -> elt_bytes:Base.int -> Base.int Base.list
val simd_lanes_for : vector_bytes:Base.int -> elt_bytes:Base.int -> extent:Base__Int.t -> Base__Int.t option
val simd_reduce_chains : lanes:Base__Int.t -> extent:int -> int
val simd_reduce_lanes_for : vector_bytes:Base.int -> elt_bytes:Base.int -> extent:Base__Int.t -> Base__Int.t option

The lane count for an ACCUMULATING Vectorized loop, which simd_lanes_for would get wrong at short extents: that rendering ends in a horizontal fold whose length is the lane count itself, so a wider vector buys fewer updates and pays a longer dependent tail. At an f32 extent of 64, 16 lanes save four vector updates over 8 and add eight operations to the fold — the wider width losing on a loop the elementwise metric would hand it. The cost mirrors the emission term for term, so the two cannot drift: chains and step as C_syntax computes them, the serial leftover, and the epilogue's vector combines and scalar fold.

The lane count an explicit-SIMD rendering should use for a loop of extent iterations over elt_bytes-wide elements on a vector_bytes-wide register file: the width of simd_lane_ladder that minimizes loop trips, None where even the narrowest exceeds the extent. (The register-tiled micro-kernel searches the ladder itself: its remainder is a narrower register tile with less operand reuse, and Register_tile.default prices that.)

A single width would make a wider machine emit less vector code than a narrower one — the renderings decline outright below one full vector, so widening the auto cc_vector_bytes from 32 to 64 on an AVX-512 target (gh-ocannl-621 follow-up) would drop every f32 loop of extent 8..15 to the serial fallback, and an accumulating loop loses reassociation with it, which is the whole point of the Vectorized retype. Nor is "the widest that fits" enough: at extent 40 a 16-lane vector covers 32 columns and leaves 8 to scalar code, where 8 lanes divide the extent evenly — a wider register file made to run slower. Hence a ladder and a choice, not a number.

The floor is min vector_bytes 32 — never narrower than the width the machine used before any widening. Degrading past it would newly vectorize loops that used to render serially, and a vector accumulation reassociates, so that is a numerics change rather than a scheduling one and does not belong in a width default.

Shared by the renderer (C_syntax) and autotune's seeding pre-filter (Sketch_families) so the two agree on which extents are vectorizable: a stricter seeding rule would withhold candidates the renderer would in fact tile.

module type Slab_alloc = sig ... end

The backend slab allocator, replacing the per-tnode Alloc_buffer interface. The shared allocator seam (see Backends) mints deterministic per-device pool_ids and calls these int-in / int-out primitives; the backend keeps the pool_id -> 'base table private. The pool_id -> 'base resolution (then base + offset) stays inside the backend.

type merge_buffer_use =
  1. | No
  2. | Copy
val sexp_of_merge_buffer_use : merge_buffer_use -> Sexplib0.Sexp.t
type kparam_source =
  1. | Log_file_name
  2. | Merge_buffer
  3. | Kparam_ptr of Tnode.t
  4. | Kparam_pool_slab of Base.int
    (*

    gh-ocannl-344: the i-th pool base-pointer parameter of a pooled kernel (Metal). A fixed number of these is emitted; at link the backend binds slab i to the pool assigned index i (or a duplicate of an in-use pool for the unused tail). Lets a kernel reach hundreds of tensor nodes through a handful of bound pools, staying under Metal's ~31 binding limit.

    *)
  5. | Kparam_pool_slots of Tnode.t Base.list
    (*

    gh-ocannl-344: the per-routine slot table accompanying Kparam_pool_slab. For the k-th tnode in this list the backend writes (pool_index, byte_offset); the shader reads it to form the typed pointer by casting (pools at pool_index) + byte_offset. Emitted only by pooled (Metal) codegen; per-tnode pointer backends (C, CUDA) never produce it.

    *)
  6. | Static_idx of Indexing.static_symbol

Kernel-parameter sources: the codegen <-> backend contract for a compiled routine's parameters. Implementation-facing (consumed by C_syntax and the backends' link steps); it lives in this file because the shared Backend_impl.Lowered_no_device_backend signature mentions it.

val sexp_of_kparam_source : kparam_source -> Sexplib0.Sexp.t
val unexpected_pooled_kparam : backend:Base.String.t -> 'a

The link-time impossibility every `Per_param backend shares: pooled kparams are emitted only by pooled codegen, so a backend whose C_syntax_config.ptr_param_style is `Per_param can never be handed one. Named here, once, next to the constructors whose invariant it states.

type 'context routine = {
  1. context : 'context;
  2. schedule : Task.t;
  3. bindings : Indexing.lowered_bindings;
  4. name : Base.string;
  5. inputs : Base.Set.M(Ir.Tnode).t;
    (*

    The materialized read-only and read-before-write (within the routine) non-constant nodes. They are inputs in a broad sense, as they could be recurrent nodes or parameters.

    *)
  6. merge_buffer_input : Tnode.t Base.option;
    (*

    Similar to inputs, for the merge buffer. The execution-dependency ledger consumes this as a read edge on the transfer that filled the transient slab, but it is not an ordinary context input requiring initialization.

    *)
  7. outputs : Base.Set.M(Ir.Tnode).t;
    (*

    All the materialized nodes written-to by the routine.

    *)
}
val sexp_of_routine : 'context. ('context -> Sexplib0.Sexp.t) -> 'context routine -> Sexplib0.Sexp.t
module type Device_config_common = sig ... end
module Constant_key : sig ... end

The key of a device's constant cache (gh-ocannl-1654). A node every routine of a compile only reads, and that is not context_owned, lives in a constant pool that outlives the context that allocated it, until the last release unit binding it is released (field-device.constant_owners). A host-initialized literal (Host_inits) is Shared: one buffer per device, uploaded once, which every root on the device binds while any of them holds it. Any other such node -- a data input, a zero-valid accumulator -- is Rooted under the field-context.constant_root of the independently created context that allocated it, so compile siblings and descendants of one root share it while another root gets a buffer of its own: a write in one root can never change what another root reads.

val next_constant_root : int Stdlib.Atomic.t

The source of field-context.constant_root: one per Device.make_context.

type ('dev, 'runner, 'event) device = {
  1. dev : 'dev;
  2. ordinal : Base.int;
    (*

    The number of the represented backend's device, in the range from 0 to the number of the backend's devices - 1.

    *)
  3. device_id : Base.int;
    (*

    A unique identifier among all device instances of all backends. Note that multiple device_id (distinct device instances) might refer to the same physical device.

    *)
  4. runner : 'runner;
  5. merge_buffer : buffer_loc Base.option Base.ref;
    (*

    The merge buffer's reserved single-tenant pool location, or None if not yet allocated. The slab can be reused (grown in place) for nodes that fit.

    *)
  6. mutable merge_buffer_capacity : Base.int;
    (*

    Byte capacity of the reserved merge-buffer pool; drives the grow decision.

    *)
  7. updating_for : 'event Base.Hashtbl.M(Ir.Tnode).t;
    (*

    The completion event for the most recent updating (writing to) a node via this device, while it is not done: each new write drops the done entries first (gh-ocannl-1747), so the table does not keep the nodes of graphs nobody holds.

    *)
  8. mutable updating_for_merge_buffer : (Tnode.t * 'event Base.option) Base.option;
    (*

    The tensor node that was most recently scheduled to be in the device's merge buffer. See also updating_for.

    *)
  9. constant_buffer_cache : buffer_loc Base.Hashtbl.M(Constant_key).t;
    (*

    Per-device cache for read-only/constant buffer allocations: device-wide for host literals, per root otherwise (Constant_key). Either kind's pool is freed, and its entries dropped, when no live release unit binds it (constant_owners); what is left at device teardown is freed there.

    *)
  10. constant_owners : Base.int Base.Hashtbl.M(Base.Int).t;
    (*

    For each constant pool (one holds either Rooted entries of one root or Shared ones, never both, and nothing else), how many live release units bind it (field-context.held_constant_pools), across roots for a Shared pool; the release taking it to 0 frees the pool and drops its cache entries (gh-ocannl-1654 for Rooted, gh-ocannl-1762 for Shared: dropping a literal's strong Tnode.t key is what lets its node, and through Host_inits its host array, be collected). Guarded by constant_mutex.

    *)
  11. constant_mutex : Stdlib.Mutex.t;
    (*

    The lock of constant_owners and of the cache entries a release drops.

    *)
  12. mutable next_pool_id : Base.int;
    (*

    Deterministic per-device pool-id counter, advanced by the shared allocator seam in tnode iteration order. Pool id 0 is reserved for the merge buffer; tnode pools start at 1.

    *)
}

A device bundles its single compute runner with the associated buffer and event tracking: the merge_buffer, the updating_for writer events (used for cross-device coherence by Backend.device_to_device), and the deterministic pool-id counter. The design is forward-compatible with a future fixed-role prefetch/transfer runner.

val sexp_of_device : 'a -> 'b -> 'c -> ('d, 'e, 'f) device -> Sexplib0.Sexp.t
val equal_device : ('a, 'b, 'c) device -> ('d, 'e, 'f) device -> bool
val merge_buffer_pool_id : int

Pool id 0 on every device is reserved for the (single-tenant) merge buffer.

val invalidate_merge_slab : ('a, 'b, 'c) device -> remove_slab_claim:(unit -> unit) -> unit

Invalidate every software ownership claim for the reserved merge slab as one transaction. remove_slab_claim drops the backend-private pool-table entry and must not perform the fallible raw free: callers free the previously-found slab only after this function has made it unreachable. The writer is recommitted separately, after the replacement copy is scheduled.

val commit_merge_slab : ('a, 'b, 'c) device -> size_in_bytes:Base.int -> install_slab_claim:(unit -> unit) -> unit

Commit a successfully allocated reserved merge slab and its backend-private table entry as one transaction. The writer marker stays invalid until the copy into this slab is scheduled.

type ('dev, 'runner, 'event) context = {
  1. device : ('dev, 'runner, 'event) device;
  2. parent : ('dev, 'runner, 'event) context Base.option;
  3. ctx_buffers : ctx_buffers;
    (*

    This map contains the deterministic buffer locations used in this context or an ancestor context.

    *)
  4. finalized : Utils.atomic_bool;
  5. mutable released_pool_ids : Base.Set.M(Base.Int).t;
    (*

    Pools this context has already released. Retained across a failed-finalize retry so a cleanup that freed some pools before raising never calls the backend free twice.

    *)
  6. upload_tip : ctx_buffers Base.ref;
    (*

    Latest map in this linear upload chain. Uploading from an older map forks its ownership into an independent leaf; linear uploads supersede their argument without an extra owner.

    *)
  7. upload_arenas : upload_arenas;
    (*

    This upload lineage's arenas and pool reference counts, fresh in Device.make_child.

    *)
  8. release_hooks : (Base.unit -> Base.unit) Base.list Base.ref;
    (*

    What a release does besides freeing pools: drop device resources a routine linked into this context retains across runs (the CUDA/HIP graph execs of a graph-captured segment batch, gh-ocannl-1445). Run by every finalize of a value that shares the list -- a linear upload chain and its upload forks -- so each hook must be idempotent and must leave its routine runnable (rebuilding what it dropped), since a fork can outlive the value it came from. Fresh in Device.make_child.

    *)
  9. optimize_ctx : Low_level.optimize_ctx;
    (*

    The optimization context threaded through compilation: all OCANNL backends compile through the Low_level IR, so this is concretely Low_level.optimize_ctx (the abstraction for hypothetical assignments-level backends was retired; the Assignments.comp -> code seam can be reintroduced if such a backend ever materializes).

    *)
  10. held_constant_pools : Base.Set.M(Base.Int).t;
    (*

    The constant pools this release unit binds -- its root's Rooted ones and the Shared host literals it resolved -- each counted once in field-device.constant_owners when the unit was created and released when it is finalized. Shared by every value of the unit (record copies keep it).

    *)
  11. constant_root : Base.int;
    (*

    The independently created context this one derives from, as far as constants go: minted by Device.make_context, inherited by every derived context. Keys the non-literal entries of field-device.constant_buffer_cache (Constant_key).

    *)
  12. merge_buffer_node : Tnode.t Base.option;
    (*

    The tensor node that a Backend.device_to_device transfer with into_merge_buffer:Copy placed (or will place) into this context's device's merge buffer. It is a static, immutably-chained fact carried producer -> consumer: linking a consumer whose code expects a merge-buffer node verifies it against this field at link time. A transfer with into_merge_buffer:No does not touch the merge buffer and inherits the parent's value.

    *)
}
val sexp_of_context : 'dev 'runner 'event. ('dev -> Sexplib0.Sexp.t) -> ('runner -> Sexplib0.Sexp.t) -> ('event -> Sexplib0.Sexp.t) -> ('dev, 'runner, 'event) context -> Sexplib0.Sexp.t
val constant_key : root:Base.int -> Tnode.t -> Constant_key.t

Where a fresh constant allocation of tn by a context of root is cached (Constant_key).

val find_constant : ('a, 'b, 'c) device -> root:Base.int -> Tnode.t -> buffer_loc option

The cached constant buffer of tn a context of root resolves to: its root's own, else the device-wide literal.

val find_shared_constant : ('a, 'b, 'c) device -> Tnode.t -> buffer_loc option

The device-wide (host-literal) constant buffer of tn, the one kind every root shares.

val constant_pools_of : ('a, 'b, 'c) device -> root:Base.int -> ctx_buffers -> (Base.Int.t, Base.Int.comparator_witness) Base.Set.t

The constant pools ctx_buffers binds, for a context of root: its root's Rooted ones (gh-ocannl-1654) and the device-wide Shared literals (gh-ocannl-1762) -- what a release unit created over that map holds. A node bound at its cached location, under either key.

val hold_constants : ('a, 'b, 'c) device -> (Base.Int.t Base.Hashtbl.key, 'd) Base.Set.t -> (Base.Int.t Base.Hashtbl.key, 'd) Base.Set.t

Count a new release unit binding pools; returns them, for its held_constant_pools.

val reclaim_constants : ('a, 'b, 'c) device -> (Base.Int.t Base.Hashtbl.key, Base.Int.comparator_witness) Base.Set.t -> mode:[< `Release | `Unheld | `Unhold ] -> (Base.int, Base.Int.comparator_witness) Base.Set.t

Give back constant pools (gh-ocannl-1654, gh-ocannl-1762), under the device's lock, in one of three ways; returns the pools the caller must now free, already dropped from the cache so no later compile resolves them:

  • `Release: a finalized release unit's holds. The pools no live unit binds any more.
  • `Unhold: the holds of a unit discarded before anyone could finalize it (a link failing after its make_child). Nothing: the cache is left as it is, for the delta's discard to reclaim.
  • `Unheld: of the pools a discarded delta binds, those no live unit holds. A pool another unit holds, such as a cache hit an earlier compile of the root allocated, stays.
val owned_pool_ids : device:('a, 'b, 'c) device -> root:Base.int -> owned_elsewhere:(Tnode.t -> Base.bool) -> (Tnode.t, buffer_loc, 'd) Base.Map.t -> (Base.Int.t, Base.Int.comparator_witness) Base.Set.t

Working pools in a context map, excluding compile ancestors and cached constant locations. Shared by ownership registration and cleanup so both use the same pool classification.

val evolve_with_buffer : ('a, 'b, 'c) context -> Tnode.t -> buffer_loc -> ('a, 'b, 'c) context

Extend the current upload leaf in place, or fork an older value into an independent leaf. Linear uploads keep one owner, even if callers retain intermediate persistent values. A fork retains only pools present in its result, so releasing either sibling preserves shared data and the last sibling frees it. Registration is the final, non-fallible transfer commit.

module type Device_types = sig ... end
module type Device = sig ... end
module type Backend_device_common = sig ... end

The device, event and synchronization part of the backend interface, shared by the user-facing Backend and the implementation-facing Backend_impl.Lowered_backend. Does not include: compilation and linking (they differ between the user-facing and lowered interfaces); copying and tensor-node-level synchronization (copying is different for user-facing and implementation-facing APIs, synchronization is provided by a component outside of backend implementations).

module type With_buffer_retrieval_and_syncing = sig ... end

gh-ocannl-1493: the device allocation linking a routine would make on a context — its delta, not the routine's whole footprint: nodes the context already holds and constants the device already caches are not in it.

gh-ocannl-1554: a floor under the device allocation of every schedule of a lowering a compile can ship on a context — what the device must admit for any of them to link. The bytes are raw node sizes (no layout padding), over the nodes of the lowering's delta its code touches, less what some schedule can keep off the device or replace, and under the liveness planner counting only nodes no kernel sequence can overlap plus the largest overlap-eligible one per precision class (these conflict pairwise in every plan).

module type Backend = sig ... end