2018年2月26日 星期一

Halide Tutorial 非官方中文翻譯 - Part 7

Part 7 是記憶體格式與歸納範圍的進階技巧(非矩型, 向量化與平行化)
在實務上是用來處理複雜的計算規則, 以及對歸納的加速方式

==

Lesson 16 - RGB 影像與記憶體格式 (Part 1, Part 2)


#include "Halide.h"
#include <stdio.h>

using namespace Halide;
// 定義一個用以調亮 RGB 影像的 generator
class Brighten : public Halide::Generator<Brighten> {
public:
    // 定義輸入為一個三維影像, 前兩個維度為座標 x, y
    // 第三個為 color channel
   Input<Buffer<uint8_t>> input{"input", 3};
    // 為了能接收不同記憶體格式的輸入與輸出,
    // 將會以許多不同方式編譯此 generator
    // 而這是 GeneratorParam 好用的地方 (見 lesson 15)
    enum class Layout { Planar, Interleaved, Either, Specialized };
    GeneratorParam<Layout> layout{"layout",            // 預設值
            Layout::Planar,            // 自名稱到數值的對應
                {{ "planar",        Layout::Planar },
                 { "interleaved",   Layout::Interleaved },
                 { "either",        Layout::Either },
                 { "specialized",   Layout::Specialized }}};

    // 定義一個純量輸入來控制調整亮度的程度
    Input<uint8_t> offset{"offset"};
    // 宣告輸出
    Output<Buffer<uint8_t>> brighter{"brighter", 3};
    // 宣告變數
    Var x, y, c;

    void generate() {
        // 定義函式
        brighter(x, y, c) = input(x, y, c) + offset;

        // 排程
        brighter.vectorize(x, 16);
        // 取決於 generator 中的 'layout' 參數,
        // 將會編譯此 pipeline 來處理不同的記憶體格式
        if (layout == Layout::Planar) {
            // 此 pipeline 處理每條 scanline 為單一顏色的緊密排列.
            // 以 Lesson 10 stride 方式的描述, 在 x 的 stride 為 1
            // 這限制只允許平面的影像, 紅綠藍 channl 以下列方式擺放:

            // RRRRRRRR
            // RRRRRRRR
            // RRRRRRRR
            // RRRRRRRR
            // GGGGGGGG
            // GGGGGGGG
            // GGGGGGGG
            // GGGGGGGG
            // BBBBBBBB
            // BBBBBBBB
            // BBBBBBBB
            // BBBBBBBB

            // 這也能使用在較少使用的逐行的格式
            // 每一個 scanline 的紅綠藍依序擺放

            // RRRRRRRR
            // GGGGGGGG
            // BBBBBBBB
            // RRRRRRRR
            // GGGGGGGG
            // BBBBBBBB
            // RRRRRRRR
            // GGGGGGGG
            // BBBBBBBB
            // RRRRRRRR
            // GGGGGGGG
            // BBBBBBBB

        } else if (layout == Layout::Interleaved) {
            // 另一個常見的格式為'interleaved',
            // 對於每個 pixel 的紅綠藍數值在記憶體中依序存放:

            // RGBRGBRGBRGBRGBRGBRGBRGB
            // RGBRGBRGBRGBRGBRGBRGBRGB
            // RGBRGBRGBRGBRGBRGBRGBRGB
            // RGBRGBRGBRGBRGBRGBRGBRGB

            // 在這情形中 x 的 stride 值為 3, y 的 stride 值為影像寬的 3 倍
            // 而 c 的 stride 值為 1.
            // 能夠藉由以下來告知 Halide 輸入輸出格式為此:

            input
                .dim(0).set_stride(3) // dimension 0 (x) 的 stride 值為 3
                .dim(2).set_stride(1); // dimension 2 (c) 的 stride 值為 1

            brighter
                .dim(0).set_stride(3)
                .dim(2).set_stride(1);
            // 對於交錯格式, 可能會期望使用不同的排程
            // 藉由告知 Halide 額外的認定並檢測有 3 個 color channel 的情況,
            // 並利用這特性來讓 c loop 在最內層並且將其展開

            input.dim(2).set_bounds(0, 3); // Dimension 2 (c) starts at 0 and has extent 3.
            brighter.dim(2).set_bounds(0, 3);
            // 將 color channel loop 移置於最內層, 並且將其展開
            brighter.reorder(c, x, y).unroll(c);
            // 請注意當處理具有 alpha channel 的影像時,
            // x 的 stride 數值與 channel bound 為 4, 而非原本的 3
        } else if (layout == Layout::Either) {
            // 亦能夠移除所有的限制來編譯產生能是用在任何記憶體格式上
            // 但如此可能會很慢, 因為所有向量的載入變成了 gather (彙集)
            // 而所有向量的寫入變成了 scatter (散佈)
            input.dim(0).set_stride(Expr()); // 使用預設建構方式
                                             // 未定義的 Expr 表示沒有限制

            brighter.dim(0).set_stride(Expr());

        } else if (layout == Layout::Specialized) {
            // 藉由在執行時期告知 Halide 預期的記憶體格式, 與偵測到的 stride
            // 就能夠以良好的效率接收任何記憶體格式.
            // 首先解除當 dim(0).stride == 1 時的預設限制:

            input.dim(0).set_stride(Expr()); // 使用未定義的 Expr  來表示沒有限制

            brighter.dim(0).set_stride(Expr());
            // 建構用以偵測在執行時期是否為平面或是交錯的布林表示.
            // 這些條件必須針對所有被使用的情形之下一一檢測
            Expr input_is_planar =
                (input.dim(0).stride() == 1);
            Expr input_is_interleaved =
                (input.dim(0).stride() == 3 &&
                 input.dim(2).stride() == 1 &&
                 input.dim(2).extent() == 3);

            Expr output_is_planar =
                (brighter.dim(0).stride() == 1);
            Expr output_is_interleaved =
                (brighter.dim(0).stride() == 3 &&
                 brighter.dim(2).stride() == 1 &&
                 brighter.dim(2).extent() == 3);

            // 能夠使用 Func::specialize 來撰寫在執行時期切換的排程
            // 相關特定程式碼基於一個布林表示 (boolean Expr)
            // 該程式碼上利用著已知為 true 的 Expr.

            brighter.specialize(input_is_planar && output_is_planar);
            // 目前已對 brighter 作向量化與平行化,
            // 而此兩個列舉會繼承那些排程指示
            // 此外也能加入額外只套用在單一列舉的排程指令
            // 藉此告知 Halide 產生列舉版本的程式碼,
            // 其內容為交錯的輸入格式, 需調整次序且展開 loop

            brighter.specialize(input_is_interleaved && output_is_interleaved)
                .reorder(c, x, y).unroll(c);
            // 此外也能針對輸入為交錯, 輸出為平面而加入列舉, 或者相反的情況
            // 但列舉兩個情況已經足夠展示這個功能特性
            // 後續將會探索 Func::specialize 更創新的用法
            // 增加列舉可能會根本上增進套用條件的效能
            // 但也同時增加了需要編譯與產生的程式碼
            // 若執行檔大小是個考量, 且輸入輸出是明確的已知,
            // 應用上或許會想改用 set_stride 與 set_extent 取代
        }
    }
};
// 如同在 lesson 15 中, 註冊 generator 並且接著搭配 tools/GenGen.cpp 編譯
HALIDE_REGISTER_GENERATOR(Brighten, brighten)

