Architecture Overview¶
Compilation Pipeline¶
When a @tack.kernel is called, every backend's execute() goes through
resolve_variant() in runtime/kernel_utils.py, which runs this pipeline:
@tack.kernel Python function
Once per IR template (vector widths, texture extents, template structure,
debug mode), unless the disk cache has it: [disk_cache.py]
→ Template rewrite (if @tack.device_class args) [template_rewrite.py]
→ Source validation (reject unsupported syntax) [source_validation.py]
→ AST transform → Tack IR, @tack.func inlining [ast_transform.py, lowering/, call_bindings.py]
(debug mode: bounds checks on every index) [ir_bounds.py]
→ Verify 'lowered', cache the template on the Kernel [ir_verify.py, kernel.py]
Once per compiled variant (cache miss), on a clone_ir copy of the template,
unless the disk cache has it:
→ IR resolve (dimension sizes, texture shapes) [ir_resolve.py]
→ Type inference (from actual arguments) [type_inference.py]
→ Dispatch type check (backend dtypes) [type_inference.py]
→ Scalar localization (params and outer locals) [kernel_utils.py]
→ Atomic / workgroup checks [atomic_support.py, workgroup_*.py]
→ IR optimize (conservative copy propagation) [ir_optimize.py]
→ Scalar packing (GPU backends only, on a copy) [ir_pack_scalars.py]
→ IR type annotate (resolved types for codegen) [ir_type_annotate.py]
(debug mode: guard checked accesses) [ir_bounds.py]
→ Backend-specific codegen + native compile, store on disk
Every call:
→ Read the call's signature; a known one reuses its variant (fast path)
→ Otherwise derive the variant key, look it up
→ Alignment, read-only and launch-size checks [kernel_utils.py]
→ Bind texture storage and arguments, launch (queued on GPU backends)
→ Debug mode: raise IndexError for an out-of-bounds access [debug_bounds.py]
verify_ir() checks the IR after each of these stages. The IR template
cached on the Kernel is never mutated; each new variant clones it and the
passes mutate the clone. Compiled variants are cached per backend and per
kernel object, under a key built from argument dtypes and array/scalar/texture
categories, vector widths, texture extents, template structure and constants,
the dimension sizes baked into the code, and (on the CPU) whether the array
arguments overlap. A repeat call with the same key runs no passes and no
verification; a repeat call with the same argument signature does not even
build the key. Lowered templates and compiled variants are also kept on
disk, keyed by content, so a new process can skip both. See Compilation Pipeline
and Specialization and Caching
for the full account.
Package Structure¶
Tack is split into two packages in a monorepo:
packages/
tack-core/ # Core compute framework
src/tack/
__init__.py # Public API + pkgutil.extend_path
lang/ # Frontend: AST → IR
kernel.py # @tack.kernel decorator, Kernel class, IR template cache
func.py # @tack.func decorator, Func class
device_class.py # @tack.device_class decorator
source_validation.py # Reject unsupported syntax before lowering
call_bindings.py # Static resolution of device-function calls
ast_transform.py # Python AST → Tack IR
lowering/ # KernelTransformer's parts: loops, arrays, vectors, inlining, ...
template_rewrite.py # AST pre-pass for template parameters
ir.py # IR node definitions (30+ node types)
ir_traversal.py # walk_ir, transform_ir, clone_ir
ir_names.py # Binding names, fresh_name
ir_verify.py # verify_ir: invariants at each pass boundary
ir_bounds.py # Debug mode's bounds checks: sites, access guards
ir_resolve.py # Replace IRDimSize with constants
ir_optimize.py # conservative copy propagation
ir_type_annotate.py # Annotate expressions with dtype
ir_pack_scalars.py # Group scalar params into array buffers
type_inference.py # Annotate IR params from actual args
atomic_support.py # Atomic target and dtype checks
workgroup_support.py, workgroup_participation.py # Workgroup checks
inspect_kernel.py # tack.inspect()
types.py # ScalarType: f32, i32, i64, etc.
array.py # Array, Texture3D
dlpack.py # DLPack import/export
codegen/ # IR → target code
identifiers.py # Collision-free target identifiers
llvm_gen.py # → LLVM IR (for CPU backend)
msl_gen.py # → Metal Shading Language
cuda_gen.py # → CUDA C
hip_gen.py # → HIP C (extends cuda_gen)
opencl_gen.py # → OpenCL C (extends cuda_gen)
bounds.py # Debug mode's bounds-check helper (C family)
runtime/ # Dispatch and device management
backend.py # Backend base class: declared capabilities
kernel_utils.py # resolve_variant(), the fast path, variant keys, kernel caches
disk_cache.py # Lowered IR and compiled variants on disk (tack.cache)
debug_bounds.py # Debug mode's hidden arguments and IndexError
dispatch.py # devices, the default device, launch placement, tack.sync()
cpu.py # CPU backend (llvmlite JIT, threading policy)
cpu_pool.py # CPU thread pool, JIT-compiled from LLVM IR
metal.py # Metal backend (pyobjc)
cuda_backend.py # CUDA backend (cuda-python)
hip_backend.py # HIP backend (hip-python)
level_zero_backend.py # Level Zero backend (ctypes)
cache.py # tack.cache: settings, MPI bundles, python -m tack.cache
algorithms/ # General-purpose parallel primitives
scan.py # Parallel prefix sum (exclusive/inclusive)
copy.py # copy, fill_value
stats.py # Statistics and histograms
tack-vis/ # Scientific visualization algorithms
src/tack/
dataset/ # Datasets, spaces, views and their algorithms
algorithms/ # contour, clip, threshold, gradients, ... (one per file)
interop/ # External framework interop
vtk.py # VTK ↔ Tack zero-copy exchange
Both packages share the tack namespace via pkgutil.extend_path;
tack-vis depends on tack-core. The path tracer that was tack-rendering
is archived under archive/tack-rendering until the dataset API settles.
Code Size¶
tack-core is about 25,100 lines of Python, including docstrings and
comments (wc -l over packages/tack-core/src, October 2026);
tack-vis adds about 12,400:
| Directory | Lines | Role |
|---|---|---|
lang/ |
~8,800 | Frontend: validation, AST transform, IR, passes, verification |
codegen/ |
~4,500 | 5 code generators and shared helpers |
runtime/ |
~10,200 | 5 backend runtimes, variant resolution, disk cache, CPU threading and pool |
No C/C++ code. No build step. Everything is pure Python with JIT compilation at runtime.
Key Design Decisions¶
The decisions below are the structural ones. The principles behind them, and the correctness rules the compiler follows, are in Design Principles.
Pure Python throughout. Unlike Taichi (which has a C++ core with pybind11 bindings), Tack implements the entire pipeline in Python. This makes the codebase easy to read, modify, and debug. Kernels run as native code generated by LLVM or the vendor compiler, not through Python; the Python cost is in compilation, paid once per compiled variant, and in per-call dispatch overhead.
Separate IR from codegen. The IR is a simple tree of Python objects (not strings, not LLVM objects). Each codegen backend walks the same IR and emits its own output format. This makes adding a new backend straightforward.
AST-level function inlining. @tack.func calls are inlined by
copy-and-rename at the AST level: during IR transformation, each call site
lowers a renamed copy of the callee's validated AST. This avoids needing
function call support in the IR or any backend.
Template expansion at AST level. @tack.device_class objects are
resolved by rewriting the kernel AST before IR transformation. Class-level
scalar attributes become constants, instance scalar attributes and array
attributes become parameters, and method calls become inlined function
bodies. The IR and codegens never see
templates.
Non-owning pointer interop. tack.array_from_ptr() wraps external
device memory without allocation or copy. Each backend implements
wrap_ptr() so that the wrapped memory is never freed by Tack (the
CUDA, HIP and Level Zero buffers carry an _owned = False flag).
Such arrays are read-only by default, with an explicit writable
opt-in. The flag is checked by host-side Array methods and, for
kernels, at dispatch: a read-only array bound to a parameter the kernel
may store to is refused before the launch. This enables interop with in-situ frameworks
(Catalyst), GPU libraries (pycuda, cupy), and cross-library buffer
sharing. See Interoperability.