MPI与并行计算02:6个基本函数

Posted by Ling on 2023-07-19

本章讲解MPI的6个基本函数,掌握了这六个基本函数,就能开始写一个MPI程序了。考虑到读者可能有人擅长C++,有人擅长Fortran。因此在讲这些函数接口时,我会将两种语言的接口和参数类型都附上。如果只是应用MPI处理科学计算的问题,很多参数不用深究其意义,甚至不需要记忆,只需要在写或读代码的时候随时查手册即可。这6个基本函数基本可以实现大部分功能,因此有必要对这6个函数有比较准确的理解。

一、MPI_Init

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

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

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

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

二、MPI_Finalize

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

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

三、MPI_COMM_RANK

1
call MPI_COMM_RANK(comm, rank) 
1
int MPI_Comm_Rank(MPI_Comm comm, int *rank)

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Program main 
    use mpi
    implicit none 
    integer :: myid 
    MPI_INIT() 
    call MPI_COMM_RANK(MPI_COMM_WOLRD,myid) 
    if (myid==0) then 
        print *, "Hello!" 
    end if 
    if (myid==1) 
        print *, "Hi!" 
    end if 
    MPI_FINALIZE() 
end Program

C和C++版本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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(); 
} 

四、MPI_COMM_SIZE

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

1
MPI_COMM_SIZE(comm, size) 
1
int MPI_Comm_Size(MPI_Comm, int *size)

五、MPI_SEND

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

1
call MPI_SEND(buf, count, datatype, dest, tag, comm)
1
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去接收,避免接收混乱。

六、MPI_RECV

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

1
call MPI_RECV(buf, count, datatype, source, tag, commstatus) 
1
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)来调用。

SEND和RECV需要成对出现,若两进程需要相互发送消息时,对调用的顺序也有要求,不然可能会出现死锁或内存溢出等比较严重的问题,具体在之后的对等模式这一章中详细介绍。

七、Example

发送和接收这两个函数参数过多,初学可能看不懂部分参数的意义以及使用方法,在学了这六个函数之后,再来看本系列第一章提到的一个简单的例子,这个例子就把这六个函数都使用上了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//第一章提到的案例,具体描述可以回看第一章
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();

这一章主要介绍了MPI的最基本的六大接口,理解了这六个函数接口,就可以写一个最基本的MPI并行程序了,下一章详细讲解如何写一个MPI的基本程序,通过编程实践,将会进一步加深对这些函数接口的理解。

参考资料

1.都志辉. 高性能计算并行编程技术:MPI并行程序设计[M]. 清华大学出版社, 2001.

2.两小时入门MPI与并行计算系列 - 知乎 (zhihu.com)