MPI入门

第一章 消息传递基础

问题分解

设计并行算法的第一步是把问题分解成更小的问题。粗略来看,有两种分解方式:

1、域分解:也称为“数据并行化”。数据被分割成大约相同尺寸的小块,然后被映射到不同的处理器。每个处理器只处理被分配给它的部分数据。当然,处理器之间可能需要周期性地通信以便交换顺序。

数据并行化的优点是维护了单个控制流。数据并行算法由应用到数据的基本指令序列(只在前一个指令完成才开始的指令)组成。单程序多数据(SPMD)遵循这个模型,所有处理器里的代码都相同。

这种策略普遍应用于有限差分算法(finite differencing algorithm),处理器可以独立地操作大块的部分数据,只在每次迭代交换小得多的边界数据。

2、功能分解:域分解策略通常不是一个并行程序的最高效的算法。这种情况下被分配到不同处理器的各个数据片段需要明显不同的处理时间。代码性能的瓶颈在于最慢的进程。剩余的空闲进程没有做有用的工作。在这种情况下,功能分解(也称任务并行化)比域分解更合理。此时问题被分解为很多更小的任务,而任务在可用时会被分配到不同的处理器。更快完成的处理器会被分配更多的任务。

任务并行化由C/S结构实现。任务由主进程分配到一组从进程上,而主进程自身也可能执行一些任务。事实上C/S结构可以在程序的任何层次上实现。例如,你简单地想要运行一个有多个输入的程序。一个并行的C/S实现可能只是运行代码的多个拷贝,服务器串行地把不同的输入分配到每个进程。当各个进程完成了自己的任务,它被分配一个新的输入。除此之外,任务并行话也可以在代码的更深层次实现。


数据并行与消息传递模型

历史上,有两种方式可以写并行程序。

1、使用基于指令的数据并行语言;

2、通过标准编程语言的库调用来显式地传递消息。

在基于指令的数据并行语言里,比如High Performance Fortran(HPF)或OpenMP,通过加入(看起来像注释的)指令来告诉编译器如何跨处理器分发数据和工作,使得串行代码并行化。数据并行化语言通常在共享内存架构上实现,因为全局内存空间极大地简化了编译器的实现。

使用消息传递方式的话,程序员需要显式地把数据和工作分配到多个处理器,并管理它们之间的通信。这种方式非常地灵活。


并行编程需知

因为写并行化程序的主要目的是比串行版本得到更好的性能,所以在设计并行代码时有一些方面需要考虑,以便在解决问题的同时得到最好的效率。这些方面包括:

1、负载平衡(load balancing);

2、最小化通信(minimizing communication);

3、重叠通信与计算(overlapping communication and computation)。


负载平衡:把任务平等地分配到可用的进程。当所有进程都执行相同操作(来处理数据的不同片段)时比较容易。但是当处理时间取决于计算的数据值时就没那么简单了。当处理时间会有很大波动时,你可能需要使用不同的方式来解决这个问题。

最小化通信:在并行编程里总的运行时间是一个主要考虑,因为它是比较和提升所有程序的关键部分。执行时间由三个部分组成:

1、计算时间(Computation time):在数据计算上花费的时间。理想状态下,如果有N个处理器计算一个问题,你可以需要N分之一的串行工作时间来完成这个工作。如果所有的处理器时间都花在计算上时,可以达到。

2、空闲时间(Idle time):进程等待其它处理器的时间。在等待期间,处理器不做有用的工作。例如用并行程序处理I/O。许多消息传递库都没有解决并行I/O,使得某个进程处理所有的工作,而其它所有处理器都处于空闲状态。

3、通信时间(Communication time):进程发送和接收消息的时间。执行时间里的通信花费可以用“潜在时间(latency)”和“带宽(bandwidth)”来衡量。潜在时间用于设置通信所需的信封,而带宽是传输的真正速度,即每单位时间传送的位数。串行程序不需要使用进程间通信。因此,你必须最小化这个时间的使用,来得到最好的性能提升。

重叠通信与计算:有几种方式可以最小化进程的空闲时间,一个例子是重叠通信与计算。当某一进程等待通信完成时,它被分配一个或多个新的任务,所以它可以在等待时处理另一个任务。对非阻塞通信和非特定数据的计算的谨慎使用可以实现这种场景。在实践中交叉计算与通信其实是非常难的。



