1.1 为什么选择GPGPU?什么是异构计算?
C++ AMP:用Visual C++加速大规模并行计算
作为开发者,面对周围不断变化的世界,努力调整自己,这种生活我们早已习以为常。IT行业对世界的影响自成体系。我们学习新的语言,使用新的方法,考虑采用新的用户界面,并理所当然地认为它们都能让我们的程序变得更好。为了让下一版比上一版更完善,如果认为采用某种方法会碰壁,我们就会转而换用另外一种方法。而有些开发者现在要使用的最新方法就是异构计算。
本章将回顾计算性能提升的历史,让读者们看看开发者们都碰到过哪些问题。我们将了解CPU和GPU的本质区别,CPU和GPU是异构计算解决方案的两个组成部分。同时我们还会了解什么问题适合使用并行技术进行加速。随后,本章介绍了现在广泛使用的CPU和GPU并行技术,以及C ++ AMP的理论概念,为后续章节做好了铺垫。
1.1.1 性能提升史
回到20世纪70年代中期,那时计算机仍然只是个人使用的奢侈品,还没有得到广泛应用。追溯往昔,“个人计算机”这个词最早只能到1975年。在随后几十年间,每张办公桌上都放置一台计算机的想法已经从一个雄心勃勃的甚至是看起来不可能的目标变成再平常不过的想法。事实上,现在许多桌子上都不止搁着一台电脑,就连客厅里也如此。很多人口袋里随身携带着一台称作智能手机的小小电脑。在前30年的膨胀发展过程中,电脑不只是变得更便宜和更普及,也变得越来越快。芯片厂商每年都会发行时钟频率更高、高速缓存更大、性能更好的芯片。开发者也乐意在他们的软件里添加新的特性和功能。开发者并不需要担心这么做会让软件运行得更慢,因为每隔6个月到1年,就会出现速度更快的计算机,软件运行又会更快,响应时间也会更短。这就是由于硬件性能的不断提升而带来的“免费午餐”。最终,十亿次浮点运算每秒(gigaFLOPS)级别的性能,即每秒数十亿次浮点运算,也变得触手可及,可以承担了。
不幸的是,这个“免费午餐”大约在2005年走到了尽头。尽管制造商继续在单个芯片上植入更多的晶体管,但由于物理限制,例如芯片散热问题,决定了时钟频率不可能持续不断地增加。然而,市场一如既往地对性能更强的机器情有独钟。为了迎合这种需求,制造商开始出售多核A计算机,在一台电脑中安装两颗、四颗或更多颗CPU。“一名用户、一颗CPU”曾经是一个崇高的目标,但是“免费午餐”时代结束后,用户需要的再不仅仅是单核CPU,对于多核的需求自桌面计算机开始,然后到笔记本电脑,最后到智能手机。在过去五六年间,每张桌子上、每间客厅里、每个人的口袋中都能找到并行巨型计算机,这种现象已极为普遍。
但是,只增加内核并不能让程序运行得更快。软件大致可以分为两大类:可并行的和不可并行的。不可并行的软件通常只使用一半、四分之一或者八分之一的CPU核。它只能在单核上运行,每次当用户拿到配备了更多CPU核的新计算机时,都会错过性能提升的机会。开发者已经学会了当有更多CPU内核可供使用时,要如何编写软件才能让软件性能近乎线性地增长。换句话说,性能增长同计算机核数相关,即在双核计算机上两倍增长,4核计算机上4倍增长,依此类推。懂行的消费者可能会感到奇怪,为什么有些开发者会无视能够给应用程序带来额外性能提升的机会。
1.1.2 异构平台
在多核CPU计算机出现的五六年间,大多数计算机上使用的显卡同样也在发生着变化。与CPU只有双核或四核不同,后开发出来的GPU拥有几十核甚至数百核。GPU内核与CPU内核截然不同。最初把它们开发出来是为了提高图形相关计算的速度,例如确定屏幕上某个像素点的颜色值。做这种工作,GPU的速度确实要比CPU更快,而且因为现代显卡包含很多内核,因此可以进行大规模并行计算。很自然地,利用GPU进行图形无关数值运算的想法很快就被提上了台面。同时拥有CPU和GPU内核的计算机,无论它们是否做在同一块芯片上,都被认为是异构超级计算机,拥有CPU和GPU搭配的计算机集群,也是如此。显然,下一个时代是张张桌面上、每个客厅里、各个口袋中都有异构超级计算机的时代。
时至2012年年初,典型的CPU配置是4核、双超线程,约10亿个晶体管。顶级CPU在做双精度计算时,峰值可以达到约0.1 TFlop或100 GFlops。同样在2012年年初,典型的GPU配置是32核、32线程,大约两倍于CPU的晶体管数量。顶级GPU在做单精度计算时,峰值可到3 TFlop,约是CPU峰值计算速度的30倍。
注意事项:
有些GPU支持双精度,有些不支持,但报告里的性能数据一般都是单精度的。
GPU可以达到更高计算速度并不仅仅是因为晶体管的数量或者核数。CPU的内存带宽较低,仅有20GB/s,而GPU的内存带宽却有150GB/s。CPU支持通用代码,包括支持多任务处理、I/O、虚拟化、深执行管线和随机访问等特征。与此相反,GPU是为图形和数据的并行执行而设计的,其特征包括固定功能处理器、浅执行管线和顺序访问等。实际上,GPU的速度提升仅适用于针对GPU设计的任务,而不是通用任务。比速度更重要的是,GPU的功耗低,CPU的功耗约为每瓦10亿次浮点运算(1 GFLop/W),而GPU的功耗约是10 GFLop/W。
对于许多应用而言,执行特定计算的耗电量可能比耗时指标更重要。手持设备,例如智能手机和笔记本电脑,需要电池供电,因此用户往往会明智地选择耗电量较小的兼容应用代替耗电较快的应用。对笔记本电脑来讲这也是一个问题,笔记本用户希望的是全天使用电池,同时还要运行执行重要计算的应用。在智能手机等小型设备上多个CPU和一个GPU的配置已经司空见惯。一些设备可以针对单核调整供电量,从而达到调节电池续航时间的目的。在这样的环境中,将一些计算移植到GPU上做意味着对“有些应用只能在办公室中运行,因为它太耗电”和“我的生活中不能没有这些应用”这两个选项提供了不同的选择。另一个极端是,数据中心的主要运行成本是数据中心的供电成本。大规模数据中心计算或云计算消耗的电量如能节省20%,将会直接给能源成本带来本质改变。
另一个问题是这些内核访问的内存。谈及计算速度,高速缓存大小要比时钟频率更重要,所以CPU要有一个大高速缓存,确保总有数据要处理,这样预取数据时CPU核基本不需要等待。CPU运算往往会重复访问相同的数据,高速缓存方法因而会起到巨大作用。相比之下,GPU的高速缓存比较小,但GPU会使用大量线程,这样某些线程就会一直处在工作状态。GPU可以通过预取数据隐藏内存延迟,但因为数据很可能只被访问一次,所以高速缓存带来的好处不大,显得并没有那么必要。对于这种工作方式,理想情况下,最好要有大量数据,并且只需要对数据做简单计算。
也许最重要的区别在于开发人员如何使用这两种技术进行编程。市面上有许多主流的CPU编程语言和工具。就功耗和性能而言,C++是首选,可以在不放弃控制权的情况下提供抽象和强大的功能库。对于通用GPU编程(GPGPU)来讲,选择受到更多的限制,在大多数情况下,需要用到能满足特定客户群的、奇异的编程模型。这一限制意味着,到目前为止,只有少数领域和问题才能够充分利用GPU的强大能力,解决计算密集型的数值计算。这同时也意味着,主流开发者的选择往往会是避免学习和使用GPU。开发者需要的是能提高应用程序速度或者能减少某个特定计算耗电量的方法。今天,这些好处都可以从使用GPU中获得。理想的解决方案是,现在先通过GPU获取这些好处,将来可以选择使用其他异构计算模式获益。
1.1.3 GPU架构
如前所述,GPU的执行管线浅、高速缓存小,但具有为数众多的线程可以执行顺序访问。这些线程并非是完全独立的,它们被编排成组。在NVIDIA硬件系中,这些线程组称作“warp”;在AMD硬件系中,被称作“wavefront”。本书中,我们称其为“warp”。多个warp一起运行,共享内存,相互协作。本地内存可以在短短的4个时钟周期内被读取,而更大(最高至4 GB)的全局内存访问可能需要400~600个周期。如果一组线程由于读操作阻塞,另一组线程可以同时执行。GPU可以用非常快的速度切换这些线程组。当相邻线程使用相邻的内存时,内存读取有巨大的速度优势。但是,如果一组中某些线程所访问的内存与该组中其他线程所访问内存并不相邻,性能将受到影响,如图1-1所示。
图1-1
CPU和GPU二者的架构有很大的不同。使用高级语言的开发者一般都不需要理会CPU架构。操作系统和优化编译器等底层工具需要掌握CPU架构,同时编译器和操作系统会将很多“普通”应用与硬件细节隔离开。我们认为不言自明的最佳实践或经验法则也许本身并非如此。例如,导致CPU高速缓存未命中的简单的整数加法所花的时间,要比只访问高速缓存附近缓冲的文件内容的读操作的长。
一些开发者发现,在编写对性能高度敏感的应用时,自己需要了解高速缓存未命中所损失的时间能执行多少条指令,或者从一个文件(文件字节数动不动就会以百万计)中读取一个字节需要多少时钟周期。如果我们现在要使用类似GPU的非CPU架构,这方面的知识就必不可少。CPU编程中编译器和操作系统所提供的保护层在GPU中并不存在。例如,我们需要知道一个warp有多少个执行线程,或者共享内存缓存的大小。我们在设计计算时,可以让迭代只涉及相邻内存的操作,避免随机访问。如果需要了解应用程序可以达到的加速比,那么至少在概念层面上,必须得弄清楚硬件的组织方式。
1.1.4 通过并行性提升性能的候选方案
GPU对数据并行问题处理得最好。大问题该如何分解成为诸多小问题,使处理器可以分而治之地并行处理,有时候一眼就能看出来。以矩阵加法为例,结果矩阵中的每个元素都可以完全独立地进行计算。100×100的矩阵加法会产生10 000次加法操作,但我们可以把这些操作分配到10 000个线程上,这样所有加法就可以一次完成。矩阵加法就是一个典型的数据并行问题。
在其他情况下,我们需要设计算法将工作分散到独立的线程上去执行。例如,要在一个由大量数字组成的集合中寻找最大值的问题。我们可以选择遍历每个列表元素,将每个元素与“当前的最大值”进行比较,如果碰到更大的值,就更新“当前的最大值”。如果集合中有10 000个值,就需要进行10 000次比较。或者,还可以创建一定数量的线程,分别给每个线程分配一个集合的部分子集进行计算。譬如说有100个线程,每个线程处理100个数,分别确定各份子集中的最大值。这样只需要进行100次比较。最后,第101个线程可以比较100个“各个子集最大的”数字,从而找到整体最大的数。调整线程数也就相应地调整了每个线程的比较次数,可以最大限度地减少查找集合最大值的时间。当比较成本远大于创建线程的成本时,可以采取一种极端的做法:创建5 000个线程,每个线程比较两个值,然后2 500个线程比较首轮最大值,1 250线程比较第二轮最大值,一直持续迭代下去。用这种方式,14轮之后就能找到最大值,我们只需要14次的比较开销。“锦标赛”方法还适用于其他操作,例如将集合中的值全部相加,求解某个区间内有多少个数值等。归约求解(reduction)`这个术语经常被用来表示在大规模数据集中查寻某个数值(总计、最小值、最大值等)的问题。
事实证明,任何涉及大量数据的问题都会自然选择并行处理方法。率先使用这种方法的是以下几个领域。
科学建模与仿真。物理学、生物学、生物化学,以及诸如此类的学科都会使用简单的方程来模拟需要对海量数据执行计算的超复杂场景。计算的数据越多,仿真就越准确。只有当一次仿真可以在一个合理的时间段内执行时,我们才会认为这个仿真测试理论是可行的。
实时控制系统。从海量传感器合并收集到的数据,明确操作范围,调整控制装置复原到最优运行态,这些都是高风险的处理过程。火灾、爆炸、高代价停机,甚至生命损失,都是该类软件系统要竭力避免的场景。通常,读取传感器的数量会受到计算时长的限制。
财务跟踪、模拟和预测。面对高度复杂的计算,一定要有大量的数据支撑,才能识别出趋势、找出差距和盈利点。时机转瞬即逝,我们必须把握住,把机会识别出来,设置计算时间上限。
游戏。大多数游戏基本上都是对现实世界的一种模拟,或是对遵循某种不同的物理法则运转的、经过精心构造的世界的一种模拟。物理计算用到的数据越多,游戏的可信度就越高,但同时性能不能因为计算量的增长而被拉低。
图像处理。无论是医学图像异常检测,还是安保视频面部识别和指纹匹配,抑或是执行数十项类似工作其一,都要在有限的图像处理时间内避免误报(false negative)和漏报(false positive)。
在这些领域,当计算密集型应用达到10倍加速比时,我们就能获得两种能力之一。最容易得到的收益是,虽然计算数据量更大,但计算时长并没有增加。通常来讲,结果会更准确,最终业务用户也会对自己的决策更有信心。真正有趣的地方在于,加速比是在何时让不可能变成可能的。例如,如果我们可以在短短两个小时内完成以前耗时20个小时的财务指标计算,闭市后在夜间工作,这样第二天早上就可以根据计算结果再采取行动。假如我们实现了百倍加速比呢?以前需要1 000小时的计算,要40多天才能完成,算完的时候数据可能都已经过期了。但是,如果相同的计算只需要10个小时,一个通宵就能完成的话,结果就更可能仍然是有意义的。
时间窗口并非是金融财务软件独有的特性,它同样适用于安全扫描、医学成像,以及其他应用,包括密码破解和数据挖掘中的一系列十分可怕的应用。如果蛮力密码破解要用40天,而人家每隔30天就变换一次密码,那密码就是安全的。而如果破解操作仅需要10个小时就能完成,情况又会怎样?
相对而言,达到10倍加速比相对简单,但要达到100倍加速比就要困难得多。并非是因为GPU做不到,而是因为应用的某些部分无法实现并行化。比方说有三个应用程序,它们执行某项任务都需要100个单位时间。第一个应用里不可并行的部分(例如,把报表送至打印机的程序)占总时间的25%。第二个应用里占总时间的1%,第三个应用仅占总时间的0.1%,如表1-1所示。如果我们加快每个应用程序的可并行部分,会怎么样?
当第一个应用程序并行部分的加速比达到10倍时,其串行部分花的时间比并行部分花的时间多得多。而整体加速只有3倍多一点。就算为并行部分找到一个100倍加速比的方法也没有太大的帮助,因为串行部分花的时间相对很多。即使获得了无穷倍的加速比,将并行部分的执行时间降为0,也无法抹去串行部分所消耗的时间,整体加速比仍被限制为4倍。10倍加速比时其他两个应用程序有比较好的表现,但第二个应用程序不能从100倍加速比中获得更大的收益,整体只能获得50倍加速比。即使有无穷倍的加速比,第二个应用程序的整体加速比也会被限制到100倍。
这看起来有些矛盾,刚开始时,串行部分所占比重无论多小,最终都将成为加速比的决定因素,这就是所谓的阿姆达尔定律。这并不是说100倍的加速比是不可能达到的,而是意味着选择算法尽量减少非并行部分所花费的时间,对最大程度提高性能是非常重要的。此外,当选择一个数据并行算法时,使用GPGPU加速应用获得的好处远比选择一个高速高效高度串行化但无法并行的算法要好得多。百万数据点的问题解法可能并不适用于1亿个数据点的问题求解。