Environment Variables

This page lists every SAREK_* and SPOC_* environment variable that the libraries and the PPX read, with the values each one accepts. Check here before relying on a value such as true or 0: the variables do not share one parsing rule, and a value a variable does not recognise is usually ignored without a message.

Test and benchmark programs read further variables of their own (for example SAREK_REQUIRE_PTX, SPOC_BENCH_*); they are not part of the libraries’ interface and are not listed.

Value conventions

Convention Variables
Only the exact string 1 has an effect SPOC_DISABLE_*, SAREK_EXPLAIN (any other value is off; an unknown one warns once)
Only the exact string 0 switches off SAREK_HIP_IGPU_UNCACHED, SAREK_DISK_CACHE
A comma-separated list of names, case-insensitive, spaces and empty items ignored; an unknown item warns and is ignored SAREK_OPT_DISABLE
1, true or yes (lowercase) switches on SAREK_PPX_DEBUG
A positive number (surrounding spaces ignored, except for SPOC_METAL_BATCH and SPOC_VULKAN_BATCH); an invalid value falls back to the default without a message SAREK_DOMAIN_COUNT, SAREK_CPU_POOL_MAX_DOMAINS, SAREK_CPU_POOL_IDLE_MS, SPOC_METAL_BATCH, SPOC_VULKAN_BATCH
A positive number; an invalid value is an error SPOC_VULKAN_FENCE_TIMEOUT_S
Set to anything, even empty SPOC_GPU_STATS

So SPOC_DISABLE_GPU=true does nothing.

“Read at” says when the value is taken: PPX is compile time (the environment of the dune build that expands [%kernel ...]), start is when the library or plugin module is loaded at program start, and launch or code generation is later, while the program runs.

Backends and devices

