Tutoriel MPI

From CC Doc
Jump to: navigation, search
This page is a translated version of the page MPI and the translation is 98% complete.

Outdated translations are marked like this.
Other languages:
English • ‎français

Introduction à la programmation parallèle

To pull a bigger wagon it is easier to add more oxen than to find (or build) a bigger ox. [traduction libre, Pour tirer une plus grosse charrette, il est plus facile d'ajouter des bœufs que de trouver un plus gros bœuf.]
—Gropp, Lusk & Skjellum, Using MPIMessage Passing Interface

Pour construire une maison le plus rapidement possible, on n'engage pas la personne qui peut faire tout le travail plus rapidement que les autres. On distribue plutôt le travail parmi autant de personnes qu'il faut pour que les tâches se fassent en même temps, d'une manière parallèle. Cette solution est valide aussi pour les problèmes numériques. Comme il y a une limite à vitesse d'exécution d'un processeur, la fragmentation du problème permet d'assigner des tâches à exécuter en parallèle par plusieurs processeurs. Cette approche sert autant la vitesse du calcul que les exigences élevées en mémoire.

L'aspect le plus important dans la conception et le développement de programmes parallèles est la communication. Ce sont les exigences de la communication qui créent la complexité. Pour que plusieurs travailleurs accomplissent une tâche en parallèle, ils doivent pouvoir communiquer. De la même manière, plusieurs processus logiciels qui travaillent chacun sur une partie d'un problème ont besoin de valeurs qui sont ou seront calculées par d'autres processus.

Il y a deux modèles principaux en programmation parallèle : les programmes à mémoire partagée et les programmes à mémoire distribuée.

Dans le cas d'une parallélisation avec mémoire partagée (SMP pour shared memory parallelism), les processeurs voient tous la même image mémoire, c'est-à-dire que la mémoire peut être adressée globalement et tous les processeurs y ont accès. Sur une machine SMP, les processeurs communiquent de façon implicite; chacun des processeurs peut lire et écrire en mémoire et les autres processeurs peuvent y accéder et les utiliser. Le défi ici est la cohérence des données puisqu'il faut veiller à ce que les données ne soient modifiées que par un seul processus à la fois.


Figure 1: Architecture à mémoire partagée

Pour sa part, la parallélisation avec mémoire distribuée s'apparente à une grappe, un ensemble d'ordinateurs reliés par un réseau de communication dédié. Dans ce modèle, les processus possèdent chacun leur propre mémoire et ils peuvent être exécutés sur plusieurs ordinateurs distincts. Les processus communiquent par messages : un processus utilise une fonction pour envoyer un message et l'autre processus utilise une autre fonction pour recevoir le message. Le principal défi ici est d'avoir le moins de communications possible. Même les réseaux avec les connexions physiques les plus rapides transmettent les données beaucoup plus lentement qu'un simple ordinateur : l'accès mémoire se mesure habituellement en centièmes de nanosecondes alors que les réseaux y accèdent généralement en microsecondes.

Figure 2: Architecture à grappes, mémoire distribuée

Nous discuterons ici uniquement de la programmation avec mémoire distribuée sur grappe, avec MPIMessage Passing Interface.

Qu'est-ce que MPIMessage Passing Interface?

MPIMessage Passing Interface (message passing interface) est en réalité une norme avec des sous-routines, fonctions, objets et autres éléments pour développer des programmes parallèles dans un environnement à mémoire distribuée. MPIMessage Passing Interface est implémentée dans plusieurs bibliothèques, notamment Open MPIMessage Passing Interface, MPICH et MVAPICH. La norme décrit la méthode d'appel en Fortran, C et C++, mais il existe aussi des méthodes indirectes d'appel pour plusieurs autres langages (Boost.MPIMessage Passing Interface, mpi4py, Rmpi, etc.).

Puisque MPIMessage Passing Interface est une norme ouverte sans droits exclusifs, un programme MPIMessage Passing Interface peut facilement être porté sur plusieurs ordinateurs différents. Les programmes MPIMessage Passing Interface peuvent être exécutés concurremment sur plusieurs cœurs à la fois et offrent une parallélisation efficace, permettant une bonne extensibilité (scalability). Puisque chaque processus possède sa propre plage mémoire, certaines opérations de débogage s'en trouvent simplifiées; en ayant des plages mémoire distinctes, les processus n’auront aucun conflit d’accès à la mémoire comme c'est le cas en mémoire partagée. Aussi, en présence d'une erreur de segmentation, le fichier core résultant peut être traité par des outils standards de débogage série. Le besoin de gérer la communication et la synchronisation de façon explicite donne par contre l'impression qu'un programme MPIMessage Passing Interface est plus complexe qu'un autre programme où la gestion de la communication serait implicite. Il est cependant recommandé de restreindre les communications entre processus pour favoriser la vitesse de calcul d'un programme MPIMessage Passing Interface.

Nous verrons plus loin quelques-uns de ces points et proposerons des stratégies de solution; les références mentionnées au bas de cette page sont aussi à consulter.

Principes de base

Dans ce tutoriel, nous présenterons le développement d'un code MPIMessage Passing Interface en C et en Fortran, mais les différents principes de communication s'appliquent à tout langage et à toute bibliothèque permettant une utilisation indirecte de l'API de MPIMessage Passing Interface. Notre but ici est de paralléliser le programme simple "Hello World" utilisé dans les exemples.

File : hello.c

#include <stdio.h>
 
 int main()
 {
     printf("Hello, world!\n");
 
     return(0);
 }


File : hello.cpp

#include <iostream>
using namespace std;

int main()
{
    cout << "Hello, world!" << endl;
    return 0;
}


File : hello.f90

program hello
 
     print *, 'Hello, world!'
 
 end program hello


Pour compiler et exécuter le programme :

[~]$ vi hello.c
[~]$ cc -Wall hello.c -o hello
[~]$ ./hello 
Hello, world!

Modèle SPMD

La parallélisation MPIMessage Passing Interface utilise le modèle d'exécution SPMD (single program multiple data), où plusieurs instances s'exécutent en même temps. Chacune des instances est un processus auquel est assigné un numéro unique qui représente son rang; l'instance peut obtenir son rang lorsqu'elle est lancée. Afin d'attribuer un comportement différent à chaque instance, on utilisera habituellement un énoncé conditionnel if.

Figure 3: Contrôle de comportements divergents

Cadre d'exécution

Un programme MPIMessage Passing Interface importe le fichier entête approprié (mpi.h en C/C++; mpif.h en Fortran); il peut donc être compilé puis relié à l'implémentation MPIMessage Passing Interface de notre choix. Dans la plupart des cas, l'implémentation possède un script pratique qui enveloppe l'appel au compilateur (compiler wrapper) et qui configure adéquatement include et lib, entre autres pour relier les indicateurs. Nos exemples utilisent les scripts de compilation suivants :

  • pour le C, mpicc
  • pour le Fortran, mpif90
  • pour le C++, mpiCC

Une fois les instances lancées, elles doivent se coordonner, ce qui se fait en tout premier lieu par l'appel d'une fonction d'initialisation :

 int MPI_Init(int *argc, char **argv[]);
boost::mpi::environment(int &, char **&, bool = true);
 MPI_INIT(IERR)
 INTEGER :: IERR

En C, les arguments de MPIMessage Passing Interface_Init pointent vers les variables argc et argv qui sont les arguments en ligne de commande. Comme pour toutes les fonctions MPIMessage Passing Interface en C, la valeur retournée représente l'erreur de la fonction. En Fortran, les routines MPIMessage Passing Interface retournent l'erreur dans l'argument IERR.

On doit aussi appeler la fonction MPIMessage Passing Interface_Finalize pour faire un nettoyage avant la fin du programme, le cas échéant :

 int MPI_Finalize(void);

Nothing needed

 MPI_FINALIZE(IERR)
 INTEGER :: IERR

Règle générale, il est recommandé d'appeler MPIMessage Passing Interface_Init au tout début du programme et MPIMessage Passing Interface_Finalize à la toute fin.

File : phello0.c

#include <stdio.h>
#include <mpi.h>
 
int main(int argc, char *argv[])
{
    MPI_Init(&argc, &argv);
 
    printf("Hello, world!\n");
 
    MPI_Finalize();
    return(0);
}


File : phello0.cpp

#include <iostream>
#include <boost/mpi.hpp>
using namespace std;
using namespace boost;

int main(int argc, char *argv[])
{
    mpi::environment env(argc, argv);
    cout << "Hello, world!" << endl;
    return 0;
}


File : phello0.f90

program phello0
 
     include "mpif.h"
 
     integer :: ierror
 
     call MPI_INIT(ierror)
     print *, 'Hello, world!'
     call MPI_FINALIZE(ierror)

 end program phello0


Fonctions rank et size

Le programme pourrait être exécuté tel quel, mais le résultat ne serait pas très convainquant puisque chacun des processus produirait le même message. Nous allons plutôt faire en sorte que chaque processus fasse afficher la valeur de son rang et le nombre total de processus en opération.

 int MPI_Comm_size(MPI_Comm comm, int *nproc);
 int MPI_Comm_rank(MPI_Comm comm, int *myrank);
int mpi::communicator::size();
int mpi::communicator::rank();
 MPI_COMM_SIZE(COMM, NPROC, IERR)
 INTEGER :: COMM, NPROC, IERR
 
 MPI_COMM_RANK(COMM, RANK, IERR)
 INTEGER :: COMM, RANK, IERR

Le paramètre de sortie nproc est donné à la fonction MPIMessage Passing Interface_Comm_size afin d'obtenir le nombre de processus en opération. De même, le paramètre de sortie myrank est donné à la fonction MPIMessage Passing Interface_Comm_rank afin d'obtenir la valeur du rang du processus actuel. Le rang du premier processus a la valeur de 0 au lieu de 1; pour N processus, les valeurs de rang vont donc de 0 à (N-1) inclusivement. L'argument comm est un communicateur, soit un ensemble de processus pouvant s'envoyer entre eux des messages. Dans nos exemples, nous utilisons la valeur de MPIMessage Passing Interface_COMM_WORLD, soit un communicateur prédéfini par MPIMessage Passing Interface et qui représente l'ensemble des processus lancés par la tâche. Nous n'abordons pas ici le sujet des communicateurs créés par programmation; voyez plutôt la liste des autres sujets en bas de page.

Utilisons maintenant ces fonctions pour que chaque processus produise le résultat voulu. Notez que, puisque les processus effectuent tous le même appel de fonction, il n'est pas nécessaire d'introduire des énoncés conditionnels.

File : phello1.c

#include <stdio.h>
 #include <mpi.h>
 
 int main(int argc, char *argv[])
 {
     int rank, size;
 
     MPI_Init(&argc, &argv);
     MPI_Comm_rank(MPI_COMM_WORLD, &rank);
     MPI_Comm_size(MPI_COMM_WORLD, &size);
 
     printf("Hello, world! "
             "from process %d of %d\n", rank, size);
 
     MPI_Finalize();
     return(0);
 }


File : phello1.cpp

#include <iostream>
#include <boost/mpi.hpp>
using namespace std;
using namespace boost;

int main(int argc, char *argv[])
{
    mpi::environment env(argc, argv);
    mpi::communicator world;

    cout << "Hello, world! from process " << world.rank() << " of " << world.size() << endl;
    return 0;
}


File : phello1.f90

program phello1
 
    include "mpif.h"
 
    integer :: rank, size, ierror
 
    call MPI_INIT(ierror)
    call MPI_COMM_SIZE(MPI_COMM_WORLD, size, ierror)
    call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierror)
 
    print *, 'Hello from process ', rank, ' of ', size
 
    call MPI_FINALIZE(ierror)
 
 end program phello1


