GPU并行计算OpenCL(1)——helloworld

GPU并行计算OpenCL(1)——helloworld

随着现在GPU越来越强大,我们看论文的时候经常听到GPU加速的某某某算法,但是到底如何进行加速呢?CUDA可能大家更加熟悉(奈何电脑是MAC),这里介绍就OpenCL。

OpenCL(Open Computing Langugae)是第一个面向异构系统(此系统中可由CPU,GPU或其它类型的处理器架构组成)的并行编程的开放式标准。它是跨平台的。OpenCL由两部分组成,一是用于编写kernels(在OpenCL设备上运行的函数)的语言,二是用于定义并控制平台的API(函数)。OpenCL提供了基于任务和基于数据两种并行计算机制,它极大地扩展了GPU的应用范围,使之不再局限于图形领域。

我们首先了解一个知识:并发性,并发性指软件系统包含多个操作流时,这些操作流在运行的时候同时向前推进。当我们的硬件有多个处理单元的时候,我们就称为并行性,即硬件支持的并发性就是并行性。

 

OpenCL基本概念

 

OpenCL作为面向异构的平台,必须完成以下步骤:

1)发现构成异构系统的组件。

2)探查组件的特征,是软件能适应不同硬件单元的特定特性。

3)创建将在平台上运行的指令块(内核)。

4)建立并管理计算中涉及的内存对象。

5)在系统中正确的组件上按正确的顺序执行内核。

6)收集最终结果。

我们可以吧上述过程转换为4种模型:平台模型,执行模型,内存模型,编程模型。

 

平台模型

平台模型总包括一个宿主机OpenCL设备。

宿主机:通常为我们的CPU,其作用包括定义kernel、为kernel指定上下文、定义NDRange和队列等。

OpenCL设备:通常称为计算设备,可以是CPU、GPU、DSP或硬件提供以及OpenCL开发商支持的任何其他处理器。

计算设备进一步划分为计算单元,计算单元还可以细分为一个或多个处理单元,设备的计算都在处理单元中完成。

 

执行模型

OpenCL应用由两个不同部分组成:一个宿主机程序以及一个或者多个内核组成的集合。

宿主机程序在宿主机上运行,内核在OpenCL设备上运行。OpenCL定义了两类内核:

OpenCL内核:用OpenCL C编程语言编写,并应用OpenCL编译器编译的函数,所有OpenCL实现必须支持OpenCL内核。

原生内核:OpenCL之外穿创建的函数,在OpenCL中可以通过一个函数指针来访问。例如,这些函数可以是宿主机中定义的函数或者专门的库导出的函数。

由于编写OpenCL的重点就是执行内核,所以我们首先了解内核如何在OpenCL设备上执行

内核在宿主机上定义。宿主机程序发出一个命令,提交内核在一个OpenCL设备上执行。由宿主机发出这个命令时,OpenCL运行时系统会创建一个整数索引空间。对应这个索引空间中的各个点将分别执行内核的一个实例。我们把执行内核的各个实例称为一个工作项,工作项由它在索引空间中的坐标来标识,这些坐标就是工作项的全局ID。

多个工作项可以组成一个工作组。整个全局索引空间可以分成多个工作组,每个工作组中又有多个工作项。因此工作组的维度大学需要正好设置能整除全局索引空间。因此我们会有一个工作组的索引,工作组内的工作项也能有局部索引。这样我们就可以通过全局索引来索引各个工作项。可以以先索引工作组,然后根据工作组内的局部索引来获得具体的工作项。这里值得注意的是,OpenCL只能确保一个工作组中的工作项并发执行(虽然工作组或内核通常也会并发执行,但是我们设计算法不能依赖此)。

在这里,我们把索引空间称为NDRange。 如下图:

该例子很好的解释了工作项,工作组和索引空间的概念。如该图,我们的全局空间由12*12个工作项组成,我们把每4*4个工作项分为一组工作组,因此我们图中的黑色格子的全局索引为(6,5)。局部索引为(1,1)工作组内的(2,1)工作项。

OpenCL应用的计算工作在OpenCL设备上进行。不过宿主机同样需要完成非常多的工作。内核就是在宿主机上定义的,而且宿主机还为内核建立了上下文。宿主机还定义了NDRange和队列,队列将控制内核如何执行以及何时执行的细节。

 

上下文

宿主机的第一个任务就是定义上下文。上下文就是一个环境,内核将在该环境下定义和执行。该环境需要以下资源:

设备:宿主机使用的OpenCL设备集合。

内核:在OpenCL设备上运行的OpenCL函数。

程序对象:实现内核的程序源代码和可执行文件。

内存对象:内存中对OpenCL设备可见的一组对象,包含可以由内核实例处理的值。

上下文由宿主机使用OpenCL API函数创建,宿主机程序在其中一个CPU上运行,宿主机程序请求系统发现资源(系统内的CPU和GPU),然后决定OpenCL应用中使用哪些设备。

上下文还包括一个或者多个程序对象,程序对象包含内核的代码。程序对象会在运行时由宿主机程序构建。假设我们的上下文包含OpenCL设备和一个程序对象,将从这个程序对象中取出内核来执行。我们需要考虑内核如何与内存交互。OpenCL引入了内存对象。内存对象在宿主机上明确定义,并在宿主机和OpenCL设备间移动。

我们现在已经了解了OpenCL应用中的上下文。上下文就是OpenCL设备,程序对象,内核以及内核在执行时使用的内存对象。现在我们要看宿主机如何向OpenCL设备发出命令。

 

命令队列

