《OpenACC并行程序设计:性能优化实践指南》一 2.2 描述并行度

2.2 描述并行度

已经获知了代码最为耗时的部分,接下来开始并行化重要的循环体。通常最好的优化方法是始于最为耗时的子程序,逐步向下探索。加速耗时75%的代码,效果优于加速仅耗时15%的代码。这表明应该首先致力于matvec子程序的加速,然后再对waxpby和dot进行加速。但是,因为这可能是读者的OpenACC处女行,所以从这三个函数中最为简单的一个开始,逐步改进,直至优化最为复杂的子程序。这就是并非首先加速matvec函数的原因。

2.2.1 加速waxpby

从vector_functions.h第33行代码开始,并行化waxpby子程序。该函数仅包含一个循环,通过向循环体起始点添加OpenACC kernels编译指导命令进行并行化。通过添加该编译指导命令,告知编译器,该循环具有OpenACC编译指导命令,要求编译器为目标加速器生成代码。将要在装有NVIDIA Tesla K20c GPU的计算机上执行这段代码,因此,选择tesla目标加速器。激活OpenACC对NVIDIA GPU的支持,需要使用-ta=tesla命令行选项。尽管编译器的目标加速器是用于大数据中心硬件环境的NVIDIA Tesla GPU,目标代码依然可以运行在其他的NVIDIA GPU上。对makefile文件进行的修改见图2-5,编译器对waxpby子程序编译产生的反馈信息见图2-6。

从编译器反馈信息可知,尽管生成了一个GPU核函数,但当编译器试图并行化循环体时发生了错误。编译器发现循环变量存在依赖性。当某次循环依赖于其他循环迭代中的数据时,数据依赖就产生了。经过仔细检查循环体,发现循环间是相互独立的,循环体中的变量与其他循环中的变量是毫不相关的。那么问题产生了,为什么编译器认为该循环存在数据依赖呢?这个问题是由于C和C++编程语言的底层特性导致的。C/C++语言使用指针表示内存中的数组,但不同的指针很可能指向相同的内存。问题在于,编译器无法证实循环体中的三个数组不是别名的或相互重叠的。因此,谨慎起见,编译器假定该循环的并行化是不安全的。关于这几个数组,应该向编译器提供更多的信息。一个可选的做法是向编译器提供更多关于指针的信息,告知编译器这些指针不是指向同一个内存地址的。添加C99的restrict关键词可以解决这个问题。尽管这不是C++关键字,PGI编译器仍可在C++代码中辨识出该关键字还可以通过使用OpenACC loop导语向编译器提供关于循环自身的更多的信息。loop导语告知编译器关于紧邻该导语的下一个循环的额外信息。可以通过loop independent子句告知OpenACC编译器,该循环的所有迭代是相互独立的,即任意迭代间的数据相互不依赖。这告知编译器不要采用默认的循环体数据依赖性的假定。以上两种措施保证了程序员对编译器的控制。如果假定被破坏,将产生不可预测的结果。该代码中,无法确保向量不是别名化的,因为有时候该函数调用时,传递进来的y和w可能是同一个向量。因此,使用loop independent来替代。waxpby最终代码见图2-7,编译器反馈信息见图2-8。从编译器反馈信息可知,现在,第41行对应的循环已经是可并行化的了。

