引 言
在上一篇PAC在线丨高性能计算编程入门之OpenMP中,我们对OpenMP多线程编程技术做了简单介绍,并且提供了一些代码示例,相信大家对共享存储编程有了一些了解。下面,本文将介绍文章《并行应用的开发与优化》中提到的进阶并行编程技术——MPI。
![5515a722b5f3e63543f31b284e07ab6e.png](https://i-blog.csdnimg.cn/blog_migrate/ccff99afa749d2782157b9ecaa25ec33.png)
1
MPI技术简介
MPI是一种标准化的、可移植的消息传递通信协议。支持点对点通信和集合通信,因其可靠性和可移植性广泛地应用于全球各种各样的并行计算架构,尤其是在高性能计算领域,目前有OpenMPI、MVAPICH、MPICH等多个不同的封装版本。
MPI起源于1991年奥地利的一次计算机会议上的讨论。在1992年底,由Jack Dongarra等人提出了“MPI1”草案。经过汇集来自学术界以及工业界等各方意见,研究人员在1994年的超级计算机会议上正式提交MPI,并于1994年6月正式发布MPI 1.0版本。该版本并没有共享内存的概念,只是强调了消息传递功能;2.0版本在原协议的基础上扩充了并行I/O、动态进程管理、远程内存操作等功能,绑定了新的C、C++、Fortran 90等语言标准,同时具有有限的分布式共享内存概念;3.0版本中加入了非阻塞集合操作,也被认为是一个重大的标准更新;3.1版本加入了非阻塞的集合I/O。
消息也就是进程间通信的数据是MPI的基础,由于MPI需要面对不同节点上类型表现不同的异构环境(例如在Intel处理器架构上是小字节序,即在内存高地址中存储高字节,但是IBM处理器上却是使用大字节序,即把高字节存储在内存低地址中),MPI有一套派生出来的数据类型,可以减少不同架构之间的兼容问题,需要在传递消息时就指定进程之间发送的数据类型。
MPI协议如今的普及与其通用的消息传递方式密不可分,可以应对IBM、Intel、AMD等不同厂商硬件系统的不同特性,为高性能计算应用做出重大的贡献。
2
MPI并行编程
2.1 MPI编程模型
在第1部分中提到,数据是MPI的基础,MPI也有独有的一套数据类型。以C语言为例,部分对应关系如表1所示。
表1 部分MPI数据类型对比
其中,MPI_BYTE类型大小为1字节,所以可以用来传输任意字节的数据。MPI_PACKED用于发送或者接收明确打过包的数据。
与OpenMP类似,MPI也需要通过接口函数与内置变量对程序的计算过程加以控制。以C语言为例,以下是一些基础的MPI内置变量和接口函数:
(1)int MPI_Init(int *argc, char ***argv)
初始化MPI运行环境函数。该函数为程序中第一个调用且仅调用一次的MPI函数,只有在该函数调用并返回0后,其他MPI接口函数才能在程序中使用。函数中的参数一般对应main函数的参数argc及argv。
(2)int MPI_Finalize(void )
终止MPI运行环境函数。该函数与MPI_Init函数对应,为程序最后一个调用且仅调用一次的MPI函数,在该函数调用并返回0后,其他MPI接口函数将不能再在程序中使用。
(3)MPI_Comm MPI_COMM_WORLD
MPI内置的全局通信子常量,包括用户使用的全部进程。通信子(communicator)是MPI中特有的概念,用于连接MPI中各个进程组,每个通信子为其包含的每个进程都提供了一个独立的标识符加以区分。通信子也可以分为用于组内通信的域内通信子和用于组间通信的域间通信子。
(4)int MPI_Comm_size(MPI_Comm comm, int *size)
获得通信组的大小函数。输入参数MPI_Comm类型的对象comm一般为MPI内置变量MPI_COMM_WORLD,用于获得程序使用的进程数。
(5)int MPI_Comm_rank(MPI_Comm comm, int *rank)
获得调用该函数的进程在整个通信组中的序号。参数一般为MPI_COMM_WORLD,用于获得调用该函数的进程的进程号。
(6)int MPI_Send(const void *buf, int count, MPI_Datatype datatype, int dest,int tag, MPI_Comm comm)
标准模式阻塞发送函数。将起始地址为buf的count个datatype类型的数据在comm通信域中发送给进程号为dest的进程,标签为tag,标签用于区别本进程发送给同一个comm域中进程号为dest的进程的其他信息。阻塞模式将一直等待该消息被接收后才继续执行程序。
(7)int MPI_Recv(void *buf, int count, MPI_Datatype datatype,int source, int tag, MPI_Comm comm, MPI_Status *status)
标准模式阻塞接收函数。与MPI_Send函数对应,接收comm通信域中进程号为source标签为tag的消息,存放在起始地址为buf的count个datatype类型的缓存空间中,接收的信息长度必须小于或等于缓存空间,否则将导致报错。status保存此次消息传递的状态信息。
接收函数可以在对应的发送函数之前调用,但是只有在匹配的发送函数成功调用之后才算是真正完成一组消息传递。
(8)int MPI_Bcast(void *buf, int count, MPI_Datatype datatype,int root, MPI_Comm comm)
标准模式阻塞广播消息函数。将起始地址为buf的count个datatype类型的数据在comm通信域中的root号进程广播给其他所有进程。该函数相当于在root号进程中调用MPI_Send函数,然后在其他所有进程中调用MPI_Recv接收函数。
(9)int MPI_Pack(const void *inbuf, int incount, MPI_Datatype datatype,void *outbuf, int outsize, int *position, MPI_Comm comm)
数据打包函数。将起始位置为inbuf、数据类型为datatype、大小为incount的数据打包到outbuf中,偏移量为position,在打包完成后,position会叠加打包的数据的字节大小,outsize为数据包的字节大小。
(10)int MPI_Unpack(const void *inbuf, int insize, int *position, void *outbuf, int outcount, MPI_Datatype datatype, MPI_Comm comm)
数据解包函数。将起始位置为inbuf、数据类型为datatype、大小为insize的数据包解包到outbuf中,偏移量为position,在解包完成后,position会叠减解包的数据的字节大小,outsize为数据包的字节大小。需要注意的是,在MPI_Unpack中,解包的长度完全取决于outcount参数,使用者需要明确了解数据包中各个字据的字节数。否则,解包后的数据将是错位的、没有意义的。
(11)int MPI_Reduce(const void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm)
标准模式阻塞规约函数。将起始位置为sendbuf、数据类型为datatype、大小为count的数据通过op操作规约后,集中到root号进程的起始位置为recvbuf的数据中。其中,op操作包括MPI_SUM求和、MPI_PROD求积、MPI_MAX最大值、MPI_MIN最小值等。
2.2 MPI编程实例
下面将对第2.1节中提到的部分内容提供C语言代码实例,以展示其运行效果。
以下代码运行环境为Linux操作系统4进程并行,使用Intel MPI,通过mpicc test.c命令编译程序,使用mpirun -np 4 ./a.out运行程序。
2.2.1 进程数与进程号
#include
#include "mpi.h"
int main( int argc, char *argv[] )
{
int myid, numprocs;
//初始化MPI运行环境
MPI_Init( &argc, &argv );
//获取进程号
MPI_Comm_rank( MPI_COMM_WORLD, &myid );
//获取进程总数
MPI_Comm_size( MPI_COMM_WORLD, &numprocs );
printf("I am %d of %d\n", myid, numprocs );
//终止化MPI运行环境
MPI_Finalize();
return 0;
}
程序运行截图:
可以看到,每个进程都获取到了启动的进程总数与进程号,而每个进程独有的进程号,是我们控制各个进程的计算过程的基础。
2.2.2 点对点通信
#include
#include "mpi.h"
int main(int argc, char* argv[])
{
int numprocs, myid, source;
MPI_Status status;
char message[100];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
if (myid != 0) {
//在非0号进程中创建一条信息
sprintf(message, "Greetings from process %d !",myid);
//非0号进程将各自创建的信息发送到0号进程中
MPI_Send(message,strlen(message)+1, MPI_CHAR, 0,99, MPI_COMM_WORLD);
} else {/* myid == 0 */
//0号进程接收其他进程的信息,并打印输出
for (source = 1; source < numprocs; source++) {
MPI_Recv(message, 100, MPI_CHAR, source, 99, MPI_COMM_WORLD, &status);
printf("%s\n", message);
}
}
MPI_Finalize();
return 0;
}
程序运行截图:
可以看到,除0号进程外,其他进程的消息都发送到了0号进程中,完成了3次点对点通信。
2.2.3 广播通信
#include
#include "mpi.h"
int main(int argc, char* argv[])
{
int numprocs, myid, source;
MPI_Status status;
char message[100];
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
sprintf(message, "message from process %d !",myid);
//调用广播函数,将0号进程中的信息发送给其他进程
MPI_Bcast(message,strlen(message)+1, MPI_CHAR, 0, MPI_COMM_WORLD);
printf("Process %d get %s\n", myid, message);
MPI_Finalize();
return 0;
}
程序运行截图:
可以看到,除0号进程外的进程,输出的信息是来自0号进程中的信息,所有进程都调用MPI_Bcast函数来进行一次广播通信。
2.3 利用MPI并行计算Pi值
以下为利用近似法求解Pi值的OpenMP并行实现代码,其算法原理是利用梯形公式近似求解积分
#include "mpi.h"
#include
#include
double f( double a )
{
return (4.0 / (1.0 + a*a));
}
int main( int argc, char *argv[])
{
int done = 0, n, myid, numprocs, i;
double PI25DT = 3.141592653589793238462643;
double mypi, pi, h, sum, x;
int namelen;
MPI_Init(&argc,&argv);
MPI_Comm_size(MPI_COMM_WORLD,&numprocs);
MPI_Comm_rank(MPI_COMM_WORLD,&myid);
fprintf(stderr,"Process %d \n", myid);
n = 0;
while (!done)
{
if (myid == 0)
{
if (n==0) n=100; else n=0;
}
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
if (n == 0)
done = 1;
else {
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 approximately %.16f, Error is %.16f\n", pi, fabs(pi - PI25DT));
}
}
}
MPI_Finalize();
return 0;
}
程序运行截图:
3
总 结
MPI是高性能计算领域中必不可少的并行技术,可以实现节点间的分布式内存多机并行,相比OpenMP编程复杂。MPI技术多用于数据级粗粒度并行,适合大规模并行算法,并且在各类并行机上都具有良好的适用性,因此,被广泛应用于各种科学计算领域的开源、商业软件中。
●友情提示
疫情还未结束,请大家继续保持良好习惯,出门戴口罩,回家勤洗手。疫情防控人人有责,PAC和你在一起。
欢迎大家留言提问,另PAC2020报名已启动,点击PAC公众号菜单栏处“PAC报名”即可报名参赛。