// 編譯後來看看如何使用
// 這是實際上如何使用已經編譯出的 Halide pipeline 範例
// 由於並不需要依賴 libHalide, 因此無需引入 Halide.h.
//
// 取而代之地, 需要自上述程式所產生的 header 檔
#include "brighten_planar.h"
#include "brighten_interleaved.h"
#include "brighten_either.h"
#include "brighten_specialized.h"
// 將使用 Halide::Runtime::Buffer 在 pipeline 中傳遞資料
#include "HalideBuffer.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#include "clock.h"
// 提供各情況下執行時間的輔助函式. 印出自上次呼叫此函式起算的間隔時間
double tick(const char *name) {
    static double t_old = 0;
    double t_new = current_time();
    double dt = t_new - t_old;
    if (name) {
        printf("%s: %f\n", name, dt);
    }
    t_old = t_new;
    return dt;
}

int main(int argc, char **argv) {
    // 產生以交錯與平面格式儲存的影像
    // Halide::Runtime::Buffer 的預設是平面格式
    Halide::Runtime::Buffer<uint8_t> planar_input(1024, 768, 3);
    Halide::Runtime::Buffer<uint8_t> planar_output(1024, 768, 3);
    Halide::Runtime::Buffer<uint8_t> interleaved_input =
        Halide::Runtime::Buffer<uint8_t>::make_interleaved(1024, 768, 3);
    Halide::Runtime::Buffer<uint8_t> interleaved_output =
        Halide::Runtime::Buffer::make_interleaved(1024, 768, 3);
    // 提供在設置 generator 時限制的數值來檢查 stride 數值是否如預期,
    assert(planar_input.dim(0).stride() == 1);
    assert(planar_output.dim(0).stride() == 1);
    assert(interleaved_input.dim(0).stride() == 3);
    assert(interleaved_output.dim(0).stride() == 3);
    assert(interleaved_input.dim(2).stride() == 1);
    assert(interleaved_output.dim(2).stride() == 1);
    // 接著呼叫所編譯出的不同函數並一一確認執行效能

    // 啟動時鐘
    tick(NULL);
    // 在平面影像上執行平面版本, 以及在交錯影像上執行交錯版本
    // 每個版本都會執行 1000 次來取得評測結果.
    for (int i = 0; i < 1000; i++) {
        brighten_planar(planar_input, 1, planar_output);
    }
    double planar_time = tick("brighten_planar");
    // Click to show output ...

    for (int i = 0; i < 1000; i++) {
        brighten_interleaved(interleaved_input, 1, interleaved_output);
    }
    double interleaved_time = tick("brighten_interleaved");
    // Click to show output ...

   // 多數的操作上平面格式通常會叫交錯格式來得快
    assert(planar_time < interleaved_time);
    // 因為 stride 並非 generator 中所設定的, 因此下列兩個註解掉的呼叫會產生錯誤

    // brighten_planar(interleaved_input, 1, interleaved_output);
    // Error: Constraint violated: brighter.stride.0 (3) == 1 (1)

    // brighten_interleaved(planar_input, 1, planar_output);
    // Error: Constraint violated: brighter.stride.0 (1) == 3 (3)

    // 執行彈性版本並檢測效能, 應會正常運作但比上面版本都慢
    for (int i = 0; i < 1000; i++) {
        brighten_either(planar_input, 1, planar_output);
    }
    double either_planar_time = tick("brighten_either on planar images");
    assert(planar_time < either_planar_time);
    // Click to show output ...

    for (int i = 0; i < 1000; i++) {
        brighten_either(interleaved_input, 1, interleaved_output);
    }
    double either_interleaved_time = tick("brighten_either on interleaved images");
    assert(interleaved_time < either_interleaved_time);
    // Click to show output ...

    // 針對不同的格式執行列舉的版本
    // 在內部切換到等義程式碼的作法應與對不同格式編譯的版本效能一致
    for (int i = 0; i < 1000; i++) {
        brighten_specialized(planar_input, 1, planar_output);
    }
    double specialized_planar_time = tick("brighten_specialized on planar images");
    // Click to show output ...

    // if 的描述的成本應該可以被忽略, 但這個測試允許 50% 為測量誤差
    assert(specialized_planar_time < 1.5 * planar_time);

    for (int i = 0; i < 1000; i++) {
        brighten_specialized(interleaved_input, 1, interleaved_output);
    }
    double specialized_interleaved_time = tick("brighten_specialized on interleaved images");
    assert(specialized_interleaved_time < 2.0 * interleaved_time);
    // Click to show output ...

    return 0;
}

==

Lesson 17: 在非長方區域做歸納

// 這節示範如何定義使用 predicate (推斷, 使...基於)在部份的歸納區域做更新

#include "Halide.h"
#include <stdio.h>

using namespace Halide;

