MPI编程指南

MPI编程指南

一、     MPI概述

1.1  MPI的发展史

MPI标准化涉及到大约60个国家的人们,他们主要来自于美国和欧洲的40个组织,这包括并行计算机的多数主要生产商,还有来自大学、政府实验室和工厂的研究者们。1992年4月,并行计算研究中心在Williamsburg,Virginia,召开了一个关于消息传递的标准的工作会议,会议上讨论了标准消息传递的必要的、基本的特点,并建立了工作组继续进行标准化工作。

1992年10月,MPI的初步草稿MPI1形成,MPI1主要包含的是在Williamsburg工作组会议上讨论的基本消息传递的接口,因为它的基本目的就是促进讨论并继续此项工作,所以它主要集中在点对点的通信。1993年1月,第一届MPI会议在Dallas举行,1993年2月MPI1修定版本公布;之后定期召开了一系列关于MPI的核心的研讨会;1993年11月MPI的草稿和概述分别发表于Supercomputing'93和the proceedings。直到1994年5月,MPI标准正式发布。1994年7月发布了MPI标准的勘误表。

现在该工作组正在着手扩展MPI、完善MPI,从事于MPI2(MPI的扩展标准)的制定工作。

1.2  MPI的优点

●可移植性和易于使用。以低级消息传递程序为基础的较高级和(或)抽象程序所构成的分布存储通信环境中,标准化的效益特别明显。

●MPI是被正式的详细说明的:它已经成为一个标准。消息传递标准的定义能提供给生产商清晰定义的程序库,以便他们能有效地实现这些库或在某些情况下为库程序提供硬件支持,因此加强了可扩展性。

●MPI有完备的异步通信:使得send,recieve能与计算重叠。

●可以很有效的在MPP上或Cluster上用MPI编程:MPI的虚拟拓扑反映了应用程序所申请的一组结点的通信模式,在MPP上实现的MPI便可以利用这种信息来优化处理器间的通信路径。

1.3  主要内容

    ●点对点通信(point_to_point communication)

    ●群体操作(collective operations)

    ●进程组(process groups)

    ●通信上下文(communication contexts)

    ●进程拓扑结构(process topologies)

    ●与Fortran 77和C语言的邦定(bindings for Fortran 77 and C)

    ●环境的管理与查询(environmental management and inquiry)

    ●轮廓管理(profiling interface)

1.4  MPI的各种实现

在国外有许多自从MPI标准制定以来在各种机器上的MPI的portable的实现,并且他们仍从事于自己的MPI实现的性能改进,其中最著名的一些见表1。

表1 国外MPI的各种实现

名称

单 位

网 址

 CHIMP

U. of Edinburgh

ftp.epcc.ed.ac.uk/pub/chimp/.

 LAM

Ohio state

tbag.osc.edu/pub/lam/

 MPICH

Argonne_Mississippi state

info.mcs.anl.gov/pub/mpi

 Unify

Mississippi state

ftp.erc.msstate.edu/unify

二、    MPI入门介绍

以下均以MPICH为例。

2.1 MPI的编程方式

       对于基本的应用,MPI同其它消息传送系统一样易于使用。下面是一个简单的基于C语言的MPI样本代码。它是SPMD方式的,即每个进程都执行该程序,通过返回的进程编号来区分不同的进程。该程序完成进程0向进程1传递数据buf。

/* first.c */

#include "mpi.h"     /*MPI的头函数,提供基本的MPI定义和类型*/

#include <stdio.h>

int main( argc, argv )

int argc;

char **argv;

{

int rank, size, tag=333;

int buf[20]

MPI_Status status

MPI_Init( &argc, &argv ); /*MPI的初始化函数*/

MPI_Comm_rank( MPI_COMM_WORLD, &rank );   /*该进程的编号*/

MPI_Comm_size( MPI_COMM_WORLD, &size );     /*总的进程数目*/

if (rank==0)

MPI_Send( buf, 20, MPI_Int, 1, tag, MPI_COMM_WORLD);       /*发送buf到进程1*/

if (rank==0)

     MPI_Recv( buf, 20, MPI_Int, 0, tag, MPI_COMM_WORLD, &status);  /*从进程0接收buf*/

MPI_Finalize();       /*MPI的结束函数*/

return 0;

}

