利用迭代应用程序来提高分布式系统上基于任务的编程模型的可扩展性

《ACM Transactions on Architecture and Code Optimization》:Leveraging iterative applications to improve the scalability of task-based programming models on distributed systems

【字体: 时间:2025年11月07日 来源:ACM Transactions on Architecture and Code Optimization

编辑推荐:

  针对分布式任务模型中任务创建和依赖管理的瓶颈问题,提出基于taskiter的分布式迭代优化方案,通过预计算全循环任务图和MPI通信,消除单节点构建任务图的效率损失,显著提升大规模并行性能。实验表明,在128节点集群上,该方案较传统OmpSs-2@Cluster快7.7倍,接近异步TAMPI+OmpSs-2的性能,且代码量减少约50%。

  分布式任务模型,如OmpSs-2@Cluster、StarPU-MPI和PaRSEC,通过显式的任务依赖关系将高性能计算(HPC)应用程序表示为任务图。这些模型提供了一种统一的方式来表达并行性,涵盖CPU核心、加速器和分布式内存节点,相较于传统的MPI + X模型,显著提升了程序员的生产力。大多数基于任务的模型采用顺序构建任务图的方式,这种方式提供了清晰且熟悉的编程模型,简化了代码的开发、维护和移植。然而,这种设计引入了任务创建和依赖管理的瓶颈,限制了性能和可扩展性。因此,除非任务粒度非常粗,当前的分布式顺序任务模型无法达到MPI + X模型的性能水平。

许多科学应用具有迭代特性,每个时间步都会构建相同的有向无环任务图(DAG)。我们利用这一结构,消除顺序任务构建中的瓶颈,同时控制消息开销,而保持模型的简洁和生产力。我们的方法基于最近提出的taskiter指令,用于OpenMP和OmpSs-2。通过这种方式,一个迭代可以表示为一个循环图。运行时系统将循环图分配到各个节点,预计算所有MPI数据传输,并随后以低开销执行循环体。通过将MPI通信直接集成到应用程序的任务图中,我们的方法自然地重叠计算和通信,有时甚至能够暴露比传统的fork–join MPI + OpenMP模型更多的并行性。

我们定义了编程模型,并描述了完整的运行时实现,同时将该提案整合到OmpSs-2@Cluster中。我们使用五个基准测试,这些测试的规模可以扩展到MareNostrum 5超级计算机的128个节点。对于具有fork–join并行性的应用程序,我们的方法性能与fork–join MPI + OpenMP相似,使它成为一种可行的生产力替代方案,而现有的OmpSs-2@Cluster模型在128节点上比MPI + OpenMP慢7.7倍。对于2D Gauss–Seidel计算,我们的方法使得3D波前计算成为可能,其性能比fork–join MPI + OpenMP快22倍,与最先进的异步TAMPI + OmpSs-2模型相当。为了公平起见,这些应用程序都不是不平衡的,因此所有的性能提升都来自于减少开销以及任务模型暴露更多并行性的能力。我们预期,对于不平衡的应用程序,这种模型将带来更大的优势。所有软件,包括编译器、运行时系统和基准测试,均以开源形式发布。

本文的主要贡献包括以下几点:首先,我们提出了一种扩展OmpSs-2@Cluster的方法,以提高其对迭代应用的效率。其次,我们描述了运行时实现和相关优化。第三,我们在Nanos6@Cluster运行时系统中实现了该模型,并在MareNostrum 5超级计算机的128个节点上进行了实验评估。第四,我们证明了,尽管现有的OmpSs-2@Cluster方法仅适用于约16个节点,我们的方法在至少128个节点上与异步TAMPI + OmpSs-2模型表现相当。

本文的其余部分结构如下。第2节总结了相关的背景信息。第3节通过一个2D Gauss–Seidel热方程的实例来说明我们的动机。第4节描述了程序员的模型。第5节详细介绍了运行时的实现。第6节描述了评估方法。第7节展示了实验结果。第8节讨论了相关工作。第9节总结了本文的结论。

