摘要
本文介绍Allreduce操作在MPI中的算法设计和实现,归纳和整理其发展脉络,追溯到最新的研究进展,并分析MPI的典型实现MPICH库中是怎样实现Allreduce例程的。
Allreduce介绍
Allreduce操作是MPI中最常用的集合通信操作,与之相似的是Reduce操作,假设有P个进程,每个进程都持有一个含N个元素的向量,所有的P个进程将自己的向量发送给根进程,根进程收集这些向量计算规约的结果(求和、求最大最小值等等),Reduce操作结果保存在根进程,Allreduce则将根进程的结果再广播出去。简单的在应用程序中调用MPI_Allreduce就可以完成上述例程,函数定义如下:
程序可以表示为:
1 | int MPI_Allreduce( |
MPI_Reduce和MPI_Allreduce例程的图解如下所示:
图片源网址和MPI_Allreduce的入门教程在这里:英文版和中文版
MPI_Allreduce广泛用于各种并行与分布式应用程序:科学计算、大数据、分布式机器学习、分布式深度学习DNN等等,并且已有工作表明MPI_Allreduce是使用频率和运行时间最长的集合通信操作。
自从MPI标准在1994年提出以来,MPI_Allreduce的相关研究从上世纪90年代就已经有很多了,而本文从2005年的一篇综述论文出发,一直追溯到现在,总结MPI_Allreduce的算法激情燃烧的昨天、老骥伏枥的今天和仰望星空过后的明天。
经典数据结构与算法
0、评估模型:
2005:Optimization of Collective Communication Operations in MPICH是一篇经典的MPI集合通信论文,介绍了常用的集合通信操作和对应的算法:Allgather, Broadcast, All-to-all, Reduce-Scatter, Reduce和Allreduce,并进一步的讨论了MPI_Reduce和MPI_Allreduce的优化。
这篇文章采用的性能评估模型是
也就是传输延迟L加上两端处理器的处理开销2o,和n个字节的传输时间,注意overhead是任意一个消息的处理开销,gap是连续两个字节的传输时间间隔。将上述式子变形,可以得到:
这也就是本文性能评估模型的公式,许多文献和教材中也用到了这个公式。我们将
下面深入讨论这篇论文介绍的集合通信算法。
1、Allgather操作及算法
Allgather操作的图解如下:
MPI_Allgather函数可以参考MPI_Allgather。
Ring Algorithm
MPICH中最初实现Allgather操作使用的就是Ring算法。
每一个进程i发送本地数据给进程
Recursive Doubling
递归加倍算法的流程如下图所示:
在第一步,彼此间距离为1的进程之间互相交换数据,数据量为
其中带宽项和环算法相同,这是因为:
这个等式的内在逻辑是任意一个进程总要接受来自其他p-1进程发送的总共
Bruck Algorithm
Bruck算法能够很好的处理进程数非2的幂次的情况,算法的执行步骤为
每个进程都有一片大小为n的缓存存放数据,在算法的开始,每个进程将本地数据拷贝到缓存的顶部。在第k步,进程i向目标进程
现在,所有进程都已经获得了全部数据,但是数据并不是以正确的顺序排列在缓存中:进程i中的所有数据块都向上偏移了i块。因此简单的将所有数据块循环向下移动i块就能将数据块调整到正确的位置上。算法的时间开销为:
Allgather操作的算法选取策略是:
- 当进程数量为2的幂并且发送短消息或者中等规模消息,采用Recursive doubling算法;
- 当发送短消息以及进程数量非2的幂的情况下,采用Bruck算法;
- 发送大消息,无论进程数量是多少,并且进程数量非2幂且发送中等规模消息,采用Ring算法。
2、Broadcast操作及算法
广播操作由根进程将根进程中的数据广播给所有进程,对应的是MPI_Bcast函数,可以参考MPI_Bcast
Bionomial Tree
MPICH中广播操作最初使用二项树算法。在第一步,根进程root向目标进程
Scatter + Allgather
这是一种组合算法,又叫Van de Geijn算法,将Scatter和Allgather两个操作组合成了Broadcast操作。Scatter(散播)操作与Broadcast操作的对比如下:
在该算法中,要广播的数据先分成若干份,散播到各个进程中,接着,散播的数据又收集到所有进程中,也就是再执行MPI_Allgather操作。其中Scatter操作使用二项树算法,时间消耗为:
时间消耗和Allgather递归加倍算法相同,仔细观察你会发现两者互为逆过程。而Allgather操作可以使用递归加倍算法或者环算法,总时间等于两者之和。
因此广播操作的二项树算法和Scatter+Allgather算法的时间消耗对比如下:
对比两个式子我们可以很容易得到:
- 当消息两较小(即n较小)或者进程数量少时(小于8),我们使用二项树算法;
- 当消息较大时或者进程数量较大时,我们采用Scatter + Allgather的组合算法。
3、Reduce-Scatter操作及其算法
Reduce-Scatter操作(多对多规约)是数据规约操作Reduce的一个变种,Reduce操作的结果保存在根进程中,而Reduce-Scatter将结果散发(Scatter)给所有进程。
二项树Reduce+线性Scatter
在MPICH中的老算法中,Reduce-Scatter操作先是将所有进程的数据通过二项树规约到0号进程,然后通过线性的散发操作将数据分发出去。二项树规约操作的时间为:
线性散发操作的时间为:
总的时间为:
Recursive Halving
递归减半算法和前面Allgather操作的递归加倍算法互为逆过程。
在第1步,进程分为2个子集,每一个进程都和与自己间隔
该算法能够正确执行的前提是规约操作是满足交换率的(commutative),满足交换律的规约操作使用频率更高,这是由于MPI定义的许多规约操作都是可交换的,例如MPI_SUM,MPI_MAX。如果进程的数量不是2的幂次,我们首先将进程的数量减少到2的幂次,具体做法是最开始的
Pairwise Exchange
成对交换算法适用于规约操作不满足交换律,其思想类似于Allgather操作递归加倍算法。在第1步,每一对邻居进程交换数据;第2步,彼此间距为2的进程交换数据;在第3步,彼此间距为4的进程交换数据,如此进行下去。然而它相较于Allgather操作,交换的数据更多。在第一步,进程交换除了自己所需要数据以外的所有数据(
该算法适用于传输的消息量小于256B的情况。对长消息发送(满足交换律的操作是
Tips:
- Commutative Operations(满足交换律的操作):MPI定义的数据归约操作包含sum、min、max、MinLoc、MaxLoc、(bitwise)OR、AND、XOR等等,其中有些是满足交换律的,有些不满足,
- Associative Operation(满足结合律的操作):浮点加法和乘法满足交换律但不满足结合律,因为
,例如 。 - Recursive Havling适合于满足交换律的操作,Recursive Doubling只适用于满足结合律的操作。
MPI_Reduce_scatter策略:
- 当操作满足交换律,消息
采用递归减半算法, 则采用(p-1)步的成对交换算法; - 操作不满足交换律,消息
时采用 步的成对交换算法, 时采用(p-1)步的成对交换算法。
4、Reduce操作及其算法
Bionomial Tree
MPICH中老算法采用二项树算法,执行
Reduce_scatter+Gather组合算法
该算法将Reduce-scatter和Gather两个操作组合成Reduce操作,也叫Rabenseifner算法。回顾广播操作的Scatter+Allgather组合算法,成功将二项树算法的
策略:
- 当消息量小(
)时,采用二项树算法; - 当消息量大(
)时,采用Reduce-scatter + Gather算法。
小结:看到这里也应该能摸索出一些规律,大消息发送时(n较大)我们要尽量较少带宽项,也就是减少
Ring Algorithm
和下面将要介绍的Ring Allreduce类似,使用Reduce-scatter + Gather的方式,但是Reduce-scatter只发送一部分数据(
5、Allreduce操作及其算法
Reduce+Broadcast
MPICH中老算法先将结果Reduce到根进程然后再将根进程的结果Broadcast到所有进程中。
Recursive Doubling
Allreduce的递归加倍算法和Allgather的递归加倍算法是非常相似的,只是每一步都伴随规约操作且交换的数据量也不同,每次进程间两两交换的数据量都是n。因此算法执行的时间为:
Reduce_scatter+Allgather
该算法也叫Rabenseifner算法。回顾Reduce操作我们采用了Reduce-scatter + Gather算法,这里我们在第二步将Gather换成了Allgather操作,采用Reduce-scatter + Allgather算法。算法的总开销为:
截至目前,上述Reduce操作的Reduce-scatter + Gather算法和Allgather操作的Reduce-scatter + Allgather算法,当进程的数量不是2的幂次的时候需要额外处理。移除
下图展示了带有13个进程的Allreduce操作算法的例子。输入向量和规约结果被分成了8个部分(A,B,…,H),因为8是小于且最接近13的2的幂次,用
Bionary Block Algorithm
叫做二方块算法。该算法能够降低进程数量非2幂时Reduce-scatter+Allgather算法的负载不均衡问题。以下图为例,在初始阶段对进程划分为若干块,使每一个块内进程子集的数量为2的方幂。每个块内部执行Reduce-scatter操作。然后,从最小的块开始,被划分为若干段做为更高一块的输入,更高的一块对收集过来的数据执行规约操作,如
在算法的第二阶段,是Allgather操作。上一个更大的块必须向小块发送数据,如图。
Ring Algorithm
环算法,其实就是Reduce-scatter+Allgather算法的变形,在Reduce-scatter阶段是各个进程直接将一部分数据发送到其目的节点,并且Allgather操作使用环算法来执行。
借用OpenMPI中的例子来解释Ring Allreduce算法。
假设有5个进程,则进程的输入数据分成5份,先进行Computation stage(也就是Reduce-scatter)然后是Distribution phase(也就是Allgather的环算法)。
该算法的执行时间为:
Allreduce选择最佳算法
在上面介绍的Allreduce的5种算法中根据进程数量和消息大小来选择不同算法,这张图是展示不同进程数量和消息大小对应的最佳算法(对MPI_DOUBLE型的数据进行MPI_SUM求和操作)。havling+doubling就是Reduce-scatter+Allgather算法。
这个是消息大小为32KB时对MPI_DOUBLE型的数据进行MPI_SUM求和操作不同算法的带宽。
策略:
- 对于短消息,使用Recursive Doubling算法;
- 对于长消息,先进行Reduce-scatter(Recursive-halving算法),再进行Allgather(Recursive Doubling算法)。
后面的三种方法只是进一步优化,没有在MPICH里集成。
Allreduce算法的评估和对比
常用的经典Allreduce算法的消耗评估模型:
Allreduce Algorithm | Cost Model | Efficient Bandwidth |
---|---|---|
Reduce + Broadcast | ||
Recursive Doubling | … | |
Reduce-Scatter + Allgather | … | |
Binary Block | … | … |
Ring Algorithm |
OpenMPI和MPICH中的Allreduce算法
1、OpenMPI-4.1.2的MPI_Allreduce实现
OpenMPI-4.1.2是最新版本的OpenMPI,算法的具体选择在ompi/mca/coll/tuned/coll_tuned_decision_fixed.c和ompi/mca/coll/tuned/coll_tuned_decision_dynamic.c文件里,用户可以指定规则以及选择使用的算法,并且OpenMPI使用了6种算法,分别是
1 | Algorithms: |
默认使用/coll_tuned_decision_fixed.c里的规则(固定算法选择规则),具体的选择方法如下(原代码是100多行的else-if,贼暴力):
除了默认的规则之外,用户还可以指定参数来选择对应的算法。
函数选择逻辑:
1 | //动态算法选择规则 |
2、MPICH-4.0.2的MPI_Allreduce实现
MPI应用程序在调用MPI_Allreduce时执行的主要算法、主要函数以及选择逻辑如下:
1 | /* |