仔细检查编译器反馈信息,发现编译器不仅并行化了该循环,它还为GPU做了更多的编译工作。编译器发现,目标加速器与主机CPU使用的是不同的物理存储器,因此,有必要将输入数组在GPU上进行内存分配,计算完成后将结果数组拷贝回主机端。第40行输出显示,编译器发现xcoefs和ycoefs是输入数组,因此编译器为它们生成了面向GPU的copy in操作。wcoefs是输出数组,因此编译器为它生成了拷贝出GPU的copy out操作。反馈信息实际上显示了data子句,提供了关于数据是如何在计算区域使用的额外信息。在本示例中,编译器能够正确地确定这三个数组的大小、形状和用法。因此,编译器为这些数组显式生成了一些data子句。有时候,编译器对于数据移动表现得过于谨慎,因此,程序员必须重写这些默认的data子句,并在kernels导语后添加相应的显式data子句。其他情况下,编译器实际上无法获知计算区域中数组的大小和形状,因此编译器将停止运行并向程序员提出询问。这种情况与在优化matvec子程序中遇到的情况类似。
在继续下一步任务前,重新编译和运行一遍代码并检查可能由于并行化而引入的错误是十分必要的。对代码进行小改动后的排错是很容易的,难度远小于大幅改动代码后再进行DEBUG。运行代码后,发现计算结果是一致的,但是代码运行速度反而下降了。如果使用PGProf性能分析器运行可执行文件,将能够发现代码减速的原因。图2-9展示了性能分析器收集的GPU时间线,为清晰起见,这里进行了一些放大。从时间线上可以看到独立的GPU运算(核函数)和相关的数据移动。注意到,对于每次waxpby调用,将两个数组拷贝到设备中,计算完成后将一个数组拷贝回主机端供其他函数利用。这种做法非常低效。理想情况下,希望数据尽量驻留在GPU存储器中,并极力避免这些不必要的即时拷贝。解决这一问题的唯一途径是将其他函数也进行并行化,这样数据移动就不再必要了。

2.2.2 加速dot

快速浏览一下dot子程序,编译器将利用OpenACC kernels编译指导命令。与之前一样,将要对感兴趣的循环添加kernels指导命令并且重新编译代码。图2-10展示了该函数的OpenACC版本,图2-11展示了编译器反馈信息。从反馈信息可知,第29行中生成了一个隐式归约(reduction)。循环的每次迭代计算其自身对应的xcoefs[i]*ycoefs[i]值,但编译器认为程序实际上并不关注各循环的计算结果,而是关注这些结果的和。

归约实现了循环中n个不同数值计算以及最终求得它们的和。由于浮点数计算的天生误差,需要明确,并行归约的计算结果虽然同样正确,但这个结果与串行计算结果略有差别。计算结果差别的大小与被加数本身、被加数的个数、加法执行的次序等因素有关,但结果的差别可能只有几位数字。切记,串行结果和并行结果都是正确的,因为浮点数计算本身就是不精确的。由于使用了kernels编译指导命令将循环并行化,编译器将帮助处理和辨识复杂的归约计算。如果使用了更为高级的parallel编译指导命令,而非kernels,并行归约计算的操作讯号的传递这一任务将落在程序员的肩膀上。在转到下一部分前,请不要忘记再次运行代码来检查错误。

2.2.3 加速matvec

最后一个需要加速的子程序是matvec。如前所述,该函数是一个恰当的起点,可作为一个教学练习。该子程序是目前为止最重要的程序,且该子程序具有一些有趣的复杂性,它要求采取额外的手段来表达并行性。采用其他两个子程序中类似的手段开始并行化。对数组添加restrict关键字,使用kernels导语修饰循环。当编译子程序时,编译器报错了,见图2-12所示。

第34行显示的加速器限制是这段编译器输出结果中最重要的部分。它表示编译器无法获知kernels区域中数组的大小,尤其是最内层循环中出现的三个数组。在之前的两个例子中,编译器可以基于循环迭代次数来确定有多少数据需要迁移到加速器上,但由于matvec内部的循环边界是隐藏在构建的矩阵数据结构中的,因此编译器无法获知如何将关键的数组迁移到加速器上。这个例子非常典型,需要给kernels导语添加显式data子句来提示编译器。表2-1列出了五种常用的data子句和它们的意义。已经见过copyin和copyout子句,它们在之前的例子中已被编译 器隐式使用了。

