如何编写用于动态加载的MPI包装器

How to write an MPI wrapper for dynamic loading

本文关键字:MPI 包装 加载 何编写 用于 动态      更新时间:2023-10-16

由于MPI不提供二进制兼容性,只提供源代码兼容性,我们被迫将我们的求解器源代码交付给客户,让他们将我们的解算器与他们首选的MPI版本一起使用。好吧,我们已经到了不能再提供源代码的地步。

因此,我正在研究如何围绕MPI调用创建包装器。我们的想法是提供一个存根函数的头,用户将编写实现,用它创建一个动态库,然后我们的求解器将在运行时加载它。

但解决方案并不"优雅",而且容易出错。由于存在struct自变量(例如MPI_Request),其struct定义可能因MPI实现而异,因此我们需要为许多存根自变量接受(void*)。此外,如果不同MPI的参数数量不同(我不确定是否保证永远不会发生),那么唯一的解决方法就是使用var_args

//header (provided by us)
int my_stub_mpi_send(const void buf, int count, void* datatype,
        int dest, int tag, void* comm);
//*.c (provided by user)
#include <my_stub_mpi.h>
#include <mpi.h>
int my_stub_mpi_send(const void buf, int count, void* datatype,
        int dest, int tag, void* comm)
{
    return MPI_Send(buf, count, *((MPI_Datatype) datatype),
            dest, tag, ((MPI_Comm) comm));
}
//Notes: (1) Most likely the interface will be C, not C++,
//           unless I can make a convincing case for C++;
//       (2) The goal here is to avoid *void pointers, if possible;

我的问题是,是否有人知道围绕这些问题的解决方案?

如果您只针对支持PMPI评测接口的平台,那么有一个通用的解决方案,它只需要对原始源代码进行最小的更改,甚至不需要更改。基本思想是(ab-)使用包装器的PMPI接口。在某种非OO意义上,它可能是桥接模式的实现。

首先,几点观察。MPI标准中定义了一种单一的结构类型,即MPI_Status。它只有三个公开可见的字段:MPI_SOURCEMPI_TAGMPI_ERR。没有MPI函数按值取MPI_Status。该标准定义了以下不透明类型:MPI_AintMPI_CountMPI_OffsetMPI_Status(为了清楚起见,在此放弃了几个Fortran互操作性类型)。前三个是积分。然后有10种句柄类型,从MPI_CommMPI_Win。句柄可以实现为特殊整数值,也可以实现为指向内部数据结构的指针。MPICH和其他基于它的实现采用第一种方法,而Open MPI采用第二种方法。作为指针或整数,任何类型的句柄都可以放在单个C数据类型中,即intptr_t

其基本思想是覆盖所有MPI函数,并将其参数重新定义为intptr_t类型,然后让用户编译的代码转换为正确的类型,并进行实际的MPI调用:

mytypes.h:中

typedef intptr_t my_MPI_Datatype;
typedef intptr_t my_MPI_Comm;

mympi.h:中

#include "mytypes.h"
// Redefine all MPI handle types
#define MPI_Datatype my_MPI_Datatype
#define MPI_Comm     my_MPI_Comm
// Those hold the actual values of some MPI constants
extern MPI_Comm     my_MPI_COMM_WORLD;
extern MPI_Datatype my_MPI_INT;
// Redefine the MPI constants to use our symbols
#define MPI_COMM_WORLD my_MPI_COMM_WORLD
#define MPI_INT        my_MPI_INT
// Redeclare the MPI interface
extern int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm);

mpiwrap.c:中

#include <mpi.h>
#include "mytypes.h"
my_MPI_Comm my_MPI_COMM_WORLD;
my_MPI_Datatype my_MPI_INT;
int MPI_Init(int *argc, char ***argv)
{
   // Initialise the actual MPI implementation
   int res = PMPI_Init(argc, argv);
   my_MPI_COMM_WORLD = (intptr_t)MPI_COMM_WORLD;
   my_MPI_INT = (intptr_t)MPI_INT;
   return res;
}
int MPI_Send(void *buf, int count, intptr_t datatype, int dest, int tag, intptr_t comm)
{
   return PMPI_Send(buf, count, (MPI_Datatype)datatype, dest, tag, (MPI_Comm)comm);
}

在您的代码中:

#include "mympi.h" // instead of mpi.h
...
MPI_Init(NULL, NULL);
...
MPI_Send(buf, 10, MPI_INT, 1, 10, MPI_COMM_WORLD);
...

MPI包装器可以静态链接,也可以动态预加载。只要MPI实现为PMPI接口使用弱符号,这两种方法都可以工作。您可以扩展上面的代码示例,以涵盖所使用的所有MPI函数和常量。所有常量都应保存在MPI_Init/MPI_Init_thread的包装器中。

处理MPI_Status在某种程度上是复杂的。尽管该标准定义了公共字段,但没有说明它们的顺序或在结构中的位置。再说一次,MPICH和Open MPI有很大不同:

// MPICH (Intel MPI)
typedef struct MPI_Status {
    int count_lo;
    int count_hi_and_cancelled;
    int MPI_SOURCE;
    int MPI_TAG;
    int MPI_ERROR;
} MPI_Status;
// Open MPI
struct ompi_status_public_t {
    /* These fields are publicly defined in the MPI specification.
       User applications may freely read from these fields. */
    int MPI_SOURCE;
    int MPI_TAG;
    int MPI_ERROR;
    /* The following two fields are internal to the Open MPI
       implementation and should not be accessed by MPI applications.
       They are subject to change at any time.  These are not the
       droids you're looking for. */
    int _cancelled;
    size_t _ucount;
};

