MPI_Init

任何MPI程序都应该首先调用该函数。 此函数不必深究,只需在MPI程序开始时调用即可(必须保证程序中第一个调用的MPI函数是这个函数)。

call MPI_INIT() # Fortran 
MPI_Init(&argc, &argv) //C++ & C 

Fortran版本调用时不用加任何参数,而C和C++需要将main函数里的两个参数传进去,因此在写main函数的主程序时,应该加上这两个形参。

int main(int *argc,char* argv[]) 
{ 
    MPI_Init(&argc,&argv); 
}

MPI_Finalize

任何MPI程序结束时,都需要调用该函数。切记Fortran在调用MPI_Finalize的时候,需要加个参数ierr来接收返回的值,否则计算结果可能会出问题甚至编译报错。在Fortran中ierr为integer型变量。 该函数同第一个函数,都不必深究,只需要求格式去写即可。

call MPI_Finalize(ierr) # Fortran
MPI_Finalize() //C++ 

在这可以给出一个典型的MPI程序有如下的基本框架

......
#inlcude<mpi.h>
......
int main(int argc, char* argv[]){
    ......
    /* No MPI call before this */
    MPI_Init(&argc, &argv);
    ......
    MPI_Finalize();
    /* No MPI call after this */
    ......
    return 0
}

MPI_COMM_RANK && MPI_COMM_SIZE

call MPI_COMM_RANK(comm, rank) 
int MPI_Comm_Rank(MPI_Comm comm, int *rank) 
MPI_COMM_SIZE(comm, size) 
int MPI_Comm_Size(MPI_Comm, int *size) 

该函数是获得当前进程的进程标识,如进程0在执行该函数时,可以获得返回值0。可以看出该函数接口有两个参数,前者为进程所在的通信域,后者为返回的进程号。通信域可以理解为给进程分组,比如有0-5这六个进程。可以通过定义通信域,来将比如[0,1,5]这三个进程分为一组,这样就可以针对该组进行“组”操作,比如规约之类的操作。这类概念会在之后的MPI进阶一章中讲解。MPI_COMM_WORLD是MPI已经预定义好的通信域,是一个包含所有进程的通信域,目前只需要用该通信域即可。

在调用该函数时,需要先定义一个整型变量如myid,不需要赋值。将该变量传入函数中,会将该进程号存入myid变量中并返回。

函数的语法结构为

int MPI_Comm_rank(
    MPI_Comm comm      /* in */
    int*     my_rank_p /* out */)
int MPI_Comm_size(
    MPI_Comm comm      /* in */
    int *    comm_sz_p /* out */)

这两个函数中,第一个函数是一个通信子,他所属类型是MPI为通信子定义的特殊类型MPI_CommMPI_Comm_size函数在它的第二个参数返回通信子的进程数;MPI_Comm_rank函数在他的第二个参数里返回正在调用进程在进程子中的进程号。在MPI_COMM_WORLD中进程用参数comm_sz表示进程的数量,用参数my_rank来表示进程号。

比如,让进程0输出Hello,让进程1输出Hi就可以写成如下方式。

C和C++版本如下

#include "mpi.h"
int main(int *argc,char* argv[]) 
{ 
    int myid; 
    MPI_Init(&argc,&argv); 
    MPI_Comm_Rank(MPI_COMM_WORLD,&myid); 
    if(myid==0) 
    { 
        printf("Hello!"); 
    } 
    if(myid==1) 
    { 
        printf("Hi!"); 
    } 
    MPI_Finalize(); 
} 

该函数是获取该通信域内的总进程数,如果通信域为MP_COMM_WORLD,即获取总进程数,使用方法和MPI_COMM_RANK相近。

MPI_SEND

该函数为发送函数,用于进程间发送消息,如进程0计算得到的结果A,需要传给进程1,就需要调用该函数。

call MPI_SEND(buf, count, datatype, dest, tag, comm) 
int MPI_Send(type* buf, int count, MPI_Datatype, int dest, int tag, MPI_Comm comm) 

该函数参数过多,不过这些参数都很有必要存在。

这些参数均为传入的参数,其中buf为你需要传递的数据的起始地址,比如你要传递一个数组A,长度是5,则buf为数组A的首地址。count即为长度,从首地址之后count个变量。datatype为变量类型,注意该位置的变量类型是MPI预定义的变量类型,比如需要传递的是C++的int型,则在此处需要传入的参数是MPI_INT,其余同理。dest为接收的进程号,即被传递信息进程的进程号。tag为信息标志,同为整型变量,发送和接收需要tag一致,这将可以区分同一目的地的不同消息。比如进程0给进程1分别发送了数据A和数据B,tag可分别定义成0和1,这样在进程1接收时同样设置tag0和1去接收,避免接收混乱。

其语法结构为

int MPI_Send(
    void*         msg_buf_p    /* in */
    int           msg_size     /* in */
    MPI_Datatype  msg_type     /* in */
    int           dest         /* in */
    int           tag          /* in */
    MPI_Comm      communicator /* in */);

MPI_RECV

该函数为MPI的接收函数,需要和MPI_SEND成对出现。

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

参数和MPI_SEND大体相同,不同的是source这一参数,这一参数标明从哪个进程接收消息。最后多一个用于返回状态信息的参数status。

在C和C++中,status的变量类型为MPI_Status,分别有三个域,可以通过status.MPI_SOURCE,status.MPI_TAG和status.MPI_ERROR的方式调用这三个信息。这三个信息分别返回的值是所收到数据发送源的进程号,该消息的tag值和接收操作的错误代码。