为了在GPU上通过编译器实现matvec中循环的并行化,需要实现数据的卸载。至少需要告知编译器循环中涉及的三个数组的形状。这三个数组用来读取,故可以使用copyin子句,但需要告知编译器数组的形状,通过查找matrix.h文件中的allocate_3d_poisson_matrix函数可以获知这一信息。data子句接受一个变量列表,内含变量大小和形状的信息。C和C++中,数组的大小和形状通过方括号([和])标明。方括号内含起始索引和需要拷贝的数组元素的个数。例如[0:100]表示从索引0处开始,拷贝100个数组元素。Fortran语言中使用圆括号,且使用拷贝的第一个元素和最后一个元素的索引进行标明。例如(1:100)表示从索引1开始到索引100结束进行数组元素的拷贝。语法上的不同是为了刻意保持编程风格与相应的编程语言语法传统相一致。因为Fortran语言的变量可以进行自我标定,因此,对于向设备端进行整个变量的拷贝时,数据大小和形状信息就可以省略。
图2-13展示了修改后的matvec函数代码。注意到向kernels导语添加了copy数据子句,告知编译器如何在设备端进行存储空间分配,且该数据仅作为循环的输入。通过这些修改,现在可以在加速器上运行完整的共轭梯度计算并行版本程序并获得正确的输出。但如图2-14所示,运行时间比原始代码反而变长了。

重新通过PGProf性能调试器运行可执行程序,检查为何GPU并行代码运行如此缓慢。图2-15展示了PGProf反馈的GPU时间线,为了显示得更为清晰,这里进行了放大。

时间: 2024-09-19 23:57:12

《OpenACC并行程序设计:性能优化实践指南》一 2.2 描述并行度的相关文章

《OpenACC并行程序设计:性能优化实践指南》一 1.1 简单的数据并行循环

1.1 简单的数据并行循环 在顺序处理器程序设计中,需要编写计算某个最终结果所需要的任务和数据操作的程序.通过创建OpenACC,编程人员可以插入编译指令给编译器提供信息,而这些编译指令是关于并行机会和数据在加速器与主机间来回传输的信息.结合编译器,程序员使用注记来创建.调试和优化并行代码,使得程序达到高性能. OpenACC帮助程序员编写高效的数据和任务并行软件. 数据并行关注跨多个并发执行线程的分布式数据操作.在计算机科学中,线程是串行执行一段代码的线程的缩写.通过使用多个线程,应用程序可以

《OpenACC并行程序设计:性能优化实践指南》一 导读

前言 欢迎阅读本书,这是一本由浅入深的书籍,从初学者到高级开发人员,都可以通过本书了解OpenACC的相关知识.本书由世界各地的24位作者共同编著而成,他们在高度并行编程的教学和实践方面分享了自己的专业知识.书中的例子既有时效性又不会过时.每个章节都是自包含的,可用于自学,也可以作为课堂教学的一部分. 这是一本关于并行编程的书,不仅仅介绍OpenACC语法或从文档中收集的信息,更介绍了如何编写实际的.高性能的以及可移植的程序,这些程序可以运行在从CPU到GPU的大量设备上.具体而言,书中演示了使

《OpenACC并行程序设计:性能优化实践指南》一 第3章 使用Score-P和Vampir分析混合应用性能

第3章 使用Score-P和Vampir分析混合应用性能 Guido Juckeland 德国亥姆霍兹联合会(HZDR)信息服务和计算机系 Robert Dietrich 德国德累斯顿工业大学 本章的目的是让读者熟悉逐步性能提升的概念,以及在向OpenACC应用程序添加其他并行模式时所涉及的工具.混合应用程序可能会遭受许多性能瓶颈,应用程序运行期间所有活动的整体图可以揭示如何提高整体性能. 阅读本章后,读者将会理解以下内容: 混合应用程序(例如,MPI+OpenACC)性能分析的术语和方法. 如

《OpenACC并行程序设计:性能优化实践指南》一 1.3 Amdahl定律及其扩展