int main(int argc, char **argv) {
    // 在 lesson 9, 已經學習過如何使用 RDom 去定義在 Halide 更新定義中使用的歸納區域
    // 然而, 以 RDom 定義的範圍通常是長方形, 而更新發生在這長方區域中的每個點
    // 在一些情況中, 可能會想在一些非長方區域做運算
    // 例如: 圓形, 能藉由使用 RDom::where 指令來達成
    {
         // 以此純定義開始:
        Func circle("circle");
        Var x("x"), y("y");
        circle(x, y) = x + y;
        // 這裡要在中心點為(3, 3) 半徑為 3 的圓範圍內更新數值為原有的平方
        // 為了達成此目的, 首先使用 RDom 定義能涵蓋此圓的最小方形
        RDom r(0, 7, 0, 7);
       // 這個方形並不需要最小的. 事實上這方形只要覆蓋要更新的圓形區域,
        // 可以是任意大小. 然而這個方形愈緊密, 產生的 loop 範圍也愈緊密
        // Halide 會自動地儘可能緊密 loop 的範圍, 但一般來說定義最小的方形會較好

        // 接著使用 RDom::where 來定義此方形上的推斷條件
        // 如此只會在推斷條件為真的區域上更新
        // 這表示在這圓形的區域
        r.where((r.x - 3)*(r.x - 3) + (r.y - 3)*(r.y - 3) <= 10);

        // 在定義推斷條件後, 接著定義更新動作
        circle(r.x, r.y) *= 2;

        Buffer<int> halide_result = circle.realize(7, 7);       
        // 下圖為視覺化結果



        // 等義的 C code:
        int c_result[7][7];
        for (int y = 0; y < 7; y++) {
            for (int x = 0; x < 7; x++) {
                c_result[y][x] = x + y;
            }
        }
        for (int r_y = 0; r_y < 7; r_y++) {
            for (int r_x = 0; r_x < 7; r_x++) {
                // Update is only performed if the predicate evaluates to true.
                if ((r_x - 3)*(r_x - 3) + (r_y - 3)*(r_y - 3) <= 10) {
                    c_result[r_y][r_x] *= 2;
                }
            }
        }

        // 檢查結果是否一致:
        for (int y = 0; y < 7; y++) {
            for (int x = 0; x < 7; x++) {
                if (halide_result(x, y) != c_result[y][x]) {
                    printf("halide_result(%d, %d) = %d instead of %d\n",
                           x, y, halide_result(x, y), c_result[y][x]);
                    return -1;
                }
            }
        }
    }

    {
        // 也能夠在 RDom 上定義多個推斷條件. 在此嘗試在三角形區域中做更新
        // 為了達到這目的, 定義三個推斷條件, 各對應為三角形的一個邊
        Func triangle("triangle");
        Var x("x"), y("y");
        triangle(x, y) = x + y;
        // 首先定義能涵蓋三角形範圍的最小方形
        RDom r(0, 8, 0, 10);
         // 接著藉由呼較多次 RDom::where 將三個推斷條件加入 RDom
        r.where(r.x + r.y > 5);
        r.where(3*r.y - 2*r.x < 15);
        r.where(4*r.x - r.y < 20);

        // 也能如下地能夠將多個推斷條件包裹成一個:
        // r.where((r.x + r.y > 5) && (3*r.y - 2*r.x < 15) && (4*r.x - r.y < 20));

        // 定義更新步驟.
        triangle(r.x, r.y) *= 2;

        Buffer<int> halide_result = triangle.realize(10, 10);

         // 下圖為視覺化結果

        // 等義 C code:
        int c_result[10][10];
        for (int y = 0; y < 10; y++) {
            for (int x = 0; x < 10; x++) {
                c_result[y][x] = x + y;
            }
        }
        for (int r_y = 0; r_y < 10; r_y++) {
            for (int r_x = 0; r_x < 8; r_x++) {
                // 更新僅會在推斷條件為 true 時執行
                if ((r_x + r_y > 5) && (3*r_y - 2*r_x < 15) && (4*r_x - r_y < 20)) {
                    c_result[r_y][r_x] *= 2;
                }
            }
        }

        // 確認結果一致:
        for (int y = 0; y < 10; y++) {
            for (int x = 0; x < 10; x++) {
                if (halide_result(x, y) != c_result[y][x]) {
                    printf("halide_result(%d, %d) = %d instead of %d\n",
                           x, y, halide_result(x, y), c_result[y][x]);
                    return -1;
                }
            }
        }
    }

    {
        // 推斷條件的使用並不只侷限於歸納範圍的變數 (r.x, r.y, ...)
        // 亦能夠參考使用其他更新定義上的自由變數
        // 甚至是呼叫使用其他 Funcs 或是遞迴呼叫一樣的 Func 像是:
        Func f("f"), g("g");
        Var x("x"), y("y");
        f(x, y) = 2 * x + y;
        g(x, y) = x + y;

        // 這個歸納範圍的推斷條件取決於 'f' 的初始值.
        RDom r1(0, 5, 0, 5);
        r1.where(f(r1.x, r1.y) >= 4);
        r1.where(f(r1.x, r1.y) <= 7);
        f(r1.x, r1.y) /= 10;

        f.compute_root();

        // 這個例子使用其他 Func 呼叫.
        RDom r2(1, 3, 1, 3);
        r2.where(f(r2.x, r2.y) < 1);
        g(r2.x, r2.y) += 17;

        Buffer<int> halide_result_g = g.realize(5, 5);
        // 下圖為視覺化結果

        // 'f' 部份等義的 C code:
        int c_result_f[5][5];
        for (int y = 0; y < 5; y++) {
            for (int x = 0; x < 5; x++) {
                c_result_f[y][x] = 2 * x + y;
            }
        }
        for (int r1_y = 0; r1_y < 5; r1_y++) {
            for (int r1_x = 0; r1_x < 5; r1_x++) {
                // 當推斷條件為 true 時才會執行更新
                if ((c_result_f[r1_y][r1_x] >= 4) && (c_result_f[r1_y][r1_x] <= 7)) {
                    c_result_f[r1_y][r1_x] /= 10;
                }
            }
        }

        // 'g' 的等義 C code:
        int c_result_g[5][5];
        for (int y = 0; y < 5; y++) {
            for (int x = 0; x < 5; x++) {
                c_result_g[y][x] = x + y;
            }
        }
        for (int r2_y = 1; r2_y < 4; r2_y++) {
            for (int r1_x = 1; r1_x < 4; r1_x++) {
                // 當推斷條件為 true 時才會執行更新
                if (c_result_f[r2_y][r1_x] < 1) {
                    c_result_g[r2_y][r1_x] += 17;
                }
            }
        }

        // 確認結果是否一致:
        for (int y = 0; y < 5; y++) {
            for (int x = 0; x < 5; x++) {
                if (halide_result_g(x, y) != c_result_g[y][x]) {
                    printf("halide_result_g(%d, %d) = %d instead of %d\n",
                           x, y, halide_result_g(x, y), c_result_g[y][x]);
                    return -1;
                }
            }
        }
    }

    printf("Success!\n");

    return 0;
}

==

Lesson 18: Factoring an associative reduction using rfactor


// 這節示範如何使用排程指令 'rfactor' 來對結合歸納作平行化或向量化

#include "Halide.h"
#include <stdio.h>

using namespace Halide;

