21

According to this website, the usage of MPI::COMM_WORLD.Send(...) is thread safe. However in my application I often (not always) run into deadlocks or get segmentation faults. Enclosing each call of MPI::COMM_WORLD methods with a mutex.lock() and mutex.unlock() consistently removes deadlocks as well as segfaults.

This is how I create threads:

const auto communicator = std::make_shared<Communicator>();
std::vector<std::future<size_t>> handles;
for ( size_t i = 0; i < n; ++i )
{
   handles.push_back(std::async(std::launch::async, foo, communicator));
}
for ( size_t i = 0; i < n; ++i )
{
   handles[i].get();
}

Communicator is a class which has a std::mutex member and exclusively calls methods such as MPI::COMM_WORLD.Send() and MPI::COMM_WORLD.Recv(). I do not use any other methods of sending/receiving with MPI. foo takes a const std::shared_ptr<Commmunicator> & as argument.

My question: Is the thread safety promised by MPI not compatible with threads created by std::async?

4

2 回答 2

29

MPI 中的线程安全不是开箱即用的。首先,您必须确保您的实现实际上支持同时进行 MPI 调用的多个线程。对于某些 MPI 实现,例如 Open MPI,这需要在构建时使用特殊选项配置库。然后你必须告诉 MPI 在适当的线程支持级别进行初始化。目前 MPI 标准定义了四个级别的线程支持:

  • MPI_THREAD_SINGLE- 表示用户代码是单线程的。如果使用,这是初始化 MPI 的默认级别MPI_Init()
  • MPI_THREAD_FUNNELED- 表示用户代码是多线程的,但只有主线程进行 MPI 调用。主线程是初始化 MPI 库的线程;
  • MPI_THREAD_SERIALIZED- 表示用户代码是多线程的,但对 MPI 库的调用是序列化的;
  • MPI_THREAD_MULTIPLE- 意味着用户代码是多线程的,所有线程都可以随时进行 MPI 调用,而无需任何同步。

为了使用线程支持初始化 MPI,必须使用MPI_Init_thread()而不是MPI_Init()

int provided;

MPI_Init_thread(&argc, &argv, MPI_THREAD_MULTIPLE, &provided);
if (provided < MPI_THREAD_MULTIPLE)
{
    printf("ERROR: The MPI library does not have full thread support\n");
    MPI_Abort(MPI_COMM_WORLD, 1);
}

具有过时(并从 MPI-3 中删除)C++ 绑定的等效代码:

int provided = MPI::Init_thread(argc, argv, MPI::THREAD_MULTIPLE);
if (provided < MPI::THREAD_MULTIPLE)
{
    printf("ERROR: The MPI library does not have full thread support\n");
    MPI::COMM_WORLD.Abort(1);
}

线程支持级别的顺序如下:MPI_THREAD_SINGLE< MPI_THREAD_FUNNELED< MPI_THREAD_SERIALIZED< MPI_THREAD_MULTIPLE,因此任何其他提供的级别,不同于MPI_THREAD_MULTIPLE将具有较低的数值 - 这就是if (...)上面的代码如此编写的原因。

MPI_Init(&argc, &argv)相当于MPI_Init_thread(&argc, &argv, MPI_THREAD_SINGLE, &provided)。实现不需要精确地在请求的级别初始化——而是可以在任何其他级别(更高或更低)初始化,这在provided输出参数中返回。

有关更多信息 - 请参阅 MPI 标准的第 12.4 节,可在此处免费获得。

对于大多数 MPI 实现,级别的线程支持MPI_THREAD_SINGLE实际上等同于级别提供的支持MPI_THREAD_SERIALIZED- 正是您在案例中观察到的。

由于您没有指定使用哪种 MPI 实现,这里有一个方便的列表。

我已经说过 Open MPI 必须在编译时启用正确的标志才能支持MPI_THREAD_MULTIPLE. 但是还有另一个问题——它的 InfiniBand 组件不是线程安全的,因此 Open MPI 在以全线程支持级别初始化时不会使用本机 InfiniBand 通信。

英特尔 MPI 有两种不同的风格 - 一种支持全多线程,另一种不支持全多线程。多线程支持是通过将-mt_mpi选项传递给 MPI 编译器包装器来启用的,该包装器启用与 MT 版本的链接。如果启用了 OpenMP 支持或自动并行器,则也隐含此选项。我不知道启用全线程支持时 IMPI 中的 InfiniBand 驱动程序如何工作。

MPICH(2) 不支持 InfiniBand,因此它是线程安全的,并且可能最近的版本提供MPI_THREAD_MULTIPLE了开箱即用的支持。

MVAPICH 是构建英特尔 MPI 的基础,它支持 InfiniBand。我不知道它在具有 InfiniBand 的机器上使用时在全线程支持级别上的表现如何。

关于多线程 InfiniBand 支持的说明很重要,因为现在许多计算集群都使用 InfiniBand 结构。禁用IB 组件(openibOpen MPI 中的 BTL)后,大多数 MPI 实现会切换到另一种协议,例如 TCP/IP(tcpOpen MPI 中的 BTL),这会导致通信速度更慢且延迟更大。

于 2013-02-12T16:31:34.110 回答
1

MPI 线程安全有四个级别,并非所有实现都支持:MPI_THREAD_SINGLE、MPI_THREAD_FUNNELED、MPI_THREAD_SERIALIZED 和 MPI_THREAD_MULTIPLE。最后一个,它允许一个进程有多个线程,可以同时调用 MPI 函数,可能是你感兴趣的那个。所以,首先,你需要确保你的实现支持 MPI_THREAD_SERIALIZED。

必须通过调用来指定所需的线程安全级别MPI_Init_thread。调用后,MPI_Init_thread您应该能够在自己创建的 boost (POSIX) 线程中安全地调用 MPI 函数。

于 2013-02-12T16:32:00.443 回答