分布式任务模型,如OmpSs-2@Cluster、StarPU-MPI和PaRSEC,将应用程序表示为任务及其依赖关系。运行时系统构建任务图并调度任务在可用计算节点上并发执行。通过透明地管理数据分布和通信,这些模型减轻了许多与MPI+X模型相关的复杂性,如同步和潜在的死锁。此外,这些模型可以扩展以支持动态负载平衡和核心及节点级别的可塑性。

taskiter构造最近被提出用于OmpSs-2和OpenMP。任何循环都可以用taskiter注释,前提是(a)每个迭代生成相同的任务图和数据访问,以及(b)如果循环体外的代码仅执行一次,程序仍然有效。条件(a)仅适用于顶层任务图:每个顶层任务的迭代可以生成不同的子任务图,但无限制。如果编译器无法在循环开始时确定总迭代次数,它会插入一个特殊的任务,称为控制任务。控制任务依赖于当前迭代的所有子任务以及前一个迭代的控制任务。控制任务的主体检查循环的条件,并在条件为假时取消taskiter。当taskiter有unroll子句时,这些控制任务会以unrolling因子进行间隔,提供一种重叠不同迭代任务的方法。

Task-Aware MPI(TAMPI)是一个库,将阻塞和非阻塞MPI原语与基于任务的编程模型集成。它引入了一种新的MPI线程支持级别,称为MPI_TASK_MULTIPLE。请求该线程级别的应用程序可以在任务中使用阻塞MPI原语,而不会产生数据竞争或死锁的风险。现有的MPI_THREAD_MULTIPLE级别会导致阻塞MPI原语阻塞运行任务的底层线程。即使MPI实现避免忙等待并让硬件线程空闲,任务运行时系统也无法发现硬件线程的可用性。TAMPI使用PMPI接口拦截MPI调用,并将任何阻塞线程释放给运行时系统,以便执行其他任务,从而防止死锁。

TAMPI还通过使非阻塞MPI原语的完成对依赖系统可见,简化和优化了其使用。这通过新的TAMPI_Iwait和TAMPI_Iwaitall调用实现,如图4所示。图4中的任务在第2行发布了一个非阻塞MPI_Irecv。然后在第6行调用TAMPI_Iwait,这会通知TAMPI该MPI请求与后续任务的依赖相关(它包含对x的输出依赖)。TAMPI使用Nanos6外部事件API推迟当前任务依赖的释放。TAMPI_Iwait是非阻塞的,因此任务可以继续执行,立即完成并释放其数据结构和栈。后来,当MPI请求完成时,TAMPI会使用外部事件API释放任务的依赖,这不同于TAMPI的阻塞MPI接收,因为它不会解除并重新调度第一个任务。此时,图4中的第8行任务变为可执行状态。

本文的动机部分通过三种不同的Gauss–Seidel 2D热方程实现方式来说明,如图5所示:fork–join MPI + OpenMP、异步TAMPI + OpenMP/OmpSs-2和使用分布式taskiter的OmpSs-2@Cluster。基准测试是一个就地2D模板计算,它根据当前时间步的上方和左侧元素值以及前一时间步的右侧和下方元素值来更新每个元素。矩阵由NBY × NBX块组成,每个块的大小为BSY × BSX元素,按行分布。

图5(a)展示了使用fork–join并行性与MPI和OpenMP的实现。本地模板计算部分包含NBY_LOCAL行,包括顶部和底部的单行halos。为了在时间步中使用2D波前并行性更新所有元素,计算使用任务和依赖(图5(a)中的第29行到第38行)。并行性从时间步开始时为零,上升到最短维度块的最大数量,然后由于第39行的taskwait下降到零。

图5(b)展示了异步TAMPI + OpenMP的实现。这种版本消除了时间步之间的taskwait,并且由于3D波前并行性,其性能更高。并行性从第一个时间步开始时为零,直到接近最后一个时间步结束时保持最大值。代价是额外的复杂性,将发送和接收封装为任务,并使用TAMPI的非阻塞API。

