高性能计算-MPI并行程序开发实战-二

高性能计算-MPI并行程序开发实战(二)

在MPI-1中,共有128个调用接口,在MPI-2中有287个,MPI是比较庞大的,从理论上说,MPI所有的通信功能可以用它的6个基本的调用实现。6个基本调用实现可以实现所有的消息传递并行程序的功能。因此,本次博客内容介绍这6个基本调用。

MPI调用的参数说明

MPICALL_FORMAT_DESC
如图所示,对于有参数的MPI调用,MPI首先给出一种独立于具体语言的说明,对各个参数的性质进行介绍,并给出对于C的原型说明,在MPI-2中,还给出了C++形式的说明。MPI对参数说明的方式有三种:分别是IN、OUT、INOUT。含义分别是:

  • IN(输入):调用部分传递给MPI的参数,MPI除了使用该参数外不允许对这一参数做任何修改
  • OUT(输出):MPI返回给调用部分的结果参数,该参数的初始值对MPI没有任何意义
  • INOUT(输入输出):调用部分首先将该参数传递给MPI,MPI对这一参数引用、修改后,将结果返回给外部调用,该参数的初始值和返回结果都有意义。

如果某一个参数在调用前后没有改变,比如某个隐含对象的句柄,但是该句柄指向的对象被修改了,这一参数仍然被说明为OUT或INOUT。MPI的定义在最大范围内避免INOUT参数的使用,因为这些使用易于出错,特别是对标量参数。
还有一种情况是MPI函数的一个参数被一些并行执行的进程用作IN,而被另一些同时执行的进程用作OUT,虽然在语义上它不是同一个调用的输入和输出,但这样的参数语法上也记为INOUT。
当一个MPI参数仅对一些并行执行的进程有意义而对其它的进程没有意义时,不关心该参数取值的进程可以将任意的值传递给该参数。
在MPI中OUT或INOUT类型的参数不能被其他的参数作为别名使用,如下定义一个C语言方法:

void copyIntBuffer(int *pin, int *pout, int len) {
    int i;
    for(i=0; i<len; ++i) {
        *pout++=*pin++;
    }
}

那么在下面代码段中,对这个函数的调用使用了参数别名。

int a[10];
copyIntBuffer(a, a+3, 7);

虽然C语言中允许这种方法,但是除非特别说明,MPI禁止此调用方法。
在MPI调用说明中,以MPI_INIT为例:

MPI_INIT():独立于语言的说明,对这个调用没有参数说明。
int MPI_Init(int argc, char ** argv):对于C中方法调用,需要给出参数argc和argv,此处给出的即为argc和argv。

在C语言的说明中,对void 需要进行特殊说明。MPI调用允许不同的数据类型使用相同的调用,如对于数据的发送操作、整型、实型、字符型等都用一个相同的调用MPI_SEND,对于这样的数据类型在C语言原型说明中,可使用void 表示。用户可根据通信的要求,对不同的数据类型,可以使用相同的调用。与面向对象中的多态具有相似之处。

MPI初始化

MPI调用接口1 MPI_INIT

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

MPI_INIT是MPI程序的第一个调用,它完成MPI程序所有的初始化工作,所有MPI程序的第一条可执行语句都是初始化调用接口

MPI结束

MPI调用接口2 MPI_FINALIZE

MPI_FINALIZE()
int MPI_Finalize(void)

MPI_FINALIZE是MPI程序的最后一个调用,它结束MPI程序的运行,它是MPI程序的最后一条可执行语句,否则程序的运行结果不可预知,可能会造成意外的异常。

当前进程标识

MPI调用接口3 MPI_COMM_RANK

MPI_COMM_RANK(comm, rank)
IN  comm    // 该进程所在的通信域(句柄)
OUT rank    // 调用进程在comm中的标识号
int MPI_Comm_rank(MPI_Comm comm, int * rank)

MPI_COMM_RANK调用返回调用进程在给定通信域中的进程标识号,有了标识号,不同的进程就可以将自身和其他的进程进行区分,实现各个进程的并行和协作。

通信域包含的进程数

MPI调用接口4 MPI_COMM_SIZE

MPI_COMM_SIZE(comm, size)
IN  comm    //通信域(句柄)
OUT size    //通信域comm内包括的进程数(整数)
int MPI_Comm_size(MPI_Comm comm, int * size)

MPI_COMM_SIZE调用返回给定的通信域中所包括的进程的个数,不同的进程通过这一调用得知在给定的通信域中一共有多少个进程在并行执行。

消息发送

MPI调用接口5 MPI_SEND

MPI_SEND(buf, count, datatype, dest, tag, comm)
IN  buf         //发送缓冲区的起始地址(可选类型)
IN  count       //将要发送的数据的个数(非负整数)
IN  datatype    //将要发送的数据的数据类型(句柄)
IN  dest        //目的进程标识号(整型)
IN  tag         //消息标志(整型)
IN  comm        //当前使用的通信域(句柄)
int MPI_Send(void* buff, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)