int main(int argc, char **argv) {
    // 宣告後續會使用到的變數
    Var x("x"), y("y"), i("i"), u("u"), v("v");

    // 以亂數建立輸入
    Buffer input(8, 8, "input");
    for (int y = 0; y < 8; ++y) {
        for (int x = 0; x < 8; ++x) {
            input(x, y) = (rand() % 256);
        }
    }

    {
        // 如同先前在 Lesson 9 提到的, 對歸納範圍部份的變數平行化是難以處理的,
        // 因為資料相依性可能橫跨多個變數

        // 考量 Lesson 9 中的 histogram 範例:
        Func histogram("hist_serial");
        histogram(i) = 0;
        RDom r(0, input.width(), 0, input.height());
        histogram(input(r.x, r.y) / 32) += 1;

        histogram.vectorize(i, 8);
        histogram.realize(8);
        // 下圖為視覺化結果
       
        // 能夠對初始化 histogram bucket 動作做向量化,
        // 由於在更新定義中對於有著對 x, y 方向的資料相依性
        // (換句話說更新參考了先前更新所計算出的數值),
        // 因此無在不產生 race condition 下對 r.x 或 r.y 作平行化或是向量化
        // 下列程式碼將產生一個錯誤:
        // histogram.update().parallel(r.y);
    }

    {
        // 然而, 請注意 histogram 是個俱備結合特性的操作 (是一種總和歸納)
        // 一個通常用來加速結合性歸納的技巧是分成多個部份,
        // 各別計算各自的部份結果, 最後將這接部份結果整併.
        // 由於每個分割的部份的計算是獨立的, 因此能夠對其做平行化
        // 回到 histogram 的例子, 藉由定義中介函數獨立計算每列的 histogram
        // 以此將歸納範圍以數列的方式分割:
        Func intermediate("intm_par_manual");
        intermediate(i, y) = 0;
        RDom rx(0, input.width());
        intermediate(input(rx, y) / 32, y) += 1;

        // 接著定義用以累加局部結果的第二個 stage:
        Func histogram("merge_par_manual");
        histogram(i) = 0;
        RDom ry(0, input.height());
        histogram(i) += intermediate(i, ry);
        // 由於中介資訊不再有著 y 方向上的資料相依性
        // 因此能夠對 y 做平行化:
        intermediate.compute_root().update().parallel(y);

        // 依然能夠對初始化作向量化
        intermediate.vectorize(i, 8);
        histogram.vectorize(i, 8);

        histogram.realize(8);

        // 下圖為視覺化結果

    }

    {
        // 對於結合歸納的手動分解是枯燥且容易出錯的.
        // 儘管對於 histogram 而言手動處理相當簡單,
        // 但若 RDom 有著推斷條件(RDom::where), 或當函數歸納到多維 tuple 上,
        // 情況將會相當快速地變得複雜
        // Halide 提供了一個透過排程指令 'rfactor' 的方式來處理此類的分解.
        // rfactor 將具結合性更新定義分為兩部份
        // 首先是一個用以計算部份歸納區域結果的中介
        // 接著以整併局部結果的新定義來取代既有的更新定義

        // 使用 rfactor, 一點都無需改變演算法:
        Func histogram("hist_rfactor_par");
        histogram(x) = 0;
        RDom r(0, input.width(), 0, input.height());
        histogram(input(r.x, r.y) / 32) += 1;
        // 分解的動作透過 rfactor 而被移至排程中.
        // rfactor 的輸入為包含可平行化的歸納變數(RVars)的成對清單.


        // 在產生的中介函式 Func, 所有參考這些歸納變數的部份將以"純"變數取代.
        // 由於藉由建構, 變數不會造成 race condition,
        // 因此中介歸納在這些維度上為可平行化.
        // 不在此清單的所有歸納變數將自原定義移除, 並移到中介函式中

        // 下列程式能產生同等於手動分解版本:
        Func intermediate = histogram.update().rfactor({{r.y, y}});
        // 將 {r.y, y} 作為 rfactor 參數傳入,
        // 如此使得 histogram 類似於手動分解版本在 y 維度上可平行處理
        intermediate.compute_root().update().parallel(y);
        // 在這個情況中, 僅對一個變數維度做分割,
        // 能夠將大括號省略, 直接以下列方式撰寫 rfactor
        // Func intermediate = histogram.update().rfactor(r.y, y);
        // 如同先前一般, 對初始作向量化
        intermediate.vectorize(x, 8);
        histogram.vectorize(x, 8);
        // 務必注意到 rfactor(一般來說, 或是歸納的分解) 只對於結合性歸納有效.
        // 結合性歸納有著無論計算如何分群結果都一致的良好特性
        // 若 rfactor 無法證明歸納的結合特性, 將會丟出錯誤.

        Buffer halide_result = histogram.realize(8);

        // 下圖為視覺化結果

        // 等義的 C code:
        int c_intm[8][8];
        for (int y = 0; y < input.height(); y++) {
            for (int x = 0; x < 8; x++) {
                c_intm[y][x] = 0;
            }
        }
        /* parallel */ for (int y = 0; y < input.height(); y++) {
            for (int r_x = 0; r_x < input.width(); r_x++) {
                c_intm[y][input(r_x, y) / 32] += 1;
            }
        }

        int c_result[8];
        for (int x = 0; x < 8; x++) {
            c_result[x] = 0;
        }
        for (int x = 0; x < 8; x++) {
            for (int r_y = 0; r_y < input.height(); r_y++) {
                c_result[x] += c_intm[r_y][x];
            }
        }

        // 確認結果一致:
        for (int x = 0; x < 8; x++) {
            if (c_result[x] != halide_result(x)) {
                printf("halide_result(%d) = %d instead of %d\n",
                       x, halide_result(x), c_result[x]);
                return -1;
            }
        }
    }

    {
        // 現在能夠透過排程指令 'rfactor' 分解結合性歸納
        // 使用此排程能探究多種的分解策略
        // 以同樣的 histogram 為例:
        Func histogram("hist_rfactor_vec");
        histogram(x) = 0;
        RDom r(0, input.width(), 0, input.height());
        histogram(input(r.x, r.y) / 32) += 1;

        // 相較先前使用 r.y, 這次改以 r.x 套用在 rfactor 上來對直行做分解
        Func intermediate = histogram.update().rfactor(r.x, u);
        // 現在對每行去獨立計算 histogram
        // 如此能在直行上作向量化
        intermediate.compute_root().update().vectorize(u, 8);
        // 請注意因為對內層維度向量化改變了數值累加到最後 histogram 的計算次序,
        // 因此這個技巧僅適用在俱備結合性(associative)與交換性(commutative)的歸納上
        // rfactor 將嘗試證明具有這些特性, 若無法則會丟出錯誤

        // 對初始做向量化
        intermediate.vectorize(x, 8);
        histogram.vectorize(x, 8);

        Buffer<int> halide_result = histogram.realize(8);

        // 下圖為視覺化結果


        // 等義的 C code:
        int c_intm[8][8];
        for (int u = 0; u < input.width(); u++) {
            for (int x = 0; x < 8; x++) {
                c_intm[u][x] = 0;
            }
        }
        for (int r_y = 0; r_y < input.height(); r_y++) {
            for (int u = 0; u < input.width() / 8; u++) {
                /* vectorize */ for (int u_i = 0; u_i < 8; u_i++) {
                    c_intm[u*4 + u_i][input(u*8 + u_i, r_y) / 32] += 1;
                }
            }
        }

        int c_result[8];
        for (int x = 0; x < 8; x++) {
            c_result[x] = 0;
        }
        for (int x = 0; x < 8; x++) {
            for (int r_x = 0; r_x < input.width(); r_x++) {
                c_result[x] += c_intm[r_x][x];
            }
        }

        // 檢查結果是否一致:
        for (int x = 0; x < 8; x++) {
            if (c_result[x] != halide_result(x)) {
                printf("halide_result(%d) = %d instead of %d\n",
                       x, halide_result(x), c_result[x]);
                return -1;
            }
        }
    }

    {
        // 也能夠一次對多維度的歸納範圍做分割
        // 這次將以 tile 方式對範圍做分割並計算部分 histogram
        Func histogram("hist_rfactor_tile");
        histogram(x) = 0;
        RDom r(0, input.width(), 0, input.height());
        histogram(input(r.x, r.y) / 32) += 1;

        // 首先以 4 為單位同時分割 r.x 與 r.y .
        RVar rx_outer("rx_outer"), rx_inner("rx_inner");
        RVar ry_outer("ry_outer"), ry_inner("ry_inner");
        histogram.update()
            .split(r.x, rx_outer, rx_inner, 4)
            .split(r.y, ry_outer, ry_inner, 4);

        // 接著呼叫 rfactor 來產生中介函式來獨立計算每個 tile 的 histogram
        Func intermediate = histogram.update().rfactor({{rx_outer, u}, {ry_outer, v}});

        // 現在對每個 tile 的中介做平行化
        intermediate.compute_root().update().parallel(u).parallel(v);

        // 亦能夠調整最外層 tile 索引的次序來做經典的 tile 尋訪
        intermediate.update().reorder(rx_inner, ry_inner, u, v);

        // 對初始做向量化
        intermediate.vectorize(x, 8);
        histogram.vectorize(x, 8);

        Buffer halide_result = histogram.realize(8);

        // 下圖為視覺化結果

        // 等義的 Code :
        int c_intm[4][4][8];
        for (int v = 0; v < input.height() / 2; v++) {
            for (int u = 0; u < input.width() / 2; u++) {
                for (int x = 0; x < 8; x++) {
                    c_intm[v][u][x] = 0;
                }
            }
        }
        /* parallel */ for (int v = 0; v < input.height() / 2; v++) {
            /* parallel */ for (int u = 0; u < input.width() / 2; u++) {
                for (int ry_inner = 0; ry_inner < 2; ry_inner++) {
                    for (int rx_inner = 0; rx_inner < 2; rx_inner++) {
                        c_intm[v][u][input(u*2 + rx_inner, v*2 + ry_inner) / 32] += 1;
                    }
                }
            }
        }

        int c_result[8];
        for (int x = 0; x < 8; x++) {
            c_result[x] = 0;
        }
        for (int x = 0; x < 8; x++) {
            for (int ry_outer = 0; ry_outer < input.height() / 2; ry_outer++) {
                for (int rx_outer = 0; rx_outer < input.width() / 2; rx_outer++) {
                    c_result[x] += c_intm[ry_outer][rx_outer][x];
                }
            }
        }

        // 確認結果是否一致:
        for (int x = 0; x < 8; x++) {
            if (c_result[x] != halide_result(x)) {
                printf("halide_result(%d) = %d instead of %d\n",
                       x, halide_result(x), c_result[x]);
                return -1;
            }
        }
    }

    printf("Success!\n");

    return 0;
}

2018年2月19日 星期一

Halide Tutorial 非官方中文翻譯 - Part 6

Part 6 涵蓋了 Tuple, Type 與 Generator
在早期版本的 Halide Tutorial 只有到此為止
而後續的版本加入了一些實務上實用的教學

