OpenCL的基本介绍

OpenCL简介

2008年,苹果公司向Khronos Group提交了一份关于跨平台计算框架的草案,该草案由苹果公司开发,并与AMD、IBM、Intel和NVIDIA公司合作逐步晚上。这个跨平台计算框架就是OpenCL。20088年12月8日,OpenCL1.0技术规范发布。2010年6月14日,OpenCL1.1发布,2011年11月19日,OpenCL1.2发布,2013年11月19日,OpenCL2.0分布。

OpenCL是一个异构并行计算平台编写程序的工作标准,此异构计算可映射到CPU、GPU、DSP和FPGA等计算设备。OpenCL提供了底层硬件结构的抽象模型,旨在提供一个通用的开发的API。开发人员可以编写在GPU上运行的通用计算程序,而无需将其算法映射到OpenGL或DirectX的3D图形的API上。

为了描述OpenCL设计的核心,Khronos Group将OpenCL异构并行计算架构划分为平台模型(platform model)、存储器模型(memory model)、执行模型(execution model)和编程模型(programming model)。这些模型相互独立、有相互联系,组成了OpenCL的有机整体。

OpenCL平台模型

平台模型是关于OpenCL如何看待硬件的一个抽象描述。OpenCL平台模型由主机以及相连的一个或多个OpenCL设备组成。如下图所示,通常主机是包含X86或ARM处理器的计算平台。OpenCL设计可以是CPU、GPU、DSP、FPGA或硬件商提供,OpenCL开发商支持的任何其他处理器。每个OpenCL设备有一个或者多个计算单元(Compute Units,CU),而每个计算单元又可以由一个或多个单元(Processing Elements,PE)组成,处理单元是设备上执行数据计算的最小单元。

在这里插入图片描述

OpenCL平台模型
由于OpenCL的平台模型包括了至少两种处理器,如何连接着两种处理器就和在这两种处理器之间传输信息的性能密切相关,目前主要通过PCI-e总线和主机相连接。

OpenCL执行模型

OpenCL程序包含主机端程序和设备端内核(kernel)程序。主机端程序运行在主机处理器上,主机端程序以命令方式将内核程序从主机提交到OpenCL设备,OpenCL设备在处理单元上执行计算。

内核在OpenCL设备上执行,完成OpenCL应用的具体工作,内核通常是一些计算量大,逻辑比较简单的函数,OpenCL设备通过内核将输入数据就散处理后输出到主机。在OpenCL中定义了三类内核:

  • OpenCL内核:用OpenCL C编程语言编写,用OpenCL C编译器编译的函数,所有OpenCL实现都必须支持OpenCL内核和OpenCL C编程语言。
  • 原生内核:OpenCL之外创建的函数,在OpenCL中可以通过一个函数指针来访问,执行原生内核是OpenCL的一个可选功能,原生内核的语义依赖于具体OpenCL实现。
  • 内建内核:被绑定到特定设备,并不需要编码编译成程序对象的函数。常见用法是针对公开固定函数硬件或固件,将它们关联到一个特定的OpenCL设备或自定义设备。内建内核OpenCL扩展功能,内建内核语义依赖于具体OpenCL实现。

由于OpenCL设备通常没有I/O处理功能,因此I/O操作通常由主机承担,这意味着程序开始执行时,数据通常在主机上,故OpenCL设备需要从主机上获得数据,在OpenCL设备计算完成后,有需要将数据从OpenCL设备复制回主机。

对于OpenCL执行模型来说,最重要的就是上下文、命令列队和内核三个概念。

上下文

OpenCL程序的计算工作是OpenCL设备上执行的,不过主机在OpenCL应用中扮演着重要的角色。主机使用OpenCL API来创建和管理上下文,内核在此上下文中执行。包含了:

  • 设备:OpenCL平台包含的一个或多个设备
  • 内核对象:在OpenCL设备上运行的OpenCL内核函数
  • 程序对象:实现整个内核程序的源代码和目标二进制码
  • 存储器对象:对主机和OpenCL设备可见的对象,内核执行时操作这些对象的实例。

设备的选择取决了具体问题和运行的内核,主机可能选择CPU、一个GPU、CPU+GPU、多个GPU等多个组合方案。

OpenCL是一套跨平台计算框架,对OpenCL开发人员来说,你可能并不知道OpenCL应用最终会在CPU平台、GPU平台还是FPGA平台上运行的,只知道目标符合OpenCL规范。解决的方法就是主机程序根据上下文中设备特性,在运行时从代码中构建程序对象。为了在性能和平台无关之间均衡,OpenCL提供了两种方式从代码中构建对象,一种是从程序源代码中构建,另外一种是从源代码中已经编译好的代码上构建。

