并行计算(二)——通讯

来源:互联网 发布:淘宝申请售后是多少天 编辑:程序博客网 时间:2024/04/28 16:50

通讯: 点对点

MPI的通讯是指程序在不同的处理器之间进行数据交换的一种行为,通讯方式按照目标的不同主要分为两类:点对点通讯和集群通讯。 点对点通讯需要一个处理器进行发送,另外一个处理器进行接收。

Message

要了解MPI的通讯,首先需要了解一下MPI中Message的结构。Message主要包含数据(3个参数),封包(3个参数)以及其他的一些与通讯有关的参数

其中,数据主要包括:

  • 数据指针(datapointer): 数据或数组的第一个元素的地址;
  • 数据长度(count): 当前类型元素的个数(注意,不一定是数组元素个数,尤其是使用自定义类型时);
  • 数据类型(datatype): 数据类型(可以是内置类型(如MPI_INT,MPI_DOUBLE),也可以是自定义类型(Derived Data Type))。

封包:

  • 通讯目标(dest): 目标处理器的rank,当目标不确定时可以使用MPI_ANY_SOURCE;
  • 封包标志(tag): 用于标识Message,只有具有相同tag的发送和接收函数才能进行通讯;
  • 通讯器(communicator): 当前通讯的系统,MPI中默认的通讯系统为MPI_COMM_WORLD。但多数情况下会根据传输方向和内容自定义一些子系统,尤其是在集群通讯中。

Message中通常还会带有关于通讯状态的结构体变量: MPI_Status。 它包含了三个重要成员:

  • MPI_SOURCE: Message源在当前通讯系统中的rank;
  • MPI_TAG: 通讯标识;
  • MPI_ERROR: 通讯中的错误信息。

这些参数都可以用.运算取得,有了这些参数在任何情况下,处理器都能清楚的知道数据发送源的信息。

Send

点对点通讯共有八种发送方式,包括四种阻塞式发送(blocking)MPI_Send,MPI_Ssend,MPI_Rsend,MPI_Bsend和四种非阻塞式(non-blocking)MPI_Isend,MPI_Issend,MPI_Irsend,MPI_Ibsend。阻塞式发送是指在发送指令执行后到数据传输结束之前,当前处理器处于阻塞状态,不会进行任何其他的运算。相对的,非阻塞模式则是在传输过程中,处理器还可以进行其他运算,因此需要额外的函数判断通讯是否结束,例如MPI_Test 和MPI_Wait。 MPI的Send函数调用方式通常为:

MPI_Send(datapointer, count, datatype, dest, tag, comm);MPI_Isend(datapointer, count, datatype, dest, tag, comm, request);

此外,还可以按照发送类型把他们分成如下四类:

  • 标准型(Standard):MPI_SendMPI_Isend,最基本的发送类型,会根据数据大小选择不同的发送方式。当数据长度小于规定阈值时,采用缓冲型;当数据较大时采用同步型。
  • 同步型(Synchronous):MPI_SsendMPI_Issend,需要先进行握手,建立链接后进行发送。
  • 立即型(Ready):MPI_RsendMPI_Irsend,不需要预先握手,只要有相关的接收命令,就可以发送。
  • 缓冲型(Buffer):MPI_BsendMPI_Ibsend,需要用户通过MPI_Buffer_attach(buffer, buflen);手动创建一个缓冲区,而后将数据通过MPI_Bsend(data,count,type,dest,tag,comm);将数据发送到缓冲区,系统会在完成握手后自动将缓冲区中的数据发送出去。

这是一段完整的使用缓冲型发送函数的程序,注意在声明缓冲区长度时需要附加一个MPI的内建变量MPI_BSEND_OVERHEAD,它代表了整个Message中其他参数所需存储空间的最大值。

[cpp] view plaincopy
  1. int buflen=totlen*sizeof(double)+MPI_BSEND_OVERHEAD;  
  2. double *buffer=malloc(buflen);  
  3. MPI_Buffer_attach(buffer,buflen);  
  4. MPI_Bsend(data,count,type,dest,tag,comm);  

Receive

不同与发送函数的各种形式,接收函数只有阻塞式MPI_Recv和非阻塞式MPI_Irecv两种。接收函数的调用方式通常为:

MPI_Recv(data, count, datatype, source, tag, comm, status);MPI_Irecv(data, count, datatype, source, tag, comm, request);