在Fortran中,status的变量类型为长度是MPI_STATUS_SIZE的整形数组。通过status(MPI_SOURCE),status(MPI_TAG)和status(MPI_ERROR)来调用。

MPI_Recv的前六个参数对应了MPI_Send的前六个参数:

int MPI_Recv(
    void*         msg_buf_p    /* out */
    int           buf_size     /* in */
    MPI_Datatype  buf_type     /* in */
    int           source       /* in */
    int           tag          /* in */
    MPI_Comm      communicator /* in */
    MPI_Status*   status_p     /* out */)

MPI_Reduce

int MPI_Reduce(
    void*        input_data_p,   /*指向发送消息的内存块的指针 */
    void*        output_data_p,  /*指向接收(输出)消息的内存块的指针 */
    int          count,         /*数据量*/
    MPI_Datatype datatype,       /*数据类型*/
    MPI_Op       operator,       /*规约操作*/
    int          dest,          /*要接收(输出)消息的进程的进程号*/
    MPI_Comm     comm            /*通信器,指定通信范围*/);

全局规约函数MPI_Reduce:
将所有的发送信息进行同一个操作。

MPI_Allreduce

许多并行应用程序需要所有进程访问规约的结果,而不仅是根进程。与MPI_AllgatherMPI_Gather的补充类似,MPI_Allreduce进行规约并将结果分发给所有进程。函数原型如下:

MPI_Allreduce(
    void*        send_data,
    void*        recv_data,
    int          count,
    MPI_Datatype datatype,
    MPI_Op       op,
    MPI_Comm     communicator)

MPI_Bcast

在一个集合通讯中,如果一个进程的数据被发送到到通信子中的所有进程没这样的集合通信就叫做广播(broadcast)

函数语法结构

int MPI_Bcast(
    void*        data_p,
    int          count,
    MPI_Datatype datatype,
    int          source_proc,
    MPI_Comm     comm);

MPI_Scatter

MPI_Scatter与MPI_Bcast非常相似,都是一对多的通信方式,不同的是后者的0号进程将相同的信息发送给所有的进程,而前者则是将一段array 的不同部分发送给所有的进程:

MPI_Scatter(
    void* send_data,//存储在0号进程的数据,array
    int send_count,//具体需要给每个进程发送的数据的个数
    //如果send_count为1,那么每个进程接收1个数据;如果为2,那么每个进程接收2个数据
    MPI_Datatype send_datatype,//发送数据的类型
    void* recv_data,//接收缓存,缓存 recv_count个数据
    int recv_count,
    MPI_Datatype recv_datatype,
    int root,//root进程的编号
    MPI_Comm communicator)

MPI_Gather

MPI_Gather和MPI_scatter刚好相反,他的作用是从所有的进程中将每个进程的数据集中到根进程中,其函数为

MPI_Gather(
    void* send_data,
    int send_count,
    MPI_Datatype send_datatype,
    void* recv_data,
    int recv_count,//注意该参数表示的是从单个进程接收的数据个数,不是总数
    MPI_Datatype recv_datatype,
    int root,
    MPI_Comm communicator)

MPI_Allgather

当数据分布在所有的进程中时,MPI_Allgather将所有的数据聚合到每个进程中。

MPI_Allgather(
    void* send_data,
    int send_count,
    MPI_Datatype send_datatype,
    void* recv_data,
    int recv_count,
    MPI_Datatype recv_datatype,
    MPI_Comm communicator)

Example

发送和接收这两个函数参数过多,初学可能看不懂部分参数的意义以及使用方法,在学了这六个函数之后,这个例子就把这六个函数都使用上了。

//第一章提到的案例,具体描述可以回看第一章
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);        //得到的变量myid即为当前的进程号
//假设要求和的数组为A={[1,1,1,1],[2,2,2,2]}
if(myid==0)
{
    memset(A,1,sizeof(int));   //将数组A全赋值为1
}
else if (myid==1)
{
    memset(A,2,sizeof(int));   //将数组A全赋值为2
}
//以上部分是将数组的两行分别存储到进程0和进程1上
for(int i=0;i<4;i++)
{
    s=s+A[i];
}
if(myid==1)
{
    MPI_Send(s,1,MPI_INT,0,99,MPI_COMM_WORLD);
    //将求和结果s发送到进程0
}
if(myid==0)
{
    MPI_Recv(s1,1,MPI_INT,1,99,MPI_COMM_WORLD,&status);
    //用s1这个变量来存储从进程1发送来的求和结果
    s=s+s1;
}
printf("%d",&s);
MPI_Finalize();

编译与执行

编译与运行程序的细节主要取决于系统,所以你可能需要询问本地专家。然而,我们需要清晰地理解细节时,我们会假定使用一个文件编辑器来编写程序代码,并且用命令行的形式来编译和运行程序。许多系统都有称为mpicc的命令来编译程序。典型情况下,mpiccC语言编译器的包装脚本(wrapper script)。包装脚本的主要目的是运行某个程序。

$ mpicc -g -Wall -o mpi_hello mpi_hello.c
  • -g 允许使用调试
  • -Wall 显示警告
  • -o <outfile> 编译出的可执行文件为<outfile>

大部分系统中,用户的目录或文件夹在默认的情况下是不会出现在用户的执行路径上的。所以,我们会在可执行的文件名前加上./来给出可执行文件的路径。

许多系统还支持用mpiexec命令和mpirun来启动程序

$ mpiexec -n <number of processes> ./mpi_hello
$ mpirun -n <number of processes> ./mpi_hello