目录
opencl在gpu及cpu上运行的不同之处
在OpenCL中,为了在不同设备上执行相同的计算,代码可能会有一些细微差别。主要区别体现在以下几个方面:
-
获取设备:在调用
clGetDeviceIDs
函数时,您需要指定不同的设备类型来获取 GPU 和 CPU 设备。例如,使用CL_DEVICE_TYPE_GPU
参数获取 GPU 设备,使用CL_DEVICE_TYPE_CPU
参数获取 CPU 设备。 -
内核代码调优:由于 GPU 和 CPU 在体系结构上存在差异,可能需要在内核代码中进行一些调优,以最大程度地发挥设备的性能潜力。例如,使用适当的向量化、工作组大小等策略来优化 GPU 内核,而针对 CPU,则可以关注并行度和线程优化。
-
内存访问模式:GPU 和 CPU 的内存访问模式可能不同。GPU 通常更适合使用向量化操作和全局内存,而 CPU 则更适合使用缓存和单个线程的计算。因此,在代码中,您可能会使用不同的内存对象(如缓冲区、图像)以及不同的内存访问模式来适应不同的设备。
然而,大部分代码逻辑和结构在 GPU 和 CPU 上保持一致。许多工作流程和函数调用(如创建上下文、命令队列、程序、内核等)都是相同的。因此,在编写代码时,您可以使用条件语句或分离函数来处理不同的设备类型,并共享大部分相同的任务代码。
总体而言,OpenCL 提供了抽象层,允许您使用统一的代码在不同的设备上执行计算。了解设备的特性和限制,根据不同的设备类型进行适当的代码调优,可以帮助您最大化地利用 GPU 和 CPU 的计算能力。
opencl基本工作流程及概念
在OpenCL中,有以下基本概念和基本流程:
- 平台(Platform):平台代表一个计算环境,可以是具有不同特性和硬件配置的计算设备组合。平台是最高级别的概念,可以包含多个设备,如CPU、GPU、FPGA等。每个平台都由不同的供应商提供,并且具有自己的特点和支持的硬件。安装多个硬件厂商的OpenCL SDK(软件开发工具包)可以同时出现多个平台。每个硬件厂商的OpenCL SDK提供了特定的平台和设备支持,因此在安装多个SDK时,每个SDK都会创建自己的平台。
通过查询可用的平台列表,您可以看到多个硬件厂商的平台在同一个系统上同时存在。每个平台代表着具有不同特性和硬件配置的计算环境。您可以使用OpenCL函数进行平台和设备的查询,例如使用clGetPlatformIDs和clGetDeviceIDs函数。
-
设备(Device):属于平台的一部分,代表计算资源,可以是CPU、GPU或其他硬件设备。每个设备都有自己的计算能力和特性。
-
上下文(Context):关联平台和设备,提供对计算资源的访问。它是OpenCL函数调用的基础,包含了在设备上执行操作所需的环境信息。
-
命令队列(Command Queue):用于将操作发送到设备,以及管理操作的执行顺序。命令队列按顺序执行操作,并使操作之间具有相互依赖性。
-
缓冲区(Buffer):用于在主机和设备之间传输数据。可以在设备上分配和使用的一块内存区域。
-
程序(Program):由一个或多个内核函数组成的代码集合。内核函数是在设备上并行执行的任务,可以接受参数和访问设备上的内存。
基本的OpenCL工作流程如下:
-
查询和选择平台和设备:使用
clGetPlatformIDs
和clGetDeviceIDs
函数查询可用的平台和设备,并选择适合的平台和设备。 -
创建上下文:使用
clCreateContext
函数创建一个上下文,将选定的设备与平台相关联。 -
创建命令队列:使用
clCreateCommandQueue
函数创建一个命令队列,用于管理操作的执行。 -
创建内核程序:使用
clCreateProgramWithSource
或clCreateProgramWithBinary
函数创建一个程序,其中包含一个或多个内核函数的源代码或二进制代码。 -
编译和构建内核程序:使用
clBuildProgram
函数将程序编译成可执行形式。 -
创建内核对象:使用
clCreateKernel
函数创建一个内核对象,以便在设备上执行内核函数。 -
创建和操作缓冲区:使用
clCreateBuffer
函数创建缓冲区,并使用函数如clEnqueueReadBuffer
和clEnqueueWriteBuffer
在主机和设备之间传输数据。 -
设置内核参数:使用
clSetKernelArg
函数为内核函数设置参数。 -
将内核函数入队执行:使用
clEnqueueNDRangeKernel
函数将内核函数入队,在设备上并行执行。 -
等待操作完成并获取结果数据:使用
clFinish
函数等待所有命令执行完成,并使用函数如clEnqueueReadBuffer
将结果数据从设备读回主机。
以上是OpenCL中基本的概念和工作流程。根据具体的应用和需求,还可以进一步进行内存管理、事件处理和错误处理等操作。
两数相加示例
#include <stdio.h>
#include <stdlib.h>
#include <CL/cl.h>
#define SIZE 1024
int main() {
cl_platform_id platform;
cl_device_id device;
cl_context context;
cl_command_queue queue;
cl_program program;
cl_kernel kernel;
cl_mem bufferA, bufferB, bufferC;
int i;
int* A = (int*)malloc(sizeof(int) * SIZE);
int* B = (int*)malloc(sizeof(int) * SIZE);
int* C = (int*)malloc(sizeof(int) * SIZE);
// 初始化输入数据
for (i = 0; i < SIZE; i++) {
A[i] = i;
B[i] = i * 2;
}
// 创建并初始化 OpenCL 环境
clGetPlatformIDs(1, &platform, NULL);
//这里显示的设备类型非常关键,弄错返回结果都为0
clGetDeviceIDs(platform, CL_DEVICE_TYPE_CPU, 1, &device, NULL);
context = clCreateContext(NULL, 1, &device, NULL, NULL, NULL);
queue = clCreateCommandQueue(context, device, 0, NULL);
// 创建内存对象
bufferA = clCreateBuffer(context, CL_MEM_READ_ONLY, sizeof(int) * SIZE, NULL, NULL);
bufferB = clCreateBuffer(context, CL_MEM_READ_ONLY, sizeof(int) * SIZE, NULL, NULL);
bufferC = clCreateBuffer(context, CL_MEM_WRITE_ONLY, sizeof(int) * SIZE, NULL, NULL);
// 将传设备
clEnqueueWriteBuffer(queue, bufferA, CL_TRUE, 0, sizeof(int) * SIZE, A, 0, NULL, NULL);
clEnqueueWriteBuffer(queue, bufferB, CL_TRUE, 0, sizeof(int) * SIZE, B, 0, NULL, NULL);
// 创建和编译内核程序
//在 C/C++ 中,字符串可以跨行分开写,只要字符串在同一行中使用双引号括起来、没有分号或逗号等终止符号,并且后续行以双引号开头即可。这被称为 “字符串字面量拼接”。
const char* source =
"__kernel void vector_add(__global const int* a, __global const int* b, __global int* c) {"
" int i = get_global_id(0);"
" c[i] = a[i] + b[i];"
"}";
program = clCreateProgramWithSource(context, 1, &source, NULL, NULL);
clBuildProgram(program, 1, &device, NULL, NULL, NULL);
// 创建内核对象
kernel = clCreateKernel(program, "vector_add", NULL);
// 设置内核参数
clSetKernelArg(kernel, 0, sizeof(cl_mem), &bufferA);
clSetKernelArg(kernel, 1, sizeof(cl_mem), &bufferB);
clSetKernelArg(kernel, 2, sizeof(cl_mem), &bufferC);
// 执行内核
size_t globalSize[1] = { SIZE };
clEnqueueNDRangeKernel(queue, kernel, 1, NULL, globalSize, NULL, 0, NULL, NULL);
// 从设备获取计算结果
clEnqueueReadBuffer(queue, bufferC, CL_TRUE, 0, sizeof(int) * SIZE, C, 0, NULL, NULL);
// 打印结果
for (i = 0; i < SIZE; i++) {
printf("%d + %d = %d\n", A[i], B[i], C[i]);
}
// 清理资源
clReleaseMemObject(bufferA);
clReleaseMemObject(bufferB);
clReleaseMemObject(bufferC);
clReleaseKernel(kernel);
clReleaseProgram(program);
clReleaseCommandQueue(queue);
clReleaseContext(context);
free(A);
free(B);
free(C);
return 0;
}
相关知识点
利用标识符完成内存从host到device的自动复制
CL_MEM_WRITE_ONLY 是 OpenCL 中内存对象的标识符之一,用于指示该内存对象仅可用于写入操作。
当你为内存对象分配内存时,可以使用不同的标识符来指定内存对象的用途。CL_MEM_WRITE_ONLY 标识符表示该内存对象只能用于写入操作,而不能进行读取操作。这意味着你可以将数据写入该内存对象,但无法从中读取数据。
在上述示例代码中,我们使用了 CL_MEM_WRITE_ONLY 标识符来创建用于存储计算结果的 bufferResult 内存对象。这意味着我们可以将计算结果写入这个内存对象,但不能直接从中读取结果。要获得计算结果,我们需要使用 clEnqueueReadBuffer 函数将结果从设备内存传输到主机内存。
这样的标识符允许 OpenCL 进行一些优化,因为它知道内存对象只用于写入,可以进行相应的优化操作。这提供了更好的性能和效率。
也可以使用标志位的方式,避免显式内存分配:
// 创建内存对象
bufferA = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, sizeof(int)*SIZE, A, NULL);
bufferB = clCreateBuffer(context, CL_MEM_READ_WRITE | CL_MEM_COPY_HOST_PTR, sizeof(int)*SIZE, B, NULL);
bufferC = clCreateBuffer(context, CL_MEM_READ_WRITE, sizeof(int)*SIZE, NULL, NULL);
// // 将传设备
// clEnqueueWriteBuffer(queue, bufferA, CL_TRUE, 0, sizeof(int) * SIZE, A, 0, NULL, NULL);
// clEnqueueWriteBuffer(queue, bufferB, CL_TRUE, 0, sizeof(int) * SIZE, B, 0, NULL, NULL);
全局执行范围和本地执行范围
在 OpenCL 中,全局执行范围(global execution range)和本地执行范围(local execution range)是用于描述并行执行的工作项和工作组的两个不同概念。
全局执行范围是指在计算设备上并行执行的总工作项的数量,也可以理解为指定了工作项的数量和分布方式。它决定了整个并行计算的规模。每个工作项在全局范围内有一个唯一的全局标识符,可以通过内置函数 get_global_id 获取。
本地执行范围是指在计算设备上并行执行的工作组的数量和大小。工作组是一组相关的工作项,可以共享本地内存,并通过内置函数 get_local_id 获取工作组内的工作项的本地标识符。工作组通常用于在计算设备上的共享内存中进行协同计算和通信。
总的来说,全局执行范围确定了工作项的数量和全局标识符,而本地执行范围确定了工作组的数量和工作组中工作项的划分。
在设置执行范围时,可以通过 clEnqueueNDRangeKernel 或相应的函数参数来指定全局执行范围和本地执行范围的大小。如果不需要使用本地执行范围,可以将其设置为 NULL 或 0。
在 OpenCL 中,get_global_id(0) 是内建的函数,用于从全局执行范围获取当前工作项的全局唯一标识符。它是一个用于并行计算的内建函数,用于确定当前工作项在其所属的全局工作范围中的位置。
在上述代码中,get_global_id(0) 是在内核函数 vectorAdd 中使用的,表示获取当前工作项在全局范围的第一个维度(维度编号为0)上的索引。这个索引可以用于从输入数组 a 和 b 中获取相应的元素,并将计算结果存储到 result 数组中。
索引举例
例如,如果在执行内核函数时使用了 128 个工作项并且当前工作项的全局索引是 10,那么 get_global_id(0) 会返回 10,表示当前工作项在全局范围的第一个维度上的索引是 10。
例如,如果在执行内核函数时将全局执行范围设置为 128,那么在第一个维度上,工作项的索引值将从 0 到 127,总共有 128 个唯一的索引值。每个工作项可以通过 get_global_id(0) 函数获取其在该维度上的索引。
这行代码是使用 OpenCL 的 C++ 包装库(cl::CommandQueue 和 cl::NDRange)来执行内核函数的全局执行范围设置的示例。
queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(N), cl::NullRange, NULL, &event);
具体地说,这行代码的含义如下:
queue 是一个已经创建的用于执行命令的命令队列对象。
enqueueNDRangeKernel 是命令队列的一个成员函数,用于将内核函数添加到命令队列并指定其执行范围。
kernel 是要执行的内核函数对象。
cl::NullRange 表示在每个维度上执行范围的起始位置。在这个示例中,我们使用了空范围,因此从第一个维度的索引 0 开始。
cl::NDRange(N) 表示在每个维度上的执行范围大小。在这个示例中,我们只在第一个维度上执行,并且执行范围大小为 N。这意味着有 N 个工作项在第一个维度上执行内核函数。
cl::NullRange 表示在每个维度上的本地执行范围大小。在这个示例中,我们未指定本地执行范围。
NULL 表示事件依赖关系列表,用于指定在执行内核函数之前等待的事件。
&event 是一个用于接收生成的事件对象的指针。这个事件对象可以用来在需要的时候等待内核函数的执行完成或获取其他相关的信息。
总的来说,这行代码将内核函数 kernel 添加到 queue 的命令队列中,并通过 cl::NDRange(N) 设置了一个具有 N 个工作项的全局执行范围。
例1
如何用上面这些方法构建一个3,5,7的多维索引,在核函数内索引多维数组进行操作:
__kernel void my_kernel(__global float* array, const int N, const int M, const int P)
{
int globalIdX = get_global_id(0); // 第一个维度上的全局索引
int globalIdY = get_global_id(1); // 第二个维度上的全局索引
int globalIdZ = get_global_id(2); // 第三个维度上的全局索引
if (globalIdX < N && globalIdY < M && globalIdZ < P)
{
int index = globalIdX + N * globalIdY + N * M * globalIdZ;
array[index] = 0.0f; // 对多维数组进行操作
}
}
size_t globalSize[3] = {3, 5, 7}; // 全局执行范围的大小
cl::Kernel kernel(program, "my_kernel"); // 创建内核对象
cl::CommandQueue queue(context, device); // 创建命令队列
kernel.setArg(0, bufferArray); // 设置内核参数
kernel.setArg(1, N);
kernel.setArg(2, M);
kernel.setArg(3, P);
queue.enqueueNDRangeKernel(kernel, cl::NullRange, cl::NDRange(globalSize), cl::NullRange);
当引入局部工作组时,以1维的情况为例,划分如下:
以上设置共有16个工作组,每个工作组的局部ID有0,1,2,3四个。
__kernel void add_vec(__global int * data_in,
__global int *mem_global_id,
__global int *mem_local_id,
__global int *data_out,
int length)
{
int i,j;
int global_id;
int local_id;
global_id = get_global_id(0);
local_id = get_local_id(0);
mem_global_id[global_id] =global_id;
mem_local_id [global_id] = local_id;
for(i=0; i<length; i++)
{
data_out[i] =data_in[i]*2;
}
}
对于2维的情况:
cl_uint work_dim = 2;
size_t global_item_size[2] = {8, 8};
size_t local_item_size[2] = {2, 2};
/* Execute Data Parallel Kernel */
ret =clEnqueueNDRangeKernel(command_queue, kernel, work_dim, NULL,
global_item_size,local_item_size,
0,NULL, NULL);
一个最大池化的例子:
/**********************************************
function:max_pooling, 2*2
2018/05/24
**********************************************/
__kernelvoid pooling(__global int * data_in,
__global int *mem_global_id,
__global int *mem_local_id,
__global int *data_out,
int width)
{
int i,j;
int global_id_x, global_id_y;
int local_id_x, local_id_y;
global_id_x = get_global_id(0);
global_id_y = get_global_id(1);
local_id_x = get_local_id(0);
local_id_y = get_local_id(1);
}
opencl中的队列概念
在 OpenCL 中引入执行队列的概念是为了实现并行计算。执行队列是一种控制和管理内核函数执行的方式。
执行队列中包含了待执行的内核函数以及其对应的参数。通过将内核函数和参数插入到执行队列中,可以实现对内核函数的调度和并行执行。执行队列可以按顺序执行内核函数,也可以同时执行多个内核函数,从而提高计算性能。
执行队列还可用于控制内核函数的执行顺序、同步操作和事件管理。可以指定内核函数的执行顺序,确保它们按照特定的顺序执行。还可以使用事件来同步内核函数的执行,等待内核函数完成后再执行后续操作。
因此,执行队列的概念在 OpenCL 中的作用是为并行计算提供了一种有效的管理和调度方式,使得程序可以充分利用计算设备的并行能力,提高计算性能。
使用多个执行队列可以同时执行多个内核函数,从而更充分地利用计算设备的并行能力。每个执行队列可以独立地管理和调度内核函数的执行,可以有不同的执行顺序、参数和事件依赖关系。
通过合理地使用多个执行队列,可以充分发挥计算设备的并行性能,提高计算效率和吞吐量。但需要注意的是,在使用多个执行队列时,需要合理分配资源和处理内存访问冲突,以避免出现竞争条件和资源争用的问题。
总之,多个执行队列的使用可以提供更灵活的并行计算策略,并帮助优化计算性能和资源利用率。