Skip to content

Backends

This page is the source of truth

Tack has five backends and the details differ in ways that matter. This table is the canonical one — the README and the guides link here rather than restating it, because a capability table copied into three places drifts, and this one has.

The five

Backend Codegen Runtime compiler Dispatch Buffer
CPU llvm_gen.py → LLVM IR llvmlite JIT ctypes call, thread pool NumpyBuffer
Metal msl_gen.py → MSL Metal API (pyobjc) compute pipeline MetalBuffer
CUDA cuda_gen.py → CUDA C NVRTC → PTX cuLaunchKernel CUDABuffer
HIP hip_gen.py → HIP C hipRTC hipModuleLaunchKernel HIPBuffer
Level Zero opencl_gen.py → OpenCL C libocloc → SPIR-V zeCommandListAppendLaunchKernel L0Buffer

hip_gen.py is a few statements: it subclasses the CUDA generator and swaps the #include and the texture handle type (hipTextureObject_t), because HIP device code otherwise uses CUDA's syntax. opencl_gen.py also extends the CUDA generator, but overrides considerably more — OpenCL C spells address spaces, thread indices, barriers and math differently, and the wrong spelling still compiles as C.

Launches and the disk cache

A kernel call returns The disk cache stores
CPU when the kernel has finished object code
Metal once queued in a command buffer, committed every 64 launches MSL source
CUDA once queued on a stream PTX, compiled by the driver at load
HIP once queued on a stream the code object
Level Zero once queued on an asynchronous immediate command list the native binary, and the SPIR-V to fall back on

A queued launch is waited for where the host reads an array or calls tack.sync(), and its errors surface there. A launch that uses memory shared with another library (DLPack export, array_from_ptr) completes before the call returns. See Backends in the User's Guide and The Kernel Cache.

Debug mode (tack.set_debug(True) or TACK_DEBUG=1) checks every index on all five.

Scalar types

i8/u8 i16/u16 i32/u32 i64/u64 f32 f64
CPU ✅ ✅ ✅ ✅ ✅ ✅
Metal ✅ ✅ ✅ ✅ ✅ ❌
CUDA ✅ ✅ ✅ ✅ ✅ ✅
HIP ✅ ✅ ✅ ✅ ✅ ✅
Level Zero ✅ ✅ ✅ ✅ ✅ ⚠️

Metal has no f64 — Apple GPUs do not implement double precision, so this is a hardware fact rather than a gap in Tack.

Level Zero decides at runtime. Some Intel parts implement f64 and some do not, so the backend queries zeDeviceGetModuleProperties when the device starts and builds its supported set from the answer. Ask the backend rather than assuming:

from tack.runtime.dispatch import get_backend
tack.set_default_device("level_zero")
print(get_backend().supports_f64)

supports_f64 is derived from supported_dtypes rather than stored separately, so the two cannot disagree.

Workgroup execution

supports_workgroups is false on CPU and true on Metal, CUDA, HIP and Level Zero. Shared memory, barriers, local thread indices and block reductions require this capability. CPU rejects such kernels before compilation or execution; use local_array / local_array_like for private scratch storage instead.

Collectives require complete 256-lane groups and conservatively proven uniform participation. Smaller Metal pipeline/Level Zero device limits and partial collective grids are rejected. Shared/thread-ID kernels without collectives retain partial-grid support. The flag itself describes the execution model and does not certify participation or atomics. See the workgroup contract.

Installing

Tack is not on PyPI yet. In a clone of the repository, each backend is an extra of tack-core (uv sync --extra cuda does the same as the line below):

pip install -e 'packages/tack-core[cpu]'          # llvmlite
pip install -e 'packages/tack-core[metal]'        # pyobjc-framework-Metal
pip install -e 'packages/tack-core[cuda]'         # cuda-python; needs the NVIDIA driver
pip install -e 'packages/tack-core[hip]'          # hip-python; needs ROCm
pip install -e 'packages/tack-core[level_zero]'   # installs nothing — see below

Two of these need a word.

HIP. hip-python's version tracks the ROCm release it binds to — 7.1.x against ROCm 7.1, 7.2.x against 7.2 — so the extra sets a lower bound rather than a pin. A system on a different ROCm wants it spelled out:

pip install 'hip-python~=7.2.0'

It ships manylinux x86_64 wheels only, so the dependency carries a platform marker and is skipped elsewhere. hip-python 10, a compatibility package over rocm.bindings, works too: it is what the lower bound installs today, and Tack runs on it against ROCm 7.1.

Level Zero. The extra is deliberately empty. Level Zero is reached through ctypes to libze_loader.so (the Level Zero runtime) and libocloc.so (the Intel offline compiler, part of intel-opencl-icd) — system libraries, not Python packages. The extra exists so the name stays valid.

Asking a backend what it can do

Backends declare their capabilities rather than being probed with hasattr:

from tack.runtime.dispatch import get_backend

be = get_backend()
be.name                       # 'cpu', 'metal', 'cuda', 'hip', 'level_zero'
be.label                      # how to spell it in a message
be.supported_dtypes           # the frozenset dispatch checks array arguments against
be.supports_f64               # derived from the above
be.supports_device_reductions # whether .sum()/.min()/.max() run on device
be.device_memory_spaces       # what memory_space() must return for a pointer

See Backend for the full contract.