其他分享
首页 > 其他分享> > 并行计算教程简介 Introduction to Parallel Computing Tutorial

并行计算教程简介 Introduction to Parallel Computing Tutorial

作者:互联网

并行计算简介

(对网上翻译文章再进行整理,可能存在些问题,请参考原贴)

1 摘要

最近项目需要实现程序的并行化,刚好借着翻译这篇帖子的机会,了解和熟悉并行计算的基本概念和程序设计。帖子的原文见这里原翻译

这篇帖子旨在为并行计算这一广泛而宏大的话题提供一个非常快速的概述,作为随后教程的先导。因此,它只涵盖了并行计算的基础知识,实用于刚刚开始熟悉该主题的初学者。我们并不会深入讨论并行计算,因为这将花费大量的时间。本教程从对并行计算是什么以及如何使用开始,讨论与其相关的概念和术语,然后解析并行内存架构(parallel memory architecture)以及编程模型(programming models)等。这些主题之后是一系列关于设计和运行并行计算程序的复杂问题的实践讨论。最后,本教程给出了几个并行化简单串行程序的示例。

2 概述

2.1 什么是并行计算?

串行计算: 传统的软件通常被设计成为串行计算模式,具有如下特点:

并行计算: 简单来讲,并行计算就是同时使用多个计算资源来解决一个计算问题:

这里的 计算资源 通常包括:

并行计算机:
通常来讲,从硬件的角度来讲,当前所有的单机都可以被认为是并行的:

通过网络连接起来的多个单机也可以形成更大的并行计算机集群:
在这里插入图片描述

例如,下面的图解就显示了一个典型的LLNL并行计算机集群:

2.2 为什么要并行计算?

真实世界就是高度并行的:

主要理由:

在这里插入图片描述

并行计算的未来:

2.3 谁都在使用并行计算?

科学界和工程界:

从历史上来讲,并行计算就被认为是“计算的高端”,许多科学和工程领域的研究团队在对很多领域的问题建模上都采用了并行计算这一模式,包括:大气与地球环境、应用物理、生物科学、遗传学、化学、分子科学、机械工程、电气工程、计算机科学、数学、国防和武器研发等。
在这里插入图片描述

工业界和商业界:

如今,商业应用为更快速计算机的研发提供了更强劲的动力。这些商业应用程序需要以更复杂的方式处理大量数据,例如:大数据、数据库、数据挖掘、石油勘探、网页搜索引擎、基于web的商业服务、医学成像和诊断、跨国公司管理、高级图形学技术以及虚拟现实、网络视频和多媒体技术、协同工作环境等。
在这里插入图片描述

全球应用:
并行计算目前在实际上被广泛采用于大量应用中。
在这里插入图片描述
Source: Top500.org
在这里插入图片描述
在这里插入图片描述
Source: Top500.org

3 概念和术语

3.1 冯诺依曼体系结构

以匈牙利数学家约翰·冯诺依曼命名的这一计算机体系结构,出现在他1945年发表的一篇论文中。这也通常被称为“存储程序计算机”——程序指令和数据都被保存在存储器中,这与早期通过“硬接线”编程的计算机不同。从此以后,所有的计算机走遵从这一基本架构:
在这里插入图片描述

那么冯诺依曼体系结构和并行计算有什么关系呢?答案是:并行计算机仍然遵从这一基本架构,只是处理单元多于一个而已,其它的基本架构完全保持不变。

3.2 弗林的经典分类

有不同的方法对并行计算机进行分类(具体例子可参见并行计算分类)。

一种被广泛采用的分类被称为弗林经典分类,诞生于1966年。弗林分类法从指令流数据流两个维度区分多处理器计算机体系结构。每个维度有且仅有两个状态:单个或者多个

下面个矩阵定义了弗林分类的四个可能状态:
在这里插入图片描述

单指令单数据(SISD): SISD是标准意义上的串行机,具有如下特点:1)单指令:在每一个时钟周期内,CPU只能执行一个指令流;2)单数据:在每一个时钟周期内,输入设备只能输入一个数据流;3)执行结果是确定的。这是最古老的一种计算机类型。
在这里插入图片描述

单指令多数据(SIMD): SIMD属于一种类型的并行计算机,具有如下特点:1)单指令:所有处理单元在任何一个时钟周期内都执行同一条指令;2)多数据:每个处理单元可以处理不同的数据元素;3)非常适合于处理高度有序的任务,例如图形/图像处理;4)同步(锁步)及确定性执行;5)两个主要类型:处理器阵列和矢量管道。
在这里插入图片描述

