高性能计算之MPI——集合通信漫谈

高性能计算之MPI------集合通信漫谈 MPI集合通信漫谈

去年8、9月份,我写了一篇关于MPI的介绍文章《高性能计算之MPI--10分钟超越99%的程序员》。

该文章收到较多反馈与催更,因此抽空更新MPI系列第二篇帖子。## P2P的暴雷:集合通信

在上一期MPI介绍的时候,提到了Send(发送)和Recv(接收)。Send/Recv有个专门的名字:P2P。

这里的P2P可不是前些年在金融界暴雷的P2P借贷,虽然它们来源于同一个英文单词:Peer-To-Peer。

Peer-To-Peer,翻译成中文就是"点对点"。

这很容易理解,因为Send和Recv本身就是从一个进程把数据发送给另外一个进程,即从一个点到另外一个点。

上一期中我们假设了这样一个场景:进程0如果要把数据发送给其他三个进程:进程1、进程2和进程3。

这种情况就不单单是一个进程和另外一个进程之间的互操作了,而是一系列进程之间的互操作。

这种有别于P2P的、涉及到多个进程之间的通信叫做:集合通信(Collective Communication).## 集合通信:先从广播开始

对于上述情况,如果使用P2P的Send/Recv来表达,大概长下面这个样子。

/* File Name: mpi_naive_bast.c

the MPI bast example (naive vesion) */

#include <stdio.h>

#include <stdlib.h>

#include <mpi.h>

int main(int argc, char **argv) {

MPI_Init(NULL, NULL);

/* tag */

int tag = 10086;

int count = 1;

MPI_Status status;

int rank;

MPI_Comm_rank(MPI_COMM_WORLD, &rank);

if (rank == 0) {

int *sendbuff = (int *)malloc(sizeof(int) * count);

sendbuff0 = 100;

/* rank0 -> rank 1/2/3: sendbuff */

for (int recvrank = 1; recvrank < 4; recvrank++) {

MPI_Send(sendbuff, count, MPI_INT, recvrank, tag, MPI_COMM_WORLD);

}

}

if (rank != 0) {

int *recvbuff = (int *)malloc(sizeof(int) * count);

/* rank 1/2/3 receive buffer from rank 0 */

MPI_Recv(recvbuff, 1, MPI_INT, 0, tag, MPI_COMM_WORLD, &status);

printf("I'm rank #%d, recvbuf0 = %d \n", rank, recvbuff0);

}

MPI_Finalize();

return 0;

}

最终打印的结果是: I'm rank #1, recvbuf0 = 100 I'm rank #3, recvbuf0 = 100 I'm rank #2, recvbuf0 = 100 虽然达到了最终的目的,但这种naive的写法简直是老太婆的裹脚布------又臭又长。 事实上,MPI标准中提供了一个非常完美的集合通信接口来表示上述的操作,这个操作叫做Broadcast(广播) int MPI_Bcast(void buffer, int count, MPI_Datatype datatype, int root, MPI_Comm comm) 这些入参中buffer、count、datatype、comm的概念我们前面都提及过,重点说说这里的root的含义。 上述例子中,是进程0发给了所有进程,所以root=0. 如果是进程1发给其他进程,则root=1. 以此类推。 所以,Bcast中的root进程就是发起广播通信的进程。 话不多说,直接上代码吧。 / File Name: mpi_bcast.c the MPI Bcast example */ #include <stdio.h> #include <stdlib.h> #include <mpi.h> int main(int argc, char **argv) { MPI_Init(NULL, NULL);int count = 1; MPI_Status status; int rank;

MPI_Comm_rank(MPI_COMM_WORLD, &rank); int *buf = (int )malloc(sizeof(int) * count); if (rank == 0) { buf0 = 100; } / Before Broadcast, only rank 0 has buffer value: 100 / printf("I'm rank #%d, Before Bcast: buffer value: %d\n", rank, buf0); MPI_Bcast(buf, count, MPI_INT, 0, MPI_COMM_WORLD); / After Broadcast, all ranks has buffer value: 100 */ printf("I'm rank #%d, After Bcast: buffer value: %d\n", rank, buf0); MPI_Finalize(); return 0; }