Compilez maintenant ce programme et faites-le exécuter avec 2, 4 et 8 processus. Vous remarquerez que le résultat produit par chacun des processus dépend de la valeur de ses variables locales et que le résultat final est la concaténation de la sortie standard (stdout) de tous les processus. Vous constaterez sans doute que les sorties produites par les processus ne sont pas nécessairement ordonnées selon leur rang : il n'est pas possible de prévoir l'ordre en sortie. [~]$ vi phello1.c [~]$ mpicc -Wall phello1.c -o phello1 [~]$ mpirun -np 4 ./phello1 Hello, world! from process 0 of 4 Hello, world! from process 2 of 4 Hello, world! from process 1 of 4 Hello, world! from process 3 of 4


Pour compiler avec Boost :

[~]$ mpic++ --std=c++11 phello1.cpp -lboost_mpi-mt -lboost_serialization-mt -o phello1

Communication

Nous avons maintenant une version parallèle de Hello World, mais sans communication entre les processus. Voyons ensuite cet aspect.

Nous demandons à chaque processus de transmettre au processus suivant le mot hello. Le processus de rang i envoie son message au processus de rang i+1 et le dernier processus de rang N-1 envoie son message au processus de rang 0 pour boucler la boucle. Exprimé concisément, le processus i envoie au processus (i+1)%N où il y a N processus et où % est l'opérateur modulo.

