如何为动态加载编写 MPI 包装器
How to write an MPI wrapper for dynamic loading
由于 MPI 不提供二进制兼容性,仅提供源代码兼容性,因此我们不得不将我们的求解器源代码发送给客户,以便他们将我们的求解器与他们首选的 MPI 版本一起使用。好吧,我们已经到了无法再提供源代码的地步。
因此,我正在研究围绕 MPI 调用创建包装器的方法。我们的想法是为我们提供一个 header 存根函数,用户将编写实现,从中创建一个动态库,然后我们的求解器将在运行时加载它。
但解决方案不是 "elegant" 并且容易出错。因为有 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 <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;
我的问题是是否有人知道解决这些问题的方法?
这似乎是 Bridge Pattern.
的一个明显用例
在这种情况下,MPI 的通用接口是实现者。客户应为其特定的 MPI 实例提供 ConcreteImplementor。您的求解器代码将是 RefinedAbstraction,因为 Abstraction 提供通往 Implementor.[=24 的桥梁=]
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 中指定一个抽象接口(如此 dynamic dispatching 示例所示)。
考虑到 MPI 是 well-defined API,您可以轻松地同时提供 header 和 MPI 包装器的源代码。客户只需根据他的 MPI 实现对其进行编译,然后您将其动态加载到您的求解器中。客户端不需要实现任何东西。
除了实际的函数包装之外,基本上还有两件事需要考虑:
正如您已经指出的,struct
s 可能不同。所以你必须把它们包起来。特别是,您需要考虑这些结构的大小,因此您不能在求解器代码中分配它们。我会为 C++ 做一个案例,因为你可以使用 RAII。
Return 代码,MPI_Datatype
和其他宏/枚举。我会为 C++ 做另一种情况,因为将 return 代码转换为异常是很自然的。
header
// 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 标准中都有明确定义。没有必要适应那个。
如果您只针对支持 PMPI 分析接口的平台,那么有一个通用的解决方案,它需要对原始源代码进行最少的更改甚至不需要更改。基本思想是(滥用)使用 PMPI 接口作为包装器。在某种非 OO 意义上,它可能是桥接模式的实现。
首先,几点观察。 MPI 标准中定义了一种结构类型,即 MPI_Status
。它只有三个 public 可见字段:MPI_SOURCE
、MPI_TAG
和 MPI_ERR
。没有 MPI 函数按值取 MPI_Status
。该标准定义了以下不透明类型:MPI_Aint
、MPI_Count
、MPI_Offset
和 MPI_Status
(+ 为清楚起见,特此删除了几个 Fortran 互操作性类型)。前三个是不可或缺的。然后是10种句柄类型,从MPI_Comm
到MPI_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
有点复杂。尽管该标准定义了 public 字段,但并未说明它们在结构中的顺序或位置。再一次,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_Status
从调用中获取信息,例如 MPI_Recv
,那么将三个 public 字段复制到包含以下内容的用户定义的静态结构中是微不足道的只有那些领域。但是,如果您还使用读取非 public 函数的 MPI 函数,那是不够的,例如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 不提供二进制兼容性,仅提供源代码兼容性,因此我们不得不将我们的求解器源代码发送给客户,以便他们将我们的求解器与他们首选的 MPI 版本一起使用。好吧,我们已经到了无法再提供源代码的地步。
因此,我正在研究围绕 MPI 调用创建包装器的方法。我们的想法是为我们提供一个 header 存根函数,用户将编写实现,从中创建一个动态库,然后我们的求解器将在运行时加载它。
但解决方案不是 "elegant" 并且容易出错。因为有 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 <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;
我的问题是是否有人知道解决这些问题的方法?
这似乎是 Bridge Pattern.
的一个明显用例在这种情况下,MPI 的通用接口是实现者。客户应为其特定的 MPI 实例提供 ConcreteImplementor。您的求解器代码将是 RefinedAbstraction,因为 Abstraction 提供通往 Implementor.[=24 的桥梁=]
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 中指定一个抽象接口(如此 dynamic dispatching 示例所示)。
考虑到 MPI 是 well-defined API,您可以轻松地同时提供 header 和 MPI 包装器的源代码。客户只需根据他的 MPI 实现对其进行编译,然后您将其动态加载到您的求解器中。客户端不需要实现任何东西。
除了实际的函数包装之外,基本上还有两件事需要考虑:
正如您已经指出的,
struct
s 可能不同。所以你必须把它们包起来。特别是,您需要考虑这些结构的大小,因此您不能在求解器代码中分配它们。我会为 C++ 做一个案例,因为你可以使用 RAII。Return 代码,
MPI_Datatype
和其他宏/枚举。我会为 C++ 做另一种情况,因为将 return 代码转换为异常是很自然的。
header
// 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 标准中都有明确定义。没有必要适应那个。
如果您只针对支持 PMPI 分析接口的平台,那么有一个通用的解决方案,它需要对原始源代码进行最少的更改甚至不需要更改。基本思想是(滥用)使用 PMPI 接口作为包装器。在某种非 OO 意义上,它可能是桥接模式的实现。
首先,几点观察。 MPI 标准中定义了一种结构类型,即 MPI_Status
。它只有三个 public 可见字段:MPI_SOURCE
、MPI_TAG
和 MPI_ERR
。没有 MPI 函数按值取 MPI_Status
。该标准定义了以下不透明类型:MPI_Aint
、MPI_Count
、MPI_Offset
和 MPI_Status
(+ 为清楚起见,特此删除了几个 Fortran 互操作性类型)。前三个是不可或缺的。然后是10种句柄类型,从MPI_Comm
到MPI_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
有点复杂。尽管该标准定义了 public 字段,但并未说明它们在结构中的顺序或位置。再一次,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_Status
从调用中获取信息,例如 MPI_Recv
,那么将三个 public 字段复制到包含以下内容的用户定义的静态结构中是微不足道的只有那些领域。但是,如果您还使用读取非 public 函数的 MPI 函数,那是不够的,例如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
或转换可能在这里或那里丢失。它应该在实践中工作并且非常易于维护。