目前两种最重要的并行编程模型是数据并行和消息传递.
对于机群系统一次通信的开销要远远大于一次计算的开销
应尽可能降低通信的次数.
目前主要的mpi实现
- mpich
- chimp
- lam
理论上说MPI所有的通信功能可以用它的6个基本的调用来实现
- MPI_Init(&argc, &argv) //初始化MPI执行环境,建立多个MPI进程之间的联系,为后续通信做准备。
- MPI_Finalize() //结束MPI执行环境。
- MPI_Comm_rank (MPI_COMM_WORLD, &rank); //标识各个MPI进程的,给出调用该函数的进程的进程号
- MPI_Comm_size (MPI_COMM_WORLD, &size); //用来标识相应进程组中有多少个进程
- MPI_Send(buf,counter,datatype,dest,tag,comm);
buf | counter | datatype | dest | tag | comm |
---|---|---|---|---|---|
发送缓冲区的起始地址,可以是数组或结构指针 | 非负整数,发送的数据个数 | 发送数据的数据类型 | 整型,目的的进程号 | 整型,消息标志 | MPI进程组所在的通信域 |
将发送缓冲区buf中的counter个datatype类型的数据发送到dest目的进程,本次发送的消息标志是tag(用于和MPI_Recv tag 匹配)
6.MPI_Recv(buf,count,datatype,source,tag,comm,status);
buf | counter | datatype | source | tag | comm | status |
---|---|---|---|---|---|---|
接收缓冲区的起始地址,可以是数组或结构指针 | 非负整数,可接收的数据个数 | 接收数据的数据类型 | 整型,发送源进程号 | 整型,消息标志 | MPI进程组所在的通信域 | 返回状态 |
MPI_Recv(&buf, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status);
可以用于接收任意源,任意tag的消息.
status是MPI定义的一个数据类型,返回状态变量
statusstatus.MPI_SOURCE | status.MPI_TAG | status.MPI_error |
---|
通过调用就可以得到返回状态中所包含的
- 发送数据进程的进程号
- 发送数据使用的tag
- 本次接收操作返回的错误代码
MPI预定义数据类型
- MPI_INT
- MPI_CHAR
- MPI_SHORT
- MPI_LONG
- MPI_FLOAT
- MPI_DOUBLE
- …8. … 9. … 10. …
MPI数据类型匹配
- 有数据类型的通信,发送方和接收方均使用相同的数据类型
- 无数据类型的通信,发送方和接收方均使用MPI_BYTE作为数据类型
- 打包数据的通信,发送方和接收方均使用MPI_PACKED.
MPI_TYPE用于不加修改的传送内存中的二进制值
MPI_PACK用于数据的打包和解包(MPI_UNPACK)
数据转换
- 数据类型的转换 mpi严格要求类型匹配,所以不存在数据类型转换问题.
- 数据表示的转换
MPI消息
消息=信封+数据
信封:<源/目,标识,通信域>
数据:<起始地址,数据个数,数据类型>
MPI_ANY_SOURCE, MPI_ANY_TAG,接收任意源,任意tag消息
一个发送必须指明一个接收,但一个接收可以有多个发送.
MPI通信域
通信域=进程组+通信上下文
进程组=所有参与通信的进程的集合, 编号从0–N.
MPI_Comm comm; //定义一个名为 comm 的通信域.
通信上下文提供一个相对独立的通信区域,不同的消息在不同的上下文中进行传递,不同上下文消息互不干涉. 用户可以定义新的通信域.
MPI时间函数
- MPI_Wtime(); 返回用双精度浮点数表示的秒数.
- MPI_Wtick(); 返回MPI_Wtime的精度,单位是秒 测试后=0.000001
MPI获取节点名字(机器名字)和mpi版本号
MPI_Get_processor_name(processor_name, &namelen);
processor_name:机器名字
namelen:返回名字的长度
MPI_Get_version(&version, &subversion);
veprintf(“version %d.%d \n”,version, subversion);
rsion,subversion均为int型, version返回mpi大版本 subversion返回小版本号 例如:3.1
判断MPI是否初始化函数
int flag = 0;
MPI_Initialized(&flag);
唯一一个可以用在MPI_Init()之前的MPI函数,功能是判断MPI_Init是否已经执行.
当MPI_Init已经执行时flag会被赋值为 1 .
MPI并行程序的两种基本模式
- 对等模式
- 主从模式
MPI通信模式
为了应对不同的通信需求
通信模式 | 发送 | 接收 |
---|---|---|
标准通信模式 | MPI_SEND | MPI_RECV |
缓存通信模式 | MPI_BSEND | |
同步通信模式 | MPI_SSEND | |
就绪通信模式 | MPI_RSEND |
MPI并行编程环境搭建配置
编译命令
- mpicc:编译并链接c MPI程序
- mpicxx:编译并链接c++ MPI程序
- mpif77:编译并链接fortran77 MPI程序
- mpif90:编译并链接fortran90 MPI程序
- mpirun
- mpiexec
这些命令在链接时会自动提供MPI需要的库
常用的编译选项
-n 4 //启用四个进程
-machinefile //启用配置文件
node01:1
node02:1
node03:1
node04:1
-p4pg pgfile
阻塞通信
非阻塞通信
非阻塞通信主要用于计算和通信的重叠,从而提高整个程序的执行效率
以上均为点对点通信
组通信
组通信一般实现三个功能
- 通信 //数据的传输
- 同步 //实现组内所有进程在特定地点上执行进度取得一致
- 计算 //对给定的数据完成一定的操作
组通信按通信方向不同可以分为三种:
一对多通信
graph LR
A-->B
A-->C
A-->D
多对一通信
graph LR
B-->A
C-->A
D-->A
多对多通信
MPI 广播
一对多组通信,将消息广播给组内所有进程,包括它本身在内
MPI_Bcast(buffer,counter,datatype,root,comm);
buf | counter | datatype | root | comm |
---|---|---|---|---|
消息缓冲区的起始地址 | 将广播/接收的数据个数 | 广播/接收的数据类型 | 广播数据的跟进程标识号 | 通信域 |
MPI收集
多对一通信 每个进程包括根进程将消息发送到根进程
根进程将收到的消息依次存放到自己的消息缓存区中
对于收集,每个进程发送的消息不同,但个数必须相同,数据类型也是相同的
根进程指定的接收数据个数是指从每一个进程接受到的数据的个数,而不是总的接收个数.
MPI_Gather(sendbuf, sendcount, datatype, recvbuf, recvcount, recvtype, root, comm);
sendbuf | sendcounter | datatype | recvbuf | recvcount | recvtype | root | comm |
---|---|---|---|---|---|---|---|
发送消息缓冲区的起始地址 | 发送消息的数据个数 | 发送消息的数据类型 | 接收消息缓冲区的起始地址 | 待接收的数据个数 | 接收数据的数据类型 | 接收进程的rank,根节点 | 通信域 |
其中下面两个仅对根节点(接收进程)有意义
- recvcount
- recvtype
MPI_Gatherv() 和MPI_Gather()功能类似,也完成数据的收集功能,区别在于它可以从不同进程接收不同数量的数据.
对于MPI_Gatherv(); recvcount(待接收数据个数)是一个数组
MPI_Gatherv(sendbuf, sendcount, sendtype, recvbuf, recvcount,displs, recvtype, root, comm);
sendbuf: 每个进程该值相同时,每次都从相同的位置取数据,不同时每个进程收集来的数据不同.
sendcounter: 每个进程该值不同.
disples: 整数数组,每个数据存放位置相对于recvbuf的位移
#include <mpi.h>
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char* argv[])
{
int rank, size,i,j;
int namelen;
char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Init (&argc, &argv); /* starts MPI*/
MPI_Status status;
MPI_Comm_rank (MPI_COMM_WORLD, &rank); /* get current process id*/
MPI_Comm_size (MPI_COMM_WORLD, &size); /* get number of processes*/
MPI_Get_processor_name(processor_name, &namelen);
//printf( "进程id为%d 共有 %d个进程 on %s\n\n\n", rank, size,processor_name );
printf(" 进程号 = %d ,共有%d个进程 , 节点 = %s\n",rank,size,processor_name);
MPI_Barrier(MPI_COMM_WORLD);//同步
int sendarray[100];
int *rbuf, *displs, *recvcount;
//int stride=5;
int sum=0;
for(i=0;i<=size-1;i++) sum+=i;
rbuf=(int*)malloc(size*sum*sizeof(int));
displs=(int*)malloc(size*sizeof(int));
recvcount=(int*)malloc(size*sizeof(int));
for(i=0;i<100;i++) sendarray[i]=i+100;
for(i=0;i<size;i++) recvcount[i] = i*size;
displs[0] = 0;
for(i=1;i<size;i++) displs[i]=displs[i-1]+(i-1)*size;
MPI_Barrier(MPI_COMM_WORLD);//同步
//MPI_Gatherv(sendbuf, sendcount, sendtype, recvbuf, recvcount,displs, recvtype, root, comm);
MPI_Gatherv(sendarray, rank*size, MPI_INT, rbuf,recvcount ,displs, MPI_INT, 0, MPI_COMM_WORLD);
if(rank==0)
{
for(i=0;i<size*sum;i++) printf("rbuf[%d] = %d\n",i,rbuf[i]);
}
MPI_Finalize(); //MPI end.
return 0;
}
MPI 散发
一对多通信
和广播不同的是root向各个进程发送的数据可以是不同的.
广播是把一个或一组相同的数据广播出去,散发可以同时给通信域中的进程发送不同的数据.
MPI_Scatter(sendbuf, sendcount, sendtype,recvbuf, recvcount, recvtype, root, comm);
sendbuf | sendcounter | sendtype | recvbuf | recvcount | recvtype | root | comm |
---|---|---|---|---|---|---|---|
发送消息缓冲区的起始地址 | 发送到各个进程的数据个数 | 待散发消息的数据类型 | 接收消息缓冲区的起始地址 | 待接收的数据个数 | 接收数据的数据类型 | 接收进程的rank,根节点 | 通信域 |
sendbuf为要散发数据的起始地址, 根据sendcount和size ,root会把缓冲区数据切成size段,然后依次发送给各个进程.
当然还有MPI_Scatterv(); //不详细展开
MPI组收集
MPI_Allgather(sendbuf, sendcount, sendtype, recvbuf, recvcount,recvtype,comm);
- MPI_GATHER是将数据收集到ROOT进程而MPI_ALLGATHER相当于每一个进程都作为ROOT执行了一次MPI_GATHER调用,即每一个进程都收集到了其它所有进程的数据
- MPI_ALLGATHER和MPI_GATHER效果相同,只不过相当与每个进程都执行了一次.
MPI_Allgatherv(sendbuf, sendcount, sendtype, recvbuf, recvcounts, displs, recvtype,comm);
MPI全互换
MPI_Alltoall(sendbuf, sendcount, sendtype, recvbuf, recvcount,recvtype, comm);
sendbuf | sendcount | sendtype | recvbuf | recvcount | recvtype | comm |
---|---|---|---|---|---|---|
发送消息缓冲区的起始地址 | 发到每个进程的消息个数 | 消息的数据类型 | 接收消息缓冲区的起始地址 | 从每个进程接收消息的个数 | 接收消息的数据类型 | 通信域 |
- 全互换是组内进程之间完全的消息交换
- 每一个进程向所有进程发送消息
- 每一个进程接收所有进程的消息
- mpi_allgather 每个进程发送一个相同的消息给所有进程, mpi_alltoall散发给不同进程的消息是不同的
- mpi_alltoall 的每个进程可以向每个接收者发送数目不同的数据
- 第 i 个进程发送的第 j 块数据将被第 j 个进程接收并存放在其接收消息缓冲区的 第 i 块
- 每个进程和根进程之间,发送的数据量必须和接受的数据量相等
MPI_Alltoallv();
MPI同步
MPI_Barrier(comm);
阻塞所有的进程知道所有进程都调用了它.
MPI归约
…
MPI组归约
…
MPI归约并散发
…
MPI扫描
…
具有不连续数据的发送
处理不连续的数据有两种基本方法:
- 允许用户自定义新的数据类型(又称派生数据类型)
- 数据的打包与解包
派生数据类型
…
打包和解包
pack ----unpack
MPI_Pack(inbuf, incount, datatype, outbuf, outcount, position, comm );
inbuf | incount | datatype | outbuf | outcount | position | comm |
---|---|---|---|---|---|---|
输入缓冲区起始地址 | 输入数据项个数 | 每个输入数据项的类型 | 输出缓冲区开始地址 | 输出缓冲区大小 | 缓冲区当前位置 | 通信域 |
MPI_Unpack(inbuf, insize, position, outbuf, outcount, datatype, comm );
inbuf | insize | position | outbuf | outcount | datatype | comm |
---|---|---|---|---|---|---|
输入缓冲区起始地址 | 输入数据项个数 | 缓冲区当前地址 | 输出缓冲区开始地址 | 输出缓冲区大小 | 每个输入数据项类型 | 通信域 |
备忘:
经常使用的MPI函数调用方式
- MPI_Status status; //状态函数
- MPI_Barrier(MPI_COMM_WORLD);//同步
- char processor_name[MPI_MAX_PROCESSOR_NAME];
MPI_Get_processor_name(processor_name, &namelen); //获取节点名称