2.2 MPI的基本语句介绍

1、MPI世界

进程由一个唯一的“标识数”(整数)表示,进程的标识数为数0、1、2、……、N-1。MPI_COMM_WORLD指“MPI应用中的所有进程”,它称为通信子,并且提供消息传递所需的所有信息。可移植库对通信子做更多的工作,以提供大多数其它系统所不能处理的同步保护。

2、进入和退出MPI

与其它系统一起,提供两个函数来初始化和结束MPI进程:

int MPI_Init(int *argc, char ***argv)   /*初始化*/

int MPI_Finalize(void) /*结束*/

3、我是谁?他们是谁?

典型地,并行程序中的进程需要知道它是谁(它的标识数)以及其它进程是怎样存在的。一个进程通过调用MPI_Comm_rank( )来发现它自己的标识数,用MPI_Comm_size( )来返回进程总数:

int MPI_Group_size(MPI_Group group, int *size)

int MPI_Group_rank(MPI_Group group, int *rank)

4、发送消息

消息是一组给定数据类型的元素。消息被发给一个指定的进程,而且用一个由用户说明的标识来标记。标识用来区分不同的由一个进程所能发送/接受的消息类型。

int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)

5、接受消息

接受进程说明标识和发送进程的标识数。MPI_ANY_TAG和MPI_ANY_SOURCE可选地用于接受任意标识和从任意发送进程而来的消息。

int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)

有关所接受消息的信息在一个状态变量status中返回。

通过这些函数,你会准备编写任意的应用。在MPI中有许多其它、更特异的函数,但是迄今为止所有这些函数可以在这里提到的函数上构造。

2.3 MPI的环境设置

       在并行机上的环境一般是设置好的,不需要变动。而在NOW上运行MPI程序需要设置一些配置文件:

1、 由于加载程序到结点上运行调用了Unix系统的rsh命令,所以需要在每个结点上设置 .rhosts文件,以使rsh能正确执行;

2、 由于NOW环境的异构性,需要在启动时指定运行结点的体系结构;若未指定,是指使用与启动并行程序的结点具有相同体系结构的结点。在启动并行程序的机器里,具有相同体系结构的几台机器的名字存放在一个名为$MPICH/util/machines/machines.<arch>的文件中,一台机器的名字占有文件的一行,其中$MPICH是一个环境变量,指明MPICH软件安装后所在的目录。并行程序加载运行时是按照文件中机器名字的先后顺序依次加载的。

2.4 MPI的编译和运行

       对于简单的程序,可以使用专门的编译命令。对于大的项目,最好使用标准的Makefile。MPICH提供的编译命令有mpicc和mpif77,它们分别是C和Fortran的编译命令:

mpicc -o first first.c

mpif77 -o first firstf.f

对于编译得到的目标程序,运行的命令为:

mpirun –arch xxx –np yyy first

其中xxx为machines.<arch>的<arch>,yyy为申请的进程数目。

2.5  例子

1、  环的C语言实现

#include "mpi.h"

#include <stdio.h>

#define T_SIZE  2000

int main(argc,argv)

int argc;

char *argv[];

{

int ierr, prev, next, tag, rank, size;

MPI_Status status;

double send_buf[T_SIZE], recv_buf[T_SIZE];

MPI_Init(&argc,&argv);

MPI_Comm_rank( MPI_COMM_WORLD, &rank);

MPI_Comm_size( MPI_COMM_WORLD, &size);

next = rank + 1;

if (next > size) next = 0;

prev = rank – 1;

if (prev < 0) prev = size – 1;

if (rank == 0) {

   MPI_Send(send_buf, T_SIZE, MPI_DOUBLE, next, tag, MPI_COMM_WORLD);

   MPI_Recv(recv_buf, T_SIZE, MPI_DOUBLE, prev, tag+1, MPI_COMM_WORLD, status);

  }  else  {

   MPI_Recv(recv_buf, T_SIZE, MPI_DOUBLE, prev, tag, MPI_COMM_WORLD, status);

   MPI_Send(recv_buf, T_SIZE, MPI_DOUBLE, next, tag+1, MPI_COMM_WORLD);

  }

MPI_Finalize();

}