打印结果如下: I'm rank #2, Before Bcast: buffer value: 439634824 I'm rank #3, Before Bcast: buffer value: 1004776328 I'm rank #0, Before Bcast: buffer value: 100 I'm rank #1, Before Bcast: buffer value: 752221064

I'm rank #0, After Bcast: buffer value: 100 I'm rank #2, After Bcast: buffer value: 100 I'm rank #1, After Bcast: buffer value: 100 I'm rank #3, After Bcast: buffer value: 100 可以看到:在调用Bcast之前,除了进程0之外,其他进程都是随机数。 经过Bcast之后,所有数字统一变成了100. 使用MPI_Bcast语义最显著的好处是:代码行数由原来的34行缩短到了24行,代码行数降低了29.4%,意味着开发效率直接提升29.4%。 不好意思,看到数字就想算个百分数,职业病犯了属于是。 显然,集合通信接口的诞生不仅是为了提升代码简洁度,而是为了提升代码效率。 或者更确切地说法是:将把优化工作由MPI的使用者抛给了计算机科学家。 至于为啥要这么做,我在本文的最后一章里会重点说明。 先回到代码效率提升这个问题,其实只要稍加分析,就明白:用Send/Recv描述出来的Bcast是个相当低效。 因为进程0在发送给进程1、2、3的同时,啥也没做。 为了更好地量化这个过程,假设一次发和收的时间为 t 在上面过程中,我们完成了: Step 1: rank 0 -> rank 1 Step 2: rank 0 -> rank 2 Step 3: rank 0 -> rank 3 三次的发包和收包的过程,所以耗时一共是 3t 既然大家发送和接受的信息是一样的,那么在进程1接受到进程0发的信息时,能不能用作发包呢?听上去这个主意不错,上面这个过程就改造成了: Step 1: rank 0 -> rank 1 Step 2: rank 0 -> rank 2 Step 3: rank 1 -> rank 3 虽说还是三步走,但刚才那个已经是"老三步", 现在我们要走的是"新三步".

"新三步"和"老三步"最大的不同是: Step2 和 Step3是可以并行执行的! 也就是说,rank 0->rank 2, rank 1->rank 3是可以同时进行的。 这样一来:Bcast总的耗时由原来 3t 的变成了 2t 时间缩短1/3,性能提升1/3啊。 不好意思,看到数字就想算个百分数的职业又犯了。

其实,无论是上述的"老三步"还是"新三步",都是MPI_Bcast的算法。 "老三步"的术语名称叫做:Flat-Tree,翻译成中文:平坦的树。

Flat Tree算法示意图,图片来源参考文献1. 刚才的例子中只有4个进程,有很多Tree算法都可以实现上面说的"新三步"的逻辑,比如:Binary Tree、K-Chain Tree等等。具体参考文献1. 文字太麻烦,不如直接上图:

不同Tree算法示意图,图片来源参考文献1.## 性能分析与测试基准

前面一章,提到了Bcast和实现Bcast的不同算法。

这里面天然地引入两个问题:

1、如何评估一个Bcast算法的好坏?

2、如何实测一个Bcast算法的好坏?

对于第一个问题,评价通信的具体模型很多,这里不做展开。

评价通信时间开销,最核心的是这样一个公式。

其中, T 表示通信总时间, α 表示通信时延(Latency), L 表示通信量, β 表示带宽(Bandwidth)。 这个公式是评价通信时间开销/通信复杂度最重要的公式,其地位与算法复杂度分析的大O模型相当。 这个公式表明,当通信量不大时(即 L 比较小时),时延是影响通信总耗时的关键因素;当通信量很大时(即 L 比较大时),带宽是影响通信总耗时的关键因素。 对于第二个问题,业界有很多测评MPI性能的Benchmark和测试集(Test Suite),参考文献2. 使用最多的是OSU Benchmark,官网参考: https://mvapich.cse.ohio-state.edu/benchmarks/ 冷知识:OSU是Ohio State University(俄亥俄州立大学)的简称。这是因为OSU benchmark是一位来自俄亥俄州立大学的教授Dhabaleswar Panda(熊猫教授)提出的。 使用次多的是IMB,是Intel MPI Benchmark的简称: https://www.intel.com/content/www/us/en/developer/articles/technical/intel-mpi-benchmarks.html 从名字上不难发现,这是Intel公司提出的一套测试集。## 集合通信の分类