熟悉MPI

消息传递模型

1、平行计算由许多进程组成,每个都计算一些本地数据。每个进程都有纯粹的本地变量,且没有任何机制可以让任何进程直接访问另一个的内存。

2、进程间的共享通过消息传递发生,也就是通过显式地在进程间发送和接收数据。

注意该模型涉及到“进程”,在理论上它们不需要运行在不同的处理器上。我们这里通常假设不同进程运行在不同的处理器上,所以术语“进程”和“处理器”可以交替使用。

这个模型有用的主要原因是它非常具有一般性。本质上讲,任何类型的并行计算都可以转换成消息传递形式。此外,这个模型:

1、可以在许多不同平台上实现,从共享内存多处理器到工作站构成的网络,甚至可以是单处理器的机器。

2、通过并行应用,通常比诸如共享内存模型的模型允许对数据位置和流动的更多控制。因此程序通过使用显式的消息传递,经常可以达到更好的性能。事实上,性能是为何消息传递不太可能从并行编程世界里消失的原因。



消息传递编程接口MPI(Message Passing Interface)被试图作为消息传递模型的一个标准实现,是由全世界工业、科研和政府部门联合建立的一个消息传递编程标准,其目的是为了基于消息传递的并行程序设计提供一个高效、可扩展、统一的编程环境。它是目前最为通用的并行编程方式,也是分布式并行系统的主要编程环境。MPI-1标准定义于1994年春:

1、它分别规范了命名、调用序列、通过Fortran 77和C调用的子例程和函数的结果。MPI的所有实现遵守这些规则,从而保证了兼容性。MPI程序可以在任何支持MPI标准的平台上编译和运行;

2、库详细的实现交给独立的厂商,他们可以自由地针对他们的机器提供优化版本;

3、MPI-1标准的实现对许多平台可用。

MPI-2标准定义了三个单向的通信操作:

1、Put:写入远程内存;

2、Get:读取远程内存;

3、Accumulate:缩减各任务间的相同内存;

MPI-2同时还定义了三种不同的同步方式(全局锁、成对锁、和远程锁)。还有并行I/O、C++和Fortran 90绑定、以及动态进程管理所使用的工具。目前有些MPI实现已经包含了部分MPI-2标准,但完整的MPI-2还不可用。


MPI的主要目标是为了:

1、提供源代码的兼容性。MPI程序应当可以在任何平台上编译和运行;

2、允许不同架构上的高效实现。

MPI也提供了:

1、许多功能,包含许多不同类型的通信、普通“收集”操作的特定指令、和处理用户定义数据类型和拓扑的能力;

2、对异构并行架构的支持。


明显没有包含在MPI-1的有:

1、运行一个MPI程序的精确机制。一般说来,这是平台相关的且你需要翻阅本地文档来找到如何完成这个机制;

2、动态进程管理,也就是在代码运行时改变进程的数量;

3、调试;

4、并行I/O。


使用MPI的时机

当你需要做以下事时应该使用MPI:

1、编写可移植的并行代码;

2、通过并行编程得到高效率,例如编写并行库;

3、处理涉及不适合“数据并行”模型的不规范或动态的数据关系的问题。

以下情况不适合使用MPI:

1、可以通过数据并行(例如High-Performance Fortran)或共享内存的方法(例如OpenMP或基于指令的专利范式)得到足够的性能和可移植性;

2、可以使用已有库里的(本身由MPI实现的)并行例程。

3、根本不需要并行机制!!


消息传递程序的基本特性

消息传递程序由通过函数调用来通信的串行程序的多个实例组成。这些调用大致可以分为四类:

1、用于初始化、管理、最终终止通信的函数;

2、用于处理器对之间通信的函数;

3、执行进程组间通信操作的函数;

4、创建任意数据类型的函数。

第1类函数由以下函数组成:开启通信、标识所使用的处理器的数量、创建子处理器组、以及标识程序的一个特定实例在哪个处理器上运行。

第2类函数被称为点对点通信操作,由不同类型的发送接收操作组成。

第3类函数为收集操作,提供进程组间的同步或特定类型的明确定义的通信操作,并执行通信/计算操作。


MPI系统

