2018年2月13日 星期二

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

新的一年希望深入 Halide, 借助於抽象化的力量來提升優化的效率與能力
因次藉由複習與深入, 重新研讀了官方的 Tutorial
並且做了簡短的意譯, 一方面確認自己確實地了解, 一方面建立相關中文教學資源
==

Lesson 01 - 認識 Func, Vars 與 Expr

// Lesson 1 主要在於展示 Halide JIT compiler 對於影像的基本使用
// Halide 的使用僅需 include 的 header 檔為 Halide.h

// Func 物件所代表的是 pipeline stage.
// 用以定義每個 pixel 數值函數, 可被認為是被計算出的影像.
Halide::Func gradient;

// Var 物件主要是作為宣告用以定義一個 Func 的變數.
// 這個物件本身並沒有額外的意義.
Halide::Var x, y;

// 通常我們使用名為 x 與 y 的變數來代表影像中的座標.
// 若你習慣以行列思考方式思考, 那麼 x 即為行的索引, y 為列的索引

// Func 被以一個透過變數與函數所組成的 Expr 來定義, 並以變數表示任意整數座標.
// Var 已被適當地 overloading 所以 x + y 會轉變為 Expr 物件.
Halide::Expr e = x + y;

// 將定義增加到 Func 物件中, 在 x, y 座標的 pixel 將或有著 Expr 所表示的數值.
// 在等號左方我們定應了一些變數; 而右邊有著使用相同變數的 Expr.
gradient(x, y) = e;

// 接著藉由 JIT 編譯實作所定義的 pipeline 程式碼以"實現"這個 Func 並執行這個 pipeline.
// 必須要告知 Halide 用來決定 x, y 範圍的值域與影像的解析度.
// Halide.h 提供了能直接使用的基本的影像 C++ template.
// 在這個範例嘗試產生 800 x 600 的影像
Halide::Buffer<int32_t> output = gradient.realize(800, 600);

// Halide 有著型別推斷功能. Var 物件表示 32-bit 的整數
// 因此 x + y 亦表示著 32-bit 整數, 因此 32-bit gradient 定義的影像
// 當呼叫 realize 我們將得到 32-bit 的整數影像, Halide 的型別轉換方式等同於 C 語言

==

Lesson 02 - 影像處理

// 這節主要展示如何傳入輸入影像與做處理

// 首先載入我們想要調亮的輸入影像
Halide::Buffer<uint8_t> input = load_image("images/rgb.png");

// 接著定義用來表示 pipeline stage 的 Func 物件
Halide::Func brighter;

// 該 Func 將會使用三個參數, 分別表示在影像中的 position 與 color channel.
// Halide 將 color channel 視為影像額外的維度.
Halide::Var x, y, c;

// 通常我們可能將整個函數定義寫成一行. 這裡我們將其分開好能夠逐步解釋

// 對於每個輸入影像的 pixel 
Halide::Expr value = input(x, y, c);

// 型別轉為浮點數
value = Halide::cast
<float>(value);

// 將數值乘上 1.5 來調亮.
// Halide 將實數表示為 float 而非 double,
// 因此我們在常數後方加上了 f.
value = value * 1.5f;

// 將值域限制在小於 255, 如此我們不會在型別轉回 unsigned 8-bit 整數時造成 overflow.
value = Halide::min(value, 255.0f);

// 型別轉換回 unsigned 8-bit 整數
value = Halide::cast
<uint8_t>(value);

// 定義函數
brighter(x, y, c) = value;

// 等同於上述步驟的一行表示方式
// brighter(x, y, c) = Halide::cast
<uint8_t>(min(input(x, y, c) * 1.5f, 255));
// 於簡短版本:
// - 忽略了型別轉為 float, 因為乘上 1.5f 會自動轉換
// - 使用了整數常數作為第2參數, 因為轉為 float 時與第1參數相容
// - 呼叫 min 時省去不必要的 Halide::

// 記住至今所做的是在記憶體中建立 Halide 程式的表示.
// 程式還尚未處理任何的 pixel.
// 甚至還沒有編譯這個 Halide 程式

// 因此這裡實現這個 Func. 輸出影像的大小必須與輸入影像一致.
// 若只想要調亮一部份的輸入影像, 能夠只要求一個較小的大小範圍.
// 然而要求一個較大的大小 Halide 將會在執行時期丟出錯誤,
// 以此告知讀取超出輸入影像大小的範圍.
Halide::Buffer
<uint8_t> output = brighter.realize(input.width(), input.height(), input.channels());

// 將輸出存下來做檢查
save_image(output, "brighter.png");


==

Lesson 03 - 檢查所產生的程式

// 本節展示如何檢查 Halide compiler 所產生的程式碼

// 首先使用來自第一節使用的 pipeline 定義

// 這節是關於 debugging, 但不幸的是在 C++ 中物件並不知道它們自己的名字
// 因此對於我們而言將難以獨懂所產生的程式碼. 為了排除這樣的問題,
// 可以傳遞一個字串提供給 Func 與 Var 的建構子以利除錯
Func gradient("gradient");
Var x("x"), y("y");
gradient(x, y) = x + y;

// 實現這個函數來產生輸出影像. 這節我們只使用非常小的大小
Buffer output = gradient.realize(8, 8);

// 在這節中嘗試設定 HL_DEBUG_CODEGEN 環境變數為 1,
// 它將會印出不同 stage 的編譯結果與一份最後表示 pipeline 的 pseudocode

