Hello, Halide!

Halide employs a static metaprogramming model similar to Tensorflow or Terra. We write C++ or Python programs that construct Halide functions that describe the algorithm. Halide then transforms these functions into lower-level code based on the schedules. This has two benefits. First, it allows the compiler frontend to be embedded in a popular language. This makes interaction of Halide with these languages easier, and allows metaprogramming Halide functions using the host language. Second, in contrast to interpreting a dynamic computation graph, the static model enables Halide to generate more efficient code by employing compiler optimizations ahead of the time. This is important for the scheduling part of Halide to have effect.

Following is a Halide program that loads an image, makes it two times brighter, and save to an image file.

// To compile & run on Linux
// g++ hello_halide.cpp -g -I /path/to/halide/distrib/include -I /path/to/halide/distrib/tools -L /path/to/halide/distrib/bin -lHalide `libpng-config --cflags --ldflags` -ljpeg -lpthread -ldl -o hello_halide -std=c++11
// LD_LIBRARY_PATH=/path/to/halide/distrib/bin ./hello_halide
// MacOS
// g++ hello_halide.cpp -g -I /path/to/halide/distrib/include -I /path/to/halide/distrib/tools -L /path/to/halide/distrib/bin -lHalide `libpng-config --cflags --ldflags` -ljpeg -o hello_halide -std=c++11
// DYLD_LIBRARY_PATH=/path/to/halide/distrib/bin ./hello_halide

#include <Halide.h>
#include <halide_image_io.h>

using namespace Halide;
using namespace Halide::Tools;

int main() {
    // Constructing Halide functions statically.
    //
    ImageParam input(Float(32), 3);
    Func f("f");
    Var x("x"), y("y"), c("c");
    // Double the values and clamp them by 1.
    f(x, y, c) = min(2 * input(x, y, c), 1.f);

    // Actually compiling/executing the Halide functions.
    //
    // Setup the input by loading an image.
    Buffer<float> input_buffer = load_and_convert_image("images/rgb.png");
    input.set(input_buffer);
    // Process the input by calling f.realize
    Buffer<float> output_buffer = f.realize(
        input_buffer.width(), input_buffer.height(), input_buffer.channels());
    // Save the image to a file.
    convert_and_save_image(output_buffer, "output.png");
}
import halide as hl
import imageio
import numpy as np

# Constructing Halide functions statically.
input = hl.ImageParam(hl.Float(32), 3)
f = hl.Func('f')
x, y, c = hl.Var('x'), hl.Var('y'), hl.Var('c')
# Double the values and clamp them by 1.
f[x, y, c] = hl.min(2 * input[x, y, c], 1.0)

# Actually compiling/executing the Halide functions.
#
# Setup the input by loading an image (Halide assumes Fortran ordering).
img = hl.Buffer(np.asfortranarray(imageio.imread('images/rgb.png').astype(np.float32) / 255.0))
input.set(img)
# Process the input by calling f.realize
output = f.realize(img.width(), img.height(), img.channels())
# Save the image to a file by converting to a numpy array.
output = np.array(output)
imageio.imsave('output.png', (output * 255.0).astype(np.uint8))

This is our input:

input image

And this is our output:

output image

We will explain the code line by line.

To use Halide in our program, we need to include the Halide header in C++ or import the Halide module in Python:

#include <Halide.h>
#include <halide_image_io.h>

using namespace Halide;
using namespace Halide::Tools; // For loading/saving images
import halide as hl

Halide’s input can be represented with an ImageParam, which is a multi-dimensional array. The first arugment describes the type of the array, and the second argument describes the dimensionality.

ImageParam input(Float(32), 3);
# Construct an ImageParam with 3 dimensions
input = hl.ImageParam(hl.Float(32), 3)

Remember that we are metaprogramming Halide code. The input does not have an actual value yet. We will define it’s content when we actually execute the Halide program.

Computations are defined in Halide functions. The following code declares a Halide function f, that does not has a definition yet:

Func f("f");
f = hl.Func('f')

The constructor of Func takes an optional name argument that is useful for debugging and pretty printing.

Each Halide function describes an infinite multi-dimensional domain of values. This means that, when accessing a Halide function, it always returns some value or triggers an out-of-bound assertion. This has the benefit of memory safety – Halide guarantees that we can never have buffer overrun issues that cause segmentation faults. This relieves the programmers from having to worry about the boundaries of a computation.

To describe the multi-dimensional domain, we need to name the coordinates. Var is designed for this:

Var x("x"), y("y"), c("c");
x, y, c = hl.Var('x'), hl.Var('y'), hl.Var('c')

Once we have Func and Var declared, we are ready to describe our computation:

// Double the values and clamp them by 1.
f(x, y, c) = min(2 * input(x, y, c), 1.f);
# Double the values and clamp them by 1.
f[x, y, c] = hl.min(2 * input[x, y, c], 1.0)

Again, keep in mind that we are metaprogramming Halide functions – at this point there is no f computed. We don’t even have our input contents yet!

Note

Apart from arrays, we can also define 0-dimensional Halide functions to represent scalars:

Func g("g");
g() = 5.0f;
g = hl.Func('g');
g[()] = hl.f32(5.0)

Now we finished defining our Halide functions, we want to use it for computing something. First we need to setup our inputs. They are represented by Halide Buffer s. Unlike Halide Func s, they are multidimensional arrays that actually store values.

Buffer<float> input_buffer = load_and_convert_image("images/rgb.png");
input_buffer = hl.Buffer(np.asfortranarray(imageio.imread('images/rgb.png').astype(np.float32) / 255.0))

Halide’s Python frontend works seamlessly with numpy. However, note that Halide Buffer assumes Fortran ordering (dimensions to the left correspond to innermost storage), so we want to use np.asfortranarray to convert our numpy arrays.

Next we set the content of the input to the buffers we just created.

input.set(input_buffer);
input.set(input_buffer)

Now, to actually compute a window of f, we need to call f.realize. This generates a Buffer that has a finite extent and actual values inside:

// Process the input by calling f.realize
Buffer<float> output_buffer = f.realize(
    input_buffer.width(), input_buffer.height(), input_buffer.channels());
# Process the input by calling f.realize
output = f.realize(input_buffer.width(), input_buffer.height(), input_buffer.channels())

Finally we save the output image to a file, and we’re done!

// Save the image to a file.
convert_and_save_image(output_buffer, "output.png");
# Save the image to a file by converting to a numpy array.
output = np.array(output)
imageio.imsave('output.png', (output * 255.0).astype(np.uint8))

Note

We can read/write Halide Buffer s using operator() in C++ and __getitem__ in Python.

Buffer<float> b(100);
for (int i = 0; i < 100; i++) {
    b(i) = float(i);
}
b = hl.Buffer(hl.Float(32), 100);
for i in range(100):
    b[i] = float(i)

Note

We can create a 0-dimensional Buffer using make_scalar().

Buffer<float> b = Buffer<float>::make_scalar();
b() = 1.f;
b = hl.Buffer.make_scalar(hl.Float(32))
b[()] = 1.0

Note

We can use Halide Buffer s as if they are ImageParam.

Buffer<int> b(3);
Func f("f");
Var x("x");
f(x) = b(x);

// fill in b
b(0) = 1; b(1) = 2; b(2) = 5;
// realize f
f.realize(b.width());
b = hl.Buffer(hl.Int(32), 3)
f = hl.Func("f")
x = hl.Var("x")
f[x] = b[x]

# fill in b
b[0] = 1; b[1] = 2; b[2] = 5
# realize f
f.realize(b.width())

In the next tutorial we will talk about how to write more complex Halide algorithms using examples.