Comment écrire un wrapper MPI pour un chargement dynamic

Étant donné que MPI n’offre pas de compatibilité binary, mais uniquement la compatibilité de source, nous sums obligés d’expédier notre code source de solutionneur aux clients pour qu’ils utilisent notre solution de résolution avec leur version préférée de MPI. Nous avons atteint le point où nous ne pouvons plus offrir de code source.

En conséquence, je cherche des moyens de créer une enveloppe autour des appels MPI. L’idée est que nous fournissions un en-tête de fonctions de stub, et que l’utilisateur rédige l’implémentation, crée une bibliothèque dynamic à partir de celle-ci, puis notre solveur la charge au moment de l’exécution.

Mais les solutions ne sont pas “élégantes” et sont sujettes aux erreurs. Comme il existe des arguments de struct (par exemple, MPI_Request ) dont les définitions de struct peuvent différer d’une implémentation MPI à une autre, nous devons accepter (void*) pour beaucoup de nos arguments de talon. De plus, si le nombre d’arguments peut différer d’un MPI à l’autre (je ne suis pas sûr que cela ne se produira jamais), c’est le seul moyen d’utiliser 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; 

Ma question est la suivante: si quelqu’un connaît une solution autour de ces problèmes?

Cela semble être un cas d’utilisation évident pour le modèle de pont .

Dans ce cas, l’interface générique de MPI est l’ implémenteur . Le client doit fournir ConcreteImplementor pour son instance MPI spécifique. Votre code de solveur serait le RefinedAbstraction car l’ abstraction fournit le pont vers l’ implémenteur .

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

Le client hérite de MPI_Interface et l’implémente par rapport à l’instance de choix de MPI. L’implémentation est ensuite transmise à l’interface du solveur et utilisée par Abstract_Solver au cours de son travail.

Ainsi, vous pouvez MPI_Interface le type MPI_Interface manière à ce que Abstract_Solver puisse exécuter son travail. Aucun void * n’est nécessaire. L’implémenteur de MPI_Instance peut stocker tout état MPI spécifique à l’implémentation dont il a besoin dans son object instancié pour remplir le contrat requirejs par l’interface. Par exemple, l’argument de comm pourrait être MPI_Interface . L’interface pourrait simplement supposer qu’une comm distincte nécessiterait une instance distincte de MPI_Instance (initialisée avec une comm différente).

Bien que le modèle de pont soit orienté object, cette solution ne se limite pas à C ++. Vous pouvez facilement spécifier une interface abstraite en C (comme dans cet exemple de répartition dynamic ).

Si vous ne ciblez que les plates-formes prenant en charge l’interface de profilage PMPI, il existe une solution générique qui nécessite peu de modifications, voire aucune modification du code source d’origine. L’idée de base est (ab-) d’utiliser l’interface PMPI pour le wrapper. Dans un sens non-OO, c’est probablement une implémentation du modèle de pont.

Tout d’abord, plusieurs observations. Un seul type de structure est défini dans la norme MPI, à savoir MPI_Status . Il ne comporte que trois champs visibles publiquement: MPI_SOURCE , MPI_TAG et MPI_ERR . Aucune fonction MPI ne prend MPI_Status par valeur. La norme définit les types opaques suivants: MPI_Aint , MPI_Count , MPI_Offset et MPI_Status (+ plusieurs types d’interopérabilité Fortran ont été supprimés pour des MPI_Status de clarté). Les trois premiers font partie intégrante. Ensuite, il existe 10 types de MPI_Comm , de MPI_Comm à MPI_Win . Les poignées peuvent être implémentées sous forme de valeurs entières spéciales ou de pointeurs vers des structures de données internes. MPICH et les autres implémentations basées sur ce dernier adoptent la première approche, tandis que Open MPI adopte la seconde. Qu’il s’agisse d’un pointeur ou d’un entier, un descripteur de n’importe quel type peut s’intégrer dans un seul type de données C, à savoir intptr_t .

L’idée de base est de remplacer toutes les fonctions MPI et de redéfinir leurs arguments comme étant de type intptr_t , puis de intptr_t le code compilé par l’utilisateur effectuer la transition vers le type approprié et effectuer l’appel MPI proprement dit:

Dans mytypes.h :

 typedef intptr_t my_MPI_Datatype; typedef intptr_t my_MPI_Comm; 

Dans 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); 

Dans 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); } 

Dans votre code:

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

Le wrapper MPI peut être lié statiquement ou préchargé dynamicment. Les deux méthodes fonctionnent tant que l’implémentation MPI utilise des symboles faibles pour l’interface PMPI. Vous pouvez étendre l’exemple de code ci-dessus à toutes les fonctions et constantes MPI utilisées. Toutes les constantes doivent être enregistrées dans le wrapper de MPI_Init / MPI_Init_thread .

Le traitement de MPI_Status est en quelque sorte compliqué. Bien que la norme définisse les champs publics, elle ne dit rien sur leur ordre ou leur placement dans la structure. Et encore une fois, MPICH et Open MPI diffèrent considérablement:

 // 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; }; 

Si vous utilisez uniquement MPI_Status pour extraire des informations d’appels tels que MPI_Recv , il est alors sortingvial de copier les trois champs publics dans une structure statique définie par l’utilisateur contenant uniquement ces champs. Mais cela ne suffira pas si vous utilisez également des fonctions MPI qui lisent les fonctions non publiques, par exemple MPI_Get_count . Dans ce cas, une approche stupide non-OO consiste simplement à intégrer la structure de statut d’origine:

Dans 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; 

Dans 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); 

Dans 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); } 

Dans votre code:

 #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); ... 

Votre système de génération doit ensuite vérifier si la sizeof(MPI_Status) de la mise en œuvre MPI réelle est inférieure ou égale à MY_MAX_STATUS_SIZE .

Ce qui précède n’est qu’une idée rapide et sale: ne l’avez pas testée et il est possible que certains const ou moulages manquent ici ou là. Cela devrait fonctionner dans la pratique et être assez facile à maintenir.

Étant donné que MPI est une API bien définie, vous pouvez facilement fournir à la fois l’en-tête et le code source du wrapper MPI. Le client doit simplement le comstackr en fonction de son implémentation MPI et vous le chargez dynamicment dans votre solutionneur. Le client n’a pas besoin d’implémenter quoi que ce soit.

En plus du wrapping de la fonction réelle, il y a fondamentalement deux choses à considérer:

  1. Comme vous l’avez déjà souligné, les struct peuvent différer. Donc, vous devez les envelopper. En particulier, vous devez tenir compte de la taille de ces structures afin de ne pas pouvoir les allouer dans votre code de solveur. Je ferais un cas pour C ++, parce que vous pouvez utiliser RAII.

  2. Codes de retour, MPI_Datatype et autres macros / enums. Je présenterais un autre cas pour C ++, car il est naturel de convertir les codes de retour en exceptions.

entête

 // 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 ... 

la source

 #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()); } 

Notez que le nombre d’arguments pour chaque fonction MPI est bien défini dans la norme MPI. Il n’y a pas besoin d’adapter cela.