OpenCL异构并行计算:原理、机制与优化实践
上QQ阅读APP看书,第一时间看更新

第1章 异构并行计算的过去、现状和未来

在正式进入本章的主题前,先让我们重温一下异构并行计算的概念。异构并行计算包含两个方面的内容:异构和并行。异构是指:计算单元由不同的多种处理器组成,如X86 CPU+GPU、ARM CPU+GPU、X86 CPU+FPGA、ARM CPU+DSP等。并行是指:要发挥异构硬件平台的全部性能必须要使用并行的编程方式。这通常包含两个层次的内容:

1)多个不同架构的处理器同时计算,要发挥异构系统中所有处理器的性能,可通过并行编程使每个处理器都参与运算,避免处理器闲置。相比于只让某一种类型的处理器参与工作,这种方式提高了性能上限,简单举例来说,在X86 CPU+GPU平台上,X86 CPU的计算能力为1TFLOPS,GPU的计算能力为4TFLOPS,如果只使用GPU,那么最大可发挥的性能是4TFLOPS,而如果加上X86 CPU,则最大可发挥的性能是5TFLOPS。

2)每个处理器都是多核向量处理本书的向量处理器指支持MIMD执行或SIMD指令集的处理器。,这要求使用并行编程以发挥每个处理器的计算能力。通常每个处理器包括多个核心,每个核心包含一个或多个长向量,如AMD GCN GPU中就包含数量不等的核心,每个核心包含4个向量,每个向量能够同时处理16个4字节长度的数据。如果没能很好地并行,则可能不能完美地发挥多核和向量化的性能。

作为本书的开篇,本章将主要介绍异构并行计算的历史、现状和未来:

1)异构并行计算的历史。即在异构并行计算出现之前,处理器是如何提升性能,使用了哪些提升性能的方法,这些方法为什么又遇到困难了。读史使人明智,通过了解异构并行计算的历史,读者可以了解到为什么异构并行计算会大行其道,也了解了为什么笔者会编写本书。

2)异构并行计算的现状。今天异构并行计算已经得到充分的发展并且还在进一步快速发展中,OpenCL和其他的异构并行计算工具已经应用到许多图像处理、视频处理及科学计算项目上,而这些工具自身也在快速进化中。近两年,许多科学计算以外的行业和领域(如互联网行业)正在应用异构并行计算来加快研究和产品化的步伐。

3)异构并行计算的未来。计算的未来是异构并行的,异构并行的概念、应用在计算机及相关领域会越来越广。任何参与计算机及相关行业的人员都应当了解并学习异构并行相关的内容。在不久的将来,不懂异构并行计算就意味着不懂计算机。

在具体介绍异构并行计算的历史、现状和未来之前,笔者想介绍几个始终贯穿本书的相关概念:

1)向量化。向量化是一种一条指令同时处理多个数据的方法,从这一点来说,它是一种数据并行技术。主流的向量化技术有两种:SIMD(Single Instruction Multiple Data,单指令多数据)和SIMT(Single Instruction Multiple Thread,单指令多线程),大多数CPU(如AMD Zen处理器)都使用SIMD向量化技术,而大多数GPU(如AMD GCN)都使用SIMT向量化技术。关于SIMD的具体描述请参看图1-1。

图1-1 向量化示例

SIMD操作可简单描述为一些具有如下特点的操作:对两个长向量寄存器中的数据按元素进行操作,结果向量寄存器和源向量寄存器长度相同。例如,对两个长度为512位的向量进行SIMD操作,按照单精度进行浮点加操作,假设单精度浮点类型占用空间大小为4个字节,那么512位向量可一次同时处理16(512位/8位每字节/4字节)个单精度浮点数据得到16个结果,其中第一个向量的第1个元素和第二个向量的第1个元素相加产生结果向量的第1个元素,第一个向量的第15个元素和第二个向量的第15个元素相加产生结果向量的第15个元素,其余类推。

2)多核。多核是指:在一块芯片上,集成多个处理器核心,这多个处理器核心共享或不共享缓存层次结构。图1-2是ARM公司设计的ARM Cortex-A72多核处理器,从中可以看出其最多具有4个核心(为了应对不同细分市场的需求,ARM处理器核心数量通常可调整),每个核心具有32KB一级数据缓存(L1 Cache),48KB一级指令缓存,4个核心共享512KB到2MB二级缓存(L2 Cache)。多核处理器通常会共享主板上的物理内存。