多指令单数据(MISD): MISD属于一种类型的并行计算机,具有如下特点:1)多指令:不同的处理单元可以独立地执行不同的指令流;2)单数据:不同的处理单元接收的是同一单数据流。这种架构理论上是有的,但是工业实践中这种机型非常少。
在这里插入图片描述

多指令多数据(MIMD): MIMD属于最常见的一种类型的并行计算机,具有如下特点:1)多指令:不同的处理器可以在同一时刻处理不同的指令流;2)多数据:不同的处理器可以在同一时刻处理不同的数据;3)执行可以是同步的,也可以是异步的,可以是确定性的,也可以是不确定性的。这是目前主流的计算机架构类型,目前的超级计算机、并行计算机集群系统,网格,多处理器计算机,多核计算机等都属于这种类型。值得注意的是,许多MIMD类型的架构中实际也可能包括SIMD的子架构。
在这里插入图片描述

3.3 一些常见的并行计算术语

和其它一些领域一样,并行计算也有自己的“术语”。下面列出了与并行计算相关联的一些常用术语,其中大部分术语我们在后面还会进行更详细的讨论。

3.4 并行程序的缺陷和代价

阿姆达尔定律: 阿姆达尔定律说一个程序的加速比潜力由其可以并行的部分所占的比例而决定,即: s p e e d u p = 1 1 − P . speedup = \frac{1}{1 - P}. speedup=1−P1​.
如果没有代码可以被并行,那么p = 0,所以加速比为1。如果所有的代码都可以被并行,那么 p = 1,加速比为无穷大(当然只是理论上而言)。如果50%的代码可以被并行,那么最大的加速比为2,意味着的运行速度最快可以被加速到2倍。

如果引入并行程序中的处理器个数,则加速比可以被重新定义为: s p e e d u p = 1 P N + S . speedup = \frac{1}{\frac{P}{N} + S}. speedup=NP​+S1​.
其中P仍然是并行代码所占的比例,N是处理器个数,S是串行代码所占比例(S = 1 - P)。
在这里插入图片描述
在这里插入图片描述

NP = 0.50p = 0.90p = 0.95p = 0.99
101.825.266.899.17
1001.989.1716.8050.25
10001.999.9119.6290.99
100001.999.9119.9699。02
1000001.999.9919.9999.90

之后我们就迅速意识到并行计算的极限所在,例如上表所示。

“注明引言:”你可以花费一生的时间使得你的代码的95%都可以被并行,然而你如论投入多少处理器,都不会获得20倍的加速比。

然而,某些问题我们可以通过增加问题的大小来提高其性能。例如:

类型时间比例
2D网格计算85秒85%
串行比例15秒15%

我们可以增加网格的维度,并且将时间步长减半。其结果是四倍的网格点数量,以及两倍的时间步长。之后的花费时间将变为:

类型时间比例
2D网格计算680秒97.84%
串行比例15秒2.16%

比起具有固定并行时间百分比的问题,那些可以随着问题规模增大而不断提高并行时间百分比的问题在并行化的意义上更具有可扩展性(复习一下可扩展性的定义_)。

复杂性: 通常而言,并行计算的程序要比相应的串行计算程序更加复杂,也许复杂一个量级。你不仅需要同时执行不同的指令流,而且需要注意他们之间数据的通信。复杂性通常由涉及软件开发周期各个方面的时间来确定,主要包括:1)设计;2)编码;3)调试;4)调参;5)维护。

遵循良好的软件开发实践对并行应用开发是至关重要的,尤其是在除你之外的其他人还需要和你合作的情况下。

可移植性: 由于一些API的标准化,例如MPI,POSIX线程以及OpenMP,并行程序的可移植性问题并不像过去那么严重,然而:

资源需求:
并行编程的主要目标就是降低时钟等待时间。然而要做到这一点,需要更多的CPU时间。例如,一个在8个处理器上跑了一个小时的并行程序实际上花费了8小时的CPU时间。

并行程序所需要的内存开销往往要大于相对应的串行程序,这是因为我们有时候需要复制数据,以满足库和子系统对并行算法的要求。

对于运行时间较短的并行陈顾,实际性能反而有可能比相对应的串行程序有所降低。这是由于并行环境的建立,任务创建,通讯,任务结束等在这个运行时间中有可能会占有比较大的比例。

可扩展性:
基于时间和解决方案的不同,可以将扩展性分为强可扩展性弱可扩展性

强可扩展性的特点是:1)在更多处理器被加入的时候,问题本身的规模不会增加;2)目标是将同一问题运行的更快;3)理想状态下,相比于对应的串行程序,运行时间为1/P。

弱可扩展性的特点是:1)随着更多处理器被加入,每个处理上需要处理的问题规模保持一致;2)目标是在相同的时间内解决更多规模的问题;3)理想状态下,在相同时间内,可以解决的问题规模增加为原问题规模的P倍。

