Halide學習筆記----Halide tutorial源碼閱讀5

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);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章