Revision 3657cf5f363fd64aeaf06432e62e3960800927b0 authored by Andrew Adams on 26 January 2024, 17:26:12 UTC, committed by GitHub on 26 January 2024, 17:26:12 UTC
* Fix bounds_of_nested_lanes bounds_of_nested_lanes assumed that one layer of nested vectorization could be removed at a time. When faced with the expression: min(ramp(x8(a), x8(b), 5), x40(27)) It panicked, because on the left hand side it reduced the bounds to x8(a) ... x8(a) + x8(b) * 4, and on the right hand side it reduced the bounds to 27. It then attempted to take a min of mismatched types. In general we can't assume that binary operators on nested vectors have the same nesting structure on both sides, so I just rewrote it to reduce directly to a scalar. Fixes #8038
1 parent 4590a09
lesson_22_jit_performance.cpp
// Halide tutorial lesson 22: JIT compilation performance
// This lesson demonstrates the various performance implications of the
// various Halide methods of doing "Just-In-Time" compilation.
// On linux, you can compile and run it like so:
// g++ lesson_22*.cpp -g -I <path/to/Halide.h> -I <path/to/tools/halide_benchmark.h> -L <path/to/libHalide.so> -lHalide -lpthread -ldl -o lesson_22 -std=c++17
// LD_LIBRARY_PATH=<path/to/libHalide.so> ./lesson_20
// On os x:
// g++ lesson_22*.cpp -g -I <path/to/Halide.h> -I <path/to/tools/halide_benchmark.h> -L <path/to/libHalide.so> -lHalide -o lesson_22 -std=c++17
// DYLD_LIBRARY_PATH=<path/to/libHalide.dylib> ./lesson_22
// If you have the entire Halide source tree, you can also build it by
// running:
// make tutorial_lesson_22_jit_performance
// in a shell at the top of the halide source tree.
#include "Halide.h"
#include "halide_benchmark.h"
#include <stdio.h>
using namespace Halide;
using namespace Halide::Tools; // for benchmark()
// Let's define a helper function to construct a simple pipeline that we'll use for our performance tests.
Pipeline make_pipeline() {
// We'll start with a simple transpose operation...
Func input("input"), output("output");
Var x("x"), y("y");
// Fill the input with a linear combination of the coordinate values...
input(x, y) = cast<uint16_t>(x + y);
input.compute_root();
// Transpose the rows and cols
output(x, y) = input(y, x);
// Schedule it ... there's a number of possibilities here to do an efficient block-wise transpose.
Var xi("xi"), yi("yi");
// Let's focus on 8x8 subtiles, and then vectorize across X, and unroll across Y.
output.tile(x, y, xi, yi, 8, 8).vectorize(xi).unroll(yi);
// For more advanced scheduling:
//
// We can improve this even more by using the .in() directive (see Tutorial 19),
// which allows us to interpose new Funcs in between input and output.
//
// Here we can inject a block_transpose function to allow us to do 8 vectorized loads from the input.
Func block_transpose("block_transpose"), block("block");
block_transpose = input.in(output).compute_at(output, x).vectorize(x).unroll(y);
//
// And now Let's reorder and vectorize in X across the block.
block = block_transpose.in(output).reorder_storage(y, x).compute_at(output, x).vectorize(x).unroll(y);
// Return the constructed pipeline
return Pipeline(output);
}
int main(int argc, char **argv) {
// Since we'll be using the same sample and iteration counts for our benchmarking,
// let's define them here in the outermost scope.
constexpr int samples = 100;
constexpr int iterations = 1;
// Now, let's measure the performance of constructing and executing a simple pipeline from scratch...
{
size_t count = 0;
double t = benchmark(samples, iterations, [&]() {
// First, create an output buffer to hold the results.
Buffer<uint16_t> result(1024, 1024);
// Now, construct our pipeline from scratch.
Pipeline pipeline = make_pipeline();
// And then call realize to execute the pipeline.
pipeline.realize(result);
++count;
});
// On a MacBook Pro M1, we should get around ~1800 times/sec.
std::cout << "Compile & Execute Pipeline (from scratch): " << int(count / t) << " times/sec\n";
}
// This time, let's create the pipeline outside the timing loop and re-use it for each execution...
{
// Create our pipeline, and re-use it in the loop below
Pipeline pipeline = make_pipeline();
size_t count = 0;
double t = benchmark(samples, iterations, [&]() {
// Create our output buffer
Buffer<uint16_t> result(1024, 1024);
// Now, call realize
pipeline.realize(result);
++count;
});
// On a MacBook Pro M1, we should get around ~175000 times/sec (almost 95-100x times faster!).
std::cout << "Compile & Execute Pipeline (re-use pipeline): " << int(count / t) << " times/sec\n";
}
// Let's do the same thing as before, but explicitly JIT compile before we realize...
{
Pipeline pipeline = make_pipeline();
// Let's JIT compile for our target before we realize, and see what happens...
const Target target = get_jit_target_from_environment();
pipeline.compile_jit(target);
size_t count = 0;
double t = benchmark(samples, iterations, [&]() {
Buffer<uint16_t> result(1024, 1024);
pipeline.realize(result);
++count;
});
// On a MacBook Pro M1, this should be about the same as the previous run (about ~175000 times/sec)
//
// This may seem somewhat surprising, since compiling before realizing doesn't seem to make
// much of a difference to the previous case. However, the first call to realize() will implicitly
// JIT-compile and cache the generated code associated with the Pipeline object, which is basically
// what we've done here. Each subsequent call to realize uses the cached version of the native code,
// so there's no additional overhead, and the cost is amortized as we re-use the pipeline.
std::cout << "Execute Pipeline (compile before realize): " << int(count / t) << " times/sec\n";
// Another subtlety is the creation of the result buffer ... the declaration implicitly
// allocates memory which will add overhead to each loop iteration. This time, let's try
// using the realize({1024, 1024}) call which will use the buffer managed by the pipeline
// object for the outputs...
count = 0;
t = benchmark(samples, iterations, [&]() {
Buffer<uint16_t> result = pipeline.realize({1024, 1024});
++count;
});
// On a MacBook Pro M1, this should be about the same as the previous run (about ~175000 times/sec).
std::cout << "Execute Pipeline (same but with realize({})): " << int(count / t) << " times/sec\n";
// Or ... we could move the declaration of the result buffer outside the timing loop, and
// re-use the allocation (with the caveat that we will be stomping over its contents on each
// execution).
Buffer<uint16_t> result(1024, 1024);
count = 0;
t = benchmark(samples, iterations, [&]() {
pipeline.realize(result);
++count;
});
// On a MacBook Pro M1, this should be much more efficient ... ~200000 times/sec (or 10-12% faster).
std::cout << "Execute Pipeline (re-use buffer with realize): " << int(count / t) << " times/sec\n";
}
// Alternatively, we could compile to a Callable object...
{
Pipeline pipeline = make_pipeline();
const Target target = get_jit_target_from_environment();
// Here, we can ask the pipeline for its argument list (these are either Params,
// ImageParams, or Buffers) so that we can construct a Callable object with the same
// calling convention.
auto arguments = pipeline.infer_arguments();
// The Callable object acts as a convenient way of invoking the compiled code like
// a function call, using an argv-like syntax for the argument list. It also caches
// the JIT compiled code, so there's no code generation overhead when invoking the
// callable object and executing the pipeline.
Callable callable = pipeline.compile_to_callable(arguments, target);
// Again, we'll pre-allocate and re-use the result buffer.
Buffer<uint16_t> result(1024, 1024);
size_t count = 0;
double t = benchmark(samples, iterations, [&]() {
callable(result);
++count;
});
// This should be about the same as the previous run (about ~200000 times/sec).
std::cout << "Execute Pipeline (compile to callable): " << int(count / t) << " times/sec\n";
// Perhaps even more convient, we can create a std::function object from the callable,
// which allows cleaner type checking for the parameters, and slightly less overhead
// for invoking the function. The list used for the template parameters needs to match
// the list for the parameters of the pipeline. Here, we have a single result buffer,
// so we specify Buffer<uint16_t> in our call to .make_std_function<>. If we had other
// scalar parameters, input buffers or output buffers, we'd pass them in the template
// parameter list too.
auto function = callable.make_std_function<Buffer<uint16_t>>();
count = 0;
t = benchmark(samples, iterations, [&]() {
function(result);
++count;
});
// On a MacBook Pro M1, this should be slightly more efficient than the callable (~1% faster).
std::cout << "Execute Pipeline (compile to std::function): " << int(count / t) << " times/sec\n";
}
// Let's see how much time is spent on just compiling...
{
Pipeline pipeline = make_pipeline();
// Only the first call to compile_jit() is expensive ... after the code is generated,
// it gets stored in a cache for later re-use, so repeatedly calling compile_jit has
// very little overhead after its been cached.
size_t count = 0;
double t = benchmark(samples, iterations, [&]() {
pipeline.compile_jit();
++count;
});
// Only the first call does any work and the rest are essentially free.
// On a MacBook Pro M1, we should expect ~2 billion times/sec.
std::cout << "Compile JIT (using cache): " << int(count / t) << " times/sec\n";
// You can invalidate the cache manually, which will destroy all the compiled state.
count = 0;
t = benchmark(samples, iterations, [&]() {
pipeline.invalidate_cache();
pipeline.compile_jit();
++count;
});
// This is an intentionally expensive loop, and very slow!
// On a MacBook Pro M1, we should see only ~2000 times/sec.
std::cout << "Compile JIT (from scratch): " << int(count / t) << " times/sec\n";
}
// Alternatively we could compile to a Module...
{
Pipeline pipeline = make_pipeline();
auto args = pipeline.infer_arguments();
// Compiling to a module generates a self-contained Module containing an internal-representation
// of the lowered code suitable for further compilation. So, it's not directly
// runnable, but it can be used to link/combine Modules and generate object files,
// static libs, bitcode, etc.
size_t count = 0;
double t = benchmark(samples, iterations, [&]() {
Module m = pipeline.compile_to_module(args, "transpose");
++count;
});
// On a MacBook Pro M1, this should be around ~10000 times/sec
std::cout << "Compile to Module: " << int(count / t) << " times/sec\n";
}
printf("DONE!\n");
return 0;
}
![swh spinner](/static/img/swh-spinner.gif)
Computing file changes ...