并行程序的性能提高取决于一系列相互依赖的因素,简单地增加更多的处理器并不一定是唯一的方法。

此外,某些问题可能本身存在扩展性的极限,因此添加更多的资源有时候反而会降低性能。这种情形会出现在很多并行程序中。

硬件在可扩展性方面也扮演者重要角色,例如:1)内存-CPU之间的带宽;2)通讯网络的带宽;3)某个机器或者某个机器集合中的内存大小;4)时钟处理速度。

支持并行的库或者子系统同样也会限制并行程序的可扩展性。
在这里插入图片描述

4 并行计算机的内存架构

4.1 共享内存

一般特征: 共享内存的并行计算机虽然也分很多种,但是通常而言,它们都可以让所有处理器以全局寻址的方式访问所有的内存空间。多个处理器可以独立地操作,但是它们共享同一片内存。一个处理器对内存地址的改变对其它处理器来说是可见的。根据内存访问时间,可以将已有的共享内存机器分为统一内存存取非统一内存存取两种类型。

统一内存存取(Uniform Memory Access): 目前更多地被称为_对称多处理器机器(Symmetric Multiprocessor (SMP))_,每个处理器都是相同的,并且其对内存的存取和存取之间都是无差别的。有时候也会被称为CC-UMA (Cache coherent - UMA)。缓存想干意味着如果一个处理器更新共享内存中的位置,则所有其它处理器都会了解该更新。缓存一致性是在硬件级别上实现的。
在这里插入图片描述

非统一内存存取(Non-Uniform Memory Access): 通常由两个或者多个物理上相连的SMP。一个SMP可以存取其它SMP上的内存。不是所有处理器对所有内存都具有相同的存取或者存取时间。通过连接而进行内存存取速度会更慢一些。如果缓存相缓存想干的特性在这里仍然被保持,那么也可以被称为CC-NUMA。
在这里插入图片描述

**优点:**全局地址空间提供了一种用户友好的编程方式,并且由于内存与CPU的阶级程度,使得任务之间的数据共享既快速又统一。

**缺点:**最大的缺点是内存和CPU之间缺少较好的可扩展性。增加更多的CPU意味着更加共享内存和缓存想干系统上的存取流量,从而几何级别地增加缓存/内存管理的工作量。同时也增加了程序员的责任,因为他需要确保全局内存“正确”的访问以及同步。

4.2 分布式内存

一般概念: 分布式内存架构也可以分为很多种,但是它们仍然有一些共同特征。分布式内存结构需要通讯网络,将不同的内存连接起来。一般而言,处理器会有它们所对应的内存。一个处理器所对应的内存地址不会映射到其它处理器上,所以在这种分布式内存架构中,不存在各个处理器所共享的全局内存地址。
在这里插入图片描述

由于每个处理器具有它所对应的局部内存,所以它们可以独立进行操作。一个本地内存上所发生的变化并不会被其它处理器所知晓。因此,缓存想干的概念在分布式内存架构中并不存在。

如果一个处理器需要对其它处理器上的数据进行存取,那么往往程序员需要明确地定义数据通讯的时间和方式,任务之间的同步因此就成为程序员的职责。尽管分布式内存架构中用于数据传输的网络结构可以像以太网一样简单,但在实践中它们的变化往往也很大。

优点: 1)内存可以随着处理器的数量而扩展,增加处理器的数量的同时,内存的大小也在成比例地增加;2)每个处理器可以快速地访问自己的内存而不会受到干扰,并且没有维护全局告诉缓存一致性所带来的开销;3)成本效益:可以使用现有的处理器和网络。

缺点: 1)程序员需要负责处理器之间数据通讯相关的许多细节;2)将基于全局内存的现有数据结构映射到该分布式内存组织可能会存在困难;3)非均匀的内存访问时间——驻留在远程结点上的数据比本地结点上的数据需要长的多的访问时间。

4.3 混合分布式-共享内存

一般概念: 目前世界上最大和最快的并行计算机往往同时具有分布式和共享式的内存架构。共享式内存架构可以是共线内存机器或者图形处理单元(GPU)。分布式内存组件可以是由多个共享内存/GPU连接而成的系统。每个结点只知道自己的内存,不知道网络上其它结点的内存。因此,需要在不同的机器上通过网络进行数据通讯。
在这里插入图片描述
从目前的趋势来看,这种混合式的内存架构将长期占有主导地位,并且成为高端计算在可见的未来中的最好选择。

优缺点: 1)继承了共享式内存和分布式内存的优缺点;2)优点之一是可扩展性;3)缺点之一是编程的复杂性。

5. 并行计算模型

5.1 概述

