Upload
others
View
81
Download
0
Embed Size (px)
Citation preview
2.1 什么是并行计算?
串行计算: 传统的软件通常被设计成为串行计算模式,具有如下特点:
一个问题被分解成为一系列离散的指令;
这些指令被顺次执行;
所有指令均在一个处理器上被执行;
在任何时刻,最多只有一个指令能够被执行。
原文:https://computing.llnl.gov/tutorials/parallel_comp/ Last Modified: 07/04/2019 中文翻译:https://blog.csdn.net/magicbean2/article/details/75174859 2017 年 07 月 20 日
并行计算: 简单来讲,并行计算就是同时使用多个计算资源来解决一个计算问题:
一个问题被分解成为一系列可以并发执行的离散部分;
每个部分可以进一步被分解成为一系列离散指令;
来自每个部分的指令可以在不同的处理器上被同时执行;
需要一个总体的控制/协作机制来负责对不同部分的执行情况进行调度。
这里的 计算问题 需要具有如下特点:
能够被分解成为并发执行离散片段;
不同的离散片段能够被在任意时刻执行;
采用多个计算资源的花费时间要小于采用单
个计算资源所花费的时间。
并行计算机:
从硬件的角度来讲,当前所有的单机都可以被认为是并行的:
多功能单元(L1 缓存,L2 缓存,图形处理器等)
多执行单元/内核
多硬件线程
IBM BG/Q Compute Chip with 18 cores (PU)
通过网络连接起来的多个单机也可以形成更大的并行计算机集群:
每个计算结点就是一个多处理器的并行计算机;
多个计算结点用无限宽带网络连接起来;
某些特殊的结点(通常也是多处理器单机)被用来执行特定的任务。
(一个典型并行计算机集群)
2.2 为什么要并行计算?
真实世界就是高度并行的:
自然界中的万事万物都在并发的,按照其内在时间序列运行着;
和串行计算相比,并行计算更适用于对现实世界中的复杂现象进行建模,模拟和理解;
主要理由:
节约时间和成本:
1)理论上来讲,在一个任务上投入更多的资源有利于缩短其完成时间,从而降低成本;
2)并行计算机可以由大量廉价的单机构成,从而节约成本。
解决更大规模更复杂的问题
提供并发性:
1)单个计算资源某个时间段只能做一件事情,而多
计算资源则可以同时做多件事情;
2)协同网络可以使得来自世界不同地区的人同时虚
拟地沟通。
可以利用更广范围中的网络资源;
更好地利用并行硬件:
1)现代计算机甚至笔记本电脑在架构上都属于多处理器/多核的;
2)并行软件已经适用于多核的并行硬件条件,例如线程等;
3)在大多数情况下,运行在现代计算机上的串行程序实际上浪费了大量的计算资源。
(Intel Xeon processor with 6 cores and 6 L3 cache units)
并行计算的未来:
在过去的二十多年中,快速发展的网络,
分布式系统以及多处理器计算机架构(甚
至在桌面机级别上)表明并行化才是计算
的未来;
在同一时期,超级计算机的性能已经有了
至少 50 万倍的增加,而且目前还没有达到
极限的迹象;
目前的峰值计算速度已经达到了 1018/秒
Source: Top500.org
2.3 谁都在使用并行计算?
科学界和工程界:
从历史上来讲,并行计算就被认为是“计算的高端”,许多科学和工程领域的研究团队在对很多领域
的问题建模上都采用了并行计算这一模式,包括:大气与地球环境、应用物理、生物科学、遗传学、
化学、分子科学、机械工程、电气工程、计算机科学、数学、国防和武器研发等。
工业界和商业界:
如今,商业应用为更快速计算机的研发提供了更强劲的动力。这些商业应用程序需要以更复杂的方式
处理大量数据,例如:大数据、数据库、数据挖掘、石油勘探、网页搜索引擎、基于 web 的商业服
务、医学成像和诊断、跨国公司管理、高级图形学技术以及虚拟现实、网络视频和多媒体技术、协同
工作环境等。
全球应用: 并行计算目前在实际上被广泛采用于大量应用中。
3 概念和术语
3.1 冯诺依曼体系结构
以匈牙利数学家约翰·冯诺依曼命名,出现在他 1945 年发表的一篇论文中。
- 四个组成部分:1)内存;2)控制器;3)处理器;4)输入输出。
- 读写操作,支持随机存储的内存用来同时保存程序指令和数据:
1)程序指令用来指导计算机操作;2)数据是操作对象。
- 控制器:从内存中读取指令或者数据,进行解码并且顺序执行。
- 处理器:提供基本的算术和逻辑操作。
- 输入输出设备:是人机交互的接口。
当前的计算机都遵循这一基本架构,包括并行计算机。
3.2 并行计算机的分类
有不同的方法对并行计算机进行分类,一种被广泛采用的分类被称
为弗林(Flynn)分类法,诞生于 1966 年,从指令流和数据流两
个维度区分多处理器计算机体系结构。
单指令单数据(SISD): 是标准意义上的串行机,具有如下特点:
1)单指令:在每一个时钟周期内,CPU 只能执行一个指令流;
2)单数据:在每一个时钟周期内,输入设备只能输入一个数据流;
3)执行结果是确定的。
这是最古老的一种计算机类型。
单指令多数据(SIMD): 属于一种并行计算机,具有如下特点:
1)单指令:所有处理单元在任何一个时钟周期内都执行同一条指令;
2)多数据:每个处理单元可以处理不同的数据元素;
3)非常适合于处理高度有序的任务,例如图形/图像处理;
4)同步(锁步)及确定性执行;
5)两个主要类型:处理器阵列和矢量管道。
多指令单数据(MISD):属于一种并行计算机,具有如下特点:
1)多指令:不同的处理单元可以独立地执行不同的指令流;
2)单数据:不同的处理单元接收的是同一单数据流。
这种架构理论上是有的,但是工业实践中这种机型非常少。
多指令多数据(MIMD): 最常见的一种并行计算机,具有如下特点:
1)多指令:不同的处理器可以在同一时刻处理不同的指令流;
2)多数据:不同的处理器可以在同一时刻处理不同的数据;
3)执行可以是同步的,也可以是异步的,可以是确定性的,也可以是
不确定性的。
这是目前主流的计算机架构类型,超级计算机、并行计算机集群系统,
网格,多处理器计算机,多核计算机等都属于这种类型。
3.3 一些常见的并行计算术语
结点(Node): 也就是一个独立的“计算机单元”。通常由多个 CPU 处理器/处理内核,内存,网
络接口等组成。结点联网在一起以构成超级计算机。
中央处理器/套接字/处理器/核(CPU / Socket / Processor / Core): 这些术语也取决于我们讨
论的语境。在过去,中央处理器通常是计算机中的一个单个执行单元。之后多处理器被植入到一个
结点中。接着处理器又被设计成为多
核,每个核成为一个独立的处理单
元。具有多核的中央处理器有时候又
被称为“套接字”——实际上也没有
统一标准。所以目前来讲,我们称一
个结点上具有多个中央处理器,每个
中央处理器上又具有多个内核。
任务(Task): 任务通常是指一个逻辑上离散的计算工作部分。一个任务通常是一段程序或者一段
类似于程序的指令集合,可以由一个处理器进行处理。一个并行程序通常由多个任务构成,并且可
以运行在多个处理器上。
流水线(Pipelining): 可以将任务分解成为不同的步骤,并且由不同的处理单元完成,里面有输入
流通过。这非常类似于一个装配线,属于一种类型的并行计算。
共享内存(Shared Memory): 从严格的硬件角度来讲,共享内存描述了一种计算机架构,其中所
有的处理器都可以对共同的物理内存进行直接存取(通常是通过总线)。从编程的角度来讲,共享
内存描述了一种模型,其中所有的并行任务都具有同一内存形态,并且都可以直接对同一内存区域
进行直接定位和存取,而无论该物理内存实际上在哪里。
对称多处理器(Symmetric Multi-Processor (SMP)): 属于一种共享内存的硬件架构,并且不同
的处理器对内存以及其它资源都具有同等的访问权限。
分布式内存(Distributed Memory): 在硬件中,表示基于网络的内存存取方式;在编程模型
中,表示任务仅仅能够从逻辑上“看到”本机上的内存,但是在其它任务执行的时候,必须通过通
讯才能对其它任务所运行的机器上的内存进行存取。
通讯(communications): 并行任务通常需要数据交换。实现数据交换的方式有多种,例如通过
共享内存或者通过网络。但是通常意义上,数据交换指的就是通讯,而无论其实现方式。
同步(Synchronization): 指的是并行任务之间的实时协调,通常伴随着通讯
(communication)。同步通常由在程序中设立同步点来实现,也就是说,在其它任务没有执行到这
一同步点的时候,某一任务不能进一步执行后面的指令。同步通常涉及到需要等待其它任务的完
成,因此有时候会增加并行程序的执行时间。
粒度(Granularity): 在并行计算中,粒度定量地描述了计算与通讯的比率。粗粒度表示在通讯过
程中需要做大量的计算性工作;细粒度则表示在通讯过程中需要做的计算性工作并不多。
加速比(Observed Speedup): 这是检测并行计算性能的最简单并且最被广泛使用的度量策略,
其定义为:串行计算的时钟周期数与并行计算的时钟周期数的比值。
并行开销(Parallel Overhead): 指的是相对于做实际计算,做协调并行任务所需要花费的时间总
数。影响并行开销的因素主要包括:1)任务启动时间;2)同步;3)数据通讯;4)由并行语
言,链接库,操作系统等因素而导致的软件开销;5)任务终止时间。
大规模并行(Massive Parallel): 指那些包含并行系统的硬件——拥有很多的处理元件。这里的
“很多”可能会随着硬件条件的进步而不断增加,但目前,最大的并行系统所拥有的处理元件高达
上百万件。
尴尬并行(Embarrassingly Parallel): 指的是同时解决很多类似而又独立的任务,其中任务之间
几乎没有需要协调的地方。
可扩展性(Scalability): 指的是并行系统(包括软件和硬件)通过添加更多资源来成比例增加并
行速度的能力。影响可扩展性的因素主要包括:1)硬件,尤其是内存-处理器带宽以及网络通讯的
质量和速度;2)应用算法;3)相对并行开销;4)具体应用的特征。
3.4 并行程序的缺陷和代价
阿姆达尔定律: 一个程序的加速比潜力由其可以并行的部分所占的比例而决定,即:
speedup= 1
1−𝑝𝑝
如果没有代码可以被并行,那么 p = 0,所以加速比为 1。
如果所有的代码都可以被并行,那么 p = 1,加速比为无穷大(当然只是理论上而言)。
如果 50%的代码可以被并行,那么最大的加速比为 2,即最快可以被加速到 2 倍。
如果引入并行程序中的处理器个数,则加速比可以被重新定义为:
speedup= 1
𝑝𝑝𝑁𝑁+𝑠𝑠
,其中 p 表示并行部分,s 表示串行部分,N 表示处理器个数。
这意味着并行计算的极限所在,比如:你可以花费一生的时间使得你的代码的 95%都可以被并
行,然而你如论投入多少处理器,都不会获得 20 倍的加速比。
然而,某些问题我们可以通过增加问题的大小来提高其性能,即提高并行部分所占比例。
比起具有固定并行比例的问题,那些可以随着问题规模增大而不断提高并行比例的问题,在并行化
的意义上更具有可扩展性。
复杂性:
通常而言,并行计算的程序要比相应的串行计算程序更加复杂,也许复杂一个量级。你不仅需要同
时执行不同的指令流,而且需要注意他们之间数据的通信。
复杂性通常由涉及软件开发周期各个方面的时间来确定,主要包括:
1) 设计;2)编码;3)调试;4)调参;5)维护。
遵循良好的软件开发实践对并行应用开发是至关重要的,尤其是在除你之外的其他人还需要和你合
作的情况下。
可移植性:
由于一些 API(Application Program Interface)的标准化,例如 MPI,POSIX 线程以及
OpenMP,并行程序的可移植性问题并不像过去那么严重,然而:
所有串行程序中所遇到的可移植性问题都会出现在相应的并行程序中;
尽管一些 API 已经被标准化,但是在一些细节的实现上任然有差异,有时候这些细节实现会影响到
可移植性;
操作系统也会是影响可移植性的关键因素;
硬件架构的不同有时候也会影响到可移植性。
资源需求:
并行编程的主要目标就是降低计算时间。然而要做到这一点,需要更多的 CPU 时间。例如,一个
在 8 个处理器上跑了一个小时的并行程序实际上花费了 8 小时的 CPU 时间。
并行程序所需要的内存开销往往要大于相对应的串行程序,这是因为我们有时候需要复制数据,以
满足库和子系统对并行算法的要求。
对于运行时间较短的并行程序,实际性能反而有可能比相对应的串行程序有所降低。这是由于并行
环境的建立,任务创建,通讯,任务结束等在这个运行时间中有可能会占有比较大的比例。
可扩展性:
基于时间和解决方案的不同,可以将扩展性分为:强扩展和弱扩展。
强扩展,特点是:1)问题本身的规模不会增加;2)投入更多处理器的目的是用更少的时间来解
决问题;3)理想状态是:运行时间为降为串行程序的 1/P,其中 P 为处理器个数。
弱扩展,特点是:1)每个处理上需要处理的问题规模保持一致;2)目的是在相同的时间内解决
求解更大规模的问题;3)理想状态下,可以解决的问题规模增加为原问题规模的 P 倍。
并行程序的可扩展性取决于一系列相互依赖的因素,简单地增加处理器并不是方法。
某些问题可能本身存在扩展性的极限,添加更多处理器有可能反而会降低性能。
硬件在可扩展性方面也扮演者重要角色,比如:1)内存-CPU 之间的带宽;2)通讯网络的带宽;
3)某个机器或者某个机器集合中的内存大小;4)处理器的计算速度。
4 并行计算机的内存架构
4.1 共享内存
共享内存的并行计算机虽然也分很多种,但是通常而言,它们都可以让所有处理器以全局寻址的方式
访问所有的内存空间。多个处理器可以独立地操作,但是它们共享同一片内存。一个处理器对内存地
址的改变对其它处理器来说是可见的。根据内存访问时间,可以将已有的共享内存机器分为 统一内存
存取 和 非统一内存存取 两种类型。
统一内存存取(Uniform Memory Access): 目前更多地被称为对
称多处理器机器(Symmetric Multiprocessor (SMP)),每个处理器都
是相同的,并且其对内存的访问都是无差别的。
非统一内存存取(Non-Uniform Memory
Access): 通常由两个或者多个物理上相连的
SMP。每个 SMP 都可以访问其它 SMP 上的内
存,但访问是有差别的。
共享内存优点:全局地址空间提供了一种用户友好的编程方式,并且由于内存与 CPU 的衔接程度,使
得任务之间的数据共享既快速又统一。
缺点:最大的缺点是内存和 CPU 之间缺少较好的可扩展性。
4.2 分布式内存
分布式内存架构有许多种,它们有一些共同特征:
1)分布式内存结构需要通讯网络,将不同的内存连接起来。
2)每个处理器有自己的内存,且不会映射到其它处理器上,所以不存在全局地址空间。
3)每个处理器可以独立工作,其内存上所发生的变化并不会被其它处理器所知晓。
4)如果一个处理器需要访问其它处理器上的数据,那么就需要通过通信来获取。
5)分布式内存架构的网络结构有很多不同的拓扑结构。
优点:
1)内存可以随着处理器的数量而扩展;
2)每个处理器可以快速访问自己的内存而不受干扰,并且没有维护全局高速缓存一致性所需的开销;
3)成本效益:可以使用现有的处理器和网络。
缺点:
1)程序员需要负责处理器之间数据通讯相关的许多细节;
2)将基于全局内存的现有数据结构映射到该分布式内存组织可能会存在困难;
3)非均匀的内存访问时间 —— 访问其它结点上的数据比本地数据需要更长的访问时间。
4.3 混合分布式-共享内存
目前世界上最大和最快的并行计算机往往同时具有分布式和共享式的内存架构。
从目前的趋势来看,这种混合式的内存架构将长期占有主导地位,并且成为高端计算在可见的未来中
的最好选择。
优缺点:
1)继承了共享式内存和分布式内存的优缺点;
2)优点之一是可扩展性;
3)缺点之一是编程的复杂性。
5. 并行计算模型
5.1 概述
常见的并行编程模型包括:
共享内存模型(无线程)
线程模型
分布式内存/消息传递模型
数据并行模型
混合模型
单程序多数据模型
多程序多数据模型
需要指出的是,并行计算模型与并行计算机的架构无关。
比如:在分布式内存架构上的共享内存编程模型。 机器内
存分布于网络上的不同结点,但是对于用户而言,看到的
确实一个具有全局地址空间的共享内存。这也通常被称为
“虚拟共享内存”。
同样,也可以在共享内存架构上实现分布式编程模型。
5.2 共享内存模型
在这种并行计算模型中,处理器共享内存空间,可以异步对内存进行读写。很多机制被用来控制对内
存的存取,用来解决访问冲突以及避免死锁。这应该是最简单的并行计算模型了。
从用户的角度来看,这种模型的好处之一是不需要指定数据之间的
通讯,程序开发将因此而变得容易。
但一个重要缺点是对数据局部性的理解和管理将变得困难:
1)保持数据的局部性将有利于减少内存的存取负担,缓存刷新次
数以及总线流量。而当多个处理器使用同一数据时,这些负担将会
经常发生;
2)不幸的是,保持数据的局部性往往比较难以理解,一般用户通
常难以做到。
5.3 线程模型
这是共享内存编程的一种模式。在该模型中,一个“重量级”进程
可以拥有多个“轻量级”的并发执行路径。例如右图所示:
主程序 a.out 在本地操作系统上运行。a.out 需要加载所有的系
统和用户资源来运行,这是里面的“重量级”进程。
a.out 首先执行一些串行工作,然后生成一系列任务(线程),
而这些线程可以在操作系统中被并发地执行。
每个线程具有本地数据,但同时也共享 a.out 的所有资源。这节
约了所有线程都复制程序资源的的开销。而每个线程同时也从全
局内存中获益,因为它可以共享 a.out 的内存空间。
一个线程的工作可以被描述为主程序的一个子程序。任何线程都
可以在其它线程运行的同时执行任何子程序。
线程之间的通讯通过全局内存来实现(对全局地址的更新)。这需要建立一种同步机制,以保证在
同一时刻,不会有多个线程对同一块地址空间进行更新。
线程可以随时生成和结束,但是 a.out 却一直存在,以提供所需的共享资源,直到整个应用程序
结束。
实现: 从编程的角度来讲,线程的实现通常包括如下两个方面:
库函数或者子程序,这些库函数或者子程序可以在并行源代码中被调用;
嵌入在并行或者串行源代码中的一组编译器指令集合。
用户需要同时负责上面的两个方面(尽管有时候编译器可以提供帮助)。
早期硬件供应商提供的线程实现方式各不相同,导致程序员很难开发可移植的多线程应用程序。
标准化工作却导致了两种完全不同的线程实现方式:POSIX Threads 和 OpenMP。
POSIX Threads:由 IEEE (1995) 定义,仅支持 C 语言,是 Unix/Linux 操作系统的一部分,是
基于库函数的,也通常被称为“Pthreads”。更多信息可见:POSIX Threads tutorial。
OpenMP:是一个工业标准,由硬件和软件提供商、国际组织和个人联合发起和制定,是基于编
译器指令的,具有可移植性/跨平台性,目前支持 Unix/Linux 和 Windows 平台,支持 C/C++和
Fortran 语言,简单易用,可以直接通过修改串行代码实现。
其它的常见线程实现有:Microsoft threads,Java、Python threads,CUDA threads for GPUs
5.4 分布式内存/消息传递模型
这种模型具有如下特点:
在计算的过程中,每个任务都仅仅使用它们自身的本地
内存。多个任务既可以寄宿在同一个物理机器上,也可
以跨越不同的物理机器。
任务之间的数据交换是通过发送和接收消息而实现的。
数据传输通常需要不同进程之间的协同操作才能实现。
例如,一个发送操作需要同时对应一个接收操作。
实现:
自上世纪 80 年代以来,出现了多种消息传递库函数。这些实现各不相同,导致用户很难开发出移
植性好的应用程序。
自从 1992 年开始,MPI Forum 形成,其主要目标是建立消息传递的标准接口。
消息传递接口(Message Passing Interface (MPI))的第一部分在 1994 年发布,第二部分在
1996 年发布,第三部分在 2012 年发布。
MPI 成为了事实上的消息传递的工业标准,取代了所有其它消息传递的实现。
几乎所有流行的并行计算平台都存在 MPI 的实现,但并不是所有的实现都包含了 MPI-1,MPI-2
和 MPI-3 的所有内容。
5.5 数据并行模型
通常也称“全局地址空间分区”(Partitioned Global Address Space/PGAS)模型,具有如下特点:
地址空间被认为是全局的。
大多数的并行工作聚焦于在数据集上的操作。数据集通常被
组织成为常用的结构,例如数组等。
一系列任务在同一块数据结构上操作,但是每个任务却操作
在该数据结构的不同分区上。
每个任务在数据结构的不同分区上执行相同的操作,例如
“给每个数组元素加上 4”。
在共享内存的架构下,所有的任务通过全局内存方式来对数
据进行存取;
在分布式内存架构下,根据任务分配,全局数据结构在物理或者逻辑上被进行分割。
实现: 目前,基于数据并行/PGAS 模型,有如下几个相对有名的实现:
Coarray Fortran: 为了支持 SPMD 并行编程而在 Fortran 95 上做的一个小的扩展,是编译器相
关的。
Unified Parallel C (UPC): 为了支持 SPMD 并行编程而在 C 语言基础上做的扩展,也是编译器相
关的。
5.6 混合模型
混合模型指的是包含了至少两个我们前面提到的并行计算模型的模型。
目前,最常见的混合模型的例子是消息传递模型(MPI)和线程模型(OpenMP)的结合:
线程使用本地数据完成计算密集型的任务;
不同的进程则在不同的结点上通过 MPI
完成数据通讯。
这种混合模型非常适合目前流行的硬件
环境 — 多核计算机组成的集群系统。
另外一种类似的,但原来越流行的例子是采用 MPI 的 CPU-GPU 的混合编程:
采用 MPI 的任务运行于 CPU 上,使用本地内存上的数据,通过网络与其它任务进行数据交换;
计算密集型的任务被加载到 GPU 上进行计算;
结点内部的内存和 GPU 上的数据交换则通过 CUDA(或者类似的东西)进行数据交换。
其它混合模型还包括:
MPI 和 Pthreads 的混合;
MPI 和 non-GPU 加速器的混合。
. . . . . .
5.7 单程序多数据模型(SPMD)和多程序多数据模型(MPMD)
单程序多数据模型(Single Program Multiple Data (SPMD)): SPMD 事实上是一种可以架构在其
它并行编程模型之上的更“高级”的编程模型:
单程序:所有任务都执行同一个程序的拷贝,可以是线程,消息传递,数据并行甚至混合;
多数据:不同的任务操作于不同的数据。
SMPD 通常需要指定任务的执行逻辑,也就是不同的任务可能会根据分支和逻辑关系,去执行整个程
序的某个部分,也就是说,不是所有的任务都必须执行整个程序——有可能只是整个程序的某个部
分。(如果不强调这一点,SPMD 就退化成了数据并行模型了。)
这种采用消息传递或者混合编程的 SPMD 模型,可能是多核集群系统上的最常见的并行计算模型。
多程序多数据模型(Multiple Program Multiple Data (MPMD)):
和 SPMD 一样,多程序多数据模型实际上也是一种可以架构在其它并行编程模型基础上的“高级”并
行编程模型:
多程序:任务可以同时执行不同的程序,这里的程序可以是线程,消息传递,数据并行或者它们的
混合。
多数据:所有的任务可以使用不同的数据。
MPMD 应用并不像 SPMD 应用那么常见,但是它可能更适合于特定类型的程序。
6 并行程序设计
6.1 自动 vs. 手动并行化
设计和实现并行程序是一个手动的过程,用户通常需要负责识别和实现并行化。而手动开发并行程序
通常是一个耗时、复杂、易于出错并且迭代的过程。多年来,很多工具被开发出来,以协助用户将串
行程序转化为并行程序。最常见的工具有:并行编译器(或预处理器),通常以如下两种方式工作:
完全自动: 由编译器分析源代码并且识别可以并行化的部分,这里的分析包括识别出哪些部分满
足并行化的条件,以及权衡并行化是否真的可以提高性能。循环(包括 do, for)通常是最容易被
并行化的部分。
程序员指令: 通过采用“编译器指令”或者编译器标识,程序员明确地告诉编译器如何并行化代
码,而这可能会和某些自动化的并行过程结合起来使用。
常见的由编译器生成并行化程序是通过使用结点内部的共享内存和线程实现(如 OpenMP)
如果你已经有了串行的程序,并且有时间和预算方面的限制,那么自动并行化也许是一个好的选
择,但是有几个重要的注意事项:
1)可能会产生错误的结果;
2)性能实际上可能会降低;
3)可能不如手动并行那么灵活;
4)只局限于代码的某个子集(通常是循环);
5)可能实际上无法真正并行化,由于编译器发现里面有依赖或者代码过于复杂。
6.2 理解问题和程序
毫无疑问,开发并行程序的第一步就是理解你将要通过并行化来解决的问题。
如果你是从一个已有的串行程序开始的,那么你需要首先理解这个串行程序。
在开始尝试开发并行解决方案之前,需要确定该问题是否真正可以被并行化。
如果任务的每次更新可以分割成多个独立部分,则该问题容易被并行化,如 Jacobi 迭代。
如果任务之间存在先后关系,则可能很难并行,如 Fibonacci 数列。
识别程序的关键点 (hotspots):
了解哪个部分完成了程序的大多数工作(这些工作往往是在某些小片段中完成)。
可以通过剖析器或者性能分析工具来帮助你分析。
专注于程序中这些关键点,忽略那些占用少量 CPU 的其余部分。
识别程序中的瓶颈 (bottlenecks):
有没有导致程序不成比例地变慢的,或者导致并行程序停止或者延迟的部分?例如有时候输入
输出操作会导致程序变慢。
有时候也可能通过重构程序,或者采用不同的算法来降低或者消除这些执行很慢的区域。
识别并行化的抑制因素。一个常见的类型是数据依赖性,如 Fibonacci 数列。
如果可能的话,研究其它算法。这可能是设计并行程序的过程中最重要的一点。
利用成熟的第三方并行软件,或者高度成熟的数学库(例如 IBM 的 ESSL,Intel 的 MKL,AMD
的 AMCL 等)。
6.3 分割 (Partitioning) 技术
设计并行程序的第一步就是将问题分解成为可以分配到不同任务中去的“块”。这被称为程序的分解
(decomposition) 或者分割 (partitioning)。通常有两种基本方法可以将并行任务进行分解:域分解
和功能分解。
域分解:将数据进行分解,每个并行任务操作
数据的一部分,可以有不同的分解方式。
功能分解:着眼于需要执行的计算,而不是数据,根据要做的工作进行分解。
例如:信号处理
音频信号数据集通过四个不同的计算滤波器,每个滤波器是一个单独的过程。第一段数据必须通过第
一个滤波器,然后才能进入第二个滤波器。当这样做时,第二段数据通过了第一个滤波器。当第四个
数据段处于第一个滤波器时(以及之后),四个滤波器同时工作。
注:在实践中将这两种分解方式结合起来是很自然的,也是很常见的。
6.4 通讯 (Communications)
任务之间的通讯需求取决于你的问题:
不需要通讯的情况: 一些程序可以被分解成为并发执行的任务,而这些任务之间不需要共享数
据。这类问题往往被称为“尴尬并行”——任务之间不需要数据通讯。
需要通讯的情况: 大多数并行程序的任务之间需要共享数据。
例如热扩散问题。
设计通讯需要考虑的因素: 在设计程序任务之间的通讯时,有大量的重要因素需要考虑。
通讯开销: 1)任务间通讯几乎总是意味着开销。2)可以用于计算的资源会转而用于对数据的封
装和传输。3)频繁的通讯需要任务之间的同步,这有可能会导致任务等待。4)竞争通讯流量可
能使可用的网络带宽饱和,从而进一步加剧性能问题。
延迟 vs. 带宽: 1)延迟 指的是从 A 点到 B 点发送最小量的信息所需要花费的时间,通常以毫秒
计。2)带宽 指的是单位时间内可以传输的数据总量,通常以 M/S 或者 G/S 来计。3)发送大量
的短消息可能会导致延迟成为通讯的主要开销。通常情况下将大量小信息封装成为大消息会更加有
效,从而提高通讯带宽的利用效率。
通讯可见性: 1)在消息传递模型中,通讯往往是显式的,并且在用户的控制之下。2)在数据并
行模型中,通讯对用户来说往往是透明的,尤其是在分布式内存架构中。用户往往甚至不能明确知
道任务之间的通讯是如何完成的。
同步 vs. 异步通讯: 1) 同步通讯需要共享数据的任务之间某种意义上的“握手”。这既可以由用
户显式地指定,也可以在底层被隐式地实现而不为用户所知。2)同步通讯也常常被称为“阻塞通
讯”,因为一些任务必须等待直到它们和其它任务之间的通讯完成。3)异步通讯允许任务之间独
立地传输数据。例如任务 1 可以准备并且发送消息给任务 2,然后立即开始做其它工作,它并不关
心任务 2 什么时候真正受到数据。4)异步通讯也常常被称为“非阻塞通讯”,因为在通讯发生的
过程中,任务还可以完成其它工作。5)在计算和通讯自由转换是异步通讯的最大优势所在。
通讯的范围: 明确哪些任务之间需要通讯在设计并行代码的过程中是非常关键的。下面两种通讯
范围既可以被设计为同步的,也可以被设计为异步的:1)点对点通讯: 涉及到两个任务,其中一
个扮演消息发送者/生产者的角色,另外一个扮演消息接受者/消费者的角色。2)广播通讯:涉及
到多于两个任务之间的数据共享。这些任务通常处于一个组或者集合中。
通讯的效率: 通常用户具有影响通讯性能的多种选择,比如:1)对于一个给定的模型,究竟应该
采用哪一种实现?例如对于消息传递模型而言,一种 MPI 的实现可能在某个给定的硬件下比其它
实现要快。2)采用什么类型的通讯操作?正如前面所提到的,异步通讯操作往往可以提高程序的
整体性能。3)网络结构:某些平台可能会提供多于一个的网络结构。那么究竟哪一个最好?
开销和复杂性
最后需要提醒的是,上面提到的仅仅是需要注意的问题的一部分!
6.5 同步 (Synchronization)
管理工作的顺序和执行它的任务是大多数并行程序设计的关键,它也可能是提升程序性能的关键,通
常也需要对某些程序进行“串行化”。
同步的类型:
障碍: 1)这通常意味着会涉及到所有任务;2)每个任务都执行自身的工作,直到它遇到障碍,
然后它们就停止,或者“阻塞”;3)当最后一个任务到达屏障时,所有任务得以同步;4)接下
来可能发生的事情就有所变化了。通常会执行一段串行代码,或者所有的任务在这里都结束了。
锁/信号量: 1)可以涉及任意多个任务;2)通常用于对全局数据或者某段代码的存取串行化(保
护),在任一时刻,只有一个任务可以使用锁/信号量;3)第一个任务会获得一个锁,然后该任务
就可以安全地对该保护数据进行存取;4)其它任务可以尝试去获得锁,但必须等到当前拥有该锁
的任务释放锁才行;5)可以是阻塞的也可以是非阻塞的。
同步通讯操作: 1)仅仅涉及到执行数据通讯操作的任务;2)当一个任务执行数据通讯操作时,
通常需要在参与通讯的任务之间建立某种协调机制。例如,在一个任务发送消息时,它必须收到接
受任务的确认,以明确当前是可以发送消息的;3)在消息通讯一节中也已经说明。
6.6 数据依赖性 (Data Dependencies)
什么是数据依赖:
依赖: 当语句的执行顺序影响程序的运行结果时,我们称程
序语句之间存在依赖关系。
数据依赖: 数据依赖是由不同任务多次存取相同的内存位置
而产生的。
数据依赖也是并行程序设计中的关键,因为它是并行化中一
个重要的抑制因素。
例子:
循环导致的数据依赖:
上面代码中,A[J-1] 必须在 A[J] 之前被计算出来,因此说 A[J] 与 A[J-1] 之间存在数据依赖。
如果任务 2 中有 A[J],任务 1 中有 A[J-1],那么要计算出正确的 A[J], 需要:
1)分布式内存架构:任务 2 必须在任务 1 计算结束之后,从任务 1 处中获取 A[J-1] 的值。
2)共享内存架构:任务 2 在任务 1 完成对 A[J-1] 的更新之后,对 A[J-1] 进行读取。
尽管在并行程序设计中,对所有数据依赖的识别都是重要的,但循环相关的数据依赖尤其重要,因为
循环往往是最常见的可并行化部分。
for(j=1; j<n; j++) A[j] = 2*A[j-1];
6.7 负载均衡 (Load Balancing)
负载均衡是指在任务之间分配大约相等数量的工作量,使空闲时间最小化,这对并行程序性能很重要
如何实现负载均衡:
平均分配任务量:
1)对于数组而言,如果每个任务都执行相同或者类似的工作,那么在任务之间平均分配数据集;2)
对于循环迭代而言,如果每个迭代完成的工作量大小类似,则在每个任务中分配相同或者类似的迭代
次数;3)如果你的架构是由具有不同性能特征的机器异构组合而成,那么请确保使用某种性能分析工
具来简则任何的负载不平衡,并相应调整工作。
采用动态任务分配方法:
即使数据在任务之间被平均分配,但是某些特定类型的问题也会导致负载不平衡,如下面三个例子:
稀疏矩阵:
某些任务会具有真实数据,
而大多数任务对应的数据却为 0。
自适应网格:
某些方格需要被细分,而其它的不需要。
N 体模拟:粒子可能会跨任务域迁移,因此某些任务会需
要承担更多的工作。
当每个任务的工作量是可变的,或者无法预测的,那么可以采用 调度任务池 (Scheduler-task pool)
方法。每当某个任务完成了它的工作,它就可以从工作队列中领取新的任务。
最终,可能需要设计一种在代码中动态发生和处理负载不平衡的算法。
6.8 粒度 (Granularity)
计算通讯比 (computation / Communication Ratio):
在并行计算中,粒度是对计算与通讯的比例的定性度量。
细粒度并行化 (Fine-grain Parallelism): 1)在通讯事件
之外进行相对较少的计算工作;2)计算通讯率较低;3)方
便负载均衡;4)意味着较高的通讯开销以及较少的性能提
升机会;5)如果粒度过细,任务之间的通讯和同步的开销
可能需要比计算更长的时间。
粗粒度并行化 (Coarse-grain Parallelism):
1)在通讯/同步事件之外需要较大量的计算工作;
2)较高的计算/通讯比;
3)意味着较大的性能提升机会;
4)难以进行较好的负载均衡。
最佳选择: 1)最有效的粒度取决于具体算法及其所运行的硬件环境。2)在大多数情况下,相对于计
算速度,通讯/同步所需的开销通常很高,此时具有粗粒度的问题是相对有利的。3)从另外一方面来
讲,细粒度则可以帮助减少由负载不均衡所造成的开销。
6.9 输入输出 (I/O)
坏消息:
1)I/O 操作通常被认为是并行化的抑制剂;
2)I/O 操作通常比内存操作需要多个数量级的时间;
3)并行 I/O 系统可能不成熟或者不适用于所有平台;
4)在所有任务均可以看到相同文件空间的环境中,写操作可能导致文件被覆盖;
5)读操作可能受到文件服务器同时处理多个读取请求的能力影响;
6)必须通过网络进行的 I/O 操作(非本地),可能导致严重的性能瓶颈,甚至文件服务器的崩溃。
好消息: 已经具有不少并行文件系统,例如:
GPFS:通用并行文件系统 (General Parallel File System)(IBM),现在也被称为 IBM
Spectrum Scale。
Lustre:针对 Linux 的集群(Intel)。
HDFS:Hadoop 分布式文件系统(Apache)。
PanFS:Panasas ActiveScale File System for Linux clusters (Panasas, Inc.)
更多并行文件系统 . . . . .
作为 MPI-2 的一部分,1996 年以来 MPI 的并行 I/O 编程借口规范已经可用。
注意事项: 1)尽可能减少整体 I/O 操作;2)尽量使用并行文件系统;3)在大块数据上执行少量写
操作往往比在小块数据上进行大量写操作有着更明显的效率提升;4)较少的文件比许多小文件更好;
5)将 I/O 操作限制在作业的特定串行部分,然后使用并行通讯将数据分发到并行任务中;6)跨任务
的 I/O 整合 —— 相比于很多任务都执行 I/O 操作,更好地策略是只让一部分任务执行 I/O 操作。
6.10 调试 (Debugging)
调试并行代码可能非常困难,特别是随着代码量的扩展
而好消息是有一些优秀的调试器可以提供帮助:
1)Threaded - Pthreads 和 OpenMP;2)MPI;3)GPU/accelerator;4)Hybrid。
在 LC 集群上也安装有一些并行调试工具:
1)TotalView (RogueWave Software);2)DDT (Allinea);3)Inspector(Intel);4)Stack
Trace Analysis Tool(STAT) 。
更多的信息可以参考:LC web pages,TotalView tutorial。
6.11 性能分析和调优 (Performance Analysis and Tuning)
对于调试而言,并行程序的性能分析和调优比串行程序更具挑战性。幸运的是,并行程序性能分析和
调优有很多优秀的工具。Livemore 计算机用户可以访问这种类似工具,其中大部分都在集群系统上。
一些安装在 LC 系统上的工具包括: LC’s web pages:https://hpc.llnl.gov/software/development-environment-software
TAU: http://www.cs.uoregon.edu/research/tau/docs.php
HPCToolkit: http://hpctoolkit.org/documentation.html
Open|Speedshop: http://www.openspeedshop.org/
Vampir / Vampirtrace: http://vampir.eu/
Valgrind: http://valgrind.org/
PAPI: http://icl.cs.utk.edu/papi/
mpitrace:https://computing.llnl.gov/tutorials/bgq/index.html#mpitrace
mpiP: http://mpip.sourceforge.net/
memP: http://memp.sourceforge.net/