Halide Tutorial 非官方中譯 - Part 1
Halide Tutorial 非官方中譯 - Part 2
Halide Tutorial 非官方中譯 - Part 3
Halide Tutorial 非官方中譯 - Part 4
Halide Tutorial 非官方中譯 - Part 5

==

Lesson 13 - Tuples

// 至今 Funcs 都用以對其範圍內每一個點計算單一純量數值(如同下列的一個)
Func single_valued;
Var x, y;
single_valued(x, y) = x + y;

// 撰寫回傳多個數值的集合的一個方法是增加集合額外的索引
// 這通常用以處理彩色影像.
// 像是下列的 Func 表示了對於每個 x, y 以 c 作索引的三數值之集合.
Func color_image;
Var c;
color_image(x, y, c) = select(c == 0, 245, // Red value
                              c == 1, 42,  // Green value
                              132);        // Blue value

// 這方法通常相當方便, 因為這讓 Func 對每個集合項目都同等地處理的使用上變簡單:
Func brighter;
brighter(x, y, c) = color_image(x, y, c) + 10;

// 然而這方法也有不方便的3個原因
//
// 1) Funcs 在無限制的區域上定義, 
// 因此 Func 能夠被用以存取像是 color_image(x, y, -17) 這樣無意義以及可能代表著錯誤的位置. 
//
// 2) 需要選擇, 若沒有限制或展開, 這能夠對效能產生影響:
// brighter.bound(c, 0, 3).unroll(c);
//
// 3) 使用這方法, 集合內所有的數值必須是相同型別. 
// 上述兩種方法僅只是不方便, 這點是個困難的限制造成無法以此方式表達特定事務.
// 亦是有可能以 Funcs 的集合來表示:
Func func_array[3];
func_array[0](x, y) = x + y;
func_array[1](x, y) = sin(x);
func_array[2](x, y) = cos(y);

// 使用 Func 集合來表示的確避免了上述的三個問題, 但是產生了新的惱人問題.
// 由於各自為分別的 Funcs, 因此難以在單一的 x,  loop 之中一同排程與計算.

// 一個第3點的替代方案是定義一個 Func 去計算 Tuple 而非 Expr.
// 一個 Tuple 是固定大小的 Expr 集合. 每個 Tuple 中的 Expr 可能有著不同的型別.
// 接著的函數計算一個整數數值 (x+y) 以及浮點數數值 (sin(x*y)).
Func multi_valued;
multi_valued(x, y) = Tuple(x + y, sin(x * y));

// 實現一個以 Tuple 為回傳型態的 Func, 會回傳一個 Buffer 的集合.
// 這物件被稱為 Realization. 等義於存放 Buffer 物件的 std::vector:
{
    Realization r = multi_valued.realize(80, 60);
    assert(r.size() == 2);
    Buffer im0 = r[0];
    Buffer im1 = r[1];
    assert(im0(30, 40) == 30 + 40);
    assert(im1(30, 40) == sinf(30 * 40));
}

// 所有 Tuple 的內容都是在同一個範圍以相同的 loop 中所計算, 
// 但存入不同的配置記憶體區域. 等義的 C++ code 如下:
{
    int multi_valued_0[80*60];
    float multi_valued_1[80*60];
    for (int y = 0; y < 80; y++) {
        for (int x = 0; x < 60; x++) {
            multi_valued_0[x + 60*y] = x + y;
            multi_valued_1[x + 60*y] = sinf(x*y);
        }
    }
}

// 當以使用 AOT 方式編譯時, 
// 以 Tuple 為回傳值的 Func 會將計算填入多個不同的輸出 buffer_t 結構
// 這些會依序而形成下列函式型別:
// int multi_valued(...input buffers and params...,
//                  buffer_t *output_1, buffer_t *output_2);

// 如同先前所做的, 能夠藉由傳入多個 Exprs 到 Tuple 建構子來建構一個 Tuple.
// 但或許能優雅地藉助 C++11 初始化列表(initializer lists) 的特性,
// 直接將 Exprs 以大括號包起來:
Func multi_valued_2;
multi_valued_2(x, y) = {x + y, sin(x*y)};

// 並不能如同 Exprs 來呼叫多回傳值 Func. 下列是個語法錯誤:
// Func consumer;
// consumer(x, y) = multi_valued_2(x, y) + 10;

// 因此必須以中括號索引 Tuple 中不同的 Expr:
Expr integer_part = multi_valued_2(x, y)[0];
Expr floating_part = multi_valued_2(x, y)[1];
Func consumer;
consumer(x, y) = {integer_part + 10, floating_part + 10.0f};

// Tuple 歸納.
{
    // Tuples 在歸納中特別有用, 這使得在範圍中歸納得以維持複雜的狀態.
    // 最簡單的例子是 argmax.
    // 首先建立一個後續將用於 argmax 的 buffer
    Func input_func;
    input_func(x) = sin(x);
    Buffer input = input_func.realize(100);

    // 接著定義有著兩個數值的 Tuple 用以追蹤最大值與其數值.
    Func arg_max;

    // 純定義
    arg_max() = {0, input(0)};

    // 更新定義
    RDom r(1, 99);
    Expr old_index = arg_max()[0];
    Expr old_max   = arg_max()[1];
    Expr new_index = select(old_max < input(r), r, old_index);
    Expr new_max   = max(input(r), old_max);
    arg_max() = {new_index, new_max};

    // 等義 C++ code:
    int arg_max_0 = 0;
    float arg_max_1 = input(0);
    for (int r = 1; r < 100; r++) {
        int old_index = arg_max_0;
        float old_max = arg_max_1;
        int new_index = old_max < input(r) ? r : old_index;
        float new_max = std::max(input(r), old_max);
        // 在 tuple 更新定義中, 所有的載入與計算都在存入前完成,
        // 所以所有 Tuple 元素都是透過遞迴呼叫相同 Func 自動更新的。
        arg_max_0 = new_index;
        arg_max_1 = new_max;
    }

    // 驗證 Halide 與 C++ 找到相同的最大值與索引值
    {
        Realization r = arg_max.realize();
        Buffer r0 = r[0];
        Buffer r1 = r[1];
        assert(arg_max_0 == r0(0));
        assert(arg_max_1 == r1(0));
    }

    // 如同 sum, product, maximum 與 minimum
    // Halide 提供的 argmax 與 argmin 是內建的歸納函式
    // 其在歸納的範圍內回傳對應於其值與各點的本身數值的 Tuple
    // 在這例子中, 回傳找到的第一個數值.
    // 在後續將使用其中之一
}