常见的并行编程模型包括:共享内存模型(无线程),线程模型,分布式内存/消息传递模型,数据并行模型,混合模型,单程序多数据模型,多程序多数据模型。

并行计算模型是作为硬件和内存架构之上的一种抽象存在。虽然不一定显而易见,但这些模型并不和特定的机器和内存架构有关。事实上,任何一个并行计算模型从理论上来讲都可以实现在任何一种硬件上。下面是两个例子。

在分布式内存架构上的共享内存模型。 机器内存分布于网络上的不同结点,但是对于用户而言,看到的确实一个具有全局地址空间的共享内存。这种方法通常被称为“虚拟共享存储器”。

在这里插入图片描述

在共享内存架构上的分布式内存模型。 最主要的是消息传递接口(MPI)。每个任务都可以直接访问跨所有机器的全局地址空间。然而,它们之间数据交换却是通过消息传递机制实现的,就像在分布式内存网络中所进行的那样。
在这里插入图片描述

那么到底使用哪一种呢?这往往取决于现有条件以及用户的偏好。没有最好的模型,但对模型的实现质量却可能存在差别。下面我们将分别描述上面提到的各种并行计算模型,并且讨论它们在实践中的一些实现方式。

5.2 共享内存模型(无线程)

在这种并行计算模型中,处理器/任务共享内存空间,并且可以异步地对内存进行读写。很多机制被用来控制对内存的存取,例如锁/信号量等,用来解决访问冲突以及避免死锁。这应该是最简单的并行计算模型了。

从编程者的角度来看,这种模型的好处之一数据“拥有者”的缺失,所以他们不必明确地指定数据之间的通讯。所有的处理器都可以看到和平等地存取共享内存。程序开发将因此而变得容易。

性能方面的一个重要缺点是对数据局部性的理解和管理讲变得困难:1)保持数据的局部性将有利于减少内存的存取负担,缓存刷新次数以及总线流量。而当多个处理器使用同一数据时,这些负担将会经常发生;2)不幸的是,保持数据的局部性往往比较难以理解,并且其难度超过一般用户的水平。

在这里插入图片描述

实现: 单机共享内存机器,本地操作系统,编译器及其对应的硬件都支持共享内存编程。在分布式内存机器上,内存在物理上存在于网络上不同的结点上,但是可以通过特殊的硬件和软件,将其变为全局可见。

5.3 线程模型

这是共享内存编程的一种模式。在线程模型中,一个单个的“重量级”进程可以拥有多个“轻量级”的并发执行路径。例如下图所示:

在这里插入图片描述

实现: 从编程的角度来讲,线程的实现通常包括如下两个方面:

程序员需要同时定义上面的两个方面(尽管有时候编译器可以提供帮助)。

线程并不是一个新概念。之前硬件供应商就曾经实现过他们自己的线程。由于这些线程的实现各不相同,所以使得程序员很难开发可移植的多线程应用程序。

而标准化工作却导致了两种完全不同的线程实现方式:POSIX ThreadsOpenMP

POSIX Threads:由IEEE POSIX 1003.1c standard (1995)定义,仅支持C语言,是Unix/Linux操作系统的一部分,是基于库函数的,也通常被称为“Pthreads”。是非常明确的并行机制,需要程序员注意大量的细节。更多信息可见:POSIX Threads tutorial

OpenMP:是一个工业标准,有一组主要计算机硬件和软件提供商,组织和个人联合发起和定义,是基于编译器指令的,具有可移植性/跨平台性,目前支持的包括Unix和Windows平台,目前支持C/C++和Fortran语言。非常简单和医用,提供了“增量并行”,可以从串行代码开始。更多信息可见:OpenMP tutorial

也有一些其它的常见线程实现,但是在这里没有讨论,包括:

5.4 分布式内存/消息传递模型

这种模型具有如下特点:

在这里插入图片描述

实现: 从编程的角度来讲,消息传递的实现通常包括子程序库。对这些子程序的调用往往就嵌入在源代码中。编程者负责并行机制的实现。

自从1980年代以来,出现了多种消息传递库函数。这些实现各不相同,导致编程者很难开发出移植性好的应用程序。自从1992年开始,MPI Forum形成,其主要目标是建立消息传递的标准接口。消息传递接口(Message Passing Interface (MPI))的第一部分在1994年发布,第二部分在1996年发布,第三部分在2012年发布。所有的MPI说明可以参见 http://mpi-forum.org/docs/。MPI成为了事实上的消息传递的工业标准,取代了所有其它消息传递的实现。几乎所有流行的并行计算平台都存在MPI的实现,但并不是所有的实现都包含了MPI-1,MPI-2和MPI-3的所有内容。关于MPI的更多信息可以参见 MPI tutorial