MPIMessage Passing Interface offre plusieurs fonctions pour échanger des données dans un grand nombre de relations entre processus (1,1; 1,n; n,1; n,n). Les fonctions les plus simples sont cependant celles qui échangent une ou plusieurs instances de données du même type de base, soit les fonctions MPIMessage Passing Interface_Send et MPIMessage Passing Interface_Recv.

Un processus envoie des données par la fonction MPIMessage Passing Interface_Send. Examinons notre exemple :

  • message est un pointeur vers un vecteur de données à envoyer;
  • count représente le nombre d'instances contiguës de type datatype est le type des données;
  • dest est le rang du processus cible;
  • tag est un identifiant entier défini par le programmeur et associé au type de message à envoyer, ce qui est utile pour distinguer les différentes communications entre les processus. Cependant, puisque cet identifiant n'est toujours pas utile à notre exemple, nous choisissons la valeur arbitraire 0;
  • MPIMessage Passing Interface_COMM_WORLD est le communicateur représentant tous les processus lancés par mpirun.
 int MPI_Send
 (
     void *message,           /* reference to data to be sent */
     int count,               /* number of items in message */
     MPI_Datatype datatype,   /* type of item in message */
     int dest,                /* rank of process to receive message */
     int tag,                 /* programmer specified identifier */
     MPI_Comm comm            /* communicator */
 );