图5(c)展示了使用taskiter的OmpSs-2@Cluster实现。尽管MPI + OpenMP和TAMPI + OpenMP/OmpSs-2版本的代码涉及数据分布和通信的协调与微管理,但OmpSs-2@Cluster版本仅引入了两处声明性的信息。首先,在第5行的nanos6_set_affinity调用描述了数据亲和性。该调用不会移动数据,而是提示运行时将矩阵的非只读行在各节点间循环分布。其次,对当前块上方和下方块的访问更精确,因为每个任务只读取这些块中的一行,而不是整个块。如果没有这个更改,过大的数据访问会误导运行时,迫使相邻节点之间交换整个块,每个块的大小为BSY × BSX元素。MPI + OpenMP和TAMPI + OpenMP/OmpSs-2中的显式MPI调用已经优化为交换单行数据,大小为BSX元素。OmpSs-2@Cluster版本的修改相对简单,因为碎片化区域依赖系统能够正确计算任务依赖,即使任务的数据访问方式重叠。

总体而言,使用分布式taskiter的OmpSs-2@Cluster版本的代码行数大约是TAMPI + OpenMP版本的三分之一,且几乎所有的代码都与实际的Gauss–Seidel计算相关。

分布式taskiter的程序员模型与第2.3节中描述的模型相同,除了与数据访问定义相关的规则。完整的数据访问定义:OmpSs-2@Cluster要求所有任务都有完整的数据访问规格,以便运行时系统可以编程任何必要的数据传输。这一要求也适用于taskiter,与SMP不同,其中taskiter只需在必要时提供数据访问以确保与兄弟任务的顺序。任何仅由子任务使用的数据应指定为弱访问(定义在第2.1节中)。任何在循环体或循环条件中使用的数据必须指定为强访问(即非弱访问)。

本文的实现部分通过图6展示了分布式taskiter的执行过程,图中从左到右展示了每个节点执行的步骤。该图旨在提供一个概述,而不是量化这些步骤的相对持续时间,这些时间并不按比例绘制。图6中的步骤①至⑥由每个节点执行。该方法可以利用任何分区算法,不需要固定或确定的方法,与StarPU-MPI和OmpSs@cloudFPGA不同。当前原型使用由node子句控制的静态分区。

图7更新了图3(c)中的示例程序,使用OmpSs-2@Cluster和分布式taskiter,并在每个任务上添加了node子句以指示将用于后续部分的分区。任务1和3在Rank 0上执行,任务2和4在Rank 1上执行,从而实现了跨两个节点的波前并行性。

图6的步骤③将完整的依赖图转换为当前进程的本地DCTG,并预计算涉及当前进程的MPI数据传输。这一步骤由所有节点并发执行,如图6所示。有两个子步骤:(1)插入通信任务和(2)创建本地DCTG。

插入通信任务:节点间的通信通过专用任务进行,以利用现有的任务图来协调通信和计算的顺序并重叠。发送任务具有对发送数据的in访问,因为MPI发送操作只需要读取数据的最新版本。相反,接收任务具有out访问,因为MPI接收操作会更新其缓冲区的新版本数据。通信任务的实现避免死锁,如第5.6节所述。

运行时算法用于添加发送和接收任务,如图8所示。它从顶层映射开始,这是一个已有的数据结构,将每个区域映射到第一个访问它的任务。该算法的复杂度为O(R),其中R是所有任务访问的区域总数。这个值对应于图8第8行的while循环迭代次数。当使用区域依赖系统时,需要额外的一次通过,以将顶层映射碎片化到最细的访问粒度。

