2016-07-18 3 views
2

Поскольку MPI не предлагает бинарную совместимость, только совместимость с исходным кодом, мы вынуждены отправлять наш исходный код для клиентов, чтобы они могли использовать наш решатель с их предпочтительной версией 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; 

Мой вопрос: если кто-нибудь знает о решении этих проблем?

+2

Как насчет того, чтобы проиллюстрировать попытку в коде, а затем определить конкретную проблему, из-за которой попытка не работать? В противном случае это кажется очевидным применением шаблона Bridge. – jxh

+0

'// заголовок (предоставленный нами) int my_stub_mpi_send (const void * buf, int count, void * datatype, int dest, int tag, void * comm); //*.c (при условии, пользователем) #include #include INT my_stub_mpi_send (сопзИте пустоту * ЬаЯ, количество INT, недействительный * тип данных, Int Dest, Int тег, недействительный * Прдч) { return MPI_Send (* buf, count, * ((MPI_Datatype *) тип данных), dest, tag, * ((MPI_Comm *) comm)); } // Примечания: (1) Скорее всего, интерфейс будет C, а не C++, если только я не смогу сделать убедительный пример для C++; // (2) Цель здесь - избегать указателей void, если это возможно; –

+0

Основная проблема здесь в том, что типы MPI варьируются от одной реализации к другой. В подходе C++ я бы использовал декларации функций шаблона шаблона. Но даже это проблема, потому что тогда реализация (* .cpp-файл) должна была бы явно создавать экземпляры, так как их определения не присутствуют в файле заголовка. Я предпочел бы держаться подальше от явных шаблонных экземпляров, потому что они могут взорваться экспоненциально. Было бы неплохо использовать шаблоны проектирования Bridge или Adapter, но это предполагает общие абстрактные основы среди всех реализаций MPI для каждого типа MPI, чего, вероятно, слишком много, чтобы предположить. –

ответ

1

Это кажется очевидным прецедентом для Bridge Pattern.

В этом случае общий интерфейс для MPI - это Исполнитель. Ожидается, что клиент предоставит ConcreteImplementor для своего конкретного экземпляра MPI. Ваш код решателя будет 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 (как видно из примера dynamic dispatching).

+0

Спасибо, jxh. Это выглядит просто. –

+0

Я думаю, что исключение аргументов, таких как comm, на самом деле является единственным возможным способом. Скажем, в моем решателе у меня есть: func (...) { MPI_Comm my_comm = ...; MPI_Send (..., my_comm); } Но при замене с помощью вызовов-заглушек my_comm _definition_ должен быть заменен прокси-оболочкой. Это не сработает, потому что у типа обертки нет необходимых данных; это просто (обязательно не абстрактный) «пустой» базовый класс. Таким образом, типы управления MPI, такие как MPI_Request, MPI_Comm, не могут быть видны интерфейсу обертки, только реализация может их видеть. Я здесь неправ? –

+1

Возможно, проще всего заставить своего клиента правильно выполнить контракт, вместо того, чтобы принимать произвольные непрозрачные аргументы, которые были бы слишком легко ошибиться. – jxh

2

Учитывая, что MPI - это хорошо определенный API, вы можете легко предоставить как заголовок, так и исходный код оболочки MPI. Клиент просто должен скомпилировать его против реализации MPI, и вы динамически загружаете его в свой решатель. Клиент не нуждается в реализации чего-либо.

В дополнение к фактической функции упаковки, в основном существуют две вещи, чтобы рассмотреть следующие вопросы:

  1. Как уже отмечалось, struct s может отличаться. Поэтому вы должны их обернуть. В частности, вам нужно учитывать размер этих структур, поэтому вы не можете выделить их в свой код решателя. Я бы сделал случай для 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. Нет необходимости адаптировать это.

+0

Это действительно шаблон моста, в котором код для ** ConcreteImplemetor ** предоставляется решателем. Но, несмотря ни на что, это не было. – jxh

+0

Спасибо, Зулан. Да, форвардных деклараций должно быть достаточно, если наш решатель не использует их в нашем коде. Проблема в том, что наш код решателя пронизан переменными MPI_Comm, MPI_Request и MPI_Status. Я очищу некоторые из них, но я не могу их очистить (например, мы сохраняем некоторый вектор MPI_Request). Единственный способ - определить прокси-указатели для каждого (скажем, BaseMPIRequest *), что impl переопределяет и устанавливает соответственно через некоторые функции setter. Итак, fwd decl недостаточно, к сожалению ... –

+0

В принципе, если вы хотите убедиться, что конфликтов нет, вы никогда не должны включать 'mpi.h' в свой решатель (прямо или косвенно). Если код пронизан устаревшим MPI, решение '# define' & PMPI от Кристо Илиева может быть менее навязчивым для решателя. – Zulan

3

Если вы ориентируетесь только на платформы, поддерживающие интерфейс профилирования PMPI, то существует общее решение, требующее минимальных изменений исходного кода. Основная идея состоит в том, чтобы (ab-) использовать интерфейс PMPI для обертки. Вероятно, в некотором не-OO понимается реализация шаблона моста.

Во-первых, несколько наблюдений. Существует один тип структуры, определенный в стандарте MPI, и это MPI_Status. Он имеет только три общедоступных поля: 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 использует слабые символы для интерфейса 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_Status, чтобы получить информацию из вызовов, таких как MPI_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); 
... 

Ваша система сборки должна затем проверить, если sizeof(MPI_Status) фактической реализации MPI меньше или равна MY_MAX_STATUS_SIZE.

Вышеупомянутая информация является просто быстрой и грязной идеей - не проверял ее, а некоторые const или отливки могут отсутствовать здесь или там. Он должен работать на практике и быть достаточно удобным.

+0

Спасибо, Христо Илиев. Мне нужно лучше понять, что делает PMPI, и если это зависимость, которую мы можем наложить на клиента. Кажется, что это «родной» для каждой реализации MPI, поэтому это должно сработать. Я беспокоюсь о штрафах за исполнение. Является ли производительность ударом с помощью PMPI, и я понимаю, что он был первоначально разработан для профилирования? –

+0

Поскольку PMPI был разработан для профилирования, это действительно минимально. Накладные расходы - это всего лишь один вызов функции плюс приведение аргументов (что на x86-64 должно быть действительно минимальным с учетом ABI). Насколько это может повлиять на производительность, когда большинство вызовов MPI, особенно коммуникационные, имеют тенденцию занимать гораздо больше времени? Конечно, лучше извлечь более крупные блоки функциональности, например. обмен ореолом, сдвиг данных и т. д. –

+2

Btw, PMPI действительно прост. Это в основном означает, что (по возможности) все символы «MPI_Bla_bla» являются слабыми псевдонимами «истинных» функций MPI «PMPI_Bla_bla» и как таковые могут быть переопределены другим кодом, предоставляющим символы с тем же именем. Он не вводит никаких дополнительных зависимостей, кроме поддержки PMPI в библиотеке (практически все поставщики по умолчанию используют его).Но, как я сказал в самом верху моего ответа, это злоупотребление PMPI и здесь просто для того, чтобы дать вам (и кому-то еще интерес) другой способ подойти к проблеме с собственными механизмами MPI. –