命令队列

OpenCL没有定义主机代码如何工作的具体细节,知识定义了它通过命令队列与OpenCL设备如何交互。命令队列有主机或运行在设备中的内核(该功能需要支持OpenCL2.0的设备)提交给命令队列。命令会在命令队列中等待,直至别调度到OpenCL设备上执行。放入命令队列中的命令分为下列三种类型:

  • 内核入队命令:将一个内核队列关联到同一个OpenCL设备的命令队列中。
  • 存储器入队命令:将在主机和设备内存对象间传输数据,或者将内存对象映射到主机地址空间,或从主机地址空间取消映射提交给命令队列。
  • 同步命令:对命令队列中需要执行的命令施加执行顺序约束,如只有某个命令执行完成其他命令才能开始执行。

命令可以异步方式执行,在这种方式下主机或运行在设备中内核向命令队列提交命令,然后继续工作,而不必等待命令完成。如果有必要等待一个命令完成,可以利用命令执行相关的同步机制进行同步。

一个命令队列中命令执行时可以有以下两种模式:

  • 按序(in-order)执行:命令按其排入命令队列中的先后顺序执行,并按顺序完成。
  • 乱序(out-of-order)执行:命令以任意顺序执行,通过显示的同步点或显示事件依赖项来约束顺序。

OpenCL平台都支持按序模式,但乱序模式是可选的。

与内核相关的计算,在运行时只作为内核实例执行。常规方法上,可以通过主机程序多次启动内核实例来执行,但这样会显著地增加开销或加重应用程序控制流。一个方便有效的方法就是从内核内部嵌套内核命令队列。对支持OpenCL2.0的设备,可以在设备上入队内核,不需要主机程序参与,这称为设备端队列,实现了设备端队列,实现了嵌套并行。

内核在OpenCL设备上执行

主机发出一个命令,提交一个内核到OpenCL设备上执行,OpenCL运行时将会创建一个整数索引空间。索引空间是OpenCL支持的一个N维值的网格,称为NDRange,其中N为1,2,或3。三个长度为N的数据确定了NDRange的一下特征。

  • 每个维度索引空间(或全局大小)的范围
  • 一个偏移指数F表明每个维度的初始索引值(默认为0)
  • 一个工作组(局部大小)每个维度大小。

内核、关联内核参数值和定义索引空间的参数,这三个定义了一个内核实例。我们将执行内核的各个实例称为一个工作项(work-item),工作项将由它的索引空间中的坐标来标识,这个坐标就是工作组的全局ID,值从F到F加上该维度元素间1。每个工作项使用内核定义的同样的指令序列,尽管指令序列是相同的,但是由于代码中通过全局ID选择的数据不同,每个工作项的行为也不同。工作项提供了对索引空间细粒度的分解。

多个工作项组织成为工作组(work-group),工作组中工作项的数量由内核入队时的参数决定。工作组横跨了整个全局索引空间,提供了对索引空间粗粒度的分解。同样每个工作组被指定了一个唯一的ID,值从0开始,到该维度中工作组个数减1。除了一个全局ID,也赋予了一个局部ID来表示它所属工作组中的位置,这个局部ID的值从0快开始,到工作组内该维度元素个数减1。

例如:定义一个2维索引空间。索引空间的工作项大小为(AX,AY),每个工作组的大小为(BX,BY)、全局偏移ID(FX,FY)。索引空间的工作项总个数为AX×AY,每个工作组中工作项的个数为BX×BY,则工作组的大小为(CX,CY)的值为:
C X = c e i l ( A X / B X ) C Y = c e i l ( A Y / B Y ) CX=ceil(AX/BX)\\ CY=ceil(AY/BY) CX=ceil(AX/BX)CY=ceil(AY/BY)
工作项在工作组内的局部ID为(lx,ly),大小为(0,0)~(BX-1,BY-1)。工作组的ID为(Wx,Wy),大小为(0,0) ∼ \sim (CX-1,CY-1)。对于工作项全局ID(gx,gy)可以结合全局ID(lx,ly)和工作组ID(Wx,WY)
g x = W x × B X + l x + F X g y = W y × B Y + l y + F Y gx=Wx\times BX+lx+FX\\ gy=Wy\times BY+ly+FY gx=Wx×BX+lx+FXgy=Wy×BY+ly+FY

在这里插入图片描述

2维NDRange

OpenCL存储器模型