图8中的算法为Rank 0的分区程序生成了输出。在图6的步骤①中创建的任务中,不会在Rank 0上执行的任务(即任务2和任务4)被灰掉了。我们假设变量的虚拟地址顺序为a、b、x、y。第3行的循环处理顶层映射中的每个区域,按虚拟地址顺序进行,这里从a开始。接下来,第8行的while循环考虑每个包含a的任务访问,从任务1的out访问开始。这是对a的第一个写入(第20行的lastWriter为none),因此第21行的initialReaders为空集:由于第一个访问是写入,不需要进行循环边到任务1的数据传输。下一个对同一区域的访问是任务2的in访问。该任务读取数据,但尚未在Rank 1上存在(第9行的a.access.rank ? readerRanks),因此需要从Rank 0(lastWriter)向Rank 1传输数据。由于当前节点是Rank 0,第13行创建了一个发送任务。按照相同程序,Rank 1创建了匹配的接收任务(第15行)。这完成了对a的访问,因此第3行的循环继续处理b,过程类似,只是当前节点Rank 0创建了一个接收任务。对于x的处理略有不同,因为对x的第一个访问是任务1的in访问,读取来自上一迭代未知的最后写入的值。因此,没有创建发送-接收对(第11行到第15行被跳过),但Rank 0被添加到initialReaderRanks中(第17行)。之后,当所有访问都被考虑时,第26行的循环会检查initialReader,即Rank 0。由于lastWriter也是Rank 0,不需要发送-接收对。最后,处理y时,第一个访问是任务0的in访问,循环会检查initialReader,即Rank 0。这次,由于lastWriter是Rank 1的任务4,Rank 0上没有该数据的副本,因此创建了接收任务以实现循环的读取后写入(第31行)。这也意味着当前节点的迭代0需要在taskiter之前拥有有效数据的副本。这通过在第32行将区域添加到initialValues中进行记录。

发送和接收任务的迭代是串行的,无论使用哪种算法执行任务。每个接收任务由于其out访问的写入后写入依赖而被串行。每个发送任务由于其发送任务(具有in访问)到下一个迭代的lastWriter(在发送任务创建时定义)的写入后读取依赖而被串行。因此,不同迭代中的同一数据的MPI发送和接收总是按顺序进行。MPI标签对于相应的发送和接收总是匹配,因为发送和接收节点在相同的任务图上遵循相同的确定性算法。MPI标签由mpiTag提供,它在第1行初始化为零,并在第16行和第33行递增。

图6的步骤③将完整的依赖图转换为本地DCTG。结果是一个标准的DAG,如示例程序所示,在图9(b)中。

步骤④从当前节点获取所有需要的输入数据。一旦图6的步骤③在当前节点完成,步骤④将获取所有taskiter所需的输入数据。需要获取的数据区域由图8算法计算的initialValues值确定。由于父taskiter必须覆盖其子任务的所有访问,这些数据区域的最新版本位置已经通过运行时的正常机制已知。同样,运行时使用正常机制检查是否需要数据传输,合并连续的数据传输并执行非阻塞MPI通信。

一旦图6的步骤④在当前节点完成,步骤⑤开始执行taskiter的全部迭代。通常不需要在步骤④和⑤之间设置全局屏障,但我们在这项实验中添加了一个,以便清晰地区分启动开销和每次迭代的时间。这个额外的屏障对总执行时间影响很小。

通信任务是普通的任务,只是其任务体由运行时系统实现,而不是用户代码。通信任务体简单地发布适当的非阻塞MPI发送或接收。运行时将依赖的释放延迟到后续任务,否则这些任务会立即发生。MPI请求的完成由同一个专门用于OmpSs-2@Cluster消息完成的线程定期测试。

如果迭代次数不是在循环执行前已知,那么会插入一个控制任务,类似于第2.3节中描述的方式。Rank 0上的控制任务继承taskiter的所有强访问,忽略弱访问。Rank 0上的控制任务还依赖于同一rank的上一个控制任务。其他rank上的控制任务仅依赖于Rank 0的控制任务,用于将条件值复制到所有其他rank。如果条件为假,每个rank的控制任务将取消taskiter的其余部分。如果taskiter有unroll子句,Rank 0的控制任务会以unrolling因子进行间隔,以支持不同迭代任务的重叠。

调度策略:运行时当前支持两种调度策略。迭代优先策略将早期迭代的任务赋予更高的优先级,确保在整个循环中保持一致的进展并维持高并行度直到完成。相反,立即后继策略优先考虑那些消耗最近完成任务写入数据的任务,这通常会改善数据局部性和缓存利用率。然而,如果DCTG是不连通的,某些部分的计算可能会提前完成,导致在计算结束时并行度降低。

