Calcul parallèle sous Linux avec PVM et MPI
par Rahul U. Joshi
Traduction : C. Le Cannellier
Cet article, traduit avec l'autorisation de son auteur, et publié initialement dans LinuxGazette (#65, juin 2001), a pour objectif de fournir une introduction à la programmation avec PVM et MPI, deux solutions très largement employées pour la réalisation de programmes parallèles basés sur les bibliothèques de passage de messages. Ces bibliothèques permettent d'utiliser un ensemble de machines Unix/Linux connectées en réseau comme une seule machine dédiée à la résolution d'un problème complexe.
1. Introduction au calcul parallèle
Le calcul parallèle est une technique de calcul dans laquelle plusieurs actions sont menées simultanément, de telle sorte que le temps de résolution du problème se trouve réduit. Jusqu'alors, le calcul parallèle était réservé aux simulations à grande échelle (modélisation moléculaire, simulation de systèmes d'armes nucléaires…), à la résolution de très longs calculs ou encore au traitement de très grands volumes de données.

Cependant, en raison de la baisse rapide du coût des matériels, le calcul parallèle est devenu une technique de plus en plus fréquemment employée dans des domaines plus généraux. Les serveurs multiprocesseurs existent depuis longtemps. Le calcul parallèle est également présent au sein même de votre propre PC: par exemple, un processeur graphique travaillant avant le processeur principal de l'unité centrale, afin de rendre plus efficacement une image à l'écran, est aussi une forme de calcul parallèle.

Quoi qu'il en soit, mis à part les composants matériels destinés au calcul parallèle, un support par des composants logiciels est également nécessaire afin de coordonner l'exécution simultanée de plusieurs codes de calcul. Une telle coordination est requise en raison des interdépendances existantes entre les différents codes. Ce dernier point deviendra plus clair lorsque nous aborderons un exemple.

La technique la plus couramment employée pour assurer une telle synchronisation est le passage de messages (
message passing), dans laquelle les programmes synchronisent leur exécution et, de façon plus générale, communiquent l'un avec l'autre en s'échangeant des messages. Ainsi, par exemple, un programme peut s'adresser à un autre programme de la sorte:"J'ai fini, voici le résultat intermédiaire, à toi de le traiter maintenant." Si tout ceci vous semble trop théorique, passons à un exemple concret:
2. Un tout petit problème
Dans ce chapitre, nous allons envisager un problème très simple, et envisager comment il sera possible d'accélérer son exécution grâce au calcul parallèle.

Le problème consiste à calculer la somme d'une liste de nombres entiers stockés dans un tableau. Posons qu'il y ait 100 entiers stockés dans le tableau "éléments". Comment allons-nous paralléliser ce programme? C'est-à-dire, comment allons nous procéder, de telle sorte que ce problème soit résolu par plusieurs programmes s'exécutant simultanément, de façon concurrente. Dans de nombreux cas, en raison des dépendances entre les données, la parallélisation devient une tâche ardue. Par exemple, si vous voulez calculer l'expression (a+b)*c, qui présente bien deux opérations distinctes, il n'est pas possible d'envisager une quelconque parallélisation en raison de l'ordre dans lequel les opérations
doivent être effectuées pour obtenir un résultat exact: il faut d'abord effectuer l'addition, puis la multiplication.

Heureusement, pour l'exemple que nous allons utiliser, la parallélisation est aisée. Supposons quatre processeurs ou quatre programmes qui vont travailler de façon simultanée à la résolution du problème. La méthode la plus simple consiste alors à scinder le tableau en quatre parties, le traitement de chacune étant confié à un programme donné. En conséquence, la parallélisation de ce problème s'effectue comme suit:
·        
Quatre programmes, que nous appellerons P0, P1, P2, P3 résoudront l'ensemble du problème.
·         P0 calculera la somme des éléments 0 à 24 du tableau, P1 celle des éléments 25 à 49, P2 de 50 à 74 et enfin P3 de 75 à 99.
·         Quand ces programmes se seront exécutés, il faudra un autre programme qui effectuera la somme des quatre résultats intermédiaires afin de calculer la solution du problème. De même, les éléments du tableau ne sont pas connus des programmes P0…P3, et par conséquent un autre programme doit également leur passer les valeurs de ces mêmes éléments. En somme, en plus des programmes P0 à P3, il nous faut un cinquième programme chargé de distribuer les données, collecter les résultats et synchroniser les exécutions. Un tel programme est appelé programme maître, et les programmes P0 à P3 sont appelés programmes esclaves. Une telle organisation est dénommée paradigme (ou modèle) maître-esclave.

Avec ce modèle présent à l'esprit, attachons-nous à l'écriture des algorithmes pour les programmes maître et esclaves.

/* Algorithme pour le programme maître */
initialization du tableau `items'.

/* envoi des données aux esclaves */
for i = 0 to 3
Send items[25*i] to items[25*(i+1)-1] to slave Pi
end for

/* collecte des résultats en provenance des esclaves */
for i = 0 to 3
Receive the result from slave Pi in result[i]
end for

/* calcul du résultat final */
sum = 0
for i = 0 to 3
sum = sum + result[i]
end for

print sum

L'algorithme du programme esclave peut être codé comme suit:

/* Algorithme du programme esclave */

réception de 25 éléments en provenance du maître dans `items'

/* calcul du résultat intermédiaire */
sum = 0
for i = 0 to 24
sum = sum + items[i]
end for

