[python] Add a separate runtime module that doesn't require the compiler - #9272
Open
derek-gerstmann wants to merge 7 commits into
Open
[python] Add a separate runtime module that doesn't require the compiler#9272derek-gerstmann wants to merge 7 commits into
derek-gerstmann wants to merge 7 commits into
Conversation
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>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9272 +/- ##
==========================================
- Coverage 70.22% 70.19% -0.04%
==========================================
Files 257 257
Lines 79105 79046 -59
Branches 18954 18954
==========================================
- Hits 55552 55484 -68
- Misses 17888 17897 +9
Partials 5665 5665 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Running clang-tidy 21 (WarningsAsErrors: '*') over the new runtime code surfaced a handful of findings; fix them: * PyRuntime.cpp: use std::scoped_lock instead of std::lock_guard (modernize-use-scoped-lock); take the optional `name` argument to load() by const reference (performance-unnecessary-value-param); and mark the deliberate #include of the shared .cpp marshalling core NOLINT (bugprone-suspicious-include). * PythonExtensionRuntime.template.cpp: use nullptr instead of NULL (modernize-use-nullptr). This code is also emitted into generated Python extensions, so the improvement carries over there. * callconv_generator.cpp (test): switch on the enum value rather than the GeneratorParam wrapper so the switch is exhaustive (bugprone-switch-missing-default-case). The libHalide and python-bindings sources that the CI clang-tidy job checks (WITH_TESTS=OFF) are now clean. Verified the runtime, generated-extension, and call-convention tests still pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # src/CMakeLists.txt
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.
Add standalone halide.runtime module for calling AOT kernels without libHalide
Introduce
halide.runtime, a small Python extension that can load and callprecompiled 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_bufferandPyHalideBuffer) previously lived only as string literals insidePythonExtensionGen.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_bufferis nowinlineso itcan 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/)
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 callableKernel.Kernel:__call__marshals buffer-protocol objects (NumPy) and scalars ofevery type into the argv array driven by halide_filter_metadata_t; exposes
name,target,argument_names, andarguments(per-argumentname/kind/type/dimensions introspection).
Buffer: wraps a buffer-protocol object as a halide_buffer_t, exposing theduck-typed
_get_raw_halide_buffer_tprotocol (shared with halide.Buffer andgenerated extensions) plus a zero-copy NumPy round-trip.
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.runtimetherefore never pulls inlibHalide, even when the compiler is present. A runtime-only install raises a
clear ImportError, guiding users to the full
halidepackage, when the compileris accessed.
Packaging
Python-source install so that component is self-contained.
runtime module can be built out-of-tree (the CMake now finds the template
in-tree or via the installed Halide::Runtime include dirs).
halide-runtimewheel that buildsonly the runtime target and installs only its component (numpy dependency, no
halide-bin).
against halide-bin; a bare-environment
import halide.runtimeis itself theno-libHalide check) and publish it alongside the existing wheels.
Tests (python_bindings/test/runtime/)
interop, asserting the compiler was never imported.
kernel.argumentscoveringevery scalar type, a 2-D buffer, and a Tuple output; a second build with the
enum GeneratorParam
combine=xordemonstrates that a compile-timeGeneratorParam changes behavior without changing the runtime calling
convention.
Metal on Apple), running the backends with a live device and skipping the rest.
Docs and tutorial
section covering producing a loadable kernel, load()/Kernel/arguments, and the
Buffer type.
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
Checklist