一旦远程(非Rank 0)rank上执行的所有任务完成所有迭代,就会发送一个Task Finished消息到Rank 0。Rank 0在完成自己的迭代并收到所有其他节点的通知后,进入图6的步骤⑥,完成taskiter并释放其访问。此时,Rank 0会更新依赖系统中的数据位置,以反映执行最后写入的rank。如果没有任何任务执行写入操作,因为数据仅被读取,那么taskiter之前的数据位置保持不变。

本文的评估方法部分描述了实验平台和基准测试。实验在MareNostrum 5超级计算机的最多128个节点上进行。MareNostrum 5包含6408个计算节点,每个节点有两个56核的Intel Sapphire Rapids套件。我们使用正常的内存分区,包含6192个节点,每个节点有256 GB物理内存(每核2 GB)。互连是100 Gb/s的NVIDIA InfiniBand,具有胖树结构。所有基准测试和修改后的Nanos6@Cluster运行时系统均使用GCC 13.2.0进行编译。所有基准测试内核都位于单独的源文件中,对所有编程模型相同,并使用相同的编译器标志进行编译。运行时使用Intel MPI 2023.2,基准测试使用Intel MKL 2023.2提供的BLAS函数。所有实验使用一个MPI进程每套件,即每个节点两个MPI进程。每个数据点展示了在不同批次作业中进行的五次运行的平均值和标准差,我们确认这些批次作业具有不同的节点分配。为了确保公平性,所有编程模型使用相同的批次作业和节点分配进行测试。

基准测试列于表1中。所有基准测试均使用一个进程每套件(即每节点两个进程)进行,这是根据之前的工作确定的,因为这种配置可以提高NUMA局部性,从而带来更稳定和高效的性能。强扩展性结果报告了2到128个节点的性能,利用最多14,336个核心和256个MPI进程(禁用超线程)。每个基准测试都有可调节的块大小,决定任务粒度,结果基于达到最高每迭代性能的块大小。

表1的最后一列给出了在足够内存运行基准测试的最小节点数(2个节点对于multi-matvec和heat-gauss,其他则为4个节点)上的每迭代时间。对于multi-matmul,每迭代时间为82.9秒,其他基准测试在最小配置下的每迭代时间均小于0.5秒。这为任何分布式任务系统的强扩展性测试提供了挑战。

multi-matvec是一个重复的密集双精度矩阵-向量乘法序列,矩阵按行分布且迭代之间无依赖。它具有细粒度任务,复杂度为O(n2),且无节点间数据传输。multi-matmul是一个密集双精度矩阵-矩阵乘法序列,具有更大的O(n3)任务,且同样无节点间数据传输。Jacobi是一个密集双精度Jacobi求解器,用于严格对角占优系统。它等价于将向量反复乘以密集平方矩阵。它与multi-matvec相同的复杂度,但具有所有对所有通信模式,使其成为fork–join并行性的特别适合。heat-gauss是第3节中讨论的2D热方程模板计算的Gauss–Seidel变体。它表现出2D或3D波前并行性,并具有重叠不同迭代任务的潜力,使其成为异步任务并行性的良好匹配。heat-jacobi是相同的2D热方程,使用Jacobi更新。该版本有两个工作数组和每个时间步内更新数组的尴尬并行计算。它非常适合fork–join并行性。

图10展示了强扩展性的总体结果。五行图表对应于第6.2节中描述的五个基准测试。在所有图表中,x轴是节点数,总是每个节点有两个MPI进程;即每个套件一个进程。左侧图表列给出了初始开销(y轴为秒),与迭代次数无关,右侧图表列给出了每次迭代的性能(y轴为TFLOPS/s),排除初始开销。这种方法提供了一种独立于任意目标迭代次数的总体行为概述。要查看迭代次数对总性能的影响,包括开销,作为迭代次数的函数,请参阅第7.2节。

图10(a)展示了multi-matvec的开销,它在128个节点上的最大值为40.0秒。这个开销对应于第5节中描述的步骤①至④和步骤⑥。如第5.4.1节和第7.3节的实证分析所示,开销随着总访问次数大致线性增长。对于这个基准测试,最优块大小导致每个核心上的任务数量较少。由于每个任务具有固定数量的访问,总访问次数理论上应随着节点数线性增长。实际上,由于实验噪声和其他因素,最优块大小无法完全符合预期行为,导致图表中结果的可变性。

