mpi_allgather 操作的计算成本与收集/分散操作相比如何?

计算科学 算法 复杂 mpi
2021-11-24 03:47:56

我正在研究一个可以通过使用单个 mpi_allgather 操作或一个 mpi_scatter 和一个 mpi_gather 操作来并行化的问题。这些操作是在一个while循环中调用的,所以它们可能会被调用很多次。

在使用 MPI_allgather 方案的实现中,我将分布式向量收集到所有进程中以进行重复矩阵求解。在另一个实现中,我将分布式向量收集到单个处理器(根节点)上,在该处理器上求解线性系统,然后将解向量分散回所有进程。

我很想知道 allgather 操作的成本是否明显高于 scatter 和 collect 操作的总和。消息的长度是否在其复杂性中起重要作用?它在 mpi 的实现之间是否有所不同?

编辑:

2个回答

首先,确切的答案取决于:(1) 用法,即函数输入参数,(2) MPI 实现质量和细节,以及(3) 您正在使用的硬件。通常,(2) 和 (3) 是相关的,例如当硬件供应商为其网络优化 MPI 时。

一般来说,融合 MPI 集合体更适合较小的消息,因为启动成本可能很重要,并且如果调用之间的计算时间存在差异,则应该最小化阻塞集合体所需要的同步。对于较大的消息,目标应该是尽量减少发送的数据量。

比如,理论上MPI_Reduce_scatter_block应该比MPI_Reduce后面好MPI_Scatter,虽然前者往往是按照后者来实现的,这样并没有真正的优势。在 MPI 的大多数实现中,实现质量和使用频率之间存在相关性,供应商显然优化了机器合同要求的那些功能。

另一方面,如果一个人在蓝色基因上,MPI_Reduce_scatter_block使用 using MPI_Allreduce,它比MPI_ReduceMPI_Scatter组合进行更多的通信,实际上要快得多。这是我最近发现的一个有趣的违反 MPI 中的性能自洽原则(该原则在“自洽 MPI 性能指南”中有更详细的描述)。

在 scatter+gather 与 allgather 的具体情况下,请考虑在前者中,所有数据必须进出单个进程,这使其成为瓶颈,而在 allgather 中,数据可以立即流入和流出所有秩,因为所有等级都有一些数据要发送给所有其他等级。但是,在某些网络上,一次从所有节点发送数据不一定是一个好主意。

最后,回答这个问题的最好方法是在你的代码中执行以下操作并通过实验来回答这个问题。

#ifdef TWO_MPI_CALLS_ARE_BETTER_THAN_ONE
  MPI_Scatter(..)
  MPI_Gather(..)
#else
  MPI_Allgather(..)
#endif

一个更好的选择是让您的代码在前两次迭代期间通过实验测量它,然后在剩余的迭代中使用更快的那个:

const int use_allgather = 1;
const int use_scatter_then_gather = 2;

int algorithm = 0;
double t0 = 0.0, t1 = 0.0, dt1 = 0.0, dt2 = 0.0;

while (..)
{
    if ( (iteration==0 && algorithm==0) || algorithm==use_scatter_then_gather )
    {
        t0 = MPI_Wtime();
        MPI_Scatter(..);
        MPI_Gather(..);
        t1 = MPI_Wtime();
        dt1 = t1-t0;
    } 
    else if ( (iteration==1 && algorithm==0) || algorithm==use_allgather)
    {
        t0 = MPI_Wtime();
        MPI_Allgather(..);
        t1 = MPI_Wtime();
        dt2 = t1-t0;
    }

    if (iteration==1)
    {
       dt2<dt1 ? algorithm=use_allgather : algorithm=use_scatter_then_gather;
    }
}

杰夫关于唯一确定的方法是测量绝对正确——毕竟我们是科学家,这是一个经验问题——并就如何实施此类测量提供了极好的建议。现在让我提出一个相反的(或者,也许是互补的)观点。

编写广泛使用的代码和将其调整到特定目的之间是有区别的。总的来说,我们正在做第一个 - 构建我们的代码,以便 a) 我们可以在各种平台上使用它,并且 b) 代码在未来几年内是可维护和可扩展的。但有时我们正在做另一件事——我们在一些大型机器上分配了一年的价值,我们正在加速进行一些所需的大型模拟集,我们需要一定的性能基线来完成我们需要完成的工作授予分配的时间。

当我们编写代码时,使其广泛可用和可维护比在特定机器上减少几个百分比的运行时间更重要。在这种情况下,正确的做法几乎总是使用最能描述您想要做什么的例程 - 这通常是您可以做出的最具体的调用,它可以满足您的需求。例如,如果一个直接的 allgather 或 allgatherv 做你想要的,你应该使用它而不是滚动你自己的 scatter/gatter 操作。原因是:

  • 代码现在更清楚地代表了您正在尝试做的事情,使第二年来到您的代码的下一个不知道代码应该做什么的人更容易理解(那个人很可能是您);
  • 对于这种更具体的情况,在 MPI 级别上可以进行优化,而不是在更一般的情况下,因此您的 MPI 库可以为您提供帮助;
  • 尝试自己动手可能会适得其反;即使它在具有 MPI 实现 Y.ZZ 的机器 X 上性能更好,但当您移动到另一台机器或升级 MPI 实现时,它的性能可能会更差。

在这种相当常见的情况下,如果您发现某些 MPI 集合在您的机器上运行异常缓慢,最好的办法是向 mpi 供应商提交错误报告;您不想使您自己的软件复杂化,试图在应用程序代码中解决应该在 MPI 库级别正确修复的问题。

然而如果您处于“调整”模式 - 您有一个工作代码,您必须在短时间内(例如,长达一年的分配)升级到非常大的规模,并且您已经分析了您的代码并发现代码的这个特定部分是一个瓶颈,然后开始执行这些非常具体的调整是有意义的。希望它们不会成为您代码的长期部分 - 理想情况下,这些更改将保留在存储库的某些特定于项目的分支中 - 但您可能需要这样做。在这种情况下,由预处理器指令区分的两种不同方法的编码,或针对特定通信模式的“自动调整”方法 - 可能很有意义。

所以我并不反对 Jeff,我只是想添加一些上下文,说明您何时应该足够关注此类相关性能问题以修改您的代码来处理它。