存储器区域

OpenCL异构平台由主机端和设计端构成,存储器区域包括主机和设备的内存。

  • 主机内存(host memory):主机直接可用的内存,OPenCL并未定义主机内存的具体的行为。通过OpenCL API或者共享虚拟存储接口,实现存储器对象在主机和设备之间的传输。
  • 全局存储器(global memory):这个存储器区域允许上下文任何设备中所有工作组的所有工作项的读写,工作项可以读写存储器对象中的任意元素。全局存储器的读写能力可能会被缓存,取决于设备的能力。
  • 常量存储器(constant memory):全局存储器中的一块区域,在内核实例执行期间其保存的数据保持不变。主机负责该存储器对象的分配和初始化。
  • 局部存储器(lcoal memory):这个存储器区域对工作组是局部可见的,用来分配该工作组中所有工作项共享的变量。
  • 私有存储器(private memory):这个存储器区域是一个工作项的私有区域。

在这里插入图片描述

OpenCL内存模型

全局存储器和常量存储器可以在一个上下文内的一个或多个设备间共享,一个OpenCL设备关联局部存储器和私有存储器。

存储器对象

全局存储器中的数据内容通过存储器对象来表示,一个存储器对象就是对全局存储器区域的一个引用,主要有三种不同的类型:

  • 缓冲(buffer):内核可用一个连续的存储器区域,编程人员可以将内建数据类型、矢量数据类型或用户自定义的数据结构(符合OpenCL编程规范)映射到这个缓冲区,,内核通过指针来访问缓存区。
  • 图像(image):图像对象用户存储基于标准格式的图像,图像对象是一个不透明的数据结构,使用OpenCL API函数来管理。通常不允许OpenCL内核对单个图像同时进行读和写。在OpenCL2.0之后,提供了同步和栅栏操作来放宽这个限制。
  • 管道(pipe):管道存储器是数据项有序的队列,管道有两个端点:一个是写端点,用于插入数据项,另外一个读端点,数据项从读端点被移除。同一个时刻,仅有一个内核实例可向一个管道写入数据,同时仅有一个内核实例从一个管道读出数据。

主机和设备间有三种交互方式:读、写、映射和解映射以及拷贝。

共享虚拟存储器

通过映射,可以将设备全局存储器区域映射到主机可以访问的地址空间,而除了这种方式,在OpenCL2.0中,OpenCL通过共享虚拟存储器(Shared Virtual Memory,SVM)机制扩展了全局存储器区域到主机内存区域的方式。在OpenCL2.0中定义了三种SVM类型。

  • 粗粒度SVM:共享发生在OpenCL缓冲存储器对象区域的粒度。
  • 细粒度SVM:共享发生在OpenCL缓冲存储器对象里独立地以字节加载/存储的粒度。
  • 细粒度系统SVM:共享发生在主机内存内任何地方独立地以字节加载/存储的粒度。

OpenCL和OpenGL

从GPU诞生之日,GPU的设计逻辑与CPU的设计逻辑相差很多。GPU从诞生之日起,定位是3D图像渲染设备。在设计GPU时从其功能出发,把更多的晶体管用于数据处理。使得GPU相比CPU有更强的单精度浮点运算能力。其中不得不提OpenGL(Open Graphics Library,开发图形库)。

在这里插入图片描述

CPU和GPU架构区别

OpenGL定义了一个跨编程语言、跨平台的应用程序接口规范,用于生成二维、三维图像。这个接口由近350个不同的函数调用组成,用来从简单的图像比绘制到复杂的三维图像。

当我们把绘制的图像传递给OpenGL后,OpenGL还要做很多才能完成3D空间到屏幕的投影。这一系列的过程称为OpenGL渲染流水线。渲染的过程如下:
在这里插入图片描述

OpenGL图像流水线

在顶点装配和片段操作中,使用GPU中的着色器来进行相应操作。进行几何处理的处理器叫做顶点着色器,负责对顶点进行坐标转换、投影变换等。进行片段颜色处理叫做片段着色器。

从2007年以后,基于CUDA和OpenCL这些被设计为近似高阶语言的语法特性的新GPGPU原因,减低人们使用GPGPU的难度,平缓了开始时的学习曲线。

OpenCL与CUDA

CUDA是集硬件与软件与一体的集成技术。CUDA C编程时在C99的扩展上进行的,但是只能在NVIDIA G80架构以后的GPU上。OpenGL编程模型设计时,借鉴和参考了CUDA的编程模型。从编程语言来看,OpenGL和CUDA语法基本类似。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