本文是halide编程指南的连载,已同步至公众号
第19章 Wrapper Funcs
// 本课程演示如何使用Func::in和ImageParam::in在不同的位置对Func进行不同的调度,以及如何从Func或ImageParam转移加载.
// On linux上这样运行:
// g++ lesson_19*.cpp -g -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -lpthread -ldl -o lesson_19 -std=c++11
// LD_LIBRARY_PATH=<path/to/libHalide.so> ./lesson_19
// On os x:
// g++ lesson_19*.cpp -g -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -o lesson_19 -std=c++11
// DYLD_LIBRARY_PATH=<path/to/libHalide.dylib> ./lesson_19
// 源码树上:
// make tutorial_lesson_19_wrapper_funcs
#include "Halide.h"
// 我们还将包括printf的stdio.
#include <stdio.h>
using namespace Halide;
int main(int argc, char **argv) {
// 首先,我们将在下面声明一些要使用的变量.
Var x("x"), y("y"), xo("xo"), yo("yo"), xi("xi"), yi("yi");
// 本课程将介绍如何使用Func::in和ImageParam::in指令“wrapping”Func或ImageParam
{
// 考虑一个简单的两级管道:
Func f("f_local"), g("g_local");
f(x, y) = x + y;
g(x, y) = 2 * f(x, y) + 3;
f.compute_root();
// 这将生成以下循环嵌套:
// for y:
// for x:
// f(x, y) = x + y
// for y:
// for x:
// g(x, y) = 2 * f(x, y) + 3
// 使用Func::in,我们可以单独使用调度在f和g之间插入一个新的Func:
Func f_in_g = f.in(g);
f_in_g.compute_root();
// 等价地,我们也可以像这样链接时间表:
// f.in(g).compute_root();
// 这将生成以下三个循环嵌套:
// for y:
// for x:
// f(x, y) = x + y
// for y:
// for x:
// f_in_g(x, y) = f(x, y)
// for y:
// for x:
// g(x, y) = 2 * f_in_g(x, y) + 3
g.realize(5, 5);
// 请参见下面的可视化.
图1
// schedule指令f.in(g)用包装Func替换“g”中对“f”的所有调用,然后返回该包装。基本上,它将上面的原始管道重写为以下内容:
{
Func f_in_g("f_in_g"), f("f"), g("g");
f(x, y) = x + y;
f_in_g(x, y) = f(x, y);
g(x, y) = 2 * f_in_g(x, y) + 3;
f.compute_root();
f_in_g.compute_root();
g.compute_root();
}
// 单独来看,这样的转换似乎毫无意义,但它可以用于各种调度技巧.
}
{
// 在上面的时间表中,只有“g”对“f”的调用被替换。其他对f的调用仍会直接调用“f”。如果我们希望用一个包装器全局地替换对“f”的所有调用,我们只需用f.in().
// 考虑一个三级管道,其中有两个f消耗者:
Func f("f_global"), g("g_global"), h("h_global");
f(x, y) = x + y;
g(x, y) = 2 * f(x, y);
h(x, y) = 3 + g(x, y) - f(x, y);
f.compute_root();
g.compute_root();
h.compute_root();
// 我们将用对单个包装器的调用替换“g”和“h”中对“f”的所有调用:
f.in().compute_root();
// 等价的循环嵌套是:
// for y:
// for x:
// f(x, y) = x + y
// for y:
// for x:
// f_in(x, y) = f(x, y)
// for y:
// for x:
// g(x, y) = 2 * f_in(x, y)
// for y:
// for x:
// h(x, y) = 3 + g(x, y) - f_in(x, y)
h.realize(5, 5);
// 请看下面的一个可视化.
图2
}
{
// 我们还可以给g和h它们自己独特的f包装器。这次我们将把它们分别安排在消费者的循环嵌套中,这不是我们可以用单个全局包装器做的事情.
Func f("f_unique"), g("g_unique"), h("h_unique");
f(x, y) = x + y;
g(x, y) = 2 * f(x, y);
h(x, y) = 3 + g(x, y) - f(x, y);
f.compute_root();
g.compute_root();
h.compute_root();
f.in(g).compute_at(g, y);
f.in(h).compute_at(h, y);
// 这将创建循环嵌套:
// for y:
// for x:
// f(x, y) = x + y
// for y:
// for x:
// f_in_g(x, y) = f(x, y)
// for x:
// g(x, y) = 2 * f_in_g(x, y)
// for y:
// for x:
// f_in_h(x, y) = f(x, y)
// for x:
// h(x, y) = 3 + g(x, y) - f_in_h(x, y)
h.realize(5, 5);
// 请参见下面的可视化.
图3
}
{
// 到目前为止,这似乎是对内存的大量无意义复制。Func::in可以与其他调度指令组合用于多种用途。我们首先要研究的是为几个使用者创建不同的Func实现,并对每个使用者进行不同的调度.
// 我们将从几乎相同的管道开始.
Func f("f_sched"), g("g_sched"), h("h_sched");
f(x, y) = x + y;
g(x, y) = 2 * f(x, y);
// h will use a far-away region of f
h(x, y) = 3 + g(x, y) - f(x + 93, y - 87);
// 这一次我们将内联f.
// f.compute_root();
g.compute_root();
h.compute_root();
f.in(g).compute_at(g, y);
f.in(h).compute_at(h, y);
// g和h现在通过不同的包装器调用f。包装器是被调度的,但是f不是,这意味着f被内联到它的两个包装器中。他们将各自独立计算消费者所需的f区域。如果我们安排f compute_root,我们将计算g所需区域和h所需区域的边界框,这些区域大部分是未使用的数据.
// 我们还可以对每个包装器进行不同的调度。出于调度的目的,包装器继承它们包装的Func的纯变量,因此我们使用定义f时使用的x和y:
f.in(g).vectorize(x, 4);
f.in(h).split(x, xo, xi, 2).reorder(xo, xi);
// 请注意,第二次调用f.in(g)将返回第一次调用已经创建的包装,它不会生成新的包装.
h.realize(8, 8);
// 看一个可视化.
图4
// 请注意,因为f内联到它的两个包装器中,所以是包装器完成计算f的工作,而不仅仅是从现有的计算实现加载.
}
{
// Func::in通过一些较小的中间缓冲区(可能在堆栈上或共享GPU内存中)将Func中的加载转移到后台.
// 考虑一个管道,它转换一些 compute_root'd Func:
Func f("f_transpose"), g("g_transpose");
f(x, y) = sin(((x + y) * sqrt(y)) / 10);
f.compute_root();
g(x, y) = f(y, x);
// 我们想要的执行策略是将f的4x4块加载到寄存器中,在寄存器中转置它,然后将它写为g的4x4块:
Func f_tile = f.in(g);
// 我们现在有一个三级管道:
// f -> f_tile -> g
// f_tile将加载f的向量,并将它们转置到寄存器中。然后g将这些数据写回主存储器.
g.tile(x, y, xo, yo, xi, yi, 4, 4)
.vectorize(xi)
.unroll(yi);
// 我们将在g的tiles处计算f_transpose,并使用Func::reorder_storage来声明f_transpose 应存储在列-major中,这样g对其执行的加载可以是密集向量加载 .
f_tile.compute_at(g, xo)
.reorder_storage(y, x)
.vectorize(x)
.unroll(y);
// 我们注意确保f_transpose 只在恒定的指示符下被访问。在compute_at中存在的所有循环的完全展开/矢量化都会产生这种效果。只有在常量索引下才能访问的分配可以提升到寄存器中.
g.realize(16, 16);
// 看一个可视化
图5
}
{
// ImageParam::in的行为方式与Func::in相同,您可以使用它以类似的方式暂存加载。我们将使用ImageParam::in将输入图像的分片转移到GPU共享内存中,而不是再次转置,有效地使用共享/本地内存作为显式管理的缓存.
ImageParam img(Int(32), 2);
// 我们将计算输入的一小部分模糊.
Func blur("blur");
blur(x, y) = (img(x - 1, y - 1) + img(x, y - 1) + img(x + 1, y - 1) +
img(x - 1, y) + img(x, y) + img(x + 1, y) +
img(x - 1, y + 1) + img(x, y + 1) + img(x + 1, y + 1));
blur.compute_root().gpu_tile(x, y, xo, yo, xi, yi, 8, 8);
// 由ImageParam::in创建的包装函数有纯变量名为_0、_1等。按“blur”的平铺调度它,并将_0和_1映射到gpu线程.
img.in(blur).compute_at(blur, xo).gpu_threads(_0, _1);
// 如果没有Func::in,计算一个8x8的模糊块将对全局内存进行8*8*9的加载。在Func::in中,包装器预先向全局内存加载10*10,然后blur向共享/本地内存加载8*8*9.
// 选择适当的GPU API,就像我们在第12课中所做的那样
Target target = get_host_target();
if (target.os == Target::OSX) {
target.set_feature(Target::Metal);
} else {
target.set_feature(Target::OpenCL);
}
// 这个检查不是严格必要的,但是如果在没有预期的驱动程序和/或硬件的系统上运行,它允许更优雅的退出.
if (!host_supports_target_device(target)) {
printf("Requested GPU is not supported; skipping this test. (Do you have the proper hardware and/or driver installed?)\n");
return 0;
}
// 创建一个有趣的输入图像以供使用.
Buffer<int> input(258, 258);
input.set_min(-1, -1);
for (int y = input.top(); y <= input.bottom(); y++) {
for (int x = input.left(); x <= input.right(); x++) {
input(x, y) = x * 17 + y % 4;
}
}
img.set(input);
blur.compile_jit(target);
Buffer<int> out = blur.realize(256, 256);
// 检查输出是否符合我们的预期
for (int y = out.top(); y <= out.bottom(); y++) {
for (int x = out.left(); x <= out.right(); x++) {
int val = out(x, y);
int expected = (input(x - 1, y - 1) + input(x, y - 1) + input(x + 1, y - 1) +
input(x - 1, y) + input(x, y) + input(x + 1, y) +
input(x - 1, y + 1) + input(x, y + 1) + input(x + 1, y + 1));
if (val != expected) {
printf("out(%d, %d) = %d instead of %d\n",
x, y, val, expected);
return -1;
}
}
}
}
{
// Func::in还可以用于将Func的多个阶段分组到同一个循环嵌套中。考虑下面的管道,它计算每个像素的值,然后从左到右再向后扫描每个扫描线.
Func f("f_group"), g("g_group"), h("h_group");
// 初始化f
f(x, y) = sin(x - y);
RDom r(1, 7);
// 从左向右扫掠
f(r, y) = (f(r, y) + f(r - 1, y)) / 2;
// 从左向右扫
f(7 - r, y) = (f(7 - r, y) + f(8 - r, y)) / 2;
// 然后我们做一些复杂的访问模式:45度旋转和环绕
g(x, y) = f((x + y) % 8, (x - y) % 8);
// f应该被调度为compute_root,因为它的使用者以一种复杂的方式访问它。但这意味着f的所有阶段都是在单独的循环嵌套中计算的:
// for y:
// for x:
// f(x, y) = sin(x - y)
// for y:
// for r:
// f(r, y) = (f(r, y) + f(r - 1, y)) / 2
// for y:
// for r:
// f(7 - r, y) = (f(7 - r, y) + f(8 - r, y)) / 2
// for y:
// for x:
// g(x, y) = f((x + y) % 8, (x - y) % 8);
// 如果我们安排f所做的工作在y上共享一个公共循环,我们可以得到更好的局部性:
f.in(g).compute_root();
f.compute_at(f.in(g), y);
// f有一个带有更新阶段的Func的默认调度,该调度将在其使用者的最内部循环(现在是包装器f.in(g))处计算。因此,这将生成以下具有更好局部性的循环嵌套:
// for y:
// for x:
// f(x, y) = sin(x - y)
// for r:
// f(r, y) = (f(r, y) + f(r - 1, y)) / 2
// for r:
// f(7 - r, y) = (f(7 - r, y) + f(8 - r, y)) / 2
// for x:
// f_in_g(x, y) = f(x, y)
// for y:
// for x:
// g(x, y) = f_in_g((x + y) % 8, (x - y) % 8);
// 我们还将对的初始化进行矢量化,然后将像素值从f传输到其包装器中:
f.vectorize(x, 4);
f.in(g).vectorize(x, 4);
g.realize(8, 8);
// 请参见下面的可视化.
图6
}
printf("Success!\n");
return 0;
}
图1
图2
图3
图4
图5
图6
第20章 : 克隆函数
// 本课程演示如何使用Func::clone_in创建Func的克隆.
// On linux, 这样编译运行:
// g++ lesson_20*.cpp -g -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -lpthread -ldl -o lesson_20 -std=c++11
// LD_LIBRARY_PATH=<path/to/libHalide.so> ./lesson_20
// On os x:
// g++ lesson_20*.cpp -g -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -o lesson_20 -std=c++11
// DYLD_LIBRARY_PATH=<path/to/libHalide.dylib> ./lesson_20
// 源码树上这样做:
// make tutorial_lesson_20_cloning_funcs
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
int main(int argc, char **argv) {
// 首先,我们将在下面声明一些要使用的变量.
Var x("x"), y("y"), xo("xo"), yo("yo"), xi("xi"), yi("yi");
// 本课程将介绍如何使用Func::clone_in指令克隆Func.
{
// 考虑一个简单的两级管道:
Func f("f_single"), g("g_single"), h("h_single");
f(x, y) = x + y;
g(x, y) = 2 * f(x, y) + 3;
h(x, y) = f(x, y) + g(x, y) + 10;
f.compute_root();
g.compute_root();
h.compute_root();
// 这将生成以下循环嵌套:
// for y:
// for x:
// f(x, y) = x + y
// for y:
// for x:
// g(x, y) = 2 * f(x, y) + 3
// for y:
// for x:
// h(x, y) = f(x, y) + g(x, y) + 10
// 使用Func::clone_in,我们可以将对“g”中“f”的调用替换为单独使用调度“f”的克隆:
Func f_clone_in_g = f.clone_in(g);
f_clone_in_g.compute_root();
// 等价地,我们也可以像这样链接时间表:
// f.clone_in(g).compute_root();
// 这将生成以下循环嵌套:
// for y:
// for x:
// f(x, y) = x + y
// for y:
// for x:
// f_clone_in_g(x, y) = x + y
// for y:
// for x:
// g(x, y) = 2 * f_clone_in_g(x, y) + 3
// for y:
// for x:
// h(x, y) = f(x, y) + g(x, y) + 10
h.realize(5, 5);
// 调度指令f.clone_in(g)用“f”的克隆替换“g”中对“f”的所有调用,然后返回该克隆。基本上,它将上面的原始管道重写为以下内容:
{
Func f_clone_in_g("f_clone_in_g"), f("f"), g("g"), h("h");
f(x, y) = x + y;
f_clone_in_g(x, y) = x + y;
g(x, y) = 2 * f_clone_in_g(x, y) + 3;
h(x, y) = f(x, y) + g(x, y) + 10;
f.compute_root();
f_clone_in_g.compute_root();
g.compute_root();
h.compute_root();
}
}
{
// 在上面的时间表中,只有“g”对“f”的调用被替换。对“f”的其他调用仍将直接调用“f”(即“h”仍调用“f”,而不是克隆)。如果我们想用一个克隆替换“g”和“h”对“f”的所有调用,只需在({g,h})中f.clone_in({g, h}).
// 考虑一个三级管道,其中有两个f消耗者:
Func f("f_group"), g("g_group"), h("h_group"), out("out_group");
f(x, y) = x + y;
g(x, y) = 2 * f(x, y);
h(x, y) = f(x, y) + 10;
out(x, y) = f(x, y) + g(x, y) + h(x, y);
f.compute_root();
g.compute_root();
h.compute_root();
out.compute_root();
// 我们将用对单个克隆的调用替换“g”和“h”中对“f”的所有调用:
f.clone_in({g, h}).compute_root();
// 等价的循环嵌套是:
// for y:
// for x:
// f(x, y) = x + y
// for y:
// for x:
// f_clone(x, y) = x + y
// for y:
// for x:
// g(x, y) = 2 * f_clone(x, y)
// for y:
// for x:
// h(x, y) = f_clone(x, y) + 10
// for y:
// for x:
// out(x, y) = f(x, y) + g(x, y) + h(x, y)
out.realize(5, 5);
}
{
// Func::clone_in()的一个用例是当一个生产者的两个消费者消费该生产者不相交的区域时。以下面的例子为例:
Func f("f"), g("g"), h("h");
f(x) = x;
g(x) = 2 * f(0);
h(x) = f(99) + 10;
// 让我们安排“f”在根上计算.
f.compute_root();
// 因为“g”和“h”都消耗“f”,所以x维中“f”所需的区域是[0,99]。等价的循环嵌套是:
// for x = 0 to 99
// f(x) = x
// for x:
// g(x) = 2 * f(0)
// for x:
// h(x) = f(99) + 10
// 如果“f”的计算成本非常高,那么我们最好为每个消费者“g”和“h”提供不同的“f”副本,以避免不必要的计算。要为每个使用者创建单独的“f”副本,我们可以执行以下操作:
f.clone_in(g).compute_root();
// 等价的循环嵌套是:
// f(0) = x
// f_clone(99) = x
// for x:
// g(x) = 2 * f_clone(0)
// for x:
// h(x) = f(99) + 10
}
printf("Success!\n");
return 0;}
第 21章: 自动调度程序
// 到目前为止,我们已经手写了halide时间表,但也可以要求halide提出合理的时间表。我们称之为自动调度。本课程演示如何使用自动调度程序生成可复制粘贴的CPU调度,该调度随后可以改进.
// On linux or os x, 可以这样编译运行:
// g++ lesson_21_auto_scheduler_generate.cpp <path/to/tools/halide_image_io.h>/GenGen.cpp -g -std=c++11 -fno-rtti -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -lpthread -ldl -o lesson_21_generate
// export LD_LIBRARY_PATH=<path/to/libHalide.so>
# For linux
// export DYLD_LIBRARY_PATH=<path/to/libHalide.dylib>
# For OS X
// ./lesson_21_generate -o . -g auto_schedule_gen -f auto_schedule_false -e static_library,h,schedule target=host auto_schedule=false
// ./lesson_21_generate -o . -g auto_schedule_gen -f auto_schedule_true -e static_library,h,schedule -p <path/to/libautoschedule_mullapudi2016.so> -S Mullapudi2016 target=host auto_schedule=true machine_params=32,16777216,40
// g++ lesson_21_auto_scheduler_run.cpp -std=c++11 -I <path/to/Halide.h> -I <path/to/tools/halide_image_io.h> auto_schedule_false.a auto_schedule_true.a -ldl -lpthread -o lesson_21_run
// ./lesson_21_run
// 在源码树上可以这样:
// make tutorial_lesson_21_auto_scheduler_run.
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
// 我们将定义一个自动调度的生成器.
class AutoScheduled : public Halide::Generator<AutoScheduled>
{
public:
Input<Buffer<float>> input{"input", 3};
Input<float> factor{"factor"};
Output<Buffer<float>> output1{"output1", 2};
Output<Buffer<float>> output2{"output2", 2};
Expr sum3x3(Func f, Var x, Var y) {
return f(x - 1, y - 1) + f(x - 1, y) + f(x - 1, y + 1) +
f(x, y - 1) + f(x, y) + f(x, y + 1) +
f(x + 1, y - 1) + f(x + 1, y) + f(x + 1, y + 1);
}
void generate() {
// 对于我们的算法,我们将使用Harris角点检测.
Func in_b = BoundaryConditions::repeat_edge(input);
gray(x, y) = 0.299f * in_b(x, y, 0) + 0.587f * in_b(x, y, 1) + 0.114f * in_b(x, y, 2);
Iy(x, y) = gray(x - 1, y - 1) * (-1.0f / 12) + gray(x - 1, y + 1) * (1.0f / 12) +
gray(x, y - 1) * (-2.0f / 12) + gray(x, y + 1) * (2.0f / 12) +
gray(x + 1, y - 1) * (-1.0f / 12) + gray(x + 1, y + 1) * (1.0f / 12);
Ix(x, y) = gray(x - 1, y - 1) * (-1.0f / 12) + gray(x + 1, y - 1) * (1.0f / 12) +
gray(x - 1, y) * (-2.0f / 12) + gray(x + 1, y) * (2.0f / 12) +
gray(x - 1, y + 1) * (-1.0f / 12) + gray(x + 1, y + 1) * (1.0f / 12);
Ixx(x, y) = Ix(x, y) * Ix(x, y);
Iyy(x, y) = Iy(x, y) * Iy(x, y);
Ixy(x, y) = Ix(x, y) * Iy(x, y);
Sxx(x, y) = sum3x3(Ixx, x, y);
Syy(x, y) = sum3x3(Iyy, x, y);
Sxy(x, y) = sum3x3(Ixy, x, y);
det(x, y) = Sxx(x, y) * Syy(x, y) - Sxy(x, y) * Sxy(x, y);
trace(x, y) = Sxx(x, y) + Syy(x, y);
harris(x, y) = det(x, y) - 0.04f * trace(x, y) * trace(x, y);
output1(x, y) = harris(x + 2, y + 2);
output2(x, y) = factor * harris(x + 2, y + 2);
}
void schedule() {
if (auto_schedule) {
// 自动调度器需要对所有输入/输出大小和参数值进行估计,以便比较不同的备选方案并决定一个好的调度.
// 为了为输入图像的每个维度('input', 'filter', and 'bias')提供估计值(最小值和范围值),我们使用set_estimates()方法。set_estimates()接受相应维度的(min,extent)列表作为参数.
input.set_estimates({{0, 1024}, {0, 1024}, {0, 3}});
// 为了提供参数值的估计,我们使用set_estimate()方法 .
factor.set_estimate(2.0f);
// 为了为管道输出的每个维度提供估计值(最小值和范围值),我们使用set_estimates()方法。set_estimates()接受每个维度的(最小值,范围)列表.
output1.set_estimates({{0, 1024}, {0, 1024}});
output2.set_estimates({{0, 1024}, {0, 1024}});
// 从技术上讲,估计值可以是任何值,但是它们越接近实际用例值,生成的计划就越好.
// 要自动安排管道,我们不需要做任何其他事情。每个生成器都隐式地有一个名为“auto_schedule”的GeneratorParam;如果设置为true,Halide将自动对所有管道的输出调用auto_schedule()。每个生成器还隐式地有一个名为“machine_params”的GeneratorParams,它允许您为自动调度器指定机器体系结构的特征;它通常在Makefile中指定。如果未指定任何参数,则自动调度器将使用通用CPU体系结构的默认机器参数。让我们来看看机器参数的一些任意但合理的值.
//
// const int kParallelism = 32;
// const int kLastLevelCacheSize = 16 * 1024 * 1024;
// const int kBalance = 40;
// MachineParams machine_params(kParallelism, kLastLevelCacheSize, kBalance);
//
// MachineParams的参数是可用的最大并行级别、最后一级缓存的大小(以KB为单位)以及最后一级缓存的未命中成本与目标体系结构上的算术成本之间的比率,按此顺序排列.
// 请注意,在使用自动调度器时,不应将任何计划应用于管道;否则,自动调度器将抛出错误。当前自动计划程序无法处理部分计划的管道.
// 如果HL_DEBUG_CODEGEN设置为3或更大,调度将转储到stdout(以及许多其他信息);更有用的方法是将“schedule”添加到生成器的-e标志中。(在CMake和Bazel中,这是使用“extra_outputs”标志完成的。)
// 所生成的被转储到文件的调度表是一个实际的halide C++源,它很容易通过少量修改复制到这个非常相同的源文件中。程序员可以将此作为一个开始计划,并迭代地改进计划。请注意,当前的自动调度器只能生成CPU调度,并且只能进行平铺、简单的矢量化和并行化。它不处理行缓冲、存储重新排序或因子缩减.
// 在编写本文时,当在这个管道上运行时,自动调度器将为上面声明的估计和机器参数生成以下计划:
//
// Var x_i("x_i");
// Var x_i_vi("x_i_vi");
// Var x_i_vo("x_i_vo");
// Var x_o("x_o");
// Var x_vi("x_vi");
// Var x_vo("x_vo");
// Var y_i("y_i");
// Var y_o("y_o");
//
// Func Ix = pipeline.get_func(4);
// Func Iy = pipeline.get_func(7);
// Func gray = pipeline.get_func(3);
// Func harris = pipeline.get_func(14);
// Func output1 = pipeline.get_func(15);
// Func output2 = pipeline.get_func(16);
//
// {
// Var x = Ix.args()[0];
// Ix
// .compute_at(harris, x_o)
// .split(x, x_vo, x_vi, 8)
// .vectorize(x_vi);
// }
// {
// Var x = Iy.args()[0];
// Iy
// .compute_at(harris, x_o)
// .split(x, x_vo, x_vi, 8)
// .vectorize(x_vi);
// }
// {
// Var x = gray.args()[0];
// gray
// .compute_at(harris, x_o)
// .split(x, x_vo, x_vi, 8)
// .vectorize(x_vi);
// }
// {
// Var x = harris.args()[0];
// Var y = harris.args()[1];
// harris
// .compute_root()
// .split(x, x_o, x_i, 256)
// .split(y, y_o, y_i, 128)
// .reorder(x_i, y_i, x_o, y_o)
// .split(x_i, x_i_vo, x_i_vi, 8)
// .vectorize(x_i_vi)
// .parallel(y_o)
// .parallel(x_o);
// }
// {
// Var x = output1.args()[0];
// Var y = output1.args()[1];
// output1
// .compute_root()
// .split(x, x_vo, x_vi, 8)
// .vectorize(x_vi)
// .parallel(y);
// }
// {
// Var x = output2.args()[0];
// Var y = output2.args()[1];
// output2
// .compute_root()
// .split(x, x_vo, x_vi, 8)
// .vectorize(x_vi)
// .parallel(y);
// }
} else {
// 在这里,您可以声明手工编写的计划,或者粘贴自动调度器生成的计划。我们将在这里使用一个简单的调度来比较autoschedule和基本调度的性能.
gray.compute_root();
Iy.compute_root();
Ix.compute_root();
}
}
private:
Var x{"x"}, y{"y"}, c{"c"};
Func gray, Iy, Ix, Ixx, Iyy, Ixy, Sxx, Syy, Sxy, det, trace, harris;};
// 与第15课一样,我们注册生成器,然后用工具编译tools/GenGen.cpp.
HALIDE_REGISTER_GENERATOR(AutoScheduled, auto_schedule_gen)
// 编译完此文件后,请参阅如何使用它。 在lesson_21_auto_scheduler_run.cpp
第 22章: 自动调度
// 在开始之前,请阅读 lesson_21_auto_scheduler_generate.cpp
// 这是实际使用我们编译的halide管道的代码。它不依赖于libHalide,因此我们不会包含Halide.h。相反,它依赖于lesson_21_auto_scheduler_generator生成的头文件.
#include "auto_schedule_false.h"
#include "auto_schedule_true.h"
// 我们将使用Halide::Runtime::Buffer类将数据传入和传出管道.
#include "HalideBuffer.h"
#include "halide_benchmark.h"
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char **argv) {
// 让我们声明并初始化输入图像
Halide::Runtime::Buffer<float> input(1024, 1024, 3);
for (int c = 0; c < input.channels(); ++c) {
for (int y = 0; y < input.height(); ++y) {
for (int x = 0; x < input.width(); ++x) {
input(x, y, c) = rand();
}
}
}
Halide::Runtime::Buffer<float> output1(1024, 1024);
Halide::Runtime::Buffer<float> output2(1024, 1024);
// 多次运行每个版本的代码(无自动调度和有自动调度)以进行基准测试.
double auto_schedule_off = Halide::Tools::benchmark(2, 5, [&]() {
auto_schedule_false(input, 2.0f, output1, output2);
});
printf("Manual schedule: %gms\n", auto_schedule_off * 1e3);
double auto_schedule_on = Halide::Tools::benchmark(2, 5, [&]() {
auto_schedule_true(input, 2.0f, output1, output2);
});
printf("Auto schedule: %gms\n", auto_schedule_on * 1e3);
// auto_schedule_on应该更快,因为在auto_schedule_off版本中,调度非常简单.
if (!(auto_schedule_on < auto_schedule_off)) {
fprintf(stderr, "Warning: expected auto_schedule_on < auto_schedule_off , "
"saw auto_schedule_on=%f auto_schedule_off=%f\n", auto_schedule_on, auto_schedule_off); \
}
return 0;
}