宿主机与OpenCL设备之间的交互是通过命令完成的,这些命令由宿主机提交给命令队列。这些命令会在命令队列中等待,知道在OpenCL设备上执行。命令队列由宿主机创建,并在定义完上下文之后关联到一个OpenCL设备。。宿主机将命令放入命令队列,然后调度这些命令在关联设备上执行。

我们队列的命令执行有两种模式:

有序执行:按顺序完成命令。

乱序执行:不需等待前一个命令完成。

 

 

内存模型

OpenCL定义了两种类型的内存对象:缓冲区对象图像对象。缓冲区对象就是内核可用的一个连续的内存区,我们可以将数据结构映射到这个缓冲区,并通过指针访问缓冲区。图像对象仅限于存储图像,图像存储格式可以进行优化来满足一个特定OpenCL设备的需要。

我们还需要理解控制OpenCL程序中如何使用内存对象的抽象机制。OpenCL内存模型定义了5种不同的内存区域:

宿主机内存:该内存只对宿主机可见。

全局内存:该内存区域允许读写所有工作组中的所有工作项。

常量内存:全局内存的该内存区域在执行一个内核期间保持不变。且为只读。

局部内存:该内存区域对工作组是局部的。

私有内存:为一个工作项私有内存区域。

这些内存区域以及它们与平台和执行模型的关系图为:

其中,工作项在处理单元上运行(PE1-PEM),有其自己的私有内存。工作组在一个计算单元上运行,与该组中的工作项共享一个局部内存区域。OpenCL设备内存利用宿主机来支持全局内存区域。OpenCL设备内存利用宿主机来支持全局内存。

 

编程模型

OpenCL编程模型定义了一个OpenCL应用如何映射到处理单元,内存区域和宿主机。OpenCL定义了两种不同的编程模型:任务并行数据并行

数据并行编程模型:适合采用数据并行编程模型的问题都与数据结构有关,这些数据结构的元素可以并发更新。就是将一个逻辑指令序列并发地应用到数据结构的元素上。并行算法的结构被设计为一个序列,即对问题领域中数据结构并发更新序列。

任务并行编程模型:OpenCL将任务定义为单个工作项执行的内核,而不用考虑OpenCL应用中其他内核使用的NDRange。,如果程序员所希望的并发性来自任务,就会使用该模型。

 

HelloWorld:一个OpenCL例子

 

我们这里看一个简单的HelloWorld实例,该示例让储存在两个数组中的值相加,并把结果保存在另外一个数组中。

我们代码执行由下述概念组成:

  • 选择OpenCL平台并创建上下文。
  • 选择设备并创建一个命令队列。
  • 创建和构建一个程序对象。
  • 创建一个内核对象,并为内核参数创建内存对象。
  • 执行内核并读取结果。
  • 检查OpenCL中的错误。

 

选择OpenCL平台并创建一个上下文

创建OpenCL程序的第一步是选择一个平台。OpenCL使用可安装客户驱动程序模型,一个系统上可以有多个OpenCL实现并存。例如,在配置有一个NVIDIA GPU和一个AMD CPU的系统上,可以为两者各安装一个OpenCL实现。

我们的helloWorld示例展示了选择OpenCL平台最简单的方法:选择第一个可用的平台。以下代码展示了HelloWorld示例CreateContext函数:

cl_context CreatContext(){
    cl_int errNum;
    cl_uint numPlatforms;
    cl_platform_id firstPlatformID;
    cl_context context=NULL;
    
    /*首先,选择要运行的OpenCL平台。 对于这个例子,我们
      只需选择第一个可用平台。通常,我们也可以
      查询所有可用平台,然后选择最合适的平台。
    */
    errNum=clGetPlatformIDs(1, &firstPlatformID, &numPlatforms);
    if (errNum != CL_SUCCESS || numPlatforms <= 0)
    {
        std::cerr << "Failed to find any OpenCL platforms." << std::endl;
        return NULL;
    }
    
    /*接下来,在平台上创建一个OpenCL上下文。 尝试
      创建基于GPU的上下文,如果失败,请尝试创建
      基于CPU的上下文。
    */
    
    //创建上下文需要的资源 属性 属性值 0结束
    cl_context_properties contextProperties[]={
        CL_CONTEXT_PLATFORM,
        (cl_context_properties)firstPlatformID,
        0
    };
    
    context=clCreateContextFromType(contextProperties, CL_DEVICE_TYPE_GPU, NULL, NULL, &errNum);
    if (errNum != CL_SUCCESS)
    {
        std::cout << "Could not create GPU context, trying CPU..." << std::endl;
        context = clCreateContextFromType(contextProperties, CL_DEVICE_TYPE_CPU,
                                          NULL, NULL, &errNum);
        if (errNum != CL_SUCCESS)
        {
            std::cerr << "Failed to create an OpenCL GPU or CPU context." << std::endl;
            return NULL;
        }
    }

    return context;
}

我们可以看到该函数返回一个cl_context类型的对象,cl_context类型是OpenCL的一个内建类型,用了保存上下文信息。同样,我们的cl_intcl_uint都是内建类型,我们用errnum保存错误信息。numPlatforms保存获取的平台数量。firstPlatformId则用来保存平台索引,其类型cl_platform_id正是内建的平台索引类型。

我们首先用函数clGetPlatformIDs(1, &firstPlatformId, &numPlatforms)获取第一个可用的平台ID。该函数第一个参数为申请平台的个数,第二个参数为实际申请到平台的ID,第三个参数为实际申请到平台的个数。

我们再看函数clCreateContextFromType,它为我们创建上下文,其形式为:

cl_context clCreateContextFromType ( const cl_context_properties  *properties, 
  cl_device_type  device_type, 
  void  (CL_CALLBACK *pfn_notify) (const char *errinfo, 
  const void  *private_info, 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值