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

 

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Halide 是一个开源的图像处理和计算机视觉 DSL(领域特定语言),其目的是让程序员更加轻松地编写高性能的图像处理代码。Halide 的特点是具有易于使用的语法、高性能的代码生成以及可移植性。 本系列文章将介绍 Halide 的基本语法和使用方法,并通过一些实例来演示如何使用 Halide 进行图像处理和计算机视觉任务。 第一篇文章将介绍 Halide 的基本概念和安装方法。 ## 什么是 Halide? Halide 是由丹尼尔·瑞德福(Daniel R. Johnson)和 Jonathan Ragan-Kelley 在 MIT 开发的一个开源项目。它是一个用于编写高性能图像处理和计算机视觉代码的 DSL。 Halide 的主要目标是使程序员能够使用一种简单易懂的语法编写高性能的代码,而无需了解 CPU 或 GPU 的细节。Halide 支持多种平台,包括 x86、ARM、MIPS 和 PowerPC 等 CPU,以及 NVIDIA、AMD 和 ARM 等 GPU。 Halide 的核心概念是“函数”。函数可以看作是一组描述了如何对输入数据进行处理的指令集合。这些指令可以被 Halide 编译成高效的 CPU 或 GPU 代码,并在运行时执行。 ## Halide 的安装方法 Halide 可以在 Linux、macOS 和 Windows 等操作系统上运行,并且支持多种编译器,包括 GCC、Clang 和 MSVC 等。下面是在 Ubuntu 20.04 上安装 Halide 的步骤: 1. 添加 Halide 的 PPA: ``` sudo add-apt-repository ppa:halide/ppa ``` 2. 更新软件包列表: ``` sudo apt-get update ``` 3. 安装 Halide: ``` sudo apt-get install libhalide-dev ``` 安装完成后,可以使用以下命令检查 Halide 是否已经安装成功: ``` pkg-config --cflags --libs Halide ``` 如果输出了一些 Halide 相关的信息,则表示 Halide 安装成功。 ## 总结 本篇文章介绍了 Halide 的基本概念和安装方法。下一篇文章将介绍 Halide 的基本语法和使用方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值