1. 程式人生 > 其它 >halide程式設計技術指南(連載七)

halide程式設計技術指南(連載七)

技術標籤:深度學習機器學習深度學習

本文是halide程式設計指南的連載,已同步至公眾號

第11章 交叉編譯

// 本課演示如何使用Halide作為交叉編譯器,從任何平臺生成任何平臺的程式碼。
// 在linux平臺, 你可以像這樣編譯和執行它:
// g++ lesson_11*.cpp -g -std=c++11 -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -lpthread -ldl -o lesson_11
// LD_LIBRARY_PATH=<path/to/libHalide.so> ./lesson_11
// 在 os x平臺:
// g++ lesson_11*.cpp -g -std=c++11 -I <path/to/Halide.h> -L <path/to/libHalide.so> -lHalide -o lesson_11
// DYLD_LIBRARY_PATH=<path/to/libHalide.dylib> ./lesson_11
// 如果您擁有整個halide源樹,在源樹的最上層目錄中,在shell還可以通過執行:
// make tutorial_lesson_11_cross_compilation
#include "Halide.h"
#include <stdio.h>
using namespace Halide;
int main(int argc, char **argv) {

    // 我們將定義我們在第10課中使用的簡單單階段管道。
    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);

    // 下一行是我們在第十課做的。它編譯一個適用於執行此程式系統的物件檔案。例如,如果您有sse4.1 x86 cpu的64位linux上編譯並執行此檔案,則生成的程式碼將適用於使用sse4.1的x86上的64位linux。
    brighter.compile_to_file("lesson_11_host", args, "brighter");

    // 我們還可以編譯適用於其他cpu和作業系統的物件檔案。您可以使用可選的第三個引數compile to file來執行此操作,該引數指定要編譯的目標。

    // 讓我們用它來編譯這個程式碼的32位arm android版本:
    Target target;
    target.os = Target::Android;                // 作業系統
    target.arch = Target::ARM;                  // CPU 架構
    target.bits = 32;                           // 位寬
    std::vector<Target::Feature> arm_features;  // 要設定的功能列表
    target.set_features(arm_features);
    // 然後我們將它作為最後一個引數傳遞給compile_to_file.
    brighter.compile_to_file("lesson_11_arm_32_android", args, "brighter", target);

    // 現在是一個用於64位x86的Windows物件檔案,帶有AVX和SSE 4.1:
    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);

    // 最後是蘋果32位ARM處理器A6的iOS mach-o物件檔案。它被用於iphone5。A6使用了一個稍微修改過的ARM架構,稱為ARMv7s。對蘋果64位ARM處理器的支援在llvm中是非常新的,而且仍然有點不穩定.
    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位arm android物件檔案以magic位元組開始:
    uint8_t arm_32_android_magic[] = {0x7f, 'E', 'L', 'F',  // ELF格式
                                      1,                    // 32-bit
                                      1,                    // 2的補碼小端
                                      1};                   //  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位windows物件檔案以16位值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位arm iOS mach-o檔案以以下位元組開始:
    uint32_t arm_32_ios_magic[] = {0xfeedface,  // Mach-o magic bytes
                                   12,          // CPU 型別是 ARM
                                   11,          // CPU 子型別是 ARMv7s
                                   1};          // 它是一個可重定位的物件檔案
    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整合到交叉編譯工具鏈中。在apps資料夾下的halide儲存庫中有幾個小例子。在這裡檢視Hello Android和Hello iOS:
    // https://github.com/halide/Halide/tree/master/apps/
    printf("Success!\n");
    return 0;
}

12使用GPU