2、  环的Fortran语言实现

      program ring

      include 'mpif.h'

      integer ierr

      call MPI_INIT(ierr)

      call test_ring

      call MPI_FINALIZE(ierr)

      end

      subroutine test_ring

      include 'mpif.h'

      integer T_SIZE

      parameter (T_SIZE=2000)

      integer ierr, prev, next, tag, rank, size, status(MPI_STATUS_SIZE)

      real send_buf( T_SIZE ), recv_buf ( T_SIZE )

      call MPI_COMM_RANK( MPI_COMM_WORLD, rank, ierr )

      call MPI_COMM_SIZE( MPI_COMM_WORLD, size, ierr )

      next = rank + 1

      if (next .ge. size) next = 0

      prev = rank - 1

      if (prev .lt. 0) prev = size - 1

      if (rank .eq. 0) then

        call MPI_SEND(send_buf, T_SIZE, MPI_REAL, next, tag, MPI_COMM_WORLD, ierr)

        call MPI_RECV(recv_buf, T_SIZE, MPI_REAL, prev, tag+1, MPI_COMM_WORLD, status, ierr)

      else

        call MPI_RECV(recv_buf, T_SIZE, MPI_REAL, prev, tag, MPI_COMM_WORLD, status, ierr)

        call MPI_SEND(recv_buf, T_SIZE, MPI_REAL, next, tag+1, MPI_COMM_WORLD, ierr)

      end if

3、  PI的C语言实现

#include "mpi.h"

#include <stdio.h>

#include <math.h>

double f(a)

double a;

{

return (4.0 / (1.0 + a*a));

}

int main(argc,argv)

int argc;

char *argv[];

{

  int done = 0, n=100, myid, numprocs, i;

  double PI25DT = 3.141592653589793238462643;

  double mypi, pi, h, sum, x, a,startwtime, endwtime;

  MPI_Init(&argc,&argv);

  MPI_Comm_size(MPI_COMM_WORLD,&numprocs);

  MPI_Comm_rank(MPI_COMM_WORLD,&myid);

  if (myid == 0)    startwtime = MPI_Wtime();

  h   = 1.0 / (double) n;

  sum = 0.0;

  for (i = myid + 1; i <= n; i += numprocs)

  {  x = h * ((double)i - 0.5);

  sum += f(x);  }

  mypi = h * sum;

  MPI_Reduce(&mypi, &pi, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);

  if (myid == 0)          {

     printf("pi is %.16f, Error is %.16f\n", pi, fabs(pi - PI25DT));

     endwtime = MPI_Wtime();

     printf("wall clock time = %f\n",    endwtime-startwtime);

  }

  MPI_Finalize();

}

4、  计算PI的Fortran语言实现

      program main

      include 'mpif.h'

      double precision  PI25DT

      parameter  (PI25DT = 3.141592653589793238462643d0)

      double precision  mypi, pi, h, sum, x, f, a

      integer n, myid, numprocs, i, rc

      f(a) = 4.d0 / (1.d0 + a*a)

      call MPI_INIT( ierr )

      call MPI_COMM_RANK( MPI_COMM_WORLD, myid, ierr )

      call MPI_COMM_SIZE( MPI_COMM_WORLD, numprocs, ierr )

      n=100

      h = 1.0d0/n

      sum  = 0.0d0

      do 20 i = myid+1, n, numprocs

         x = h * (double(i) - 0.5d0)

         sum = sum + f(x)

 20   continue

      mypi = h * sum

      call MPI_REDUCE(mypi,pi,1,MPI_DOUBLE_PRECISION,MPI_SUM,0, MPI_COMM_WORLD, ierr)

      if (myid .eq. 0) then  write(6, 97) pi, abs(pi - PI25DT)

 97      format('  pi is : ', F18.16, ' Error is: ', F18.16)

      endif

 30   call MPI_FINALIZE(ierr)

      end

三、     MPI中级介绍

3.1  基本概念

  1. 消息包

在消息传递过程中,发送和接收的消息除了数据部分外, 消息还带有用于识别消息和选择接收消息的信息。这个信息是由确定的域组成的,我们称为信封,这些域有:source(源)、destination(目的)、tag(标识)、communicator(通信子)。

