Sarek_execute.ExecuteRe-export structured error types
type vector_arg = | Vec : ('a, 'b) Spoc_core.Vector.t -> vector_argV2 Vector - expands to (buffer, length) for JIT
*)| Int : int -> vector_argInteger scalar
*)| Int32 : int32 -> vector_arg32-bit integer scalar
*)| Int64 : int64 -> vector_arg64-bit integer scalar
*)| Float32 : float -> vector_arg32-bit float scalar
*)| Float64 : float -> vector_arg64-bit float scalar
*)V2 Vector argument type - supports automatic transfers and length expansion. This is the main type-safe way to pass arguments to kernels.
val custom_value_to_bytes : 'a. 'a Spoc_core.Vector.custom_type -> 'a -> bytesval custom_value_of_bytes : 'a. 'a Spoc_core.Vector.custom_type -> bytes -> 'aval exec_arg_of_vector :
'a 'b. ('a, 'b) Spoc_core.Vector.t ->
Spoc_framework.Framework_sig.exec_argConvert vector_arg list to exec_arg array (new typed interface). Creates EXEC_VECTOR wrappers for vectors.
val vector_args_to_exec_array :
vector_arg list ->
Spoc_framework.Framework_sig.exec_arg arrayval get_device_buffer :
('a, 'b) Spoc_core.Vector.t ->
Spoc_core.Device.t ->
(module Spoc_core.Vector.DEVICE_BUFFER)Retrieve device buffer for a vector on a specific device.
Returns a first-class module containing the device buffer's pointer, size, and binding function. The buffer must exist (typically created by a prior transfer).
val soa_dispatch :
soa_abi:bool ->
('a, 'b) Spoc_core.Vector.t ->
Spoc_core.Device.t ->
Spoc_core.Vector.soa_binding optionval soa_param_names :
Sarek_ir_types.kernel ->
vector_arg list ->
Spoc_core.Device.t ->
string listval transfer_vectors_to_device :
?soa_abi:bool ->
vector_arg list ->
Spoc_core.Device.t ->
unitTransfer all V2 Vector args to device
val expand_to_run_source_args :
?inject_lengths:bool ->
?soa_abi:bool ->
vector_arg list ->
Spoc_core.Device.t ->
Spoc_framework.Framework_sig.run_source_arg listExpand vector args to run_source_arg format.
The injected length is tagged RSA_Vector_Length, not RSA_Int32, so backends can tell it apart from a genuine caller-supplied scalar that happens to immediately follow a buffer (which is exactly what a ~inject_lengths:false caller can pass) - see Framework_sig.run_source_arg.
val ir_elttype_of_vector_kind :
'a 'b. ('a, 'b) Spoc_core.Vector.kind ->
Sarek_ir_types.elttype optionLaunch-time argument check
The Vec constructor of vector_arg is existential (Vec : ('a, 'b) Vector.t -> vector_arg), so a vector's element type is ERASED the moment it enters an ~args list. No OCaml type constraint on the generated kernel closure can therefore catch passing a float32 vector where the kernel declared a float16 vector — the mismatch happens on a path where the types are already gone. Executed on gfx1100: such a launch compiled clean and read/wrote 2N bytes of a 4N-byte buffer, producing 1 2 0 0 for input 1 2 3 4, with the Native path catching it only by accident.
The IR is the one place where the DECLARED parameters and the SUPPLIED arguments meet, so the check lives here, and it covers every element type.
ARITY IS CHECKED FIRST, and is an error rather than a precondition for the rest. An earlier version ran the per-argument checks only if the counts matched, which made a wrong count silently disable every other check — the conservatism was also a bypass. Worse, a SHORT argument list is a memory-safety problem in its own right and independent of f16: both Cuda_api.Kernel.launch and Hip_api.Kernel.launch size the kernel-argument array with CArray.make (ptr void) (List.length args) and hand cuLaunchKernel / hipModuleLaunchKernel a bare CArray.start params with NO count (the trailing extra pointer is NULL). The driver then reads as many entries as the COMPILED signature declares, so a short list makes it read past the end of that array and dereference whatever it finds as a parameter — for a pointer parameter, an arbitrary device address. Rejecting the arity here is what keeps that unreachable.
Element types are compared exactly where the runtime kind has an IR counterpart. Where it does not, the check does NOT silently pass: it falls back to comparing PHYSICAL ELEMENT WIDTHS, which is the property that actually matters for buffer striding. That closes the wildcard: Vector.Char holds 1-byte elements while source char lowers to TInt32, so a Char vector is accessed through a 4-byte int*. (That lowering is PRE-EXISTING — Sarek_lower_ir.elttype_of_typ mapped TReg Char -> Ir.TInt32 before this branch — and is NOT fixed here; the check simply refuses to be the thing that hides it.)
Still conservative where it must be: a Custom (record/variant) element is nominal and both sides derive it from the same registered layout, so its element comparison is skipped deliberately rather than by omission.
val elttype_label : Sarek_ir_types.elttype -> stringval ir_scalar_width : Sarek_ir_types.elttype -> int optionByte width of an IR element type, when it is a scalar. None for aggregates (whose width comes from the registered layout, not from this table).
val is_custom_kind : 'a 'b. ('a, 'b) Spoc_core.Vector.kind -> boolval ir_elttype_of_scalar_arg : vector_arg -> Sarek_ir_types.elttype optionIR element type a SCALAR launch argument is tagged with on the host. Int and Int32 denote the same 32-bit slot. None for Vec, which is handled by the vector arms.
This tag is exactly what decides how many bytes the launch writes into the argument slot, so it is the host side of the same width contract the vector check enforces — see check_launch_args.
val arg_label : vector_arg -> stringval check_launch_args :
kernel:string ->
Sarek_ir_types.kernel ->
vector_arg list ->
unitval raise_capability_refusal :
backend:string ->
target:string ->
Sarek_capability.verdict ->
'aRender a non-permitting verdict and raise it as a Backend_error.
Shared by check_device_capabilities and check_interpreter_capabilities so the two launch gates cannot drift into describing the same refusal differently — which is a real hazard here, since backlog-154 was two gates disagreeing about the same kernel.
val check_device_capabilities :
device:Spoc_core.Device.t ->
Sarek_ir_types.kernel ->
unitRefuse a launch whose kernel needs a wide element type the target device does not provide (#142).
WHY THIS IS A LAUNCH GATE AND NOT A CODEGEN REFUSAL. These are Sarek_capability.kind.Device_optional capabilities: kind_needs_device Device_optional = true, and Framework_sig.generate_source takes no device, so codegen is structurally the wrong place to ask. The device is first in scope here, next to check_launch_args.
WHAT IT REPLACES. Nothing — that is the defect. Before #142 an int64 kernel on Vulkan reached vkCreateShaderModule with SPIR-V declaring OpCapability Int64 and no shaderInt64 enabled on the logical device. On an RX 7900 XTX (RADV, Mesa 26.1.4-arch3.1) that is not a crash and not a wrong answer: results are correct and the violation is visible only under VK_LAYER_KHRONOS_validation (VUID-VkShaderModuleCreateInfo-pCode-08740). Silent undefined behaviour on the driver that happens to cope is exactly the failure mode a capability model exists to convert into a diagnostic.
Routed through Sarek_capability.permits rather than a membership test so an Sarek_capability.verdict.Unknown verdict refuses instead of falling through to permitted.
val check_interpreter_capabilities : Sarek_ir_types.kernel -> unitThe launch gate for run_interpreter_vectors (backlog-154).
run_interpreter_vectors applied check_launch_args and NOT check_device_capabilities, so the interpreter — the cross-backend numeric oracle — was the one execution path with no capability gate on it. The asymmetry was observable: in one run of test_coopmat_integer_e2e on this workstation, check_device_capabilities printed launch gate refuses on CPU Interpreter (Sequential) for the same kernel run_interpreter_vectors then evaluated to 65536 bit-exact results.
Note what could NOT be done about that: calling check_device_capabilities here. It needs a Device.t, this entry point has none by design, and — the substantive half — the interpreter's Framework_sig.capabilities says coopmat = None, which means "not probed", which refuses. Wiring the device gate in would have taken the oracle offline rather than closed a hole. The bypass was load-bearing, which is why the fix is a capability answer for the interpreter and not a call to someone else's.
Sarek_interp_capability is that answer, and it lives beside the evaluator that justifies it. See its interface for the argument about what an interpreter should advertise.
val run :
device:Spoc_core.Device.t ->
name:string ->
ir:Sarek_ir_types.kernel Stdlib.Lazy.t option ->
native_fn:
(block:Spoc_framework.Framework_sig.dims ->
grid:Spoc_framework.Framework_sig.dims ->
Spoc_framework.Framework_sig.exec_arg array ->
unit)
option ->
block:Spoc_framework.Framework_sig.dims ->
grid:Spoc_framework.Framework_sig.dims ->
?shared_mem:int ->
vector_arg list ->
unitExecute a kernel on a device using the unified dispatch mechanism.
val mark_vectors_stale : vector_arg list -> Spoc_core.Device.t -> unitMark vectors as stale on CPU after kernel execution.
After a kernel modifies vector data on a device, we need to track that the CPU-side data is now stale. This ensures future CPU reads will trigger a device→CPU transfer.
Special cases:
val vector_to_interp_array :
'a 'b. ('a, 'b) Spoc_core.Vector.t ->
Sarek_interp.Sarek_ir_interp.value arrayConvert a V2 Vector to interpreter value array.
Converts vectors of primitive types (int32, float32, etc.) to the interpreter's runtime value representation. Custom types are converted using registered type helpers from Sarek_type_helpers.
This enables the interpreter backend to execute kernels on CPU without requiring GPU infrastructure.
val interp_array_to_vector :
'a 'b. Sarek_interp.Sarek_ir_interp.value array ->
('a, 'b) Spoc_core.Vector.t ->
unitCopy interpreter value array back to V2 Vector.
After interpreter execution, this function copies the runtime values back into the typed vector representation. Performs type checking and conversion for each element.
val run_interpreter_vectors :
ir:Sarek_ir_types.kernel ->
args:vector_arg list ->
block:Spoc_framework.Framework_sig.dims ->
grid:Spoc_framework.Framework_sig.dims ->
parallel:bool ->
unitRun kernel via interpreter with V2 Vectors. Note: Interpreter works with IR params directly - one arg per param. Vectors map to ArgArray (length is intrinsic to array).
val run_vectors :
device:Spoc_core.Device.t ->
ir:Sarek_ir_types.kernel ->
args:vector_arg list ->
block:Spoc_framework.Framework_sig.dims ->
grid:Spoc_framework.Framework_sig.dims ->
?shared_mem:int ->
unit ->
unitExecute a kernel with V2 Vectors. Auto-transfers, dispatches to backend.
This is the main execution entry point for Sarek-generated kernels. It performs the complete execution pipeline:
1. **Transfer**: Move vectors to device (no-op for CPU backends) 2. **Dispatch**: Call appropriate backend's execution method 3. **Mark stale**: Update vector location tracking
The function automatically handles differences between execution models:
val sync_vectors_to_cpu : vector_arg list -> unitSync all V2 Vector outputs back to CPU
val dims1d : int -> Spoc_framework.Framework_sig.dimsCreate 1D grid and block dimensions
val dims2d : int -> int -> Spoc_framework.Framework_sig.dimsCreate 2D grid and block dimensions
val dims3d : int -> int -> int -> Spoc_framework.Framework_sig.dimsCreate 3D grid and block dimensions
Calculate grid size for a given problem size and block size
val grid_for :
problem_size:int ->
block_size:int ->
Spoc_framework.Framework_sig.dimsCalculate 1D grid dimensions for a problem size
type source_lang = Spoc_framework.Framework_sig.source_lang = Re-export source language type
val supports_lang : Spoc_core.Device.t -> source_lang -> boolCheck if a device supports a given source language.
Different backends support different source languages:
val run_source :
device:Spoc_core.Device.t ->
source:string ->
lang:source_lang ->
kernel_name:string ->
block:Spoc_framework.Framework_sig.dims ->
grid:Spoc_framework.Framework_sig.dims ->
?shared_mem:int ->
?inject_lengths:bool ->
vector_arg list ->
unitExecute an external kernel from source code.
This function allows running pre-written GPU kernels (CUDA, OpenCL, PTX) directly without going through the Sarek DSL.
val detect_lang : string -> source_langDetect source language from file extension
val run_source_file :
device:Spoc_core.Device.t ->
path:string ->
kernel_name:string ->
block:Spoc_framework.Framework_sig.dims ->
grid:Spoc_framework.Framework_sig.dims ->
?shared_mem:int ->
?inject_lengths:bool ->
vector_arg list ->
unitExecute an external kernel from a file.
Loads pre-written GPU kernel source from a file and executes it. Useful for integrating hand-optimized kernels or using features not yet supported by the Sarek PPX.
Source language is auto-detected from file extension:
Example:
(* Execute hand-written CUDA kernel *)
Execute.run_source_file
~device:(Device.get_default ())
~path:"kernels/optimized_matmul.cu"
~kernel_name:"matmul_kernel"
~block:(16, 16, 1)
~grid:(64, 64, 1)
[Vec a; Vec b; Vec c; Int32 1024l]