MPI_SEND调用将发送缓冲区中的count个datatype类型的数据发送到目的进程,目的进程在通信域中的标识号是dest,本次发送的消息标志是tag,使用这个标志,可以把本次发送的消息和本进程向同一目的进程发送的其他消息区别开。
MPI_SEND操作指定的发送缓冲区是由count个类型为datatype的连续数据空间组成,起始地址为buf,这里不以字节计数,而是以数据类型为单位指定消息的长度计数,这样即可独立于具体的实现,并且更接近用户的观点。
其中datatype数据类型即可以是MPI中既有的预定义类型,也可以是用户自定义数据类型。通过使用不同的数据类型调用MPI_SEND,可以发送不同类型的数据。

消息接收

MPI调用接口6 MPI_RECV

MPI_RECV(buf, count, datatype, source, tag, comm, status)
OUT     buf         //接收缓冲区的起始地址
IN      count       //最多可接收的数据的个数(整型)
IN      datatype    //接收数据的数据类型(句柄)
IN      source      //接收数据的来源即发送数据的进程的进程标识号(整型)
IN      tag         //消息标识,与相应的发送操作的表示相匹配相同(整型)
IN      comm        //本进程和发送进程所在的通信域(句柄)
OUT     status      //返回状态(状态类型)
int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status* status)

MPI_RECV从指定的进程source接收消息,并且该消息的数据类型和消息标识和本接收进程指定的datatype和tag相一致,接收到的消息所包含的数据元素的个数最多不能超过count。
接收缓冲区是由count个类型为datatype的连续元素空间组成,由datatype指定其类型,起始地址为buf。接收到消息的长度必须小于或等于接收缓冲区的长度,这是因为如果接收到的数据过大,MPI没有截断,接收缓冲区会发生溢出错误,因此开发者要保证接收缓冲区的长度不小于发送数据的长度。如果一个短于接收缓冲区的消息到达,那么只有相应于这个消息的那些地址被修改。count可以是零,这种情况下消息的数据部分是空的。
其中datatype数据类型可以是MPI的预定义类型,也可以是用户自定义的类型。通过指定不同的数据类型调用MPI_RECV,可以接收不同类型的数据。

返回状态status

返回状态变量status用途很广,它是MPI定义的一个数据类型,使用之前需要用户为它分配空间。
在C语言的实现中,状态变量是由至少三个域组成的结构类型,这三个域分别是:MPI_SOURCE,MPI_TAG和MPI_ERROR。它还可以包括其他的附加域。这样通过对status.MPI_SOURCE,status.MPI_TAG和status.MPI_ERROR的引用,就可以得到返回状态中所包含的发送数据进程的标识,发送数据使用的tag标识和本接收操作返回的错误代码。
除了以上三个信息之外,对status变量执行MPI_GET_COUNT调用可以得到接收到的消息的长度信息。

一个简单的发送和接收的例子

下面介绍一个简单的同时包含发送和接收调用的例子,其中一个进程(进程0)向另一个进程(进程1)发送一条消息,该消息是字符串『Hello Process 1!』,进程1在接收到该消息后,将这一条消息打印输出。

#include "mpi.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char * argv[]) {
    char message[20];
    int myrank;
    MPI_Init(&argc, &argv);                     //MPI程序的初始化
    MPI_Status status;
    MPI_Comm_rank(MPI_COMM_WORLD, &myrank);     //得到当前进程的标识
    if (myrank == 0) {              // 进程0
        /**
         * 说明
         * 先将字符串拷贝到发送缓冲区message中,然后调用MPI_Send语句将它发出。
         * 用strlen(message)指定消息的长度,用MPI_CHAR指定消息的数据类型,
         * 1指明发往进程1,使用的消息标识是99,MPI_COMM_WORLD是包含本进程(进程0)
         * 和接收消息的进程(进程1)的通信域。发送方和接收方必须在同一个通信域中。
         * 由通信域来统一协调和控制消息的发送和接收。
         */
         strcpy(message, "Hello Process 1!");
         MPI_Send(message, strlen(message), MPI_CHAR, 1, 99, MPI_COMM_WORLD);
    } else if (myrank == 1) {       // 进程1
        /**
         * 说明
         * 进程1直接执行接收消息的操作,这里它使用message作为接收缓冲区,由此可见,
         * 对于同一个变量,在发送进程和接收进程中的作用是不同的。
         * 它指定接收消息的最大长度为20,消息的数据类型为MPI_CHAR字符型
         * 接收的消息来自进程0,而接受消息携带的标识必须为99
         * 使用的通信域也是MPI_COMM_WORLD,接收完成后的各种状态信息存放在status中
         * 接收完成后,它直接将接收到的字符串输出。
         */
        MPI_Recv(message, 20, MPI_CHAR, 0, 99, MPI_COMM_WORLD, &status);
        printf("received: %s:", message);
    }
    MPI_Finalize();             //MPI程序结束
    return EXIT_SUCCESS;
}