OpenMPI 学习笔记(四)集体通信
基础
内容提要:
- Notions générales
- Synchronisation globale : MPI_Barrier
- Diusion générale : MPI_Bcast
- Diusion sélective : MPI_Scatter
- Collecte : MPI_Gather
- Collecte générale : MPI_Allgather
- Diusion : MPI_Scatterv
- Collecte : MPI_Gatherv
- Collectes et diusions sélectives : MPI_Alltoall
概念
集体通信允许在单个操作中进行一系列点对点通信。
集体沟通总是涉及指定沟通者的所有过程。
对于每一个过程,当参与集体交易完成时,通话以点对点通信的方式结束。
我们从不明确标注标签;系统透明地管理它们。优势:集体通信从不干扰点对点通信。
有三种类型的集体通信:
- 全局同步:MPI_Barrier
- 仅数据传输:
- 广播式传送:MPI_Bcast,
- 数据的选择性分配传输:MPI_Scatter,MPI_Scatterv,
- 分布式数据收集:MPI_Gather,MPI_Gatherv,
- 所有进程均进行分布式数据收集:MPI_Allgather,
- 由所有进程选择性收集和分发分布式数据:MPI_Alltoall。
- 传输和执行数据操作:
- 减少(总和,产品,最大值,最小值等),不管它们是什么类型,预定义类型(现有的)或个人自定义类型:MPI_Reduce,
- 减少并发送结果(相当于一个MPI_Reduce后跟一个MPI_Bcast):MPI_Allreduce。
一、全局同步:MPI_Barrier
如图所示:P3最先到达barrier,但是它停下来了等了等后面的P0,P1,P2,然后四个进程都在barrier之后,再次开始出发,执行。
这种通讯中,所有的进程都要调用 MPI_Barrier (没法确定谁快,谁先到了谁等,谁就bloqué)。当所有进程的状态都处于阻塞等待状态时,说明全都到齐了,可以再次出发了。
// collectives/barrier.c #include <mpi.h> #include <stdio.h> #include <time.h> int main(int argc, char ** argv) { MPI_Init(&argc, &argv); int wrank; MPI_Comm_rank(MPI_COMM_WORLD, &wrank); if (wrank==0) { FILE* f = fopen("outin", "w"); long int seconds = time(NULL); fprintf(f, "%ld", seconds); fclose(f); } MPI_Barrier(MPI_COMM_WORLD); //所有进程均调用 long int witness = 0; FILE* f = fopen("outin", "r"); fscanf(f, "%d", &witness); printf("Rang %d, witness %ld.\n", wrank, witness); fclose(f); MPI_Finalize(); return 0; }
控制台测试
$ mpirun -n 4 source
Rang 1, witness 1450259857.
Rang 0, witness 1450259857.
Rang 2, witness 1450259857.
Rang 3, witness 1450259857.
如果我们不使用全局同步,则结果如下:
Rang 1, witness 1450259857.
Rang 3, witness 1450259857.
Rang 0, witness 1450259860.
Rang 2, witness 1450259860.
这种情况是:进程1和3在进程0修改文件之前读了文件,得到的是修改之前的值,进程0和2在进程0修改文件之后读的文件,得到的是修改之后的值。
⚠️注意:MPI_Barrier函数不会将消息标识符作为参数,要用就是全局。
if (wrank==0) MPI_Barrier(MPI_COMM_WORLD); else MPI_Barrier(MPI_COMM_WORLD);
等同于:
MPI_Barrier(MPI_COMM_WORLD);
内部实现机制其实是:
所有非0进程都向进程0发送空消息,并且等待回应;进程等待除了自己的之外的所有进程给自己发空消息;当进程0收到除了自己之外的所有进程发来的消息时,它便给每一个进程发送回复(空消息),通知所有人“所有人都已到达barrier,可以再次出发了”;所有非0进程收到回复后便重新出发,0进程发送完成之后便重新出发,继续执行。
MPI_Barrier是确保进程之间同步的唯一方法。特别是,随后提出的集体通信都不会确保同步。
二、一般发送 MPI_Bcast
//所有进程都要执行这句代码。发送数据的数量count对于每一个进程都一样
int MPI_Bcast(void *buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm);
发送者:一个root,唯一的 接受者:包括发送者在内的所有人
// collectives/bcast.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { MPI_Init(&argc, &argv); int wrank; MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int witness = wrank; MPI_Bcast(&witness, 1, MPI_INT, 2, MPI_COMM_WORLD); printf("Rang %d, witness %d.\n", wrank, witness); MPI_Finalize(); return 0; }
$ mpirun -n 4 source
Rang 1, witness 2.
Rang 0, witness 2.
Rang 2, witness 2.
Rang 3, witness 2.
三、选择性分配:MPI_Scatter
int MPI_Scatter( const void *sendbuf, int sendcount, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype,int root, MPI_Comm comm);
要发送的信息缓存(整体),
要发送的信息的数量(给每个进程分配的数量),
信息数据类型,
接受信息的缓存(每个进程接收需要的实际大小),
接受的信息的数量(每个进程接收的实际数量),
接受的信息类型,
唯一发送者root,全局
所有进程均须调用该方法, 唯一的发送者root, 接受者为所有进程。
sendcount是发送到单个进程的数据的数量。MPI_Scatter剪切要发送的数据(与通信器中的进程一样多),大小为sendcount。发送的数据的顺序对应于接收过程的顺序:第i个数据集被发送到等级i的过程。发送的数据总数为sendcount *(通信器中的进程数)。
发送的数据总数 = 接受的数据总数: sendcount*sizeof(sendtype (en C)) == recvcount*sizeof(recvtype (en C))
// collectives/scatter.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { MPI_Init(NULL, NULL); int wrank; MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int array[] = {wrank, 2*wrank, 3*wrank, 4*wrank}; int witness=0; MPI_Scatter(array, 1, MPI_INT, &witness, 1, MPI_INT, 2, MPI_COMM_WORLD); printf("Rang %d, witness %d.\n", wrank, witness); MPI_Finalize(); return 0; }
$ mpirun -n 4 source
Rang 0, witness 2.
Rang 1, witness 4.
Rang 2, witness 6.
Rang 3, witness 8.
四、收集:MPI_Gather
int MPI_Gather( const void *sendbuf, int sendcount, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm);
这是MPI_Scatter的逆操作。 root是唯一的接收器,所有进程都是发射器。
sendcount是单个进程发送的数据的数量。数据按照流程的顺序进行收集。数据大小限制与MPI_Scatter相同。
recvcount是接收的总量。
// collectives/gather.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { MPI_Init(NULL, NULL); int wrank; MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int witness[] = {0, 0, 0, 0}; MPI_Gather(&wrank, 1, MPI_INT, witness, 1, MPI_INT, 2, MPI_COMM_WORLD); printf("Rang %d, witness", wrank); for (int i=0; i<4; ++i) printf(" %d", witness[i]); printf(".\n"); MPI_Finalize(); return 0; }
$ mpirun -n 4 source
Rang 0, witness 0 0 0 0.
Rang 3, witness 0 0 0 0.
Rang 1, witness 0 0 0 0.
Rang 2, witness 0 1 2 3.
⚠️:进程0,2,1,3发送自己的进程号,进程2是接收四个进程,所以进程0,1,3 的witness为0,进程2为四个进程总和0 1 2 3
五、普遍收集 MPI_Allgather
int MPI_Allgather( const void *sendbuf, int sendcount, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype, MPI_Comm comm);
- 所有的过程都是发射器,都是接收器。
- sendcount是进程发送的数据的数量。
- 数据按照流程的顺序进行收集。
- 等同于:只需将MPI_Gather的进程0作为接收方,然后使用MPI_Gather并将进程1作为接收方,以此类推就可以完成所有进程。
// collectives/allgather.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { MPI_Init(NULL, NULL); int wrank; MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int witness[] = {0, 0, 0, 0}; MPI_Allgather(&wrank, 1, MPI_INT, witness, 1, MPI_INT, MPI_COMM_WORLD); printf("Rang %d, witness", wrank); for (int i=0; i<4; ++i) printf(" %d", witness[i]); printf(".\n"); MPI_Finalize(); return 0; }
$ mpirun -n 4 source
Rang 0, witness 0 1 2 3.
Rang 3, witness 0 1 2 3.
Rang 2, witness 0 1 2 3.
Rang 1, witness 0 1 2 3.
六、选择性分配(数量不同):MPI_Scatterv
int MPI_Scatterv( const void *sendbuf, const int sendcounts[], const int displs[],MPI_Datatype sendtype,
void *recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm);
与MPI_Scatter相同的操作和属性,但我们可以调整使用参数sendcounts和displs(displacements / osets)决定发送的数据的数量和位置。
sendcounts和displs参数都只用于发送进程。
sendcounts和displs size是通信器中进程的数量。
⚠️:发送
例子:如图,我们只考虑P2,
缓存大小:sendbuf detaille1+2+1+2=6,
给每个进程发送的数据量:sendcounts = {1, 2, 1, 2}
给每个进程发送的数据在缓存中的位置:displs = {0, 1, 3, 4}.
// collectives/scatterv.c #include <mpi.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { MPI_Init(NULL, NULL); int world_size, wrank; MPI_Comm_size(MPI_COMM_WORLD, &world_size); MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int recvbuf[] = {wrank, wrank}; int recvcount = (wrank%2) + 1; int *sendbuf, *sendcounts, *displs; int total = 6;
if (wrank==2) { sendcounts = (int*) calloc(world_size, sizeof(int)); //总信息数 for (int i=0; i<world_size; ++i) sendcounts[i]= (i%2) + 1; //为每个进程分配要发送的数据量:奇数进程为1个,偶数进程为2个 displs = (int*) calloc(world_size, sizeof(int)); //为displs位置分配内存,数量=进程数 displs[0] = 0; //起始位置为0 for (int i=1; i<world_size; ++i) displs[i] = displs[i-1] + sendcounts[i-1]; //为每个进程指定要发送的缓存的起始位置:自己的进程号+前一个进程的数据量
sendbuf = (int*) calloc(total, sizeof(int)); //总缓存大小 for (int i=0; i<total; ++i) sendbuf[i] = 28+i; }
MPI_Scatterv(sendbuf, sendcounts, displs, MPI_INT, recvbuf, recvcount,MPI_INT, 2, MPI_COMM_WORLD); printf("Rang %d, recvbuf %d %d.\n", wrank, recvbuf[0], recvbuf[1]);
if (wrank==2) { free(sendcounts); free(displs); free(sendbuf); }
MPI_Finalize();
return 0;
}
$ mpirun -n 4 source
Rang 0, recvbuf 28 0.
Rang 1, recvbuf 29 30.
Rang 2, recvbuf 31 2.
Rang 3, recvbuf 32 33.
七、选择性收集 : MPI_Gatherv
int MPI_Gatherv( const void *sendbuf, int sendcount, MPI_Datatype sendtype, void *recvbuf, const int recvcounts[], const int displs[], MPI_Datatype recvtype, int root, MPI_Comm comm);
与MPI_Gather相同的操作和属性,但我们可以通过参数recvcounts和displs(位移/位置)调整接收到的数据的数量和位置。
这两个参数recvcounts和displs仅用于接收过程。
recvcounts和displs的大小是通信器中的进程数量。
⚠️:接收
例子:如图,只考虑P2:
// collectives/gatherv.c #include <mpi.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { MPI_Init(NULL, NULL); int world_size, wrank; MPI_Comm_size(MPI_COMM_WORLD, &world_size); MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int sendbuf[] = {wrank, wrank}; int sendcount = (wrank%2) + 1; //指定每个进程发送的消息个数 int *recvbuf, *recvcounts, *displs; int total = 6; if (wrank==2) { recvcounts = (int*) calloc(world_size, sizeof(int)); for (int i=0; i<world_size; ++i) recvcounts[i]= (i%2) + 1; //指定接收每个进程的消息的个数 displs = (int*) calloc(world_size, sizeof(int)); displs[0] = 0; for (int i=1; i<world_size; ++i) displs[i] = displs[i-1] + recvcounts[i-1]; recvbuf = (int*) calloc(total, sizeof(int)); //接收总缓存 }
MPI_Gatherv(sendbuf, sendcount, MPI_INT, recvbuf, recvcounts, displs, MPI_INT, 2, MPI_COMM_WORLD);//进程2接收 if (wrank==2) { for (int i=0; i<total; ++i) printf("%d ", recvbuf[i]); //打印出进程2接收到的所有信息 printf("\n"); free(recvcounts); free(displs); free(recvbuf); } MPI_Finalize(); return 0; }
$ mpirun -n 4 source
0 1 1 2 3 3.
八、收集 和有选择的发送 MPI_Alltoall
int MPI_Alltoall( const void *sendbuf, int sendcount, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype, MPI_Comm comm);
第i个进程将第j个数据集发送给第j个进程,将其放置在第i个地方
// collectives/alltoall.c #include <math.h> #include <mpi.h> #include <stdio.h> #include <stdlib.h> int main(int argc, char** argv) { MPI_Init(NULL, NULL); int world_size, wrank; MPI_Comm_size(MPI_COMM_WORLD, &world_size); MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int power = pow(10, wrank+1); int sendbuf[] = {power, power+1, power+2, power+3}; int recvbuf[] = {0, 0, 0, 0}; MPI_Alltoall(sendbuf, 1, MPI_INT, recvbuf, 1, MPI_INT, MPI_COMM_WORLD);
//不管buf多大,后面的sendcount指定了发给每个进程的数据量=1,接受量=1
printf("Rang %d, recvbuf", wrank); for (int i=0; i<world_size; ++i) //打印每个进程接收到的每个数据 printf(" %d", recvbuf[i]); printf(".\n"); MPI_Finalize(); return 0; }
Rang 0, recvbuf 10 100 1000 10000.
Rang 1, recvbuf 11 101 1001 10001.
Rang 2, recvbuf 12 102 1002 10002.
Rang 3, recvbuf 13 103 1003 10003.
进程0发送给别人的是:10,11,12,13
进程1发送给别人的是:100,101,102,103
进程2发送给别人的是:1000,1001,1002,1003
进程3发送给别人的是:10000,10001,10002,10003
进阶
一、平均分配
表扩展使用 MPI_Scatter
最优分配 MPI_Scatterv
次优分配 MPI_Scatterv
1. 表扩展使用 MPI_Scatter
如果我们希望尽可能平均的发送一个包含N个元素的tableT给P个进程;
- 如果P可以整除N,则我们只需使用MPI_Scatter:发送进程向每个进程发送N / P元素;
- 如果P不能整除N,则我们可以使用MPI_Scatterv。
- 在这两种情况下,我们都需要预先指定发送给每个进程的数量(MPI_Scatter 每个进程都一样的数目,MPI_Scatterv 发给不同进程不同数目)
设N'是可被P整除的大于N的最小整数。在C中,用整数除法,
N’ = (N/P + 1) * P;
我们可以将table T扩展为 T’, T’和T的前N个元素相同,后面的空补上无关紧要,不会影响结果的中性值,比如0(求和中) 和1(乘法中)
我们在T'上使用MPI_Scatter,并且向每个进程发送N'/ P个元素。
次优分配 使用 MPI_Scatterv
(N个元素,P个进程)
我们给每个进程(包括发送者)分配同样多信息,剩下的信息分配给单独的一个进程(一般是发送者) : N/P + N%P;
这个进程会比其他进程多1到(P-1)个元素
最优分配 使用MPI_Scatterv
选择N%P 个过程,通常是低等级的过程,以接收表中的N / P + 1个元素。 其他进程接收N / P元素。(好理解,举个例子23个元素,5个进程,可以试一下)
(相当于一个一个排,从头到尾发一遍,再返回来从头到尾发送,直到发完,有些发送了N/P+1,有的最后一轮没发到,就只有N/P个元素)
⚠️ 这种情况下不同进程之间的元素之差最大是1
二. 集体通信-减少
- Réductions réparties 分发-减少 : MPI_Reduce
- Réductions réparties 分发-减少 : MPI_Reduce, MPI_SUM
- Réductions réparties 分发-减少 : MPI_Reduce, MPI_MAXLOC
- Réductions réparties 分发-减少 : MPI_Allreduce
即由一堆元素 得到一个数值的结果,例如从多个进程求和,求最大值,最小值等。
int MPI_Reduce(const void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm); //发送缓存,接收缓存,发送数量,数据类型,reduction类型,接受者root,全局
如果发送方发送的是个表/数组,则接收方分别计算:
MPI_Op_create 和MPI_Op_free 函数允许您定义个人简化/减少操作。
主要减少操作预定义:
MPI_Reduce, MPI_SUM 某一个进程收到sum求和结果
// collectives/reduce_sum.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { MPI_Init(NULL, NULL); int wrank; MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int seeds[] = {wrank, 2*wrank}; int sums[] = {0, 0}; MPI_Reduce(&seeds, &sums, 2, MPI_INT, MPI_SUM, 2, MPI_COMM_WORLD); printf("Rang %d, sums %d %d.\n", wrank, sums[0], sums[1]); MPI_Finalize(); return 0; }
$ mpirun -n 4 source
Rang 1, sums 0 0.
Rang 0, sums 0 0.
Rang 2, sums 6 12.
Rang 3, sums 0 0.
MPI_Reduce, MPI_MAXLOC 某一个进程收到max结果
// collectives/reduce_maxloc.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { MPI_Init(NULL, NULL); int wrank; MPI_Comm_rank(MPI_COMM_WORLD, &wrank); struct { double val; int rank;
} in, out; in.val = 10*wrank; in.rank = wrank; MPI_Reduce(&in, &out, 1, MPI_DOUBLE_INT, MPI_MAXLOC, 2, MPI_COMM_WORLD); printf("Rang %d, out %f %d.\n", wrank, out.val, out.rank); MPI_Finalize(); return 0; }
⚠️上面的代码中: val 是第一列值,rank是第二列值 (这个程序可以:找出哪个进程拥有最大值)
$ mpirun -n 4 source
Rang 0, out 0.000000 0.
Rang 3, out 0.000000 0.
Rang 2, out 30.000000 3. //进程3拥有最大值30
Rang 1, out 0.000000 0.
⚠️ double_int 是MPI定义好的一种类型组合,还有其他组合可用:
MPI_FLOAT_INT,
MPI_LONG_INT,
MPI_DOUBLE_INT,
MPI_SHORT_INT,
MPI_2INT, //即两个int
MPI_LONG_DOUBLE_INT
MPI_Allreduce 每个人收到相同的reduction结果
int MPI_Allreduce(const void *sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, MPI_Comm comm);
// collectives/allreduce_sum.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) {
MPI_Init(NULL, NULL); int wrank; MPI_Comm_rank(MPI_COMM_WORLD, &wrank); int seeds[] = {wrank, 2*wrank}; //第一列值:rank int sums[] = {0, 0}; //第二列值:rank*2 MPI_Allreduce(&seeds, &sums, 2, MPI_INT, MPI_SUM, MPI_COMM_WORLD); //红色:数量和类型 allreduce无须指定接受者,所有热都接收
printf("Rang %d, sums %d %d.\n", wrank, sums[0], sums[1]); MPI_Finalize(); return 0; }
$ mpirun -n 4 source
Rang 1, sums 6 12.
Rang 0, sums 6 12.
Rang 2, sums 6 12.
Rang 3, sums 6 12.
今天写了太多内容,下一节讲: Modes de communications 通信模式
提供一个参考,很好理解。 https://blog.csdn.net/miaohongyu1/article/details/21093913