1.3 Amdahl定律及其扩展 绘制任务运行时间,可以看到并行增加应用程序的扩展行为.并行计算的理论性能:运行在有N个处理单元的并行计算机上,理论上可以获得N倍加速.换一句话说,一个程序运行在10核处理器上可能获得10倍加速(对于固定大小的问题),在支持1000个并发执行线程的GPU上获得1000倍加速.开发人员依据Amdahl定律来讨论并行与串行间的加速比. 用计算机架构师Gene Amdahl来命名Amdahl定律.它不是实际上的定律,但是修改串行程序使其并行执行时,它相当接近模型理论加速

《OpenACC并行程序设计:性能优化实践指南》一 1.4 并行执行和竞争条件

1.4 并行执行和竞争条件 OpenACC并行化for循环(Fortran中是do循环),因此循环内的代码使用并发硬件执行线程并行执行. 循环内的变量i似乎是顺序递增的,但实际上在这个for循环中使用多个i变量的线程可能同时并行执行,这可能有点令人困惑.OpenACC不保证线程执行的顺序,注意这点非常重要.实际上,甚至不可能假设单调性.例如,很有可能第nCount―1次迭代实际上先于第0次迭代执行完. OpenACC不保证线程执行的顺序,注意这点非常重要. 总之,OpenACC编程人员不能也不应

《OpenACC并行程序设计:性能优化实践指南》一 2.6 小结

2.6 小结 OpenACC是一种描述型并行编程模型.在本章中,通过一个测试函数的应用,使用了OpenACC的多种特性来描述并行度和数据操控,并针对特定平台对代码进行了优化.尽管使用的是PGI编译器和PGProf性能调试器,但类似的优化流程也是适用于任何支持OpenACC工具包的应用的. 1.获得应用程序的性能分析结果,辨识和挖掘代码中的可并行之处. 2.逐步向编译器描述代码中可挖掘出的并行性.如果主机端和设备端使用各自的存储器,这一步骤后获得的代码很可能会减速. 3.描述应用程序的数据移动.编

《OpenACC并行程序设计:性能优化实践指南》一 2.5 在多核系统中并行运行

2.5 在多核系统中并行运行 尽管本章使用了NVIDIA GPU,但OpenACC不是一个GPU编程模型,而是一种普遍适用的并行编程模型.尽管在2.4节中使用的循环优化技术仅适用于GPU,关于并行度和数据移动的技术手段适用于任意并行架构设备.本章使用的PGI编译器支持多种目标加速器,包括NVIDIA和AMD公司的GPU,以及多核x86 CPU.如果在多核CPU上开发和运行代码,将会发生什么呢?为多核目标平台重新编译代码,而不是将目标设定为tesla(见图2-29和图2-30). 如果运行可执行程

《OpenACC并行程序设计:性能优化实践指南》一 3.2 逐步性能提升

3.2 逐步性能提升 本书的示例表明,通过提交更多的活动和优化数据传输,使用性能分析驱动的开发可不断提升OpenACC应用程序的性能.图3-2列出了优化混合应用遵循的模式. 从应用准备开始性能优化周期,然后进行实际的性能测量,并对性能数据进行分析.基于这些数据,编程开发人员尝试减少性能问题,并重新开始整个过程.下面使用Score-P和Vampri讲解性能提升周期里的前三步,以CUDA加速粒子单元模拟为例,其中CUDA部分代码很容易用OpenACC实现代替来获得同样的结果.此外,还引入了各种优化步

《OpenACC并行程序设计:性能优化实践指南》一 1.5 无锁编程

1.5 无锁编程 互斥锁是用于同步进程或线程的常用机制,这些进程或线程需要访问并行程序中的一些共享资源.互斥锁就像它们名字所说的:如果一个线程锁住了资源,另一个线程希望访问它需要等待第一个线程解锁这个资源.一旦资源被解锁,第二个线程在处理这个资源时会一直锁住它.程序的线程必须遵守:一旦使用完共享资源尽快解锁,以保持程序执行流程. 由于OpenACC中没有锁,编程人员需要熟悉无锁编程和数据结构的概念.无锁方法保证至少一个执行该方法的线程的进展.可能存在某些线程可以被延迟的情况,但是保证至少一个线程