消息的source隐含地由消息发送者的标识来决定。其他域是由发送操作的参数决定。整型值的消息tag能被程序用于识别不同的消息,有效标识值的范围依赖于实现。

  1. 组和通信子

一个组(group)是进程的一个有序集。组内的每个进程与一个整数rank相联系。序列号是连续的并从0开始。组用模糊的组对象来描述,因此不能直接从一个进程到另一个进程传送。可在一个通信子中使用组来描述通信空间中的参与者并对这些参与者进行分级(rank)。

上下文是通信子所具有的一个特性,它允许对通信空间进行划分。一个上下文所发送的消息不能被另一个上下文所接收。

一个通信子(communicator)指定一个通信操作的上下文。每个通信上下文提供一个单独的“通信全域”;消息总是在一个的上下文内被发送和接收,不同的上下文发送的消息互不干涉。通信子也指定共享这个通信上下文的进程组。这个进程组被编号,并且进程是由这个组中的进程号标识。

一个预定义的通信子MPI_COMM_WORLD是由MPI提供。MPI初始化后, 它允许和可存取的全部进程通信, 进程是由他们在MPI_COMM_WORLD组中的进程号所标识。

  1. 基本数据类型

异构计算要求包含消息的数据是有类型的或者已描述的,使得它的机器的表达可以在计算机体系结构之间转换。MPI系统地描述了消息的数据类型,从简单的原始机器类型到复杂的结构、数组和下标。

与C语言邦定的基本数据类型:

MPI_CHAR

Signed char

MPI_SHORT

Signed short int

MPI_INT

Signed int

MPI_LONG

Signed long int

MPI_UNSIGNED_CHAR

Unsigned char

MPI_UNSIGNED_SHORT

Unsigned short int

MPI_UNSIGNED

Unsigned int

MPI_UNSIGNED_LONG

Unsigned long int

MPI_FLOAT

Float

MPI_DOUBLE

Double

MPI_LONG_DOUBLE

Long double

MPI_BYTE

MPI_PACKED

与Fortran语言邦定的基本数据类型:

MPI_INTEGER

INTEGER

MPI_REAL

REAL

MPI_DOUBLE_PRECISION

DOUBLE PRECISION

MPI_COMPLEX

COMPLEX

MPI_DOUBLE_COMPLEX

DOUBLE COMPLEX

MPI_LOGICAL

LOGICAL

MPI_CHARACTER

CHARCTER(1)

MPI_BYTE

MPI_PACKED

3.2  点对点通信

点对点通信是MPI中比较复杂的一部分,它有两种消息传递的机制:阻塞的和非阻塞的。对于阻塞的方式,它必须等到消息从本地送出之后,才可以执行后续的语句,保证了消息缓冲区等资源的可再用性;而非阻塞的方式不须等到消息从本地送出,就可以执行后续的语句,从而允许通信和计算的重叠,利用合适的硬件可使得计算和数据通信同时执行,但是非阻塞调用的返回并不保证资源的可再用性。

点对点通讯的发送和接收语句必须是匹配的,为了区分不同进程或同一进程发送来的不同消息,可以采用通讯体和标志位(tag)来实现相应的语句的匹配,也可以接收不确定source和tag的消息,例如MPI_ANY_SOURCE, MPI_ANY_TAG。

1、  阻塞通信

对于阻塞的标准通信,MPI_Send和MPI_Recv函数的格式为

int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag,

            MPI_Comm comm)

int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag,

            MPI_Comm comm, MPI_Status *status)

发送的含义是将包含count个datatype类型的首地址为buf的消息发送到dest进程,该消息的是与标志tag和通信体comm封装在一块的;接收的含义是将source进程接收标志为tag和通信体为comm的消息,将该消息写入首地址为buf 的缓冲区,返回值status中包含大小、标志、源进程等信息。

在C语言中, status是一个结构, 其包含两个域, 名字为 MPI_SOURCE和MPI_TAG, 该结构可以包含附加域。在Fortran中,status是一个MPI_STATUS_SIZE大小的INTEGER型数组,status(MPI_SOURCE)和status(MPI_TAG)分别包含被接收消息的源和标识。