/* émission du résultat intermédiaire vers le maître */
send `sum' as the intermediate result to the master

3. Implémentation avec PVM
L'algorithme ayant été mis au point, abordons maintenant son implémentation. Sur quel matériel exécuterons-nous ce programme ? Probablement peu de personnes parmi nous ont accès à une machine capable d'exécuter des programmes parallèles.

En tout état de cause, il n'existe aucune contrainte matérielle empêchant l'implémentation de ce programme. Une seule machine ou un ensemble de machines en réseau fera parfaitement l'affaire grâce à PVM, une solution logicielle qui nous permet d'utiliser des machines interconnectées pour l'exécution de programmes parallèles. PVM est l'acronyme de
Parallel Virtual Machine (Machine Parallèle Virtuelle). PVM permet de créer des programmes de toutes sortes qui s'exécutent de façon concurrente sur une ou plusieurs machines. PVM offre un ensemble de fonctions qui permettent de passer des messages de synchronisation entre les processes. PVM fonctionnera même sur une seule machine, monoprocesseur, bien qu'il n'y aura pas dans ce cas de réel traitement parallèle. Mais cette option s'avère intéressante pour le développement ou la formation. Nous verrons plus tard comment faire du "vrai" calcul parallèle avec PVM.

Afin de pouvoir utiliser une PVM, il vous faut installer le logiciel PVM sur votre machine Linux. PVM est disponible sous forme de packages RPM et est même présent sur les CD de nombreuses distributions Linux, de telle sorte qu'il peut être installé aisément.

Supposant que vous ayez installé PVM sur votre machine, créez les répertoires suivants dans votre répertoire
$HOME: ~/pvm3/bin/LINUX/. Pourquoi? Parce que PVM demande que les exécutables soient copiés dans ce répertoire. Cette étape effectuée, votre configuration est prête. Vous pouvez la tester en lançant la commande pvm, qui démarrera la console PVM, à partir de laquelle vous pourrez passer des commandes à la machine PVM et obtenir des informations d'état. Si tout se déroule normalement, vous devez voir apparaître l'invite pvm>. Vous pouvez alors passer la commande conf. L'affichage doit être semblable à ce qui suit:

pvm> conf
conf
1 host, 1 data format
HOST DTID ARCH SPEED DSIG
joshicomp 40000 LINUX 1000 0x00408841

Qu'est-ce que cela signifie ? L'environnement PVM vous permet de considérer un groupe de machines interconnectées comme une seule machine virtuelle, offrant des capacités de traitement supérieures à celle des machines considérées individuellement. En conséquence, PVM répartit des processes sur le réseau de machines. Mais par défaut, PVM ne considère que la machine sur laquelle vous travaillez, et l'ensemble des processes ne sera donc distribué que sur cette machine. Nous verrons ultérieurement comment ajouter d'autres n uds à la configuration. Pour l'instant, sortons de l'environnement PVM en entrant à la console la commande
halt.
3.1 Un Programme de démonstration
Maintenant que notre configuration PVM est opérationnelle, voyons comment écrire des programmes capables de l'exploiter. Ces programmes peuvent être écrits en C ou en Fortran. Nous utiliserons le C pour nos exemples. L'utilisation de PVM s'effectue par le biais d'appels à des primitives à partir du code C, et en ajoutant la bibliothèque PVM lors de l'édition de liens.

Afin de nous familiariser avec PVM, nous allons écrire une simple application en modèle maître/esclave. Le maître va envoyer une chaîne de caractères en minuscules à l'esclave, lequel convertira cette chaîne en majuscules avant de la renvoyer au maître.

Pour compiler les programmes, disponibles dans un fichier
tar sur le CD, lancez la commande make -f makefile.demo.

1 /* -------------------------------------------------------------------- *
2 * master_pvm.c *
3 * *
4 * programme maître pour la simple démonstration de PVM *
5 * -------------------------------------------------------------------- */
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <pvm3.h> /* inclusion de la bibliothèque */
9 #include <string.h>

10 int main()
11
12 int mytid; /* identifiant du maître */
13 int slave_tid; /* identifiant de l'esclave */
14 int result;
15 char message[] = "hello pvm";
16
17 /* déclaration du maître à PVM et obtention de l'identifiant */
18 mytid = pvm_mytid();

19 /* lancement de l'esclave */
20 result = pvm_spawn("slave_pvm", (char**)0, PvmTaskDefault,
21 "", 1, &slave_tid);

22 /* vérification du lancement de l'esclave */
23 if(result != 1)
24
25 fprintf(stderr, "Error: Cannot spawn slave.n");

26 /* sortie de PVM */
27 pvm_exit();
28 exit(EXIT_FAILURE);
29

30 /* initialisation des données à envoyer à l'esclave */
31 pvm_initsend(PvmDataDefault);

32 /* insertion de la chaîne de caractères dans le buffer */
33 pvm_pkstr(message);

34 /* envoi de la chaîne à l'esclave avec une étiquette=0 */
35 pvm_send(slave_tid, 0);

36 /* en attente du retour de la part de l'esclave */
37 pvm_recv(slave_tid, 0);

38
39 /* extraction du résultat */
40 pvm_upkstr(message);

41 /* visualisation du résultat */
42 printf("Data from the slave : %sn", message);

43 /* sortie de PVM */
44 pvm_exit();
45
46 exit(EXIT_SUCCESS);
47 /* end main() */

48 /* end master_pvm.c */


1 /* -------------------------------------------------------------------- *
2 * slave_pvm.c *
3 * *
4 * Programme esclave pour la simple démonstration PVM   *
5 * -------------------------------------------------------------------- */
6 #include <stdio.h>
7 #include <ctype.h>
8 #include <stdlib.h>
9 #include <pvm3.h>

10 #define MSG_LEN 20
11 void convert_to_upper(char*);

12 int main()
13
14 int mytid;
15 int parent_tid;
16 char message[MSG_LEN];

17 /* déclaration auprès de PVM */
18 mytid = pvm_mytid();

19 /* obtention de l'identifiant du maître */
20 parent_tid = pvm_parent();

21 /* réception des données en provenance du maître */
22 pvm_recv(parent_tid, 0);
23 pvm_upkstr(message);

24 /* conversion des données */
25 convert_to_upper(message);

26 /* émission vers le maître */
27 pvm_initsend(PvmDataDefault);

28 pvm_pkstr(message);
29 pvm_send(parent_tid, 0);

30 /* sortie de PVM */
31 pvm_exit();
32
33 exit(EXIT_SUCCESS);
34 /* end main() */

35 /* fonction de conversion de la chaîne de caractères */
36 void convert_to_upper(char* str)
37
38 while(*str != '0')
39
40 *str = toupper(*str);
41 str++;
42
43 /* end convert_to_upper() */

44 /* end slave_pvm.c */


1 # Makefile pour les programmes de démonstration PVM

2 .SILENT :
3 # chemin d'accès aux fichiers d'inclusion et bibliothèques
4 INCDIR=-I/usr/share/pvm3/include
5 LIBDIR=-L/usr/share/pvm3/lib/LINUX

6 # édition de liens avec la bibliothèque
7 LIBS=-lpvm3
8 CFLAGS=-Wall
9 CC=gcc
10 TARGET=all

11 # localisation du code applicatif PVM
12 PVM_HOME=$(HOME)/pvm3/bin/LINUX

13 all : $(PVM_HOME)/master_pvm $(PVM_HOME)/slave_pvm

14 $(PVM_HOME)/master_pvm : master_pvm.c
15 $(CC) -o $(PVM_HOME)/master_pvm master_pvm.c $(CFLAGS) $(LIBS) par
16 $(INCDIR) $(LIBDIR)

17 $(PVM_HOME)/slave_pvm : slave_pvm.c
18 $(CC) -o $(PVM_HOME)/slave_pvm slave_pvm.c $(CFLAGS) $(LIBS) par
19 $(INCDIR) $(LIBDIR)


Une fois vos programmes compilés, vous devez les copier dans le répertoire PVM, c'est ce que fait pour vous le makefile de notre exemple.
Pour exécuter l'application, vous devez démarrer la machine PVM. Lancez la commande
pvm pour démarrer la console, puis tapez quit à l'invite de commande pvm>, comme suit:
pvm> quit
quit

Console: exit handler called
pvmd still running.

Remarquez bien la dernière ligne, qui nous montre bien que le démon PVM, pvmd, reste actif. Nous pouvons alors lancer notre programme, et constater qu'il fonctionne bien:

[rahul@joshicomp rahul]$ cd ~/pvm3/bin/LINUX/
[rahul@joshicomp LINUX]$ ./master_pvm
Data from the slave : HELLO PVM
[rahul@joshicomp LINUX]$

3.2 Explication du fonctionnement du programme
Dans ce chapitre, nous allons détailler le fonctionnement de ce programme. La toute première chose est de spécifier dans le source le fichier d'inclusion pvm3.h. Ceci est fait aux lignes 8 et 9 des programmes maître et esclave, respectivement.

Aussi, lors de l'édition de liens, il faut ajouter la bibliothèque PVM, ce qui est fait par l'option
lpvm3 à la ligne 7 du makefile. Aux lignes 4 et 5 de ce même makefile, nous avons également spécifié les chemins d'accès à la bibliothèque et aux fichiers d'inclusion.

Dans le programme maître, nous obtenons notre propre identifiant de tâche en appelant
pvm_mytid(). PVM assigne à chaque tâche un identifiant unique, codé sur 32 bits, à la manière dont Linux assigne un PID à chaque process. Cet identifiant de tâche est ensuite utilisé dans la communication inter processes applicatifs.

Quoi qu'il en soit, notre programme maître n'utilise pas son identifiant, stocké dans
mytid. Notre but ici était juste de faire un appel à la fonction pvm_mytid(), qui enregistre le process dans la machine PVM et induit la génération de l'identifiant du process. En l'absence d'enregistrement explicite, le premier appel à une routine PVM déclenche l'enregistrement.

Nous faisons ensuite appel à
pvm_spawn() afin de créer le process esclave. Le premier paramètre passé à cette fonction est le nom du programme que l'esclave doit exécuter, le second paramètre contient l'ensemble des arguments que l'on désire passer à l'esclave, à la manière de argv en C standard. Ne voulant pas passer d'arguments, nous laissons cette valeur à 0. Le troisième paramètre est un moyen de contrôler quand et comment PVM va démarrer l'esclave. Comme nous ne disposons que d'une seule machine, nous allons donner à ce paramètre la valeur PvmTaskDefault, indiquant à PVM d'appliquer le schéma par défaut pour le démarrage de l'esclave. Le quatrième paramètre est le nom du n ud ou de l'architecture sur laquelle nous voulons voir le programme esclave être exécuté, et nous laissons ici ce paramètre non renseigné, ce dernier n'étant utilisable que si le schéma de démarrage choisi n'est pas le schéma par défaut. Le cinquième paramètre indique le nombre d'esclaves à initialiser, et le sixième est un pointeur vers un tableau ou sont stockés les identifiants des esclaves initialisés. Pvm_spawn() renvoie le nombre d'esclaves effectivement initialisés, valeur dont nous vérifions l'exactitude.

Un message PVM comporte deux segments, les données, et une étiquette qui identifie le type de message. L'étiquette permet de faire la différence entre les messages. Par exemple, dans le programme d'addition que nous allons réaliser, nous allons supposer que chaque esclave envoie au maître un entier correspondant à la somme des éléments reçus. Il est tout à fait concevable que l'esclave rencontre une erreur et veuille envoyer au maître un entier, qui est cette fois un code d'erreur. Comment le maître fait-il alors pour caractériser l'information reçue? C'est là que les étiquettes prennent tout leur sens. Il est possible d'assigner au message porteur du résultat une étiquette définie par le programmeur par un
#define dans un header, et d'assigner au message porteur d'un code d'erreur une autre étiquette définie de la même façon. Le récepteur (le maître) pourra alors effectuer son traitement en fonction de cette information.

Pour envoyer un message, il faut préalablement initialiser le buffer d'émission. Cela est effectué en appelant la fonction
pvm_initsend(). Le paramètre passé à cette fonction spécifie le type d'encodage à utiliser. Si nous voulons échanger des données entre deux machines aux architectures différentes, il nous faut alors procéder à un encodage/décodage afin de garantir la transmission sans erreur des données. La valeur par défaut de ce paramètre est PvmDataDefault, qui définit un type d'encodage propre à assurer un échange sans erreur entre des architectures diverses.

Dès que le buffer a été initialisé, nous devons y placer les données et l'encoder. Dans notre cas, les données sont une chaîne de caractères: nous utilisons donc la fonction
pvm_pkstr() pour effectuer cet encodage, et plaçons le résultat dans le buffer. Si nous avions eu à envoyer, par exemple, un entier, la fonction à utiliser aurait été pvm_pkint(). Il existe encore d`autres fonctions d'encodage pour d'autres types de données.

