[python] Split Python packages and add standalone AOT runtime - #9272
Open
derek-gerstmann wants to merge 39 commits into
Open
[python] Split Python packages and add standalone AOT runtime#9272derek-gerstmann wants to merge 39 commits into
derek-gerstmann wants to merge 39 commits into
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9272 +/- ##
==========================================
- Coverage 70.08% 70.01% -0.08%
==========================================
Files 260 260
Lines 79287 79229 -58
Branches 19327 19327
==========================================
- Hits 55569 55470 -99
- Misses 17923 17929 +6
- Partials 5795 5830 +35 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
alexreinking
force-pushed
the
dg/split-py-rt
branch
from
August 20, 2026 14:12
73adbea to
f199626
Compare
libHalide, the autoschedulers, and the generator tools were bundled into every per-Python-version wheel, so cibuildwheel rebuilt the entire LLVM-linked library from scratch once per CPython ABI per platform (~20 full builds today). Split the binary components into a new halide-bin wheel (py3-none-<platform>, built once per platform) that halide now depends on and links against via find_package(Halide), so halide's own per-version build is just the pybind11 extension. A plain `pip install .` is unaffected: the split only activates when HALIDE_SPLIT_BUILD=1 is set, which only CI does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…libHalide
Introduce `halide.runtime`, a small Python extension that can load and call
precompiled Halide AOT kernels without depending on libHalide (the compiler) or
LLVM. This lets you compile pipelines on a build machine with the full toolchain
and run them on deployment machines that have neither.
Shared marshalling core
-----------------------
The buffer-protocol <-> halide_buffer_t marshalling (`unpack_buffer` and
`PyHalideBuffer`) previously lived only as string literals inside
PythonExtensionGen.cpp. Lift it into a single source of truth,
src/PythonExtensionRuntime.template.cpp, embedded into libHalide via binary2cpp
(added to C_TEMPLATE_FILES in both src/CMakeLists.txt and the Makefile) and also
compiled directly into the runtime module. `unpack_buffer` is now `inline` so it
can be emitted into every generated .py.cpp (including the multi-library
OMIT_MODULE_DEFINITION case) without violating the ODR. PythonExtensionGen.cpp
shrinks by ~160 lines and its three copies of the conversion logic are unified.
The runtime module (python_bindings/src/halide/runtime/)
-------------------------------------------------------
* PyRuntime.cpp: a pybind11 extension linking only Halide::Runtime (headers) plus
a compiled runtime via add_halide_runtime -- never libHalide.
- `load(path, name=None)`: dlopen/LoadLibrary an artifact, dlsym its
`<name>_argv`/`<name>_metadata`, and return a callable `Kernel`.
- `Kernel`: `__call__` marshals buffer-protocol objects (NumPy) and scalars of
every type into the argv array driven by halide_filter_metadata_t; exposes
`name`, `target`, `argument_names`, and `arguments` (per-argument
name/kind/type/dimensions introspection).
- `Buffer`: wraps a buffer-protocol object as a halide_buffer_t, exposing the
duck-typed `_get_raw_halide_buffer_t` protocol (shared with halide.Buffer and
generated extensions) plus a zero-copy NumPy round-trip.
- Installs a non-aborting error handler both in its own runtime and, via
dlsym, in each loaded kernel's runtime, so a runtime error (e.g. a missing
GPU driver) raises a Python exception instead of aborting the interpreter.
Lazy compiler import
--------------------
Rewrite halide/__init__.py to defer loading the compiler extension (halide_) and
the generator helpers until a compiler attribute is first accessed (PEP 562
module __getattr__/__dir__). `import halide.runtime` therefore never pulls in
libHalide, even when the compiler is present. A runtime-only install raises a
clear ImportError, guiding users to the full `halide` package, when the compiler
is accessed.
Packaging
---------
* Install the runtime module (component Halide_PythonRuntime) and split the
Python-source install so that component is self-contained.
* Install PythonExtensionRuntime.template.cpp next to HalideRuntime.h so the
runtime module can be built out-of-tree (the CMake now finds the template
in-tree or via the installed Halide::Runtime include dirs).
* Add packaging/pip-runtime: a libHalide-free `halide-runtime` wheel that builds
only the runtime target and installs only its component (numpy dependency, no
halide-bin).
* Add a build-runtime-wheels job to .github/workflows/pip.yml (split-built
against halide-bin; a bare-environment `import halide.runtime` is itself the
no-libHalide check) and publish it alongside the existing wheels.
Tests (python_bindings/test/runtime/)
-------------------------------------
* load_aot.py: load a real generated kernel, call it, and exercise Buffer
interop, asserting the compiler was never imported.
* call_convention.py: drive a kernel entirely from `kernel.arguments` covering
every scalar type, a 2-D buffer, and a Tuple output; a second build with the
enum GeneratorParam `combine=xor` demonstrates that a compile-time
GeneratorParam changes behavior without changing the runtime calling
convention.
* gpu.py: Metal/OpenCL/CUDA/Vulkan coverage (CUDA gated on LLVM's NVPTX backend,
Metal on Apple), running the backends with a live device and skipping the rest.
* A CMake check asserting the runtime module has no libHalide dependency.
Docs and tutorial
-----------------
* doc/Python.md: a new "Calling AOT Code Without the Compiler (halide.runtime)"
section covering producing a loadable kernel, load()/Kernel/arguments, and the
Buffer type.
* python_bindings/tutorial/lesson_15_runtime.py: a self-contained lesson that
AOT-compiles a pipeline, links it into a loadable shared library (force_load on
macOS, --whole-archive on Linux, link.exe /DLL with a .def on Windows), and
then loads and runs it with only halide.runtime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
alexreinking
force-pushed
the
dg/split-py-rt
branch
from
August 21, 2026 19:06
1879225 to
b5f7e90
Compare
alexreinking
approved these changes
Aug 22, 2026
alexreinking
left a comment
Member
There was a problem hiding this comment.
This took some work, but with everything in place, we were able to pay off some longstanding tech debt.
Contributor
Author
|
@alexreinking Amazing! Thanks for all your help on this! |
Member
|
Looks like I broke Python stubs. Will fix. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rework Halide's Python distribution into three coordinated packages, add a standalone runtime API for calling precompiled AOT kernels without the compiler, and harden the shared Python binding/marshalling layer.
The full
pip install halideexperience remains intact: package managers install the matching binary and runtime distributions automatically. Deployment environments that only execute precompiled pipelines can installhalide-runtimeby itself and avoid libHalide and LLVM entirely.Package layout
halide-binhalide-runtimehalide.runtime, its lightweightBuffer, and the Halide runtime needed to invoke AOT kernelshalidehalide-binandhalide-runtimeversionsThis avoids rebuilding and bundling the LLVM-linked compiler once per CPython ABI. The repository root is now a uv workspace; source builds target the individual distribution directories rather than treating the root as the
halidedistribution. The Python bindings are organized as workspace packages with shared versioning and reproducible, packaged build dependencies.Standalone
halide.runtimehalide.runtimecan load and call a precompiled Halide AOT shared library without importing the compiler extension:The module provides:
load(path, name=None), accepting normal Python path-like objects and resolving<name>_argvand<name>_metadatafrom a shared library.Kernelobjects with metadata-driven scalar and buffer argument marshalling plus name, target, and argument introspection.halide.runtime.Bufferwith zero-copy Python buffer/NumPy views, shape transformations, element access, and device operations.halide.Buffer, runtimehalide.runtime.Buffer, and generated Python extensions through versioned typed capsules with explicit ownership.import halide.runtimenever loads libHalide; runtime-only installations produce a clear error if compiler APIs are requested.Shared marshalling and binding correctness
Move the generated-extension buffer marshalling code into
src/PythonExtensionRuntime.template.cpp, which is both embedded into generated Python extensions and reused by the standalone runtime module. This removes duplicated conversion logic and keeps generated extensions self-contained.The shared and compiler binding paths are also hardened to:
ValueErrors.std::filesystem::pathsupport instead of a custom raw-Python path caster.Build, packaging, and CI
Tests and documentation
Coverage includes:
Co-authored by @alexreinking
Checklist