2、  非阻塞通信

对于非阻塞的标准通信,MPI_Isend和MPI_Irecv函数的格式为

int MPI_Isend(void* buf, int count, MPI_Datatype datatype, int dest, int tag,

            MPI_Comm comm, MPI_Request *request)

int MPI_Irecv(void* buf, int count, MPI_Datatype datatype, int source, int tag,

            MPI_Comm comm, MPI_Request *request)

对于非阻塞通信,可以用MPI_WAIT,MPI_TEST等来结束非阻塞通信,

    int MPI_Wait(MPI_Request *request, MPI_Status *status)

    int MPI_Test(MPI_Request *request,int *flag, MPI_Status *status)

其中MPI_Test用来检测非阻塞操作提交的任务是否结束并立即返回,MPI_Wait则一直要等到非阻塞操作提交的任务结束才返回。我们可以认为:

阻塞通信=非阻塞通信 + MPI_Wait;

MPI_Wait = while (flag==0) MPI_Test。

3、  消息的检测

MPI_Probe和MPI_Iprobe操作允许在没有实际收到输入消息的情况下对其进行检查。用户于是可以根据这次返回的信息决定如何接收他们(该信息一般用status返回),或可以根据被检查消息的长度分配结束缓冲区大小,或进行条件分支等。

他们的格式如下:

int MPI_Probe(int source, int tag, MPI_Comm comm, MPI_Status *status)

int MPI_Iprobe(int source, int tag, MPI_Comm comm, int *flag,

                   MPI_Status *status)

如果MPI_Iprobe返回flag=true,则状态目标(status)才能够被获取source,tag及消息的长度。而MPI_Probe是一个阻塞的语句,它必须等到相匹配的消息(source,tag)到达才完成。

4、点对点通信的例子

#include <stdio.h>

#include "mpi.h"

int main(argc, argv)

int argc;

char** argv;

{

int locId,data[100], tag=8888;

MPI_Status status;

MPI_Init(&argc, &argv) ;

MPI_Comm_rank(MPI_COMM_WORLD, &locId) ;

if(locId == 0) {

MPI_Request events;

MPI_Isend(data, 100, MPI_INT, 1, tag , MPI_COMM_WORLD, &events) ;

MPI_Wait(&events, &status) ;

}

if(locId == 1) {

MPI_Probe(MPI_ANY_SOURCE, tag, MPI_COMM_WORLD, &status);

if (status.MPI_SOURCE==0)

MPI_Recv(data, 100, MPI_INT, 0, tag, MPI_COMM_WORLD, &status) ;

  }

  MPI_Finalize() ;

}

3.3  群体通信

群体通信意味着一个通信子中的所有进程调用同一例程,所有的群体操作都是阻塞的,它包括如下一些:

●同步(barrier)

●从一个进程到本组内的所有进程的播送(broadcast)(如图 3.3 的(a))

●从本组所有处理收集数据到一个进程(gather)(如图 3.3 的(b))

●从一个进程分散数据到本组内的所有进程(sactter)(如图 3.3 的(b))

●将gather的数据不是送到某一进程,而是要送到所有本组内的进程(allgather) (如图 3.3 的(c))

●组内的多对多的分散/收集(alltoall)(如图 3.3 的(d))

●求和,最大值,最小值及用户定义的函数等的reduce操作

●scan或prefix操作

 

1、同步

int MPI_Barrier(MPI_Comm comm)

它使得调用者阻塞,直到该通信子内所有进程都调用它。

2、广播

int  MPI_Bcast(void *buffer, int count, MPI_Datatype datatype, int root,

              MPI_Comm comm);

所有进程使用同一计数、数据类型、根和通信子。在操作前,根缓冲区包含一个消息。操作后,所有缓冲区包含来自根进程的消息。

3、散播

int  MPI_Scatter(void *sndbuf, int sndcnt, MPI_Datatype sndtype, void *rcvbuf,

           int rcvcnt, MPI_Datatype rcvtype, int root, MPI_Comm comm);