除各厂商提供的MPI系统外,一些高校、科研部门也在开发免费的通用MPI系统,其中比较著名的有:

1、MPICH

2、LAM MPI

它们均提供源代码,并支持目前绝大部分并行计算机系统(包括微机和工作站机群)。事实上许多厂商提供的MPI系统是在MPICH的基础上经过针对特定硬件优化形成的。

MPI标准的第一个版本MPI 1.0于1994年公布,最新标准为2.0版,于1998年公布。

一个MPI系统通常由一组库、头文件和相应的运行、调试环境构成。MPI并行程序通过调用MPI库中的函数来完成消息传递,编译时与MPI库链接。而MPI系统提供的运行环境则负责一个MPI并行程序的启动和退出,并提供适当的并行程序调试、跟踪方面的支持。


MPICH是目前使用最广泛的免费MPI系统,它支持几乎所有Linux/Unix以及Windows 9x、NT、2000和XP系统。利用MPICH既可以在单台微机或工作站上建立MPI程序的调试环境,使用多个进程模拟运行计算环境。事实上,它是运行在目前大部分机群系统上的主要并行环境。

在Ubuntu的软件中心搜索mpich,可以找到“Development files for MPICH2 (libmpich2-dev)”,安装它可以得到C、C++、和Fortran程序的mpi编译器,即mpicc、mpicxx、mpif77和mpif90。


第一个程序

#include <stdio.h>
#include <mpi.h>

void main(int argc, char *argv[]) {
    int err;
    err = MPI_Init(&argc, &argv);
    printf("Hello world!\n");
    err = MPI_Finalize();
}

使用命令mpicc first_program.c可以得到可执行文件a.out。

可以看到MPI的函数/子例程的名字都以MPI_开头。同时注意到头文件(mpi.h或mpif.h)包含了MPI的函数原型以及定义。MPI函数会返回一个错误码来表明是否有错误发生。


MPICH系统使用ch_p4(CHannel_Portable Programs for Parallel Processors)作为底层通信支持。基于ch_p4的MPICH可以像普通UNIX可执行文件一样直接执行,但默认情况下它只启动一个进程。当需要启动多个进程时,有两个方法来控制启动的进程数目,第一个方法是使用选项-p4pg,第二个方法是利用MPICH提供的一些脚本文件,如mpirun。在单机情况下,最方便的是用命令mpirun来运行MPICH程序。

mpirun最简单、最常用的形式为:

mpirun [-np 进程数] 程序名 [命令行参数]

方括号中为可选参数。

例如:

mpi ./a.out

输出

Hello world!

mpirun -np 4 ./a.out

输出

Hello world!
Hello world!
Hello world!
Hello world!

点对点通信与消息

MPI里基本的通信是“点对点”通信。也就是两个处理器之间的直接通信,一方发送而另一方接收。

MPI里的点对点通信是双方参与的,也就是说同时需要显式的发送和显式的接收。在没有两个进程同时参与的情况下,数据不能传输。

在通常的发送和接收里,进程间传递由一些块数据组成的消息。一个消息由指明源进程和目标进程的信封和包含要发送的真实数据的主体组成。

MPI使用三部分信息来灵活地描述消息主体:

1、缓冲:内存的起始地址,存储要发送的消息或接收到的消息;

2、数据类型:最简单的例子是基本类型float、int等。更高级的应用里可以是基于基本类型的用户定义类型,类似于C的结构体,但数据可以放置在内存的任何地方,而不必是连续的内存地址。

3、计数器:要发送的数据类型的项数。

注意MPI标准化了基本类型的名称。这意味着我们不必担心异构环境下机器表示的区别。


通信模式和竞争临界区

MPI提供了很大的灵活性来指明消息如何发送。有多种定义了传送消息的过程的通信模式,以及决定通信事件何时结束的一堆临界区。例如:同步发送被定义为只当目的地承认接收到消息时才完成。缓冲发送则在数据拷贝到一个(本地)缓冲区时则完成,而不保证消息到达目的地。在所有的情况下,发送的完成都暗示着可以安全覆盖原有数据的内存区域。

有四种可用的发送通信模式:

1、标准(Standard);

2、同步(Synchronous);

3、缓冲的(Buffered);

4、预备的(Ready)。


