Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 193 additions & 8 deletions .github/workflows/pip.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ permissions:
contents: read # to fetch code (actions/checkout)

jobs:
build-wheels:
name: Build Halide wheels for ${{ matrix.platform_tag }}
build-halide-bin:
name: Build halide-bin for ${{ matrix.platform_tag }}

runs-on: ${{ matrix.os }}
strategy:
Expand Down Expand Up @@ -64,7 +64,10 @@ jobs:
echo "Halide_LLVM_ROOT=$(halide-llvm --prefix)" >> "$GITHUB_ENV"

########################################################################
# Wheels
# Wheel (no CPython ABI to matrix over -- halide-bin is a plain
# py3-none-<platform> wheel, so we only ever need to invoke
# cibuildwheel against a single, arbitrary interpreter to drive the
# underlying CMake build once per platform).
########################################################################

#- uses: mxschmitt/action-tmate@v3
Expand Down Expand Up @@ -130,17 +133,114 @@ jobs:
CIBW_ENVIRONMENT_LINUX: >
Halide_LLVM_ROOT=/project/opt/llvm
CMAKE_PREFIX_PATH=/project/opt
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE='{local_scheme="no-local-version"}'
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE_BIN='{local_scheme="no-local-version"}'
CIBW_ENVIRONMENT_MACOS: >
CMAKE_PREFIX_PATH='${{ github.workspace }}/opt'
Python_ROOT_DIR=''
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE='{local_scheme="no-local-version"}'
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE_BIN='{local_scheme="no-local-version"}'
CIBW_ENVIRONMENT_WINDOWS: >
CMAKE_GENERATOR=Ninja
CMAKE_PREFIX_PATH='${{ github.workspace }}\opt'
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE='{local_scheme="no-local-version"}'
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE_BIN='{local_scheme="no-local-version"}'
CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: delvewheel repair --ignore-existing -w {dest_dir} {wheel}
CIBW_BEFORE_TEST_LINUX: pip install cmake ninja