Une fois la donnée encodée, un appel à
pvm_send() assure l'envoi du message. Le premier argument est l'identifiant du process auquel le message va être envoyé, et le second argument est l'étiquette associée au message. Dans notre exemple, cette valeur est laissée à 0, un seul type de message étant employé.

Une fois les données reçues, l'esclave procède au traitement et au retour vers le maître comme nous allons le voir. Côté maître, un appel à
pvm_recv() nous permet de recevoir cet envoi. Là encore, les paramètres associés sont l'identifiant et l'étiquette correspondant au message attendu. Cette fonction est synchrone, c'est-à-dire qu'une fois appelée, elle reste en attente jusqu'à réception du message.

Cela a pour conséquence de mettre le maître en attente de l'esclave. Une fois les données reçues, elles sont toujours dans le buffer et demandent à être décodées afin d'être utilisables. Cette opération est assurée par la fonction
pvm_upkstr(). Le résultat est maintenant prêt à être affiché.

Avant de sortir du programme, il faut signaler à PVM la fin d'utilisation de telle sorte que les ressources allouées au programme puissent être restituées. Cette dernière tâche est assurée par un appel à
pvm_exit(). L'exécution du programme peut alors prendre fin.

Le fonctionnement de l'esclave est aisé à comprendre. Il prend l'identifiant du maître en appelant
pvm_parent(), reçoit la chaîne de caractères à traiter et retourne le résultat.
3.3 Le programme d'addition
Maintenant que nous maîtrisons les bases de la programmation PVM, nous allons implémenter l'algorithme d'addition. Il y aura quatre esclaves et un maître. Le maître commencera par initialiser les esclaves et leur enverra leur part de travail. Les esclaves vont réaliser ce travail (somme) et renvoyer les résultats vers le maître. En conséquence, deux types de messages vont être échangés, le premier lors de l'envoi des données vers les esclaves et pour lesquels l'étiquette MSG-DATA sera utilisée, et le second lors de la ré-émission des données vers le maître pour lesquels l'étiquette MSG-RESULT sera utilisée.

