通讯: 点对点

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_Send和MPI_Isend,最基本的发送类型,会根据数据大小选择不同的发送方式。当数据长度小于规定阈值时,采用缓冲型;当数据较大时采用同步型。
  • 同步型(Synchronous):MPI_Ssend和MPI_Issend,需要先进行握手,建立链接后进行发送。
  • 立即型(Ready):MPI_Rsend和MPI_Irsend,不需要预先握手,只要有相关的接收命令,就可以发送。
  • 缓冲型(Buffer):MPI_Bsend和MPI_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);

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

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

Send-Receive

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

[cpp] view plaincopy
  1. /* Broadcast from processor 0 */
  2. if (rank == 0)
  3. a=999.999;
  4. MPI_Bcast(&a, 1, MPI_DOUBLE, 0, MPI_COMM_WORLD);
  5. 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. if (rank < size - 1) {  /* The last processor only receive, not send */
  10. 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. while(i <= size) {
  5. if (rank < i) { /*send*/
  6. if (rank + i < size){
  7. MPI_Isend(&a, 1, MPI_DOUBLE, (rank + i), 1, MPI_COMM_WORLD, &sendRequest);
  8. MPI_Wait(&sendRequest, &status);
  9. }
  10. }
  11. else if (rank <= (2 * i - 1)){ /*receive*/
  12. MPI_Irecv(&a, 1, MPI_DOUBLE, (rank - i), 1, MPI_COMM_WORLD, &recvRequest);
  13. MPI_Wait(&recvRequest, &status);
  14. printf("Processor %2d got %f from pocessor %2d \n", rank, a, status.MPI_SOURCE);
  15. }
  16. i *= 2;
  17. }

这段程序事实上就是 MPI_Bcast 的实现方式。

MPI_Scatter

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

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

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

P1 A1 A2 A3 A4
P2        
P3        
P4        
 
MPI_Scatter →
P1 A1      
P2 A2      
P3 A3      
P4 A4      

如果需要不等长度的分割,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处理器上面生效,函数作用为:

P1 A1      
P2 A2      
P3 A3      
P4 A4      
 
MPI_Gather →
P1 A1 A2 A3 A4
P2        
P3        
P4        

MPI_Allgather

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

P1 A1      
P2 A2      
P3 A3      
P4 A4      
 
MPI_Allgather →
P1 A1 A2 A3 A4
P2 A1 A2 A3 A4
P3 A1 A2 A3 A4
P4 A1 A2 A3 A4

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

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

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

MPI_Alltoall

这个函数的功能如下图:

P1 A1 B1 C1 D1
P2 A2 B2 C2 D2
P3 A3 B3 C3 D3
P4 A4 B4 C4 D4
 
MPI_Alltoall →
P1 A1 A2 A3 A4
P2 B1 B2 B3 B4
P3 C1 C2 C3 C4
P4 D1 D2 D3 D4

函数调用方法:

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);

这里 invec 和 inoutvec 都作为函数的输入变量进行二元运算(或者是二元数组运算),同时 inoutvec 又作为函数返回值,被二元运算结果重写。

MPI_Allreduce

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

MPI_Allreduce(sendbuf, recvbuf, count, datatype, op, comm);