// 本課程演示如何使用halide在使用OpenCL的GPU上執行程式碼.
// 在linux系統上,編譯並執行:
// g++ lesson_12*.cpp -g -std=c++11 -I <path/to/Halide.h> -I <path/to/tools/halide_image_io.h> -L <path/to/libHalide.so> -lHalide `libpng-config --cflags --ldflags` -ljpeg -lpthread -ldl -o lesson_12
// LD_LIBRARY_PATH=<path/to/libHalide.so> ./lesson_12
// 在os x:
// g++ lesson_12*.cpp -g -std=c++11 -I <path/to/Halide.h> -I <path/to/tools/halide_image_io.h> -L <path/to/libHalide.so> -lHalide `libpng-config --cflags --ldflags` -ljpeg -o lesson_12
// DYLD_LIBRARY_PATH=<path/to/libHalide.dylib> ./lesson_12
// 如果你有整個halide的原始碼,你可以在halide最頂層目錄 running:
//    make tutorial_lesson_12_using_the_gpu

#include <stdio.h>
#include "Halide.h"
// 包括一個時鐘做效能測試.
#include "clock.h"
// 包括一些用於載入PNG的支援程式碼.
#include "halide_image_io.h"
using namespace Halide;
using namespace Halide::Tools;

Target find_gpu_target();
// 定義一些 Vars .
Var x, y, c, i, ii, xo, yo, xi, yi;
// 我們想用幾種方法來排程管道,所以我們在一個類中定義管道,這樣我們就可以用不同的排程多次重新建立它。
class MyPipeline {public:
    Func lut, padded, padded16, sharpen, curved;
    Buffer<uint8_t> input;

    MyPipeline(Buffer<uint8_t> in)
        : input(in) {
        // 在本課中,我們將使用一個兩階段的管道,該管道將進行銳化,然後應用查詢表(LUT).

        // 首先,我們將定義LUT。它將是一條伽馬曲線.

        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位進行計算.
        padded16(x, y, c) = cast<uint16_t>(padded(x, y, c));

        // 接下來我們用一個五拍濾波器來銳化它.
        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));
    }

    // 現在我們定義了一些方法,這些方法為我們的管道提供了幾種不同的計劃。
    void schedule_for_cpu() {
        // 提前計算查詢表.
        lut.compute_root();

        // 計算最裡面的顏色通道。會有三個,然後展開。
        curved.reorder(c, x, y)
            .bound(c, 0, 3)
            .unroll(c);

        // 查詢表不能很好地向量化,所以只需在16條掃描線的切片中並行化曲線.
        Var yo, yi;
        curved.split(y, yo, yi, 16)
            .parallel(yo);

        // 根據需要計算曲線掃描線的銳化值.
        sharpen.compute_at(curved, yi);

        // 向量化銳化。它是16位的,所以我們將它向量化為8寬.
        sharpen.vectorize(x, 8);

        // 根據需要計算每個曲線掃描線的填充輸入,重用在同一條16條掃描線內計算的先前值。
        padded.store_at(curved, yo)
            .compute_at(curved, yi);

        // 同時對填充進行向量化。它是8位的,所以我們將向量化16寬。
        padded.vectorize(x, 16);

        // JIT編譯CPU的管道。
        Target target = get_host_target();
        curved.compile_jit(target);
    }

    // 現在是一個使用CUDA或OpenCL.
    bool schedule_for_gpu() {
        Target target = find_gpu_target();
        if (!target.has_gpu_feature()) {
            return false;
        }

        // 如果您希望看到所有由管道完成的OpenCL、Metal、CUDA或d3d12api呼叫,還可以啟用Debug標誌。這有助於確定哪些階段比較慢,或者CPU->GPU拷貝何時發生。不過,這會影響效能,所以我們將忽略它。
        //target.set_feature(Target::Debug);

        // 我們決定是否為每個函式單獨使用GPU。如果一個Func是在CPU上計算的,下一個Func是在GPU上計算的,那麼Halide會把它複製到引擎蓋下的GPU上。對於這個管道,沒有理由對任何階段使用CPU。Halide將在我們第一次執行管道時將輸入影象複製到GPU,並將其保留在那裡以便在後續執行中重用。

        // 和前面一樣,我們將在管道開始時計算一次LUT。
        lut.compute_root();

        // 讓我們在16個寬的一維執行緒塊中使用GPU計算查詢表。首先,我們將索引拆分為大小為16的塊:
        Var block, thread;
        lut.split(i, block, thread, 16);
        // 然後我們告訴cuda,我們的變數“block”和“thread”對應於cuda的塊和執行緒的概念,或者OpenCL的執行緒組和執行緒的概念.
        lut.gpu_blocks(block)
            .gpu_threads(thread);

        // 這是GPU上非常常見的排程模式,因此有一個速記:

        // lut.gpu_tile(i, block, thread, 16);

        // Func::gpu——tile的行為與Func::tile相同,只是它還指定tile座標對應於gpu塊,並且每個tile內的座標對應於gpu執行緒.

        // 計算最裡面的顏色通道。會有三個,然後展開.
        curved.reorder(c, x, y)
            .bound(c, 0, 3)
            .unroll(c);

        // 使用GPU計算2D 8x8塊中的曲線.
        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);

        // 我們將把銳化內聯到曲線中.

        // 根據需要計算每個GPU塊的填充輸入,將中間結果儲存在共享記憶體中。在上面的排程中,xo對應於GPU塊.
        padded.compute_at(curved, xo);

        //使用GPU執行緒作為填充輸入的x和y座標。
        padded.gpu_threads(x, y);

        // JIT編譯GPU的管道。預設情況下不啟用CUDA、OpenCL或Metal。我們必須構造一個目標物件,啟用其中一個,然後將該目標物件傳遞給編譯jit。否則你的CPU會很慢地假裝它是一個GPU,每個輸出畫素使用一個執行緒。
        printf("Target: %s\n", target.to_string().c_str());
        curved.compile_jit(target);

        return true;
    }

    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);
            }

            // 通過將緩衝區複製回CPU,強制任何GPU程式碼完成.
            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);
                    }
                }
            }
        }
    }};
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("Running pipeline on CPU:\n");
    MyPipeline p1(input);
    p1.schedule_for_cpu();
    p1.curved.realize(reference_output);

    printf("Running pipeline on GPU:\n");
    MyPipeline p2(input);
    bool has_gpu_target = p2.schedule_for_gpu();
    if (has_gpu_target) {
        printf("Testing GPU correctness:\n");
        p2.test_correctness(reference_output);
    } else {
        printf("No GPU target available on the host\n");
    }

    printf("Testing performance on CPU:\n");
    p1.test_performance();

    if (has_gpu_target) {
        printf("Testing performance on GPU:\n");
        p2.test_performance();
    }

    return 0;}
