FlashInformatique 32007
FlashInformatique 32007
En novembre 1995, Intel annonçait le Pentium Pro, une Comment utiliser OpenMP ?
révolution dans l’évolution de son processeur phare capable
d’être monté sur une carte mère bi ou quadri processeurs. D’autres avant nous ont tenté d’établir une méthodolo-
Mais en partie à cause de la découverte du désormais célèbre gie d’OpenMP-isation (voir par exemple l’excellent cours de
flag erratum (un bug dans la conversion entier-float causant l’IDRIS [13]). Il nous semblait toutefois que la présentation
une erreur overflow), le chip n’a pas connu le succès qu’il aurait des résultats en terme de performances (orienté Calcul à
pu mériter, à l’exception de celui d’avoir été le précurseur Hautes Performances) en gardant le même exemple du début
de toute la série qui a suivi [1]. Aujourd’hui la guerre des à la fin, passant en revue les améliorations, les pièges à éviter,
GHz est terminée et on assiste à un revirement de stratégie etc. avait encore un sens.
des principaux fondeurs avec l’apparition des architectures Il existe deux cas d’utilisation: sur un code existant, ou
multi-processeurs à multi-cœurs, doublées de compilateurs from scratch. Dans le premier, on part d’un code séquentiel
qui s’améliorent de version en version, autorisant la pro- mono-thread qu’on parallélise au niveau des boucles en sui-
grammation à mémoire partagée à un coût relativement vant une méthodologie que nous allons voir plus loin; c’est
faible. OpenMP est l’un des paradigmes de programmation l’approche à grain fin (fine grain). Dans le second, on part dès
parallèle adapté aux machines de type SMP, c’est l’objet de le début en ayant en tête que la machine cible sera à mémoire
cet article. partagée; c’est l’approche à grain fort (coarse grain). Il est utile
Ces briques multi-processeurs multi-cœurs sont les nœuds de rappeler ici que les drapeaux de compilation (compilation
des clusters de type Beowulf actuels (clusters construits sur flags) disponibles sont nombreux et variés [9], nous en reste-
la base d’éléments grand public). Le 6ème ordinateur le plus rons à l’optimisation –O0 (sans optimisation), –O1, –O2 (les
puissant de la planète – et le premier Beowulf derrière les deux sont similaires sur les architectures IA-32 et EM64T et
spécifiques BlueGene d’IBM et le RedStorm de Cray Inc.– est correspondent à une optimisation avec inlining, propagation
un super-ordinateur de ce type [2]. Ainsi la programmation de constantes, allocation globale des registres, propagation
à plusieurs niveaux de parallélisme est maintenant à portée des attributs des routines, analyse des adresses des variables,
de bourse de tout un chacun; on utilise le paradigme de élimination des sections mortes, suppression des variables
programmation parallèle MPI entre les nœuds et OpenMP non référencées, optimisation inter routines au niveau d’un
à l’intérieur des nœuds. fichier, récursion de queue) et –O3 (optimisation maximale:
–O2 avec en plus une analyse plus agressive des dépendances,
modification des boucles, pré-extraction des données) ainsi
OpenMP, késako ? qu’à la vectorisation des boucles via les flags –xX où X dépend
de l’architecture cible (–xT sur un Woodcrest, –xW sur un
Le standard OpenMP (Open Multi-Processing [3]) est une Xeon, –xB sur un PentiumM, etc.).
interface de programmation (API) et a été publié pour la Afin de pouvoir mesurer les peformances, les effets des
première fois en 1997 pour le langage Fortran. Aujourd’hui, optimisations proposées, nous utiliserons le temps et le rap-
OpenMP est aussi disponible pour les langages de pro- port MFlops/s comme métriques. Ces deux valeurs peuvent
grammation C/C++. OpenMP consiste en un ensemble de se mesurer grâce à des outils spécifiques non intrusifs (Intel
directives de compilation, d’une bibliothèque de fonctions VTune [10] ou la simple routine gettimeofday() précise à
ainsi qu’un ensemble de variables d’environnement. Les la micro-seconde près) ou intrusifs (Parallel API [5]). Dans
performances obtenues avec OpenMP dépendent donc de cet exemple, on mesurera le temps à la microseconde près
deux facteurs: d’une part d’un choix de partage de données et on calculera le rapport MFlops/s avec la connaissance du
judicieux de la part du programmeur, d’autre part de la nombre d’opérations. Dans la suite de cet article nous allons
qualité du compilateur (interprétation des directives de prendre l’exemple simple de la multiplication matricielle (les
compilation). Tous les exemples décrits dans cet article ont éléments des matrices sont des nombres à virgule flottante
été compilés avec la dernière version du compilateur d’Intel double précision, 64 bits):
(9.1e) sur un nœud multi-cœurs du cluster Pleiades2 [4] C=A*B
(Intel Xeon 5150 Woodcrest, 2.66 GHz, 8 GB RAM), sauf La taille des matrices choisie est de 1200 x 1200 (division
lorsque l’auteur le précise explicitement. entière par 16 possible pour l’exemple). L’exemple sera implé-
Une machine à mémoire partagée se programme toujours menté en Fortran 771 et compilé à l’aide du compilateur Intel
de la même façon: plusieurs threads résidant sur des process Fortran 9.1e sur l’un des nœuds de Pleiades2+ (la nouvelle
elements différents possèdent un espace propre et accèdent extension multi-processeurs multicœurs du cluster Pleiades).
à un espace de données partagé; entraînant les problèmes L’exemple est implémenté suivant le listing 1: le listing 1
traditionnels d’aléas de données. L’utilisation d’OpenMP présente la séquence k – j – i, il suffit d’inverser les indices
ne change en rien cet état de fait et demande toujours du pour obtenir les résultats présentés dans cet article.
programmeur une connaissance parfaite du problème qu’il Tous les résultats sont donnés en forçant 4 threads sur le
veut résoudre en terme algorithmique ainsi que des bases de nœud, soit un thread par cœur
l’architecture cible. OpenMP peut améliorer sensiblement (export OMP–NUM–THREADS = 4).
la performance d’une application, mais peut malheureuse-
ment aussi, mal utilisé, la fusiller. C’est ce que nous allons
notamment voir dans la suite de cet article.
1
Ce choix se justifie par une visibilité plus claire de l’impact du choix de l’ordre des indices. Une analogie avec le langage C/C++ est aussi
plus aisée. La performance obtenue avec la syntaxe de base Fortran 90/95 sera présentée en fin de section
c Multiplication C = A * B
Quatrième étape: c en suivant la sequence kji et unroll de 8 niveaux
3
Cette dernière remarque permet aussi à l’auteur de mettre en garde le lecteur ne jurant que par le speedup: si la version monoprocesseur
affiche une performance médiocre, il est relativement aisé d’obtenir ce genre d’effet !
t Mflop/s Moralité
kji 0.1166E+01 2964.0 Les résultats présentés ci-dessus démontrent – à l’aide
kij 0.1166E+01 2964.0 d’un code source extrêmement simple – qu’en suivant une
ikj 0.2981E+01 1159.0 stratégie d’optimisation on peut arriver à des performances
jki 0.6556E+00 5272.0 de l’ordre de 37 % de la performance maximale R∞ du pro-
ijk 0.6453E+01 535.5 cesseur considéré. Sans optimisation manuelle, en plaçant
jik 0.5951E+00 5807.0
une totale confiance dans le compilateur, on obtient un
résultat proche (flags de compilation -parallel -xT -O3)
kji2 0.6160E+00 5611.0
8.1 GFlops/s au mieux dans le cas où l’utilisateur a aidé
kji4 0.5263E+00 6566.0
manuellement le compilateur (1.4 GFlops/s au pire !). Le
kji8 0.4300E+00 8037.0
compilateur est capable de reconnaître certaines boucles et de
kji16 0.4658E+00 7419.0
changer l’ordre des indices. Malheureusement pas partout.
listing 14 – Résultats du programme MMIJK avec le flag de Les performances de l’auto-parallélisation démontrent
compilation -O3 -xT -parallel-openmp que le compilateur est capable de reconnaître certaines bou-
cles et de les paralléliser exactement comme on le fait avec
Et Fortran 90 alors ? OpenMP. Pour certains ordres d’indice, le compilateur n’en
Afin d’être complet, donnons quelques performances est pas capable. Il est aussi à noter que suivant la complexité
pour qui utiliserait les nouvelles fonctionnalités offertes par du calcul à l’intérieur de la boucle externe, le compilateur ne
Fortran 90. La principale nouveauté apparue avec Fortran sera pas capable de l’auto-paralléliser. C’est ce que nous allons
90 sur Fortran 77 était la gestion simplifiée des tableaux et voir dans la dernière partie de cet article avec l’application
l’apparition de nouvelles fonctions intrinsèques. Dans le cas de physique des plasmas TERPSICHORE d’abord, avec un
de notre petit exemple, la multiplication matricielle se note solveur de Helmholtz 3D ensuite.
tel que présenté au listing 15. Cela montre surtout que si le programmeur a une
connaissance exacte de l’algorithmique de son code, il peut
n = 1200 alors parfaitement maîtriser la localisation de ses données.
! Initialisation de A, B e t C
call initialise(n*n,A,1.0d0)
call initialise(n*n,B,1.0d0)
call initialise(n*n,C,1.0d0) Premier exemple: TERPSICHORE
ti = second() TERPSICHORE est un code MHD développé au CRPP
C = matmul(A,B)
ti = second ( ) – t i dès 1988 [7].
mflops–rate = 2.0*(n/100.)**3/ti Kinetic theory
Plasma
listing 15 – Multiplication matricielle en Fortran 90 physics Two-fluid model
resistive MHD Equilibrium
Organisation du code
La structure de TERPSICHORE est organisée en six
principales routines: eqinvm, veqrec, mtaskb, sta-
bin, mtaska et mtasks–ap. Les deux premières établissent
l’interface avec la sortie du code VMEC et reconstruisent
les variables d’équilibre MHD. La routine mtaskb est
responsable principalement de la mise en correspondance
du système de coordonnées de VMEC et du système de
coordonnées de Boozer ainsi que du calcul de la métrique.
La construction des matrices de stabilité est effectuée dans
stabin. Le solveur PAMERA calcule les valeurs propres w2
et les modes. Ce solveur prend peu de temps (ne domine
pas) pour le petit cas test considéré.
performance du code obtenue sur un Xeon 5160 (Woodcrest). Notons tout d’abord que toutes les exécutions montrent
On voit facilement que le compilateur utilisé (ifort version une meilleure performance que celle obtenue sans optimisa-
9.1e) peut améliorer les résultats d’environ 50%, mais, tion (valeur initiale), c’est-à-dire que l’optimisation à la main
comme nous l’avons vu plus haut, il est recommandé d’aider avec le flag de compilation (–O1 par exemple) était meilleure
le compilateur manuellement plutôt que d’accorder une trop d’environ 15% que ce dont est capable le compilateur (avec
grande confiance en lui. Dans tous les cas, la nature du code –O3 –xT). Deuxièmement, on observe qu’il n’y a quasiment
laisse présager d’excellentes optimisations possibles. pas de différence de performance sur la version optimisée
manuellement en changeant de flag de compilation (excepté
avec –parallel). Ceci nous encourage donc, comme précisé
dans la première partie de cet article, à paralléliser le code ma-
nuellement pour voir si on peut encore aider le compilateur
et obtenir les meilleures performances possibles (meilleures
que ce que pourrait faire le compilateur seul).
-O1 133s
-O3 124s
-O3-xT 96s Fig. 6 – la performance après optimisation sur un processeur
-O3 -xT -parallel 65s au niveau –O1
Tab. 2 – TERPSICHORE: performance sur un nœud Xeon
opt
5160 (WoodCrest) avant l’optimisation
-O1 133s 85s
-O3 124s 84s
Travailler sur l’optimisation -O1 -O3 -xT 96s 84s
Comme déjà vu plus haut, il y a quelques changements
-O3 -xT -parallel 65s 51s
qui doivent être apportés manuellement à ce niveau d’opti-
misation. Alors que le code a été déjà hautement optimisé, Tab. 3 – TERPSICHORE: performance après optimisation
mais pour une machine vectorielle, le gain principal que
l’on peut attendre vient d’une simple inversion des indices Stratégie de parallélisation
des boucles DO. La fig. 5 présente un exemple d’une telle Le code sera parallélisé en utilisant les directives OpenMP
inversion. D’autres possibilités ont été utilisées comme la sur le code optimisé sur un processeur.
division des grandes boucles, le déroulement dans des plus Nous avons observé que, pour ce code, la meilleure
petites boucles, etc. mais l’inversion des indices reste le plus stratégie de partage des données entre les threads OpenMP
efficace. Les résultats après cette première optimisation sont était (dans l’ordre):
présentés à la fig. 6 et au tab. 3. z !$OMP DO sur la boucle la plus exterieure;
Fig. 5 – La principale accélération (speedup) a été obtenue par inversion des indices dans les boucles DO
Fig. 7 – un exemple d’utilisation de la directive de compilation !$OMP SECTIONS dans une boucle indépendante (embarrassingly
parallel loop)
z division des grandes boucles indépendantes et placement Résultats
dans des directives !$OMP SECTIONS; Comme petit résumé de ce qu’on a pu observer de la
z combinaisons de petites boucles; parallélisation avec OpenMP, nous pouvons établir que –>
z appels OpenMP imbriqués; l’optimisation et la parallélisation à la main est meilleure
La fig. 7 montre un exemple de partage en sections que le compilateur –> sur des codes instables peut être aussi
indépendantes d’une boucle et la performance associée (en rapide que sur des codes stables avec OpenMP –> le Xeon
secondes). 5160 (WoodCrest) est comparable au NEC SX-5... mais il
Après la parallélisation avec OpenMP, la performance a est 30 fois moins cher. Le tab. 5 présente un petit résumé des
augmenté d’un facteur de 3 en comparaison de la version performances obtenues selon les flags de compilation ainsi
optimisée non parallèle (fig. 8 et tab. 4). que le paradigme de parallélisation utilisé (autoparallélisation
On peut observer qu’une parallélisation manuelle est ou manuellement avec les directives OpenMP).
meilleure que ce que peut faire le compilateur : -O1 -openmp
est environ 2 fois plus rapide que -O3 -xT -parallel sur
main routine routine routine non opt sp5 opt
le code non optimisé. Notons aussi qu’il n’y a quasiment
eqinvm 0.12 0.13
pas de différence entre les flags de compilation et que la
performance ainsi obtenue est comparable à celle obtenue veqrec cospol 0.64 0.0.64
sur le NEC SX-5. lgikvm 35.45 13.15
mtaskl lamcal 24.78 17.77
mtaskb vmtobo 24.16 19.35
extint 5.73 5.78
metric 15.73 6.18
bophys 4.96 5.50
stabin 0.14 0.14
mtaska fourin 10.01 7.74
Total 132.97 84.69
Tab. 5 – résumé des performances des quatre principales
routines posant problème
En outre, nous avons testé la capacité HT (HyperThrea- Ainsi il est fortement recommandé de réécrire les sous-rou-
ding) (voir [9]) des processeurs Xeon afin d’obtenir un gain tines BLAS 1 (dscal, dcopy, etc.) en utilisant les directives
pour l’exécution d’une instruction parallèle très simple (la ca- OpenMP. Le tab. 10 présente la loi d’échelle pour la routine
pacité HT autorise une augmentation infime de performance dcopy (de la bibliothèque MKL version 8.0) ainsi que pour
sur un Pentium 4). Le tab. 8 résume ce qui a été obtenu en celle écrite à la main ucopy sur 1693 éléments de matrice.
utilisant le flag de compilation –parallel (l’autoparalléli- Cela même si les BLAS sont implémentés dans MKL avec
sation automatique du compilateur). Les deux versions du les directives OpenMP.
compilateur sont à nouveau comparées.
compiling 1cpu 2cpus 4cpus
NH-ACO NH-ACO options [s/iter] [s/iter] [s/iter]
[s/iter] [s/iter] -parallel 2.07 1.37 1.07
33.00(4.85) 3.15(3.21) -openmp 2.07 1.20 0.87
Tab. 8 – Influence de l’HyperThreading sur les performances
avec l’auto-parallélisation du compilateur Tab. 9 – Performance auto-parallélisation vs. directives ma-
nuelles OpenMP sur 1, 2 et 4 cœurs d’un nœud multi-CPU’s
Il apparaît clairement que la toute dernière version du multi-cœurs Woodcrest avec le code totalement parallélisé
compilateur Intel (la version 9.1e) n’est pas capable de corri-
ger les erreurs d’écriture du code, les performances sont ainsi
subroutine 1cpu [s] 2cpus [s] 4cpus [s]
dégradées drastiquement. En effet, dans ce cas, l’auto-paral-
lélisation semble annuler toutes les autres optimisations et dcopy 2.56 x 10 –2
2.56 x 10 –2
2.56 x 10–2
donne une performance obtenue avec une optimisation basse ucopy 2.54 x 10–2 1.61 x 10–2 1.58 x 10–2
–O2. D’un autre côté, la version précédente du compilateur
(la version 9.0 disponible sur Pleiades2) donne des résultats Tab. 10 – Loi d’échelle des deux routines dcopy (de MKL) et
bien meilleurs même s’ils ne sont pas comparables avec une ucopy (écrite avec OpenMP)
optimisation manuelle. Notons finalement que la compila-
tion du code optimisé manuellement avec l’une ou l’autre des
versions du compilateur donne les mêmes résultats. Quelques observations
Parallélisation Dans cette dernière section, nous donnons quelques
L’étape suivante est la parallélisation du code sur une conseils sur la base d’observations faites lors de l’optimisation
machine SMP. Pour ce faire, nous utiliserons les nouvelles et la parallélisation d’applications à l’aide d’OpenMP.
architectures multi-cœurs d’Intel Woodcrest. Tout ce qui va De façon générale, la procédure proposée est celle com-
suivre a été obtenu sur un seul nœud du calculateur Pleia- munément utilisée en génie logiciel lorsqu’il s’agit d’optimiser
des2+. Les résultats qui vont suivre présentent uniquement une application: Monitoring –> Optimisation –> Tests.
une parallélisation manuelle; l’avantage de l’utilisation des Dans cette optique, il faut disposer d’une bonne mesure
directives OpenMP à la place de l’auto-parallélisation va du temps. Des outils existent (VTUNE, gprof, etc.). Mais
être démontrée. nous avons pu observer que le meilleur monitoring reste
L’utilisation des directives OpenMP a permis de paralléli- l’utilisation de la fonction gettimeofday(). Le profiling
ser des opérations complexes qui concernent principalement gprof donne des résultats erronnés.
une quadruple boucle imbriquée où une multiplication ma- La version du compilateur ainsi que celle des versions
tice – matrice avec des indices shiftés est effectuée entre une utilisées peuvent jouer un rôle. Dans cette optique, les ingé-
matrice 2D et une matrice 3D (10% du temps de l’entier nieurs système laissent généralement le choix de la version à
du code est utilisé pour cette opération). l’utilisateur. C’est ainsi que sur la machine Pleiades [4], pas
D’un point de vue plus précis, il est clair que l’exécution moins de 5 versions du compilateur Intel, 3 versions de la
du code sur quatre cœurs d’un nœud Woodcrest est environ bilbiothèque MKL sans compter le compilateur libre gfortran
20% plus rapide que l’auto-parallélisation, même optimisée sont disponibles pour les utilisateurs.
manuellement. Cela signifie qu’après avoir aidé le compila-
teur (déroulement des boucles, réordonnancement d’index,
etc.), il est possible d’augmenter la performance d’au moins Conclusion
20%. Afin d’être complet, il a été dit plus haut que la version
parallèle du code prenait 11.07 [s/iter] compilé avec la version On a vu que parvenir à une performance aussi proche
9.1e et 1.99 [s/iter] avec la version 9.0; ceci sur 4 cœurs. que possible de la performance maximum d’un nœud multi-
Cela signifie que cette version du code tourne sur 4 cœurs processeurs multi-cœurs demande du travail. Il est illusoire de
aussi rapidement que la version optimisée manuellement se dire que le compilateur y arrivera seul. Malgré les efforts
sur un seul cœur ! constants des compagnies qui développent des compilateurs
Avant de conclure, un dernier aspect doit être abordé. de plus en plus performants, tout utilisateur/développeur
Afin de se rapprocher le plus possible de la performance HPC devrait être capable de fournir un code source à la per-
maximale autorisée sur un cœur, nous avons observé que formance identique quelle que soit l’option de compilation,
certaines routines BLAS 1 hautement optimisées par Intel en d’autres termes, un code source optimisé à la main pour
(la bibliothèque MKL) ne supportaient pas la scalabilité ! un type d’architecture.