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

时间:2016-07-18 16:58:26

标签: c++ c dynamic mpi loading

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

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

但解决方案并不“优雅”,容易出错。因为有struct个参数(例如,MPI_Requeststruct定义可能因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;

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

3 个答案:

答案 0 :(得分:3)

如果您只是针对支持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函数,例如,这是不够的。 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或演员表。它应该在实践中起作用并且非常易于维护。

答案 1 :(得分:2)

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

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

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

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

  3. // 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函数的参数数量。没有必要适应它。

答案 2 :(得分:1)

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

在这种情况下,MPI的通用接口是 Implementor 。期望客户为其特定的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)。

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