// 使用者自定義的 Tuples
{
    // Tuples 也是個表示複雜物件的方式, 像是複數.
    // 定義一個能雙向轉換 Tuple 的物件, 
    // 是一個以使用者自定型別延伸 Halide 型別系統的方式.
    struct Complex {
        Expr real, imag;

        // 以 Tuple 來建構
        Complex(Tuple t) : real(t[0]), imag(t[1]) {}

        // 以 Expr Pair 來建構
        Complex(Expr r, Expr i) : real(r), imag(i) {}

        // 以視為 Tuple 的 Func 呼叫來建構
        Complex(FuncRef t) : Complex(Tuple(t)) {}

        // 轉換到 Tuple
        operator Tuple() const {
            return {real, imag};
        }

        // 複數加法
        Complex operator+(const Complex &other) const {
            return {real + other.real, imag + other.imag};
        }

        // 複數乘法
        Complex operator*(const Complex &other) const {
            return {real * other.real - imag * other.imag,
                    real * other.imag + imag * other.real};
        }

        // 複數平方
        Expr magnitude_squared() const {
            return real * real + imag * imag;
        }

        // 能夠在此實作其他複數運算, 但上述對於此範例已足夠
    };

    // 接著使用複數結構來計算曼德博集合(Mandelbrot set)
    Func mandelbrot;

    // 在 Func 中依照 x, y 座標來初始複數數值
    Complex initial(x/15.0f - 2.5f, y/6.0f - 2.0f);

    // 純定義.
    Var t;
    mandelbrot(x, y, t) = Complex(0.0f, 0.0f);

    // 使用一個更新定義來處理 12 個步驟
    RDom r(1, 12);
    Complex current = mandelbrot(x, y, r-1);

    // 下行使用到先前定義的複數乘法與加法
    mandelbrot(x, y, r) = current*current + initial;

    // 使用另一個 Tuple 歸納來計算第一個能逃脫半徑為 4 的圓的迭代數
    // 這能以一個 boolean 型別的 argmin 表示 - 
    // - 需要第一個回傳為 false 的索引.(將 false 認為比 true 小)
    // 而 argmax 將會回傳第一個其表示值為 true 的索引.

    Expr escape_condition = Complex(mandelbrot(x, y, r)).magnitude_squared() < 16.0f;
    Tuple first_escape = argmin(escape_condition);

    // 這裡僅需要索引, 並不需要數值, 然而 argmin 回傳兩者,
    // 因此將以中括號方式自 argmin 的 Tuple 取出代表索引的 Expr
    Func escape;
    escape(x, y) = first_escape[0];

    // 實現此 pipeline 並印出結果.
    Buffer result = escape.realize(61, 25);
    const char *code = " .:-~*={}&%#@";
    for (int y = 0; y < result.height(); y++) {
        for (int x = 0; x < result.width(); x++) {
            printf("%c", code[result(x, y)]);
        }
        printf("\n");
    }
}

==

lesson 14: The Halide type system


// 所有的 Exprs 有著純量型別, 另外所有 Funcs 計算一或多個純量型別.
// 在 Halide 中純量型別為寬度不同的無號整數, 寬度相同的有號整數,
// 單精準與倍精準的浮點數以及黑盒子的 handle (等同 void *)
// 以下包含了所有合法型別:
Type valid_halide_types[] = {
    UInt(8), UInt(16), UInt(32), UInt(64),
    Int(8), Int(16), Int(32), Int(64),
    Float(32), Float(64), Handle()
};

// 建構與檢查型別:
{
    // 能夠以程式方式確認 Halide 型別的特性
    // 這對撰寫 C++ 有著 Expr 參數的函數, 又想要確認參數型別時很有用:
    assert(UInt(8).bits() == 8);
    assert(Int(8).is_int());

    // 亦能夠以程式方式建立如同函式或其他型別的型別
    Type t = UInt(8);
    t = t.with_bits(t.bits() * 2);
    assert(t == UInt(16));

    // 或是以 C++ 純量型別來建立型別
    assert(type_of<float>() == Float(32));

    // Type 結構也能夠表示 vector 型別, 但這被保留僅供 Halide 內部使用.
    // 必須使用 Func::vectorize 來向量化程式碼, 而非直接建構向量的表示方式.
    // 程式上操作低階 Halide 程式碼時可能會遭遇向量型別, 
    // 但這是進階的題目.(見 Func::add_custom_lowering_pass).

    // 能夠查詢任何 Halide Expr 的型別.
    // 一個代表著 Var 的 Expr 有著 Int(32) 的型別:
    assert(Expr(x).type() == Int(32));

    // 在 Halide 中多數的超越函數將輸入轉換為 Float(32) 也回傳 Float(32):
    assert(sin(x).type() == Float(32));

    // 能夠藉由使用轉換操作將 Expr 型別轉變為另一個型別:
    assert(cast(UInt(8), x).type() == UInt(8));

    // 這也是使用 C++ 型別的 template 型式
    assert(cast<uint8_t>(x).type() == UInt(8));

    // 也能夠查詢任何已定義的 Func 所產生的型別
    Func f1;
    f1(x) = cast<uint8_t>(x);
    assert(f1.output_types()[0] == UInt(8));

    Func f2;
    f2(x) = {x, sin(x)};
    assert(f2.output_types()[0] == Int(32) &&
           f2.output_types()[1] == Float(32));
}


// 型別提升規則(type promotion rules)
{
    // 當合併 Expr 與不同的型別時(像是 '+', '*' 等等), 
    // Halide 有著型別提升系統的規則. 
    // 與 C 的規則不同, 為了示範將會產生每個型別的 Expr
    Var x;
    Expr u8 = cast<uint8_t>(x);
    Expr u16 = cast<uint16_t>(x);
    Expr u32 = cast<uint32_t>(x);
    Expr u64 = cast<uint64_t>(x);
    Expr s8 = cast<int8_t>(x);
    Expr s16 = cast<int16_t>(x);
    Expr s32 = cast<int32_t>(x);
    Expr s64 = cast<int64_t>(x);
    Expr f32 = cast<float>(x);
    Expr f64 = cast<double>(x);

    // 規則如下, 並且以列出的次序所套用

    // 1) 對 Handle() 的 Expr 作型別轉換或是做計算都是錯誤

    // 2) 若型別相同, 不會有轉換發生
    for (Type t : valid_halide_types) {
        // Skip the handle type.
        if (t.is_handle()) continue;
        Expr e = cast(t, x);
        assert((e + e).type() == e.type());
    }

    // 3) 若一個型別為浮點數但另一個不是, 則浮點數參數將轉為浮點數
    // (可能對數值大的整數造成精確度降低)
    assert((u8 + f32).type() == Float(32));
    assert((f32 + s64).type() == Float(32));
    assert((u16 + f64).type() == Float(64));
    assert((f64 + s32).type() == Float(64));

    // 4) 若兩者為皆浮點數, 精準度較低的會被轉為較高的精準度
    assert((f64 + f32).type() == Float(64));

    // 上述原則用以處理所有浮點數的情況
    // 以下三個原則用來處理整數的情況

    // 5) 若參數其中之一為 C++ int, 而且另一個為 Halide::Expr
    // C++ int 將強制轉為表示式型別(champ: 也就是 Expr).
    assert((u32 + 3).type() == UInt(32));
    assert((3 + s16).type() == Int(16));

    // 若此原則造成整數 overflow, Halide 將會觸發一個錯誤
    // 例如: 移除下行的註解將會以錯誤結束程式
    // Expr bad = u8 + 257;

    // 6) 若兩個型別皆為無號整數或皆為有號整數, 精準度較低的將轉為較高的型別
    assert((u32 + u8).type() == UInt(32));
    assert((s16 + s64).type() == Int(64));

    // 7) If one type is signed and the other is unsigned, both
    // arguments are promoted to a signed integer with the greater of
    // the two bit widths.
    // 7) 若一個型別為有號, 另一為無號,
    // 兩者將會提升為精準度高於兩者的有號整數
    assert((u8 + s32).type() == Int(32));
    assert((u32 + s8).type() == Int(32));

    // 必須注意, 對於相同精準度的無號整數可能潛在性造成
    assert((u32 + s32).type() == Int(32));

    // 當一個無號 Expr 轉換為精準度更高的有號型別, is converted to a wider signed type in
    // 首先是先轉精準度更高的無號型別(zero-extended) 接著解釋為有號型別
    // 例如, 將數值為 255 的 UInt(8) 轉為 Int(32) 後數值為 255 而非 -1
    int32_t result32 = evaluate<int>(cast<uint32_t>(cast<uint8_t>(255)));
    assert(result32 == 255);

    // 當有號型別明確地以轉換操作轉為精準度更高的無號型別(對此提升規則不會自動處理), 
    // 會先轉為精準度更高的有號型別(sign-extended), 接著解釋為無號型別
    // 例如: 轉換 數值為 -1 的 Int(8) 到 Init(16) 其值為 65535 而非 255
    uint16_t result16 = evaluate<uint16_t>(cast<uint16_t>(cast<uint8_t>(-1)));
    assert(result16 == 65535);
}