5.5 数据并行模型

通常也被称为“全局地址空间分区”(Partitioned Global Address Space (PGAS))模型。具有如下特点:

在这里插入图片描述

在共享内存的架构下,所有的任务通过全局内存方式来对数据进行存取;在分布式内存架构下,根据任务分配,全局数据结构在物理或者逻辑上被进行分割。

实现: 目前,基于数据并行/PGAS模型,有如下几个相对有名的实现:

5.6 混合模型

混合模型指的是包含了至少两个我们前面提到的并行计算模型的模型。目前,最常见的混合模型的例子是消息传递模型(MPI)和线程模型(OpenMP)的结合:

这种混合模型非常适合目前流行的硬件环境——多核计算机组成的集群系统。

在这里插入图片描述

另外一种类似的,但原来越流行的例子是采用MPI和CPU-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. 手动并行化

设计和实现并行程序是一个非常手动的过程,程序员通常需要负责识别和实现并行化,而通常手动开发并行程序是一个耗时,复杂,易于出错并且迭代的过程。很多年来,很多工具被开发出来,用以协助程序员将串行程序转化为并行程序,而最常见的工具就是可以自动并行化串行程序的并行编译器(parallelizing compiler)或者预处理器 (pre-processor)。

并行编译器通常以如下两种方式工作:

最常见的由编译器生成的并行化程序是通过使用结点内部的共享内存和线程实现的(例如OpenMP)。

如果你已经有了串行的程序,并且有时间和预算方面的限制,那么自动并行化也许是一个好的选择,但是有几个重要的注意事项:1)可能会产生错误的结果;2)性能实际上可能会降低;3)可能不如手动并行那么灵活;4)只局限于代码的某个子集(通常是循环);5)可能实际上无法真正并行化,由于编译器发现里面有依赖或者代码过于复杂。

接下来的部分仅适用于手动开发并行程序。

6.2 理解问题和程序

毫无疑问,开发并行程序的第一步就是理解你将要通过并行化来解决的问题。如果你是从一个已有的串行程序开始的,那么你需要首先理解这个串行程序。

在开始尝试开发并行解决方案之前,需要确定该问题是否真正可以被并行化。

计算数千个独立分子构象中每一个的势能,完成之后,找出能量构象最小的那一个。

采用如下公式计算菲波那切数列 (0,1,1,2,3,5,8,13,21,…):F(n) = F(n-1) + F(n-2)

‎F(n) 值的计算使用 F(n-1) 和 F (n-2) 的计算,必须首先计算。‎
‎用于解决此问题的平行算法示例(使用 Binet 的公式):‎
在这里插入图片描述
其中,在这里插入图片描述

识别程序的关键点 (hotspots)

识别程序中的瓶颈 (bottlenecks)

识别并行化的抑制因素。一个常见的类型是_数据依赖性 (data dependence)_,例如上面提到的菲波那切数列的例子。

如果可能的话,研究其它算法。这可能是设计并行程序的过程中最重要的一点。

利用成熟的第三方并行软件,或者高度成熟的数学库(例如IBM的ESSL,Intel的MKL,AMD的AMCL等)。
在这里插入图片描述

6.3 分割 (Partitioning)

设计并行程序的第一步就是将程序分解成为可以分配到不同任务中去的“块”。这被称为程序的分解 (decomposition) 或者分割 (partitioning)。通常有两种基本方法可以将并行任务进行分解:域分解功能分解

域分解: 在这种分割方式中,将数根据问题进行分解。每个并行任务操作数据的一部分。

在这里插入图片描述

通常由不同的方式来对数据进行分割:

在这里插入图片描述

功能分解:

在这种方法中,重点在于要执行的计算,而不是计算所操纵的数据。问题根据要做的工作进行分解,然后每个任务执行整个工作的一部分。

在这里插入图片描述

这种功能分解非常适合于可分为不同任务的问题,例如:

在这里插入图片描述

在这里插入图片描述

在实践中将这两种分解方式结合起来是很自然的,也是很常见的。

6.4 通讯 (Communications)

任务之间的通讯需求取决于你的问题:

不需要通讯的情况: 一些程序可以被分解成为并发执行的任务,而这些任务之间不需要共享数据。这类问题往往被称为“尴尬并行”——任务之间不需要数据通讯。例如如果我们需要对下面一副图片的颜色进行取反(黑色的部分变为白色的,白色的变为黑色的),那么图像数据可以被简单地分解为多个任务,并且每个任务可以被独立地执行。

在这里插入图片描述

需要通讯的情况: 大多数并行程序并不像上一问题这么简单,任务之间确实需要共享数据。例如下面这幅热度扩散图需要一个任务知道其它任务在它的邻居方格中的计算结果。邻居数据的变化将直接影响到该任务的数据。