图1-2 ARM Cortex-A72多核向量处理器架构

3)多路。硬件生产商会将多个多核处理器互联(如AMD的HT(Hyper Transport)总线)在同一个主板上,各个多核处理器之间通常共享缓存(如三级缓存或eDRAM)或内存来交换数据。由于主板的设计会导致NUMA(非一致性内存访问)特性,感兴趣的读者可参考刘文志(花名风辰)的著作《并行算法设计与性能优化机械工业出版社华章公司出版,书号978-7-111-50102-2。中的2.6节。

多核和向量化是现代处理器提升性能的两种主要途径,今天的绝大多数处理器都已经是多核向量化处理器。在介绍为什么多核或向量化处理器如此流行之前,先让我们了解一下之前的单核标量处理器遇到了什么问题。

1.1 单核标量处理器的困境

在2005年之前,大多数处理器都是单核的,一些处理器已经开始支持向量化(如X86处理器支持的MMX(多媒体扩展)和SSE(流式SIMD扩展)指令集),但是绝大多数应用程序并没有进行向量化,故绝大多数代码只能利用到单核处理器的标量性能。对于单核标量处理器(或者运行在单核向量处理器上的标量代码)来说,处理器生产商只能考虑如何提升单核标量处理器的性能。处理器生产商通过提升单核标量处理器的频率和指令级并行处理能力(即提升指令流水线性能)来提升处理器的计算性能,如图1-3所示。

图1-3 处理器频率、性能、功耗和核数变化

Original data collected and plotted by M.Horowitz, F. Labonte, O. Shacham, K. Olukotun, L. Hammond and C. Batten Dotted line extrapolations by C.Morre

从图1-3中可以看出,在2005年之前,单核标量处理器的性能基本上是每18个月近似提升一倍,这称为摩尔定律。关于摩尔定律有许多不同的表述,也有一些表述上的不同和争议,本节就不追究其原因和细节,只简单地称“单核标量处理器性能每18个月提升一倍”为摩尔定律。

在2005年之前,单核标量处理器性能提升能满足摩尔定律的时期称为提升软件性能的“免费午餐”时期,因为单核标量代码的性能可以满足摩尔定律描述的速度提升,在这个前提下,应用程序无须修改,只需要等待下一代处理器的推出,到时现在的代码自然就能够跑得更快。处理器生产商、研究人员和软件开发人员都非常高兴且享受摩尔定律带来的成果:

1)对处理器生产商来说,能够稳定地推出性能更好的产品能够帮助他们顺利推动产品的更新换代,卖出更多新产品,淘汰旧产品,获得更多利润。处理器生产商获得了更多利润就能够进一步增加研发投入,以推出性能更好的产品。对处理器生产商来说,这是一个良性循环。

机械工业出版社华章公司出版,书号978-7-111-50102-2。

2)对研究人员来说,他们基于当前处理器的计算能力来设计应用,获得研究结果,并依据摩尔定律来估计下一代处理器能够提供的性能,设计在下一代处理器上能够快速运行的应用。在下一代处理器推出后,就可以获得更好的结果。

3)对软件开发人员来说,无须花费太多精力来优化程序性能,只需要建议老板购买新硬件即可获得性能提升。

4)在这种处理器生产商和软件开发人员相互促进的良性循环下:软件开发人员依据当时处理器的性能设计应用,并依据摩尔定律对下一代处理器的性能提出预期(设计在下一代处理器上能够流畅运行的应用),处理器生产商生产新处理器以满足摩尔定律对性能的要求,并将新处理器卖给软件开发人员,周而复始,相互促进。

在2005年之后,单核标量处理器的性能基本上达到顶峰,很难进一步大幅度(超过10%)提升性能。在回答为什么单核标量处理器的性能无法接着以摩尔定律要求的速度提升之前,先让我们看一下,在2005年之前单核标量处理器如何提升性能,因为只有知道之前如何提升性能,才能知道为什么不能以同样的方式接着提升性能。

1.1.1 单核标量处理器如何提高性能

在2005年之前,单核标量处理器以近似摩尔定律的方式提升性能,其主要通过以下几种方式提升性能:

