MPI
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 charette, il est plus facile d'ajouter des bœufs que de trouver un plus gros bœuf.]
—Gropp, Lusk & Skjellum, Using MPI
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.
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.
Nous discuterons ici uniquement de la programmation avec mémoire distribuée sur grappe, avec MPI.
Qu'est-ce que MPI?
MPI (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. MPI est implémentée dans plusieurs bibliothèques, notamment Open MPI, 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.MPI, mpi4py, Rmpi, etc.).
Puisque MPI est une norme ouverte sans droits exclusifs, un programme MPI peut facilement être porté sur plusieurs ordinateurs différents. Les programmes MPI 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 MPI 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 MPI.
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 MPI 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 MPI. Notre but ici est de paralléliser le programme simple "Hello World" utilisé dans les exemples.
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
return(0);
}
#include <iostream>
using namespace std;
int main()
{
cout << "Hello, world!" << endl;
return 0;
}
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 MPI 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.
Cadre d'exécution
Un programme MPI 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 MPI 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 MPI_Init
pointent vers les variables argc
et argv
qui sont les arguments en ligne de commande. Comme pour toutes les fonctions MPI en C, la valeur retournée représente l'erreur de la fonction. En Fortran, les routines MPI retournent l'erreur dans l'argument IERR
.
On doit aussi appeler la fonction MPI_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 MPI_Init
au tout début du programme et MPI_Finalize
à la toute fin.
#include <stdio.h>
#include <mpi.h>
int main(int argc, char *argv[])
{
MPI_Init(&argc, &argv);
printf("Hello, world!\n");
MPI_Finalize();
return(0);
}
#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;
}
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 MPI_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 MPI_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 MPI_COMM_WORLD, soit un communicateur prédéfini par MPI 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.
#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);
}
#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;
}
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.
MPI 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 MPI_Send et MPI_Recv.
Un processus envoie des données par la fonction MPI_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;
- MPI_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 MPI. 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 MPI. En langage C : MPI_CHAR, MPI_FLOAT, MPI_SHORT, MPI_INT, etc. En Fortran :MPI_CHARACTER, MPI_INTEGER, MPI_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 MPI_Recv, on ajoute l'argument status : en C, l'argument réfère à une structure allouée MPI_Status et en Fortran, l'argument contient une matrice MPI_STATUS_SIZE de nombres entiers. À son retour, MPI_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 l'utilisation simple que nous faisons de MPI_Send et MPI_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.
#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);
}
#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;
}
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 MPI n'offre aucune garantie que MPI_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 MPI_Send et retournent sans avoir attendu leur livraison. Par contre, si la mémoire tampon n'était pas utilisée, notre code bloquerait. Chaque processus appellerait MPI_Send et attendrait que le processus voisin appelle MPI_Recv. Puisque le processus voisin serait aussi en attente d'une réponse de MPI_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 MPI, les appels MPI_Send et MPI_Recv sont des appels bloquants. MPI_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, MPI_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 MPI offre ou non l’accès à une mémoire tampon. Sur 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 MPI_Recv retourne. Par contre, MPI_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 MPI_Send en premier. Puisque la norme MPI 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.
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 MPI_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, MPI_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.
How do we rewrite our "Hello, World!" program to make it safe? A common solution to this kind of problem is to adopt an odd-even pairing and perform the communication in two steps. Since in our example communication is a rotation of data one rank to the right, we should end up with a safe program if all even ranked processes execute a send followed by a receive, while all odd ranked processes execute a receive followed by a send. The reader can easily verify that the sends and receives are properly paired avoiding any possibility of deadlock.
#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);
}
#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;
}
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
Is there still a problem here if the number of processors is odd? It might seem so at first as process 0 (which is even) will be sending while process N-1 (also even) is trying to send to 0. But process 0 is originating a send that is correctly paired with a receive at process 1. Since process 1 (odd) begins with a receive, that transaction is guaranteed to complete. When it does, process 0 will proceed to receive the message from process N-1. There may be a (very small!) delay, but there is no chance of a deadlock.
[~]$ 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"]
Note that many frequently-occurring communication patterns have been captured in the collective communication functions of MPI. If there is a collective function that matches the communication pattern you need, you should use it instead of implementing it yourself with MPI_Send and MPI_Recv.
Comments and Further Reading
This tutorial presented some of the key syntax, semantics, and design concepts associated with MPI programming. There is still a wealth of material to be considered in designing any serious parallel program, including but not limited to:
- MPI_Send/MPI_Recv variants (buffered, non-blocking, synchronous, etc.)
- collective communications (reduction, broadcast, barrier, scatter, gather, etc.)
- derived data types
- communicators and topologies
- one-sided communication and other features of MPI-2
- efficiency issues
- parallel debugging
- Tutorial on Boost MPI (in French)
Selected references
- William Gropp, Ewing Lusk, and Anthony Skjellum. Using MPI: Portable Parallel Programming with the Message-Passing Interface (2e). MIT Press, 1999.
- Comprehensive reference covering Fortran, C and C++ bindings
- Peter S. Pacheco. Parallel Programming with MPI. Morgan Kaufmann, 1997.
- Easy to follow tutorial-style approach in C.
- Blaise Barney. Message Passing Interface (MPI). Lawrence Livermore National Labs.
- Wes Kendall, Dwaraka Nath, Wesley Bland et al. mpitutorial.com.
- Various authors; IDRIS. Formation "MPI" (en français).