所有进程使用同一计数、数据类型、根和通信子。在操作前,根发送缓冲区包含长度为`sndcnt * N'的消息,这里N是进程数目。操作后,相等地划分消息,并且分散到随后标识数序的所有进程(包括根)。

4、归约

int  MPI_Reduce(void *sndbuf, void *rcvbuf, int count, MPI_Datatype datatype,

MPI_Op op, int root, MPI_Comm comm);

所有进程使用同一计数、数据类型、根和通信子。操作后,根进程在它的接受缓冲区中有所有进程的发送缓冲区的归约结果,包括:MPI_MAX, MPI_MIN, MPI_SUM, MPI_PROD, MPI_LAND, MPI_BAND, MPI_LOR, MPI_BOR, MPI_LXOR, MPI_BXOR,或者是用户定义的归约函数。

5、收集

int  MPI_Gather(void *sndbuf, int sndcnt, MPI_Datatype sndtype, void *rcvbuf,

               int rcvcnt, MPI_Datatype rcvtype, int root, MPI_Comm comm);

所有进程使用同一计数、数据类型、根和通信子。此例程是MPI_Scatter()的相反:操作后,根进程在它的接受缓冲区中包含所有进程的发送缓冲区的连接(包括它自己),所有消息长度为`rcvcnt * N', 这里N是进程数目。按照随后的标识数序收集消息。

6、群体通信的例子

 下面简单的代码段使用了四个基本的集合例程以操纵一个静态的已划分的规则区域(这里是一维)。全域的长度从根进程广播到所有其它进程。初始数据集在进程间分配(分散)。在每一计算步骤之后,确定全局的最大数并由根所使用。根然后收集最终的数据集。

#include <mpi.h>

{

    int        i,myrank,size,root,full_domain_length,sub_domain_length;

    double     global_max,local_max,*full_domain, *sub_domain;

    MPI_Comm_rank(MPI_COMM_WORLD, &myrank);

    MPI_Comm_size(MPI_COMM_WORLD, &size);

    root = 0;

    if (myrank == root)  get_full_domain(&full_domain, &full_domain_length);

    MPI_Bcast(&full_domain_length, 1, MPI_INT, root, MPI_COMM_WORLD);

    sub_domain_length = full_domain_length / size;

    sub_domain = (double *) malloc(sub_domain_length * sizeof(double));

MPI_Scatter(full_domain, sub_domain_length, MPI_DOUBLE, sub_domain,

           sub_domain_length, MPI_DOUBLE, root, MPI_COMM_WORLD);

    compute(sub_domain, sub_domain_length, &local_max);

    MPI_Reduce(&local_max, &global_max, 1, MPI_DOUBLE, MPI_MAX,

                 root, MPI_COMM_WORLD);

MPI_Gather(sub_domain, sub_domain_length, MPI_DOUBLE, full_domain,

           sub_domain_length, MPI_DOUBLE, root, MPI_COMM_WORLD);

}

3.3  环境管理与查询

MPI的环境管理与查询包括:设置正确的参数、执行环境(出错句柄),以及进入MPI和退出MPI等。这些函数对于写正确的、健壮的程序很重要,特别是对于建筑更高层的可移植的消息传递的程序尤其重要。

1、  时钟

MPI定义了一个计时器,因为对并行程序计时在“性能调试”中是很重要的,而已存在的计时器要么是不方便的,要么是没能提供对高分辨率计时器的足够的访问。

double MPI_Wtime(void)

它返回当前的系统的墙壁时钟的时间,是一个浮点的秒数。

double MPI_Wtick()

MPI_Witck返回秒中MPI_WTIME的精度。即,它返回连续时钟滴答的秒数,是一双精度值。例如,如果由硬件实现的时钟作为每毫秒递增的计数器,那么由MPI_WTICK返回的值应为10e-3。

2、  MPI的初始与结束

int MPI_Init(int *argc,char ***argv)

int MPI_Finalize(void)

它是MPI的初始化函数及MPI的结束函数,所有的MPI程序必须含有MPI_Init,并且必须放在所有MPI的其它调用之前,MPI_Finalize清除所有的MPI的申明,用户必须确保所有的通信必须在调用MPI_Finalize之前完成。

猜你喜欢

转载自www.cnblogs.com/feiniao-carrie/p/9506114.html
mpi
今日推荐