Ir.Backend_intfThe 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.
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_locval sexp_of_buffer_loc : buffer_loc -> Sexplib0.Sexp.tval compare_buffer_loc : buffer_loc -> buffer_loc -> Base.intval equal_buffer_loc : buffer_loc -> buffer_loc -> Base.booltype ctx_buffers = buffer_loc Base.Map.M(Ir.Tnode).tval sexp_of_ctx_buffers : ctx_buffers -> Sexplib0.Sexp.tDevice 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 = | Mma_f32Genuine f32 multiply-accumulate (Metal simdgroup_float8x8).
| Mma_tf32f32 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.
| Mma_f16| Mma_bf16| Mma_fp8_e5m2OCANNL'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_formatval sexp_of_mma_input_format : mma_input_format -> Sexplib0.Sexp.tval compare_mma_input_format : mma_input_format -> mma_input_format -> Base.intval equal_mma_input_format : mma_input_format -> mma_input_format -> Base.booltype mma_staged_layout = | Mma_swizzled_b128Low_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_layoutval sexp_of_mma_staged_layout : mma_staged_layout -> Sexplib0.Sexp.tval compare_mma_staged_layout :
mma_staged_layout ->
mma_staged_layout ->
Base.intval equal_mma_staged_layout :
mma_staged_layout ->
mma_staged_layout ->
Base.booltype mma_capability = {mma_simd_width : Base.int;Threads cooperating in one tile-MMA instruction (CUDA warp / Metal simdgroup width).
*)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.
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.
mma_f16_wide_acc : Base.bool;Whether the backend's uniform-f16 arm — an f16-storage destination with f16 operands — can hold the accumulator in f32 and convert once at the d boundary, which is what the Numerics.fp16_mode.Fp16_wide policy requires of every rendering (gh-ocannl-680). CUDA's inline-PTX mma.sync.m16n8k16 arm can (sm_80+, the architecturally-defined fragment layouts are shared by .f16 and .bf16); HIP can since gh-ocannl-789 (rocWMMA's (f16, f16, f32) fragments, the boundary converted elementwise through a destination-typed accumulator fragment); Metal cannot (simdgroup_matrix is uniform-precision only, structurally). Where false, the seeding gate in Sketch_families.mma_tile_for_precisions withholds uniform-f16 seeds under the wide policy — the serial legs then carry the f32 residency via accum_prec, keeping the width schedule-uniform per the gh-ocannl-545/663 discipline.
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.
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 mma_capability_of_sexp : Sexplib0.Sexp.t -> mma_capabilityval sexp_of_mma_capability : mma_capability -> Sexplib0.Sexp.tval compare_mma_capability : mma_capability -> mma_capability -> Base.intval equal_mma_capability : mma_capability -> mma_capability -> Base.booltype hardware_limits = {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).
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.
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, so the gate is a backstop rather than the first line of defence.
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.
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.
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.
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).
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.
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.
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.
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 hardware_limits_of_sexp : Sexplib0.Sexp.t -> hardware_limitsval sexp_of_hardware_limits : hardware_limits -> Sexplib0.Sexp.tval compare_hardware_limits : hardware_limits -> hardware_limits -> Base.intval equal_hardware_limits : hardware_limits -> hardware_limits -> Base.boolval no_hardware_limits : hardware_limitstype device_dump = {group : Base.string;The group atom naming the dump, e.g. "cuda_devices".
devices : (Base.string * Base.Sexp.t) Base.list Base.list;One (key, value) assoc per device, in ordinal order.
}A parsed Backend_device_common.static_properties dump: see parse_static_properties.
val sexp_of_device_dump : device_dump -> Sexplib0.Sexp.tval parse_static_properties : Base.Sexp.t -> device_dump Base.optionReads 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:
(<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._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.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.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.
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 peel is a scalar column loop rather than a remainder of the same body, and it has a fitted cost model for 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 ... endThe 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.
val sexp_of_merge_buffer_use : merge_buffer_use -> Sexplib0.Sexp.ttype kparam_source = | Log_file_name| Merge_buffer| Kparam_ptr of Tnode.t| Kparam_pool_slab of Base.intgh-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.
| Kparam_pool_slots of Tnode.t Base.listgh-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.
| Static_idx of Indexing.static_symbolKernel-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.tThe 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 = {context : 'context;schedule : Task.t;bindings : Indexing.lowered_bindings;name : Base.string;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.
*)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.
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.tmodule type Device_config_common = sig ... endtype ('dev, 'runner, 'event) device = {dev : 'dev;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.
*)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.
runner : 'runner;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.
mutable merge_buffer_capacity : Base.int;Byte capacity of the reserved merge-buffer pool; drives the grow decision.
*)updating_for : 'event Base.Hashtbl.M(Ir.Tnode).t;The completion event for the most recent updating (writing to) a node via this device.
*)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.
constant_buffer_cache : buffer_loc Base.Hashtbl.M(Ir.Tnode).t;Per-device cache for read-only/constant buffer allocations.
*)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.tPool 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) ->
unitInvalidate 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) ->
unitCommit 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 = {device : ('dev, 'runner, 'event) device;parent : ('dev, 'runner, 'event) context Base.option;ctx_buffers : ctx_buffers;This map contains the deterministic buffer locations used in this context or an ancestor context.
*)finalized : Utils.atomic_bool;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.
*)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).
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.tval evolve_with_buffer :
('a, 'b, 'c) context ->
Tnode.t ->
buffer_loc ->
('a, 'b, 'c) contextThe one constructor for evolving a context in place of itself with a newly allocated buffer: the result supersedes ctx as the lineage leaf while deliberately keeping its lifecycle identity — the same context.parent link and the same context.finalized flag, so at most one of the pair can ever free the pools they share — and no context creation is counted in Alloc_census. The buffer-allocating transfer entry points (init_from_host, init_from_device) go through this; a compile/link result is a new lifecycle node and goes through Device.make_child instead.
module type Device_types = sig ... endmodule type Device = sig ... endmodule type Backend_device_common = sig ... endThe 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 ... endmodule type Backend = sig ... end