31.1 概述
多设备上下文(Multiple Device Context)是OpenCL的核心特性之一,允许在单个上下文中管理多个设备,实现跨设备的内存共享和任务协调。本章基于OpenCL-CTS test_conformance/multiple_device_context/ 测试源码,介绍多设备上下文的创建、使用和测试方法。
31.2 多设备上下文基础
31.2.1 创建多设备上下文
基本用法:
c
cl_int err;
cl_platform_id platform;
cl_device_id devices[MAX_DEVICES];
cl_uint num_devices;
// 获取所有设备
clGetPlatformIDs(1, &platform, NULL);
clGetDeviceIDs(platform, CL_DEVICE_TYPE_ALL, MAX_DEVICES,
devices, &num_devices);
// 创建包含多个设备的上下文
cl_context context = clCreateContext(NULL, num_devices, devices,
NULL, NULL, &err);
指定特定设备类型:
c
cl_device_id gpu_devices[10];
cl_device_id cpu_devices[10];
cl_uint num_gpus, num_cpus;
// 获取GPU设备
clGetDeviceIDs(platform, CL_DEVICE_TYPE_GPU, 10,
gpu_devices, &num_gpus);
// 获取CPU设备
clGetDeviceIDs(platform, CL_DEVICE_TYPE_CPU, 10,
cpu_devices, &num_cpus);
// 创建混合设备上下文
cl_device_id all_devices[20];
memcpy(all_devices, gpu_devices, num_gpus * sizeof(cl_device_id));
memcpy(all_devices + num_gpus, cpu_devices, num_cpus * sizeof(cl_device_id));
cl_context context = clCreateContext(NULL, num_gpus + num_cpus,
all_devices, NULL, NULL, &err);
31.2.2 查询上下文设备
c
// 查询上下文包含的设备数量
cl_uint num_devices;
clGetContextInfo(context, CL_CONTEXT_NUM_DEVICES,
sizeof(num_devices), &num_devices, NULL);
// 查询设备列表
cl_device_id* devices = (cl_device_id*)malloc(
num_devices * sizeof(cl_device_id));
clGetContextInfo(context, CL_CONTEXT_DEVICES,
num_devices * sizeof(cl_device_id), devices, NULL);
// 打印设备信息
for (cl_uint i = 0; i < num_devices; i++) {
char device_name[128];
clGetDeviceInfo(devices[i], CL_DEVICE_NAME,
sizeof(device_name), device_name, NULL);
printf("Device %d: %s\n", i, device_name);
}
31.3 多设备内存对象
31.3.1 跨设备缓冲区共享
创建在所有设备上可访问的缓冲区:
c
size_t buffer_size = 1024 * 1024;
// 缓冲区在上下文创建时就关联了所有设备
cl_mem buffer = clCreateBuffer(context, CL_MEM_READ_WRITE,
buffer_size, NULL, &err);
// 可以在任何设备上访问
cl_command_queue queue1 = clCreateCommandQueue(context, devices[0], 0, &err);
cl_command_queue queue2 = clCreateCommandQueue(context, devices[1], 0, &err);
// 设备0写入数据
clEnqueueWriteBuffer(queue1, buffer, CL_TRUE, 0, buffer_size,
host_data, 0, NULL, NULL);
// 设备1读取数据
clEnqueueReadBuffer(queue2, buffer, CL_TRUE, 0, buffer_size,
host_result, 0, NULL, NULL);
31.3.2 设备间数据传输
直接在设备间复制:
c
cl_mem buffer1 = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &err);
cl_mem buffer2 = clCreateBuffer(context, CL_MEM_READ_WRITE, size, NULL, &err);
// 设备0上的队列写入buffer1
clEnqueueWriteBuffer(queue_dev0, buffer1, CL_TRUE, 0, size,
data, 0, NULL, NULL);
// 使用设备1的队列从buffer1复制到buffer2
clEnqueueCopyBuffer(queue_dev1, buffer1, buffer2, 0, 0, size,
0, NULL, NULL);
// 验证数据
clEnqueueReadBuffer(queue_dev1, buffer2, CL_TRUE, 0, size,
result, 0, NULL, NULL);
31.3.3 图像对象的多设备访问
c
cl_image_format format = { CL_RGBA, CL_UNSIGNED_INT8 };
cl_image_desc desc = {
.image_type = CL_MEM_OBJECT_IMAGE2D,
.image_width = 1024,
.image_height = 1024,
.image_depth = 0,
.image_array_size = 0,
.image_row_pitch = 0,
.image_slice_pitch = 0,
.num_mip_levels = 0,
.num_samples = 0,
.buffer = NULL
};
cl_mem image = clCreateImage(context, CL_MEM_READ_WRITE,
&format, &desc, NULL, &err);
// 多个设备可以访问同一图像
clEnqueueWriteImage(queue_dev0, image, CL_TRUE, origin, region,
0, 0, image_data, 0, NULL, NULL);
clEnqueueReadImage(queue_dev1, image, CL_TRUE, origin, region,
0, 0, output_data, 0, NULL, NULL);
31.4 CTS测试用例
31.4.1 test_multiple_devices
测试内容: 验证在包含多个设备的上下文中创建和使用内存对象。
测试步骤:
c
int test_multiple_devices(cl_device_id deviceID, cl_context context,
cl_command_queue queue, int num_elements)
{
// 1. 查询上下文中的设备数量
cl_uint num_devices;
clGetContextInfo(context, CL_CONTEXT_NUM_DEVICES,
sizeof(num_devices), &num_devices, NULL);
if (num_devices < 2) {
log_info("Test requires at least 2 devices\n");
return 0; // Skip test
}
// 2. 获取所有设备
cl_device_id* devices = malloc(num_devices * sizeof(cl_device_id));
clGetContextInfo(context, CL_CONTEXT_DEVICES,
num_devices * sizeof(cl_device_id), devices, NULL);
// 3. 为每个设备创建命令队列
cl_command_queue* queues = malloc(num_devices * sizeof(cl_command_queue));
for (cl_uint i = 0; i < num_devices; i++) {
queues[i] = clCreateCommandQueue(context, devices[i], 0, &err);
}
// 4. 创建共享缓冲区
size_t buffer_size = num_elements * sizeof(cl_int);
cl_mem buffer = clCreateBuffer(context, CL_MEM_READ_WRITE,
buffer_size, NULL, &err);
// 5. 在第一个设备上写入数据
cl_int* input_data = malloc(buffer_size);
for (int i = 0; i < num_elements; i++) {
input_data[i] = i;
}
clEnqueueWriteBuffer(queues[0], buffer, CL_TRUE, 0, buffer_size,
input_data, 0, NULL, NULL);
// 6. 在其他设备上读取并验证
for (cl_uint i = 1; i < num_devices; i++) {
cl_int* output_data = malloc(buffer_size);
clEnqueueReadBuffer(queues[i], buffer, CL_TRUE, 0, buffer_size,
output_data, 0, NULL, NULL);
// 验证数据
for (int j = 0; j < num_elements; j++) {
if (output_data[j] != input_data[j]) {
log_error("Data mismatch on device %d at index %d\n", i, j);
return -1;
}
}
free(output_data);
}
return 0;
}
31.4.2 test_multiple_contexts
测试内容: 验证多个独立上下文的正确性。
测试场景:
c
int test_multiple_contexts(cl_device_id deviceID, cl_context context,
cl_command_queue queue, int num_elements)
{
cl_int err;
// 创建多个独立上下文,每个包含一个设备
cl_context context1 = clCreateContext(NULL, 1, &devices[0],
NULL, NULL, &err);
cl_context context2 = clCreateContext(NULL, 1, &devices[1],
NULL, NULL, &err);
// 在各自上下文中创建缓冲区
cl_mem buffer1 = clCreateBuffer(context1, CL_MEM_READ_WRITE,
buffer_size, NULL, &err);
cl_mem buffer2 = clCreateBuffer(context2, CL_MEM_READ_WRITE,
buffer_size, NULL, &err);
// 缓冲区之间不能直接传输(不同上下文)
// 需要通过主机内存中转
cl_command_queue queue1 = clCreateCommandQueue(context1, devices[0], 0, &err);
cl_command_queue queue2 = clCreateCommandQueue(context2, devices[1], 0, &err);
// 设备1写入
clEnqueueWriteBuffer(queue1, buffer1, CL_TRUE, 0, buffer_size,
input_data, 0, NULL, NULL);
// 读到主机
void* host_buffer = malloc(buffer_size);
clEnqueueReadBuffer(queue1, buffer1, CL_TRUE, 0, buffer_size,
host_buffer, 0, NULL, NULL);
// 从主机写到设备2
clEnqueueWriteBuffer(queue2, buffer2, CL_TRUE, 0, buffer_size,
host_buffer, 0, NULL, NULL);
// 验证
clEnqueueReadBuffer(queue2, buffer2, CL_TRUE, 0, buffer_size,
output_data, 0, NULL, NULL);
// 清理
clReleaseMemObject(buffer1);
clReleaseMemObject(buffer2);
clReleaseContext(context1);
clReleaseContext(context2);
return 0;
}
31.5 跨设备内核执行
31.5.1 同一内核在多设备执行
c
// 创建程序对象(关联到多设备上下文)
cl_program program = clCreateProgramWithSource(context, 1,
&kernel_source, NULL, &err);
// 为所有设备构建
clBuildProgram(program, num_devices, devices, NULL, NULL, NULL);
// 创建内核对象
cl_kernel kernel = clCreateKernel(program, "my_kernel", &err);
// 在多个设备上并行执行
for (cl_uint i = 0; i < num_devices; i++) {
cl_mem device_buffer = clCreateBuffer(context, CL_MEM_READ_WRITE,
buffer_size, NULL, &err);
clSetKernelArg(kernel, 0, sizeof(cl_mem), &device_buffer);
size_t global_size = num_elements;
clEnqueueNDRangeKernel(queues[i], kernel, 1, NULL, &global_size,
NULL, 0, NULL, NULL);
}
// 等待所有设备完成
for (cl_uint i = 0; i < num_devices; i++) {
clFinish(queues[i]);
}
31.5.2 流水线式跨设备处理
c
// 生产者-消费者模式
cl_mem intermediate_buffer = clCreateBuffer(context, CL_MEM_READ_WRITE,
buffer_size, NULL, &err);
// 设备0: 预处理
cl_kernel preprocess = clCreateKernel(program, "preprocess", &err);
clSetKernelArg(preprocess, 0, sizeof(cl_mem), &input_buffer);
clSetKernelArg(preprocess, 1, sizeof(cl_mem), &intermediate_buffer);
clEnqueueNDRangeKernel(queue_dev0, preprocess, 1, NULL, &global_size,
NULL, 0, NULL, &event1);
// 设备1: 主处理(等待设备0完成)
cl_kernel process = clCreateKernel(program, "process", &err);
clSetKernelArg(process, 0, sizeof(cl_mem), &intermediate_buffer);
clSetKernelArg(process, 1, sizeof(cl_mem), &output_buffer);
clEnqueueNDRangeKernel(queue_dev1, process, 1, NULL, &global_size,
NULL, 1, &event1, &event2);
clWaitForEvents(1, &event2);
31.6 性能优化策略
31.6.1 负载均衡
按设备能力分配任务:
c
// 查询各设备的计算单元数
cl_uint compute_units[MAX_DEVICES];
for (cl_uint i = 0; i < num_devices; i++) {
clGetDeviceInfo(devices[i], CL_DEVICE_MAX_COMPUTE_UNITS,
sizeof(cl_uint), &compute_units[i], NULL);
}
// 计算总计算能力
cl_uint total_compute = 0;
for (cl_uint i = 0; i < num_devices; i++) {
total_compute += compute_units[i];
}
// 按比例分配工作量
for (cl_uint i = 0; i < num_devices; i++) {
size_t device_workload = (total_workload * compute_units[i]) / total_compute;
size_t offset = /* 计算偏移 */;
clEnqueueNDRangeKernel(queues[i], kernel, 1, &offset,
&device_workload, NULL, 0, NULL, NULL);
}
31.6.2 减少设备间通信
数据本地化:
c
// 为每个设备创建独立的输入/输出缓冲区
cl_mem* device_inputs = malloc(num_devices * sizeof(cl_mem));
cl_mem* device_outputs = malloc(num_devices * sizeof(cl_mem));
for (cl_uint i = 0; i < num_devices; i++) {
device_inputs[i] = clCreateBuffer(context, CL_MEM_READ_ONLY,
chunk_size, NULL, &err);
device_outputs[i] = clCreateBuffer(context, CL_MEM_WRITE_ONLY,
chunk_size, NULL, &err);
// 写入各自的输入数据
clEnqueueWriteBuffer(queues[i], device_inputs[i], CL_FALSE, 0,
chunk_size, &host_data[i * chunk_size],
0, NULL, NULL);
}
// 并行处理
for (cl_uint i = 0; i < num_devices; i++) {
clSetKernelArg(kernel, 0, sizeof(cl_mem), &device_inputs[i]);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &device_outputs[i]);
clEnqueueNDRangeKernel(queues[i], kernel, 1, NULL, &chunk_workload,
NULL, 0, NULL, NULL);
}
31.6.3 异步操作
c
// 使用事件进行细粒度同步
cl_event write_events[MAX_DEVICES];
cl_event kernel_events[MAX_DEVICES];
cl_event read_events[MAX_DEVICES];
// 异步写入
for (cl_uint i = 0; i < num_devices; i++) {
clEnqueueWriteBuffer(queues[i], buffers[i], CL_FALSE, 0, size,
input_data, 0, NULL, &write_events[i]);
}
// 异步执行(等待各自的写入完成)
for (cl_uint i = 0; i < num_devices; i++) {
clEnqueueNDRangeKernel(queues[i], kernel, 1, NULL, &global_size,
NULL, 1, &write_events[i], &kernel_events[i]);
}
// 异步读取(等待各自的内核完成)
for (cl_uint i = 0; i < num_devices; i++) {
clEnqueueReadBuffer(queues[i], buffers[i], CL_FALSE, 0, size,
output_data, 1, &kernel_events[i], &read_events[i]);
}
// 等待所有操作完成
clWaitForEvents(num_devices, read_events);
31.7 常见陷阱
31.7.1 设备能力差异
问题: 不同设备可能支持不同的特性。
c
// 检查所有设备是否支持所需特性
for (cl_uint i = 0; i < num_devices; i++) {
cl_bool image_support;
clGetDeviceInfo(devices[i], CL_DEVICE_IMAGE_SUPPORT,
sizeof(image_support), &image_support, NULL);
if (!image_support) {
log_error("Device %d does not support images\n", i);
return -1;
}
}
31.7.2 内存一致性
问题: 跨设备访问同一缓冲区时需要显式同步。
c
// 设备0写入
clEnqueueNDRangeKernel(queue_dev0, write_kernel, 1, NULL, &global_size,
NULL, 0, NULL, &write_event);
// 必须等待写入完成才能在设备1读取
clEnqueueNDRangeKernel(queue_dev1, read_kernel, 1, NULL, &global_size,
NULL, 1, &write_event, NULL);
31.7.3 上下文切换开销
问题: 频繁在多个上下文间切换会带来性能开销。
建议: 尽量使用单个多设备上下文,而非多个单设备上下文。
31.8 总结
多设备上下文是OpenCL的强大特性,支持:
- 资源共享: 内存对象在多设备间共享
- 并行执行: 充分利用系统中的所有计算资源
- 灵活调度: 根据设备能力分配任务
- 高效协作: 设备间可以直接传输数据
最佳实践:
- 使用单个多设备上下文代替多个单设备上下文
- 根据设备能力进行负载均衡
- 最小化设备间数据传输
- 使用异步操作和事件同步
- 验证所有设备的特性兼容性
CTS测试覆盖:
- 多设备上下文创建和查询
- 跨设备内存对象访问
- 多上下文隔离性验证
- 设备间数据传输正确性
多设备上下文使OpenCL应用能够充分发挥异构系统的全部潜力,实现更高的性能和吞吐量。