1 /* -------------------------------------------------------------------- *
2 * common.h *
3 * *
4 * cet en-tête définit des constantes communes.
*
5 * -------------------------------------------------------------------- */
6 #ifndef COMMON_H
7 #define COMMON_H

8 #define NUM_SLAVES 4 /* nombre d'esclaves */
9 #define SIZE 100 /* taille des données */
10 #define DATA_SIZE (SIZE/NUM_SLAVES) /* taille pour chaque esclave */

11 #endif
12 /* end common.h */

1 /* -------------------------------------------------------------------- *
2 * tags.h *
3 * *
4 * Cet en-tête définit les étiquette utilisés pour les messages *
5 * -------------------------------------------------------------------- */
6 #ifndef TAGS_H
7 #define TAGS_H

8 #define MSG_DATA 101 /* donnée du maître vers l'esclave */
9 #define MSG_RESULT 102 /* résultat de l'esclave vers le maître */

10 #endif

11 /* end tags.h */


1 /* -------------------------------------------------------------------- *
2 * master_add.c *
3 * *
4 * Programme maître pour l'addition des éléments d'un tableau avec PVM *
5 * -------------------------------------------------------------------- */
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <pvm3.h> /* constantes et déclarations PVM */
9 #include "tags.h" /* étiquettes des messages */
10 #include "common.h" /* constantes communes */

11 int get_slave_no(int*, int);

12 int main()
13
14 int mytid;
15 int slaves[NUM_SLAVES]; /* tableau pour les Id des esclaves */
16 int items[SIZE]; /* données à traiter */
17 int result, i, sum;
18 int results[NUM_SLAVES]; /* résultat des esclaves */

19 /* enregistrement dans PVM */
20 mytid = pvm_mytid();

21 /* initialisation du tableau `items' */
22 for(i = 0; i < SIZE; i++)
23 items[i] = i;

24 /* initialisation des esclaves */
25 result = pvm_spawn("slave_add", (char**)0, PvmTaskDefault,
26 "", NUM_SLAVES, slaves);

27 /* vérification si les esclaves sont en nombre correct */
28 if(result != NUM_SLAVES)
29
30 fprintf(stderr, "Error: Cannot spawn slaves.n");
31 pvm_exit();
32 exit(EXIT_FAILURE);
33

34 /* distribution des données aux esclaves */
35 for(i = 0; i < NUM_SLAVES; i++)
36
37 pvm_initsend(PvmDataDefault);
38 pvm_pkint(items + i*DATA_SIZE, DATA_SIZE, 1);
39 pvm_send(slaves[i], MSG_DATA);
40

41 /* réception du résultat en provenance des esclaves */
42 for(i = 0; i < NUM_SLAVES; i++)
43
44 int bufid, bytes, type, source;
45 int slave_no;
46
47 /* réception du message en provenance d'un esclave quelconque */
48 bufid = pvm_recv(-1, MSG_RESULT);

49 /* obtention des informations sur le message */
50 pvm_bufinfo(bufid, &bytes, &type, &source);
51
52 /* on determine quell esclave a envoyé le message */
53 slave_no = get_slave_no(slaves, source);

54 /* stockage des résultats au bon endroit */
55 pvm_upkint(results + slave_no, 1, 1);
56

57 /* résultat final */
58 sum = 0;
59 for(i = 0; i < NUM_SLAVES; i++)
60 sum += results[i];

61 printf("The sum is %dn", sum);

62 /* sortie de PVM */
63 pvm_exit();

64 exit(EXIT_SUCCESS);
65 /* end main() */
66
67 /* fonction retournant le numéro d'un esclave en fonction de son ID */
68 int get_slave_no(int* slaves, int task_id)
69
70 int i;

71 for(i = 0; i < NUM_SLAVES; i++)
72 if(slaves[i] == task_id)
73 return i;

74 return -1;
75 /* fin de get_slave_no() */

76 /* fin de master_add.c */


1 /* -------------------------------------------------------------------- *
2 * slave_add.c *
3 * *
4 * Programme esclave additionnant les éléments d'un tableau avec PVM *
5 * -------------------------------------------------------------------- */
6 #include <stdlib.h>
7 #include <pvm3.h>
8 #include "tags.h"
9 #include "common.h"

10 int main()
11
12 int mytid, parent_tid;
13 int items[DATA_SIZE]; /* données envoyées par le maître */
14 int sum, i;
15
16 /* enregistrement dans PVM */
17 mytid = pvm_mytid();

18 /* obtention de l'ID du maître */
19 parent_tid = pvm_parent();

20 /* réception des données en provenance du maître */
21 pvm_recv(parent_tid, MSG_DATA);
22 pvm_upkint(items, DATA_SIZE, 1);

23 /* on effectue la somme des éléments */
24 sum = 0;
25 for(i = 0; i < DATA_SIZE; i++)
26 sum = sum + items[i];

27 /* envoi du résultat au maître */
28 pvm_initsend(PvmDataDefault);
29 pvm_pkint(&sum, 1, 1);
30 pvm_send(parent_tid, MSG_RESULT);

31 /* sortie de PVM */
32 pvm_exit();
33
34 exit(EXIT_SUCCESS);
35 /* fin de main() */