1)提升处理器的时钟频率:处理器的时钟频率表示处理器1秒内可以运行多少个基本操作,这些基本操作需要一个时钟周期运行。在某个固定的处理器上,一些复杂的操作可能需要多个时钟才能执行完成,或由多个基本操作组成。一条指令从开始到执行完成所需要的时钟周期数,称为指令的延迟。一个具体的操作在不同的处理器上,其所花费的时钟周期数量可以相同,也可以不同。通过提升某个单核标量处理器的时钟频率,在指令的延迟保持不变的前提下,处理器1秒内就可以执行更多的基本操作,这提升了处理器上运行的所有指令的执行速度。

2)提升指令级并行能力:单核标量处理器上具有许多不同的部件,每个部件执行不同的指令操作,如有的部件负责从内存中加载数据,有些部件负责计算乘加指令,一些部件负责计算内存地址。如果能够让多条做不同动作的指令同时操作,那么多个部件就可以同时进行指令操作,这称为指令级并行。如果在一个处理器上,能够同时操作3条指令,在提升指令级并行能力后,它可能能够同时处理多达5条指令。提升指令级并行能力并没有减少某条指令的延迟,但是它提升了处理器能够同时处理的指令数量。

在“免费午餐”时期,通过提升处理器的时钟频率以大幅度提升性能,如图1-3中的绿图1-3中从上向下数,第三条线为绿色线,本书为单色印刷,特为读者指出,带来不便,敬请谅解。 所示。而通过将一条指令拆分成多个阶段以提高指令级并行能力已得到广泛使用,关于为什么将一条指令拆分成多个阶段能够提高处理器的性能,以经典的5阶段流水线为例,请参考图1-4。

图1-4 流水线示例

五阶段流水线将指令的执行过程划分成:取指令(Instruction Fetch,IF)、指令解码(Instruction Decode,ID)、执行(Execution,EX)、访存(Memory Access,MEM)和写回(Write Back,WB)。同时假设处理器支持两条流水线同时操作。在开始执行时(t0),有两条指令(i0,i1)在取指令;在t1时,指令i0、i1在解码,而两条新指令i2、i3可以进行取指令操作;在t2时,又有两条新指令i4、i5进行取指令操作,而此时指令i2、i3进行解码操作,而指令i0、i1正在执行;在t3时,又有两条新指令i6、i7进行取指令操作,而此时指令i4、i5进行解码操作,而指令i2、i3正在执行,指令i0、i1正在访存;在t4时,又有两条新指令i8、i9在进行取指令操作,而此时指令i6、i7进行指令解码操作,指令i5、i4正在执行,指令i2、i3正在访存,指令i0、i1正在写回,写回结束后,指令i0、i1就完成了,以此类推,在随后的每个周期内,都会有两条指令执行完成,两条新指令加入执行。从整体来看,若没有使用流水线执行,则原来需要5个周期才能完成2个操作,而使用流水线执行后,则每个周期能够完成2个操作。

要完全利用硬件指令流水线的所有性能,程序代码需要提供足够多样(不同种类)的指令,编译器需要从源代码中获得足够多的信息以安排流水线获得最好性能。另外,不是所有的指令都需要执行硬件指令流水线的所有阶段,现代处理器采用了许多不同的办法来处理这个问题。这里就不展开讨论这些问题了。

在介绍完单核标量处理器如何提升性能之后,我们接着来了解单核标量处理器无法继续以摩尔定律的速度提升性能,即为什么单核标量处理器性能到达瓶颈。

1.1.2 为什么单核标量处理器性能到达瓶颈

1.1.1节提到,在2005年之前,提升单核标量处理器性能以满足摩尔定律描述的速度提升,而在2005年之后,单核标量处理器的性能不能再以摩尔定律的速度提升,这主要是因为:

1)功耗限制了频率的继续提升:从物理定理来看,随着处理器工艺制程的推进,处理器的最大功耗(主要是漏电功耗)越来越大,大致来说处理器的功耗和处理器的频率的三次方近似成正比(硬件设计实践中有许多方法降低指数,具体细节请读者参考相关著作,笔者就不详细解释了),这意味着随着处理器频率的增加,处理器功耗会大幅度增加。处理器功耗增加,则处理器工作时越来越热,对散热系统的要求会越来越高。而今天散热系统已经从风冷、散热片、水冷到油冷。在处理器散热要求已经达到现实环境、技术能够满足的界限情况下,如果再增加频率,那么硬件组件可能不能正常工作,甚至烧掉。