图10(b)展示了multi-matvec每次迭代的吞吐量。fork-join MPI + OpenMP和distributed taskiter的Immediate Successor版本以及TAMPI + OmpSs-2的性能和扩展性相当。在128个节点上,distributed taskiter版本比其他两个版本快8.9%,这是由于较低的任务管理开销。OpenMP的调度策略是实现定义的,但我们通过Paraver追踪验证了,对于这个基准测试,OpenMP遵循与Immediate Successor相似的调度。由于这个基准测试具有断开的任务图,因为输出向量的元素来自完全独立的计算,Immediate Successor和Iteration Priority策略之间存在较大差异。Iteration Priority策略在128个节点上性能比Immediate Successor低35%。这是因为尽管Iteration Priority可靠地实现了直到循环结束的全并行性,但数据局部性较差导致缓存利用率较低。原始的OmpSs-2@Cluster版本只能扩展到16个节点,且在32个节点上比fork–join MPI慢2倍。由于multi-matvec基准测试没有节点间通信,OmpSs-2@Cluster的扩展性差是由于任务卸载和依赖管理的控制消息开销,而分布式taskiter方法完全消除了这一开销。

图10(c)展示了multi-matmul的初始开销,其在128个节点上的最大值约为0.2秒。同样,开销预计会随着节点数大致线性增长,但图显示了高度的可变性。如图10(d)所示,分布式taskiter的性能非常接近最先进的TAMPI + OmpSs-2实现,两种调度策略均如此。由于任务调度策略的差异,MPI + OpenMP的结果不太稳定,导致在64个节点以下的性能更高,但在128个节点上的性能略有降低。原始的OmpSs-2@Cluster版本在64个节点上性能比分布式taskiter低约50%,之后无法进一步扩展性能。

图10(e)显示了jacobi的初始开销,从2到64个节点大致呈二次增长。这个基准测试具有所有对所有通信,因此任务数量和每个任务的访问次数都随着核心数线性增长,它们的组合导致了二次增长。在64个节点时,我们遇到了MKL库的异常行为,其在小块大小时表现出降级的dgemv性能,因此128个节点的最优块大小与64个节点相同。这解释了为什么在64个节点时二次开销中断。128个节点的最大开销为60秒。

图10(f)显示,分布式taskiter版本在最多64个节点上与fork–join MPI版本匹配,误差在16.8%以内,但在128个节点上下降到29.9%。有两个原因。首先,fork–join MPI版本使用集体通信,而分布式taskiter始终使用点对点通信。其次,分布式taskiter版本在任务内部使用异步通信,而不是fork–join并行性。在这种情况下,我们看到fork–join MPI + OmpSs-2与fork–join MPI + OpenMP在所有节点上匹配,误差在128个节点上为3.9%。对于这个分布式taskiter基准测试,任务内部的通信导致MPI库中的竞争。我们看到结果在64个节点以内与异步TAMPI + OmpSs-2版本非常接近,误差在7.8%以内,而在超过64个节点时,性能比TAMPI + OmpSs-2高出30.5%。未来的工作可能会研究实现集体通信以及使用非阻塞集体MPI通信的方法,尽管这样做可能会导致不必要的进程同步,这取决于MPI实现。无论如何,结果已经大大优于原始的OmpSs-2@Cluster版本。

图10(g)展示了heat-gauss的开销。由于3D波前并行性,使用最小块大小以保持可接受的性能开销是最有效的。因此,开销大致恒定,从2个节点的1.8秒增长到128个节点的2.6秒。图10(h)显示,fork–join MPI + OpenMP版本的性能较差,受限于每个时间步内的2D波前并行性。通过启用3D波前并行性,异步TAMPI + OmpSs-2版本的性能显著提高,在128个节点上比fork–join MPI + OpenMP快20.0倍。分布式taskiter版本的性能与之相当,在128个节点上比fork–join MPI + OpenMP快24.0倍。相比之下,OmpSs-2@Cluster的实现性能甚至比fork–join MPI + OpenMP差,比其慢7.0倍。