Variable Accepted values Default Read at Effect
SPOC_DISABLE_GPU 1 unset start, and at each plugin init () No GPU backend registers (CUDA, HIP, OpenCL, Vulkan, Metal). OpenCL CPU devices go with the OpenCL backend. Native and Interpreter stay.
SPOC_DISABLE_CUDA 1 unset start, and at each init () The CUDA backends (PTX and CUDA/C) do not register.
SPOC_DISABLE_HIP 1 unset start, and at each init () The HIP backend does not register.
SPOC_DISABLE_OPENCL 1 unset start, and at each init () The OpenCL backend does not register.
SPOC_DISABLE_VULKAN 1 unset start, and at each init () The Vulkan backend does not register.
SPOC_DISABLE_METAL 1 unset start, and at each init () The Metal backend does not register.
SAREK_CUDA_INCLUDE :-separated directories (empty is ignored) unset first CUDA/C kernel compile Extra include directories for NVRTC, tried before CUDA_PATH, CUDA_HOME, CUDA_ROOT and the conventional install roots; only directories that exist are kept. Needed for kernels that include cuda_fp16.h.
SAREK_HIP_IGPU_UNCACHED 0 unset (uncached allocation on integrated GPUs) start 0 makes HIP allocate device buffers on integrated GPUs with plain hipMalloc again. The only warning is a warn-level log line (stdout, once, at the first integrated-GPU allocation) printed only when SAREK_DEBUG enables the device component; otherwise nothing signals the override. Unsafe: on the gfx1036 iGPU, kernel writes into such memory can be lost across host idle gaps and results are silently wrong (issue #427). It exists to reproduce that loss. Discrete GPUs always use hipMalloc.
SPOC_METAL_BATCH integer >= 1 32 start Most dispatches one Metal command buffer collects before it is committed while the GPU is still busy.
SPOC_VULKAN_BATCH integer >= 1 32 start Most dispatches one Vulkan command buffer collects before it is submitted. 1 submits and waits for every launch (the fallback; a failed launch then raises at the launch instead of at the first wait). Batching is the default again on every driver, AMD included; set SPOC_VULKAN_BATCH=1 if you suspect it: test_vulkan_async once produced GPU write page faults and ring resets on RADV (Sarek #504), the cause is unconfirmed and the issue stays open.
SPOC_VULKAN_HOST_IMPORT 1 unset (off) start Allows Vulkan register_host (import of host memory, VK_EXT_external_memory_host); alloc_pinned does not import and needs no opt-in. Refused on AMD drivers even when set: importing host pages was followed by a kernel oops in amdgpu_hmm_invalidate_gfx (Sarek #503).
SPOC_VULKAN_HOST_IMPORT_AMDGPU_I_ACCEPT_KERNEL_OOPS 1 unset start With SPOC_VULKAN_HOST_IMPORT=1, allows the import on AMD drivers anyway. For disposable sessions only.
SPOC_VULKAN_FENCE_TIMEOUT_S positive number of seconds unset (no bound) start; a malformed value is refused when a Vulkan device is created Bounds how long Vulkan waits for submitted work; meant as a hang detector in tests.

Device selection is explained in Device and backend selection; the Metal batch in Memory and transfers; the Vulkan batch in Launch window and fusion; the fence bound in the Vulkan README.

Optimisation policy: restrict and explain

SPOC applies some optimisations on its own (the launch window, epilogue fusion, the matrix-unit rewrite, the exact-narrowing proof, loop unrolling), and some only after the program has accepted a deviation from the interpreter’s result (a grant, see Matrix units and Launch window and fusion). The environment can only restrict: no variable grants precision, turns an optimisation on that the program or the backend does not allow, or accepts a deviation. Both variables are read once, when the program starts, and a restriction is one-way for the process.

Variable Accepted values Default Read at Effect
SAREK_OPT_DISABLE comma-separated items, below unset start Takes the named optimisations away, and revokes the named grants, for the whole process.
SAREK_EXPLAIN 1 unset (off) start Writes one line on stderr for every distinct decision of every optimisation. Any other value is off; a value other than empty or 0 warns once. A reporter installed with Sarek_policy.set_reporter replaces the stderr one.

SAREK_OPT_DISABLE

Items are separated by commas, compared case-insensitively, and surrounding spaces and empty items are ignored (Window, ,Unroll works). An unknown item prints one line on stderr naming the item and the valid items, and is ignored; the other items apply.

Item Effect
window No deferred launches and no horizontal fusion, on every backend. Execute.Window.set_enabled stays for programs that need the window off in part of their run.
epilogue No epilogue fusion, on every backend.
matrix No matrix-unit rewrite, for Execute.run_vectors and Sarek_matrix.run. A kernel the program wrote with Sarek_gemm.Coopmat is not a rewrite and still runs.
narrowing-proof The f32 to f16 narrowing keeps its barrier everywhere, even where one rounding is provably equal to two.
unroll No automatic #pragma unroll hint on small constant-trip loops.
accumulation-order, operand-rounding-f16, operand-rounding-bf16, operand-rounding-bf16x3, single-rounding, cross-kernel-contraction, automatic-float-rewrite Revokes that grant for the whole process, whatever scope the program grants it at. The program’s call still succeeds and is recorded, but no decision sees the grant. SAREK_EXPLAIN prints grant-revoked for it.

There is no all. Other variables that used to switch these off are gone (see CHANGES.md): SAREK_LAUNCH_WINDOW, SAREK_LAUNCH_EPILOGUE, SAREK_MATRIX_REWRITE, SAREK_AUTO_UNROLL and SAREK_MATRIX_REPORT are no longer read, and setting them does nothing.

The generated source and the launch memo follow the policy: a restriction cannot make an earlier check stale.

SAREK_EXPLAIN

One line per distinct (kernel, target, optimisation, decision): a decision that comes back after another one is not printed again (a window switched off and on per prompt chunk prints two lines, not one per switch). The memory of what was printed is bounded (4096 distinct decisions, then forgotten). Fields are separated by single spaces; spaces inside a kernel name are written _, and an epilogue pair p + c is written p+c.

The target says where the decision was taken, in one scheme:

Target Used by Example
<framework>:<device id> decisions about a device: window, matrix, coopmat-gate HIP:0, OpenCL:1
<framework> decisions about a backend: epilogue HIP, CUDA/PTX
gen:<generator> code generation, which does not know the device: narrowing-proof, unroll gen:cuda-c (HIP and CUDA/C), gen:opencl, gen:metal, gen:glsl
sarek-explain <kernel> <target> <optimisation> applied proof=<text>
sarek-explain <kernel> <target> <optimisation> applied granted=<grant>[,<grant>] reason="<reason>" [lowering="<description>"]
sarek-explain <kernel> <target> <optimisation> applied exact
sarek-explain <kernel> <target> <optimisation> refused <reason>
sarek-explain <kernel> <target> coopmat-gate applied|refused ...
sarek-explain <kernel> - policy refused grant-on-generated-name <grant>
sarek-explain <kernel> - policy refused grant-revoked <grant>

<optimisation> is window, epilogue, matrix, narrowing-proof or unroll; <grant> is one of the grant names above. reason="..." holds only what the program gave when it granted (each reason once, in order, joined with ; ). lowering="..." is the matrix rewrite’s own description (lowering, variant, fragment configuration), present on the float rewrites. coopmat-gate is an event kind of its own, not an optimisation: it reports the accuracy gate of the device backends for a kernel the program wrote with Sarek_gemm.Coopmat (the interpreter’s gate is not reported), SAREK_OPT_DISABLE has no name for it, and nothing restricts it. The last two lines say that a grant the program made has no effect: a Kernel grant on a generated name (a fused, derived or static-fusion kernel) is never counted, and a revoked grant is not obeyed; each is printed once, at the next report for that kernel, also when its decisions settled before. A reporter set with Sarek_policy.set_reporter is called outside the policy’s lock, possibly from several domains: lines of different domains may interleave. Sample lines from a run on an AMD RX 7900 XTX (HIP) are in CHANGES.md.

Revoking an accuracy grant has a visible effect. accumulation-order is the closest equivalent of the old SAREK_MATRIX_REWRITE=exact for float paths: it refuses every float matrix-unit path (the rewrite and Sarek_gemm.Coopmat) and keeps int32. An explicit cooperative-matrix kernel the program wrote then fails at launch with a capability refusal that says accumulation-order is revoked by SAREK_OPT_DISABLE. Revoking operand-rounding-bf16 or -bf16x3 does the same for bfloat16 kernels. Revoking one operand rounding can make the rewrite pick another precision the program also granted (for example float16 instead of bfloat16), which is less accurate; revoke all of them to rule that out.

Launch window and fusion

The launch window and epilogue fusion have no variable of their own. SAREK_OPT_DISABLE=window and =epilogue take them away; they are explained in Launch window and fusion.

Matrix units

Variable Accepted values Default Read at Effect
SAREK_MATRIX_SCAN a file path unset PPX Appends one line per analysed kernel and finding to the file.

The rewrite is restricted with SAREK_OPT_DISABLE=matrix or =automatic-float-rewrite and reported with SAREK_EXPLAIN=1. See Matrix units.

Code generation

SAREK_OPT_DISABLE=unroll switches off the automatic unroll hint on small constant-trip for loops in the CUDA, HIP, OpenCL, Metal and GLSL generators. See Kernel language notes.

CPU backends

Variable Accepted values Default Read at Effect
SAREK_DOMAIN_COUNT a positive integer Domain.recommended_domain_count () (4 if that fails) each time a CPU pool is created, and at each barrier-free Native launch (work split) Number of OCaml domains used by the Native runtime and the Interpreter, before the SAREK_CPU_POOL_MAX_DOMAINS cap. The Native parallel pool uses the value as its worker count; without it, it uses one fewer than the domain count (at least 1). A value that is not a positive integer is ignored. Meant for tests that need more domains than cores.
SAREK_CPU_POOL_MAX_DOMAINS a positive integer unset (no cap) each time a CPU pool is created, and at each barrier-free Native launch (work split) Upper bound on the workers of each CPU pool (the Interpreter and Native block pool, the Native thread and parallel pools), so a program can keep cores for itself. Cpu_pool_lifecycle.set_max_domains takes precedence. A value that is not a positive integer is ignored.
SAREK_CPU_POOL_IDLE_MS a positive integer (milliseconds) 1000 at each poll of the pool reaper, while a pool is live A CPU pool (or a fission-queue launch dispatcher) that no launch has used for this long is retired: its worker domains exit, and the next launch creates it again. Idle domains still take part in every minor collection, so retiring them keeps host allocation fast. Cpu_pool_lifecycle.set_idle_timeout takes precedence. A value that is not a positive integer is ignored.

Compile cache

Vulkan (SPIR-V) and OpenCL (program binaries) keep compiled kernels on disk so that a new process does not compile them again. The files are in the spoc directory under $XDG_CACHE_HOME, or under $HOME/.cache when that is unset; the parent directory must already exist. The cache is used only when that directory is owned by the current user and not writable by group or others (a directory Sarek creates is mode 0700), and never from the shared temporary directory: without XDG_CACHE_HOME and HOME it is off. OpenCL entries are native device binaries that the driver loads, so a directory others can write to would let them substitute code. A refused directory prints one warning and the program compiles as if the cache were off.

Variable Accepted values Default Read at Effect
SAREK_DISK_CACHE 0 turns it off unset (on) each cache lookup and store With 0, no disk cache is read or written: Vulkan compiles GLSL again and OpenCL builds from source in every process. Nothing else changes. The in-process kernel caches are not affected. Metal has no disk cache of ours (not needed: OS shader cache, measured, see Performance).
XDG_CACHE_HOME a directory $HOME/.cache each cache lookup and store Where the spoc cache directory lives. Not a Sarek variable; set it to move or isolate the cache (the tests point it at a temporary directory).

An OpenCL entry is keyed by the platform name and version, the device name and version, the driver version, the exact build-option string and the source, so a driver update or a different option set compiles again and writes a new entry. A cached binary that the driver refuses, or that fails to build, is ignored: the kernel is compiled from source and the entry is overwritten. No error reaches the program. A driver that returns no binary for a program (size 0) is never cached. An entry is a checksum followed by the binary; a truncated or damaged file fails the checksum and is treated the same way. Entries are never evicted; delete the spoc directory to clear them. Set SAREK_DEBUG=device to see each lookup ([Cache] lines).

The key cannot see a driver rebuilt from new sources under the same version string: OpenCL uses CL_DRIVER_VERSION, so entries made by the old build are reused. Vulkan keys on the device name and the Vulkan API version, not on the shaderc version, so a new shaderc leaves old SPIR-V in use. After rebuilding or replacing such a component, delete the spoc cache directory or run once with SAREK_DISK_CACHE=0.

Diagnostics

Variable Accepted values Default Read at Effect
SAREK_DEBUG a comma-separated list; components transfer, kernel, device, memory, execute, all or *; levels debug, info, warn, error (case-insensitive, unknown items ignored) unset (no logging) start Runtime logging, per component.
SAREK_DEBUG (framework logs) the whole value is exactly all, or one comma-separated item is exactly device (no trimming, lowercase) unset start Also enables the [Cache] and [Framework] messages of the backend cache and registry.
SAREK_PPX_DEBUG 1, true, yes unset PPX Prints the PPX’s SAREK DEBUG: trace on stderr.
SAREK_DEBUG (PPX) 1, true, yes unset PPX The same trace, when SAREK_PPX_DEBUG is not set to one of those values.
SPOC_GPU_STATS any value unset start At exit, prints device-memory statistics (current and peak bytes, allocations, frees, GC-triggered frees) on stderr.

SAREK_DEBUG is read in three places with three meanings; the rows above are the three. The runtime list is described in the sarek.core README.