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
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 mma_input_format_of_sexp : Sexplib0.Sexp.t -> mma_input_format
val sexp_of_mma_input_format : mma_input_format -> Sexplib0.Sexp.t
val compare_mma_input_format : mma_input_format -> mma_input_format -> Base.int
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. 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 mma_staged_layout_of_sexp : Sexplib0.Sexp.t -> mma_staged_layout
val sexp_of_mma_staged_layout : mma_staged_layout -> Sexplib0.Sexp.t
val compare_mma_staged_layout : mma_staged_layout -> mma_staged_layout -> Base.int
val equal_mma_staged_layout : mma_staged_layout -> mma_staged_layout -> Base.bool
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_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, can feed A from ldmatrix in that orientation but not B — 4 fp8 bytes of a B register are strided there — so a swizzled fp8 twin would be timed and ranked as a tensorized candidate while rendering the scalar fallback. Empty everywhere the question does not arise.

    *)
}

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 mma_capability_of_sexp : Sexplib0.Sexp.t -> mma_capability
val sexp_of_mma_capability : mma_capability -> Sexplib0.Sexp.t
val compare_mma_capability : mma_capability -> mma_capability -> Base.int
val equal_mma_capability : mma_capability -> mma_capability -> Base.bool
type hardware_limits = {
  1. 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).

    *)
  2. 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.

    *)
  3. 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.

    *)
  4. simd_vector_bytes : Base.int;
    (*

    Vector register width in bytes used by the C backends' explicit vector-extension renderings (Vectorized loops, the register-tiled Tile_mma micro-kernel); 0 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.

    *)
  5. 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.

    *)
  6. 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.

    *)
  7. 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.

    *)
}
val hardware_limits_of_sexp : Sexplib0.Sexp.t -> hardware_limits
val sexp_of_hardware_limits : hardware_limits -> Sexplib0.Sexp.t
val compare_hardware_limits : hardware_limits -> hardware_limits -> Base.int
val equal_hardware_limits : hardware_limits -> hardware_limits -> Base.bool
val no_hardware_limits : hardware_limits
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
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.

    *)
  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
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.

    *)
  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(Ir.Tnode).t;
    (*

    Per-device cache for read-only/constant buffer allocations.

    *)
  10. 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.

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. 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).

    *)
  6. 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
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
module type Backend = sig ... end