4.5.1MPI介绍安装
MPI 消息传递接口(Message Passing Interface,简称 MPI)是一种编程接 口标准,而不是一种具体的编程语言。
与多线程、OpenMP 并行程序不同,MPI 是一种基于消息传递的并行编程技术。 MPI 编程是基于消息传递的并行编程技术,是如今应用最为广泛的并行程序开发方法。
MPI实现:MPICH INTELMPI
MPI安装:sudo apt install mpich
4.5.2MPI 程序特点
MPI 程序是基于消息传递的并行程序。消息传递指的是并行执行的各个进程具有自己独 立的堆栈和代码段,作为互不相关的多个程序独立执行,进程之间的信息交互完全通过显式 地调用通信函数来完成。
这与多线程、OpenMP 程序共享同一内存空间有着显著的不同,而 且有利有弊。好处在于带来了更多的灵活性,除了支持多核并行、SMP 并行之外,消息传 递更容易实现多个节点间的并行处理;而另一方面,也正是源于这种进程独立性和显式消息 传递的特点,MPI 标准更加繁复,基于其开发并行程序也更加复杂。 基于消息传递的并行程序可以划分为单程序多数据(Single Program Multiple Data,简 称 SPMD)和多程序多数据 MPMD 两种形式。SPMD 使用一个程序来处理多个不同的数据 集以达到并行的目的。并行执行的不同程序实例处于完全对等的位置。相应的,MPMD 程 序使用不同的程序处理多个数据集,合作求解同一个问题。 SPMD 是 MPI 程序中最常用的并行模型。下图为 SPMD 执行模型的示意图,其中表 示了一个典型的 SPMD 程序,同样的程序 prog_a 运行在不同的处理核上,处理了不同的数 据集。
MPI六个基本函数:
1.MPI_Init
任何MPI程序都应该首先调用该函数。 此函数不必深究,只需在MPI程序开始时调用即可(必须保证程序中第一个调用的MPI函数是这个函数)。
MPI_Init(&argc, &argv) //C++ & C
2.MPI_Finalize() //C++
任何MPI程序结束时,都需要调用该函数。切记Fortran在调用MPI_Finalize的时候,需要加个参数ierr来接收返回的值,否则计算结果可能会出问题甚至编译报错。
3.MPI_COMM_RANK
MPI_Comm_Rank(MPI_Comm comm, int *rank)
该函数是获得当前进程的进程标识,如进程0在执行该函数时,可以获得返回值0。可以看出该函数接口有两个参数,前者为进程所在的通信域,后者为返回的进程号。通信域可以理解为给进程分组,比如有0-5这六个进程。可以通过定义通信域,来将比如[0,1,5]这三个进程分为一组,这样就可以针对该组进行“组”操作,比如规约之类的操作。这类概念会在之后的MPI进阶一章中讲解。MPI_COMM_WORLD是MPI已经预定义好的通信域,是一个包含所有进程的通信域,目前只需要用该通信域即可。
在调用该函数时,需要先定义一个整型变量如myid,不需要赋值。将该变量传入函数中,会将该进程号存入myid变量中并返回。
4.MPI_COMM_SIZE
该函数是获取该通信域内的总进程数,如果通信域为MP_COMM_WORLD,即获取总进程数,使用方法和MPI_COMM_RANK相近。
MPI_COMM_SIZE(comm, size) int MPI_Comm_Size(MPI_Comm, int *size)
5.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去接收,避免接收混乱。
6.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值和接收操作的错误代码。
SEND和RECV需要成对出现,若两进程需要相互发送消息时,对调用的顺序也有要求,不然可能会出现死锁或内存溢出等比较严重的问题
4.5.3如何编写MPI代码
1:对于rank,size的理解
#include<bits/stdc++.h>
#include<mpi.h>
using namespace std;
int main(int argc,char * argv[])
{
int myid,rank,size;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
MPI_Comm_size(MPI_COMM_WORLD,&size);
cout<<"rank: "<<rank<<" size: "<<size<<endl;
MPI_Finalize();
return 0;
}
编译:mpic++ first.cpp -o first
运行:mpirun -np 4 ./first
运行:mpirun -np 6 ./first
代码解读:首先需要添加头文件<mpi.h>。主函数内初始化,获得当前进程标识符和进程总数。但在代码内并没有进程数量大小的设置,而是在编译的时候指定进程数量。-np 后所跟数字即为进程数量。而rank size的值将非常重要的应用到后面的代码编写中。总的理解就是在MPI初始化后,所有的进程执行同一域内的代码,而每个进程的区分靠其独有的rank size的值。
2:对于发送、接收的理解
#include<bits/stdc++.h>
#include<mpi.h>
using namespace std;
int main(int argc, char *argv[])
{
int rank,size,i,buf[1];
MPI_Status status;
MPI_Init(&argc,&argv);
MPI_Comm_rank(MPI_COMM_WORLD,&rank);
MPI_Comm_size(MPI_COMM_WORLD,&size);
if(rank==0)
{
• for(i=0;i<100*(size-1);i++)
• {
• MPI_Recv(buf,1,MPI_INT,MPI_ANY_SOURCE,MPI_ANY_TAG,MPI_COMM_WORLD,&status);
• cout<<"Msg= "<<buf[0]<<" from "<<status.MPI_SOURCE<<" with tag "<<status.MPI_TAG<<endl;
• }
}
else
{
• for(i=0;i<100;i++)
• {
• buf[0]=rank+i;
• MPI_Send(buf,1,MPI_INT,0,i,MPI_COMM_WORLD);
• }
}
MPI_Finalize();
return 0;
}
编译:mpic++ recive.cpp -o recive
运行:mpirun -np 4 ./recive
结果:
代码分析:
MPI初始化后,通过循环下标控制多个进程向主进程发送消息,主进程接收并打印。发送和接收消息是对应的。
MPI矩阵乘
下方代码存在问题,double判断是否相等需要设置精度范围而不是直接相减为零,自己当时编码脑子不在线,大家注意别犯我的错误就好:)
代码:
#include <bits/stdc++.h>
#include <omp.h>
#include<mpi.h>
#include <immintrin.h>
using namespace std;
#define max 1024
#define location(i, j, n) (i * n + j)
typedef chrono::high_resolution_clock Clock;
const int n = max;
int *A = new int[max * max];
int *B = new int[max * max];
int *C = new int[max * max];
int *D = new int[max * max];
int main(int argc,char *argv[])
{
cout << "Size of Matrix: " << n << endl;
srand((int)time(0));
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
A[location(i,j,n)]=1;
B[location(i,j,n)]=1;
}
}
auto startTime = Clock::now();
int rank,size;
MPI_Status status;
MPI_Init(&argc,&argv);
MPI_Comm_rank (MPI_COMM_WORLD,&rank);
MPI_Comm_size (MPI_COMM_WORLD,&size);
if(rank==0)
{
for(int i=1;i<size;i++)
{
MPI_Send(&A[i*max/size*max],n*max/size,MPI_INT,i,9,MPI_COMM_WORLD);
MPI_Send(B,max*max,MPI_INT,i,9,MPI_COMM_WORLD);
}
for(int i=0;i<max/size;i++)
{
for(int j=0;j<n;j++)
{
C[location(i,j,n)]=0;
for(int k=0;k<n;k++)
{
C[location(i,j,n)]+=A[location(i,k,n)]*B[location(k,j,n)];
}
}
}
}
else
{
MPI_Recv (&A[rank*max/size*max],n*max/size,MPI_INT,0,9,MPI_COMM_WORLD,&status);
MPI_Recv (B,max*max,MPI_INT,0,9,MPI_COMM_WORLD,&status);
for(int i=rank*max/size;i<(rank+1)*max/size;i++)
{
for(int j=0;j<n;j++)
{
C[location(i,j,n)]=0;
for(int k=0;k<n;k++)
{
C[location(i,j,n)]+=A[location(i,k,n)]*B[location(k,j,n)];
}
}
}
MPI_Send(&C[rank*(max/size)*max],max*max/size,MPI_INT,0,9,MPI_COMM_WORLD);
}
if(rank==0)
{
for(int k=1;k<size;k++)
{
MPI_Recv(&C[k*(max/size)*max],max*max/size,MPI_INT,k,9,MPI_COMM_WORLD,&status);
}
}
auto endTime = Clock::now();
auto compTime = chrono::duration_cast<chrono::microseconds>(endTime-startTime);
cout<<"One core time : "<<compTime.count()/1000<<"ms"<<endl;
MPI_Finalize();
return 0;
}
编译指令:mpic++ mpi-matrix.cpp -o mpi-matrix
运行:mpirun -np 4 ./mpi-matrix
运行结果:
代码分析:
由于矩阵乘法的实现数据依赖性不强,通过简单的划分使得矩阵A的多行乘矩阵B。通过设置进程数使得每个进程获得A的一部分,进而独立运算后将数据回传,这样理论上将会带来速度的提升。但本实验测试环境为12代I5-12400F,进程切换以及资源的竞争导致运算速度并没有得到显著的提升。本例只是为演示MPI的基本操作。对于大规模集群计算,MPI跨界点可以带来计算速度以及计算能力的提升。