除了Bcast外,还有那些集合通信语义呢?

有很多,比如Scatter、Gather、Allgather

从上图中可以看出: Scatter的含义是:进程0原来有一段数据,要把数据Scatter到其他进程。 Gather的含义则恰好相反------所有的进程把数据汇总到进程0中。 所以Gather和Scatter是互为逆操作。 观察一下,MPI_Allgather的操作可以看做是:Gather+Bcast. 即:

再比如,Reduce和Allreduce.

Reduce(规约)操作:就是把所有进程的求和(SUM)、乘积(Prod)、最大值(Max)或最小值(Min)从各个进程求解出来,最终结果放在进程0. 上图例子,进程0/1/2/3在Reduce前分别拥有的数字是5、2、7、4,在进过Reduce操作后,这四个数字的之和18就会放在进程0中。

可以看到,Allreduce相比Reduce规约操作,不同点在于:Reduce的结果只会放在进程0中,而allreduce的结果则会放在所有进程中。也就是说:

这些集合通信可以大概分为三类: · One-To-Many(一对多) · Many-To-One(多对一) · Many-To-Many(多对多) 其实分类的名字还挺形象的:如果集合通信前,原数据掌握在一个进程,集合通信结束后,需要的数据被所有进程都有,那就是One-To-Many。比如本篇重点介绍的Bcast,又比如刚才提到的Scatter和Reduce. 相反地,如果在集合通信前,数据在多个进程中; 而在集合通信结束后,需要的数据被唯一一个进程所拥有,那就是Many-To-One. 比如刚才介绍的Gather操作。 如果集合通信前后,数据都在所有进程中,那就是Many-To-Many,比如Allreduce、Allgather和Alltoall. 这里有个规律:凡是API接口属于"All字辈",基本都是Many-To-Many类型。## 一名HPC从业者的碎碎念

观前提示:如果你看这篇文章的目的只是单纯地想学习和了解MPI,那么这一章就可以跳过了。

如果你想听一个在HPC领域摸爬滚打了几年的人碎碎念,顺带给你讲讲私货,那么欢迎来到本章节。

前面章节在提到MPI集合通信接口的诞生原因时,我说了这么一句话:

MPI集合通信接口的诞生本质上是把优化工作由MPI的使用者抛给了计算机科学家。

这句话背后的逻辑是:使用MPI的人和优化MPI的人是两拨人。

真可谓"遍身罗绮者,不是养蚕人"。

前面的文章中我提到过,HPC这个领域最重要的是上层各式各样的应用。

这些应用通常是为了解决某一个或某一类科学问题,比如气象(天气预报)、物理学(高能物理、核物理)、医疗制药(分子结构模拟)、机械制造(计算流体力学)等等。

HPC应用通常都是谁写的呢?显然,都是一些科学家,或者科研工作者。

如果按照专业划分的话,这些人来自于大气科学系、物理系、化学系、机械系等等,可谓"百花齐放、百家争鸣"。

MPI(进程级并行)是这些HPC应用最常见的并行策略和并行方式;这些五花八门的人就是MPI的最大用户群体。

HPC程序开发完成后,对一个有追求的开发者而言。

他的第一个问题是:如何能让我的程序跑得快一点?

程序的性能优化是HPC应用开发者们不太擅长的,还是得学计算机的人来。

计算机专家做优化,往往需要理解应用里面的逻辑,可应用的逻辑往往不太好懂。

这里举个例子:计算物理学中有个重要的分支:计算电磁学(Computational Electromagnetics).