这里面的statusrequest分别是MPI_StatusMPI_Request类型变量的地址,MPI_Status已经在前文提到过了,这里就只说一下MPI_Request。这个变量是用来表明当前通讯请求状态的,不论是非阻塞的发送还是接收,都需要将这个变量放入MPI_WaitMPI_Test中判断传输是否结束,从而进行下一步操作或者结束程序。

注意:通常情况下,对于在同一个处理的的发送和接收函数会采用不同的MPI_Request变量,以免相互覆盖影响。


Send-Receive

点对点通讯中还包括了使用一个函数进行发送接收的MPI_SendrecvMPI_Sendrecv_replace,他们的调用方式如下:

MPI_Sendrecv(sendbuf, sendcount, sendtype, dest, sendtag, recvbuf, recvcount, recvtype, source, recvtag, comm, status);MPI_Sendrecv_replace(buff, count, datatype, dest, sendtag, source, recvtag, comm, status);

这两个函数主要是用在一连串阻塞传输中,因为在一连串的阻塞发送和阻塞接收过程中,发送和接收的顺序需要格外注意,以免造成锁死的现象。而这两个函数会自动避免锁死状况的出现,会降低编程的难度。 这两个函数的区别是第一个函数需要两个缓冲区,而第二个函数只需要一个。


单边通讯

MPI还提供了一类单边通讯的函数:MPI_GetMPI_Put。它们采用异步方式对其他处理器的内存空间进行直接读写。


通讯:集群

集群通讯是指将一个定义好的处理器集群作为一个整体,在其中进行消息的传递。这种通讯方式通常是阻塞式的,需要在集群中的所有处理器都参与进来,并在完成操作后才能进行下一步的运算。集群通讯相比点对点更加智能,会最大限度的发挥并行处理的好处。

MPI_Barrier

在集群通讯中,最重要的问题就是同步,尽管多数的集群通讯函数都会自动进行同步,但是MPI还是提供了一个手动同步的函数int MPI_Barrier(MPI_Comm Comm)。这个函数的功能是让Comm集群中的所有处理器都运行到MPI_Barrier后才可以继续进行后续的运算。作为同步整个通讯集群的的函数,MPI_Barrier的主要用途是在手动debug和评估算法。

MPI_Barrier(Comm);time = MPI_Wtime();/* 运行算法程序 */MPI_Barrier(Comm);time = MPI_Wtime() - time;

MPI_Bcast

MPI_Bcast(buffer, count, datatype, root, comm);这个函数的主要功能是在整个集群中进行广播,将rootbuffer的内容(长度为count)传播给comm集群中其他的处理器。下面的两段程序分别使用了MPI_Bcast和点对点通讯,实现的功能是完全一样的。需要注意的是,所有的处理器都是调用相同的MPI_Bcast函数以及参数,尤其是保持root参数都是同一个处理器的编号。

[cpp] view plaincopy
  1. /* Broadcast from processor 0 */  
  2. if (rank == 0)                 
  3.     a=999.999;  
  4.   
  5. MPI_Bcast(&a, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);                           
  6. printf("Processor %2d received %f be broadcast from processor 0\n",rank ,a);  