1 # Makefile pour le programme PVM d'addition - makefile.add

2 .SILENT :
3 # chemins d'accès aux fichiers d'inclusion et aux bibliothèques
4 INCDIR=-I/usr/share/pvm3/include
5 LIBDIR=-L/usr/share/pvm3/lib/LINUX

6 # edition de liens
7 LIBS=-lpvm3
8 CFLAGS=-Wall
9 CC=gcc
10 TARGET=all

11 # localisation des executables PVM
12 PVM_HOME=$(HOME)/pvm3/bin/LINUX

13 all : $(PVM_HOME)/master_add $(PVM_HOME)/slave_add

14 $(PVM_HOME)/master_add : master_add.c common.h tags.h
15 $(CC) -o $(PVM_HOME)/master_add master_add.c $(CFLAGS) $(LIBS) par
16 $(INCDIR) $(LIBDIR)
17
18 $(PVM_HOME)/slave_add : slave_add.c common.h tags.h
19 $(CC) -o $(PVM_HOME)/slave_add slave_add.c $(CFLAGS) $(LIBS) par
20 $(INCDIR) $(LIBDIR)

En raison de la simplicité du programme esclave, nous allons commencer par celui-ci. Ce programme reçoit 25 éléments en provenance du maître dans le tableau
items, effectue leur somme et retourne un message au maître avec l'étiquette MSG_RESULT.

Abordons maintenant le cas du maître. Nous définissons un tableau
slave, de taille NUM_SLAVES, qui va contenir les identifiants des esclaves initialisés par le parent, et un autre tableau, results, dans lequel sont stockés les résultats venant des esclaves. Le maître initialise les tableaux avant de lancer les esclaves, puis distribue les données. Dans l'appel à pvm_pkint(), ligne 38, le premier paramètre est le pointeur vers le tableau où sont stockés les résultats, le second paramètre est le nombre d'entiers à encoder et le troisième est le "stride". Le "stride", ou décalage, définit le nombre d'éléments à sauter lors de l'encodage des éléments. Quand cette valeur est à 1, les éléments sont pris consécutivement, quand elle est à 2, PVM passe deux éléments entre chaque élément encodé, ce qui a pour résultat de ne prendre que les éléments avec un indice pair (0, 2, 4, …). Nous conservons ici cette valeur à 1.

Quand les données ont été distribuées aux esclaves, le maître doit attendre jusqu'à ce que les esclaves aient retourné les résultats intermédiaires. Une solution pour la réception de ces résultats est que le maître collecte d'abord les résultats en provenance de l'esclave 0, puis le 1, et ainsi de suite. Bien qu'ayant l'avantage de la simplicité, cette solution n'est peut-être pas la meilleure, l'ordre de disponibilité des résultats n'étant pas garanti. Il est donc préférable de donner au maître la capacité de répondre aux messages de tout esclave. C'est ce que nous allons entreprendre ici.

Lors de l'appel à
pvm-recv() ligne 48, nous savons que le premier paramètre est l'identifiant de la source du message. Si cette valeur est positionnée à 1, tout message (c'est-à-dire en provenance de tout process) avec l'étiquette MSG_RESULT sera accepté par le maître.

Le message reçu, avec ses informations de contrôle est placé dans un buffer appelé
active receive buffer. L'appel retourne un unique identifiant pour ce buffer. Nous voulons connaître qui est l'émetteur du message, de telle sorte que nous puissions stocker les données au bon endroit dans le tableau final results.

La fonction
pvm_bufinfo() renvoie des informations concernant le message dans le buffer, comme l'étiquette, la taille en octets et l'identifiant de l'émetteur. Avec cet identifiant, nous savons dans quelle case du tableau results ranger les données reçues.
3.4 Travailler avec PVM
Dans le cadre du développement d'applications parallèles, il est probable que vous rencontriez des bogues. Et dans le cadre du débogage, vous aurez à réinitialiser l'état du système avant de redémarrer. Sous la console PVM, vous pouvez utiliser la commande halt qui tue le démon pvm. Tous les processes seront alors arrêtés, ou vous pouvez les stopper avec la commande kill.

Dans le cas où un réseau de machines est à votre disposition, vous pouvez alors faire du calcul parallèle "pour de vrai". Il vous faudra alors installer PVM sur toutes les machines que vous souhaitez utiliser, et employer la commande
add à partir de la console PVM afin d'ajouter des n uds à votre machine virtuelle. Alors PVM se chargera de la répartition des tâches sur l'ensemble de n uds, et le traitement parallèle effectif deviendra une réalité.
4. Implémentation avec MPI
Nous avons vu dans la section précédente l'implémentation du programme d'addition avec PVM. Nous allons maintenant envisager une autre approche, qui peut être utilisée dans le développement d'applications parallèles. Cette approche consiste à utiliser MPI. MPI est un acronyme signifiant Message Passing Interface (Interface de Passage de Messages). MPI est un standard développé pour le développement d'applications parallèles portables. MPI offre des fonctions pour échanger des messages, ainsi que de nombreuses autres fonctions. Il faut noter que contrairement à PVM, qui est un environnement, MPI est un standard, de telle sorte que de nombreuses implémentations existent. Nous allons utiliser dans nos exemples une implémentation de MPI appelée LAM (Local Area Multicomputer). Cette implémentation est disponible pour toutes les distributions Linux majeures, sous forme de package, de telle sorte que l'installation ne soulève pas de difficulté particulière.

Une fois le package installé, créez un fichier nommé
conf.lam dans /usr/boot et renseignez-le avec la ligne suivante: lamd $inet_topo. Le même répertoire contiendra également un fichier nommé bhost.def, lequel contiendra la ligne: localhost. Il ne nous reste plus désormais qu'à tester cette nouvelle installation: dans un shell, entrez la commande lamboot, et si tout se passe bien, vous devez voir apparaître quelque chose comme cela:
[rahul@joshicomp boot]$ lamboot

LAM 6.3.1/MPI 2 C++/ROMIO - University of Notre Dame

[rahul@joshicomp boot]$

Supposant maintenant que LAM/MPI est correctement installé sur votre machine, nous allons passer à l'écriture d'un programme.
4.1 Ecriture d'un programme MPI
Suivant toujours le modèle maître/esclave, nous allons écrire un programme qui évalue l'expression (a+b)*(c-d). Le maître, assurant l'interface utilisateur, lira les valeurs. Puis, un esclave calculera a+b tandis que l'autre calculera c-d. Voici les sources:

