OpenCL C 内核(Kernel)
1. 内核(Kernel)的基本概念
内核函数:内核是一个特殊的函数,它在 OpenCL 设备上并行执行。当主机(CPU)程序发起执行请求时,这个函数会被大量的工作项(Work-Items)同时执行。
并行模型:OpenCL 使用 NDRange(N-Dimensional Range)模型来定义并行性。你可以把它想象成一个一维、二维或三维的网格,网格中的每一个点都是一个工作项,每个工作项都执行相同的内核代码,但通过不同的全局ID来区分彼此,从而处理不同的数据。
2. 内核函数的编写规则
一个标准的 OpenCL C 内核函数需要遵循以下规则:
使用
__kernel
关键字:这个限定符声明一个函数是内核函数,可以从主机端调用。返回值必须是
void
:内核函数不能有返回值。参数限制:所有参数都必须位于特定的地址空间(
__global
,__constant
,__local
,__private
)。指针必须明确指定地址空间。
3. 内核函数的参数:地址空间限定符
这是 OpenCL C 中最关键的概念之一,它指明了数据存储在设备内存的哪个部分。
限定符 | 描述 | 用途 | 类比(以 NVIDIA GPU 为例) |
---|---|---|---|
__global | 全局内存。所有工作项都可读写,但访问速度较慢。 | 用于输入和输出的大型数据缓冲区。 | 显存(DRAM) |
__constant | 常量内存。只读,所有工作项可访问,缓存速度快。 | 用于在内核执行期间不会改变的只读数据(如查找表、配置参数)。 | 常量缓存 |
__local | 局部内存。工作组(Work-Group)内的工作项可共享的内存。速度比全局内存快。 | 用于工作组内部的通信和共享数据的临时存储。 | 共享内存 |
__private | 私有内存。每个工作项的私有内存(默认)。 | 用于函数内部的局部变量和寄存器溢出。 | 寄存器和线程私有内存 |
示例:
c
__kernel void myKernel(__global const float* inputA, // 输入缓冲区 A,只读__global const float* inputB, // 输入缓冲区 B,只读__global float* output, // 输出缓冲区__constant float* coefficients, // 常量参数(如滤波器系数)__local float* sharedTemp, // 局部共享内存int arraySize // 一个标量值,默认为 private
) {// ... 内核代码 ...
}
4. 获取工作项标识
在内核内部,你需要知道当前是哪个工作项在执行,以便处理正确的数据。OpenCL 提供了内置函数来获取这些信息:
get_global_id(dim)
: 返回在 全局NDRange 中指定维度(0, 1, 2)的ID。get_local_id(dim)
: 返回在 工作组内部 指定维度的ID。get_group_id(dim)
: 返回当前工作组的 工作组ID。get_global_size(dim)
: 返回 全局NDRange 在指定维度的大小。get_local_size(dim)
: 返回 工作组 在指定维度的大小。
一维示例:向量加法
这是最经典的入门示例,演示了如何通过全局ID来映射数据。
c
// Kernel: Vector Addition (VecAdd)
// 每个工作项计算一个输出元素 output[i] = inputA[i] + inputB[i]__kernel void vecAdd(__global const float* a,__global const float* b,__global float* c)
{// 获取当前工作项的全局一维IDint gid = get_global_id(0);// 执行加法操作c[gid] = a[gid] + b[gid];
}
主机端需要确保启动的全局工作项数量(Global Work Size)至少等于向量的长度。
二维示例:图像处理(旋转、模糊等)
c
// Kernel: 图像处理(例如,每个工作项处理一个像素)__kernel void imageFilter(__global const uchar4* inputImage,__global uchar4* outputImage,int width,int height)
{// 获取当前工作项在2D网格中的坐标int x = get_global_id(0);int y = get_global_id(1);// 检查边界,防止越界if (x < width && y < height) {// 计算一维索引int idx = y * width + x;// 读取输入像素(例如,一个包含RGBA的4分量向量)uchar4 pixel = inputImage[idx];// 进行处理(例如,简单的颜色反转)uchar4 outputPixel;outputPixel.x = 255 - pixel.x; // RoutputPixel.y = 255 - pixel.y; // GoutputPixel.z = 255 - pixel.z; // BoutputPixel.w = pixel.w; // A (Alpha通道保持不变)// 写入输出像素outputImage[idx] = outputPixel;}
}
5. 使用局部内存(__local
)的工作组同步
局部内存允许一个工作组内的所有工作项高效地共享和协作。这通常需要配合屏障(Barrier) 来同步工作组内所有工作项的执行。
经典示例:并行归约(求和)
c
__kernel void sumReduction(__global const float* input,__global float* partialSums,__local float* localSums) // 局部内存,大小由主机在运行时指定
{int gid = get_global_id(0);int lid = get_local_id(0); // 工作组内的本地IDint groupId = get_group_id(0);// 将全局数据拷贝到局部内存localSums[lid] = input[gid];// 等待工作组内所有工作项都完成拷贝barrier(CLK_LOCAL_MEM_FENCE);// 在工作组内部进行归约求和for (int stride = get_local_size(0) / 2; stride > 0; stride >>= 1) {if (lid < stride) {localSums[lid] += localSums[lid + stride];}// 等待所有工作项完成这一轮的归约barrier(CLK_LOCAL_MEM_FENCE);}// 第一个工作项将工作组的局部求和结果写入全局内存if (lid == 0) {partialSums[groupId] = localSums[0];}
}
注意:barrier(CLK_LOCAL_MEM_FENCE)
确保所有工作项在执行到这一点时都已完成对局部内存的写入/读取操作。它是工作组内同步所必需的。
6. 其他重要特性
向量类型:OpenCL C 原生支持
float2
,float4
,int8
,char16
等向量类型,便于进行SIMD(单指令多数据)操作,提高数据并行效率。c
float4 a = (float4)(1.0f, 2.0f, 3.0f, 4.0f); float4 b = (float4)(5.0f, 6.0f, 7.0f, 8.0f); float4 c = a + b; // 一次性完成4个浮点数的加法
大量内置函数:OpenCL C 提供了丰富的数学函数(
sin
,cos
,exp
,sqrt
)、几何函数、整数函数、图像读取写入函数等,它们都针对设备进行了高度优化。图像对象:除了缓冲区(Buffer),OpenCL 还支持专门的图像对象(
image2d_t
,image3d_t
),配合read_imagef
,write_imagef
等函数使用,可以自动处理寻址、滤波等操作,非常适合图像处理。
7. 创建内核的完整流程
第1步:准备内核源代码
c
// 内核源代码字符串
const char* kernel_source =
"__kernel void vector_add(__global const float* a, \n"
" __global const float* b, \n"
" __global float* c) { \n"
" int i = get_global_id(0); \n"
" c[i] = a[i] + b[i]; \n"
"} \n"
"\n"
"__kernel void vector_mul(__global const float* a, \n"
" __global const float* b, \n"
" __global float* c) { \n"
" int i = get_global_id(0); \n"
" c[i] = a[i] * b[i]; \n"
"} \n";
第2步:从文件读取内核代码
c
char* read_kernel_from_file(const char* filename) {FILE* file = fopen(filename, "rb");if (!file) {printf("无法打开文件: %s\n", filename);return NULL;}fseek(file, 0, SEEK_END);long length = ftell(file);fseek(file, 0, SEEK_SET);char* buffer = (char*)malloc(length + 1);if (buffer) {fread(buffer, 1, length, file);buffer[length] = '\0';}fclose(file);return buffer;
}
8. 创建程序和内核对象
创建程序对象
c
cl_program create_program_from_source(cl_context context, const char* source) {cl_int err;// 从源代码创建程序cl_program program = clCreateProgramWithSource(context, 1, &source, NULL, &err);if (err != CL_SUCCESS) {printf("创建程序失败: %d\n", err);return NULL;}return program;
}// 从二进制创建程序
cl_program create_program_from_binary(cl_context context, cl_device_id device, const char* binary_file) {FILE* file = fopen(binary_file, "rb");if (!file) return NULL;fseek(file, 0, SEEK_END);long size = ftell(file);fseek(file, 0, SEEK_SET);unsigned char* binary = (unsigned char*)malloc(size);fread(binary, 1, size, file);fclose(file);size_t binary_size = size;cl_int binary_status;cl_int err;cl_program program = clCreateProgramWithBinary(context, 1, &device, &binary_size, (const unsigned char**)&binary, &binary_status, &err);free(binary);if (err != CL_SUCCESS) {printf("从二进制创建程序失败: %d\n", err);return NULL;}return program;
}
编译程序
c
cl_int build_program(cl_program program, cl_device_id device, const char* options) {cl_int err = clBuildProgram(program, 1, &device, options, NULL, NULL);if (err != CL_SUCCESS) {// 获取构建日志size_t log_size;clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size);char* log = (char*)malloc(log_size);clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, log, NULL);printf("编译错误:\n%s\n", log);free(log);}return err;
}
创建内核对象
c
cl_kernel create_kernel(cl_program program, const char* kernel_name) {cl_int err;cl_kernel kernel = clCreateKernel(program, kernel_name, &err);if (err != CL_SUCCESS) {printf("创建内核 %s 失败: %d\n", kernel_name, err);return NULL;}return kernel;
}// 创建多个内核
cl_kernel* create_all_kernels(cl_program program, cl_uint* num_kernels) {cl_int err;// 获取内核数量err = clCreateKernelsInProgram(program, 0, NULL, num_kernels);if (err != CL_SUCCESS) return NULL;// 创建所有内核cl_kernel* kernels = (cl_kernel*)malloc(*num_kernels * sizeof(cl_kernel));err = clCreateKernelsInProgram(program, *num_kernels, kernels, NULL);if (err != CL_SUCCESS) {free(kernels);return NULL;}return kernels;
}
9. 设置内核参数
设置缓冲区参数
c
cl_int set_kernel_args(cl_kernel kernel, cl_mem* buffers, int num_args) {cl_int err;for (int i = 0; i < num_args; i++) {err = clSetKernelArg(kernel, i, sizeof(cl_mem), &buffers[i]);if (err != CL_SUCCESS) {printf("设置内核参数 %d 失败: %d\n", i, err);return err;}}return CL_SUCCESS;
}
设置标量参数
c
cl_int set_kernel_scalar_args(cl_kernel kernel, int start_index, ...) {va_list args;va_start(args, start_index);cl_int err;int arg_index = start_index;while (1) {void* arg_value = va_arg(args, void*);size_t arg_size = va_arg(args, size_t);if (arg_value == NULL) break;err = clSetKernelArg(kernel, arg_index, arg_size, arg_value);if (err != CL_SUCCESS) {printf("设置标量参数 %d 失败: %d\n", arg_index, err);va_end(args);return err;}arg_index++;}va_end(args);return CL_SUCCESS;
}
设置本地内存参数
c
cl_int set_kernel_local_memory(cl_kernel kernel, int arg_index, size_t local_mem_size) {return clSetKernelArg(kernel, arg_index, local_mem_size, NULL);
}
5. 执行内核
基本执行函数
c
cl_int execute_kernel(cl_command_queue queue, cl_kernel kernel, size_t global_size, size_t local_size) {cl_event event;cl_int err;err = clEnqueueNDRangeKernel(queue, // 命令队列kernel, // 内核对象1, // 工作维度NULL, // 全局工作偏移量&global_size, // 全局工作大小&local_size, // 局部工作大小0, NULL, // 等待事件列表&event // 返回的事件对象);if (err == CL_SUCCESS) {clReleaseEvent(event); // 释放事件对象}return err;
}
多维执行
c
cl_int execute_kernel_2d(cl_command_queue queue, cl_kernel kernel,size_t global_x, size_t global_y,size_t local_x, size_t local_y) {cl_event event;cl_int err;size_t global_work_size[2] = {global_x, global_y};size_t local_work_size[2] = {local_x, local_y};err = clEnqueueNDRangeKernel(queue,kernel,2, // 二维工作NULL,global_work_size,local_work_size,0, NULL,&event);if (err == CL_SUCCESS) {clReleaseEvent(event);}return err;
}
带事件依赖的执行
c
cl_int execute_kernel_with_dependencies(cl_command_queue queue, cl_kernel kernel,size_t global_size, size_t local_size,cl_uint num_events, const cl_event* wait_events,cl_event* out_event) {return clEnqueueNDRangeKernel(queue,kernel,1,NULL,&global_size,&local_size,num_events,wait_events,out_event);
}
10. 内核信息查询
c
void print_kernel_info(cl_kernel kernel, cl_device_id device) {char kernel_name[128];cl_uint num_args;size_t work_group_size;size_t preferred_work_group_size_multiple;size_t private_mem_size;size_t local_mem_size;// 获取内核信息clGetKernelInfo(kernel, CL_KERNEL_FUNCTION_NAME, sizeof(kernel_name), kernel_name, NULL);clGetKernelInfo(kernel, CL_KERNEL_NUM_ARGS, sizeof(num_args), &num_args, NULL);// 获取内核工作组信息clGetKernelWorkGroupInfo(kernel, device, CL_KERNEL_WORK_GROUP_SIZE, sizeof(work_group_size), &work_group_size, NULL);clGetKernelWorkGroupInfo(kernel, device, CL_KERNEL_PREFERRED_WORK_GROUP_SIZE_MULTIPLE,sizeof(preferred_work_group_size_multiple), &preferred_work_group_size_multiple, NULL);clGetKernelWorkGroupInfo(kernel, device, CL_KERNEL_PRIVATE_MEM_SIZE,sizeof(private_mem_size), &private_mem_size, NULL);clGetKernelWorkGroupInfo(kernel, device, CL_KERNEL_LOCAL_MEM_SIZE,sizeof(local_mem_size), &local_mem_size, NULL);printf("内核信息:\n");printf(" 名称: %s\n", kernel_name);printf(" 参数数量: %u\n", num_args);printf(" 最大工作组大小: %zu\n", work_group_size);printf(" 首选工作组大小倍数: %zu\n", preferred_work_group_size_multiple);printf(" 私有内存大小: %zu bytes\n", private_mem_size);printf(" 本地内存大小: %zu bytes\n", local_mem_size);
}
11. 完整示例:向量加法
c
#include <CL/cl.h>
#include <stdio.h>
#include <stdlib.h>#define DATA_SIZE 1024const char* get_cl_error_string(cl_int error) {// 错误字符串映射函数return "错误代码";
}int main() {cl_int err;cl_platform_id platform;cl_device_id device;cl_context context;cl_command_queue queue;cl_program program;cl_kernel kernel;cl_mem input_a, input_b, output;// 1. 初始化 OpenCLclGetPlatformIDs(1, &platform, NULL);clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 1, &device, NULL);context = clCreateContext(NULL, 1, &device, NULL, NULL, &err);queue = clCreateCommandQueue(context, device, 0, &err);// 2. 内核源代码const char* kernel_source = "__kernel void vector_add(__global const float* a, \n"" __global const float* b, \n"" __global float* result) { \n"" int idx = get_global_id(0); \n"" if (idx < 1024) { \n"" result[idx] = a[idx] + b[idx]; \n"" } \n""} \n";// 3. 创建和编译程序program = clCreateProgramWithSource(context, 1, &kernel_source, NULL, &err);err = clBuildProgram(program, 1, &device, NULL, NULL, NULL);if (err != CL_SUCCESS) {size_t log_size;clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, 0, NULL, &log_size);char* log = (char*)malloc(log_size);clGetProgramBuildInfo(program, device, CL_PROGRAM_BUILD_LOG, log_size, log, NULL);printf("编译错误:\n%s\n", log);free(log);return 1;}// 4. 创建内核kernel = clCreateKernel(program, "vector_add", &err);// 5. 创建内存对象input_a = clCreateBuffer(context, CL_MEM_READ_ONLY, DATA_SIZE * sizeof(float), NULL, &err);input_b = clCreateBuffer(context, CL_MEM_READ_ONLY, DATA_SIZE * sizeof(float), NULL, &err);output = clCreateBuffer(context, CL_MEM_WRITE_ONLY, DATA_SIZE * sizeof(float), NULL, &err);// 6. 设置内核参数clSetKernelArg(kernel, 0, sizeof(cl_mem), &input_a);clSetKernelArg(kernel, 1, sizeof(cl_mem), &input_b);clSetKernelArg(kernel, 2, sizeof(cl_mem), &output);// 7. 准备数据float* a = (float*)malloc(DATA_SIZE * sizeof(float));float* b = (float*)malloc(DATA_SIZE * sizeof(float));float* results = (float*)malloc(DATA_SIZE * sizeof(float));for (int i = 0; i < DATA_SIZE; i++) {a[i] = (float)i;b[i] = (float)(i * 2);}// 8. 传输数据clEnqueueWriteBuffer(queue, input_a, CL_TRUE, 0, DATA_SIZE * sizeof(float), a, 0, NULL, NULL);clEnqueueWriteBuffer(queue, input_b, CL_TRUE, 0, DATA_SIZE * sizeof(float), b, 0, NULL, NULL);// 9. 执行内核size_t global_size = DATA_SIZE;size_t local_size = 64;clEnqueueNDRangeKernel(queue, kernel, 1, NULL, &global_size, &local_size, 0, NULL, NULL);// 10. 读取结果clEnqueueReadBuffer(queue, output, CL_TRUE, 0, DATA_SIZE * sizeof(float), results, 0, NULL, NULL);// 11. 验证结果int correct = 1;for (int i = 0; i < DATA_SIZE; i++) {if (results[i] != a[i] + b[i]) {correct = 0;break;}}printf("计算结果: %s\n", correct ? "正确" : "错误");// 12. 清理资源free(a);free(b);free(results);clReleaseMemObject(input_a);clReleaseMemObject(input_b);clReleaseMemObject(output);clReleaseKernel(kernel);clReleaseProgram(program);clReleaseCommandQueue(queue);clReleaseContext(context);return 0;
}
12. 高级内核特性
图像处理内核
c
// 图像处理内核示例
const char* image_kernel_source = "__kernel void image_filter(__read_only image2d_t input, \n"" __write_only image2d_t output, \n"" sampler_t sampler) { \n"" int2 coord = (int2)(get_global_id(0), get_global_id(1)); \n"" float4 pixel = read_imagef(input, sampler, coord); \n"" // 简单的灰度化处理 \n"" float gray = 0.299f * pixel.x + 0.587f * pixel.y + 0.114f * pixel.z; \n"" write_imagef(output, coord, (float4)(gray, gray, gray, 1.0f)); \n""} \n";
使用本地内存的矩阵乘法
c
const char* matrix_mul_kernel = "__kernel void matrix_mul(__global const float* A, \n"" __global const float* B, \n"" __global float* C, \n"" int width, int height, \n"" int common_dim, \n"" __local float* A_tile, \n"" __local float* B_tile) { \n"" int row = get_global_id(0); \n"" int col = get_global_id(1); \n"" int local_row = get_local_id(0); \n"" int local_col = get_local_id(1); \n"" int tile_size = get_local_size(0); \n"" \n"" float sum = 0.0f; \n"" for (int t = 0; t < common_dim; t += tile_size) { \n"" // 将数据加载到本地内存 \n"" A_tile[local_row * tile_size + local_col] = A[row * common_dim + t + local_col]; \n"" B_tile[local_row * tile_size + local_col] = B[(t + local_row) * width + col]; \n"" barrier(CLK_LOCAL_MEM_FENCE); \n"" \n"" // 计算 tile 内的乘积和 \n"" for (int k = 0; k < tile_size; k++) { \n"" sum += A_tile[local_row * tile_size + k] * \n"" B_tile[k * tile_size + local_col]; \n"" } \n"" barrier(CLK_LOCAL_MEM_FENCE); \n"" } \n"" \n"" C[row * width + col] = sum; \n""} \n";
13. 最佳实践
内核优化:使用本地内存减少全局内存访问
工作组大小:选择合适的工作组大小(通常是32的倍数)
内存对齐:确保数据访问模式是连续的
错误检查:检查每个 OpenCL 函数调用的返回值
资源释放:及时释放内核、程序等资源
编译选项:使用合适的编译优化选项
总结
函数 | 用途 | 说明 |
---|---|---|
clCreateProgramWithSource | 创建程序 | 从源代码创建程序对象 |
clCreateProgramWithBinary | 创建程序 | 从二进制创建程序对象 |
clBuildProgram | 编译程序 | 编译程序源代码 |
clCreateKernel | 创建内核 | 创建特定的内核对象 |
clCreateKernelsInProgram | 创建所有内核 | 创建程序中的所有内核 |
clSetKernelArg | 设置参数 | 设置内核参数值 |
clEnqueueNDRangeKernel | 执行内核 | 在设备上执行内核 |