在这里插入图片描述

设计通讯需要考虑的因素: 在设计程序任务之间的通讯时,有大量的重要因素需要考虑:

在这里插入图片描述

在这里插入图片描述

最后需要意识到,上面提到的仅仅是需要注意的问题的一部分!

6.5 同步 (Synchronization)

管理工作的顺序和执行它的任务是大多数并行程序设计的关键,它也可能是提升程序性能的关键,通常也需要对某些程序进行“串行化”。

在这里插入图片描述

同步的类型:

6.6 数据依赖性 (Data Dependencies)

定义:

数据依赖也是并行程序设计中的关键,因为它是并行化中一个重要的抑制因素。

在这里插入图片描述

例子:

DO J = MYSTART,MYEND
   A(J) = A(J-1) * 2.0
END DO
task 1        task 2
------        ------

X = 2         X = 4
  .             .
  .             .
Y = X**2      Y = X**3

尽管在并行程序设计中,对所有数据依赖的识别都是重要的,但循环相关的数据依赖尤其重要,因为循环往往是最常见的可并行化部分。

处理方法: 1)分布式内存架构:在同步点传输所需数据;2)共享式内存结构:在任务之间同步读写操作。

6.7 负载均衡 (Load Balancing)

负载均衡是指在任务之间分配大约相等数量的工作的做法,以便所有任务在所有时间保持繁忙,它也可以被认为是使任务空闲时间最小化。出于性能原因方面的考虑,负载均衡对并行程序很重要。例如如果所有恩物都收到屏障同步点的影响,那么最慢的任务将决定整体性能。
在这里插入图片描述

如何实现负载均衡:

对于数组/矩阵而言,如果每个任务都执行相同或者类似的工作,那么在任务之间平均分配数据集;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)难以进行较好的负载均衡。
在这里插入图片描述

最佳选择: 最有效的粒度取决于具体算法及其所运行的硬件环境。在大多数情况下,与通讯/同步相关的开销相对于执行速度很高,因此具有粗粒度的问题是相对有利的。而从另外一方面来讲,细粒度则可以帮助减少由负载不均衡所造成的开销。

6.9 输入输出 (I/O)

坏消息: 1)I/O操作通常被认为是并行化的抑制剂;2)I/O操作通常比内存操作需要多个数量级的时间;3)并行I/O系统可能不成熟或者不适用于所有平台;4)在所有任务均可以看到相同文件空间的环境中,写操作可能导致文件被覆盖;5)读操作可能受到文件服务器同时处理多个读取请求的能力影响;6)必须通过网络进行的I/O操作(NFS,非本地)可能导致严重的性能瓶颈,甚至导致文件服务器崩溃。

在这里插入图片描述

好消息: 已经具有不少并行文件系统,例如:

作为MPI-2的一部分,1996年以来MPI的并行I/O编程借口规范已经可用。

注意事项: 1)尽可能减少整体I/O操作;2)如果你有权访问并行文件系统,请使用它;3)在大块数据上执行少量写操作往往比在小块数据上进行大量写操作有着更明显的效率提升;4)较少的文件比许多小文件更好;5)将I/O操作限制在作业的特定串行部分,然后使用并行通讯将数据分发到并行任务中。例如任务1可以读输入文件,然后将所需数据传送到其它任务。同样,任务2可以再从所有其它任务收到所需数据之后执行写入操作;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 pagesTotalView tutorial
在这里插入图片描述

6.11 性能分析和调优 (Performance Analysis and Tuning)

对于调试而言,并行程序的性能分析和调优比串行程序更具挑战性。幸运的是,并行程序性能分析和调优有很多优秀的工具。Livemore计算机用户可以访问这种类似工具,其中大部分都在集群系统上。一些安装在LC系统上的工具包括:

在这里插入图片描述

7 并行示例

7.1 数组处理

此示例演示了对二维数组元素的操作:将某个函数作用于二维数组中的每个元素,其中对每个数组元素的操作都是独立于其它数组元素的,并且该问题是计算密集型的。对于串行程序而言,我们依次对每个元素进行操作,其代码类似于:

do j = 1,n
  do i = 1,n
    a(i,j) = fcn(i,j)
  end do
end do

在这里插入图片描述

问题:

并行方案1:

Column-major:

do j = mystart, myend 
  do i = 1, n 
    a(i,j) = fcn(i,j) 
  end do 
end do

Row-major:

for i (i = mystart; i < myend; i++) {
  for j (j = 0; j < n; j++) {
    a(i,j) = fcn(i,j);
  }
}

在这里插入图片描述

