OpenCL C++ 内核(Kernel)
1. 内核是什么?
在 OpenCL 中:
内核 (Kernel):一个用 OpenCL C 语言编写的、可以在支持 OpenCL 的设备上执行的函数。它被标记为
__kernel
,是并行执行的单元。程序 (Program):包含一个或多个内核函数以及其他辅助函数(如
__constant
,__device
)的源代码或二进制文件的集合。
在 C++ API 中,这两个概念分别由 cl::Kernel
和 cl::Program
类表示。
2. 核心类
cl::Program
:管理内核源代码的编译和链接。cl::Kernel
:代表一个具体的、可执行的内核函数实例。用于设置参数和提交执行。
3. 创建内核的完整流程
第 1 步:准备内核源代码
内核代码是普通的字符串,可以用 C++ 的多行字符串字面量方便地编写。
cpp
const std::string kernel_code = R"(__kernel void vector_add(__global const float* a,__global const float* b,__global float* c) {int gid = get_global_id(0);c[gid] = a[gid] + b[gid];}__kernel void vector_mul(__global const float* a,__global const float* b,__global float* c) {int gid = get_global_id(0);c[gid] = a[gid] * b[gid];}
)";
第 2 步:创建程序对象
将源代码传入程序构造函数。
cpp
cl::Program program(context, kernel_code);
第 3 步:编译程序(构建)
调用 build()
方法编译程序。这一步非常重要,需要处理编译错误。
cpp
try {program.build(); // 为上下文中的所有设备编译
} catch (const cl::Error& e) {// 如果编译失败,获取详细的构建日志std::string build_log = program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(device);std::cerr << "构建错误:\n" << build_log << std::endl;throw e; // 重新抛出异常
}
第 4 步:创建内核对象
从已编译的程序中创建特定的内核。
cpp
// 创建 'vector_add' 内核
cl::Kernel kernel_add(program, "vector_add");// 创建 'vector_mul' 内核
cl::Kernel kernel_mul(program, "vector_mul");
注意:内核函数名(如 "vector_add"
)必须与代码中的 __kernel
函数名完全一致。
4. 设置内核参数
在执行内核之前,必须为其所有参数设置值。使用 cl::Kernel::setArg()
方法。
设置缓冲区 (Buffer) 参数
cpp
cl::Buffer buffer_a(context, CL_MEM_READ_ONLY, size);
cl::Buffer buffer_b(context, CL_MEM_READ_ONLY, size);
cl::Buffer buffer_c(context, CL_MEM_WRITE_ONLY, size);// 设置内核参数(按顺序,从 0 开始)
kernel_add.setArg(0, buffer_a); // 对应 __global const float* a
kernel_add.setArg(1, buffer_b); // 对应 __global const float* b
kernel_add.setArg(2, buffer_c); // 对应 __global float* c
设置标量值参数
cpp
__kernel void process_with_factor(__global float* data, float factor) {int gid = get_global_id(0);data[gid] *= factor;
}// C++ 端设置
float scale_factor = 2.5f;
cl::Kernel kernel(program, "process_with_factor");
kernel.setArg(0, buffer_data); // 第一个参数是缓冲区
kernel.setArg(1, scale_factor); // 第二个参数是标量值
设置本地内存参数
本地内存(Local Memory)是 GPU 上每个计算单元内部的高速共享内存。
cpp
__kernel void matrix_mult(__global const float* A,__global const float* B,__global float* C,__local float* A_tile,__local float* B_tile,int tile_size) {// ... 矩阵分块乘法实现
}// C++ 端设置
int tile_size = 16;
size_t local_mem_size = tile_size * tile_size * sizeof(float);cl::Kernel kernel(program, "matrix_mult");
kernel.setArg(0, buffer_A);
kernel.setArg(1, buffer_B);
kernel.setArg(2, buffer_C);
kernel.setArg(3, cl::Local(local_mem_size)); // 为 A_tile 分配本地内存
kernel.setArg(4, cl::Local(local_mem_size)); // 为 B_tile 分配本地内存
kernel.setArg(5, tile_size);
5. 执行内核
使用命令队列的 enqueueNDRangeKernel()
方法来提交内核执行。
cpp
// 定义执行范围(全局工作大小)
cl::NDRange global_size(ARRAY_SIZE); // 总共启动 ARRAY_SIZE 个工作项// 定义工作组大小(局部工作大小)
cl::NDRange local_size(64); // 每个工作组包含 64 个工作项// 提交内核执行
cl::Event event;
queue.enqueueNDRangeKernel(kernel_add, // 要执行的内核cl::NullRange, // 偏移量(通常为 0)global_size, // 全局工作项数量local_size, // 局部工作项数量(可选)nullptr, // 等待事件列表&event); // 返回的事件对象// 等待内核完成
event.wait();
执行范围详解:
全局工作大小 (Global Size):总共要创建的工作项(Work-Item)数量。
局部工作大小 (Local Size):每个工作组(Work-Group)中包含的工作项数量。必须是全局工作大小的约数。
偏移量 (Offset):全局工作项的起始索引,通常为
cl::NullRange
(即 0)。
6. 完整示例:向量加法
cpp
#include <CL/cl2.hpp>
#include <iostream>
#include <vector>
#include <string>const int ARRAY_SIZE = 10000;int main() {try {// 1. 获取平台和设备std::vector<cl::Platform> platforms;cl::Platform::get(&platforms);cl::Platform platform = platforms[0];std::vector<cl::Device> devices;platform.getDevices(CL_DEVICE_TYPE_GPU, &devices);cl::Device device = devices[0];// 2. 创建上下文和命令队列cl::Context context(device);cl::CommandQueue queue(context, device, CL_QUEUE_PROFILING_ENABLE);// 3. 准备内核源代码std::string kernel_code = R"(__kernel void vector_add(__global const float* a,__global const float* b,__global float* c) {int gid = get_global_id(0);if (gid < 10000) { // 边界检查c[gid] = a[gid] + b[gid];}})";// 4. 创建和构建程序cl::Program program(context, kernel_code);try {program.build();} catch (...) {std::string build_log = program.getBuildInfo<CL_PROGRAM_BUILD_LOG>(device);std::cerr << "构建日志:\n" << build_log << std::endl;throw;}// 5. 创建内核cl::Kernel kernel(program, "vector_add");// 6. 准备数据std::vector<float> a(ARRAY_SIZE, 1.0f);std::vector<float> b(ARRAY_SIZE, 2.0f);std::vector<float> c(ARRAY_SIZE, 0.0f);// 7. 创建缓冲区cl::Buffer buffer_a(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * ARRAY_SIZE, a.data());cl::Buffer buffer_b(context, CL_MEM_READ_ONLY | CL_MEM_COPY_HOST_PTR, sizeof(float) * ARRAY_SIZE, b.data());cl::Buffer buffer_c(context, CL_MEM_WRITE_ONLY, sizeof(float) * ARRAY_SIZE);// 8. 设置内核参数kernel.setArg(0, buffer_a);kernel.setArg(1, buffer_b);kernel.setArg(2, buffer_c);// 9. 执行内核cl::Event event;queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(ARRAY_SIZE), cl::NullRange,nullptr, &event);// 10. 读取结果queue.enqueueReadBuffer(buffer_c, CL_TRUE, 0, sizeof(float) * ARRAY_SIZE, c.data());// 11. 验证结果bool correct = true;for (int i = 0; i < 10; ++i) { // 只检查前10个元素if (std::abs(c[i] - 3.0f) > 1e-5f) {correct = false;break;}}std::cout << "向量加法结果: " << (correct ? "正确" : "错误") << std::endl;} catch (const cl::Error& e) {std::cerr << "OpenCL错误: " << e.what() << " (代码: " << e.err() << ")" << std::endl;return 1;}return 0;
}
7. 高级技巧和最佳实践
从文件读取内核代码
cpp
std::string read_kernel_from_file(const std::string& filename) {std::ifstream file(filename);if (!file.is_open()) {throw std::runtime_error("无法打开内核文件: " + filename);}return std::string((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
}// 使用
std::string kernel_code = read_kernel_from_file("kernels/my_kernel.cl");
cl::Program program(context, kernel_code);
使用预编译的二进制文件
cpp
// 获取已编译程序的二进制
std::vector<size_t> binary_sizes = program.getInfo<CL_PROGRAM_BINARY_SIZES>();
std::vector<char*> binaries = program.getInfo<CL_PROGRAM_BINARIES>();// 保存二进制到文件
std::ofstream bin_file("kernel.bin", std::ios::binary);
bin_file.write(binaries[0], binary_sizes[0]);
bin_file.close();// 从二进制文件创建程序
std::ifstream bin_file("kernel.bin", std::ios::binary);
std::vector<char> binary_data((std::istreambuf_iterator<char>(bin_file)), std::istreambuf_iterator<char>());
cl::Program::Binaries binaries_list = { binary_data };
cl::Program program(context, {device}, binaries_list);
program.build(); // 仍然需要build,但主要是链接
一次设置多个内核
cpp
// 创建多个相同功能的内核实例
std::vector<cl::Kernel> kernels;
for (int i = 0; i < 4; ++i) {kernels.emplace_back(program, "vector_add");// 为每个内核设置不同的参数...
}
总结
关键概念 | 描述 |
---|---|
cl::Program | 管理内核源代码的容器,负责编译。 |
cl::Kernel | 具体的可执行函数实例,用于设置参数和执行。 |
编译流程 | 源代码 → Program对象 → build() → Kernel对象 |
参数设置 | 使用 setArg() 按顺序设置缓冲区、标量值和本地内存参数。 |
内核执行 | 使用 enqueueNDRangeKernel() 指定全局和局部工作大小。 |
错误处理 | 务必检查构建日志 (getBuildInfo<CL_PROGRAM_BUILD_LOG> ) |