- uses: actions/upload-artifact@v7
with:
name: wheels-bin-${{ matrix.platform_tag }}
path: ./wheelhouse/*.whl

build-wheels:
name: Build Halide wheels for ${{ matrix.platform_tag }}
needs: build-halide-bin

runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform_tag: manylinux_x86_64
- os: windows-latest
platform_tag: win_amd64
- os: macos-15-intel
platform_tag: macosx_x86_64
- os: macos-15
platform_tag: macosx_arm64

env:
MACOSX_DEPLOYMENT_TARGET: 11
HALIDE_SPLIT_BUILD: "1"

steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true

- uses: ilammy/msvc-dev-cmd@v1
- uses: lukka/get-cmake@v4.4.0
with:
cmakeVersion: "~3.28.0"

########################################################################
# halide-bin
#
# Building the Python bindings no longer needs LLVM/FlatBuffers/WABT at
# all -- it links against an already-installed halide-bin via
# find_package(Halide). Fetch the wheel this platform's
# build-halide-bin job just produced, and unpack a copy of it so
# CMAKE_PREFIX_PATH can point straight at a real directory (as opposed
# to a still-zipped wheel).
########################################################################

- uses: actions/download-artifact@v8
with:
name: wheels-bin-${{ matrix.platform_tag }}
path: dist-bin

- name: Unpack halide-bin for the build
shell: bash
run: python -m zipfile -e dist-bin/*.whl opt/halide-bin

########################################################################
# Wheels
########################################################################

#- uses: mxschmitt/action-tmate@v3

- name: Build wheels
uses: pypa/cibuildwheel@v4.1.1
env:
CIBW_BUILD: "cp3*-${{ matrix.platform_tag }}"
CIBW_SKIP: "cp3{5,6,7,8,9}* cp314t-*"
CIBW_ENVIRONMENT_LINUX: >
CMAKE_PREFIX_PATH=/project/opt/halide-bin/halide_bin/data
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE='{local_scheme="no-local-version"}'
CIBW_ENVIRONMENT_MACOS: >
CMAKE_PREFIX_PATH='${{ github.workspace }}/opt/halide-bin/halide_bin/data'
Python_ROOT_DIR=''
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE='{local_scheme="no-local-version"}'
CIBW_ENVIRONMENT_WINDOWS: >
CMAKE_GENERATOR=Ninja
CMAKE_PREFIX_PATH='${{ github.workspace }}\opt\halide-bin\halide_bin\data'
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE='{local_scheme="no-local-version"}'
# halide_.pyd/.so now depends on a Halide.dll/libHalide supplied by the
# already-installed halide-bin package, not one bundled in this wheel --
# `--exclude` stops delvewheel from vendoring a second copy of it, while
# `--add-path` lets its dependency scan actually resolve it.
CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: >
delvewheel repair --ignore-existing --exclude Halide.dll
--add-path '${{ github.workspace }}\opt\halide-bin\halide_bin\data\bin'
-w {dest_dir} {wheel}
# halide-bin isn't published yet during this CI run, so resolve it
# from the wheel we just downloaded instead of the package index.
# Linux additionally needs cmake/ninja installed into the (otherwise
# bare) per-wheel manylinux test container -- macOS/Windows already
# have them on PATH via the get-cmake action above.
CIBW_BEFORE_TEST_LINUX: >
pip install cmake ninja &&
pip install --no-index --find-links {project}/dist-bin halide-bin
CIBW_BEFORE_TEST: pip install --no-index --find-links {project}/dist-bin halide-bin
CIBW_TEST_COMMAND: >
cmake -G Ninja -S {project}/python_bindings/apps -B build -DCMAKE_BUILD_TYPE=Release &&
cmake --build build &&
Expand All @@ -156,9 +256,94 @@ jobs:
name: wheels-${{ matrix.platform_tag }}
path: ./wheelhouse/*.whl

build-runtime-wheels:
name: Build halide-runtime wheels for ${{ matrix.platform_tag }}
needs: build-halide-bin

runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform_tag: manylinux_x86_64
- os: windows-latest
platform_tag: win_amd64
- os: macos-15-intel
platform_tag: macosx_x86_64
- os: macos-15
platform_tag: macosx_arm64

env:
MACOSX_DEPLOYMENT_TARGET: 11
HALIDE_SPLIT_BUILD: "1"

steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
fetch-tags: true

- uses: ilammy/msvc-dev-cmd@v1
- uses: lukka/get-cmake@v4.4.0
with:
cmakeVersion: "~3.28.0"

########################################################################
# halide-bin
#
# The runtime module is compiled against an already-installed halide-bin
# (for the runtime headers, the shared marshalling source, and GenRT to
# generate its bundled Halide runtime), exactly like build-wheels above.
# Unlike that job, the resulting extension links no libHalide and the wheel
# has no halide-bin dependency.
########################################################################

- uses: actions/download-artifact@v8
with:
name: wheels-bin-${{ matrix.platform_tag }}
path: dist-bin

- name: Unpack halide-bin for the build
shell: bash
run: python -m zipfile -e dist-bin/*.whl opt/halide-bin

########################################################################
# Wheels
########################################################################

- name: Build wheels
uses: pypa/cibuildwheel@v4.1.1
with:
package-dir: packaging/pip-runtime
env:
CIBW_BUILD: "cp3*-${{ matrix.platform_tag }}"
CIBW_SKIP: "cp3{5,6,7,8,9}* cp314t-*"
CIBW_ENVIRONMENT_LINUX: >
CMAKE_PREFIX_PATH=/project/opt/halide-bin/halide_bin/data
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE_RUNTIME='{local_scheme="no-local-version"}'
CIBW_ENVIRONMENT_MACOS: >
CMAKE_PREFIX_PATH='${{ github.workspace }}/opt/halide-bin/halide_bin/data'
Python_ROOT_DIR=''
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE_RUNTIME='{local_scheme="no-local-version"}'
CIBW_ENVIRONMENT_WINDOWS: >
CMAKE_GENERATOR=Ninja
CMAKE_PREFIX_PATH='${{ github.workspace }}\opt\halide-bin\halide_bin\data'
SETUPTOOLS_SCM_OVERRIDES_FOR_HALIDE_RUNTIME='{local_scheme="no-local-version"}'
# The runtime wheel links no libHalide, so its test environment needs
# nothing installed: a successful `import halide.runtime` in a bare
# environment is itself proof that the module is libHalide-free.
CIBW_TEST_COMMAND: >
python -c "import halide.runtime as r; assert r.load and r.Kernel and r.Buffer; print('halide.runtime OK')"

- uses: actions/upload-artifact@v7
with:
name: wheels-runtime-${{ matrix.platform_tag }}
path: ./wheelhouse/*.whl

publish:
name: Publish on PyPI
needs: build-wheels
needs: [build-halide-bin, build-wheels, build-runtime-wheels]
runs-on: ubuntu-latest
permissions:
id-token: write
Expand Down
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,8 @@ SOURCE_FILES = \

C_TEMPLATE_FILES = \
CodeGen_C_prologue \
CodeGen_C_vectors
CodeGen_C_vectors \
PythonExtensionRuntime

HTML_TEMPLATE_FILES = \
StmtToHTML_dependencies.html \
Expand Down
126 changes: 126 additions & 0 deletions doc/Python.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
- [Using a Generator for JIT compilation](#using-a-generator-for-jit-compilation)
- [Using a Generator for AOT compilation](#using-a-generator-for-aot-compilation)
- [Calling Generator-Produced code from Python](#calling-generator-produced-code-from-python)
- [Calling AOT Code Without the Compiler (`halide.runtime`)](#calling-aot-code-without-the-compiler-halideruntime)
- [Producing a loadable kernel](#producing-a-loadable-kernel)
- [Loading and calling a kernel](#loading-and-calling-a-kernel)
- [The `halide.runtime.Buffer` type](#the-halideruntimebuffer-type)
- [Advanced Generator-Related Topics](#advanced-generator-related-topics)
- [Generator Aliases](#generator-aliases)
- [Dynamic Inputs and Outputs](#dynamic-inputs-and-outputs)
Expand Down Expand Up @@ -621,6 +625,128 @@ pass `order='F'` to make numpy use the Halide-compatible memory layout. If
you're passing in an array constructed somewhere else, the easiest thing to do
is to `.transpose()` it before passing it to your Halide code.

### Calling AOT Code Without the Compiler (`halide.runtime`)

The approach above imports a Python extension that was produced at build time by
`add_halide_python_extension_library`. Sometimes you instead want to load a
precompiled Halide kernel _dynamically_, at runtime, from an ordinary shared
library -- and to do so in an environment that does not have the Halide compiler
(or `libHalide`) installed at all. This is what the `halide.runtime` module is
for: it is a small, standalone package that can load and call AOT-compiled
Halide kernels without depending on `libHalide`.

This is primarily useful for deployment. You can compile your pipelines on a
build machine that has the full Halide toolchain, then ship only the resulting
kernels plus this tiny runtime, and run them on machines that have neither the
compiler nor LLVM installed.

`halide.runtime` is always included in the full `halide` package, but it is also
published as a separate, `libHalide`-free wheel for exactly this deployment
case:

```shell
pip install halide-runtime
```

Importing `halide.runtime` never loads `libHalide`; in the full package, the
compiler is loaded only if and when you first access a compiler symbol such as
`hl.Func`. (In a runtime-only install there is no compiler at all, so accessing
one raises an `ImportError` explaining that only the runtime is present and
pointing you at the full `halide` package.)

#### Producing a loadable kernel

`halide.runtime` loads a shared library that exports a Halide filter's
`<name>_argv` and `<name>_metadata` symbols -- the ordinary product of AOT
compilation. Note that this is _not_ the same artifact as the Python extension
produced by `add_halide_python_extension_library`, which deliberately hides
every symbol except its `PyInit_` entry point. Instead, link the AOT library
into a plain shared module that keeps those symbols visible:

```cmake
add_halide_library(my_kernel FROM my_generator GENERATOR my_kernel)

# Wrap the static AOT library in a shared module, keeping the filter's
# _argv/_metadata symbols exported so they can be resolved at load time.
# (A MODULE library needs at least one source of its own; an empty stub is fine.)
add_library(my_kernel_module MODULE stub.c)
target_link_libraries(my_kernel_module PRIVATE
"$<LINK_LIBRARY:WHOLE_ARCHIVE,my_kernel>")
set_target_properties(my_kernel_module PROPERTIES
PREFIX "" OUTPUT_NAME my_kernel
C_VISIBILITY_PRESET default
CXX_VISIBILITY_PRESET default)
```

Because `add_halide_library` bundles a Halide runtime into the library by
default, the resulting shared module is self-contained and, like
`halide.runtime` itself, has no dependency on `libHalide`.

#### Loading and calling a kernel

Once you have such a library, loading and calling it looks much like using the
compiled extension above:

```python
import numpy as np
import halide.runtime as hlr

# dlopen the shared library and locate the Halide filter inside it. The filter
# name defaults to the library's file name; pass name=... if it differs.
kernel = hlr.load("/path/to/my_kernel.so", name="my_kernel")

print(kernel.name) # "my_kernel"
print(kernel.target) # the Target string it was compiled for
print(kernel.argument_names) # e.g. ['input', 'offset', 'output']

# `kernel.arguments` gives the full calling convention: one dict per argument,
# in argv order, with its name, kind ('input_scalar', 'input_buffer', or
# 'output_buffer'), element type (e.g. 'uint8'), and dimensions (0 for scalars).
for arg in kernel.arguments:
print(
arg
) # {'name': 'input', 'kind': 'input_buffer', 'type': 'uint8', 'dimensions': 2}

input_buf = imageio.imread("/path/to/some/file.png")
output_buf = np.empty(input_buf.shape, dtype=input_buf.dtype)

# Arguments -- inputs, scalars, and the output buffer(s) -- are passed
# positionally in the order given by kernel.argument_names, or by keyword,
# in the Python manner:
kernel(input_buf, np.int32(5), output_buf)
# kernel(input=input_buf, offset=5, output=output_buf)
```

As with the compiled extension, Halide does not allocate outputs for you: you
must pass in a correctly-sized output buffer, and error conditions raise a
Python exception rather than returning an int.

#### The `halide.runtime.Buffer` type

Any object that supports the Python buffer protocol (such as a numpy array) may
be passed directly to a kernel. For finer control, `halide.runtime.Buffer` wraps
such an object as a Halide runtime buffer, without copying:

```python
buf = hlr.Buffer(np.empty((480, 640), dtype=np.uint8))
buf.dimensions # 2
buf.type # "uint8"
buf.shape # [480, 640]

view = np.asarray(buf) # a zero-copy view of the same memory
```

A `Buffer` exposes the same `_get_raw_halide_buffer_t` protocol that
`halide.Buffer` and the extensions produced by
`add_halide_python_extension_library` use, so the very same object can be handed
either to a kernel loaded via `halide.runtime.load` or to a function in a
generated extension module.

The same memory-order caveats described in the previous section apply here:
numpy's default row-major layout corresponds to Halide's axes in reverse order,
so construct your arrays with `order='F'` (or `.transpose()` them) when the axis
order matters.

### Advanced Generator-Related Topics

#### Generator Aliases
Expand Down
Loading
Loading