MPI_Igather 是线程安全的吗?

Is MPI_Igather thread-safe?

我正在尝试启动一系列 MPI_Igather 调用(来自 MPI 4 的非阻塞集体),做一些工作,然后每当 Igather 完成时,对该数据做更多的工作。

工作正常,除非我在每个 MPI 等级上从不同的线程 启动 Igather。 在那种情况下,即使我调用 MPI_Init_thread 以确保提供了 MPI_THREAD_MULTIPLE,我也经常遇到死锁。 非阻塞集合没有匹配发送和接收的标签,但我认为这是由 MPI_Request 对象处理的 与每个集体行动相关联?

我发现失败的最简单示例是:

我做了这个程序的两个变体:igather_threaded.cpp(下面的代码),其行为如上所述,以及 igather_threaded_v2.cpp,它收集 MPI 等级 0 上的所有内容。这个版本不会死锁, 但数据也没有正确排序。

igather_threaded.cpp:

#include <iostream>
#include <memory>
#include <mpi.h>

#define PRINT_ARRAY(STR,PTR) \
    for (int r=0; r<nproc; r++){\
        if (rank==r){\
          for (int j=0; j<nproc; j++){\
            (STR) << (PTR)[j] << " ";\
          }\
          (STR) << std::endl << std::flush;\
        }\
        MPI_Barrier(MPI_COMM_WORLD);\
    }

int main(int argc, char* argv[]) {
    int provided;
    MPI_Init_thread(&argc,&argv, MPI_THREAD_MULTIPLE, &provided);
    if (provided!=MPI_THREAD_MULTIPLE){
        std::cerr << "Your MPI installation does not provide threading level MPI_THREAD_MULTIPLE, required for this program." << std::endl;
        MPI_Abort(MPI_COMM_WORLD, -1);
    }
    int rank, nproc;
    MPI_Comm_rank(MPI_COMM_WORLD,&rank);
    MPI_Comm_size(MPI_COMM_WORLD,&nproc);

    std::unique_ptr<double[]> M1(new double[nproc]);
    std::unique_ptr<double[]> M2(new double[nproc]);

    for (int j=0; j<nproc; j++) M1[j] = rank*nproc+j+1;

    PRINT_ARRAY(std::cout,M1);

    std::unique_ptr<MPI_Request[]> requests(new MPI_Request[nproc]);

    #pragma omp parallel for schedule(static) shared(requests)
    for (int j=0; j<nproc; j++){
        MPI_Igather(
            M1.get()+j,  1,  MPI_DOUBLE,
            M2.get(), 1, MPI_DOUBLE, j,
            MPI_COMM_WORLD, &requests[j]
        );
    }

    MPI_Waitall(nproc,requests.get(),MPI_STATUSES_IGNORE);

    if (rank==0) std::cout << " => "<<std::endl;
    PRINT_ARRAY(std::cout,M2);
    MPI_Finalize();
    return 0;
}

我的问题是:我的代码在理论上是否正确,这是 OpenMPI(此处使用 4.0.3)中的错误,还是我遗漏了任何不允许启动 MPI_Igather 调用的内容 多线程?我也用 Intel MPI 尝试过,结果相似。

为了编译,我使用 OpenMPI 4.0.3(配置为支持 MPI_THREAD_MULTIPLE)、gcc 11.2.0 和命令行

> mpicxx -std=c++17 -fopenmp -o igather_threaded igather_threaded.cpp

到运行,我用

mpirun -np 4 --bind-to none env OMP_NUM_THREADS=4 env OMP_PROC_BIND=false ./igather_threaded

您可能需要增加 MPI 等级数 and/or OpenMP 线程,and/or 运行 多次,因为确切的行为是不确定的。

在你的方案中,因为集合体是在不同的线程中启动的,所以它们可以按任意顺序启动。拥有不同的请求不足以消除它们的歧义:MPI 坚持它们在每个进程上以相同的顺序启动。

您可以通过以下方式解决此问题:

  #pragma omp parallel for schedule(static) shared(requests) ordered
  for (int j=0; j<nprocs; j++) {
    #pragma omp ordered
    MPI_Igather(M1.data()+j,  1,  MPI_DOUBLE,
                M2.data(), 1, MPI_DOUBLE, j,
                MPI_COMM_WORLD, &requests[j]);
  }

但这会带走您希望从线程中获得的大部分加速。

MPI 标准在 §6.12 中指出

All processes must call collective operations (blocking and nonblocking) in the same order per communicator.

两个具有不同根参数的Igather操作是不同的集合操作。您的线程在同一个通信器上没有任何同步地发出它们。所有进程的结果顺序可能不同。

一个解决方案是为每个线程使用不同的通信器(如果所有进程使用相同数量的线程)。这样您就可以在每个通信器上进行单一的集体操作,并且可以确保正确的线程在彼此之间交换数据。如果您多次重复该代码,只需预先创建它们并重新使用它们。