template<typename T> void mpi::communicator::send(
  int dest,                  /* rank of process to receive message */ 
  int tag,                          /* programmer specified identified */
  const T & value              /* message */
) const;
 MPI_SEND(MESSAGE, COUNT, DATATYPE, DEST, TAG, COMM, IERR)
 <type> MESSAGE(*)
 INTEGER :: COUNT, DATATYPE, DEST, TAG, COMM, IERR

Remarquez que l'argument datatype qui identifie le type des données contenus dans le buffer message est une variable définie par la norme MPIMessage Passing Interface. Ceci assure une couche de compatibilité entre les processus opérant sur des architectures où le format natif des données serait différent. Il est possible d'utiliser de nouveaux types de données, mais nous nous limitons ici aux types définis nativement par MPIMessage Passing Interface. En langage C : MPIMessage Passing Interface_CHAR, MPIMessage Passing Interface_FLOAT, MPIMessage Passing Interface_SHORT, MPIMessage Passing Interface_INT, etc. En Fortran :MPIMessage Passing Interface_CHARACTER, MPIMessage Passing Interface_INTEGER, MPIMessage Passing Interface_REAL, etc. Pour la liste complète des types de données, consultez la section Références en bas de page.

À la fonction de réception MPIMessage Passing Interface_Recv, on ajoute l'argument status : en C, l'argument réfère à une structure allouée MPIMessage Passing Interface_Status et en Fortran, l'argument contient une matrice MPIMessage Passing Interface_STATUS_SIZE de nombres entiers. À son retour, MPIMessage Passing Interface_Recv contiendra de l'information sur le message reçu. Nos exemples ne montrent pas cet argument, mais il doit faire partie des instructions.

 int MPI_Recv
 (
     void *message,           /* reference to buffer for received data */
     int count,               /* number of items to be received */
     MPI_Datatype datatype,   /* type of item to be received */
     int source,              /* rank of process from which to receive */
     int tag,                 /* programmer specified identifier */
     MPI_Comm comm            /* communicator */
     MPI_Status *status       /* stores info. about received message */
 );