图10(i)展示了heat-jacobi的开销,这是2D热方程的Jacobi版本。每个迭代是尴尬并行的,最优块大小在1到128个节点之间大致恒定。开销几乎恒定,增长到128个节点时的最大值略高于3.0秒。最后,在图10(j)中,除了原始的OmpSs-2@Cluster实现外,所有版本的性能扩展至少到128个节点。分布式taskiter版本的性能与fork–join MPI版本相比,在128个节点上误差为7.7%。与分布式taskiter不同,原始的OmpSs-2@Cluster版本在每次迭代中重新创建任务,导致内存消耗过多,使其在该基准测试中超出内存限制,因此未包含在图表中。

总体而言,这些结果表明分布式taskiter的初始开销是可以接受的,最大为1.1秒。其中大部分时间用于图6的步骤③中的图转换,如第5.4节所述。该步骤由我们的实现在一个线程上执行,但可以轻松地在每个进程的56个线程上并行化。每次迭代的性能与fork–join MPI + OpenMP匹配或超过,误差在16.8%到29.9%之间,对于jacobi基准测试在64和128个节点上。对于heat-gauss,受益于异步通信,性能与异步TAMPI + OmpSs-2版本相似,比fork–join MPI + OpenMP快20.0倍。在四个基准测试中,分布式taskiter的性能远超原始的OmpSs-2@Cluster版本,后者在128个节点上比fork–join MPI + OpenMP慢7.7倍。

图11展示了循环迭代次数对整体性能的影响。五个图表对应于在最多5分钟执行时间中运行的五个基准测试。x轴是迭代次数,以对数刻度表示。y轴是整体性能,以TFLOPS/s表示,这与图10的右侧列不同,它包含了初始开销。这些结果是新的,而不是通过将图10的左侧和右侧列组合估计的。

我们看到,包括启动开销,分布式taskiter(Iteration Priority)版本在multi-matmul、heat-gauss和heat-jacobi基准测试中,从第一个迭代开始就超过了原始的OmpSs-2@Cluster版本。对于jacobi基准测试,分布式taskiter(Iteration Priority)版本在10到100次迭代时超过了原始版本,而分布式taskiter(Immediate Successor)版本在100到1000次迭代时超过了multi-matvec版本。分布式taskiter(Immediate Successor)版本的渐近性能与fork–join MPI + OpenMP和fork–join MPI + OmpSs-2(Immediate Successor)版本相似,但经过10^4次迭代后。对于multi-matvec基准测试,由于任务内部的通信导致MPI库中的竞争,我们看到结果在10^4次迭代时与异步TAMPI + OmpSs-2版本非常接近,误差在7.8%以内,而在超过10^4次迭代时,性能比TAMPI + OmpSs-2高出30.5%。未来的工作可能会研究实现集体通信以及使用非阻塞集体MPI通信的方法,尽管这样做可能会导致不必要的进程同步,这取决于MPI实现。无论如何,结果已经大大优于原始的OmpSs-2@Cluster版本。

图11(g)展示了heat-gauss的开销。由于3D波前并行性,使用最小块大小以保持可接受的性能开销是最有效的。因此,开销大致恒定,从2个节点的1.8秒增长到128个节点的2.6秒。图11(h)显示,fork–join MPI + OpenMP版本的性能较差,受限于每个时间步内的2D波前并行性。通过启用3D波前并行性,异步TAMPI + OmpSs-2版本的性能显著提高,在128个节点上比fork–join MPI + OpenMP快20.0倍。分布式taskiter版本的性能与之相似,在128个节点上比fork–join MPI + OpenMP快24.0倍。相比之下,OmpSs-2@Cluster的实现性能甚至比fork–join MPI + OpenMP差,比其慢7.0倍。