2)提升指令级并行遇到瓶颈:指令级并行能够让处理器的多个不同的流水线组件同时工作。如图1-4所示,在一条指令取指的阶段,另外一条指令正在解码,与此同时其他的指令正在计算、写回存储器等。指令级并行能够增大处理器组件的利用率,极大地提高处理器的性能。但是要利用好处理器的指令级并行能力需要代码优化人员、编译器作者和处理器设计师共同努力。处理器设计师在硬件层次提供了重排缓冲区(Reorder Buffer,ROB)、发射队列(issue queue)和寄存器重命名单元(register renaming)等来挖掘指令执行时的不相关性。硬件层面的支持能够挖掘出软件层面不知道的信息,如是否存在存储器别名。编译器作者要让编译器合理安排生成的指令,尽量让生成的指令没有依赖性,或者让依赖指令的距离足够远,合理重用寄存器等。代码优化人员需要以编译器和处理器友好的方式编写代码,以便编译器生成处理器能够高效执行的代码。为了提高硬件的指令级并行执行能力,处理器设计通常会增加硬件流水线的级次,而现在这一方法也达到其局限。

在提升指令级并行遇到瓶颈后,硬件设计师通过增加硬件寄存器的长度提升性能。例如,原来寄存器长度为32位,现在提升到128位,这意味着如果原来一个寄存器只能保存一个单精度浮点数据,而现在一个寄存器能够保存4个这样的数据。如果同时增加指令的操作能力,让指令可以同时对一个128位的向量寄存器中的数据进行操作,那么就能够成倍地增加处理器的吞吐量,这称为SIMD向量化。SIMD向量化也是一种数据并行形式的指令级并行,通过向量化,硬件只需要增加很少的单元就能够成倍地提升峰值性能。

由于散热导致处理器的频率不能接着提升,硬件生产商转而采用将多个处理器组成到一个芯片上,这称为多核。通过稍微降低频率和电压,多核处理器能够以稍微增加的功耗获得更高的性能。

1.2 多核并行计算与向量化的出现

由于散热技术和硬件生产技术无法满足提高处理器频率对功耗的设计要求,现代处理器的频率近似停滞。为了提供更高性能的处理器,处理器硬件生产商通过增加寄存器的宽度和指令的宽度来同时处理多个数据,这称为向量化。多核和向量化的出现提升了处理器的执行能力。通过稍微降低频率,现在的散热技术能够满足处理器对功耗的需求。

今天的绝大多数处理器,如X86多核CPU、ARM多核CPU、GPU及DSP等,都已经是多核向量处理器。多核和向量化的出现满足了应用对计算能力的需求,但是它们也带来了两个重要的问题:如何编程以发挥多核和向量的计算能力;如何保证随着核数和向量长度的增加,性能的提升依旧接近线性。

1.2.1 为什么会有多核

多核通过复制处理器核心成倍增加了处理器的计算能力,多核的出现除了功耗的原因外,还有许多其他的原因。

许多应用需要同时进行许多不相关的处理,如希望用户界面在不影响用户处理的同时进行计算,以提升用户体验;如网页服务器需要同时满足多个用户的访问请求。多核能够支持线程级并行处理,通过将线程映射到硬件核心上,以同时处理多个不同的请求,提高用户使用体验。

随着数据量越来越大,处理大量数据需要的计算性能的需求也越来越大,如科学计算领域需要长时间、细精度的模拟以重现真实系统;深度学习系统需要在大量数据集上训练以调整模型,通常其运行时间需要几天、几周。应用对性能的要求促使处理器硬件生产商想方设法、不断提升处理器的性能。

如果处理器生产商没有办法提供性能更好的处理器,他就没有办法要求用户为他的新处理器埋单。

1.2.2 为什么会有向量化

许多算法、应用中具有向量级并行(数据并行)能力,如一些算法需要对多个不同数据执行同一个操作,如代码清单1-1所示。

代码清单1-1 向量化示例代码

for(int i = 0; i 〈 n; i++)
{
    __m128 da = a[i];
    __m128 db = b[i];
    c[i] = _mm_add_ps(da, db);
}