template<typename T> void mpi::communicator::send(
  int source,                  /* rank of process from which to receive */ 
  int tag,                          /* programmer specified identified */
  const T & value               /* message */
) const;
 MPI_RECV(MESSAGE, COUNT, DATATYPE, SOURCE, TAG, COMM, STATUS, IERR)
 <type> :: MESSAGE(*)
 INTEGER :: COUNT, DATATYPE, SOURCE, TAG, COMM, STATUS(MPI_STATUS_SIZE), IERR

Dans notre cas simple avec MPIMessage Passing Interface_Send et MPIMessage Passing Interface_Recv, le processus qui envoie doit connaitre le rang du processus qui reçoit et vice versa. Rappelons-nous des règles mathématiques suivantes :

  • (rank + 1) % size est le processus auquel on envoie
  • (rank + 1) % size est le processus duquel on reçoit

Modifions maintenant notre programme parallèle.

File : phello2.c

#include <stdio.h>
#include <mpi.h>
 
#define BUFMAX 81
 
int main(int argc, char *argv[])
{
     char outbuf[BUFMAX], inbuf[BUFMAX];
     int rank, size;
     int sendto, recvfrom;
     MPI_Status status;
 
     MPI_Init(&argc, &argv);
     MPI_Comm_rank(MPI_COMM_WORLD, &rank);
     MPI_Comm_size(MPI_COMM_WORLD, &size);
 
     sprintf(outbuf, "Hello, world! from process %d of %d", rank, size);
 
     sendto = (rank + 1) % size;
     recvfrom = (rank + size - 1) % size;
 
     MPI_Send(outbuf, BUFMAX, MPI_CHAR, sendto, 0, MPI_COMM_WORLD);
     MPI_Recv(inbuf, BUFMAX, MPI_CHAR, recvfrom, 0, MPI_COMM_WORLD, &status);
 	
     printf("[P_%d] process %d said: \"%s\"]\n", rank, recvfrom, inbuf);
 
     MPI_Finalize();
     return(0);
}


File : phello.cpp

#include <iostream>
#include <string>
#include <boost/mpi.hpp>

using namespace std;
using namespace boost;

int main(int argc, char *argv[])
{
    mpi::environment env(argc, argv);
    mpi::communicator world;
    int rank = world.rank();
    int size = world.size();

    string outmessage = "Hello, world! from process " + to_string(rank) + " of " + to_string(size);
    string inmessage;
    int sendto = (rank + 1) % size;
    int recvfrom = (rank + size - 1) % size;

    cout << outmessage << endl;

    world.send(sendto,0,outmessage);
    world.recv(recvfrom,0,inmessage);

    cout << "[P_" << rank << "] process " << recvfrom << " said: \"" << inmessage << "\"" << endl;
    return 0;
}


File : phello2.f90

program phello2

     implicit none
     include 'mpif.h'
     integer, parameter :: BUFMAX=81
     character(len=BUFMAX) :: outbuf, inbuf, tmp
     integer :: rank, num_procs, ierr
     integer :: sendto, recvfrom
     integer :: status(MPI_STATUS_SIZE)
 
     call MPI_INIT(ierr)
     call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
     call MPI_COMM_SIZE(MPI_COMM_WORLD, num_procs, ierr)
 
     outbuf = 'Hello, world! from process ' 
     write(tmp,'(i2)') rank
     outbuf = outbuf(1:len_trim(outbuf)) // tmp(1:len_trim(tmp))
     write(tmp,'(i2)') num_procs
     outbuf = outbuf(1:len_trim(outbuf)) // ' of ' // tmp(1:len_trim(tmp))
 
     sendto = mod((rank + 1), num_procs)
     recvfrom = mod((rank + num_procs - 1), num_procs)
 
     call MPI_SEND(outbuf, BUFMAX, MPI_CHARACTER, sendto, 0, MPI_COMM_WORLD, ierr)
     call MPI_RECV(inbuf, BUFMAX, MPI_CHARACTER, recvfrom, 0, MPI_COMM_WORLD, status, ierr)
 
     print *, 'Process', rank, ': Process', recvfrom, ' said:', inbuf
 
     call MPI_FINALIZE(ierr)
 
