2018年2月17日 星期六

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

Halide Tutorial 非官方中譯 - Part 1
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
}

沒有留言:

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

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