第五章 消息传递接口MPI.docx
《第五章 消息传递接口MPI.docx》由会员分享,可在线阅读,更多相关《第五章 消息传递接口MPI.docx(23页珍藏版)》请在冰豆网上搜索。
第五章消息传递接口MPI
第5章消息传递接口MPI
MPI(消息传递接口)是用于分布式存储器并行计算机的标准编程环境。
MPI的核心构造是消息传递:
一个进程将信息打包成消息,并将该消息发送给其他进程。
但是,MPI包含比简单的消息传递更多的内容。
MPI包含一些例程,这些例程可以同步进程、求分布在进程集中的数值的总和、在同一个进程集中分配数据,以及实现更多的功能。
在20世纪90年代早期人们创建了MPI,以提供一种能够运行在集群、MPP、甚至是共享存储器机器中的通用消息传递环境。
MPI以一种库的形式发布,官方的规范定义了对C和Fortran的绑定(对其他语言的绑定也已经被定义)。
当今MPI程序员主要使用MPI版本1.1(1995年发行)。
在1997年发行了一个增强版本的规范,MPI2.0,它具有并行I/O、动态进程管理、单路通信和其他高级功能。
遗憾的是,由于它对原有的标准增加了复杂的内容,使得到目前为止,仅有少量的MPI实现支持MPI2.0。
因为这个原因,我们将在本章中集中介绍MPI1.1。
5.1MPI编程的基本概念
5.1.1什么是MPI
对MPI的定义是多种多样的,但不外乎下面三个方面,它们限定了MPI的内涵和外延。
1MPI是一个库,而不是一门语言。
许多人认为MPI就是一种并行语言,这是不准确的。
但是按照并行语言的分类,可以把FORTRAN+MPI或C+MPI,看作是一种在原来串行语言基础之上扩展后得到的并行语言。
MPI库可以被FORTRAN77/C/Fortran90/C++调用,从语法上说,它遵守所有对库函数/过程的调用规则,和一般的函数/过程没有什么区别。
2MPI是一种标准或规范的代表,而不特指某一个对它的具体实现。
迄今为止,所有的并行计算机制造商都提供对MPI的支持,可以在网上免费得到MPI在不同并行计算机上的实现,一个正确的MPI程序,可以不加修改地在所有的并行机上运行。
3MPI是一种消息传递编程模型,并成为这种编程模型的代表和事实上的标准。
MPI虽然很庞大,但是它的最终目的是服务于进程间通信这一目标的。
关于什么是MPI的问题设计到多个不同的方面。
当我们提到MPI时,不同的上下文中会有不同的含义,它可以是一种编程模型,也可以是一种标准,当然也可以指一类库。
只要全面把握了MPI的概念,这些区别是不难理解的。
5.1.2MPI的三个主要目的
1较高的通信性能;
2较好的程序可移植性;
3强大的功能。
MPI为自己制定了一个雄心勃勃的目标,总结概括起来,它包括几个在实际使用中都十分重要但有时又是相互矛盾的三个方面,具体地说,包括以下几个方面:
提供应用程序编程接口。
提高通信效率。
措施包括避免存储器到存储器的多次重复拷贝,允许计算和通信的重叠等。
可在异构环境下提供实现。
提供的接口可以方便C语言和Fortran77的调用。
提供可靠的通信接口。
即用户不必处理通信失败。
定义的接口和现在已有接口(如PVM,NX,Express,p4等)差别不能太大,但是允许扩展以提供更大的灵活性。
定义的接口能在基本的通信和系统软件无重大改变时,在许多并行计算机生产商的平台上实现。
接口的语义是独立于语言的。
接口设计应是线程安全的。
MPI提供了一种与语言和平台无关,可以被广泛使用的编写消息传递程序的标准,用它来编写消息传递程序,不仅实用、可移植、高效和灵活,而且和当前已有的实现没有太大的变化。
5.1.3MPI的语言绑定与实现
在MPI-1中,明确提出了MPI和FORTRAN77与C语言的绑定,并且给出了通用接口和针对FORTRAN77与C的专用接口说明,MPI-1的成功说明MPI选择的语言绑定策略是正确和可行的。
Fortran90是FORTRAN的扩充,它在表达数组运算方面有独特的优势,还增加了模块等现代语言的方便开发与使用的各种特征,它目前面临的一个问题是Fortran90编译器远不如FORTRAN77编译器那样随处可见,但提供Fortran90编译器的厂商正在逐步增多。
C++作为面向对象的高级语言,随着编译器效率和处理器速度的提高,它可以取得接近于C的代码效率,面向对象的编程思想已经被广为接受,因此在MPI-2中,除了和原来的FORTRAN77和C语言实现绑定之外,进一步与Fortran90和C++结合起来,提供了四种不同的接口,为编程者提供了更多选择的余地。
但是MPI-2目前还没有完整的实现版本。
下面列出了一些主要的MPI免费实现。
表5.1MPI的一些实现
实现名称
研制单位
网址
Mpich
ArgonneandMSU
http:
//www-unix.mcs.anl.gov/mpi/mpich
Chimp
Edinburgh
ftp:
//ftp.epcc.ed.ac.uk/pub/packages/chimp/
Lam
OhioStateUniversity
http:
//www.mpi.nd.edu/lam/
MPICH是一种最重要的MPI实现,它可以免费从http:
//www-unix.mcs.anl.gov/mpi/mpich取得。
更为重要的是,MPICH是一个与MPI-1规范同步发展的版本,每当MPI推出新的版本,就会有相应的MPICH的实现版本,目前MPICH的最新版本是MPICH-1.2.1,它支持部分的MPI-2的特征。
ArgonneandMSU(阿尔贡)国家试验室和MSU(密西根州立大学)对MPICH作出了重要的贡献。
在本书中,未特别说明,均指在基于Linux集群的MPICH实现。
CHIMP是Edinburgh(爱丁堡、英国苏格兰首府)开发的另一个免费MPI实现,是在EPCC(EdinburghParallelComputingCentre)的支持下进行的,从ftp:
//ftp.epcc.ed.ac.uk/pub/packages/chimp/release/可以免费下载该软件,CHIMP的开发从1991年到1994年,主要开发人员有AlasdairBruce,James(Hamish)Mills,和GordonSmith。
LAM(LocalAreaMulticomputer)也是免费的MPI实现,由OhioStateUniversity美国俄亥俄州国立大学开发,它目前的最新版本是LAM/MPI6.3.2,可以从http:
//www.mpi.nd.edu/lam/download/下载。
它主要用于异构的计算机网络计算系统。
5.1.4MPI编程的基本概念
一个MPI并行程序由一组运行在相同或不同计算机/计算结点上的进程或线程构成。
这些进程或线程可以运行在不同处理机上,也可以运行在相同的处理机上。
为统一起见,MPI程序中一个独立参与通信的个体称为一个进程(process)。
一个MPI进程通常对应于一个普通进程或线程,但是在共享存储/消息传递混合模式程序中,一个MPI进程可能代表一组UNIX线程。
一个MPI程序中由部分或全部进程构成的一个有序集合称为一个进程组(processgroup)。
进程组中每个进程被赋予一个该组中的序号(rank),用于在该组中标识该进程,称为进程号。
进程号的取值范围从0开始。
MPI程序中进程间的通信、同步等通过通信器(communicator)进行(一些资料中将通信器翻译成通信子,本书中将统一使用术语通信器)。
MPI的通信器有域内通信器(intra-communicator)和域间通信器(inter-communicator)两种,前者用于属于同一进程组的进程间的通信,后者用于分属两个不同进程组的进程间的通信。
这里只对域内通信器进行介绍,后文中除非特别提及,“通信器”一词一律指域内通信器。
一个通信器由它所包含的进程组及与之相关联的一组属性(例如进程间的拓扑连接关系)构成。
通信器提供进程间通信的基本环境,MPI程序中所有通信都必须在特定的通信器中完成。
MPI程序启动时会自动创建两个通信器,一个称为MPI_COMM_WORLD,它包含程序中的所有进程,另一个称为MPI_COMM_SELF,它是每个进程独自构成的、仅包含自己的通信器。
在MPI程序中,一个MPI进程由一个通信器(或进程组)和进程在该通信器(或进程组)中的进程号唯一标识。
注意进程号是相对于通信器或进程组而言的:
同一个进程在不同的通信器(或进程组)中可以有不同的进程号。
进程号是在通信器(或进程组)被创建时赋予的。
MPI系统提供了一个特殊进程号MPI_PROC_NULL,它代表空进程(不存在的进程),与MPI_PROC_NULL进行通信相当于一个空操作,对程序的运行没有任何影响,它的引入可以方便一些程序的编写。
MPI程序中进程间的通信(communications)通过消息的收发或同步操作完成。
一个消息(message)指在进程间进行的一次数据交换。
MPI消息包括信封和数据两个部分,信封指出了发送或接收消息的对象及相关信息,而数据是本消息将要传递的内容。
信封和数据又分别包括三个部分。
可以用一个三元组来表示。
信封:
<源/目,标识,通信域>
数据:
<起始地址,数据个数,数据类型>
以MPI_SEND和MPI_RECV(receive)为例,下图分别给出了它们的信封和数据部分。
图5.1MPI_SEND语句的消息信封和消息数据
图5.2MPI_RECV语句的消息信封和消息数据
在消息信封中除了源/目外,为什么还有tag标识呢?
这是因为,当发送者发送两个相同类型的数据给同一个接收者时,如果没有消息标识,接收者将无法区别这两个消息。
一个接收操作对消息的选择是由消息的信封管理的。
如果消息的信封与接收操作所指定的值source,tag和comm相匹配,那么这个接收操作能接收这个消息。
接收者可以给source指定一个任意值MPI_ANY_SOURCE,标识任何进程发送的消息都可以接收,即本接收操作可以匹配任何进程发送的消息,但其它的要求还必须满足,比如tag的匹配;如果给tag一个任意值MPI_ANY_TAG,则任何tag都是可接收的。
在某种程度上,类似于统配符的概念。
MPI_ANY_SOURCE和MPI_ANY_TAG可以同时使用或分别单独使用。
但是不能给comm指定任意值。
如果一个消息被发送到接收进程,接收进程有匹配的通信域,有匹配的source(或其source=MPI_ANY_SOURCE),有匹配的tag(或其tag=MPI_ANY_TAG),那么这个消息能被这个接收操作接收。
5.2MPI的原始数据类型
MPI系统中数据的发送与接收操作都必须指定数据类型。
数据类型可以是MPI系统预定义的,称为原始数据类型,也可以是用户在原始数据类型的基础上自己定义的数据类型。
在此只介绍原始数据类型,自定义数据类型请参看参考文献2。
MPI为C预定义的原始数据类型在表1中给出。
除表中列出的外,某些MPI系统可能支持更多的原始数据类型,如MPI_INTEGER2,MPI_LONG_LONG_INT,等等。
表5.2MPI原始数据类型
MPI数据类型
C类型
MPI_INT
MPI_FLOAT
MPI_DOUBLE
MPI_SHORT
MPI_LONG
MPI_CHAR
MPI_UNSIGNED_CHAR
MPI_UNSIGNED_SHORT
MPI_UNSIGNED
MPI_UNSIGNED_LONG
MPI_LONG_DOUBLE
MPI_BYTE
MPI_PACKED
int
float
double
short
long
char
unsignedchar
unsignedshort
unsigned
unsignedlong
longdouble
unsignedchar
无
MPI系统的原始数据类型只适合于收发一组在内存中连续存放的数据。
当要收发的数据在内存中不连续,或由不同数据类型构成时,则需要将数据打包或者使用自定义的数据类型。
自定义数据类型用于描述要发送或接收的数据在内存中的确切分布。
数据类型是MPI的一个重要特征,它的使用可有效地减少消息传递的次数,增大通信粒度,并且,与数据打包相比,在收/发消息时可以避免或减少数据在内存中的拷贝、复制。
关于MPI自定义数据类型的详细使用,限于篇幅,在此不作详细说明,有兴趣的读者请参看参考文献文献[2]。
5.3MPI程序的基本结构
一个MPI程序的各个进程通过调用MPI函数进行通信,协同完成一项计算任务。
在MPI的C语言接口中,所有函数名均采用MPI_Xxxxx的形式,如MPI_Send,MPI_Type_commit等等,它们以MPI_开始,以便与其他函数名相区别,前缀MPI_之后的第一个字母大写,其余字母一律小写。
MPI的C语言接口函数通常返回一个整数值表示操作成功与否,返回值为MPI_SUCCESS(0)表示操作成功,否则表示操作的错误码。
MPI接口中除了函数和SUBROUTINE外,还定义了一组常量及C变量类型,它们的命名规则为:
所有常量的名称全部大写,如MPI_COMM_WORLD,MPI_INT等;而C变量类型的命名则与C函数一样,如MPI_Datatype,MPI_Status等。
MPI并行程序和串行程序没有很大的差别,它们通过对MPI函数的调用来实现特定的并行算法。
一个MPI并行程序主要由三个部分组成:
1进入并行环境:
调用MPI_Init来启动并行计算环境,它包括在指定的计算结点上启动构成并行程序的所有进程以及构建初始的MPI通信环境和通信器MPI_COMM_WORLD、MPI_COMM_SELF。
2主体并行任务:
这是并行程序的实质部分,所有需要并行来完成的任务都在这里进行。
在这个部分中,实现并行算法在并行计算机上的执行过程。
3退出并行环境:
调用MPI_Finalize退出并行环境。
一般说来,退出并行计算环境后程序的运行亦马上结束。
下面是C语言MPI程序的典型结构
用C语言编写的MPI程序中每个源文件必须包含MPI的C语言头文件mpi.h,以便得到MPI函数的原型说明及MPI预定义的常量和类型。
注意源文件中包含头文件“mpi.h”时不要含路径,必要时可在编译时通过“-I”选项指定mpi.h所在的路径,以方便程序在不同MPI系统间的移植。
MPI_Init函数用于初始化MPI系统。
在调用其他MPI函数前(除MPI_Initialized外)必须先调用该函数。
在许多MPI系统中,第一个进程通过MPI_Init来启动其他进程。
注意要将命令行参数的地址(指针)&argc和&argv传递给MPI_Init,因为MPI程序启动时一些初始参数是通过命令行传递给进程的,这些参数被添加在命令行参数表中,MPI_Init通过它们得到MPI程序运行的相关信息,如需要启动的进程数、使用那些结点、以及进程间的通信端口等,返回时会将这些附加参数从参数表中去掉。
因此一个MPI程序如果需要处理命令行参数,最好在调用MPI_Init之后再进行处理,这样可以避免遇到MPI系统附加的额外参数。
函数MPI_Comm_size与MPI_Comm_rank分别返回指定通信器(这里是MPI_COMM_WORLD,它包含了所有进程)中进程的数目以及本进程的进程号。
MPI_Finalize函数用于退出MPI系统。
调用MPI_Finalize之后不能再调用任何其他MPI函数。
各MPI函数的详细使用方法请参看下一节的例子程序及附录3,在此先不作详细说明。
5.4MPI通信简介
5.4.1点到点通信和通信模式
MPI最基本的通信模式是在一对进程之间进行的消息收发操作:
一个进程发送消息,另一个进程接收消息。
这种通信方式称为点对点通信(pointtopointcommunications)。
MPI提供两大类型的点对点通信函数。
第一种类型称为阻塞型(blocking),第二种类型称为非阻塞型(nonblocking)。
阻塞型函数需要等待指定操作的实际完成,或至少所涉及的数据已被MPI系统安全地备份后才返回。
如MPI_Send和MPI_Recv都是阻塞型的。
MPI_Send函数返回时表明数据已经发出或已被MPI系统复制,随后对发送缓冲区的修改不会影响所发送的数据。
而MPI_Recv返回时,则表明数据已经接收到并且可以立即使用。
阻塞型函数的操作是非局部的,它的完成可能需要与其他进程进行通信。
非阻塞型函数调用总是立即返回,而实际操作则由MPI系统在后台进行。
非阻塞型函数名MPI_前缀之后的第一个字母为“I”,最常用的非阻塞型点对点通信函数包括MPI_Isend和MPI_Irecv。
在调用了一个非阻塞型通信函数后,用户必须随后调用其他函数,如MPI_Wait或MPI_Test等,来等待操作完成或查询操作的完成情况。
在操作完成之前对相关数据区的操作是不安全的,因为随时可能与正在后台进行的通信发生冲突。
非阻塞型函数调用是局部的,因为它的返回不需要与其他进程进行通信。
在有些并行系统上,通过非阻塞型函数的使用可以实现计算与通信的重叠进行。
此外,对于点对点消息发送,MPI提供四种发送模式,这四种发送模式的相应函数具有一样的调用参数,但它们发送消息的方式或对接收方的状态要求不同。
标准模式(standardmode)由MPI系统来决定是先将消息拷贝至一个缓冲区然后立即返回(此时消息的发送由MPI系统在后台进行),还是等待将数据发送出去后再返回。
大部分MPI系统预留了一定大小的缓冲区,当发送的消息长度小于缓冲区大小时会将消息拷贝到缓冲区然后立即返回,否则则当部分或全部消息发送完成后才返回。
标准模式发送操作是非局部的,因为它的完成需要与接收方联络。
标准模式阻塞型发送函数是MPI_Send。
缓冲模式(bufferedmode)MPI系统将消息拷贝至一个用户提供的缓冲区然后立即返回,消息的发送由MPI系统在后台进行。
用户必须确保所提供的缓冲区足以容下采用缓冲模式发送的消息。
当消息大小超过缓冲区容量时,应用程序会收到错误汇报。
缓冲模式发送操作是局部的,因为函数不需要与接收方联络即可立即完成(返回)。
缓冲模式阻塞型发送函数为MPI_Bsend。
同步模式(synchronousmode)在标准模式的基础上要求确认接收方已经开始接收数据后函数调用才返回。
即发送动作的结束不仅意味着发送缓冲区已经可以用于其它用途,而且还表示接收端也执行了一定程序的接收工作。
显然,同步模式的发送是非局部的。
同步模式阻塞型发送函数为MPI_Ssend。
就绪模式(readymode)调用就绪模式发送时必须确保接收方已经处于就绪状态(正在等待接收该消息),否则将产生一个错误。
该模式设立的目的是在一些以同步方式工作的并行系统上由于发送时可以假设接收方已经准备好接收而减少一些握手开销。
如果一个使用就绪模式的MPI程序是正确的,则将其中所有就绪模式的消息发送改为标准模式后也应该是正确的。
就绪模式阻塞型发送函数为MPI_Rsend。
表5.3MPI点对点通信类型及模式汇总
函数类型
通信模式
阻塞型
非阻塞型
消息发送函数
标准模式
MPI_Send
MPI_Isend
缓冲模式
MPI_Bsend
MPI_Ibsend
同步模式
MPI_Ssend
MPI_Issend
就绪模式
MPI_Rsend
MPI_Irsend
消息接收函数
MPI_Recv
MPI_Irecv
消息检测函数
MPI_Probe
MPI_Iprobe
等待通信完成或查询完成情况
MPI_Wait
MPI_Test
MPI_Waitall
MPI_Testall
MPI_Waitany
MPI_Testany
MPI_Waitsome
MPI_Testsome
释放通信请求
MPI_Request_free
取消通信
MPI_Cancel
MPI_Test_cancelled
具体函数的使用请参看附录3。
各通信模式的详细分析和举例限于篇幅在此不作详细说明,有兴趣的读者请参看参考文献[2]。
5.4.2聚合通信与同步
聚合通信指在一个通信器的所有进程间同时进行的通信。
聚合通信总是在一个通信器中的所有进程间进行,调用一个聚合通信函数时,通信器中的所有进程必须同时调用同一函数,共同参与操作。
聚合通信包括障碍同步(MPI_Barrier)、广播(MPI_Bcast)、数据收集(MPI_Gather)、数据散发(MPI_Scatter)、数据转置(MPI_Alltoall)和归约(MPI_Reduce)。
1.障碍同步
障碍同步函数MPI_Barrier用于一个通信器中所有进程的同步。
调用该函数时进程将处于等待状态,直到通信器中所有进程都调用了该函数后才继续执行。
2.广播
指一个进程(称为根进程)同时发送同样的消息给通信器中的所有其他进程。
MPI的广播函数是MPI_Bcast。
3.数据收集
数据收集操作指一个进程,称为根进程,从指定通信器中的所有进程,包括根进程自己,收集数据。
MPI的基本数据收集函数为MPI_Gather,它从每个进程收集相同长度的数据。
如果从各个进程收集的数据长度不同,则应该调用函数MPI_Gatherv。
函数MPI_Allgather用于在通信器中的所有进程中同时进行数据收集,它的作用相当于先用MPI_Gather将数据收集到一个进程中,紧接着用MPI_Bcast将收集到的数据广播给其他进程。
类似地,MPI_Allgatherv用于收集不同长度的数据到通信器中的所有进程中。
4.数据散发
数据散发函数MPI_Scatter正好是数据收集函数MPI_Gather的逆向操作,它将一个进程中的数据按块散发给通信器中的所有进程,散发给每个进程的数据块长度相同。
函数MPI_Scatterv用于散发不同长度的数据块。
5.数据转置
函数MPI_Alltoall用于同时进行收集和散发操作:
通信器中所有进程从其他进程收集数据,同时将自己的数据散发给其他进程。
它的作用相当于将一个分布式存储的数据场在处理机间进行一次转置。
函数MPI_Alltoall要求参与操作的所有数据块长度一样,如果数据块长度不同,则应该调用MPI_Alltoallv函数。
6.归约
归约运算是指在分布在不同进程的数据间进行指定的运算,常用的运算有求和、求最大或最小值等。
MPI的归约函数中可以使用预定义的运算(如MPI_SUM,MPI_MAX等,参看附录3),也可以使用用户自行定义的运算(参看MPI_Op_create)。
MPI用于归约操作的
基本函数是MPI_Reduce,它在指定的进程(称为根进程)中返回归约运算结果。
如果希望所有进程都得到归约运算的结果,则可使用函数MPI_Allreduce。
此外,MPI还提供一个函数MPI_Scan,称为前缀归约或扫描归约,用于计算数据的部分和。
表5.4MPI点对点通信类型及模式汇总
类型
函数名
含义
同步
MPI_Barrier
路障同步
广播
MPI_Bcast
一对多广播同样的消息
数据收集
MPI_Gather
多对一收集各个进程的消息
MPI_Gatherv
MPI_Gather的一般化
MPI_Allgather
全局收集
MPI_Allgatherv
MPI_Allgather的一般化
数据散发
MPI_Scatter
一对多散播不同的消息
MPI_Svcatte