1 /* -------------------------------------------------------------------- *
2 * mpi_demo.c *
3 * *
4 * Programme de démonstration MPI permettant d'évaluer une expression.
*
5 * -------------------------------------------------------------------- */
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <lam/mpi.h> /* définitions MPI */

9 #define MSG_DATA 100 /* message du maître aux esclaves */
10 #define MSG_RESULT 101 /* message des esclaves au maître */

11 #define MASTER 0 /* rank du maître */
12 #define SLAVE_1 1 /* rank du premier esclave */
13 #define SLAVE_2 2 /* rank du second esclave */

14 /* fonctions d'exécution du maître et des deux esclaves */
15 void master(void);
16 void slave_1(void);
17 void slave_2(void);

18 int main(int argc, char** argv)
19
20 int myrank, size;
21
22 /* initialisation de MPI */
23 MPI_Init(&argc, &argv);

24 /* obtention de la taille du communicateur, ou du nombre de processes */
25 MPI_Comm_size(MPI_COMM_WORLD, &size);

26 /* vérification du bon nombre de processes */
27 if(size != 3)
28
29 fprintf(stderr, "Error: Three copies of the program should be run.n");
30 MPI_Finalize();
31 exit(EXIT_FAILURE);
32
33
34 /* obtention du rank du process */
35 MPI_Comm_rank(MPI_COMM_WORLD, &myrank);

36 /* exécution des tâches en fonction du rank */
37 if(myrank == MASTER)
38 master();
39 else if(myrank == SLAVE_1)
40 slave_1();
41 else
42 slave_2();

43 /* sortie de MPI */
44 MPI_Finalize();

45 exit(EXIT_SUCCESS);
46 /* fin de main() */

47 /* fonction exécutant les tâches du maître */
48 void master(void)
49
50 int a, b, c, d;
51 int buf[2];
52 int result1, result2;
53 MPI_Status status;

54 printf("Enter the values of a, b, c, and d: ");
55 scanf("%d %d %d %d", &a, &b, &c, &d);

56 /* envoi de a et b au premier esclave */
57 buf[0] = a;
58 buf[1] = b;
59 MPI_Send(buf, 2, MPI_INT, SLAVE_1, MSG_DATA, MPI_COMM_WORLD);

60 /* envoi de c et d au second esclave */
61 buf[0] = c;
62 buf[1] = d;
63 MPI_Send(buf, 2, MPI_INT, SLAVE_2, MSG_DATA, MPI_COMM_WORLD);

64 /* réception des résultats en provenance des esclaves */
65 MPI_Recv(&result1, 1, MPI_INT, SLAVE_1, MSG_RESULT,
66 MPI_COMM_WORLD, &status);
67 MPI_Recv(&result2, 1, MPI_INT, SLAVE_2, MSG_RESULT,
68 MPI_COMM_WORLD, &status);

69 /* résultat final */
70 printf("Value of (a + b) * (c - d) is %dn", result1 * result2);
71 /* end master() */

72 /* fonction exécutant les tâches du premier esclave*/
73 void slave_1(void)
74
75 int buf[2];
76 int result;
77 MPI_Status status;
78
79 /* réception des valeurs en provenance du maître*/
80 MPI_Recv(buf, 2, MPI_INT, MASTER, MSG_DATA, MPI_COMM_WORLD, &status);
81
82 /* find a + b */
83 result = buf[0] + buf[1];

84 /* envoi des résultats au maître */
85 MPI_Send(&result, 1, MPI_INT, MASTER, MSG_RESULT, MPI_COMM_WORLD);
86 /* end slave_1() */

87 /* fonction exécutant les tâches du second esclave*/
88 void slave_2(void)
89
90 int buf[2];
91 int result;
92 MPI_Status status;
93
94 /* réception des valeurs en provenance du maître */
95 MPI_Recv(buf, 2, MPI_INT, MASTER, MSG_DATA, MPI_COMM_WORLD, &status);
96
97 /* find c - d */
98 result = buf[0] - buf[1];

99 /* envoi des résultats au maître */
100 MPI_Send(&result, 1, MPI_INT, MASTER, MSG_RESULT, MPI_COMM_WORLD);
101 /* end slave_2() */

102 /* end mpi_demo.c */


1 # Makefile pour le programme de démonstration MPI - makefile.mpidemo
2 .SILENT:
3 CFLAGS=-I/usr/include/lam -L/usr/lib/lam
4 CC=mpicc

5 mpi_demo : mpi_demo.c
6 $(CC) $(CFLAGS) mpi_demo.c -o mpi_demo

Pour compiler ce programme, passez la commande
make f makefile.mpidemo. Une fois le code compilé, il vous faudra démarrer lam pour pouvoir l'exécuter, ce qui va être fait avec la commande lamboot. Il n'y aura plus alors qu'à passer la commande mpirun np 3 mpi_demo.
[rahul@joshicomp parallel]$ lamboot

LAM 6.3.1/MPI 2 C++/ROMIO - University of Notre Dame

[rahul@joshicomp parallel]$ mpirun -np 3 mpi_demo
Enter the values of a, b, c, and d: 1 2 3 4
Value of (a + b) * (c - d) is -3
[rahul@joshicomp parallel]$
4.2 Explication du programme
Pour utiliser l'environnement MPI et ses fonctions, vous devez déclarer le fichier d'inclusion mpi.h dans vos sources, comme il est fait ligne 8.

Dans le cas de PVM, les divers processes sont identifiés avec leurs ID, tandis que sous MPI, chaque process se voit assigné un entier, unique, nommé
rank. La première valeur de rank est 0, et cette valeur, propre à chaque process. Elle est ensuite utilisée pour identifier le process et communiquer avec.

Ensuite, chaque process est membre d'un groupe de communication. Un groupe de communication réunit l'ensemble des processes ayant la capacité de communiquer entre eux. Par défaut, tous les processes sont membres du groupe
MPI_COMM_WORLD. La création de nouveaux groupes de communication augmentant significativement la complexité d'un programme, nous nous limiterons dans notre exemple à l'utilisation du groupe par défaut.