代码使用128位向量处理单精度数据,那么每次可以同时处理4个数据。

向量化提升了处理器的计算能力,能够满足应用软件对计算性能的需求。同时向量化并没有大幅度增加处理器功耗。

1.2.3 如何利用多核和向量化的能力

要同时发挥向量化和多核的计算能力,必须要编写向量化和多线程代码。这通常表现为两种实现方式:①分别编写向量化代码和线程级代码;②统一编写向量化代码和多线程代码。

常见的编程语言,如C11/C++11和Java等语言本身已经内置了线程级并行能力,而其他的语言则需要使用语言自身的机制。要发挥多核向量处理器的向量计算能力,则需要使用硬件生产商提供的内联汇编(也称为内置函数)。例如,Intel/AMD为其X86处理器提供了SSE/AVX指令集的C语言内置函数,ARM也为其CPU处理器提供了NEON指令集的C语言内置函数。

一些新出现的C编程语言扩展,如CUDA和OpenCL,它们通过层次化的线程/编程模型使得一份代码同时支持向量化和多核。目前CUDA和OpenCL已经广泛应用在基于GPU的异构并行计算中。

OpenCL不但支持GPU,还支持X86 CPU和ARM,一些移动处理器的GPU也开始支持OpenCL,如高通的Adreno系列、Imagnation Technology的PowerVR系列和ARM的Mali系列都已经支持OpenCL。目前一些FPGA和DSP的厂商也已经提供了OpenCL的支持。

1.2.4 多核和向量化的难点

虽然多核和向量化能够大幅度地提升代码性能,但是它依旧面临许多现实的困境:

1)无论是向量化还是多核并行化,这两者都意味着需要并行化代码,依据Amdahl定律,程序中的串行代码比例限制了并行化代码能够取得的最好效果。一个比较简单的示例是:如果你的代码中有10%的代码是必须要串行执行的,那么无论你怎么优化,都不可能获得超过10倍的加速。

2)现在的多核向量处理器(尤其是X86)为了减少获取数据的延迟,使用了大量的缓存来保存多次重复访问的数据,在很多情况下,缓存能够增加程序的现实计算能力。但是缓存并不贡献硬件的原生计算能力,这意味着实际上处理器的大量缓存并没有用来提升硬件的计算能力(从某种程度上说,缓存是对处理器资源的一种浪费)。

3)有些代码不能使用多核并行化或向量化。一些算法的运算具有内在的串行特点,因此必须要串行执行。如果代码无法多核并行化或向量化,那么多核向量处理器的一些计算能力就会浪费。

4)在许多情况下,要发挥向量化和多核的计算能力,可能需要多份代码,这增加了代码维护代价。要编写向量化代码或多核并行代码,需要分析代码中数据和操作的依赖关系,处理任务和数据划分,并将其高效地映射到向量化和多核硬件上。

5)在一些应用严格的应用场景下,限制了其不能允许向量化和多线程导致的计算结果出现偏差。

虽然多核和向量化具有许多现实的困难,但是软件开发人员和硬件设计人员正在紧密合作,以减轻这些因素的影响。

1.3 异构并行计算的崛起

从2007年NVIDIA推出CUDA计算环境开始,异构并行计算逐渐得到大众的认同,从学术界走向了工业界。异构并行计算包含两个子概念:异构和并行。

1)异构是指异构并行计算需要同时处理多个不同架构的计算平台的问题,如目前主流的异构并行计算平台X86+GPU、X86+FPGA,以及目前正在研发中的ARM/Power+GPU。

2)并行是指异构并行计算主要采用并行的编程方式,无论是X86处理器,还是ARM和GPU处理器以及DSP,这里所有的处理器都是多核向量处理器,要发挥多种处理器混合平台的性能也必须要采用并行的编程方式。

最先拥抱异构并行计算的领域是科学计算,如分子动力学模拟中需要长时间的运算,使用X86+GPU的异构并行模式能够将模拟时间由几周缩短为1周或几天。

异构并行计算的出现缓解了处理器发展面临的两个主要问题:性能问题和功耗问题。

1)由于不同的硬件适合处理不同的计算问题。合理地将不同类型的计算分发到异构平台的不同硬件上能够获得更好的计算性能,如将需要短时间运行的串行计算分发给X86,而将需要长时间运行的并行计算部分分发给GPU。