// Handle() 型別
{
    // Handle 用來代表未知型別的 pointer, 
    // 對於任何 pointer 行別套用 type_of 都會回傳 Handle()
    assert(type_of<void *>() == Handle());
    assert(type_of<const char * const **>() == Handle());
    // 無論編譯的目標平台為何, Handles 皆以 64-bit 存放
    assert(Handle().bits() == 64);

    // Expr 型別的 Handle 主要用在透過 Halide 傳遞到外部的程式碼
}

// 泛型程式碼
{
    // 在 Halide 中明確使用型別主要用途在於參數的型別. 而在 C++ 中可能是透過模板. 
    // 在 Halide 中並不需要如此 - 能夠在 C++ 執行時期動態地檢查與修改型別
    // 下列定義的函式會對兩個相同型別的表示作平均運算
    Var x;
    assert(average(cast<float>(x), 3.0f).type() == Float(32));
    assert(average(x, 3).type() == Int(32));
    assert(average(cast<uint8_t>(x), cast<uint8_t>(3)).type() == UInt(8));
}

Expr average(Expr a, Expr b) {
    // 型別必須一致
    assert(a.type() == b.type());

    // 對於浮點數型別
    if (a.type().is_float()) {
        // 依照上方的規則3, 這裡的 '2' 將被提升為浮點數
        return (a + b)/2;
    }

    // 對於整數型別, 必須計算更高精準度的中間值以避免 overflow
    Type narrow = a.type();
    Type wider = narrow.with_bits(narrow.bits() * 2);
    a = cast(wider, a);
    b = cast(wider, b);
    return cast(narrow, (a + b)/2);
}

==

Lesson 15 - Generators (Part 1, Part 2)


#include "Halide.h"
#include <stdio.h>

using namespace Halide;

// Generators 是個更結構性方式來作 pipeline 的 ahead-of-time 編譯.
// 相較於在 Lesson 10 中是撰寫一個 int main() 與透過對等的指令介面,
// 這裡取而代之的是定義一個繼承自 Halide::Generator 的類別
class MyFirstGenerator : public Halide::Generator<MyFirstGenerator> {
public:
    // 這裡以 Input 宣告 pipeline 的輸入作為公開的成員變數
    // 其會以所宣告的次序出現在所產生的函數型別宣告中
    Input<uint8_t> offset{"offset"};
    Input<Buffer<uint8_t>> input{"input", 2};

    // 亦以 Output 宣告 pipeline 的輸出作為公開的成員變數
    Output<Buffer<uint8_t>> brighter{"brighter", 2};

    // 通常也會在這個範圍以 Var 宣告變數, 如此可以被用在之後所增添的函式中
    Var x, y;

    // 接著定義建構並回傳 pipeline 的函式:
    void generate() {
        // 在 Lesson 10 當中, 在此是呼叫 Func::compile_to_file
        // 而在 Generator 當中, 只需要去定義代表輸出 pipeline 的 Output
        brighter(x, y) = input(x, y) + offset;

        // 排程
        brighter.vectorize(x, 16).parallel(y);
    }
};

// 需搭配 tools/GenGen.cpp 來編譯此檔案. 
// GenGen.cpp 定義了提供 CLI 來產生使用自訂 Generator 的 "int main(...)"
// 需要告知該程式關於自定義 generator 的存在
// 這是由下列這行達成:
HALIDE_REGISTER_GENERATOR(MyFirstGenerator, my_first_generator)

// 此外, 能夠同時在一個檔案放入多個 Generator
// 特別是在一同放入的多個 Generator 有個共用的程式碼.
// 以下為更複雜的例子:
class MySecondGenerator : public Halide::Generator<MySecondGenerator> {
public:
    // 此 generator 也將會使用一些編譯時期參數
    // 如此能夠編譯產生 pipeline 不同的衍生結果
    // 這裡定義了是否告知要在排程中做平行化:
    GeneratorParam<bool> parallel{"parallel", /* default value */ true};

    // ... 而另一個表示將作為縮放係數的常數
    GeneratorParam<float> scale{"scale",
            1.0f /* default value */,
            0.0f /* minimum value */,
            100.0f /* maximum value */};

    // 對所有基本純量型別都可定義 GeneratorParams
    // 對於數值型別可以如同上面選擇性地提供最小與最大值

    // 亦能夠對 enums 定義為 GeneratorParam
    // 為了正確運作, 對此必須提供自字串到 enum 數值的對應
    enum class Rotation { None, Clockwise, CounterClockwise };
    GeneratorParam<Rotation> rotation{"rotation",
            /* default value */
            Rotation::None,
            /* map from names to values */
            {{ "none", Rotation::None },
             { "cw",   Rotation::Clockwise },
             { "ccw",  Rotation::CounterClockwise }}};

    // 使用如同先前相同的 Inputs:
    Input<uint8_t> offset{"offset"};
    Input<Buffer<uint8_t>> input{"input", 2};

    // 類似地以 Output 定義輸出. Note that we don't specify a type for the Buffer:
    // 請注意在編譯時期並沒有指定型別, 
    // 而是必須明確地透過 "output.type" 這個 GeneratorParam 來指定
    // (對此 Output 是被不明確地定義)
    Output<Buffer<>> output{"output", 2};

    // 另外如同先前以 Var 宣告了變數
    Var x, y;

    void generate() {
        // 定義此 Func. 如同執行時期的 offset 參數般使用編譯時期的縮放係數
        Func brighter;
        brighter(x, y) = scale * (input(x, y) + offset);

        // 取決於 enum 數值, 或許會做些旋轉的處
        // 為了得到 GeneratorParam 的數值而將其轉型為是當的型別
        // 該轉型在多數情況下潛在性地發生 (例如跟上面的縮放)

        Func rotated;
        switch ((Rotation)rotation) {
        case Rotation::None:
            rotated(x, y) = brighter(x, y);
            break;
        case Rotation::Clockwise:
            rotated(x, y) = brighter(y, 100-x);
            break;
        case Rotation::CounterClockwise:
            rotated(x, y) = brighter(100-y, x);
            break;
        }

        // 之後將其轉型為所需要的輸出型別
        output(x, y) = cast(output.type(), rotated(x, y));

        // 最終 pipeline 的結構取決於 generator params.
        // 排程亦然

        // 在此對輸出做向量化, 由於不知道型別, 因此難以挑出好的係數.
        // Generator 提供了一個稱為""natural_vector_size"輔助函式
        // 這輔助函式將對於型別與編譯的目標平台選出合理的係數
        output.vectorize(x, natural_vector_size(output.type()));

        // 依參數決定平行化:
        if (parallel) {
            output.parallel(y);
        }

        // 若有旋轉, 以輸出的 scanline 方式依照型別作向量化與排程
        if (rotation != Rotation::None) {
            rotated
                .compute_at(output, y)
                .vectorize(x, natural_vector_size(rotated.output_types()[0]));
        }
    }

};

// 註冊第二個 generator:
HALIDE_REGISTER_GENERATOR(MySecondGenerator, my_second_generator)


// 在編譯此檔案後, 閱讀下方 lesson_15_generators_build.sh 內容以得知如何使用
(暫時不翻譯)

# To run this script:
# bash lesson_15_generators_usage.sh