Tout programme MPI doit d'abord appeler la fonction
MPI_Init(), qui est à la base de l'enregistrement du process dans le système MPI. Ensuite, il nous faut déterminer la taille du communicateur, c'est-à-dire trouver le nombre de processes utilisant la fonction MPI_Comm_size(). Le premier paramètre de cette fonction est le communicateur, et le second est un pointeur vers un entier dans lequel la taille sera retournée. Dans notre cas, nous avons besoin de 3 processes, un maître et deux esclaves.

Ensuite, nous obtenons le
rank en appelant MPI_Comm_rank(). Les trois processes auront comme ranks 0, 1, et 2, respectivement. Tous les processes sont sur un pied d'égalité, c'est-à-dire qu'il n'existe pas de relation maître/esclave entre eux. Nous choisissons le process de rank 0 comme étant le maître, et les processes de rank 1 et 2 comme esclaves. Nous pouvons également voir que le programme contient le code du maître et des deux esclaves, et nous choisissons quelle partie exécuter en fonction du rank.

Remarquez qu'il n'y a pas de filiation de processes comme avec PVM, et comme nous allons le voir, nous décidons de choisir le nombre de processes à générer en fonction d'un argument passé sur la ligne de commande.

Une fois cette opération achevée, nous allons appeler
MPI_Finalize() pour effectuer une sortie dans les règles. Abordons maintenant le cas du maître. Après saisie des valeurs a, b, c, d par l'utilisateur, il faut envoyer a et b vers l'esclave 1 et c et d vers l'esclave 2. Au lieu d'envoyer les valeurs séparément, nous allons plutôt les placer dans un tableau que nous enverrons à chaque esclave. Il est préférable de procéder ainsi, de façon à limiter le plus possible les échanges de messages, afin d'obtenir les meilleures performances.
Lorsque le tableau est rempli, nous n'avons pas à nous préoccuper de l'encodage, car, contrairement à PVM, ces opérations sont réalisées de façon masquée. Nous pouvons donc appeler directement
MPI_Send() pour envoyer les données. En ligne 59, le premier paramètre est l'adresse du buffer, le second le nombre d'éléments contenus dans le message, et le troisième la spécification du type de donnée contenue dans le buffer (MPI_INT dans ce cas). Viennent ensuite le rank du process auquel nous voulons que le message soit envoyé, puis l'étiquette associée au message, similaire dans ce cas à l'étiquette PVM. Enfin, le dernier paramètre est le groupe de communication dont le récepteur est membre, MPI_COMM_WORLD dans ce cas.

Une fois les données distribuées aux esclaves, le maître doit attendre le retour des résultats. Pour plus de simplicité, nous recevons le message de l'esclave 1, puis celui de l'esclave 2. Pour ce faire, nous utilisons la fonction
MPI_Recv(). Comme précédemment, l'encodage est effectué de façon transparente. Le premier argument, en ligne 65, est l'adresse du buffer dans lequel les données vont être récupérées, le second argument est le nombre d'éléments contenus dans le buffer, 1 dans notre cas. Ensuite, le type de donnée (MPI_INT). Les trois paramètres qui suivent caractérisent le rank de la source du message, l'étiquette du message attendu et le communicateur dont l'émetteur est membre. Le dernier argument est un pointeur vers une structure de type MPI_Status dans lequel des informations d'état vont être retournées.
Dans ce programme, le code du maître et celui des esclaves est contenu dans le même exécutable. Nous verrons ultérieurement comment lancer de multiples programmes. Dans le
makefile, nous voyons que la compilation est assurée par un sur-programme, mpicc, qui assure automatiquement l'édition de liens avec les bibliothèques appropriées.
4.3 Le programme d'addition, à nouveau
Nous allons ré-effectuer l'implémentation du programme d'addition que nous avons conçu avant d'aborder MPI. Nous allons voir ici comment exécuter des programmes distincts avec MPI. Lorsqu'un seul exécutable est utilisé, l'on se trouve en mode SPMD (Single Program Multiple Data), et en mode MPMD (Multiple Program Multiple Data) dans le cas où plusieurs exécutables différents sont utilisés. Le mode MPMP requiert un protocole d'exécution, mais avant cela, découvrons les sources des programmes maître et esclave.

1 /* -------------------------------------------------------------------- *
2 * master_mpi.c *
3 * *
4 * Programme maître pour l'addition des éléments d'un tableau avec MPI *
5 * -------------------------------------------------------------------- */
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <lam/mpi.h> /* constants et fonctions MPI */
9 #include "tags.h" /* étiquettes des messages */
10 #include "common.h" /* constantes communes */

11 int main(int argc, char** argv)
12
13 int size, i, sum;
14 int items[SIZE];
15 int results[NUM_SLAVES];
16 MPI_Status status;

17 /* initialisation de MPI */
18 MPI_Init(&argc, &argv);

19 /* vérification du bon nombre de processes */
20 MPI_Comm_size(MPI_COMM_WORLD, &size);

21 if(size != 5)
22
23 fprintf(stderr, "Error: Need exactly five processes.n");
24 MPI_Finalize();
25 exit(EXIT_FAILURE);
26

27 /* initialize the `items' array */
28 for(i = 0; i < SIZE; i++)
29 items[i] = i;

30 /* distribution des données aux esclaves */
31 for(i = 0; i < NUM_SLAVES; i++)
32 MPI_Send(items + i*DATA_SIZE, DATA_SIZE, MPI_INT, i + 1,
33 MSG_DATA, MPI_COMM_WORLD);

34 /* récupération des données en provenance des esclave */
35 for(i = 0; i < NUM_SLAVES; i++)
36
37 int result;
38
39 MPI_Recv(&result, 1, MPI_INT, MPI_ANY_SOURCE, MSG_RESULT,
40 MPI_COMM_WORLD, &status);
41 results[status.MPI_SOURCE - 1] = result;
42

43 /* obentention du résultat final */
44 sum = 0;
45 for(i = 0; i < NUM_SLAVES; i++)
46 sum = sum + results[i];

47 printf("The sum is %dn", sum);

48 /* sortie de MPI */
49 MPI_Finalize();

50 exit(EXIT_SUCCESS);
51 /* fin de main() */

52 /* fin de master_mpi.c */


1 /* -------------------------------------------------------------------- *
2 * slave_mpi.c *
3 * *
4 * Programme esclave pour l'addition des éléments d'un tableau avec MPI *
5 * -------------------------------------------------------------------- */
6 #include <stdio.h>
7 #include <stdlib.h>
8 #include <lam/mpi.h> /* constantes et fonctions MPI */
9 #include "tags.h" /* étiquettes des messages */
10 #include "common.h" /* constantes communes */