图11(i)展示了heat-jacobi的开销,这是2D热方程的Jacobi版本。每个迭代是尴尬并行的,最优块大小在1到128个节点之间大致恒定。开销几乎恒定,增长到128个节点时的最大值略高于3.0秒。最后,在图11(j)中,除了原始的OmpSs-2@Cluster实现外,所有版本的性能扩展至少到128个节点。分布式taskiter版本的性能与fork–join MPI版本的每次迭代性能相比,在128个节点上误差为7.7%。与分布式taskiter不同,原始的OmpSs-2@Cluster版本在每次迭代中重新创建任务,导致内存消耗过多,使其在该基准测试中超出内存限制,因此未包含在图表中。

总体而言,这些结果表明分布式taskiter的初始开销是可以接受的,最大为1.1秒。其中大部分时间用于图6的步骤③中的图转换,如第5.4节所述。该步骤由我们的实现在一个线程上执行,但可以轻松地在每个进程的56个线程上并行化。每次迭代的性能与fork–join MPI + OpenMP匹配或超过,误差在16.8%到29.9%之间,对于jacobi基准测试在64和128个节点上。对于heat-gauss,受益于异步通信,性能与异步TAMPI + OmpSs-2版本相似,比fork–join MPI + OpenMP快20.0倍。在四个基准测试中,分布式taskiter的性能远超原始的OmpSs-2@Cluster版本,后者在128个节点上比fork–join MPI + OpenMP慢7.7倍。

图12展示了初始开销与任务访问区域数量之间的关系。任务访问区域的数量定义在第5.4.1节中,是所有任务访问区域的总和,记为R。数据点展示了所有基准测试、节点数和块大小的扫描(不仅仅是第7.1节中使用的最优块大小)。基准测试之间存在一个小的影响,特别是对于jacobi,它具有所有对所有通信,每个访问的通信量高于平均。然而,我们看到在四个数量级范围内,访问数量与初始开销之间存在强烈线性关系,仅有几个异常值,这证明了第5.4.1节中给出的O(R)复杂度的理论正确性。

图13展示了访问数量如何依赖于块大小。x轴是块大小,y轴是单个节点上执行的访问数量。对于multi-matvec和multi-matmul,它们的访问数量相同,因为它们具有相同的访问数量。访问数量是任务数量的常数倍,而任务数量与块大小成反比。其他基准测试的访问数量与块大小的平方成反比。对于jacobi,这是因为在块大小减少时,任务数量和每个任务的访问数量均成反比。对于heat-gauss和heat-jacobi,每个任务的访问数量固定,而任务数量与块大小成反比。

本文的结论部分指出,尽管分布式顺序任务模型(STF)非常具有生产力,但对于细粒度和中等粒度的任务,其性能和可扩展性受到限制。本文提出了一种扩展OmpSs-2@Cluster的方法,以提高其对具有常见迭代模式的应用程序的效率,同时保持OmpSs-2@Cluster的编程模型。尽管现有的OmpSs-2@Cluster实现仅适用于约16个节点,而我们的方法在至少128个节点上与异步TAMPI + OmpSs-2模型表现相当。对于具有重叠迭代潜力的应用程序,例如2D热方程的Gauss–Seidel更新计算,我们的方法比fork–join MPI + OpenMP暴露了显著更多的并行性。这导致在128个节点上性能提高24.0倍,与最先进的异步TAMPI + OmpSs-2模型相当。因此,该模型结合了STF模型的生产力与最先进的MPI+X方法的性能,通过利用科学应用程序的迭代特性。它还避免了MPI+X方法中的同步和死锁问题。未来的工作将在该基础上进行,包括自动分区、启动时的优化以及潜在的使用集体通信,以及用于动态负载平衡和可塑性的重新分区。
相关新闻
生物通微信公众号
微信
新浪微博
  • 急聘职位
  • 高薪职位

知名企业招聘

热点排行

    今日动态 | 人才市场 | 新技术专栏 | 中国科学人 | 云展台 | BioHot | 云讲堂直播 | 会展中心 | 特价专栏 | 技术快讯 | 免费试用

    版权所有 生物通

    Copyright© eBiotrade.com, All Rights Reserved

    联系信箱:

    粤ICP备09063491号