7

我的内核的函数签名如下:

template< size_t S, typename Field, typename Type1, typename Type2>
void kernel(const Type1 arg1, const Type2 arg2, Field *results) {
// S is known at compile time
// Field might be float or double
// Type1 is an object holding data and also methods
// Type2 is an object holding data and also methods

// The computation start here

}

我知道可以使用 c++ 的功能子集来编写内核,使用 AMD 的 OpenCL 实现的扩展,但生成的代码仅限于在 AMD 卡上运行。

2.0 之前版本的 OpenCL 语言标准规范限制了程序员使用 C99 编写内核,我相信 2.1 和 2.2 版本尚未广泛用于 Linux 发行版。但是,我在这里发现 Boost::compute 在某种程度上允许在内核规范中使用 c++ 功能的子集。但是,不清楚是否可以使用 Boos::compute 实现上述代码片段中的内核签名。在多大程度上可以实现这样的内核?代码示例将不胜感激。

4

1 回答 1

1

TL; DR:是和不是。在某种程度上确实可以编写模板化内核,但它们的功能远不及对应的 CUDA。

我知道可以使用 c++ 的功能子集来编写内核,使用 AMD 的 OpenCL 实现的扩展,但生成的代码仅限于在 AMD 卡上运行。

它不仅限于在 AMD 卡上运行。它仅限于在 AMD 的 OpenCL 实现上编译。例如,它应该可以在 Intel CPU 上运行,只要它是在 AMD 的实现上编译的。

我在这里发现 Boost::compute 在某种程度上允许在内核规范中使用 c++ 特性的子集。但是,不清楚是否可以使用 Boos::compute 实现上述代码片段中的内核签名。

Boost.Compute 本质上是 OpenCL C API 之上的一个花哨的抽象层,使其更易于使用且使用起来不那么乏味,但它仍然让您可以完全访问底层 C API。这意味着,如果某些东西在 C API 中是可行的,那么理论上它在 Boost.Compute 中也应该是可行的。


由于 OpenCL 代码是在运行时编译的,因此在单独的传递中,您将无法像 CUDA 在编译时那样自动进行模板实例化。CUDA 编译器可以看到主机和设备代码,并且可以在整个调用图中进行适当的模板实例化,就好像它是一个单独的翻译单元一样。这在 OpenCL 中是不可能的,这在设计上是不可能的。

1. 您必须手动实例化您需要的所有可能的模板实例,修改它们的名称,然后分派到正确的实例。

2. 模板实例化中使用的所有类型也必须在 OpenCL 代码中定义。

这种限制使得 OpenCL 模板化内核并非完全无用,而且与 CUDA 相比也不是很实用。它们的主要目的是避免代码重复。

这种设计的另一个结果是内核模板模板参数列表中不允许非类型模板参数(至少据我所知,但我真的很想在这个问题上出错!)。这意味着您必须将内核模板的非类型模板参数降低为 arguments 之一的非类型模板参数。换句话说,转换一些看起来像这样的东西:

template<std::size_t Size, typename Thing>
void kernel(Thing t);

变成这样:

template<typename Size, typename Thing>
void kernel(Size* s, Thing t);

然后通过使用在精神上类似于std::integral_constant<std::size_t, 512>(或任何其他可以在整数常量上模板化的类型)作为第一个参数来区分不同的实例化。这里的指针只是避免需要在主机端定义大小类型的技巧(因为我们不关心它)。


免责声明:我的系统不支持 OpenCL,所以我无法测试下面的代码。它可能需要一些调整才能按预期工作。但是,它确实可以编译。

auto source = R"_cl_source_(
    // Type that holds a compile-time size.
    template<std::size_t Size>
    struct size_constant {
        static const std::size_t value = Size;
    };

    // Those should probably be defined somewhere else since
    // the host needs to know about them too.
    struct Thing1 {};
    struct Thing2 {};

    // Primary template, this is where you write your general code.
    template<typename Size, typename Field, typename Type1, typename Type2>
    kernel void generic_kernel(Size*, const Type1 arg1, const Type2 arg2, Field *results) {
        // S is known at compile time
        // Field might be float or double
        // Type1 is an object holding data and also methods
        // Type2 is an object holding data and also methods

        // The computation start here
        // for (std::size_t s = 0; s < Size::value; ++s)
        // ...
    }

    // Instantiate the template as many times as needed.
    // As you can see, this can very quickly become explosive in number of combinations.
    template __attribute__((mangled_name(kernel_512_float_thing1_thing2)))
    kernel void generic_kernel(size_constant<512>*, const Thing1, const Thing2, float*);

    template __attribute__((mangled_name(kernel_1024_float_thing1_thing2)))
    kernel void generic_kernel(size_constant<1024>*, const Thing1, const Thing2, float*);

    template __attribute__((mangled_name(kernel_1024_double_thing1_thing2)))
    kernel void generic_kernel(size_constant<1024>*, const Thing1, const Thing2, double*);
)_cl_source_";

namespace compute = boost::compute;

auto device = compute::system::default_device();
auto context = compute::context { device };
auto queue = compute::command_queue { context, device };

// Build the program.
auto program = compute::program::build_with_source(source, context, "-x clc++");

// Retrieve the kernel entry points.
auto kernel_512_float_thing1_thing2 = program.create_kernel("kernel_512_float_thing1_thing2");
auto kernel_1024_float_thing1_thing2 = program.create_kernel("kernel_1024_float_thing1_thing2");
auto kernel_1024_double_thing1_thing2 = program.create_kernel("kernel_1024_double_thing1_thing2");

// Now you can call these kernels like any other kernel.
// Remember: the first argument is just a dummy.
kernel_512_float_thing1_thing2.set_arg(0, sizeof(std::nullptr_t), nullptr);

// TODO: Set other arguments (not done in this example)

// Finally submit the kernel to the command queue.
auto global_work_size = 512;
auto local_work_size = 64;
queue.enqueue_1d_range_kernel(kernel_512_float_thing1_thing2, 0, global_work_size, local_work_size);

祝你好运,随时使用您的更改编辑这篇文章,以便其他人可以从中受益!

于 2018-02-16T06:29:02.130 回答