// 若將 HL_DEBUG_CODEGEN 設為愈大的數值, 則可以看到愈多 Halide 編譯的細節.
// 將 HL_DEBUG_CODEGEN 設為 2 將會顯示在每個 stage 的編譯顯示 Halide code
// 以及最後所產生的 llvm bitcode.

// Halide 也能產生支援標示語法與程式碼折疊的 HTML 版本的輸出.
// 這對於閱讀較大的 pipeline 比較方便.
// 對於這節能在執行範例後使用瀏覽器打開 gradient.html 檔案gradient.compile_to_lowered_stmt("gradient.html", {}, HTML);


==

Lesson 04  - 使用 tracing, print, 與 print_when 來除錯

Var x("x"), y("y");
// 當 Func 計算時印出數值
{
// 如同以往定義 gradient 函數
Func gradient("gradient");
gradient(x, y) = x + y;

// 告知 Halide, 希望收到所有計算的通知.
gradient.trace_stores();

// 以 8x8 大小來實現這個函數
printf("Evaluating gradient\n");
Buffer<int>output = gradient.realize(8, 8);
// Click to show output ...

// 對於每一次的gradeient(x, y)的數值計算都會印出

//如此就可以監看 Halide 的行為,首先嘗試原始的排程.
// 後續會使用以平行方式處理每條 scanline 的新版本
Func parallel_gradient("parallel_gradient");
parallel_gradient(x, y) = x + y;

// 追蹤平行處理版本函數
parallel_gradient.trace_stores();

// 至今只定義了演算法, 但是並沒有談論任何關於排程的事.
// 通常來說, 探索不同的排程方式並不改變演算的描述

//現在告知 Halide 使用平行的 for 迴圈處理 y 座標.
// 在 Linux 平台上執行時會使用 thread pool 與 task queue.
// 而在 OS X 上會使用 grand central dispatch. 對於結果來說是等義的.
parallel_gradient.parallel(y);

// 由於每條 scanline 是由不同的 thread 處理, 因此這次結果將亂序地印出.
// Thread 的數目取決於運作的系統,
// 然而在 Linux 上能夠藉由設定環境變數 HL_NUM_THREADS 來控制
printf("\nEvaluating parallel_gradient\n");
parallel_gradient.realize(8, 8);
// Click to show output ...
}

// 印出個別的 Expr
{
// trace_store() 僅能印出 Func 的數值.
// 有時候會需要檢查內部的表示式而非整個 Func.
// 內建的 print 可以封裝起任何的 Expr 並且在被計算時印出.

// 例如, 對於一些以兩個項目和形成的 Func 
Func f;
f(x, y) = sin(x) + cos(y);

// 若需要監視其中之一的項目, 可以如下使用 print 來僅將其封裝起來
Func g;
g(x, y) = sin(x) + print(cos(y));

printf("\nEvaluating sin(x) + cos(y), and just printing cos(y)\n");
g.realize(4, 4);
// Click to show output ...
}

// 印出額外的內容
{
// print 能使用多個參數, 在計算第一個參數時印所有的參數.
// 參數能夠是 Expr 或是常數字串. 這能夠用來印出除了數值外額外的內容.
Func f;
f(x, y) = sin(x) + print(cos(y), "<- 4="" and="" br="" context="" cos="" f.realize="" is="" more="" n="" nevaluating="" printing="" sin="" this="" when="" with="" x="" y="">// Click to show output ...

// 在像是上方跨越多行的分開表示式很有用,
// 這使得在除錯時能簡單地開關特定數值
Expr e = cos(y);

// 將下列移除註解符號來印出 cos(y) 的數值
// e = print(e, "<- this is cos(", y, ") when x =", x);
}

// 條件列印
{
// 條件印出訊息
// print 與 trace_store 能產生大量的輸出.
// 然而當在尋找少見的事件或是特定事件發生的 pixel, 這樣數量的訊息難以挖掘.
// 相對地, print_when 能夠用來處理條件下的 Expr 訊息輸出.
// 第一個參數為 boolean 型態的 Expr,
// 當該 Expr 數值為 true, 將傳回第二個參數, 並且印出所有參數.
// 若為 false, 則僅傳回第二個參數而不做訊息輸出
Func f;
Expr e = cos(y);
e = print_when(x == 37 && y == 42, e, "<- this is cos(y) at x, y == (37, 42)");
f(x, y) = sin(x) + e;
printf("\nEvaluating sin(x) + cos(y), and printing cos(y) at a single pixel\n");
f.realize(640, 480);
// Click to show output ...

// print_when 也能夠用來檢查你不預期的數值
Func g;
e = cos(y);
e = print_when(e < 0, e, "cos(y) < 0 at y ==", y);
g(x, y) = sin(x) + e;
printf("\nEvaluating sin(x) + cos(y), and printing whenever cos(y) < 0\n");
g.realize(4, 4);
// Click to show output ...
}

// 編譯時期印出表示式
{
// 上述的程式碼僅以數行方式建構 Halide Expr.
// 若以程式方式建構複雜的表示式, 會需要檢查建立的 Expr 是否如所想的一般.
// 這時能夠使用 C++ streams 的方式印出表示式本身
Var fizz("fizz"), buzz("buzz");
Expr e = 1;
for (int i = 2; i < 100; i++) {
    if (i % 3 == 0 && i % 5 == 0) e += fizz*buzz;
    else if (i % 3 == 0) e += fizz;
    else if (i % 5 == 0) e += buzz;
    else e += i;
}
std::cout << "Printing a complex Expr: " << e << "\n";
// Click to show output ...
}

沒有留言:

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

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