并行计算(二)——通讯相关推荐

  1. Java 多线程初探(二) - 通讯与协调

    Java中线程共有5中状态: 新建:当创建一个线程对象时,新线程对象就处于新建状态,并获得除CPU外所需的资源. 就绪:当处于新建状态的线程被启动后,将进入线程队列等待CPU资源.这时它就具备了运行条 ...

  2. 通过Dapr实现一个简单的基于.net的微服务电商系统(二)——通讯框架讲解

    首先感谢张队@geffzhang公众号转发了上一篇文章,希望广大.neter多多推广dapr,让云原生更快更好的在.net这片土地上落地生根. 书接上回通过Dapr实现一个简单的基于.net的微服务电 ...

  3. Simotion应用与组网之二通讯篇

    1.上位机400 建立ProfNET总线,端口建立PN -等多条线路其中一条可通过交换机和PNPcoupler接入多个Simotion. 2.PNP 基本步骤是先PNP配置好flash进去,两端都要f ...

  4. R语言中的并行计算——二、搭建R的集群

    zhuanzai :http://blog.sina.com.cn/s/blog_83bb57b70101qeys.html 一直纠结于R的大数据计算问题,希望可以找到一个彻底的方案解决它.而云服务器 ...

  5. 通过Dapr实现一个简单的基于.net的微服务电商系统(十二)——istio+dapr构建多运行时服务网格...

    多运行时是一个非常新的概念.在 2020 年,Bilgin Ibryam 提出了 Multi-Runtime(多运行时)的理念,对基于 Sidecar 模式的各种产品形态进行了实践总结和理论升华.那到 ...

  6. 多串口服务器的DCS485多主机通讯

    1.概述 上海卓岚信息科技有限公司是一家专业提供工业物联网解决方案的高新技术企业,注册商标"ZLAN".公司研发的产品:物联网芯片.串口转以太网模块.多串口服务器.Modbus网关 ...

  7. 通过Dapr实现一个简单的基于.net的微服务电商系统(十九)——分布式事务之Saga模式...

    目录: 一.通过Dapr实现一个简单的基于.net的微服务电商系统 二.通过Dapr实现一个简单的基于.net的微服务电商系统(二)--通讯框架讲解 三.通过Dapr实现一个简单的基于.net的微服务 ...

  8. 通过Dapr实现一个简单的基于.net的微服务电商系统(十八)——服务保护之多级缓存...

    很久没有更新dapr系列了.今天带来的是一个小的组件集成,通过多级缓存框架来实现对服务的缓存保护,依旧是一个简易的演示以及对其设计原理思路的讲解,欢迎大家转发留言和star 目录: 一.通过Dapr实 ...

  9. 通过Dapr实现一个简单的基于.net的微服务电商系统(十七)——服务保护之动态配置与热重载...

    在上一篇文章里,我们通过注入sentinel component到apigateway实现了对下游服务的保护,不过受限于目前变更component需要人工的重新注入配置以及重启应用更新componen ...

  10. 通过Dapr实现一个简单的基于.net的微服务电商系统(十六)——dapr+sentinel中间件实现服务保护...

    dapr目前更新到了1.2版本,在之前4月份的时候来自阿里的开发工程师发起了一个dapr集成Alibaba Sentinel的提案,很快被社区加入到了1.2的里程碑中并且在1.2 release 相关 ...

最新文章

  1. Hadoop hdfs完全分布式搭建教程
  2. VSCODE安装必要的插件实现浏览器中打开,以及显示网址
  3. 2012年度IT博客大赛【星光评委】申请说明
  4. OpenCV文字绘制支持中文显示
  5. 洛谷 P1506 拯救oibh总部-dfs染色法
  6. 教育|我在美国读博士才发现,美国高等教育如此残酷,以前的感觉完全是扯淡...
  7. DataGrid与SQL Server 2000数据绑定
  8. ElasticSearch 5学习(2)——Kibana+X-Pack介绍使用(全)
  9. python菜鸟教程 pdf-菜鸟教程 python pdf/Python菜鸟教程怎么样
  10. android viewholder模式,为什么在ViewHolder模式中ViewHolder类应该是静态的?
  11. 计蒜客 青出于蓝胜于蓝 【DFS序 + 树状数组】
  12. numpy 之average
  13. 中央台“互联网时代”纪录片分集要点
  14. 免费抠图神器!五秒在线搞定抠图
  15. 上海税务局网站 环境检测 可信任站点未设置问题
  16. 钉钉 服务器 消息推送,【20210727 更新】 全能推送PushBot(原钉钉推送)支持企业微信,PushPlus,Bark...
  17. Python: pathlib基础用法
  18. 基于CPT构建网络,熟悉各层协议
  19. LOJ6482. LJJ 爱数数
  20. 从尾到头打印一个链表

热门文章

  1. java守护线程与用户线程_详解Java线程-守护线程与用户线程
  2. db h2 数据类型_H2数据库函数及数据类型概述-阿里云开发者社区
  3. python换算消费品价格,做折实
  4. 昆仑通态如何连接sqlserver数据库_sqlserver数据库怎么开启远程连接,给到别人访问...
  5. 创建的函数带有编译错误。_AST实现函数错误的自动上报(原理到实践)
  6. windows下python SSH-Client模块paramiko的安装与修改
  7. $_FILES['userfile']['error'] 错误码
  8. urtracker 项目管理工具
  9. 请教如何维护好iis服务器?
  10. opensips mysql 版本_Opensips-1.11版本安装过程