# First we define a helper function that checks that a file exists
check_file_exists()
{
    FILE=$1
    if [ ! -f $FILE ]; then
        echo $FILE not found
        exit -1
    fi
}

# And another helper function to check if a symbol exists in an object file
check_symbol()
{
    FILE=$1
    SYM=$2
    if !(nm $FILE | grep $SYM > /dev/null); then
        echo "$SYM not found in $FILE"
    exit -1
    fi
}

# Bail out on error
#set -e

# Set up LD_LIBRARY_PATH so that we can find libHalide.so
export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:../bin
export DYLD_LIBRARY_PATH=${DYLD_LIBRARY_PATH}:../bin

#########################
# Basic generator usage #
#########################

# First let's compile the first generator for the host system:
./lesson_15_generate -g my_first_generator -o . target=host

# That should create a pair of files in the current directory:
# "my_first_generator.a", and "my_first_generator.h", which define a
# function "my_first_generator" representing the compiled pipeline.

check_file_exists my_first_generator.a
check_file_exists my_first_generator.h
check_symbol my_first_generator.a my_first_generator

#####################
# Cross-compilation #
#####################

# We can also use a generator to compile object files for some other
# target. Let's cross-compile a windows 32-bit object file and header
# for the first generator:

./lesson_15_generate \
    -g my_first_generator \
    -f my_first_generator_win32 \
    -o . \
    target=x86-32-windows

# This generates a file called "my_first_generator_win32.lib" in the
# current directory, along with a matching header. The function
# defined is called "my_first_generator_win32".

check_file_exists my_first_generator_win32.lib
check_file_exists my_first_generator_win32.h

################################
# Generating pipeline variants #
################################

# The full set of command-line arguments to the generator binary are:

# -g generator_name : Selects which generator to run. If you only have
# one generator in your binary you can omit this.

# -o directory : Specifies which directory to create the outputs
# in. Usually a build directory.

# -f name : Specifies the name of the generated function. If you omit
# this, it defaults to the generator name.

# -n file_base_name : Specifies the basename of the generated file(s). If
# you omit this, it defaults to the name of the generated function.

# -e static_library,o,h,assembly,bitcode,stmt,html: A list of
# comma-separated values specifying outputs to create. The default is
# "static_library,h". "assembly" generates assembly equivalent to the
# generated object file. "bitcode" generates llvm bitcode for the pipeline.
# "stmt" generates human-readable pseudocode for the pipeline (similar to
# setting HL_DEBUG_CODEGEN). "html" generates an html version of the
# pseudocode, which can be much nicer to read than the raw .stmt file.

# -r file_base_name : Specifies that the generator should create a
# standalone file for just the runtime. For use when generating multiple
# pipelines from a single generator, to be linked together in one
# executable. See example below.

# -x .old=new,.old2=.new2,... : A comma-separated list of file extension
# pairs to substitute during file naming.

# target=... : The target to compile for.

# my_generator_param=value : The value of your generator params.

# Let's now generate some human-readable pseudocode for the first
# generator:

./lesson_15_generate -g my_first_generator -e stmt -o . target=host

check_file_exists my_first_generator.stmt

# The second generator has generator params, which can be specified on
# the command-line after the target. Let's compile a few different variants:
./lesson_15_generate -g my_second_generator -f my_second_generator_1 -o . \
target=host parallel=false scale=3.0 rotation=ccw output.type=uint16

./lesson_15_generate -g my_second_generator -f my_second_generator_2 -o . \
target=host scale=9.0 rotation=ccw output.type=float32

./lesson_15_generate -g my_second_generator -f my_second_generator_3 -o . \
target=host parallel=false output.type=float64

check_file_exists my_second_generator_1.a
check_file_exists my_second_generator_1.h
check_symbol      my_second_generator_1.a my_second_generator_1
check_file_exists my_second_generator_2.a
check_file_exists my_second_generator_2.h
check_symbol      my_second_generator_2.a my_second_generator_2
check_file_exists my_second_generator_3.a
check_file_exists my_second_generator_3.h
check_symbol      my_second_generator_3.a my_second_generator_3

# Use of these generated object files and headers is exactly the same
# as in lesson 10.

######################
# The Halide runtime #
######################

# Each generated Halide object file contains a simple runtime that
# defines things like how to run a parallel for loop, how to launch a
# cuda program, etc. You can see this runtime in the generated object
# files.

echo "The halide runtime:"
nm my_second_generator_1.a | grep "[SWT] _\?halide_"

# Let's define some functions to check that the runtime exists in a file.
check_runtime()
{
    if !(nm $1 | grep "[TSW] _\?halide_" > /dev/null); then
        echo "Halide runtime not found in $1"
    exit -1
    fi
}

check_no_runtime()
{
    if nm $1 | grep "[TSW] _\?halide_" > /dev/null; then
        echo "Halide runtime found in $1"
    exit -1
    fi
}

# Declarations and documentation for these runtime functions are in
# HalideRuntime.h

# If you're compiling and linking multiple Halide pipelines, then the
# multiple copies of the runtime should combine into a single copy
# (via weak linkage). If you're compiling and linking for multiple
# different targets (e.g. avx and non-avx), then the runtimes might be
# different, and you can't control which copy of the runtime the
# linker selects.

# You can control this behavior explicitly by compiling your pipelines
# with the no_runtime target flag. Let's generate and link several
# different versions of the first pipeline for different x86 variants:

# (Note that we'll ask the generators to just give us object files ("-e o"), 
# instead of static libraries, so that we can easily link them all into a 
# single static library.)

./lesson_15_generate \
    -g my_first_generator \
    -f my_first_generator_basic \
    -e o,h \
    -o . \
    target=host-x86-64-no_runtime

./lesson_15_generate \
    -g my_first_generator \
    -f my_first_generator_sse41 \
    -e o,h \
    -o . \
    target=host-x86-64-sse41-no_runtime

./lesson_15_generate \
    -g my_first_generator \
    -f my_first_generator_avx \
    -e o,h \
    -o . \
    target=host-x86-64-avx-no_runtime

# These files don't contain the runtime
check_no_runtime my_first_generator_basic.o
check_symbol     my_first_generator_basic.o my_first_generator_basic
check_no_runtime my_first_generator_sse41.o
check_symbol     my_first_generator_sse41.o my_first_generator_sse41
check_no_runtime my_first_generator_avx.o
check_symbol     my_first_generator_avx.o my_first_generator_avx

# We can then use the generator to emit just the runtime:
./lesson_15_generate \
    -r halide_runtime_x86 \
    -e o,h \
    -o . \
    target=host-x86-64
check_runtime halide_runtime_x86.o

# Linking the standalone runtime with the three generated object files     
# gives us three versions of the pipeline for varying levels of x86,      
# combined with a single runtime that will work on nearly all x86     
# processors.
ar q my_first_generator_multi.a \
    my_first_generator_basic.o \
    my_first_generator_sse41.o \
    my_first_generator_avx.o \
    halide_runtime_x86.o

check_runtime my_first_generator_multi.a
check_symbol  my_first_generator_multi.a my_first_generator_basic
check_symbol  my_first_generator_multi.a my_first_generator_sse41
check_symbol  my_first_generator_multi.a my_first_generator_avx

echo "Success!"


在 ARM 平台上使用 Function Multi-Versioning (FMV) - 以使用 Android NDK 為例

Function Multi-Versioning (FMV) 過往的 CPU 發展歷程中, x86 平台由於因應各種應用需求的提出, 而陸陸續續加入了不同的指令集, 此外也可能因為針對市場做等級區隔, 支援的數量與種類也不等. 在 Linux 平台上這些 CPU 資訊可以透過...