Les librairies de communication parallèles
Trois librairies sont très utilisées pour les systèmes parallèles :
1. MPI : standard pour la programmation dans les architectures parallèles à
mémoire distribuée.
2. OpenMp : standard pour la programmation dans les architectures
parallèles à mémoire partagée.
3. Cuda : Utilisée pour le développement dans les GPU parallèles
1. MPI (Message Passing Interface)
Les processeurs ne peuvent pas accéder directement à la mémoire d’autres
processeurs
Les variables sont privées pour chaque processeur
Les processeurs communiquent par passage de message
M
M
M : message envoyé d’un
1 processeur à un autre
MPI :
MPI n’est pas un langage de programmation
o Il n’existe pas un compilateur MPI
MPI est disponible sous forme de fonction/procédure
Le compilateur C où Fortran invoqué ne connait pas ce que fait
MPI
MPI est un standard
o Première version en 1993
o Actuellement version 3
Programmation du MPI :
Le parallélisme est explicite :
Le programme inclut des instructions spécifiques pour chaque
communication.
Quelles sont les données à envoyer / recevoir
Quand envoyer / recevoir
Synchronisation
C’est au développeur de déterminer la décomposition parallèle et de
l’implémenter.
Comment diviser le problème
Quand communiquer entre les processeurs.
2
EXEMPLE :
include <mpi.h>
int main (int argc, char* argr [] )
int taille, rang ;
MPI_Init (& argc, & argv) ;
MPI_Com_size (MPI_COM_WORLD, & taille) ;
MPI_Com_rank (MPI_COM_WORLD, & rang) ;
printf (“ le nombre total de processeur est : %d “, taille) ;
printf (“Je suis le processeur numéro %d”, rang);
MPI_Finalize () ;
Explication :
La fonction MPI_Init () : permet d’initialiser l’environnement de MPI
La fonction MPI_Com_size () : donne le nombre total de processeur
La fonction MPI_Com_rank () : donne le numéro du processeur actuel exécutant cette portion
du code.
2. Open MP (Open Multi Processing):
Utilisée pour les architectures à mémoire partagée:
Les processeurs accèdent au même espace mémoire.
Open MP est un ensemble d’extensions pour Fortran, C, C++.
Le parallélisme est moins explicite que pour MPI
Le programmeur spécifie quel partie du code à paralléliser et le
compilateur se charge de diviser le programme.
Une forme très commune de OpenMP est de paralléliser les tâche
d’une boucle.
3
Exemple :
include <stdio.h>
include <omp.h>
int main (int argc, char* argr [] )
int taille, rang ;
pragma omp parallel
rang = omp_get_thread_num();
taille = omp_get_num_threads() ;
printf (“ le nombre total de processeur est : %d \n“, taille) ;
printf (“Je suis le processeur numéro %d”, rang);
return 0 ;
Explication :
include <omp.h>: permet d’inclure la bibliothèque OpenMP pour pouvoir
utiliser ses différentes fonctions .
La fonction omp_get_num_threads() : donne le nombre total de processeur .
La fonction omp_get_num_threads(): donne le numéro du processeur actuel
exécutant cette portion du code.
4
pragma omp parallel {….}: permet de spécifier le code à exécuter en
parallèle. Dans l’exemple les instructions :
rang = omp_get_thread_num();
taille = omp_get_num_threads() ;
printf (“ le nombre total de processeur est : %d \n“,
taille) ;
printf (“Je suis le processeur numéro %d”, rang);
Sont exécutées en parallèle.
5
Détails sur la programmation en MPI (Message
passing interface)
1. Définitions :
Une application MPI est un ensemble de processus autonomes exécutant chacun leur
propre code et communiquant via des appels à des sous-programmes de la bibliothèque
MPI.
Il existe plusieurs versions de MPI (version 1.0 de 1994 version 3.0 en 2012 version
plus récentes).
Le programme MPI est écrit dans un langage classique « Fortran, c, c++, java, Python
(MPI4Py),… »
Un processeur accède aux données d’autres processeurs via une transaction appelée
« message passing » dans laquelle une copie de la donnée (message) est transférée (passed)
depuis un processeur vers un autre.
- Les variables du programme sont
privées.
M1 M1 Mn - Appel dans le programme à des
programmes particuliers
P1 P1 Pn
Transfert de
message (send
receive )
Pg Pg Pg
Le message est constitué des paquets de données (variables , tableaux,…) envoyées du
processeur émetteur vers processeur récepteur.
Le message doit contenir :
L’identificateur du processeur émetteur (le numéro du processeur émetteur id)
Le type de la donnée envoyée
6
La longueur de la donnée envoyée
L’identificateur du processeur récepteur (le numéro du processeur émetteur id)
Id_Eme Id_rec type long donnée
2. La programmation en MPI :
Il faut inclure un fichier d’entête " mpi.h "
MPI_Init () : permet d’initialiser l’environnement nécessaire
MPI_Finalize() : permet de désactiver l’environnent à la fin
Exemple d’un programme minimal en MPI :
include " mpi.h "
main (int argc, char * argv[]) {
MPI_Init (& argc, & argv) ;
MPI_Finalize () ;
return 0 ;
On peut écrire un programme fonctionnel juste en utilisant les six routines (fonctions)
suivantes :
MPI_Init : initialiser MPI
MPI_Finalize : terminer MPI
MPI_Comm_size : déterminer le nombre de processeurs
MPI_Comm_rank : déterminer le numéro du processeur courant (le label du processeur
appelant)
MPI_Send : pour envoyer un message
MPI_Recv : pour recevoir un message
7
Les prototypes des fonctions MPI_Init et MPI_Finalize sont :
Cette fonction retourne une valeur
int MPI_Init ( int * argc, char ** argv)
MPI_SUCCESS si l’initialisation est réussite
int MPI_Finalize () ;
Les fonctions basiques de l’envoi et de la réception des messages sont MPI_Send et
Mpi_Recv. Leurs prototypes sont les suivants :
int MPI_Send (void *buf, int count, MPI_Datatype datatype, int dest, int tag MPI_Comm
comm) ;
int MPI_Recv (void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm
comm, MPI_Status * status)
Structure contenant des informations sur
l’opération de réception
3. La notion de communicateur :
Toutes les opérations effectuées par MPI porte sur des communicateurs. Le communicateur
par défaut est MPI_COMM_WORLD qui comprend tous les processeurs actifs.
P1 Le communicateur
P2
P5
P3
P4
MPI_Comm_size () : permet de connaître le nombre de processeurs géré par une
communicateur.
MPI_Comm_rank () : permet d’obtenir le rang d’un processeur { 0,…, size-1}
8
Exemple :
include " mpi.h "
main (int argc, char * argv[]) {
MPI_Init (& argc, & argv) ;
MPI_Comm_rank (MPI_COMM_WORLD, & mon_rang);
MPI_Comm_size (MPI_COMM_WORLD, & nbr_proc);
Printf ( “ je suis le processeur %d parmis %d processeurs“ , mon_rang, nbr_proc);
MPI_Finalize () ;
return 0 ;
Après exécution du programme et si on est dans le processeur 2 par exemple et le nombre
total de processeurs est 10 , on obtient l’affichage suivant :
« Je suis le processeur 2 parmis 10 processeurs »
4. Les différents types de communication en MPI :
Il existe deux types de communication en MPI :
- La communication point à point
- La communication entre groupe de processeurs
4.1 Communication point à point :
Cette communication est entre deux processeurs (émetteur , récepteur). Dans ce type de
communication nous utilisons les deux fonctions : MPI_Send() et MPI_Recv().
L’enveloppe d’un message est constitué par :
1. Rang du processeur émetteur (id)
2. Rang du processeur récepteur (id)
3. Etiquette (tag) du message
9
4. Le communicateur qui défini le groupe de processeurs et le contexte de la
communication
Les donnée échangées sont typées (entiers, réels, ou type personnels). Les différents types en
MPI sont :
MPI_CHAR signed char
MPI_SHORT signed short
MPI_INT signed int
MPI_LONG signed long int
MPI_UNSIGNED_CHAR unsigned char
MPI_UNSIGNED_SHORT unsigned short
MPI_UNSIGNED unsigned int
MPI_UNSIGNED_LONG unsigned long int
MPI_FLOAT float
MPI_DOUBLE double
MPI_LONG_DOUBLE long double
Il existe 3 types de transfert / communication point à point :
1. En utilisant les fonction d’envoi /réception de messages standards (MPI_Send /
MPI_Recv ):
Utile pour les envoies de messages de base
Fonctions d’envoi bloquantes (le retour de l’appel n’est fait que lorsque l’espace
des données est disponible)
2. En utilisant les fonctions non bloquantes
Initialisent l’envoi et la réception des données et retourne une valeur « status »
immédiatement, on peut ensuite interroger ou attendre la réalisation des opérations
L’espace mémoire des données n’est pas « accessible » tant que les opérations ne
sont pas finies.
MPI_Prob() ou MPI_Wait () renvoient les informations sur l’état de l’échange non
bloquant.
3. En utilisant les fonctions d’échange ‘Synchrenous’ et ‘Buffered’
10
Exemple :
include < studio.h >
include " mpi.h "
main (int argc, char * argv[]) {
int num_proc, rang, dest, source, Rc, compt, tag=1 ;
char in_msg, out_msg = ‘x’;
MPI_Status etat;
MPI_Init (& argc, & argv) ;
MPI_Comm_rank (MPI_COMM_WORLD, & mon_rang);
MPI_Comm_size (MPI_COMM_WORLD, & nbr_proc);
If (rang ==0) {
Dest =1 ;
Source =1 ;
Rc = MPI_Send (& out_msg, 1, MPI_CHAR, dest, tag, MPI_COMM_WORLD);
Rc= MPI_Recv (& in_msg, 1, MPI_CHAR, source, tag, MPI_COMM_WORLD, & etat);
else if (rang ==1) {
dest = 0;
source =0;
Rc= MPI_Recv (& in_msg, 1, MPI_CHAR, source, tag, MPI_COMM_WORLD, & etat);
Rc = MPI_Send (& out_msg, 1, MPI_CHAR, dest, tag, MPI_COMM_WORLD);
Rc= MPI_Get_cout (& etat, MPI_CHAR, & count);
Printf (“ taache %d : recoit %d char (s) de la taache task %d avec tag %d \n”, rang, count,
etat, MPI_SOURCE, Stat_MPIIAG);
11
MPI_Finalize () ;
return 0 ;
Exemple sur MPI_Send et MPI_Recv:
include " mpi.h "
main (int argc, char * argv[]) {
int rang, valeur, code, tag=1;
int status[MPI_STATUS_SIZE];
MPI_Init (& argc, & argv) ;
MPI_Comm_rank (MPI_COMM_WORLD, & mon_rang);
MPI_Comm_size (MPI_COMM_WORLD, & nbr_proc);
If (rang == 2) {
valeur =1000 ;
Source =1 ;
MPI_Send (& valeur, 1, MPI_INT, 5, tag, MPI_COMM_WORLD, code);
else if (rang == 5){
MPI_Recv (valeur, 1, MPI_INT, 2, tag, MPI_COMM_WORLD, status, code);
printf ("moi processeur 5 j’ai reçus la valeur : %d du processeur 2", valeur ) ;
MPI_Finalize () ;
Return 0 ; }
12
Remarque : les affichages sont redirigés via le réseau sur le terminal du processeur master,
donc la machine contenant le processeur master affiche les résultats.
- Le processeur master c’est le processeur qui gère la communication, les autres
processeurs sont appelés slave.
4.2 La communication entre groupe de processeurs :
- Au lieu d’une communication entre seulement deux processeurs (point à point) on
peut avoir une communication entre plusieurs processeurs.
- MPI fournit des fonctions (routines) qui envoient des messages à un groupe de
processeurs ou reçoivent des messages d'un groupe de processeurs
Pas absolument nécessaire pour la programmation
Plus efficace que les fonctions « point à point » séparées
Exemples : les fonctions MPI suivantes
MPI_Bcast , MPI_Gather, MPI_Scatter, MPI_Reduce, MPI_Allreduce, MPI_Alltoall,
MPI_Barrier ….etc
4.2.1 Les principales fonctions de la communication entre groupe
de processeurs :
- MPI_Bcast : diffusion globale de données (broadcaste) message envoyé à
tous les autres processeurs en même temps.
- MPI_Scatter (): diffusion sélective de données.
- MPI_Gather() : collecte de données réparties
- MPI_Reduce() : assure la gestion des communication et effectuent des
opérations sur les données transférées (somme, produit, max, min,….).
13
- Les opérations de MPI_Reduce:
MPI_SUM: somme des éléments
MPI_PROD: produit des éléments
MPI_MIN : recherche du minimum
MPI_MAX : recherche du maximum
MPI_MAXLOC : recherche l’indice du maximum
MPI_MINLOC : recherche l’indice du minimum
MPI_LAND : et logique
MPI_LOR : ou logique
MPI_LXOR : ou exclusif logique
A) MPI_Bcast() :
Processeur 0 Processeur 1 Processeur n-1
data data data
…………
…….
MPI_Bcast() MPI_Bcast() MPI_Bcast()
La donnée à envoyer à tous les autres processeurs en Broadcast, cette donnée peut être
une seule valeur ou un tableau (plusieurs valeurs)
Pour pouvoir envoyer une seule donnée du processeur P 0 à tous les autres processeurs nous
avons le choix entre deux approches :
1) Utiliser plusieurs fonctions MPI_Send et MPI_Recv entre P0 et les autres processeurs
2) Utiliser une seule fonction MPI_Bcast() permettant d’envoyer la donnée depuis le
processeur P0 à tous les autres processeurs
14
Le prototype de la fonction MPI_Bcast () est le suivant:
MPI_Bcast(void *buf, int count, MPI_Datatype datatype, int root , MPI_Comm comm) ;
buf: le tableau à envoyer (on peut envoyer une seule valeur) .
count : le nombre d’éléments du tableau à envoyer, dans le cas d’une valeur count=1.
datatype: le type du tableau à envoyer, par exemple: MPI_INT, MPI_FLOAT, etc…..
root : la racine ou le numéro du processeur qui va envoyer la donnée, dans l’exemple
root=0.
comm : le communicateur, dans la plupart des cas nous utilisons le communicateur
par défaut MPI_COMM_WORLD qui contient tous les processeurs.
B) MPI_Scatter() :
Processeur 0 Processeur 1 Processeur n-1
recvbuf recvbuf recvbuf
…. …………
Send buffer …….
MPI_Scatter() MPI_Scatter() MPI_Scatter()
Les données à envoyer : on divise le tableau à envoyer en plusieurs éléments, chaque
élément est envoyé à un processeur selon son ordre dans le tableau send buffer.
15
Le prototype de la fonction MPI_Scatter() est le suivant:
MPI_Scatter (void * sendbuf, int sendcount, MPI_Datatype send_datatype, void * recvbuf,
int recvcount, MPI_Datatype recv_datatype , int root , MPI_Comm comm) ;
sendbuf: le tableau à envoyer.
sendcount : le nombre d’éléments du tableau à envoyer.
send_datatype: le type du tableau à envoyer, par exemple: MPI_INT, MPI_FLOAT,
etc…..
recvbuf: le tableau qui va recevoir la donnée (on peut recevoir une seule valeur).
recvcount : le nombre d’éléments du tableau qui va recevoir la donnée, dans le cas
d’une seule valeur recvcount=1.
recv_datatype: le type du tableau à envoyer, par exemple: MPI_INT, MPI_FLOAT,
etc…..
root : la racine ou le numéro du processeur qui va envoyer les données dans l’exemple
root=0.
comm : le communicateur, dans la plupart des cas nous utilisons le communicateur
par défaut MPI_COMM_WORLD qui contient tous les processeurs.
16
Exemple MPI_Scatter():
include " mpi.h "
main (int argc, char * argv[]) {
int rang, nbr_proc,send_count, recv_count, source = 0;
float sendbuf[4][4]=
{{1.0,2.0,3.0,4.0},{5.0,6.0,7.0,8.0},{9.0,10,11.0,12.0},{13.0,14.0,15.0,16.0}};
float recvbuf[4][4]=
La racine le numéro du processeur qui va
envoyer les données, dans l’exemple 0
MPI_Init (& argc, & argv) ;
MPI_Comm_rank (MPI_COMM_WORLD, & rang);
MPI_Comm_size (MPI_COMM_WORLD, & nbr_proc);
if (nbr_proc == 4) {
MPI_Scatter ( sendbuf, sendcount, MPI_FLOAT, recvbuf, recvcount, MPI_FLOAT, source,
MPI_COMM_WORLD);
printf (“le numéro du processeur courant est :%d, les valeurs reçus pour ce processeur sont :
%f %f %f %f \n”, rang, recvbuf[0], recvbuf[1], recvbuf[2], recvbuf[3]);
else{
printf (“il faut avoir 4 processeurs”); }
MPI_Finalize () ;
return 0 ;
17
C) MPI_Gather() :
Processeur 0 Processeur 1 Processeur n-1
data data data
…. …………
buffer …….
MPI_Gather() MPI_Gather() MPI_Gather()
Le tableau qui va recevoir les données provenant des différents processeurs
MPI_Gather = Rassembler (grouper) les données de n processeurs dans un seul
processeur. Dans le schéma les données sont groupées dans le tableau buffer du
processeur P0.
Le prototype de la fonction MPI_Gather() est le suivant:
MPI_Gather (void * data, int datacount, MPI_Datatype datatype, void * buffer, int
buffercount, MPI_Datatype buffertype , int root , MPI_Comm comm) ;
data: le tableau à envoyer des différents processeur Pi vers le processeur P0 (on peut
envoyer une seule valeur).
datacount : le nombre d’éléments du tableau à envoyer, dans le cas d’une seule valeur
datacount=1.
datatype: le type du tableau à envoyer, par exemple: MPI_INT, MPI_FLOAT, etc…..
buffer: le tableau qui va recevoir les données collectées de tous les autres
processeurs.
buffercount : le nombre d’éléments du tableau global qui va recevoir les données,
buffertype: le type du tableau global (buffer), par exemple: MPI_INT, MPI_FLOAT,
etc…..
18
root : la racine ou le numéro du processeur qui va recevoir les données collectées,
dans l’exemple root= 0.
comm : le communicateur, dans la plupart des cas nous utilisons le communicateur
par défaut MPI_COMM_WORLD qui contient tous les processeurs.
Exemple MPI_Gather():
include " mpi.h "
main (int argc, char * argv[]) {
int rang, valeur, code, tag=1;
int data[10] ; /*data to be ghathered from processors */
MPI_Init (& argc, & argv) ;
MPI_Comm_rank (MPI_COMM_WORLD, & rang);
MPI_Comm_size (MPI_COMM_WORLD, & nbr_proc); Les données vont être envoyées
vers le processeur P0.
If (rang == 0) {
Buffer = (int *) maloc (nbr_proc*10, sizeof(int));
MPI_Gather ( data, 10, MPI_INT, buffer, nbr_proc*10, MPI_INT, 0,
MPI_COMM_WORLD);
MPI_Finalize () ;
return 0 ;
19
D) MPI_Reduce() :
Une grande collection de données va se réduire en une seule valeur en utilisant
les opérations :
MPI_SUM: somme des éléments
MPI_PROD: produit des éléments
MPI_MIN : recherche du minimum
MPI_MAX : recherche du maximum
MPI_MAXLOC : recherche l’indice du maximum
MPI_MINLOC : recherche l’indice du minimum
MPI_LAND : et logique
MPI_LOR : ou logique
MPI_LXOR : ou exclusif logique
Les données contenues dans les processeurs Pi vont être réduites en utilisant une
opération par exemple la somme, puis le résultat obtenu est envoyé au
processeur racine P0.
Il existe plusieurs types de réductions selon l’opération utilisée (addition,
multiplication, …)
Processeur 0 Processeur 1 Processeur n-1
data data data
op
buffer …
…
…
MPI_Gather() MPI_Gather() MPI_Gather()
…
…
Le tableau qui va recevoir les données provenant des différents processeurs
….
Op : est l’opération de réduction utilisée (par exemple la somme,….)
20
Le prototype de la fonction MPI_Reduce () est le suivant:
MPI_Reduce (void * data, int datacount, MPI_Datatype datatype, void * buffer, int
buffercount, MPI_Datatype buffertype, MPI_Op op , int root , MPI_Comm comm) ;
data: le tableau à envoyer des différents processeurs Pi vers le processeur P0 (on peut
envoyer une seule valeur).
datacount : le nombre d’éléments du tableau à envoyer, dans le cas d’une seule valeur
datacount=1.
datatype: le type du tableau à envoyer, par exemple: MPI_INT, MPI_FLOAT, etc…..
buffer: le tableau qui va recevoir les données collectées de tous les autres
processeurs.
buffercount : le nombre d’éléments du tableau global qui va recevoir les données,
buffertype: le type du tableau global (buffer), par exemple: MPI_INT, MPI_FLOAT,
etc…..
op : l’opération de réduction utilisée ( MPI_SUM, MPI_PROD, MPI_MIN,
MPI_MAX, MPI_MAXLOC, MPI_MINLOC, MPI_LAND, MPI_LOR, MPI_LXOR)
root : la racine ou le numéro du processeur qui va recevoir les données collectées,
dans l’exemple root= 0.
comm : le communicateur, dans la plupart des cas nous utilisons le communicateur
par défaut MPI_COMM_WORLD qui contient tous les processeurs.
21