# Halide Bindings for Python Halide provides Python bindings for most of its public API. Only Python 3.x is supported; at this time, Python 3.8 (or higher) is recommended. The Python bindings are supported on 64-bit Linux, OSX, and Windows systems. In addition to the ability to write just-in-time Halide code using Python, you can also use existing Halide Generators (written in C++) to produce Python extensions that can be used within Python code. (Note that the ability to write Halide Generators in Python is *not* supported at present, but is expected to be supported soon.) ## Python Requirements: Before building, you should ensure you have prerequite packages installed in your local Python environment. The best way to get set up is to use a virtual environment: ```console $ python3 -m venv venv $ . venv/bin/activate $ python3 -m pip install -U setuptools wheel $ python3 -m pip install -r python_bindings/requirements.txt ``` ## Compilation Instructions Build as part of the CMake build with `-DWITH_PYTHON_BINDINGS=ON` (this is the default). Note that this requires both Halide and LLVM to be built with RTTI and exceptions **enabled**, which is not the default for LLVM. ## Documentation and Examples As mentioned elsewhere, the Python API attempts to mimic the [C++ Halide API](http://halide-lang.org/docs) as directly as possible; there isn't separate Python-specific documentation for the API at this time. For now, examine the code for the example applications in the `test/apps/` and `tutorial/` subdirectories. The tests run as part of the standard CTest infrastructure and are labeled with the `python` label. You can run the Python tests specifically by running: ``` $ ctest -L python ``` From the Halide build directory. ## Differences from C++ API The Python bindings attempt to mimic the Halide C++ API as closely as possible, with some differences where the C++ idiom is either inappropriate or impossible: - Most APIs that take a variadic argumentlist of ints in C++ take an explicit list in Python. For instance, the usual version of the `Buffer` ctor in C++ offers both variadic and list versions: ``` Buffer<>(Type t, int extent_dim_0, int extent_dim_1, ...., extent_dim_N, string name = ""); Buffer<>(Type t, vector extents, string name = ""); ``` in Python, only the second variant is provided. - `Func` and `Buffer` access is done using `[]` rather than `()` - For zero-dimensional `Func` and `Buffer`, you must explicitly specify `[()]` -- that is, use an empty tuple as the index -- because `[]` is not syntactically acceptable in Python. - Some classes in the Halide API aren't provided because standard Python idioms are a better fit: - `Halide::Tuple` doesn't exist in the Python bindings; an ordinary Python tuple of `Halide::Expr` is used instead. - `Halide::Realization` doesn't exist in the Python bindings; an ordinary Python tuple of `Halide::Buffer` is used instead. - static and instance method overloads with the same name in the same class aren't allowed, so some convenience methods are missing from `Halide::Var` - Templated types (notably `Halide::Buffer<>` and `Halide::Param<>`) aren't provided, for obvious reasons; only the equivalents of `Halide::Buffer` and `Halide::Param` are supported. - The functions in `Halide::ConciseCasts` are present in the toplevel Halide module in Python, rather than a submodule: e.g., use `halide.i8_sat()`, not `halide.ConciseCasts.i8_sat()`. - Only things in the `Halide` namespace are supported; classes and methods that involve using the `Halide::Internal` namespace are not provided. - No mechanism is provided for overriding any runtime functions from Python. - No mechanism is provided for supporting `Func::define_extern`. - `Buffer::for_each_value()` isn't supported yet. - `Func::in` becomes `Func.in_` because `in` is a Python keyword. - `Func::async` becomes `Func.async_` because `async` is a Python keyword. - `ParamMap` isn't supported as an argument to any `Func` or `Pipeline` method, and never will be: it exists as a way to support thread-safe arguments to JIT-compiled functions, which can now be supported more simply and elegantly via `compile_to_callable()`. (It is likely that `ParamMap` will be removed from the C++ bindings in a future version of Halide as well.) - The `not` keyword cannot be used to negate boolean Halide expressions. Instead, the `logical_not` function can be used and is equivalent to using `operator!` in C++. - There is no way to override the logical `and`/`or` operators in Python to work with `Expr`: you must use the bitwise `|` and `&` instead. (Note that incorrectly using using `and`/`or` just short-circuits weirdly, rather than failing with some helpful error; this is an issue that we have not yet found any way to improve, unfortunately.) - Some error messages need to be made more informative. - Some exceptions are the "incorrect" type (compared to C++ expectations). - Many hooks to override runtime functions (e.g. Func::set_error_handler) aren't yet implemented. - The following parts of the Halide public API are currently missing entirely from the Python bindings (but are all likely to be supported at some point in the future): - `DeviceInterface` - `evaluate()` ## Example of Simple Usage The Python bindings for Halide are built as a standard part of the `install` target, and are present in the Halide install location at `$HALIDE_INSTALL/lib/python3/site-packages`; adding that to your `PYTHONPATH` should allow you to simply `import halide`: ```python # By convention, we import halide as 'hl' for terseness import halide as hl # Some constants edge = 512 k = 20.0 / float(edge) # Simple formula x, y, c = hl.Var('x'), hl.Var('y'), hl.Var('c') f = hl.Func('f') e = hl.sin(x * ((c + 1) / 3.0) * k) * hl.cos(y * ((c + 1) / 3.0) * k) f[x, y, c] = hl.cast(hl.UInt(8), e * 255.0) f.vectorize(x, 8).parallel(y) # Realize into a Buffer. buf = f.realize([edge, edge, 3]) # Do something with the image. We'll just save it to a PNG. import imageio imageio.imsave("/tmp/example.png", buf) ``` It's worth noting in the example above that the Halide `Buffer` object supports the Python Buffer Protocol (https://www.python.org/dev/peps/pep-3118) and thus is converted to and from other compatible objects (e.g., NumPy's `ndarray`), at essentially zero cost, with storage being shared. Thus, we can usually pass it directly to existing Python APIs (like `imsave()`) that expect 'image-like' objects without any explicit conversion necessary. ## Using Halide Generators from Python ### Using a C++ Generator from Python Let's say we have a very simple Generator: ``` class MyFilter : public Halide::Generator { public: Input> input{"input"}; Input mask{"mask"}; Output> output{"output"}; void generate() { output(x, y) = input(x, y) & mask; output.vectorize(x, 8).compute_root(); } }; HALIDE_REGISTER_GENERATOR(MyFilter, my_filter) ``` If you are using CMake, the simplest thing is to use `add_halide_library` and `add_halide_python_extension_library()`: ``` # Build a Halide library as you usually would, but be sure to include `PYTHON_EXTENSION` add_halide_library(my_filter GENERATOR my_filter_generator SOURCES my_filter_generator.cpp PYTHON_EXTENSION output_path_var [ FEATURES ... ] [ PARAMS ... ]) # Now wrap the generated code with a Python extension. # (Note that module name defaults to match the target name; we only # need to specify MODULE_NAME if we need a name that may differ) add_halide_python_extension_library(my_extension MODULE_NAME my_module HALIDE_LIBRARIES my_filter) ``` This compiles the Generator code in `my_filter_generator.cpp` with the registered name `my_filter` to produce the target `my_filter`, and then wraps the compiled output with a Python extension. The result will be a shared library of the form `..so`, where describes the specific Python version and platform (e.g., `cpython-310-darwin` for Python 3.10 on OSX.) Note that you can combine multiple Halide libraries into a single Python module; this is convenient for packagaing, but also because all the libraries in a single extension module share the same Halide runtime (and thus, the same caches, thread pools, etc.). ``` add_halide_library(my_filter1 ...) add_halide_library(my_filter2 ...) add_halide_library(my_filter3 ...) add_halide_python_extension_library(my_extension MODULE_NAME my_module HALIDE_LIBRARIES my_filter my_filter2 my_filter3) ``` Note that you must take care to ensure that all of the `add_halide_library` targets specified use the same Halide runtime; it may be necessary to use `add_halide_runtime` to define an explicit runtime that is shared by all of the targets: ``` add_halide_runtime(my_runtime) add_halide_library(my_filter1 USE_RUNTIME my_runtime ...) add_halide_library(my_filter2 USE_RUNTIME my_runtime ...) add_halide_library(my_filter3 USE_RUNTIME my_runtime ...) add_halide_python_extension_library(my_extension MODULE_NAME my_module HALIDE_LIBRARIES my_filter my_filter2 my_filter3) ``` ### Calling a C++ Generator from Python As long as the shared library is in `PYTHONPATH`, it can be imported and used directly. For the example above: ``` from my_module import my_filter import imageio import numpy as np # Read in some file for input input_buf = imageio.imread("/path/to/some/file.png") assert input_buf.dtype == np.uint8 # arbitrary mask = 0x7f # create a Buffer-compatible object for the output; we'll use np.array output_buf = np.empty(input_buf.shape, dtype=input_buf.dtype) # Note, Python code throws exception for error conditions rather than returning an int my_filter(input_buf, mask, output_buf) imageio.imsave("/tmp/masked_file.png", output_buf) ``` Above, we're using common Python utilities (`numpy`, `imageio`) to construct the input/output buffers we want to pass to Halide. **Note**: Getting the memory order correct can be a little confusing for numpy. By default numpy uses "C-style" [row-major](https://docs.scipy.org/doc/numpy-1.13.0/reference/internals.html) order, which sounds like the right option for Halide; however, this nomenclature assumes the matrix-math convention of ordering axes as `[rows, cols]`, whereas Halide (and imaging code in general) generally assumes `[x, y]` (i.e., `[cols, rows]`). Thus what you usually want in Halide is column-major ordering. This means numpy arrays, by default, come with the wrong memory layout for Halide. But if you construct the numpy arrays yourself (like above), you can 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.) ## Keeping Up To Date If you use the Halide Bindings for Python inside Google, you are *strongly* encouraged to [subscribe to announcements for new releases of Halide](https://github.blog/changelog/2018-11-27-watch-releases/), as it is likely that enhancements and tweaks to our Python support will be made in future releases. ## License The Python bindings use the same [MIT license](https://github.com/halide/Halide/blob/master/LICENSE.txt) as Halide. Python bindings provided by Connelly Barnes (2012-2013), Fred Rotbart (2014), Rodrigo Benenson (2015) and the Halide open-source community.