Prog Parallel e
Prog Parallel e
Prog Parallel e
Camille Coti
[email protected]
6 Outils de profiling
Plan du cours
6 Outils de profiling
Programmation multithreadée
Accès mémoire
Tous les threads ont accès à une mémoire commune
Modèle PRAM
Techniques de programmation
Utilisation de processus
Création avec fork()
Communication via un segment de mémoire partagée
Utilisation de threads POSIX
Création avec pthread_create(), destruction avec pthread_join()
Communication via un segment de mémoire partagée ou des variables
globales dans le tas
Rappel : la pile est propre à chaque thread, le tas est commun
Utilisation d’un langage spécifique
Exemple : OpenMP
Exemple : OpenMP
Fonctionnement
Langage d’annotations
Le code est relativement peu modifié
Directives de compilation
Utilisation de #pragma : si le compilateur ne connait pas, OpenMP est
débrayé et le code fonctionne en séquentiel
Paréllélisation du calcul
Parallélisation de la boucle for
On “tranche” l’intervalle sur lequel a lieu de calcul
Les tranches sont réparties entre les threads
Annotations OpenMP
Sections parallèles
#pragma omp parallel : début d’une section parallèle (fork)
#pragma omp for : boucle for parallélisée
Synchronisations
#pragma omp critical : section critique
#pragma omp barrier : barrière de synchronisation
Compilation et exécution
En-têtes
#include <omp.h>
Compilation
Activation avec une option du compilateur
Pour gcc : -fopenmp
Exéuction
Nombre de threads :
Par défaut : découverte du nombre de cœurs et utilisation de tous
Fixé par l’utilisateur via la variable d’environnement $OMP_NUM_THREADS
Exemple : HelloWorld
int main() {
int nbthreads, tid;
return EXIT_SUCCESS;
}
Camille Coti ED IG / Calcul Scientifique 9 / 80
Programmation de système à mémoire partagée OpenMP
Approche naïve
On peut parallélise la boucle et écrire le résultat directement dans une variable
partagée
Algorithme
max = 0
parfor i = 0 à N-1 :
si max < tab[i] : alors max = tab[i]
Problème : les accès à max doivent se faire dans une section critique
Solution : utilisation de #pragma omp critical
Séquentialisation des accès → séquentialisation de la boucle !
Meilleure approche
Calcul d’un maximum local puis à la fin du calcul, maximum global des
maximums locaux
code parmax.c
Static
Le découpage est fait à l’avance
Des tranches de tailles égales sont attribuées aux threads
Adapté aux boucles dont les itérations ont des temps de calcul équivalent
Dynamic
Le découpage est fait à l’exécution
Le scheduler attribue une tranche aux threads libres
Attention à la taille des tranches : si trop petites, seul le thread 0
travaillera
Guided
Similaire à dynamic
Les tailles des tranches diminuent durant l’exécution
Utilisation
#pragma omp for schedule (type, taille)
Plan du cours
6 Outils de profiling
Mémoire distribuée
Réseau
Mise en œuvre
Réseau d’interconnexion
Les nœuds ont accès à un réseau d’interconnexion
Tous les nœuds y ont accès
Communications point-à-point sur ce réseau
Espace d’adressage
Chaque processus a accès à sa mémoire propre et uniquement sa mémoire
Il ne peut pas accéder à la mémoire des autres processus
Pour échanger des données : communications point-à-point
C’est au programmeur de gérer les mouvements de données
entre les processus
Système d’exploitation
Chaque nœud exécute sa propre instance du système d’exploitation
Besoin d’un middleware supportant l’exécution parallèle
Bibliothèque de communications entre les processus
Avantages et inconvénients
Avantages
Modèle plus réaliste que PRAM
Meilleur passage à l’échelle des machines
Pas de problème de cohérence de la mémoire
Inconvénients
Plus complexe à programmer
Intervention du programmeur dans le parallélisme
Temps d’accès aux données distantes
Communications inter-processus
Passage de messages
Envoi de messages explicite entre deux processus
Un processus A envoie à un processus B
A exécute la primitive : send( dest, &msgptr )
B exécute la primitive : recv( dest, &msgptr )
Autres modèles
Autres modèles
NUMA en réseau
L’interconnexion entre les processeurs et les bancs de mémoire est fait par un
réseau à faible latence
Exemple : SGI Altix
La norme MPI
Implémentations
Portabilité des applications écrites en MPI
Applis MPI exécutables avec n’importe quelle implémentation de MPI
Propriétaire ou non, fournie avec la machine ou non
Fonctions MPI
Interface définie en C, C++, Fortran 77 et 90
Listées et documentées dans la norme
Commencent par MPI_ et une lettre majuscule
Le reste est en lettres minuscules
Historique de MPI
Évolution
Appel à contributions : SC 1992
1994 : MPI 1.0
Communications point-à-point de base
Communications collectives
1995 : MPI 1.1 (clarifications de MPI 1.0)
1997 : MPI 1.2 (clarifications et corrections)
1998 : MPI 2.0
Dynamicité
Accès distant à la mémoire des processus (RDMA)
2008 : MPI 2.1 (clarifications)
2009 : MPI 2.2 (corrections, peu d’additions)
En cours : MPI 3.0
Tolérance aux pannes
Collectives non bloquantes
et d’autres choses
Communicateur
Les processus communiquant ensemble sont dans un communicateur
Ils sont tous dans MPI_COMM_WORLD
Chacun est tout seul dans son MPI_COMM_SELF
MPI_COMM_NULL ne contient personne
Possibilité de créer d’autres communicateurs au cours de l’exécution
Rang
Les processus sont désignés par un rang
Unique dans un communicateur donné
Rang dans MPI_COMM_WORLD= rang absolu dans l’application
Utilisé pour les envois / réception de messages
Déploiement de l’application
Lancement
mpiexec lance les processus sur les machines distantes
Lancement = exécution d’un programme sur la machine distante
Le binaire doit Ãa tre accessible de la machine distante
Possibilité d’exécuter un binaire différent suivant les rangs
"vrai" MPMD
Transmission des paramètres de la ligne de commande
Redirections
Les entrées-sorties sont redirigées
stderr, stdout, stdin sont redirigés vers le lanceur
MPI-IO pour les I/O
Finalisation
mpiexec retourne quand tous les processus ont terminé normalement ou un
seul a terminé anormalement (plantage, défaillance...)
Qui suis-je ?
Combien de processus dans l’application ?
MPI_Comm_size( MPI_COMM_WORLD, &size );
Quel est mon rang ?
MPI_Comm_rank( MPI_COMM_WORLD, &rank );
Code complet
#i n c l u d e < s t d i o . h>
#i n c l u d e < s t d l i b . h>
#i n c l u d e <mpi . h>
i n t main ( i n t a r g c , c h a r ∗∗ a r g v ) {
i n t s i z e , rank ;
MPI_Finalize ( ) ;
r e t u r n EXIT_SUCCESS ;
}
Compilation
Compilateur C : mpicc
Wrapper autour du compilateur C installé
Fournit les chemins vers le mpi.h et la lib MPI
Équivalent à
gcc -L/path/to/mpi/lib -lmpi -I/path/to/mpi/include
mpicc -o helloworld helloworld.c
Exécution
Lancement avec mpiexec
On fournit une liste de machines (machinefile)
Le nombre de processus à lancer
mpiexec –machinefile ./machinefile -n 4 ./helloworld
Hello, I am rank 1 in 4
Hello, I am rank 2 in 4
Hello, I am rank 0 in 4
Hello, I am rank 3 in 4
Communications point-à-point
Communications bloquantes
Envoi : MPI_Send
int MPI_Send( void *buf, int count, MPI_Datatype datatype,
int dest, int tag, MPI_Comm comm )
Réception : MPI_Recv
int MPI_Recv( void *buf, int count, MPI_Datatype datatype,
int orig, int tag, MPI_Comm comm, MPI_Status *status )
Communications point-à-point
Données
buf : tampon d’envoi / réception
count : nombre d’éléments de type datatype
datatype : type de données
Utilisation de datatypes MPI
Assure la portabilité (notamment 32/64 bits, environnements hétérogènes...)
Types standards et possibilité d’en définir de nouveaux
Identification de la communication
Utilisation du tag
En réception : possibilité d’utilisation d’une wildcard
MPI_ANY_TAG
Après réception, le tag du message est dans le status
Code complet
#i n c l u d e < s t d i o . h>
#i n c l u d e < s t d l i b . h>
#i n c l u d e <mpi . h>
i n t main ( i n t a r g c , c h a r ∗∗ a r g v ) {
i n t rank ;
i n t token = 42;
MPI_Status s t a t u s ;
M P I _ I n i t ( &a r g c , &a r g v ) ;
MPI_Comm_rank ( MPI_COMM_WORLD, &r a n k ) ;
i f ( 0 == r a n k ) {
MPI_Send ( &t o k e n , 1 , MPI_INT , 1 , 0 , MPI_COMM_WORLD ) ;
MPI_Recv ( &t o k e n , 1 , MPI_INT , 1 , 0 , MPI_COMM_WORLD, &s t a t u s ) ;
} else {
i f ( 1 == r a n k ) {
MPI_Recv ( &t o k e n , 1 , MPI_INT , 0 , 0 , MPI_COMM_WORLD, &s t a t u s ) ;
MPI_Send ( &t o k e n , 1 , MPI_INT , 0 , 0 , MPI_COMM_WORLD ) ;
}
}
MPI_Finalize ( ) ;
r e t u r n EXIT_SUCCESS ;
}
Remarques
À un envoi correspond toujours une réception
Même communicateur, même tag
Rang de l’émetteur et rang du destinataire
On utilise le rang pour déterminer ce que l’on fait
On envoie des entiers → MPI_INT
Communications non-bloquantes
But
La communication a lieu pendant qu’on fait autre chose
Superposition communication/calcul
Plusieurs communications simultanées sans risque de deadlock
Quand on a besoin des données, on attend que la communication ait été
effectuée complètement
Communications
Envoi : MPI_Isend
int MPI_Isend( void *buf, int count, MPI_Datatype datatype,
int dest, int tag, MPI_Comm comm, MPI_Request *request )
Réception : MPI_Irecv
int MPI_Irecv( void *buf, int count, MPI_Datatype datatype,
int orig, int tag, MPI_Comm comm, MPI_Request *request )
Communications non-bloquantes
Attente de complétion
Pour une communication :
int MPI_Wait( MPI_Request *request, MPI_Status *status )
Attendre plusieurs communications : MPI_{Waitall, Waitany, Waitsome}
Test de complétion
Pour une communication :
int MPI_Test( MPI_Request *request, int *flag, MPI_Status
*status )
Tester plusieurs communications : MPI_{Testall, Testany, Testsome}
Différences
MPI_Wait est bloquant, MPI_Test ne l’est pas
MPI_Test peut Ãa tre appelé simplement pour entrer dans la bibliothèque
MPI (lui redonner la main pour faire avancer des opérations)
Camille Coti ED IG / Calcul Scientifique 32 / 80
Programmation de système à mémoire distribuée Communications collectives
i n t main ( i n t a r g c , c h a r ∗∗ a r g v ) {
i n t s i z e , rank , token ;
M P I _ I n i t ( &a r g c , &a r g v ) ;
MPI_Finalize ( ) ;
r e t u r n EXIT_SUCCESS ;
}
Réduction
Équivalent à :
Un Reduce
Suivi d’un Broadcast (fan-in-fan-out)
Ce qui serait une implémentation très inefficace !
Distribution
Barrière de synchronisation
Sémantique : un processus ne sort de la barrière qu’une fois que tous les autres
processus y sont entrés
MPI_Barrier( MPI_Comm comm );
Apporte une certaine synchronisation entre les processus : quand on
dépasse ce point, on sait que tous les autres processus l’ont au moins
atteint
Équivalent à un Allgather avec un message de taille nulle
Distribution d’un tampon de tous les processus vers tous les processus
int MPI_Alltoall( void *sendbuf, int sendcount, MPI_Datatype
sendtype, void *recvbuf, int recvcount, MPI_Datatype
recvtype, MPI_Comm comm );
Sur chaque processus, le tampon d’envoi est découpé et envoyé vers tous
les processus du communicateur
Chaque processus reçoit des donnés de tous les autres processus et les
concatène dans son tampon de réception
PAS de possibilité d’utiliser MPI_IN_PLACE
Bindings non-officiels
Par exemple : mpi4py
Import du module :
from mpi4py i m p o r t MPI
Communications point-à-point
Deux familles de routines de communications :
pour envoyer des objets Python : le nom de la routine commence par une
minuscule
pour envoyer des buffers (plus rapide) : le nom de la routine commence
par une majuscule
Attention : il existe des fonctions d’envoi non-bloquantes (Isend/isend), mais
pas de fonctions de réception.
#!/ bin / python
from mpi4py import MPI
Plan du cours
6 Outils de profiling
Principe
On définit le datatype
MPI_Type_contiguous, MPI_Type_vector, MPI_Type_hvector,
MPI_Type_indexed, MPI_Type_hindexed, MPI_Type_struct
On le commit
MPI_Type_commit
On le libère à la fin
MPI_Type_free
Données contigües
On crée un block d’éléments :
int MPI_Type_contiguous( int count, MPI_Datatype oldtype,
MPI_Datatype *newtype );
VECTOR
MPI_INT
Vecteur de données
On crée un vecteur d’éléments :
int MPI_Type_vector( int count, int blocklength, int stride
MPI_Datatype oldtype, MPI_Datatype *newtype );
On aggrège des blocs de blocklength éléments séparés par un vide de stride
éléments.
Structure générale
On donne les éléments, leur nombre et l’offset auquel ils sont positionnés.
int MPI_Type_struct( int count, int *array_of_blocklengths,
MPI_Aint *array_of_displacements, MPI_Datatype
*array_of_types, MPI_Datatype *newtype);
Exemple
On veut créer une structure MPI avec un entier et deux flottants à double
précision
count = 2 : on a deux éléments
array_of_blocklengths = 1, 2 : on a 1 entier et 2 doubles
array_of_displacements = 0, sizeof( int), sizeof( int ) + sizeof( double )
(ou utilisation de MPI_Address sur une instance de cette structure)
array_of_types = MPI_INT, MPI_DOUBLE
La FFT 3D
Calcul de la transformée de Fourier 3D d’une matrice 3D
FFT dans une dimension
FFT dans la deuxième dimension
FFT dans la troisième dimension
Parallélisation de la FFT 3D
Transposition de la matrice
On effectue une rotation de la matrice
Redistribution des vecteurs
All to All
Rotation des points (opération locale)
Algorithme
MPI_Alltoall(tab 2 dans tab 1 )
for( i = 0 ; i < c ; ++i ) {
for( j = 0 ; j < b ; ++j ) {
for( k = 0 ; k < a ; ++k ) {
tab2[i][j][k] = tab2[i][k][j];
}
}
}
Complexité
Le Alltoall coûte cher
La rotation locale est en O(n3 )
Dataypes non-contigüs
Définition d’opérations
Syntaxe
int MPI_Op_create( MPI_User_function *function, int commute,
MPI_Op *op );
On fournit un pointeur sur fonction. La fonction doit être associative et peut
être commutative ou pas. Elle a un prototype défini.
Exemple
Définition d’une fonction :
void addem( int *, int *, int *, MPI_Datatype * );
void addem( int *invec, int *inoutvec, int *len, MPI_Datatype
*dtype ){
int i;
for ( i = 0 ; i < *len ; i++ )
inoutvec[i] += invec[i];
}
Déclaration de l’opération MPI :
MPI_Op_create( (MPI_User_function *)addem, 1, &op );
Definition
La décomposition QR d’une matrice A est une décomposition de la forme
A = QR
Où Q est une matrice orthogonale (QQT = I)
et R est une matrice triangulaire supérieure.
Algorithme TSQR
General QR
factorization
QR factorisation
of type TT
QR factorisation
of type TT
Algorithme
R0 R00 R00
P0 A0
R0 R
Sur un arbre binaire :
R1 R20
V0 V00 V000
QR sur sa sous-matrice
R1
P1 A1 Communication avec le
V1 voisin
R2 R2 0
R2
P2 A2 Si rang pair : réception
V2
R3
V20 de r + 1
R3 Si rang impair : envoi à
P3 A3 r−1
V3
Si rang impair : fin
Arbre de réduction
Opération effectuée
On effectue à chaque étape de la réduction une factorisation QR des deux
facteurs R mis l’un au-dessus de l’autre :
R = QR(R1 , R2 )
Utilisation de MPI_Reduce
L’opération remplit les conditions pour être une MPI_Op dans un
MPI_Reduce
Définition d’un datatype pour les facteurs triangulaires supérieurs R
Utilisation de ce datatype et de l’opération QR dans un MPI_Reduce
Plan du cours
6 Outils de profiling
Maître-esclave
Efficacité
Files d’attentes de données et résultats au niveau du maître
On retrouve la partie séquentielle de la loi d’Amdahl
Communications: maître ↔ esclaves
Les esclaves ne travaillent que quand ils attendent des données ou qu’ils
envoient leurs résultats
Seuls les esclaves participent effectivement au calcul
Possilibité d’un bon speedup à grande échelle (esclaves >> maître) si les
communications sont rares
Peu rentable pour quelques processus
Attention au bottleneck au niveau du maître
Maître-esclave
Découpage en grille
Grille de processus
On découpe les données et on attribue un processus à chaque sous-domaine
Décomposition 1D Décomposition 2D
Découpage en bandes Découpage en rectangles, plus scalable
0 1 2 3
4 5 6 7
0 1 2 3
8 9 10 11
12 13 14 15
Ghost region
Travaux pratiques 3
Utilisation d’une ghost region dans le
maître-esclave dynamique en utilisant
une décomposition 1D.
En 2D
comm_old : le communicateur de
0 1 2 3 départ
ndims : ici 2
4 5 6 7 dims : nombre de processus dans
chaque dimension (ici {4, 4})
periods : les dimensions
8 9 10 11
sont-elles périodiques
reorder : autoriser ou non de
12 13 14 15 modifier les rangs des processus
comm_cart : nouveau
communicateur
Extraction de sous-communicateurs
Communicateur de colonnes, de rangées...
int MPI_Cart_sub( MPI_Comm comm_old, int *remain_dims,
MPI_Comm *comm_new );
Communicateurs de lignes
comm_old : le communicateur de
départ
remain_dims : quelles dimensions
sont dans le communicateur ou
non
comm_new : le nouveau
communicateur
Travaux pratiques 4
Implémenter la grille 2D de gauche et
faire un broadcast sur chaque rangée.
Plan du cours
6 Outils de profiling
Lancement
Sources de ralentissement
Synchronisations entre processus
Mouvements de données
Attentes
processus 1 processus 2
processus 0 Synchronisations
Adaptations algorithmiques
L’algorithme parallèle peut être différent
de l’algorithme séquentiel
Calculs supplémentaires
Finalisation
Efficacité du parallélisme ?
Accélération
Définition
L’accélération d’un programme parallèle (ou speedup) représente le gain en
rapidité d’exécution obtenu par son exécution sur plusieurs processeurs.
Mesure de l’accélération
On la mesure par le rapport entre le temps
d’exécution du programme séquentiel et le
temps d’exécution sur p processeurs
Tseq
Sp =
Tp
Appréciation de l’accélération
Accélération linéaire : parallélisme optimal
Accélération sur-linéaire : attention
Accélération sub-linéaire : ralentissement
dû au parallélisme
Camille Coti ED IG / Calcul Scientifique 68 / 80
Performance du calcul parallèle
Loi d’Amdahl
Énnoncé
On note s la proportion parallélisable de l’exécution et p le nombre de
processus. Le rendement est donné par la formule :
1
R= s
(1 − s) + p
Remarques
1
si p → ∞ : R = (1−s)
L’accélération est toujours limitée par la partie non-parallélisable du
programme
Si (1 − s) → 0, on a R ∼ p : l’accélération est linéaire
Remarque préliminaire
On a vu avec la loi d’Amdahl que la performance augmente théoriquement
lorsque l’on ajoute des processus.
Comment augmente-t-elle en réalité ?
Y a-t-il des facteurs limitants (goulet d’étranglement...)
Augmente-t-elle à l’infini ?
Définition
Le passage à l’échelle d’un programme parallèle désigne l’augmentation des
performances obtenues lorsque l’on ajoute des processus.
Obstacles à la scalabilité
Synchronisations
Algorithmes ne passant pas à l’échelle (complexité de l’algo)
Complexité en opérations
Complexité en communications
Scalabilité forte
On fixe la taille du problème et on augmente le nombre de processus
Relative au speedup
Si on a une hyperbole : scalabilité forte parfaite
On augmente le nombre de processus pour calculer plus vite
Scalabilité faible
On augmente la taille du problème avec le nombre de processus
Le problème est à taille constante par processus
Si le temps de calcul est constant : scalabilité faible parfaite
On augmente le nombre de processus pour résoudre des problèmes de plus
grande taille
Plan du cours
6 Outils de profiling
Scalasca : affichage
Vampir
Vampir : affichage
Bibliothèque mpiP
Utilise l’interface de profiling des fonctions MPI
Se place entre l’application et la bibliothèque MPI
Utilisation de mpiP
MPI_Pcontrol ( 1 ) ;
Argument Comportement
0 Désactiver le profiling
1 Activer le profiling
2 Remettre à zéro les données
3 Générer un rapport complet
4 Générer un rapport concis
Infos générales : temps passé dans les routines MPI processus par processus
------------------------------------------------------------------
@ - - - MPI Time ( seconds ) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
------------------------------------------------------------------
Task AppTime MPITime MPI %
0 65.6 24.3 37.01
1 65.6 14.2 21.60
2 65.6 24.6 37.50
3 65.6 15.8 24.09
4 65.6 24.7 37.57
5 65.6 15.3 23.28
6 65 24.2 37.28
7 65.6 26.7 40.74
8 65 17 26.09
9 65.6 23 35.08
10 65.6 22.5 34.34
11 65.6 19.6 29.87
12 65.6 24.1 36.69
13 65.6 17.8 27.13
14 65.6 24.4 37.14
15 65.6 11.6 17.70
* 1.05 e +03 330 31.44
Puis fonction par fonction, processus par processus, appel par appel
Camille Coti ED IG / Calcul Scientifique 80 / 80