计算电磁学的核心是求解麦克斯韦方程组。

这个方程长这样:

咱们就先不说能不能理解方程的实质,单单要想弄懂上面那个倒着写的三角符的含义,最起码得学过微积分+矢量代数。

这还不算完。这些微分方程要变成可求解的方式需要一系列的算法。

在计算电磁领域,常用算法包括但限于:有限差分法、有限元方法、矩量法、快速多极子方法。

把这些算法理解完成后,才能把程序的架构设计出来,才具备写程序的现决条件。

我有位从事HPC应用优化且计算机科班出身的同事告诉我:他们去HPC相关的学术会议听报告,最怕听到应用的人讲报告,全是满屏的公式,连符号都看不懂。每次到了这些环节,都昏昏欲睡。

说了半天,我想表达的核心观点是:对从事性能优化的计算机专家来说,HPC应用的算法逻辑太复杂了。

这句话并非贬低学计算机的人。其实即使对同是物理专业的人,做计算电磁的和做流体力学仿真的两个子领域的人都不能相互理解。

这都不是"隔行如隔山",这属于"隔小行如隔山,隔行如隔大山"。

说回MPI,MPI集合通信接口的设计把MPI的使用者(HPC应用开发者)和MPI优化者(计算机科学家)划了一条清晰的分割线:对HPC应用开发者来说,他们可以轻松地使用这些接口,把这些优化工作交给更专业的计算机专家来做。

MPI是个把两拨人分开的很好的例子,但悲剧的是:在高性能计算领域,这样的例子并不算多。

在本系列的第一篇文章中,我就表达过一个观点:HPC这个领域,容易学起来上瘾。HPC这个领域还是太复杂了,需要懂的东西太多了。

知识一多,就容易学杂。

学杂了,一不小心把书架上的《母猪的产后护理》给抽出来了。

不抖机灵,送给大家一句话:学无止境。

与诸君共勉。

感谢阅读。这个专栏有没有下一期,就看各位的点赞、收藏和关注了。

你还想知道点儿啥,评论区告诉我。

(本文原载于知乎,知乎作者也是本人,原文链接: https://zhuanlan.zhihu.com/p/1899504956299534916

参考文献

1. EminNuriyev et al, Model-based selection of optimal MPI broadcast algorithms for multi-core clusters, Journal of Parallel and Distributed Computing. 2022.

2. Sascha Hunold and Alexandra Carpen-Amarie, MPI Benchmarking Revisited: Experimental Design and Reproducibility.

https://arxiv.org/pdf/1505.07734

3. Jan Thorbecke, Programming with MPI Collectives,

https://janth.home.xs4all.nl/MPIcourse/PDF/05_MPI_Collectives.pdf

4. MPI Tutorial: MPI Bcast and Collective Communication:

https://mpitutorial.com/tutorials/mpi-broadcast-and-collective-communication/

相关推荐
fiveym11 小时前
01 - iPXE + Clonezilla 网络装机原理解析
linux·运维·服务器·网络
虎头金猫11 小时前
如何在群晖NAS上通过Docker部署CloudSaver?群晖部署CloudSaver教程|聚合资源搜索并实现远程访问
运维·服务器·网络·python·docker·容器·pandas
dog25011 小时前
网络优化,还是降低各阶矩
服务器·网络·php
非凡ghost15 小时前
分区助手:Windows 磁盘管理的“瑞士军刀“,分区调整不再怕丢数据
服务器·windows·电脑·软件需求
许彰午16 小时前
# allbytaskid——一个接口扛六个DataStore
运维·服务器·php
吠品16 小时前
Java byte数组与String互转:编码细节与踩坑记录
java·linux·服务器
木白CPP16 小时前
Linux 驱动UART子系统源码分析
linux·运维·服务器·驱动开发·嵌入式硬件·开源软件
Titan202419 小时前
Linux网络基础知识
linux·服务器·网络·c++·学习
Titan202420 小时前
Linux网络学习:套接字、UDP的封装与应用
linux·服务器·开发语言·网络·c++