end program phello2


Compilez ce programme et faites-le exécuter avec 2, 4 et 8 processus. Le fonctionnement semble approprié, mais il y a cependant un problème caché. En effet, la norme MPIMessage Passing Interface n'offre aucune garantie que MPIMessage Passing Interface_Send retournera avant que le message ait été livré. Dans la plupart des implémentations, les données sont mises en mémoire temporaire par MPIMessage Passing Interface_Send et retournent sans attendre d'être livrés. Par contre, si la mémoire tampon n'était pas utilisée, notre code bloquerait. Chaque processus appellerait MPIMessage Passing Interface_Send et attendrait que le processus voisin appelle MPIMessage Passing Interface_Recv. Puisque le processus voisin serait aussi en attente d'une réponse de MPIMessage Passing Interface_Send, tous les processus seraient indéfiniment en attente. Les bibliothèques des systèmes de Calcul Canada utilisent les buffers puisque notre code n'a pas bloqué; ce modèle de conception n'est toutefois pas fiable. Sans mémoire tampon offerte par la bibliothèque, le programme pourrait faire défaut, et malgré la mémoire tampon, un appel pourrait être bloqué si celle-ci est saturée.

[~]$ mpicc -Wall phello2.c -o phello2
[~]$ mpirun -np 4 ./phello2
[P_0] process 3 said: "Hello, world! from process 3 of 4"]
[P_1] process 0 said: "Hello, world! from process 0 of 4"]
[P_2] process 1 said: "Hello, world! from process 1 of 4"]
[P_3] process 2 said: "Hello, world! from process 2 of 4"]

Éviter les impasses

Dans la norme MPIMessage Passing Interface, les appels MPIMessage Passing Interface_Send et MPIMessage Passing Interface_Recv sont des appels bloquants. MPIMessage Passing Interface_Send ne retourne pas tant qu'il n'est pas sécuritaire pour le module qui appelle de modifier le contenu de la mémoire tampon. De même, MPIMessage Passing Interface_Recv ne retourne pas tant que tout le contenu du message ne se trouve pas dans la mémoire tampon.

Le message est reçu, que la bibliothèque MPIMessage Passing Interface offre ou non l’accès à une mémoire tampon. À la réception des données, le contenu du message est placé dans la mémoire tampon identifiée par l’appel et ce dernier est bloqué jusqu’à ce que MPIMessage Passing Interface_Recv retourne. Par contre, MPIMessage Passing Interface_Send n’a pas besoin d’être bloqué si la bibliothèque offre une mémoire tampon; dès que les données sont copiées de leur lieu d’origine, ce dernier peut être modifié et l’appel peut retourner. Ceci explique pourquoi notre exemple ne mène pas à une impasse malgré le fait que chacun des processus appelle MPIMessage Passing Interface_Send en premier. Puisque la norme MPIMessage Passing Interface ne requiert pas l'usage de la mémoire tampon et que notre code en dépend, nous considérons le programme comme étant à risque d'une impasse.

Un programme qui ne serait pas à risque en serait un dont le bon fonctionnement ne requiert pas l’usage d’une mémoire tampon, comme illustré ici :

Interblocage

 ...
    if (rank == 0)
    {
        MPI_Recv(from 1);
        MPI_Send(to 1);
    }
    else if (rank == 1)
    {
        MPI_Recv(from 0);
        MPI_Send(to 0);
    }
...

Dans les deux cas, l'appel de réception est lancé avant l'appel d'envoi correspondant : il y a ainsi interblocage causé par MPIMessage Passing Interface_Recv.

Situation à risque

...
    if (rank == 0)
    {
        MPI_Send(to 1);
        MPI_Recv(from 1);
    }
    else if (rank == 1)
    {
        MPI_Send(to 0);
        MPI_Recv(from 0);
    }
...