如果只使用MPI_StatusMPI_Recv等调用中获取信息,那么将三个公共字段复制到只包含这些字段的用户定义静态结构中是很简单的。但是,如果您还使用读取非公共函数(例如MPI_Get_count)的MPI函数,那么这还不够。在这种情况下,一种愚蠢的非OO方法是简单地嵌入原始状态结构:

mytypes.h:中

// 64 bytes should cover most MPI implementations
#define MY_MAX_STATUS_SIZE 64
typedef struct my_MPI_Status
{
   int MPI_SOURCE;
   int MPI_TAG;
   int MPI_ERROR;
   char _original[MY_MAX_STATUS_SIZE];
} my_MPI_Status;

mympi.h:中

#define MPI_Status        my_MPI_Status
#define MPI_STATUS_IGNORE ((my_MPI_Status*)NULL)
extern int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm, MPI_Status *status);
extern int MPI_Get_count(MPI_Status *status, MPI_Datatype datatype, int *count);

mpiwrap.c:中

int MPI_Recv(void *buf, int count, my_MPI_Datatype datatype, int dest, int tag, my_MPI_Comm comm, my_MPI_Status *status)
{
   MPI_Status *real_status = (status != NULL) ? (MPI_Status*)&status->_original : MPI_STATUS_IGNORE;
   int res = PMPI_Recv(buf, count, (MPI_Datatype)datatype, dest, tag, (MPI_Comm)comm, real_status);
   if (status != NULL)
   {
      status->MPI_SOURCE = real_status->MPI_SOURCE;
      status->MPI_TAG = real_status->MPI_TAG;
      status->MPI_ERROR = real_status->MPI_ERROR;
   }
   return res;
}
int MPI_Get_count(my_MPI_Status *status, my_MPI_Datatype datatype, int *count)
{
   MPI_Status *real_status = (status != NULL) ? (MPI_Status*)&status->_original : MPI_STATUS_IGNORE;
   return PMPI_Get_count(real_status, (MPI_Datatype)datatype, count);
}

在您的代码中:

#include "mympi.h"
...
MPI_Status status;
int count;
MPI_Recv(buf, 100, MPI_INT, 0, 10, MPI_COMM_WORLD, &status);
MPI_Get_count(&status, MPI_INT, &count);
...

然后,构建系统应该检查实际MPI实现的sizeof(MPI_Status)是否小于或等于MY_MAX_STATUS_SIZE

以上只是一个快速而肮脏的想法——还没有测试过,一些const或类型转换可能会在这里或那里丢失。它应该在实践中起作用,并且很容易维护。

考虑到MPI是一个定义良好的API,您可以轻松地提供MPI包装器的头和源代码。客户只需要根据他的MPI实现进行编译,然后将其动态加载到求解器中。客户端不需要实现任何东西。

除了实际的函数包装,基本上还有两件事需要考虑:

  1. 正如您已经指出的,struct可能有所不同。所以你必须把它们包起来。特别是,您需要考虑这些结构的大小,因此无法在求解器代码中分配它们。我会为C++做一个例子,因为你可以使用RAII。

  2. 返回代码、MPI_Datatype和其他宏/枚举。我会为C++做另一个例子,因为将返回代码转换为异常是很自然的。

收割台

// DO NOT include mpi.h in the header. Only use forward-declarations
struct MPI_Status;
class my_MPI_Status {
public:
    // Never used directly by your solver.
    // You can make it private and friend your implementation.
    MPI_Status* get() { return pimpl.get(); }
    int source() const;
    ... tag, error
private:
    std::unique_ptr<MPI_Status> pimpl;
}
class my_MPI_Request ...

来源

#include <mpi.h>
static void handle_rc(int rc) {
    switch (rc) {
        case MPI_SUCCESS:
            return;
        case MPI_ERR_COMM:
            throw my_mpi_err_comm;
        ...
    }
}
// Note: This encapsulates the size of the `struct MPI_Status`
// within the source. Use `std::make_unique` if available.
my_MPI_Status::my_MPI_Status() : pimpl(new MPI_Status) {}
int my_MPI_Status::source() const {
    return pimpl->MPI_SOURCE;
}
void my_MPI_Wait(my_MPI_Request request, my_MPI_Status status) {
    handle_rc(MPI_Wait(request.get(), status.get());
}

请注意,每个MPI函数的参数数量在MPI标准中都有很好的定义。没有必要对此进行调整。

这似乎是桥接模式的一个明显用例。

在这种情况下,MPI的通用接口是实现器。客户应为其特定的MPI实例提供ConcreteImplementor。您的求解器代码将是RefinedAbstraction,因为抽象实现器提供了桥梁。

Abstract_Solver <>--> MPI_Interface
      .                    .
     /_                  /_
      |                    |
    Solver            MPI_Instance

客户从MPI_Interface继承并针对其选择的MPI实例实现它。然后将实现提供给求解器接口,并由Abstract_Solver在执行其工作时使用。

因此,您可以将MPI_Interface设置为Abstract_Solver完成工作所必需的类型安全。不需要void *MPI_Instance的实现者可以将其所需的任何特定于实现的MPI状态存储在其实例化对象中,该状态将是实现接口所需的契约所必需的。例如,comm自变量可以从MPI_Interface中删除。接口可以只是假设一个单独的comm将需要MPI_Instance的一个单独实例(初始化为不同的comm)。

虽然桥接模式是面向对象的,但该解决方案并不局限于C++。您可以很容易地在C中指定一个抽象接口(如这个动态调度示例所示)。