Halide Tutorial 非官方中譯 - Part 2
Halide Tutorial 非官方中譯 - Part 3
Halide Tutorial 非官方中譯 - Part 4
==
Lesson 10 - AOT Compilation (Part 1, Part 2)
/* champ:* 原 Tutorial 中將 AOT Compilation 分為兩個部份
* 一為定義用以產生 Ahead-of-Time (AOT) 編譯出 static library 的範例
* 再者為應用 AOT static library 的範例
* 即為下面依序的兩個 main 函式
*/
#include "Halide.h" #include <stdio.h> using namespace Halide; int main(int argc, char **argv) { // 定義簡單的 one-stage pipeline: Func brighter; Var x, y; // 此 pipeline 只有著單一的 scalar 參數. Param<uint8_t> offset; // 輸入一個 8-bit 灰階 buffer. // 第一個建構子參數提供 pixel 的類別, 第二個指定維度(並非 channel 數) // 對於灰階影像為 2, 對於彩色影像為 3, 對於輸入與輸出的最大維度為 4. ImageParam input(type_of<uint8_t>(), 2); // 若是透過 JIT 編譯方式, 只會有一個整數與一個 buffer, // 但這裡要將 pipeline 只做一次編譯然後套用到任何的參數. // 這裡需要能像 Expr 使用的 Param 物件, // 以及能像 Buffer 使用的 ImageParam 物件 // 定義此 Func brighter(x, y) = input(x, y) + offset; // 將其排程 brighter.vectorize(x, 16).parallel(y); // 這次將較於立即編譯與執行的 brighter.realize(...) 呼叫, // 將透過一個呼叫產生 static library 與 header files // // 對於透過 AOT 所編譯的程式碼, 需要明確宣告函數的參數 // 這個函數有著兩個參數. 參數通常是 Params 或 ImageParams brighter.compile_to_static_library("lesson_10_halide", {input, offset}, "brighter"); printf("Halide pipeline compiled, but not yet run.\n"); // 下列另一個 main 接續著這一節 return 0; } // 套用 AOT 結果相依於在執行上列的程式後所產生的 header #include "lesson_10_halide.h" // 在使用 AOT 所編譯的程式時, 依然想要使用 Halide::Buffer. // 由於這是僅需 HalideBuffer.h 類別, 因此明確地 include 進來, // 而無需 link libHalide #include "HalideBuffer.h" #include int main(int argc, char **argv) { // 請先看一下上面的 header(除非執行先前程式否則不會產生) // 下列為所產生的函式型別: // int brighter(halide_buffer_t *_input_buffer, uint8_t _offset, halide_buffer_t *_brighter_buffer); // 自 ImageParam 的輸入將轉變為指向 halide_buffer_t 的 pointers // 這結構在 Halide 中用以表示陣列型態的資料 // 除非自 C code 呼叫 Halide pipeline 函式程式碼, 否則不會需要直接使用 // Halide::Runtime::Buffer 只是 halide_buffer_t 簡單的包裝, // 並不以明確的方式轉為 halide_buffer_t * // 在這些欄位中將會傳遞 Halide::Runtime::Buffer 物件 // 對於已使用在 JIT 程式碼的 Halide::Buffer 類別, // 只是 Halide::Runtime::Buffer 類別所共用的 pointer, 共用著相同 API // 最後, brighter 回傳數值是個錯誤碼. 成功時其值為 0. // 產生一個用以輸入與輸出的 buffer. Halide::Runtime::Buffer<uint8_t> input(640, 480), output(640, 480); // Halide::Runtime::Buffer 也有不配置記憶體而是包裝既有存在資料的建構子 // 這用在當有需要使用的影像資料的時後 int offset = 5; int error = brighter(input, offset, output); if (error) { printf("Halide returned an error: %d\n", error); return -1; } // 檢查 filter 如同宣稱的運作 // 預期這 offset 會加到每個輸入的 pixel 上 for (int y = 0; y < 480; y++) { for (int x = 0; x < 640; x++) { uint8_t input_val = input(x, y); uint8_t output_val = output(x, y); uint8_t correct_val = input_val + offset; if (output_val != correct_val) { printf("output(%d, %d) was %d instead of %d\n", x, y, output_val, correct_val); return -1; } } } // 成功! printf("Success!\n"); return 0; } ==
Lesson 11 - Cross-compilation
#include "Halide.h" #include <stdio.h> using namespace Halide; int main(int argc, char **argv) { // 定義 Lesson 10 中使用的簡單 one-stage pipeline Func brighter; Var x, y; // 宣告參數 Param<uint8_t> offset; ImageParam input(type_of<uint8_t>(), 2); std::vector<Argument> args(2); args[0] = input; args[1] = offset; // 定義 Func. brighter(x, y) = input(x, y) + offset; // 排程 brighter.vectorize(x, 16).parallel(y); // 下列這行用在 Lesson 10 中, 這會編譯出適合系統上適合使用的物件檔 // 例如, 你在有著 sse 4.1 支援的 x86 平台上的 64-bit linux 中編譯與執行 // 所產生的檔案就適合 sse 4.1 支援的 x86 平台上的 64-bit linux brighter.compile_to_file("lesson_11_host", args, "brighter"); // 也能夠編譯出適合其他 CPU 與 OS 的物件檔 // 這是藉由 compile_to_file 的用以指定編譯目的系統的第3個參數 // 試著使用來編譯出 32-bit arm android 版本: Target target; target.os = Target::Android; // 作業系統 target.arch = Target::ARM; // CPU 架構 target.bits = 32; // 架構位元寬度 std::vector<Target::Feature> arm_features; // A list of features to set target.set_features(arm_features); // 帶入作為 compile_to_file 的最後參數 brighter.compile_to_file("lesson_11_arm_32_android", args, "brighter", target); // 現在試著產生支援 AVX 與 SSE 4.1 的 64-bit x86 Windows 物件檔: target.os = Target::Windows; target.arch = Target::X86; target.bits = 64; std::vector<Target::Feature> x86_features; x86_features.push_back(Target::AVX); x86_features.push_back(Target::SSE41); target.set_features(x86_features); brighter.compile_to_file("lesson_11_x86_64_windows", args, "brighter", target); // 最後是用於 iPhone 5 的 Apple 32-bit 處理器 A6 的 iOS mach-o 物件檔 // A6 使用一個稍微修改自 ARMv7 的架構. 這特性是藉由目標來欄位來指定 // 在 llvm 中對於 Apple 的 64-bit ARM 處理器的支援還很新, 還相當片斷 target.os = Target::IOS; target.arch = Target::ARM; target.bits = 32; std::vector<Target::Feature> armv7s_features; armv7s_features.push_back(Target::ARMv7s); target.set_features(armv7s_features); brighter.compile_to_file("lesson_11_arm_32_ios", args, "brighter", target); // 接著藉由開頭的數個位元來檢測這些檔案 // 32-bit arm android 物件檔案開頭的 magic bytes: uint8_t arm_32_android_magic[] = {0x7f, 'E', 'L', 'F', // ELF format 1, // 32-bit 1, // 2's complement little-endian 1}; // Current version of elf FILE *f = fopen("lesson_11_arm_32_android.o", "rb"); uint8_t header[32]; if (!f || fread(header, 32, 1, f) != 1) { printf("Object file not generated\n"); return -1; } fclose(f); if (memcmp(header, arm_32_android_magic, sizeof(arm_32_android_magic))) { printf("Unexpected header bytes in 32-bit arm object file.\n"); return -1; } // 64-bit windows 物件檔開頭的 magic 16-bit 數值 0x8664 // (推想代表著 x86-64) uint8_t win_64_magic[] = {0x64, 0x86}; f = fopen("lesson_11_x86_64_windows.obj", "rb"); if (!f || fread(header, 32, 1, f) != 1) { printf("Object file not generated\n"); return -1; } fclose(f); if (memcmp(header, win_64_magic, sizeof(win_64_magic))) { printf("Unexpected header bytes in 64-bit windows object file.\n"); return -1; } // 32-bit arm iOS mach-o 檔案以下列作為開頭的 magic bytes: uint32_t arm_32_ios_magic[] = {0xfeedface, // Mach-o magic bytes 12, // CPU type is ARM 11, // CPU subtype is ARMv7s 1}; // It's a relocatable object file. f = fopen("lesson_11_arm_32_ios.o", "rb"); if (!f || fread(header, 32, 1, f) != 1) { printf("Object file not generated\n"); return -1; } fclose(f); if (memcmp(header, arm_32_ios_magic, sizeof(arm_32_ios_magic))) { printf("Unexpected header bytes in 32-bit arm ios object file.\n"); return -1; } // 這些所產生的物件檔乍看對於這些目標平台似是而非. 在此將這些計為成功. // 而對於實際的應用需要去了解如何將 Halide 整合入跨平台編譯工具 // 有許多的範例在 Halide repository 中的 app 目錄 // 請到此參考 HelloAndroid 以及 HelloiOS: // https://github.com/halide/Halide/tree/master/apps/ printf("Success!\n"); return 0; } ==
Lesson 12 - 使用 GPU
#include "Halide.h" #include <stdio.h> using namespace Halide; // Include 支援載入 png 檔的程式碼 #include "halide_image_io.h" using namespace Halide::Tools; // Include clock 來做效能檢測 #include "clock.h" // 定義後續會使用到的 Vars Var x, y, c, i, ii, xo, yo, xi, yi; // 由於將以許多方式排程 pipeline, // 因此為了能夠反覆多次以不同排程建構, 這裡以類別的方式定義 class MyPipeline { public: Func lut, padded, padded16, sharpen, curved; Buffer<uint8_t> input; MyPipeline(Buffer<uint8_t> in) : input(in) { // 對於這節, 將使用先銳化後套用查表(LUT)的 two-stage pipeline // 首先定義 LUT, 這會是個 gamma 曲線. lut(i) = cast<uint8_t>(clamp(pow(i / 255.0f, 1.2f) * 255.0f, 0, 255)); // 對於輸入以邊界條件補強 padded(x, y, c) = input(clamp(x, 0, input.width()-1), clamp(y, 0, input.height()-1), c); // 在計算前轉型為 16-bit 精準度 padded16(x, y, c) = cast<uint16_t>(padded(x, y, c)); // 接著以 5-tap 濾波器做銳化 sharpen(x, y, c) = (padded16(x, y, c) * 2- (padded16(x - 1, y, c) + padded16(x, y - 1, c) + padded16(x + 1, y, c) + padded16(x, y + 1, c)) / 4); // 最後套用 LUT curved(x, y, c) = lut(sharpen(x, y, c)); } // 定義賦予 pipeline 不同排程的物件方法 void schedule_for_cpu() { // 事先計算所有的 LUT lut.compute_root(); // 於最內層中計算 color channels.
// 保證 channel 數為 3 並且以此作 unrolling curved.reorder(c, x, y) .bound(c, 0, 3) .unroll(c); // Look-up-tables 無法有效 vectorized, // 所以只做以 16 scanline 作分割的平行化 Var yo, yi; curved.split(y, yo, yi, 16) .parallel(yo); // 銳利計算只在需要的 scanline 中計算 sharpen.compute_at(curved, yi); // 對銳利做 vectorization. 由於為 16-bit 因此套用 8-wide vector. sharpen.vectorize(x, 8); // 計算 scanline 所需的輸入 padding, // 重複使用在 16 scanline 中先前已計算的數值 padded.store_at(curved, yo) .compute_at(curved, yi); // 亦對 padding 作 vectorization. 由於是 8-bit, 因此討用 16-wide vector padded.vectorize(x, 16); // 對於 CPU 平台, 以 JIT 編譯 pipeline. curved.compile_jit(); } // 使用會用到 CUDA 或 OpenCL 的排程 void schedule_for_gpu() { // 對於每個 Func 是否使用 GPU 的決定是獨立的 // 若一個 Func 是在 CPU 上計算而次一個是在 GPU 上計算 // Halide 是在底層是藉由 copy-to-gpu 的方式 // 對於如此的 pipeline 並沒有在任何 stage 上使用 CPU 的理由 // Halide 將會在第一次執行 pipeline 時複製輸入影像到 GPU, // 並且保留以利後續執行的重複使用 // 如同先前, 在 pipeline 開始之前事先計算 LUT 一次 lut.compute_root(); // 這裡使用 GPU 以 1D thread blocks 計算 look-up-table // 首先將 index 以 16 筆分為一個 block Var block, thread; lut.split(i, block, thread, 16); // 告知 Var 中的 'block' 與 'thread',
// 對應於 CUDA 中 blocks, threads 的概念 // 與 OpenCL 中 thread groups 與 threads 的概念 lut.gpu_blocks(block) .gpu_threads(thread); // 這在 GPU 上是很普遍的排程模式, 因此有著簡短表示: // lut.gpu_tile(i, block, thread, 16); // Func::gpu_tile 行為上如同 Func::tile, // 除了它指定了tile 座標對應的 GPU blocks, // 以及所有 tile 內的座標對應每個 GPU thread // 於最內層中計算 color channels. // 保證 channel 數為 3 並且以此作 unrolling curved.reorder(c, x, y) .bound(c, 0, 3) .unroll(c); // 透過 GPU 以 2D 8x8 tile 來計算 curved.gpu_tile(x, y, xo, yo, xi, yi, 8, 8); // 這等同於: // curved.tile(x, y, xo, yo, xi, yi, 8, 8) // .gpu_blocks(xo, yo) // .gpu_threads(xi, yi); // 將銳化以 inline 方式置於 curved 中. // 計算每個 GPU block 所需的輸入 padding, 將中間結果寫入共用記憶體 // 在此排程中 xo 對應到 GPU blocks padded.compute_at(curved, xo); // 對於 padded x, y 座標使用 GPU threads 來計算 padded.gpu_threads(x, y); // JIT-compile the pipeline for the GPU. CUDA, OpenCL, or // Metal are not enabled by default. We have to construct a // Target object, enable one of them, and then pass that // target object to compile_jit. Otherwise your CPU will very // slowly pretend it's a GPU, and use one thread per output // pixel. // 對 GPU, GPU, OpenCL 或 Metal 以 JIT 編譯輸出預設是關閉的 // 必須建構 Target 物件, 並且打開這些設定然後傳遞給 compile_jit. // 否則 CPU 將會偽裝為一個 GPU, 並對於每個 pixel 使用一個 thread // 以適合當前運作平台的目標開始 Target target = get_host_target(); // 取決於平台來打開 OpenCL 或 Metal 的支援. // OS X 不在更新其 OpenCL drivers, 所以預期是無法使用的 // 對於俱備 Nvidia GPU 的平台, CUDA 也是個好選擇 if (target.os == Target::OSX) { target.set_feature(Target::Metal); } else { target.set_feature(Target::OpenCL); } // 移除下列的註解以嘗試 CUDA // target.set_feature(Target::CUDA); // 若想要觀察 pipeline 中所有的 OpenCL, Metal, 或 CUDA API 的呼叫 // 必須先設定好 Debug flag, 這對於了解哪個 stage 緩慢很有用 // 或當 CPU -> GPU 複製發生的時候. 這對效能有相當頂想 // 所以這裡保持為註解的型式 // target.set_feature(Target::Debug); curved.compile_jit(target); } void test_performance() { // 測試 MyPipeline 的排程效能. Buffer<uint8_t> output(input.width(), input.height(), input.channels()); // 先執行一次濾波器來初始化 GPU 執行時期狀態. curved.realize(output); // 取效能最好的三次的時間 double best_time = 0.0; for (int i = 0; i < 3; i++) { double t1 = current_time(); // 執行濾波器 100 次 for (int j = 0; j < 100; j++) { curved.realize(output); } // 強制任何 GPU 程式結束以複製回 buffer 到 CPU. output.copy_to_host(); double t2 = current_time(); double elapsed = (t2 - t1)/100; if (i == 0 || elapsed < best_time) { best_time = elapsed; } } printf("%1.4f milliseconds\n", best_time); } void test_correctness(Buffer<uint8_t> reference_output) { Buffer<uint8_t> output = curved.realize(input.width(), input.height(), input.channels()); // 確認結果是否一致 for (int c = 0; c < input.channels(); c++) { for (int y = 0; y < input.height(); y++) { for (int x = 0; x < input.width(); x++) { if (output(x, y, c) != reference_output(x, y, c)) { printf("Mismatch between output (%d) and " "reference output (%d) at %d, %d, %d\n", output(x, y, c), reference_output(x, y, c), x, y, c); exit(-1); } } } } } }; bool have_opencl_or_metal(); int main(int argc, char **argv) { // 載入一張輸入影像 Buffer<uint8_t> input = load_image("images/rgb.png"); // 配置用以存放校正輸出的影像 Buffer<uint8_t> reference_output(input.width(), input.height(), input.channels()); printf("Testing performance on CPU:\n"); MyPipeline p1(input); p1.schedule_for_cpu(); p1.test_performance(); p1.curved.realize(reference_output); if (have_opencl_or_metal()) { printf("Testing performance on GPU:\n"); MyPipeline p2(input); p2.schedule_for_gpu(); p2.test_performance(); p2.test_correctness(reference_output); } else { printf("Not testing performance on GPU, " "because I can't find the opencl library\n"); } return 0; } // 一個用以確認 OpenCL 環境是否存在的輔助函數 #ifdef _WIN32 #include#else #include #endif bool have_opencl_or_metal() { #ifdef _WIN32 return LoadLibrary("OpenCL.dll") != NULL; #elif __APPLE__ return dlopen("/System/Library/Frameworks/Metal.framework/Versions/Current/Metal", RTLD_LAZY) != NULL; #else return dlopen("libOpenCL.so", RTLD_LAZY) != NULL; #endif }
沒有留言:
張貼留言