对于接收方,只有一种通信模式。当数据真正到达并可用时,接收才算完成。


阻塞与非阻塞通信

阻塞发送/接收直到操作完成才从函数返回。这保证了调用进程继续执行时相关的竞争临界条件已经得到满足。

非阻塞发送/接收立即返回,而不保证竞争条件是否满足。这样的好处是进程可以在后台进行通信,同时还能做其它事情。


集体通信(Collective Communications)

除了点对点通信,MPI还有执行集体通信的函数。它们允许更大的进程组以不同的方式通信,比如一对多或多对一。

相比与点对点通信,使用集体通信函数的好处有:

1、出错的概率大大降低。一行调用集体通信函数的代码,等价于多行点对点通信的调用代码。

2、源代码可读性更高,因而简化了调试和维护。

3、集体通信的优化形式经常比等价的点对点通信更快。

集体通信的例子包括广播操作、收集与散播操作、以及收缩操作。


广播操作(Broadcast Operation)

最简单的集体操作类型。单个进程把一些数据的拷贝发送给一个组的所有其它进程。多个接收进程都收到相同数据。


收集与散播操作(Gather and Scatter Operations)

可能是最重要的收集操作类型。把一个进程的数据分发到一组进程,或反之。MPI提供了两种收集与散播操作,即数据均匀或不均匀地跨进程分发。

在散播操作中,所有的数据(某种类型的数组)初始由单个进程收集,之后数据的各个片段被分发到不同的进程上。这些数据片段可能不是均匀分割的。收集操作是散播操作的逆操作:把分布在多个进程上的数据片段以恰当的顺序聚集到单个进程上。


收缩操作(Reduction Operations)

该操作里,单个进程(根进程)从某个组里的其它进程收集数据,并把它们合并成单个数据项。例如,使用收缩操作来求一个数组分布到各个进程上的元素的和。其它的例子还有求最大值、最小值、各种逻辑或位运算等等。


第三章  MPI程序结构

一般MPI程序

所有的MPI程序有以下通用的结构:

1、包含MPI头文件;

2、变量声明;

3、初始化MPI环境;

4、计算以及MPI通信调用;

5、关闭MPI通信。


MPI头文件包含MPI描述的定义以及函数原型。

之后是变量声明,每个进程调用一个MPI例程来初始化消息传递环境。所有的MPI通信例程的调用必须在初始化之后。

最后在程序结束前,每个进程都必须调用一个例程来终止MPI。在终止例如调用后不能用MPI例程被调用。注意任何进程在执行时不遵循这个约定的话,程序会表现为挂起。


MPI头文件

包含MPI函数/子例程的原型、宏定义、特殊常量、MPI使用的数据类型。C语言中使用#include <mpi,h>来包含该头文件。


MPI命名规范

所有MPI项(例程、常量、类型等)都由MPI_开头来避免冲突。C语言里函数名以大写开头,其余为小写,比如MPI_Init。MPI常量均为大写,例如MPI_COMM_WORLD。特殊定义的类型对应于许多MPI项,类型名和函数命名一样,例如:MPI_Comm对应于一个MPI类型communicator。


MPI例程和返回值

在C里MPI例程被实现为函数。通常一个错误码会返回,用于测试例程是否成功。例如:

int err;

err = MPI_Init(&argc, &argv);


MPI_SUCCESS表示例程成功运行:

if (err == MPI_SUCCESS) {

    ...

}


MPI句柄

MPI定义并维护与通信相关的它自己的内部数据结构。你通过句柄引用这些数据结构。句柄通过各种MPI调用返回,并可作为其它MPI调用的参数。

在C语言里,句柄是指向特定数据类型的指针。数组以0开始索引。


MPI 数据类型

MIP提供了它自己的引用数据类型,对应于C的各种基本数据类型。变量通常声明为C类型,MPI类型名用于MPI例程的参数。

MPI隐藏了数据类型表示的细节,比如浮点数的表示。这是实现考虑的事情。

MIP允许异构环境下表示的自动转换。

作为一个通用规则,接收的MPI数据类型必须与发送时指定的MPI数据类型匹配。

此外,MPI允许基于基本类型构建任意数据类型。


C语言的基本MPI数据类型

MPI数据类型 C类型
MPI_CHAR signed char
MPI_SHORT signed short int<
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值