一种可能的解决方案: 1)采用单程序多数据 (SPMD) 模型进行实现,每个任务执行相同的程序;2)主进程对数组进行初始化,将信息发送给子任务,并且接收计算结果;3)子进程接收到信息之后,执行计算任务,并且将结果发送给主进程;4)采用Fortran的存储结构,对数组进行块划分;5)伪代码如下所示。6)具体的C代码可以参见MPI Program in C

find out if I am MASTER or WORKER

if I am MASTER

  initialize the array
  send each WORKER info on part of array it owns
  send each WORKER its portion of initial array

  receive from each WORKER results 

else if I am WORKER
  receive from MASTER info on part of array I own
  receive from MASTER my portion of initial array

  # calculate my portion of array
  do j = my first column,my last column 
    do i = 1,n
      a(i,j) = fcn(i,j)
    end do
  end do

  send MASTER results 

endif

并行方案2:

上一个并行方案展示了静态负载均衡:1)每个任务执行固定量的工作;2)某些很快或者负载较轻的处理器将会拥有空闲时间,而最慢执行的任务最终决定整体性能。

如果所有任务在同一台机器上运行相同量的工作,那么静态负载均衡往往并不是一个主要问题。但是如果你确实有负载均衡方面的问题(某些任务比其它任务运行的快),那么你可以采用“任务池”(pool of tasks)模式。

任务池模式: 里面包含两个进程:

工作进程在运行之前不知道它将处理数组的哪一部分,以及它将执行多少任务。动态负载均衡发生在运行时:运行最快的任务将完成更多的任务。一段可能的源代码如下:

find out if I am MASTER or WORKER

if I am MASTER

  do until no more jobs
    if request send to WORKER next job
    else receive results from WORKER
  end do

else if I am WORKER

  do until no more jobs
    request job from MASTER
    receive from MASTER next job

    calculate array element: a(i,j) = fcn(i,j)

    send results to MASTER
  end do

endif

讨论: 1)在上述任务池例子中,每个任务计算数组的某一个元素,计算与通讯比率是细粒度的;2)细粒度的解决方案为了减少任务空闲时间,往往会导致更多的通讯开销;3)更好地解决方案可能是在每个任务中分配更多的工作,“正确”的工作量依然是依赖于具体问题的。

7.2 圆周率计算

计算圆周率的方法有多种。我们这里采用蒙特卡洛方法来近似计算圆周率:1)在一个边长为 2r2r2r 的正方形中画一个半径为 rrr 的内接圆;2)内接圆的面积是 πr2πr2\pi r^2,正方形的面积是 4r24r24r^2;3)圆的面积与正方形的面积之比是 πr2/4r2=π/4πr2/4r2=π/4\pi r^2 / 4r^2 = \pi / 4;4)如果你在正方形内随机地产生 NNN个点,那么大约会有 N∗π/4N∗π/4N * \pi / 4 个点(MMM)会位于内接圆内;5)所以 ππ\pi 可以被近似为:N∗π/4=MN∗π/4=MN * \pi / 4 = M,π/4=M/Nπ/4=M/N\pi / 4 = M / N,π=4∗M/Nπ=4∗M/N\pi = 4 * M / N;6)注意越多的点产生,则对 ππ\pi的近似就越准确。

在这里插入图片描述

该问题的串行代码大约是这样的:

npoints = 10000
circle_count = 0

do j = 1,npoints
  generate 2 random numbers between 0 and 1
  xcoordinate = random1
  ycoordinate = random2
  if (xcoordinate, ycoordinate) inside circle
  then circle_count = circle_count + 1
end do

PI = 4.0*circle_count/npoints

该问题是计算密集型的——大多数时间将花费在对循环的执行。

问题:

解决方案:

又一个容易被并行化的问题:1)每个点的计算都是独立的,不存在数据依赖;2)工作可以被平均分配,不需要考虑负载均衡;3)任务之间不需要通讯和同步。

并行化策略:1)将任务平均划分为相同大小的多个部分,用于在任务池中被执行;2)每个任务独立地完成任务;3)采用单程序多数据(SPMD)模型;4)其中一个任务作为主进程来收集计算结果,并且计算 ππ\pi 的值。

在这里插入图片描述

下面是并行化之后的伪代码:

npoints = 10000
circle_count = 0

p = number of tasks
num = npoints/p

find out if I am MASTER or WORKER 

do j = 1,num 
  generate 2 random numbers between 0 and 1
  xcoordinate = random1
  ycoordinate = random2
  if (xcoordinate, ycoordinate) inside circle
  then circle_count = circle_count + 1
end do

if I am MASTER

  receive from WORKERS their circle_counts
  compute PI (use MASTER and WORKER calculations)

else if I am WORKER

  send to MASTER circle_count

endif

C语言的示例程序可以参考这里:MPI Program in C