Dans cet exemple, le programme pourrait fonctionner adéquatement si la bibliothèque permet l'usage d'une mémoire tampon. Si ce n'est pas le cas, ou si le contenu des messages dépasse la capacité de la mémoire tampon, MPIMessage Passing Interface_Send fera bloquer le code, créant ainsi une impasse. L'exemple suivant présente une solution.

Code fiable

...
    if (rank == 0)
    {
        MPI_Send(to 1);
        MPI_Recv(from 1);
    }
    else if (rank == 1)
    {
        MPI_Recv(from 0);
        MPI_Send(to 0);
    }
...

L’envoi est ici couplé avec la réception, sans usage d'une mémoire tampon. Le processus pourrait être arrêté momentanément en attente de l’appel correspondant, mais il n’y aura pas interblocage.

Comment peut-on s’assurer que notre code est correct? Une solution commode est d'utiliser des couples pair-impair et de procéder en deux temps. Dans notre exemple, la communication se fait séquentiellement d'un incrément vers la droite; notre code devrait donc être juste si tous les processus pairs exécutent l'envoi suivi de la réception et que tous les processus impairs exécutent la réception suivie de l'envoi. Les couples envoi-réception sont bien définis et la possibilité d'interblocage est éliminée.

File : phello3.c

#include <stdio.h>
#include <mpi.h>

#define BUFMAX 81

int main(int argc, char *argv[])
{
    char outbuf[BUFMAX], inbuf[BUFMAX];
    int rank, size;
    int sendto, recvfrom;
    MPI_Status status;


    MPI_Init(&argc, &argv);
    MPI_Comm_rank(MPI_COMM_WORLD, &rank);
    MPI_Comm_size(MPI_COMM_WORLD, &size);

    sprintf(outbuf, "Hello, world! from process %d of %d", rank, size);

    sendto = (rank + 1) % size;
    recvfrom = ((rank + size) - 1) % size;

    if (!(rank % 2))
    {
        MPI_Send(outbuf, BUFMAX, MPI_CHAR, sendto, 0, MPI_COMM_WORLD);
        MPI_Recv(inbuf, BUFMAX, MPI_CHAR, recvfrom, 0, MPI_COMM_WORLD, &status);
    }
    else
    {
        MPI_Recv(inbuf, BUFMAX, MPI_CHAR, recvfrom, 0, MPI_COMM_WORLD, &status);
        MPI_Send(outbuf, BUFMAX, MPI_CHAR, sendto, 0, MPI_COMM_WORLD);
    }

    printf("[P_%d] process %d said: \"%s\"]\n", rank, recvfrom, inbuf);

    MPI_Finalize();

    return(0);
}


File : phello3.cpp

#include <iostream>
#include <string>
#include <boost/mpi.hpp>

using namespace std;
using namespace boost;

int main(int argc, char *argv[])
{
    mpi::environment env(argc, argv);
    mpi::communicator world;
    int rank = world.rank();
    int size = world.size();

    string outmessage = "Hello, world! from process " + to_string(rank) + " of " + to_string(size);
    string inmessage;
    int sendto = (rank + 1) % size;
    int recvfrom = (rank + size - 1) % size;

    cout << outmessage << endl;

    if (!(rank % 2)) {
        world.send(sendto,0,outmessage);
        world.recv(recvfrom,0,inmessage);
    }
    else {
        world.recv(recvfrom,0,inmessage);
        world.send(sendto,0,outmessage);
    }

    cout << "[P_" << rank << "] process " << recvfrom << " said: \"" << inmessage << "\"" << endl;
    return 0;
}


File : phello3.f90