2)由于采用为特定应用优化的处理器。处理器设计可以依据应用的具体特点来优化,故功耗方面也会获得更好的结果。

在性能和功耗都比较重要的情况下,如何衡量处理器的性能就变得复杂起来,对于计算性能至上的应用来说,性能更为重要。而对于功耗有特殊要求的应用来说,性能功耗比可能更为合适。性能功耗比,即每瓦功耗能够支撑的处理器计算能力。

1.3.1 GPGPU的理念

在NVIDIA推出其CUDA计算环境之前,许多科学家就已经意识到如果能够利用GPU提供的强大计算能力来计算一些通用运算,那么就能够获得很高的计算速度。但是那个时候的GPU(姑且称之为GPU)还是为图形渲染特殊设计的流水线,那时GPU的每个部件都是为了图形渲染的某一个阶段特殊设计。渲染一个图形需要顺序地经过所有流水线的处理。在那个时代,要使用GPU计算,则必须要将算法映射成图形的渲染过程,那时用来进行图形编程的主要应用编程接口是OpenGL,故更明确的说法是:使用OpenGL将计算过程映射成为图形渲染过程,进而达成计算的目的,这称之为GPGPU。

由于需要将计算过程映射为图形的渲染过程,故编写GPGPU程序需要专家级的GPU硬件的图形渲染知识和OpenGL编程知识。

1.3.2 CUDA的崛起

在2007年,NVIDIA推出了GTX8800 GPU,与之前为图形渲染的每个阶段独立设计流水线不同,GTX8800采用了统一的渲染架构,同一处理器会处理图形渲染的全部流水线,这不仅提升了硬件的利用率,获得了图形渲染的高性能;同时统一架构也使得在GPU上进行通用计算更为容易。为了和传统的GPGPU计算相区别,称之为GPU计算时代。

在NVIDIA推出GTX8800的同时,NVIDIA也推出了其通用GPU计算编程工具CUDA,在一开始,NVIDIA称CUDA是计算统一设备架构(Computing Unif ied Device Architecture)的缩写,但是今天CUDA的范围已经远远超出了NVIDIA当初的定义,CUDA已经成为NVIDIA通用GPU并行计算编程平台和编程模型的抽象,故其已经变成了一个符号,一个生态系统。

CUDA平台本身提供了CUDA C语言扩展,相比普通C语言,CUDA C增加了使用NVIDIAGPU进行通用计算必不可少的一些语言扩展,其他功能则都通过函数库提供。

CUDA C以C/C++语法为基础而设计,因此对熟悉C系列语言的程序员来说,CUDA的语法比较容易掌握。另外CUDA只对ANSI C进行了最小的扩展,以实现其关键特性:线程按照两个层次进行组织、共享存储器(shared memory)和栅栏(barrier)同步。

由于CUDA完美地结合了C语言的指针抽象,NVIDIA不断升级其CUDA计算平台, CUDA获得了大量科学计算人员的认可,已经成为目前世界上使用最广泛的并行计算平台。通过CUDA,NVIDIA成功打破了Intel在超算市场上的绝对主导地位。在今天,大多数大中小型超算中心中都有GPU的身影。

由于CUDA由NVIDIA一家设计,并未被Intel和AMD等接受,因此目前使用CUDA编写的程序只支持NVIDIA GPU,而OpenCL的出现解决了这一问题。

1.3.3 OpenCL横空出世

OpenCL全称为Open Computing Language(开放计算语言),先由Apple设计,后来交由Khronos Group维护,是异构平台并行编程的开放的标准,也是一个编程框架。Khronos Group是一个非盈利性技术组织,维护着多个开放的工业标准,并且得到了业界的广泛支持。OpenCL的设计借鉴了CUDA的成功经验,并尽可能地支持多核CPU、GPU或其他加速器。OpenCL不但支持数据并行,还支持任务并行。同时OpenCL内建了多GPU并行的支持。这使得OpenCL的应用范围比CUDA广。为了能适用于一些更低端的嵌入式设备(如DSP+单片机这种环境),OpenCL API基于纯C语言进行编写,所以OpenCL API的函数名比较长,参数也比较多(因为不支持函数重载),因此函数名相对难以熟记。不过,借助像Xcode、Visual Studio等现代化的集成开发环境,利用代码智能感知自动补全,其实开发人员也不需要刻意去死背OpenCL的API。

