本节书摘来自华章出版社《多核与GPU编程:工具、方法及实践》一书中的第2章,第2.4节, 作 者 Multicore and GPU Programming: An Integrated Approach[阿联酋]杰拉西莫斯·巴拉斯(Gerassimos Barlas) 著,张云泉 贾海鹏 李士刚 袁良 等译, 更多章节内容可以访问云栖社区“华章计算机”公众号查看。
2.4 程序结构模式
模式不仅可以帮助选择合适的工作负载分解方法,还可用于程序的开发,这正是程序结构模式的目标。接下来的一节将讨论和分析几个最著名的模式。
并行程序结构模式可以分为两大类。
全局并行局部串行(Globally Parallel Locally Sequential,GPLS):GPLS表示应用程序可以并发执行多个任务,每个任务串行执行。这类模式包括:
单程序多数据
多程序多数据
主/从
map-reduce
全局串行局部并行(GSLP):GSLP表示应用程序串行执行,当需要时,一些单独的部分可以并行执行。这类模式包括:
fork/join
循环并行
两种分类的区别在图2-12的描述中更加明显。GPLS模式更易于提供更高的可扩展性,尤其对于无共享的体系结构。而GSLP常用于将串行程序变为并行时,并行的通常是最影响性能的部分。
2.4.1 单程序多数据
对单程序多数据(Single Program Multiple Data,SPMD)模式,执行平台的所有节点都运行相同的程序,但它们或者将相同的操作用于不同数据,或者执行程序中不同的执行路径。
将所有的应用逻辑保存在一个程序中促进了更简单及无故障的开发,使SPMD成为受程序员欢迎的选择。典型的程序结构包括下面的步骤。
程序初始化:这个步骤通常包括将程序部署到并行平台,并初始化负责多线程或进程通信及同步的运行时系统。
获取唯一的标识符:标识符通常从0开始计数,枚举使用的线程或进程。一些情况下,标识符可以是向量而不是标量(如CUDA)。标识符生命周期与其标识的线程或进程一致。标识符也可以是持久的,即在整个程序过程中都存在,或者在需要时动态产生。
运行程序:执行与唯一ID一致的执行路径,这里可能包括工作负载或数据分配、角色多样化等。
关闭程序:关闭线程或进程,可能需要将部分结果合并产生最终结果。
SPMD方法很方便,但也有缺点:所有应用程序的代码和静态(即全局)数据都要在所有节点复制。这可能是一个优势,但当所有上述条目都不需要时,这也会是个不足。
2.4.2 多程序多数据
SPMD很灵活足,以覆盖大部分的情况,只有在下列情况下有不足:
若执行平台是易异构的,则需要根据节点的体系结构布局不同的执行文件。
应用程序内存需求非常严苛以至于需要降低上载到每个节点的程序逻辑以保证基本的要件。
多程序多数据(MPMD)模式通过允许可能来自于不同工具链的不同执行文件被组合成一个应用程序来覆盖上述情况。每个计算几点都可以运行自己的程序逻辑处理自己的数据集,但可能仍要遵从前一节识别出的步骤序列。
大部分主要的并行平台都支持MPMD模式,一个特别的例子是CUDA,其程序被编译为单独的文件,但实际包含两种不同的二进制:一个给CPU主机,一个给GPU协处理器。
大部分情况下,只需要将不同执行文件映射到合适的计算节点的配置文件。这种例子将在5.5.2节中展示。
2.4.3 主/从
主/从范例将计算节点的任务分为两种,主节点的职责包括:
将工作发放给从节点
从从节点收集计算结果
代表从节点执行I/O职责,例如给它们发送需要处理的数据或访问一个文件
与用户交互
对最简单的形式,主/从模式只包含一个主节点和若干个从节点。然而,这种安排不能随节点数扩展,因为主节点可能成为瓶颈。这种情况可以使用包含多个主节点的层次化方法,每个主节点控制一部分可用机器,对更高级主节点负责。这种安排如图2-13b所示。
主/从结构概念非常简单,并能自然地应用到许多问题,前提是总体计算能分解为不需要节点间通信的分离且独立的片段。一个额外的好处是这种结构能提供隐式的负载均衡,即它将工作负载分配给空闲节点,以确保工作分配时极少甚至没有不均衡情况。
工作负载可用多种多样的方式描述,从最特殊的,如为已知函数的执行提供参数,到最一般的,如提供具有任何种类计算组合的类实例。
2.4.4 map-reduce
map-reduce是主/从模式一个流行的衍生物,被Google用来运行其搜索引擎后,从此开始流行起来。map-reduce模式应用程序的运行上下文需要通过应用(映射)一个函数来处理大型独立数据的集合(易并行)。所有局部计算的结果需要应用另一个函数进行归约。
map-reduce模式正如Google教程[48]所宣传的,以如图2-14所示的一般形式工作。用户程序产生一个主进程监督整个过程,也会产生一些从进程,这些从进程不仅要负责处理输入数据和中间结果,而且负责合并结果产生最终解答。
在实践中,执行映射和归约阶段的工作者可以相同,两种类型从进程之间的数据存储可以是持久的(如文件)或暂时的(如内存缓冲区)。
map-reduce模式和典型的主/从结构的主要不同在于这种规划允许使用自动化工具,该工具负责应用程序的部署和负载均衡。Apache Hadoop项目是使用map-reduce引擎的一个框架。Hadoop map-reduce引擎提供两种类型的进程:JobTracker以及TaskTracker,前者等价于图2-14中的主线程,后者等价于图2-14中的从线程。这些进程作为系统服务(守护进程)产生。JobTracker负责给TaskTracker分配任务,并追踪它们的进度和状态(例如,如果它们死机,其工作会被调度到其他地方)。每个TaskTracker为分配的任务维护一个简单的先进先出(first-in first-out,FIFO)队列,并作为独立的进程(即单独的Java虚拟机)执行这些任务。
图2-14 map-reduce模式的一般形式。步骤包括产生主进程和从进程,主进程分配任务,由执行映射的从进程输入数据,保存局部结果,⑤归约从进程读取局部结果,⑥保存最终结果
2.4.5 fork/join
并行算法在运行时需要动态创建(fork)任务时可使用fork/join模式。这些子任务(进程或线程)通常需要在父进程或线程恢复执行之前终止(join)。
产生的任务可以通过产生新的线程或进程来运行,或者通过使用存在的线程池来处理它们。后者能最小化创建线程的开销并可能最优地管理机器进程资源(通过将线程数和可用处理器核数相匹配)。
在实际中,一个关于fork/join模式的例子(以并行快速排序算法的实现形式)如下所示:
第6行中的PartitionData
函数调用将输入数据分为两个部分,一部分包含小于或等于中间点(pivot)的元素的元素,一部分包含大于或等于中间点的元素。pos索引指向中心点的位置,实际上分了两个部分,分别跨越[0,pos) 和[pos + 1,N)
范围。两个部分接续着并行排序,一个通过产生一个新任务(第7、8行),一个使用原本的线程或进程(第9行)。只要被排序的数组大小超过THRES,新的任务就会产生。
重申之前阐述的一点,新任务的产生并不需要创建新线程或进程来处理它。如果要排序的数组非常大,这可能是避免灾难的秘诀,因为此时很有可能使操作系统崩溃。为了说明这可能产生多大的问题,下面考虑在执行代码清单2-8所示的并行快速排序时有多少任务会产生。
如果用T(N)表示输入大小为N时产生的任务总数(去掉第一个,根任务),假设PartitionData
函数可以将输入数据分为两个相等的部分(最佳情况),则:
(2-25)
反向代入可以解决这个递推关系。假设N和THRES是2的幂,则当时时以下展开式停止:
(2-26)
将这个k值代入式(2-26),当T(THRES) = 0 时得到:
(2-27)
举例来说,N = 220
,THRES = 210
,需要210 - 1 = 1023
个任务。一个较好的方法是使用线程池执行产生的任务,这种方法在3.8节做了彻底的探讨。
2.4.6 循环并行
将软件移植到多核体系结构上是一个艰巨的任务。循环并行模式通过允许开发者移植已有的串行代码来解决这个问题,移植过程会并行化支配执行时间的循环。
这种模式对OpenMP平台尤为重要,在这一平台上,在程序员的帮助下,循环半自动地并行化。程序员需要以指令的形式提供提示来帮助完成这个任务。
从提升问题的全新并行解法设计的意义上来说,循环并行模式的可用性有限,该模式注重的是串行到并行解法的演化。这也是性能收益通常较小的原因,但至少需要的开发投入也相应地很小。