program phello3


    implicit none
    include 'mpif.h'

    integer, parameter :: BUFMAX=81
    character(len=BUFMAX) :: outbuf, inbuf, tmp
    integer :: rank, num_procs, ierr
    integer :: sendto, recvfrom
    integer :: status(MPI_STATUS_SIZE)

    call MPI_INIT(ierr)
    call MPI_COMM_RANK(MPI_COMM_WORLD, rank, ierr)
    call MPI_COMM_SIZE(MPI_COMM_WORLD, num_procs, ierr)

    outbuf = 'Hello, world! from process '
    write(tmp,'(i2)') rank
    outbuf = outbuf(1:len_trim(outbuf)) // tmp(1:len_trim(tmp))
    write(tmp,'(i2)') num_procs
    outbuf = outbuf(1:len_trim(outbuf)) // ' of ' // tmp(1:len_trim(tmp))

    sendto = mod((rank + 1), num_procs)
    recvfrom = mod(((rank + num_procs) - 1), num_procs)

    if (MOD(rank,2) == 0) then
        call MPI_SEND(outbuf, BUFMAX, MPI_CHARACTER, sendto, 0, MPI_COMM_WORLD, ierr)
        call MPI_RECV(inbuf, BUFMAX, MPI_CHARACTER, recvfrom, 0, MPI_COMM_WORLD, status, ierr)
    else
        call MPI_RECV(inbuf, BUFMAX, MPI_CHARACTER, recvfrom, 0, MPI_COMM_WORLD, status, ierr)
        call MPI_SEND(outbuf, BUFMAX, MPI_CHARACTER, sendto, 0, MPI_COMM_WORLD, ierr)
    endif

    print *, 'Process', rank, ': Process', recvfrom, ' said:', inbuf

    call MPI_FINALIZE(ierr)

end program phello3


À première vue, il semblerait qu'un nombre impair de processus puisse poser un problème. En effet, le processus pair 0 lance un envoi alors que l'autre processus pair N-1 tente de lancer une réception. Cependant, l'envoi fait par le processus 0 est correctement apparié à la réception lancée par le processus 1; puisque le processus impair 1 commence par une réception, la transaction sera sûrement complétée. Sur ce, le processus 0 reçoit le message du processus 1; il pourrait y avoir un certain délai (minime), mais il n'y a pas de risque d'interblocage. [~]$ mpicc -Wall phello3.c -o phello3

[~]$ mpirun -np 16 ./phello3
[P_1] process 0 said: "Hello, world! from process 0 of 16"]
[P_2] process 1 said: "Hello, world! from process 1 of 16"]
[P_5] process 4 said: "Hello, world! from process 4 of 16"]
[P_3] process 2 said: "Hello, world! from process 2 of 16"]
[P_9] process 8 said: "Hello, world! from process 8 of 16"]
[P_0] process 15 said: "Hello, world! from process 15 of 16"]
[P_12] process 11 said: "Hello, world! from process 11 of 16"]
[P_6] process 5 said: "Hello, world! from process 5 of 16"]
[P_13] process 12 said: "Hello, world! from process 12 of 16"]
[P_8] process 7 said: "Hello, world! from process 7 of 16"]
[P_7] process 6 said: "Hello, world! from process 6 of 16"]
[P_14] process 13 said: "Hello, world! from process 13 of 16"]
[P_10] process 9 said: "Hello, world! from process 9 of 16"]
[P_4] process 3 said: "Hello, world! from process 3 of 16"]
[P_15] process 14 said: "Hello, world! from process 14 of 16"]
[P_11] process 10 said: "Hello, world! from process 10 of 16"]

Plusieurs modèles sont décrits dans la page Web Introduction and Overview. Nous vous recommandons d'utiliser la fonction de communication qui s'applique à votre situation, plutôt que de la créer par vous-même.

Autres considérations

Nous avons abordé la conception et la syntaxe de programmes MPIMessage Passing Interface; il y a cependant beaucoup d'autres éléments à considérer dans le développement d'applications parallèles robustes avec MPIMessage Passing Interface. Sans être exhaustive, la liste suivante en donne un aperçu.

Lectures recommandées

  • GROPP, William, Ewing LUSK et Anthony SKJELLUM. Using MPIMessage Passing Interface: Portable Parallel Programming with the Message-Passing Interface, 2e éd, MIT Press, 1999.
    • méthodes d'appel indirectes en Fortran, C et C++
  • PACHECO, Peter S., Parallel Programming with MPIMessage Passing Interface, Morgan Kaufmann, 1997.
    • tutoriel avec langage C
  • BARNEY, Blaise Message Passing Interface (MPI), Lawrence Livermore National Labs.
  • KENDALL, Wes, et autres. mpitutorial.com
  • Institut du développement et des ressources en informatique scientifique, Formation "MPI" (en français).