OpenCL覆盖的领域不但包括GPU,还包括其他的多种处理器芯片。到现在为止,支持OpenCL的硬件主要局限在CPU、GPU、DSP和FPGA上,目前在桌面端和服务器端提供OpenCL开发环境的主要有Apple、NVIDIA、AMD、ARM和Intel,其中Apple提供了一个独立的OpenCL框架并与自家的OS X系统完整地融合在一起;NVIDIA和AMD都提供了基于自家GPU的OpenCL在Windows和Linux上的实现,而AMD和Intel提供了基于各自CPU在Windows和Linux上的OpenCL实现。目前除了OS X系统,NVDIA、AMD与Intel提供的OpenCL实现都不约而同地不支持自家产品以外的产品。由于硬件的不同,为了写出性能优异的代码,可能需要为不同的平台做相应的优化,这会对可移植性造成影响,这个需要权衡。

OpenCL包含两个部分:一是OpenCL C语言(OpenCL 2.1将开始使用OpenCL C++作为内核编程语言)和主机端API;二是硬件架构的抽象。为了使C程序员能够方便简单地学习OpenCL,OpenCL只是给C11进行了非常小的扩展,以提供控制并行计算设备的API以及一些声明计算内核的能力。软件开发人员可以利用OpenCL开发并行程序,并且可获得比较好的在多种设备上运行的可移植性。

为了使得OpenCL程序能够在各种硬件平台上运行,OpenCL提供了一个硬件平台层。同时各种不同设备上的存储器并不相同,相应地,OpenCL提供了一个存储器抽象模型。与CUDA相似,OpenCL还提供了执行模型和编程模型。

OpenCL不但包括一门编程语言,还包括一个完整的并行编程框架,通过编程语言、API以及运行时系统来支持软件在整个平台上的运行。

相比CUDA,OpenCL的优点在于它提供了一种能够在不同平台上可移植的编程方式,另外其原生支持的多设备并行也是一大亮点。

1.4 异构并行计算的未来(百花齐放)

近十年来,异构并行计算平台、标准如雨后春笋般地出现,发展也是一日千里。从私有的CUDA、C++AMP、Direct3D、Metal API,到开放的OpenCL、OpenACC、OpenGL。而老牌共享存储器编程环境OpenMP和分布式编程环境MPI也增加了对异构计算的支持,这些无一不显示着这个领域现在的辉煌,这是一个异构计算正在百花齐放的时代,在短期内这个领域依旧会呈现百花齐放的形态,甚至还会有新的平台、新的标准出现。而长期来说,最有可能笑到最后的,必定是OpenCL,这主要是因为OpenCL具有以下特点:

高性能:OpenCL是一个底层的API,它能够很好地映射到更底层的硬件上,充分发挥硬件中各个层次的并行性,故能够获得很好的性能。

适用性强:OpenCL是一个抽象的API,它抽象了当前主流的异构并行计算硬件的不同架构的共性,同时又兼顾了不同硬件的特点,因此具有广泛的适用性。

开放:OpenCL是由开放组织开发、维护的标准,不会被一家厂商所控制,故能够获得最广泛的硬件支持,如AMD、Intel、NVIDIA、ARM、Qualcomm和联发科等都已经或正在其硬件上支持OpenCL。

无替代选项:无论是NVIDIA的CUDA,还是微软的C++AMP和Google的Render Script,都没有获得大量厂商的支持,只有OpenCL得到了几乎所有相关主流硬件厂商的支持。

由于具有高性能、适用性强、开放和没有替代方案,未来OpenCL必将在异构并行计算领域占有不可动摇的地位,甚至一统异构并行计算领域。

1.MPI 3对异构并行计算的支持

经典的分布式存储并行标准MPI在其版本3中明确了允许在数据传输函数调用时使用异构平台上的指针的内容,并且同时在标准中扫清了相关的数据匹配等问题。这使得MPI在进行数据传输时,能够直接传递指向GPU或其他硬件上内存地址的指针。从另外一个角度来说,MPI 3对异构并行计算的支持是象征性的,只是把几家MPI实现厂商关于CUDA的优化升级为标准中的一个内容固定下来,未来的MPI 4会有更多对异构并行计算支持的重量级内容。

