Halide入門教程05
// Halide教程第五課:向量化,並行化,平鋪,數據分塊
// 本課展示瞭如何才操作函數像素索引的計算順序,包括向量化/並行化/平鋪/分塊等技術
// 在linux系統中,採用如下指令編譯並執行
// g++ lesson_05*.cpp -g -I ../include -L ../bin -lHalide -lpthread -ldl -o lesson_05 -std=c++11
// LD_LIBRARY_PATH=../bin ./lesson_05
#include "Halide.h"
#include <stdio.h>
#include <algorithm>
using namespace Halide;
int main(int argc, char **argv) {
Var x("x"), y("y");
// First we observe the default ordering.
{
Func gradient("gradient");
gradient(x, y) = x + y;
gradient.trace_stores();
//默認遍歷像素的順序是行優先,即內層循環沿着行方向,外層循環沿着列方向
printf("Evaluating gradient row-major\n");
Buffer<int> output = gradient.realize(4, 4);
// The equivalent C is:
printf("Equivalent C:\n");
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
}
printf("\n\n");
// 跟蹤系統調度可以很容易理解調度系統如何工作。可以通過Halide提供的函數來打印出實際工作
// 是執行的哪種循環調度。
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
// Because we're using the default ordering, it should print:
// compute gradient:
// for y:
// for x:
// gradient(...) = ...
}
// Reorder variables.
{
Func gradient("gradient_col_major");
gradient(x, y) = x + y;
gradient.trace_stores();
// 可以通過reorder函數來改變函數遍歷的順序,下面的語句將行方向(y)置於內層循環,而將原本的內層
// 循環調整到了外循環。也就是說y遍歷比x遍歷更快。是一種列優先的遍歷方法
gradient.reorder(y, x);
printf("Evaluating gradient column-major\n");
Buffer<int> output = gradient.realize(4, 4);
printf("Equivalent C:\n");
for (int x = 0; x < 4; x++) {
for (int y = 0; y < 4; y++) {
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
}
printf("\n");
//
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
}
// Split a variable into two.
{
Func gradient("gradient_split");
gradient(x, y) = x + y;
gradient.trace_stores();
// 原始調度中,最有效的就是split調度了,它將一個大循環,拆解成一個外部循環和一個內部循環;
// 即,將x方向的循環,拆成一個外部循環x_outer和一個內部循環x_inner
// 下面的split將x拆成x_outer,x_inner, 內循環的長度爲2
Var x_outer, x_inner;
gradient.split(x, x_outer, x_inner, 2);
printf("Evaluating gradient with x split into x_outer and x_inner \n");
Buffer<int> output = gradient.realize(4, 4);
printf("Equivalent C:\n");
for (int y = 0; y < 4; y++) {
for (int x_outer = 0; x_outer < 2; x_outer++) {
for (int x_inner = 0; x_inner < 2; x_inner++) {
int x = x_outer * 2 + x_inner;
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
}
}
printf("\n");
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
}
// Fuse two variables into one.
{
Func gradient("gradient_fused");
gradient(x, y) = x + y;
// 和split相反的是fuse,它將兩個變量融合成一個變量,fuse的重要性並沒有split高。
Var fused;
gradient.fuse(x, y, fused);
printf("Evaluating gradient with x and y fused\n");
Buffer<int> output = gradient.realize(4, 4);
printf("Equivalent C:\n");
for (int fused = 0; fused < 4*4; fused++) {
int y = fused / 4;
int x = fused % 4;
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
printf("\n");
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
}
// Evaluating in tiles.
// tile的中文意思是瓦片,在這裏是指將圖像數據拆分成和瓦片一項的小圖像塊
{
Func gradient("gradient_tiled");
gradient(x, y) = x + y;
gradient.trace_stores();
// 既然我們可以拆分和調整順序,我們可以按照劃分數據塊的方式來進行計算。將x和y方向拆分,然後
// 調製x和y的順序,按照小的數據塊的方式來進行遍歷。
// 一個小的數據塊將整個圖像劃分成小的矩形,外層循環在tile擊斃恩進行循環,遍歷所有的tile。
Var x_outer, x_inner, y_outer, y_inner;
gradient.split(x, x_outer, x_inner, 4);
gradient.split(y, y_outer, y_inner, 4);
gradient.reorder(x_inner, y_inner, x_outer, y_outer);
// This pattern is common enough that there's a shorthand for it:
// gradient.tile(x, y, x_outer, y_outer, x_inner, y_inner, 4, 4);
printf("Evaluating gradient in 4x4 tiles\n");
Buffer<int> output = gradient.realize(8, 8);
printf("Equivalent C:\n");
for (int y_outer = 0; y_outer < 2; y_outer++) {
for (int x_outer = 0; x_outer < 2; x_outer++) {
for (int y_inner = 0; y_inner < 4; y_inner++) {
for (int x_inner = 0; x_inner < 4; x_inner++) {
int x = x_outer * 4 + x_inner;
int y = y_outer * 4 + y_inner;
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
}
}
}
printf("\n");
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
}
// Evaluating in vectors.
{
Func gradient("gradient_in_vectors");
gradient(x, y) = x + y;
gradient.trace_stores();
// split能夠讓內層循環變量在一個劃分銀子內變化。這個劃分因子通常是指定的,因此在編譯時是一個常數
// 因此我們可以調用向量化指令來執行內部循環。在這裏我們指定這個因子爲4,這樣就可以調用x86機器上的SSE
//指令來計算4倍寬的向量,這裏充分利用了cpu的SIMD指令來加快計算
Var x_outer, x_inner;
gradient.split(x, x_outer, x_inner, 4);
gradient.vectorize(x_inner);
// 上述過程有更簡單的形式
// gradient.vectorize(x, 4);
// 等價於
// gradient.split(x, x, x_inner, 4);
// gradient.vectorize(x_inner);
// 這裏我們重用了x,將它當作外循環變量,稍後的調度將x當作外循環(x_outer)來進行調度
// 這次在一個8x4的矩形上執行gradient算法
printf("Evaluating gradient with x_inner vectorized \n");
Buffer<int> output = gradient.realize(8, 4);
printf("Equivalent C:\n");
for (int y = 0; y < 4; y++) {
for (int x_outer = 0; x_outer < 2; x_outer++) {
// The loop over x_inner has gone away, and has been
// replaced by a vectorized version of the
// expression. On x86 processors, Halide generates SSE
// for all of this.
int x_vec[] = {x_outer * 4 + 0,
x_outer * 4 + 1,
x_outer * 4 + 2,
x_outer * 4 + 3};
int val[] = {x_vec[0] + y,
x_vec[1] + y,
x_vec[2] + y,
x_vec[3] + y};
printf("Evaluating at <%d, %d, %d, %d>, <%d, %d, %d, %d>:"
" <%d, %d, %d, %d>\n",
x_vec[0], x_vec[1], x_vec[2], x_vec[3],
y, y, y, y,
val[0], val[1], val[2], val[3]);
}
}
printf("\n");
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
}
// Unrolling a loop.
{
Func gradient("gradient_unroll");
gradient(x, y) = x + y;
gradient.trace_stores();
// 如果多個像素共享一些重複的(overlapping)數據,可以將循環鋪平,從而共享的數據只需要載入或者
// 計算一次。它和向量化的表達方式類似。先將數據進行劃分,然後將內層循環鋪平。
Var x_outer, x_inner;
gradient.split(x, x_outer, x_inner, 2);
gradient.unroll(x_inner);
// The shorthand for this is:
// gradient.unroll(x, 2);
printf("Evaluating gradient unrolled by a factor of two\n");
Buffer<int> result = gradient.realize(4, 4);
printf("Equivalent C:\n");
for (int y = 0; y < 4; y++) {
for (int x_outer = 0; x_outer < 2; x_outer++) {
// Instead of a for loop over x_inner, we get two
// copies of the innermost statement.
{
int x_inner = 0;
int x = x_outer * 2 + x_inner;
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
{
int x_inner = 1;
int x = x_outer * 2 + x_inner;
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
}
}
printf("\n");
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
}
// Splitting by factors that don't divide the extent.
{
Func gradient("gradient_split_7x2");
gradient(x, y) = x + y;
gradient.trace_stores();
// 當原來圖像尺寸不能整除劃分的小矩形尺寸時,最後的一行或者一列的tile在邊界處會出現重複計算的現象
Var x_outer, x_inner;
gradient.split(x, x_outer, x_inner, 3);
printf("Evaluating gradient over a 7x2 box with x split by three \n");
Buffer<int> output = gradient.realize(7, 2);
printf("Equivalent C:\n");
for (int y = 0; y < 2; y++) {
for (int x_outer = 0; x_outer < 3; x_outer++) { // Now runs from 0 to 2
for (int x_inner = 0; x_inner < 3; x_inner++) {
int x = x_outer * 3;
// Before we add x_inner, make sure we don't
// evaluate points outside of the 7x2 box. We'll
// clamp x to be at most 4 (7 minus the split
// factor).
if (x > 4) x = 4;
x += x_inner;
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
}
}
printf("\n");
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
// 如果仔細查看程序的輸出,你會發現有些像素點進行了不止一次計算。這是因爲尺寸不能整除所致
// 由於Halide函數沒有邊緣效應,因此計算多次並不會產生副作用。
}
// Fusing, tiling, and parallelizing.
{
// 這裏纔是fuse真正發揮威力的地方。如果想要在多個維度進行並行計算,
// 可以將多個循環網fuse起來,然後在fuse後的維度下進行並行計算
Func gradient("gradient_fused_tiles");
gradient(x, y) = x + y;
gradient.trace_stores();
Var x_outer, y_outer, x_inner, y_inner, tile_index;
gradient.tile(x, y, x_outer, y_outer, x_inner, y_inner, 4, 4);
gradient.fuse(x_outer, y_outer, tile_index);
gradient.parallel(tile_index);
// 每個調度函數返回的是引用類型,因此可以按如下方式用點號連接起多次調用
// gradient
// .tile(x, y, x_outer, y_outer, x_inner, y_inner, 2, 2)
// .fuse(x_outer, y_outer, tile_index)
// .parallel(tile_index);
printf("Evaluating gradient tiles in parallel\n");
Buffer<int> output = gradient.realize(8, 8);
// tile層面的調度是亂序的,但是在每一個tile內部,是行優先的計算順序
printf("Equivalent (serial) C:\n");
// This outermost loop should be a parallel for loop, but that's hard in C.
for (int tile_index = 0; tile_index < 4; tile_index++) {
int y_outer = tile_index / 2;
int x_outer = tile_index % 2;
for (int y_inner = 0; y_inner < 4; y_inner++) {
for (int x_inner = 0; x_inner < 4; x_inner++) {
int y = y_outer * 4 + y_inner;
int x = x_outer * 4 + x_inner;
printf("Evaluating at x = %d, y = %d: %d\n", x, y, x + y);
}
}
}
printf("\n");
printf("Pseudo-code for the schedule:\n");
gradient.print_loop_nest();
printf("\n");
}
// 將前面演示的調度綜合在一起
{
Func gradient_fast("gradient_fast");
gradient_fast(x, y) = x + y;
// tile尺寸爲64x64,採用並行計算
Var x_outer, y_outer, x_inner, y_inner, tile_index;
gradient_fast
.tile(x, y, x_outer, y_outer, x_inner, y_inner, 64, 64)
.fuse(x_outer, y_outer, tile_index)
.parallel(tile_index);
// 將內部的64x64tile繼續拆分成更小的tile。在x方向上採用向量化的計算(調用SIMD指令),
// 在y方向進行平鋪
Var x_inner_outer, y_inner_outer, x_vectors, y_pairs;
gradient_fast
.tile(x_inner, y_inner, x_inner_outer, y_inner_outer, x_vectors, y_pairs, 4, 2)
.vectorize(x_vectors)
.unroll(y_pairs);
Buffer<int> result = gradient_fast.realize(350, 250);
printf("Checking Halide result against equivalent C...\n");
for (int tile_index = 0; tile_index < 6 * 4; tile_index++) {
int y_outer = tile_index / 4;
int x_outer = tile_index % 4;
for (int y_inner_outer = 0; y_inner_outer < 64/2; y_inner_outer++) {
for (int x_inner_outer = 0; x_inner_outer < 64/4; x_inner_outer++) {
// We're vectorized across x
int x = std::min(x_outer * 64, 350-64) + x_inner_outer*4;
int x_vec[4] = {x + 0,
x + 1,
x + 2,
x + 3};
// And we unrolled across y
int y_base = std::min(y_outer * 64, 250-64) + y_inner_outer*2;
{
// y_pairs = 0
int y = y_base + 0;
int y_vec[4] = {y, y, y, y};
int val[4] = {x_vec[0] + y_vec[0],
x_vec[1] + y_vec[1],
x_vec[2] + y_vec[2],
x_vec[3] + y_vec[3]};
// Check the result.
for (int i = 0; i < 4; i++) {
if (result(x_vec[i], y_vec[i]) != val[i]) {
printf("There was an error at %d %d!\n",
x_vec[i], y_vec[i]);
return -1;
}
}
}
{
// y_pairs = 1
int y = y_base + 1;
int y_vec[4] = {y, y, y, y};
int val[4] = {x_vec[0] + y_vec[0],
x_vec[1] + y_vec[1],
x_vec[2] + y_vec[2],
x_vec[3] + y_vec[3]};
// Check the result.
for (int i = 0; i < 4; i++) {
if (result(x_vec[i], y_vec[i]) != val[i]) {
printf("There was an error at %d %d!\n",
x_vec[i], y_vec[i]);
return -1;
}
}
}
}
}
}
printf("\n");
printf("Pseudo-code for the schedule:\n");
gradient_fast.print_loop_nest();
printf("\n");
// Note that in the Halide version, the algorithm is specified
// once at the top, separately from the optimizations, and there
// aren't that many lines of code total. Compare this to the C
// version. There's more code (and it isn't even parallelized or
// vectorized properly). More annoyingly, the statement of the
// algorithm (the result is x plus y) is buried in multiple places
// within the mess. This C code is hard to write, hard to read,
// hard to debug, and hard to optimize further. This is why Halide
// exists.
// 從前面給出的幾個調度的例子可以看出,Halide對應版本的代碼相對於C的代碼,算法和調度分別進行分離
// 調度方便,發ingbianjinxing遊動優化,而對應的C語言代碼,冗長雜亂,很難寫/難讀/調試困難,
// 不方便進一步優化
}
printf("Success!\n");
return 0;
}
編譯執行:
$ g++ lesson_05*.cpp -g -I ../include -L ../bin -lHalide -lpthread -ldl -o lesson_05 -std=c++11
$ ./lesson_05
執行結果:
由於篇幅太長,略
代碼詳解與效果圖示:
1 算法描述,對於所有版本都是一樣,和算法調度/優化高度分離,無依賴性
gradient(x, y) = x + y;
2 reorder函數,默認是x爲內循環,y爲外循環
gradient.reorder(y, x); // 交換x和y循環的順序
x爲內循環
y爲內循環
3 split函數,將一個大的變量拆分成兩個小的變量進行循環
for x in(0, 20):
//...
Func.split(x, x_outer, x_inner, 5);
// 等價於
for x_outer in(0,4):
for x_inner in(0, 5):
// ...
4 fuse函數,將多個循環合併成一個大循環,可以方便進行多個維度的並行調度
for x in(1,4):
for y in(1,4):
//...
funcs.fuse(x, y, xy):
//等價於
for xy in (1,16):
x = xy / 4;
y = xy % 4;
// ...
5 tile將整個圖像劃分成多個小矩形,分別在小矩形上進行處理,每個tile內部仍然是按照行優先的調度進行計算
6 vertorize向量化計算,調用x86的SSE指令,通常是單指令多數據指令(SIMD),加快計算
7 unroll,將循環鋪平,鋪平並不會影響計算順序,當循環內部有數據共享時,鋪平可以將重複計算的數據只需讀一次和計算一次,減少重複計算
8 原圖像尺寸不能整數split後tile小矩形尺寸情形
9 tile的外層循環融合成一個整體循環,在此基礎上調用多核並行
Var x_outer, y_outer, x_inner, y_inner, tile_index;
gradient.tile(x, y, x_outer, y_outer, x_inner, y_inner, 4, 4);
gradient.fuse(x_outer, y_outer, tile_index);
gradient.parallel(tile_index);
10 綜合調度
// tile尺寸爲64x64,採用並行計算
Var x_outer, y_outer, x_inner, y_inner, tile_index;
gradient_fast
.tile(x, y, x_outer, y_outer, x_inner, y_inner, 64, 64)
.fuse(x_outer, y_outer, tile_index)
.parallel(tile_index);
// 將內部的64x64tile繼續拆分成更小的tile。在x方向上採用向量化的計算(調用SIMD指令),
// 在y方向進行平鋪
Var x_inner_outer, y_inner_outer, x_vectors, y_pairs;
gradient_fast
.tile(x_inner, y_inner, x_inner_outer, y_inner_outer, x_vectors, y_pairs, 4, 2)
.vectorize(x_vectors)
.unroll(y_pairs);