11 #define MASTER 0 /* rank du maître */

12 int main(int argc, char** argv)
13
14 int items[DATA_SIZE];
15 int size, sum, i;
16 MPI_Status status;

17 /* initialisation de MPI */
18 MPI_Init(&argc, &argv);

19 /* vérification du bon nombre de processes */
20 MPI_Comm_size(MPI_COMM_WORLD, &size);

21 if(size != 5)
22
23 fprintf(stderr, "Error: Need exactly five processes.n");
24 MPI_Finalize();
25 exit(EXIT_FAILURE);
26

27 /* réception des données en provenance du maître */
28 MPI_Recv(items, DATA_SIZE, MPI_INT, MASTER, MSG_DATA,
29 MPI_COMM_WORLD, &status);

30 /* calcul de la somme */
31 sum = 0;
32 for(i = 0; i < DATA_SIZE; i++)
33 sum = sum + items[i];

34 /* envoi des résultats au maître */
35 MPI_Send(&sum, 1, MPI_INT, MASTER, MSG_RESULT, MPI_COMM_WORLD);

36 /* sorie de MPI */
37 MPI_Finalize();

38 exit(EXIT_SUCCESS);
39 /* fin de main() */

40 /* fin de slave_mpi.c */


1 # Makefile du programme d'addition MPI - makefile.mpiadd
2 .SILENT:
3 CFLAGS=-I/usr/include/lam -L/usr/lib/lam
4 CC=mpicc

5 all : master_mpi slave_mpi

6 master_mpi : master_mpi.c common.h tags.h
7 $(CC) $(CFLAGS) master_mpi.c -o master_mpi

8 slave_mpi : slave_mpi.c common.h tags.h
9 $(CC) $(CFLAGS) slave_mpi.c -o slave_mpi

Pour compiler, il suffit de passer la commande make f makefile.mpiadd, ce qui aura pour résultat la création des programmes master_mpi et slave_mpi. Il nous faut maintenant décrire à MPI comment exécuter ces programmes, et c'est là qu'intervient le protocole d'exécution. Ce protocole est décrit dans un fichier, qui spécifie les exécutables et les n uds sur lesquels ces exécutables doivent être lancés. Créez un fichier add.schema avec le contenu suivant:

# protocole d'exécution pour le code d'addition sous MPI
n0 master_mpi
n0 -np 4 slave_mpi

Ce fichier décrit comment MPI doit lancer un exemplaire du maître (lequel aura le
rank 0) et quatre exemplaires des esclaves, l'ensemble sur le n ud n0 (le n ud local). De nombreux autres paramètres peuvent être spécifiés, et vous trouverez une description complète dans les manpages de appschema(1).
Dès que le fichier est créé, vous pouvez lancer le programme comme suit:

[rahul@joshicomp parallel]$ lamboot

LAM 6.3.1/MPI 2 C++/ROMIO - University of Notre Dame

[rahul@joshicomp parallel]$ mpirun add.schema
The sum is 4950
[rahul@joshicomp parallel]$

L'ensemble du programme doit être assez simple à comprendre. A la ligne 39, lors de la réception des résultats intermédiaires en provenance des esclaves, nous définissons la source comme
MPI_ANY_SOURCE, voulant avoir la possibilité de répondre aux esclaves dès qu'ils ont fini leur exécution. Dans ce cas, la structure status contient l'identifiant de l'émetteur dans le champ MPI_SOURCE, que nous utilisons pour savoir où stocker le résultat reçu.

Dans le cas où l'on a à disposition un réseau de machines, il est possible de faire tourner les programmes sur plusieurs n uds en modifiant le fichier descripteur du protocole d'exécution en conséquence. Au lieu de définir
n0 comme unique n ud, définissez le nom de n ud et le nombre de processes que vous voulez lancer.
5. Conclusion
Nous avons vu comment écrire des programmes parallèles utilisant les bibliothèques PVM et MPI. Ces bibliothèques étant disponibles sur de nombreuses plates-formes, et étant des standards de facto, les programmes développés sur ces bases tourneront avec peu ou même pas de modifications sur de grandes configurations si le besoin survient.

Nous nous sommes focalisés dans cet article sur les fonctions de communication point à point proposées par ces bibliothèques et leur utilisation dans le passage de messages. A côté de ces fonctions, PVM, tout comme MPI, offre beaucoup de fonctions évoluées, comme des fonctions de communication globale (broadcast ou multicast), de création et de gestion de groupes de processes, des fonctions de réduction, etc. Vous êtes invités à découvrir ces fonctions avancées. Ces produits en domaine public nous permettent d'utiliser un réseau de machines comme un seul grand système. Par conséquent, si vous avez un problème complexe à résoudre, vous pouvez envisager d'utiliser le réseau à votre disposition. Vous pourrez vous référer aux ouvrages listés ci-dessous pour plus d'informations, ainsi qu'à de nombreux autres ouvrages également existants.
1.      
PVM : Parallel Virtual Machine - A User's Guide and Tutorial for Networked Parallel Computing. Al Geist, Adam Beguelin, Jack Dongarra, Robert Manchek, Weicheng Jiang et Vaidy Sunderam, MIT Press. Disponible sur hyperlink
2.       MPI : The Complete Reference. Marc Snir, Steve Otto, Steven Huss-Lederman, David Waker et Jack Dongarra, MIT Press. Disponible sur hyperlink.
3.       RS/6000 SP : Practical MPI Programming. Yukiya Aoyama et Jan Nakano, International Technical Support Organization, IBM Corporation, hyperlink.
4.       A Beginner's Guide to PVM Parallel Virtual Machine. Clay Breshears et Asim YarKhan, Joint Institute of Computational Science, University of Tennessee, USA. hyperlink.
5.       PVM : An Introduction to Parallel Virtual Machine. Emily Angerer Crawford, Office of Information Technology, High Performance Computing, hyperlink.
6. Remerciements
Je voudrais remercier mon chef de travaux, le Dr. Uday Khedker pour son aide et ses encouragements. Je voudrais également remercier le Center for Developement of Advanced Computing pour m'avoir autorisé à exécuter les codes MPI et PVM sur le supercalculateur PARAM, et le Dr. Anabarsu pour m'avoir guidé lors du développement.

Copyright © 2001, Rahul U. Joshi.
Copying license
hyperlink
Published in Issue 65 of
Linux Gazette, April 2001