对于使用OpenCL的软件开发人员来说,MPI 3对异构的支持几乎为0,因此本节也就不展开了。

2.OpenMP对异构并行计算的支持

经典的共享存储器并行编程环境OpenMP在其4.0版标准中增加了许多支持异构计算的构造,如target、target data等,考虑到本书的定位,关于OpenMP 4对异构并行计算的具体支持内容,请参见刘文志(花名风辰)的著作《并行编程方法与优化实践机械工业出版社华章公司出版,书号978-7-111-50194-7。的3.5节。

在笔者编写此书时,GCC和Clang的OpenMP都在实现OpenMP 4标准,相信当读者拿到本书时,GCC已经部分或完全支持OpenMP 4标准了。

3.OpenCL无处不在

在OpenCL推出之前,异构并行计算领域主要是基于X86 CPU+GPU,这个市场主要有NVIDIA和AMD两个玩家,其中NVIDIA的CUDA占据了绝大多数的市场份额,AMD的brook+勉力维持,因此NVIDIA实际上成了这个市场的垄断霸主。

AMD很清楚brook+不是CUDA的对手,因此在OpenCL推出后,立刻全面转向OpenCL,放弃brook+。AMD不但在其GPU上全面支持OpenCL,还在其X86 CPU上支持OpenCL,是第一个提供CPU+GPU全面支持OpenCL的厂家。AMD转向OpenCL后,和OpenCL的编程模型紧密结合,推出了备受赞誉的GCN系列GPU,GCN系列使得AMD有实力在异构并行计算市场上和NVIDIA一较高下,并且不断地从NVIDIA口中夺食。

NVIDIA CUDA进入超算中心后,Intel意识到危险,如是开启了GPU计算硬件项目(即larrabee,是今天MIC的前身),但是Intel之前的大多数工具都不是为了异构并行计算设计的,Intel迫切地需要设计或改进一种语言以满足其GPU计算硬件的需要。一开始Intel选择了MPI和OpenMP,由于在MIC上运行MPI程序遇到了许多问题,因此OpenMP成为事实上的唯一选项。Intel通过修改其OpenMP实现,在其OpenMP实现中加入了支持异构并行计算的内容,Intel的OpenMP实现中关于异构并行计算的扩展后来成为OpenMP 4的基础,在OpenMP 4标准制定时,标准委员会有OpenACC(参见附录B)和Intel的OpenMP扩展两个选择,最终Intel赢了。不久之后,Intel意识到OpenMP并不能很好地发挥MIC的性能,而且随着其业务扩展,Intel可能要为每种产品都设计一个扩展(如为其集成GPU架构Gen设计一个,为以后的嵌入式CPU/GPU又要设计一个)。Intel最终很不情愿地推出了其OpenCL支持方案,但是这种支持是全面且一劳永逸的,无论是Intel的X86 CPU、MIC GPU,还是Intel X86 CPU集成的GEN架构GPU都得到了支持。一个统一的、基于OpenCL的平台,能够让开发人员编写的一份代码运行在Intel的所有处理器上,这将会是Intel历史上非常明智的举动之一。

2013/2014年,主流的移动处理器厂商也推出了基于其移动GPU的OpenCL编译运行环境,如Imagination Technology的PowerVR系列移动GPU、高通的Adreno系列移动GPU、ARM的Mali系列GPU都支持了OpenCL。与此同时,主流的FPGA厂商Altera和Xilinx也推出了其OpenCL编译运行环境。

今天OpenCL已经广泛应用于分子动力学模拟、计算流体力学、图形图像处理、视频音频处理,目前正在用于计算机视觉等领域。许多研究人员正在研究如何将OpenCL应用于大数据处理等更多领域。

1.5 本章小结

本章介绍了异构并行计算的历史、现状和未来,并且介绍了关联的许多概念。在2005年之前,处理器通常提高频率来提高计算性能,由于性能是可预测的,因此在硬件生产商、研究人员和软件开发人员之间形成了一个良性循环。由于功耗的限制,处理器频率不能接着提升,硬件生产商转而使用向量化或多核技术。

随着GPU计算的兴起,CUDA和OpenCL渐渐获得了广泛的关注,异构并行计算从学术界走向工业界,获得了大众的认可。今天几乎所有主流的处理器硬件生产商都已经在支持OpenCL,未来OpenCL必将无处不在。