在之前 博客帖子 我谈到了使用任务并行编程模型来提高多核计算节点的性能和系统利用率的机会。 任务分配所面临的主要挑战是不同计算线程之间的负载不平衡,以及叉连接并行模型有效利用并发执行的局限性。 结果表明,在来自不同供应商的多个处理器中,使用任务分配共享内存代码具有更好的缩放和性能。 性能改进在10%到20%之间,执行时间降低了35。 这些优化导致更快的模拟周转时间,加速了全球高性能计算(HPC)用户的科学进步。
该研究的重点是共享内存并行执行在一个节点的计算使用 OpenMP。 开放MP是HPC中共享内存并行和加速器卸载的第一并行编程模型。 然而,大型超级计算机不是单一的共享内存系统,而是一组计算节点,每个节点都有自己的内存,通过高带宽、低延迟网络连接。 消息传递是这种分布式内存系统的首选编程范例。 消息传递接口 MPI是HPC分布式内存系统的主要并行编程模型。
大多数科学和工程应用都使用纯MPI策略并行化,其中来自参与计算节点的每个计算线程都工作在总域问题的子域中。 在MPI的上下文中,每个计算线程被称为MPI进程或秩。 将一个大域划分为分布在不同等级之间的较小子域的技术称为域分解。 通常,这种分区需要在域空间中作为邻居的行列之间交换子域边界数据。 在这种情况下,相邻的等级通过使用MPI接口发送和接收边界数据来交换消息。 然而,纯MPI策略不是利用节点内并行性的最佳选择。 应用程序对负载不平衡变得更加敏感,因此重叠应用程序阶段和线程变得复杂。 它们使用不必要的显式消息进行通信,而不是使用共享内存空间。
之间最近的协作工作 巴塞罗那超级计算中心 and Arm Research 报告的经验,任务的自适应网格细化代码从 美国Exascale计算项目在开放MP和MPI级别。 本文发表了实现共享内存和分布式内存库的互操作性的性能结果和收获“ 面向自适应网格细化应用的数据流并行化 ”他说 IEEECluster 2020会议。 本文详细介绍了任务分配方法,该方法利用自动全叠负载平衡和通信计算重叠来实现更好的缩放、更高的系统利用率、效率和性能。
MPI开放MP
在深入研究先进的编程技术之前,除了纯MPI方法外,我们还需要介绍科学代码中使用的基本编程策略。 最常见的替代方案是混合并行编程。 将MPI和Open MP结合起来,使大规模的科学代码并行化,为开发两个世界的最佳代码提供了一个机会,同时减轻了它们的弱点。 混合MPI Open MP应用程序创建一组MPI级别,然后每个级别都可以执行一组OpenMP线程。
通常,科学应用具有迭代算法。 迭代通常在模拟中执行一个时间步骤,其特征是对数据进行操作的计算部分,以及一个通信部分,其中等级交换下一次迭代的更新数据。 通常,在混合MPI开放MP代码中,计算部分具有所有MPI级别中的所有OpenMP线程,通信部分具有MPI级别传递消息。 通信部分通常由主线程串行执行(下图中用蓝色显示)。 这种简单的方法通常在混合编程的上下文中被命名为fork-join。
MPI具有完全并行和固有的局部性优势。 MPI应用程序中的所有级别从初始化到执行结束独立运行。 它们在数据的私有分区或副本上工作,从而防止不必要的共享数据问题。 另一方面,Open MP本质上是串行的,并且只在并行部分上打开并行性,这些并行部分处理共享数据。 它还可能受到远程缓存效果和一致性工件的影响,例如错误共享。 开放处理共享数据的MP具有避免数据复制以进行消息传递的优点,因为所有线程都可以访问数据的单个副本。 将这两种方法结合起来,允许包含MPI级别,以利用合并消息传递模型在分布式内存系统中进行通信,每个级别运行OpenMP轻量级线程,利用共享数据,减少了总体数据复制需求。
程序员肯定可以按照类似的方案使用Open MP编程。 它们可以自始至终具有完整的并发执行,并在仍然访问共享数据的同时跨线程分发工作。 然而,不幸的是,采用自下而上的方法与Open MP并行是一种常见的做法:并行单个循环并将串行部分保持在中间。 这就规定了所规定的比例限制 Amdahl的法律.
混合应用程序中等级的常见配置是每个计算节点一个等级,或每个非Uniform内存访问(NUMA)节点一个等级。 在MPI级别中打开MP线程,通过共享内存空间中的共享数据结构隐式通信,而不是交换MPI消息。 利用每个NUMA节点的一个秩通常会提高数据的局部性,因为给定秩中的线程访问相同的NUMA节点的内存。 从不同的NUMA节点访问内存会在线程之间带来显著的内存延迟差异,从而导致不平衡的场景。
MPI和Open MP之间的互操作性
这种混合模型提供了这两种模型的优点,但在表中留下了机会。 异步传输(例如,MPI_Isend/MPI_Irecv)等特性通过允许一些通信和计算重叠来提供混合模型的一些好处。 然而,具有全局同步的fork-join模型(如图1中绿色所示)限制了计算-通信重叠的数量,并允许在不同级别的不同迭代中执行的重叠。 为了脱离fork-join模型,并允许开发更高级别的并行性,以及任务分配提供的异步计算和通信,MPI和OpenMP库需要一起工作。
这种互操作性今天不存在。 两个库相互独立工作,两者之间的编排由程序员负责。 在当前的MPI和OpenMP标准中,在并发任务中执行MPI通信操作(例如,并行交换子域边界的任务)是两者 危险的 and 不称职的.
一方面,从并发任务中调用阻塞MPI函数是不安全的。 注意,阻塞MPI操作会阻塞MPI库内的当前线程,直到操作完成。 图2说明了这个问题。 我们假设一个混合应用程序具有两个MPI等级:一个实例化多个并发任务以发送不同的数据块,另一个实例化相同数量的并发任务以接收数据。 我们还假设它们调用常见的阻塞MPI_Send和MPI_Recv方法来发送和接收每个块,并且每个块数据消息都被标记为其块标识符。
如果通信任务的数量大于可以运行任务的OpenMP线程的数量,则程序可能挂起,在这种情况下,OpenMP线程的数量是每个级别两个(每个核心一个。 这是因为通信任务是并发的,所以OpenMP调度程序可以根据调度策略和执行情况自由地决定它们的执行顺序。 由于不能保证两个级别的执行顺序相同,运行中的任务可能试图交换一组不同的块。 这将阻塞MPI库中两个级别的Open MP线程,从而引发死锁情况。 请注意,当OpenMP线程在MPI库中阻塞时,OpenMP线程调度程序无法知道线程已被阻塞,因此无法在该核心上调度另一个OpenMP线程。 因此,核心不能同时执行其他“准备”通信任务。
图2:缺乏MPI开放的MP可操作性可能导致MPI调用任务的死锁
另一方面,从任务中发布MPI操作通常是低效的。 通信任务需要人工数据依赖,以定义所有级别的相同执行顺序,并防止以前的死锁情况。 非阻塞MPI操作(例如,MPI_Irecv)的执行,它启动操作并返回一个MPI请求,以检查其稍后的完成情况,很难管理内部任务。 用户将负责手动检查MPI请求,在大多数情况下导致算法效率低下。
任务-软件MPI(TAMPI)库
The 任务-软件MPI(TAMPI)库 目的是克服所有这些限制,允许安全和高效地执行阻塞和非阻塞MPI操作,从任务内部,在开放MP和 OmpSs-2 任务型模特。 在调用阻塞MPI函数的任务(例如,MPI_Recv)的情况下,库暂停任务,直到操作完成,允许其他“就绪”任务同时在该核心上执行。 该库还为所有非阻塞MPI函数(例如TAMPI_Irecv)定义了TAMPI变体)。 这些函数是非阻塞和异步的,将调用任务的完成绑定到它们所表示的相应的非阻塞操作的最终确定(例如,MPI_Irecv)。 该函数立即返回,以便即使MPI操作尚未完成,任务也可以完成其执行。 当任务执行完成时,任务被认为是完成的,所有挂起的MPI操作都完成了。
图3:HPC软件堆栈与MPI和开放MP互操作性通过TAMPI。
我们在下面的代码中展示了如何使用TAMPI支持进行非阻塞操作的示例。 程序同时接收并使用任务并行处理多个整数。 第一个任务是接收机,它调用TAMPI_Irecv函数开始接收操作。 这使得任务完成取决于接收操作的最终完成。 注意,它声明了对用于接收数据的缓冲区的输出依赖(即数据将写入缓冲区)。 当操作仍在进行时,TAMPI函数可能会立即返回,因此缓冲区不能在那里被消耗。 相反,我们可以在下面的后续任务中使用它,该任务将缓冲区作为输入依赖项。 这样,当MPI操作最终完成时,TAMPI库将透明地完成接收任务并满足消费者任务的输入依赖。 这将最终运行以消耗接收到的数据。 这样,TAMPI库允许开发人员与多个任务并行执行高效和安全的通信。
int recvdata[N]; MPI_Status status[N]; for (int i = 0; i < N; ++i) { #pragma omp task out(recvdata[i]) out(status[i]) { int tag = i; TAMPI_Irecv(&recvdata[i], 1, MPI_INT, 0, tag, MPI_COMM_WORLD, &status[i]); // non-blocking and asynchronous // recvdata cannot be accessed yet } #pragma omp task in(recvdata[i]) in(status[i]) { check_status(&status[i]); consume_data(&recvdata[i]); } } #pragma omp taskwait
通过利用OpenMP或OmpSS-2等任务分配模型和TAMPI库,我们可以对大多数应用程序进行有效的任务化,包括计算和通信部分。 这导致计算和通信的有效重叠,这是任务分配模型固有的。 然后,开发人员可以集中精力公开他们的应用程序的并行性,而不是担心低级方面,例如任务发布的MPI操作的处理,这些操作隐藏在TAMPI中。 这种策略还可以通过任务化高级函数来实现自上而下的并行化策略,而不是在叉接方法中看到的低效的自下而上策略。
运用我们的方法
到目前为止,我们已经探讨了MPI和OpenMP之间缺乏互操作性所带来的问题,以及它如何阻碍MPI级别的任务分配。 我们还讨论了提供在TAMPI中实现的这种互操作性的建议。 在里面 这个博客的第二部分我们研究了如何将所提出的方法应用于自适应网格细化应用。 由此产生的代码使用任务跨MPI和开放MP与重要的加速高达12288核心。
审核编辑 黄昊宇
全部0条评论
快来发表一下你的评论吧 !