// 一個輔助函式,用於檢查主機上是否存在OpenCL、Metal或D3D12。
Target find_gpu_target() {
    // 從一個適合你執行這個的機器的目標開始。
    Target target = get_host_target();

    std::vector<Target::Feature> features_to_try;
    if (target.os == Target::Windows) {
        // 先嚐試D3D12 ; 不行的話就試試 OpenCL.
        if (sizeof(void*) == 8) {
            // D3D12計算機支援目前僅在64位系統上可用。
            features_to_try.push_back(Target::D3D12Compute);
        }
        features_to_try.push_back(Target::OpenCL);
    } else if (target.os == Target::OSX) {
        // osx不更新OpenCL驅動程式,所以它們很容易損壞。CUDA也將是一個很好的選擇在NVidia的GPU的機器.
        features_to_try.push_back(Target::Metal);
    } else {
        features_to_try.push_back(Target::OpenCL);
    }
    // 取消對以下行的註釋以嘗試CUDA::
    // features_to_try.push_back(Target::CUDA);

    for (Target::Feature f : features_to_try) {
        Target new_target = target.with_feature(f);
        if (host_supports_target_device(new_target)) {
            return new_target;
        }
    }

    printf("Requested GPU(s) are not supported. (Do you have the proper hardware and/or driver installed?)\n");
    return target;
}