[cpp] view plaincopy
  1. /* Send from processor 0 */  
  2. if (rank == 0) {  
  3.     a=999.999;  
  4.     MPI_Send(&a, 1, MPI_DOUBLE, (rank + 1), 111, MPI_COMM_WORLD);  
  5. }  
  6. else {  
  7.     MPI_Recv(&a, 1, MPI_DOUBLE, (rank - 1), 111, MPI_COMM_WORLD, &status);  
  8.         printf("Processor %d got %f from processor %d\n", rank, a, status.MPI_SOURCE);  
  9.   
  10.         if (rank < size - 1) {  /* The last processor only receive, not send */  
  11.         MPI_Send(&a, 1, MPI_DOUBLE, (rank + 1), 111, MPI_COMM_WORLD);  
但是,这两种方法的效率是不一样的:上面的点对点通讯方式的效率与处理器数量p呈线性关系,而MPI_Bcast则是log2(p)。事实上,对点对点通讯进行优化,也可以达到对数关系。下面的程序采用了点对点的非阻断通讯模式

[cpp] view plaincopy
  1. i = 1;  
  2. if (rank == 0)  
  3.     a = 999.999;  
  4.   
  5. while(i <= size) {  
  6.     if (rank < i) { /*send*/  
  7.         if (rank + i < size){  
  8.             MPI_Isend(&a, 1, MPI_DOUBLE, (rank + i), 1, MPI_COMM_WORLD, &sendRequest);  
  9.             MPI_Wait(&sendRequest, &status);  
  10.         }  
  11.     }  
  12.     else if (rank <= (2 * i - 1)){ /*receive*/  
  13.         MPI_Irecv(&a, 1, MPI_DOUBLE, (rank - i), 1, MPI_COMM_WORLD, &recvRequest);  
  14.         MPI_Wait(&recvRequest, &status);  
  15.         printf("Processor %2d got %f from pocessor %2d \n", rank, a, status.MPI_SOURCE);  
  16.     }  
  17.     i *= 2;  
  18. }  
这段程序事实上就是MPI_Bcast的实现方式。

MPI_Scatter

这个函数是专门用来等长度的均匀分割数组,并按顺序的分配的集群中的每个处理器中。调用方式为:

MPI_Scatter(sendbuff, sendcount, sendtype, recvcount, recvtype, root, comm);

其中,sendbuff只有在root处理器上才有效,这个函数的作用如下所示

P1A1A2A3A4P2    P3    P4     MPI_Scatter →P1A1   P2A2   P3A3   P4A4   


如果需要不等长度的分割,MPI还提供了一个函数MPI_Scatterv(sendbuff, sendcounts, displs, sendtype, recvbuf, recvcount, recvtype, root, comm)。其中,sendcounts是一个整型数组,表示发送给各个处理器的数据长度;displs也是整型数组,分别表示了每个发送数据在sendbuff中的起始位置;sendtype则存储了每一个发送数据的数据类型。其余的参数与MPI_Scatter基本一致。

MPI_Gather

MPI_Scatter正好相反,MPI_Gather是将集群中所有处理器的数据整合到root上,调用方式与为MPI_Gather完全一样:

MPI_Gather(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, root, comm);

不同的是recvbuf只有在root处理器上面生效,函数作用为:

P1A1   P2A2   P3A3   P4A4    MPI_Gather →P1A1A2A3A4P2    P3    P4    


MPI_Allgather

这个函数的目的是将分散在各个处理器上的数据收集起来,并在每个处理器上都留有一个拷贝,如图:

P1A1   P2A2   P3A3   P4A4    MPI_Allgather →P1A1A2A3A4P2A1A2A3A4P3A1A2A3A4P4A1A2A3A4


与之前类似,这个函数的调用方式:

MPI_Allgather(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, comm);

通常情况下这个函数相当于MPI_Gather+MPI_Bcast

MPI_Alltoall

这个函数的功能如下图:

P1A1B1C1D1P2A2B2C2D2P3A3B3C3D3P4A4B4C4D4 MPI_Alltoall →P1A1A2A3A4P2B1B2B3B4P3C1C2C3C4P4D1D2D3D4

函数调用方法:

MPI_Alltoall(sendbuf, sendcount, sendtype, recvbuf, recvcount, recvtype, comm);

这个函数的主要用途就是在于运算矩阵转置和FFT。


MPI_Reduce

这个函数在并行计算中非常非常非常重要,它提供了一系列将分散数据集中的方法,如求和,求积,求最大最小值等,通常情况下这些运算也会内在的考虑到算法复杂度的问题,会尽量的降低运算时间复杂度。。这个函数的调用方式为:

MPI_Reduce(sendbuf, recvbuf, count, datatype. op, root, comm);

op以外,其他参数应该都很熟悉了,这里就不多说。op指的是还原操作,例如MPI_SUM,MPI_PROD,MPI_MAX,MPI_MIN等预定义操作,同时,用户也可以通过MPI_Op_create(user_fn, commute, op)自定义操作,这其中user_fn是用户自定义的函数指针;commute是一个整型数,当它为真的时候(非0)表明自定义函数的两个输入可以交换位置,即遵守交换率,当它为假时(等于0),则不遵守。
对于用户自定义函数,需要遵守下面的函数原型规则:

typedef void MPI_User_function(void* invec, void* inoutvec, int *len, MPI_Datatype *datatype);
这里invecinoutvec都作为函数的输入变量进行二元运算(或者是二元数组运算),同时inoutvec又作为函数返回值,被二元运算结果重写。

MPI_Allreduce

不但进行还原运算,还会将结果分发给集群的全部处理器。

MPI_Allreduce(sendbuf, recvbuf, count, datatype, op, comm);
原创粉丝点击