用户需要 MPI C++ 接口的哪些功能?

计算科学 C++ mpi
2021-12-17 20:18:32

MPI 标准的 3.0 版本正式删除了 C++ 接口(之前已弃用)。虽然实现可能仍然支持它,但 MPI-3 中的新功能没有 MPI 标准中定义的 C++ 接口。有关详细信息,请参阅http://blogs.cisco.com/performance/the-mpi-c-bindings-what-happened-and-why/

从 MPI 中删除 C++ 接口的动机是它与 C 接口相比没有显着价值。除了“s/_/::/g”之外,几乎没有什么区别,并且没有使用 C++ 用户习惯的许多功能(例如,通过模板自动确定类型)。

作为参加 MPI 论坛并与许多 C++ 项目合作的人,这些项目已经实现了自己的 C++ 接口到 MPI C 函数,我想知道 MPI 的 C++ 接口的理想特性是什么。虽然我什么都不做,但我有兴趣看到满足许多用户需求的独立 MPI C++ 接口的实现。

是的,我熟悉 Boost::MPI ( http://www.boost.org/doc/libs/1_54_0/doc/html/mpi.html ) 但它只支持 MPI-1 功能并且序列化模型是极难支持 RMA。

我喜欢的 MPI 的一个 C++ 接口是 Elemental(https://github.com/poulson/Elemental/blob/master/src/core/imports/mpi.cpp),所以也许人们可以提供一些优点和缺点方法。特别是,我认为 MpiMap 解决了一个基本问题。

编辑

为了回应不适合 StackExchange 的建设性反馈,请将此讨论移至GitHub 上的 MPI 论坛问题

4个回答

让我首先回答为什么我认为 MPI 的 C++ 接口通常并没有过于成功,在尝试决定我们是否应该只使用 MPI 的标准 C 绑定或构建更高级别的东西时考虑了这个问题很长时间:

当您查看现实世界的 MPI 代码(例如,PETSc,或者在我的情况下为 deal.II)时,会发现可能令人惊讶的是,MPI 调用的数量实际上并不是很大。例如,在 deal.II 的 500k 行中,只有大约 100 个 MPI 调用。这样做的结果是使用诸如 MPI C 绑定之类的较低级别接口所涉及的痛苦并不太大。相反,使用更高级别的接口不会获得那么多。

我的第二个观察是许多系统安装了多个 MPI 库(不同的 MPI 实现或不同的版本)。如果您想使用不只是由头文件组成的 boost::mpi,这会带来很大的困难:要么还需要多次安装此软件包,要么需要将其构建为使用 boost::mpi 的项目(但这本身又是一个问题,因为 boost 使用自己的构建系统,这与其他任何东西都不一样)。

所以我认为所有这些都与当前 MPI 的 C++ 接口相冲突:旧的 MPI C++ 绑定没有提供任何优势,并且外部包在现实世界中遇到了困难。

综上所述,这就是我认为我想从更高级别的界面中获得的杀手级功能:

  • 它应该是通用的。必须指定变量的数据类型显然不像 C++。当然,它也会导致错误。Elemental 的 MpiMap 类已经是很好的第一步(尽管我不明白为什么该MpiMap::type变量不是静态常量,因此可以在不创建对象的情况下访问它)。

  • 它应该具有流式传输任意数据类型的设施。

  • 需要MPI_Op参数的操作(例如,归约)应该与 C++ 的std::function接口很好地集成,以便只传递函数指针(或 lambda!)而不是笨拙地注册一些东西。

boost::mpi 实际上满足了所有这些。我认为如果它是一个只有标题的库,它在实践中会更受欢迎。如果它支持 MPI 1.0 后的功能也会有所帮助,但说实话:这涵盖了我们大部分时间所需的大部分内容。

为了让球滚动,这是我的两个需求:

  • 接口应该能够消除冗余或不必要的参数,例如 MPI_IN_PLACE。
  • 该接口应自动检测 Elemental 的 MpiMap 等内置数据类型。
  • 如果/可能的话,应该为类构建用户定义的数据类型。

我的列表没有特别的优先顺序。界面应该:

  • 仅是标头,没有任何依赖关系,但是<mpi.h>,和标准库,
  • 通用且可扩展,
  • 是非阻塞的(如果要阻塞,则显式阻塞,而不是默认阻塞),
  • 允许非阻塞操作的基于延续的链接,
  • 支持可扩展和高效的序列化(Boost.Fusion 之类的,这样它就可以与 RMA 一起使用),
  • 具有零抽象损失(即至少与 C 接口一样快),
  • 安全(调用未就绪的future的析构函数?-> std::terminate!),
  • 有一个强大的DEBUG模式,有大量的断言,
  • 非常类型安全(没有更多的 ints/void* ,我希望标签是类型!),
  • 它应该与 lambda 一起使用(例如,所有 reduce + lambda),
  • 始终使用异常作为错误报告和错误处理机制(不再有错误代码!不再有函数输出参数!),
  • MPI-IO 应该提供 Boost.AFIO 风格的非阻塞 I/O 接口,
  • 并且只需遵循良好的现代 C++ 接口设计实践(定义常规类型、非成员非友元函数、很好地使用移动语义、支持范围操作……)

附加功能:

  • 请允许我选择 MPI 环境的执行器,即它使用哪个线程池。现在,您可以同时拥有混合了 OpenMP、MPI、CUDA 和 TBB 的应用程序……每个运行时都认为它拥有环境,因此每次他们感觉时都向操作系统询问线程它。严重地?

  • 使用 STL(和 Boost)命名约定。为什么?每个 C++ 程序员都知道。

我想写这样的代码:

auto buffer = some_t{no_ranks};
auto future = gather(comm, root(comm), my_offsets, buffer)
              .then([&](){
                /* when the gather is finished, this lambda will 
                   execute at the root node, and perform an expensive operation
                   there asynchronously (compute data required for load 
                   redistribution) whose result is broadcasted to the rest 
                   of the communicator */
                return broadcast(comm, root(comm), buffer);
              }).then([&]() {
                /* when broadcast is finished, this lambda executes 
                   on all processes in the communicator, performing an expensive
                   operation asynchronously (redistribute the load, 
                   maybe using non-blocking point-to-point communication) */
                 return do_something_with(buffer);
              }).then([&](auto result) {
                 /* finally perform a reduction on the result to check
                    everything went fine */
                 return all_reduce(comm, root(comm), result, 
                                  [](auto acc, auto v) { return acc && v; }); 
              }).then([&](auto result) {
                  /* check the result at every process */
                  if (result) { return; /* we are done */ }
                  else {
                    root_only([](){ write_some_error_log(); });
                    throw some_exception;
                  }
              });

/* Here nothing has happened yet! */

/* ... lots and lots of unrelated code that can execute concurrently 
   and overlaps with communication ... */

/* When we now call future.get() we will block 
   on the whole chain (which might have finished by then!).
*/

future.get();

想一想如何使用 MPI_C 的requests 链接所有这些操作。您必须通过大量不相关的代码在多个(或每个)中间步骤进行测试,以查看是否可以在不阻塞的情况下推进您的链。

就我个人而言,出于 Wolfgang 提到的确切原因,我并不介意调用长 C 风格的函数;你需要调用它们的地方真的很少,即使那样,它们几乎总是被一些更高级别的代码包裹起来。

C 风格的 MPI 真正困扰我的唯一事情是自定义数据类型,并且在较小程度上是自定义操作(因为我不经常使用它们)。至于自定义数据类型,我想说一个好的 C++ 接口应该能够支持通用且有效的处理方式,很可能是通过序列化。这当然是已经采取的路线,boost.mpi如果您小心,可以节省大量时间。

至于boost.mpi有额外的依赖项(特别是boost.serialization它本身不是仅头文件),我最近遇到了一个名为谷物的仅头文件 C++ 序列化库,这似乎很有希望;授予它需要符合 C++11 的编译器。它可能值得研究并将其用作类似于boost.mpi.