7.3 简单热方程

大多数并行计算问题需要任务之间的通讯,其中一大部分问题需要“相邻”任务之间的通讯。

在这里插入图片描述

二维热方程问题描述了在给定初始温度分布以及边界条件的情况下,温度随着时间的变化。有限差分方案可以采用数值方法求解正方形区域内的热扩散方程:

每个元素的文图的计算取决于它的邻居的温度:

在这里插入图片描述

串行程序的代码可能使这个样子:

do iy = 2, ny - 1
  do ix = 2, nx - 1
    u2(ix, iy) =  u1(ix, iy)  +
        cx * (u1(ix+1,iy) + u1(ix-1,iy) - 2.*u1(ix,iy)) +
        cy * (u1(ix,iy+1) + u1(ix,iy-1) - 2.*u1(ix,iy))
  end do
end do

问题:

解决方案:

该问题更具有挑战性。因为存在数据依赖,所以需要任务之间的通讯和同步。整个数组需要被风格成为子数组,并分配给不同任务,每个任务拥有整个数组的一部分。由于任务量是均匀划分的,所以不需要考虑负载均衡。
在这里插入图片描述

确定数据依赖:1)一个任务的 内部元素 和其它任务之间不存在数据依赖;2)一个任务的 边界元素 则和它的邻居任务之间需要产生数据通讯。

采用单程序多数据模型(SPMD)进行实现:1)主进程向工作进程发送初始信息,然后等待并收集来自工作进程的计算结果;2)工作进程在特定的时间步长内计算结果,并与邻居进程之间进行数据交换。伪代码如下:

find out if I am MASTER or WORKER

if I am MASTER
  initialize array
  send each WORKER starting info and subarray
  receive results from each WORKER

else if I am WORKER
  receive from MASTER starting info and subarray

  # Perform time steps
  do t = 1, nsteps
    update time 
    send neighbors my border info
    receive from neighbors their border info 
    update my portion of solution array

  end do

  send MASTER results

endif

示例程序可以参加:MPI Program in C

7.4 一维波动方程

在这个例子中,我们计算经过指定时间量之后,一维波动曲线的振幅。其中的计算会涉及到:1)y轴上的振幅;2)x轴上的位置索引i;3)沿着波动曲线的节点;4)以离散时间步长更新振幅。

在这里插入图片描述

这里需要求解的是如下一维波动方程,其中c是常数。

A(i,t+1) = (2.0 * A(i,t)) - A(i,t-1) + (c * (A(i-1,t) - (2.0 * A(i,t)) + A(i+1,t)))

我们注意到,t时刻的振幅取决于前一刻的时间步长(t, t-1)以及相邻点(i - 1, i + 1)。

问题:

解决方案:

这是涉及到数据依赖的另外一个例子,其并行方案将会涉及到任务见的通讯和同步。整个振幅阵列被分割并分配给所有的子任务,每个任务拥有总陈列的相等的部分。由于所有点需要相等的工作,所以我们应该均匀地分配节点。我们可以将工作分成多个块,并且允许每个任务拥有大多数连续的数据点。而通讯只需要在数据边界上进行,块大小越大,则所需的通信越少。
在这里插入图片描述

采用单程序多数据(SPMD)模型的实现:1)主进程向工作进程发送初始信息,并且等到各个工作进程返回计算结果;2)工作进程对特定步长之内的任务进行计算,并且在必要的时候和邻居进程进行数据通讯。其伪代码如下:

find out number of tasks and task identities

#Identify left and right neighbors
left_neighbor = mytaskid - 1
right_neighbor = mytaskid +1
if mytaskid = first then left_neigbor = last
if mytaskid = last then right_neighbor = first

find out if I am MASTER or WORKER
if I am MASTER
  initialize array
  send each WORKER starting info and subarray
else if I am WORKER`
  receive starting info and subarray from MASTER
endif

#Perform time steps 
#In this example the master participates in calculations
do t = 1, nsteps 
  send left endpoint to left neighbor
  receive left endpoint from right neighbor
  send right endpoint to right neighbor
  receive right endpoint from left neighbor

  #Update points along line
  do i = 1, npoints
    newval(i) = (2.0 * values(i)) - oldval(i) 
    + (sqtau * (values(i-1) - (2.0 * values(i)) + values(i+1))) 
  end do

end do

#Collect results and write to file
if I am MASTER
  receive results from each WORKER
  write results to file
else if I am WORKER
  send results to MASTER
endif 

程序示例可以参见:MPI Program in C

8 参考文献和更多信息

标签:通讯,Computing,Introduction,程序,并行,任务,并行计算,内存
来源: https://blog.csdn.net/Ber_Bai/article/details/118363160