OpenMPI 学习笔记(四)集体通信


 

基础

内容提要:

  • Notions générales
  • Synchronisation globale : MPI_Barrier
  • Di􏰀usion générale : MPI_Bcast
  • Di􏰀usion sélective : MPI_Scatter
  • Collecte : MPI_Gather
  • Collecte générale : MPI_Allgather
  • Di􏰀usion : MPI_Scatterv
  • Collecte : MPI_Gatherv
  • Collectes et di􏰀usions sélectives : MPI_Alltoall

 

概念

集体通信允许在单个操作中进行一系列点对点通信。
集体沟通总是涉及指定沟通者的所有过程。
对于每一个过程,当参与集体交易完成时,通话以点对点通信的方式结束。
我们从不明确标注标签;系统透明地管理它们。优势:集体通信从不干扰点对点通信。

有三种类型的集体通信:

  • 全局同步:MPI_Barrier
  • 仅数据传输:
    • 广播式传送:MPI_Bcast
    • 数据的选择性分配传输:MPI_ScatterMPI_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

posted @ 2018-04-19 23:05  yanwenliqjl  阅读(2464)  评论(0编辑  收藏  举报