如何为动态加载编写MPI包装器

由于MPI不提供二进制兼容性,只提供源兼容性,因此我们不得不将求解器源代码发送给客户,以便他们将我们的求解器与其首选版本的MPI一起使用。 好吧,我们已经达到了无法再提供源代码的程度。

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

但解决方案并不“优雅”,容易出错。 因为有struct参数(比方说, MPI_Request ),其struct定义可能因MPI实现而异,我们需要接受(void*)许多存根参数。 此外,如果参数的数量可以从一个MPI到另一个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  #include  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; 

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

这似乎是Bridge Pattern的一个明显用例。

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

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

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

因此,您可以将MPI_InterfaceAbstract_Solver所需的类型安全,以完成其工作。 没有void *是必要的。 MPI_Instance的实现者可以在其实例化对象中存储它所需的任何特定于实现的MPI状态,这是实现接口所需的合同所必需的。 例如,可以从MPI_Interface省略comm参数。 接口可能只是假设一个单独的comm需要一个单独的MPI_Instance实例(初始化为不同的comm )。

虽然Bridge Pattern是面向对象的,但此解决方案不仅限于C ++。 您可以在C中轻松指定抽象接口(如此动态调度示例中所示)。

如果您只是针对支持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  #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函数,例如MPI_Get_count ,那么这还MPI_Get_count 。 在这种情况下,一个愚蠢的非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实现进行编译,然后将其动态加载到求解器中。 客户端无需实现任何操作。

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

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

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

 // 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 pimpl; } class my_MPI_Request ... 

资源

 #include  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函数的参数数量。 没有必要适应这一点。