Table of Contents
Commit 2010-08-10
Mise en place d'OpenMP
J'ai décidé de mettre entre parenthèse la refonte de la base de données de Metafor pour avancer un peu du côté du parallélisme. Ce commit permet de compiler Metafor avec OpenMP. Pour ceux qui ne le saurait pas, OpenMP est un ensemble d'extensions du compilateur C++ (et quelques routines) pour permettre la création de code parallèle. On parle ici de parallélisme SMP (“Symmetric MultiProcessing” ou “Shared Memory Programming” selon les auteurs), c'est-à-dire des instructions qui s'exécutent sur les différents cœurs d'une même machine.
Makefiles
- Les makefiles ont été adaptés pour compiler le code avec OpenMP (aussi bien avec gcc que le compilateur intel). Il n'y a pas de problème sous linux mis à part que certaines machines n'ont pas de gcc supportant OpenMP. C'est le cas de clifton et gaston. Sous Windows, il y a un petit problème si on veut compiler le projet avec le compilateur Intel: l'option OpenMP n'est pas mal traduite par CMake dans le projet et donc mal traduite lors de la conversion du projet en projet Intel. Bien entendu, un powergrep ne fonctionne pas puisque CMake détecte que le projet a été corrompu et remouline tout! La seule manière que j'ai trouvé est d'ajouter brutalement l'option intel
/Qopenmp
dans leCMakeLists.txt
. C'est moche (le projet visual fait alors un warning a chaque fichier) mais ça marche… - Il est bien sûr toujours possible de compiler sans OpenMP (mettre à
OFF
l'optionMETAFOR_USE_OPENMP
). Par défaut, elle est active. Dans le cas où on désactive OpenMP, le code de metafor n'est plus compilé avec OpenMP mais il est toujours possible de bénéficier de l'OpenMP des MKL. - Sous linux, la séquence des libs MKL au link a été adaptée pour pouvoir mixer de l'OpenMP gcc et celui des MKL (intel). Tel quel, ça faisait un beau core au premier appel.
Interface OpenMP (et MKL)
Pour éviter d'ajouter des “#ifdef _OPENMP
” un peu partout dans le code, j'ai créé une classe OpenMP
dont les fonctions sont toutes statiques et qui permettent d'accéder à l'API OpenMP même si le code n'est pas compilé avec OpenMP. Par exemple, OpenMP::status()
affiche:
OpenMP status: max_threads = 2 num_procs = 8 dynamic = 0 nested = 0
La première valeur correspond à la valeur OMP_NUM_THREADS
, la deuxième au nombre de cores de votre machine (hyperthreading inclus), la troisième à OMP_DYNAMIC
(autorise le programme à modifier dynamiquement le nombre de threads OpenMP en fonction de la charge machine), et le quatrième à OMP_NESTED
(permet le parallélisme dans les boucles déjà parallèles).
Pour s'y retrouver dans ce qui est parallèle et ce qui ne l'est pas (encore), j'ai fait la même chose pour MKL: il existe maintenant une classe nommée Blas
qui permet de gérer le parallélisme des MKL. Par exemple Blas::status()
affiche:
MKL 10.2 (Intel(R) Core(TM) i7 Processor) num_threads = 2 dynamic = 1
La première valeur est votre MKL_NUM_THREADS
(ou OMP_NUM_THREADS
). La seconde MKL_DYNAMIC
(ou OMP_DYNAMIC
).
On voit déjà que les MKL s'amusent à mettre des valeurs par défaut différentes d'OpenMP (par défaut, MKL ajuste le nombre de threads dynamiquement et pas OpenMP).
Ces infos ci dessus s'affichent maintenant toujours au démarrage de Metafor. Ca me semble bien utile pour la suite.
Les classes OpenMP
et Blas
peuvent également être utilisées dans n'importe quel jeu de données pour ajuster le nombre de threads:
OpenMP.setNumThreads(8) # le code OpenMP (inexistant actuellement) tournera sur 8 CPUs Blas.setNumThreads(2) # les routines MKL tourneront sur 2 CPUs
Option "-j ncpu"
Il est possible d'ajuster le nombre de threads par l'option “-j” en ligne de commande:
metafor -nogui -j 8 -run snecma.tests.snecmaRup
lance snecmaRup sur 8 CPUs (en pratique, le seul le solveur DSS en bénéficie aujourd'hui).
Timers
Étape suivante après avoir réglé la compilation et affiché les infos concernant OpenMP, il faut pouvoir mesurer le temps qui s'écoule. Ca paraît simple ou même déjà fait… et bien non. Le temps CPU actuellement mesuré est le temps “utilisateur”, c'est à dire le temps qui est passé dans les routines de Metafor par tous les threads. Si il y en a plus d'un, le temps CPU est la somme du temps de tous les threads. Bref, si on lance un test en “-j 8” avec le solveur linéaire parallèle, il y a beaucoup de chance pour que le temps CPU augmente (par exemple, pour le “snecmaRup” de la batterie, on passe de 76s a 106s). Cependant, le calcul s'effectue plus rapidement (70s). Ce dernier temps est le temps “réel” ou le “wall clock time” (temps horloge). C'est bien sûr ce temps qui compte au final puisque c'est celui qu'on passe à attendre les résultats.
J'ai donc modifié le Timer
de metafor pour qu'il mesure ces deux types de temps. J'ai ajouté également le “kernel time” (temps passé dans les appels système, les i/o, la gestion des threads, etc.) et le chrono de l'API d'OpenMP (qui donne le temps réel lui aussi).
Le Timer
lance les 4 chronos en même temps. il est donc possible de mesurer ces 4 temps avec un seul objet Timer
. J'ai également mieux typé les sorties et amélioré l'affichage et la conversion en secondes. Ce n'était pas évident vu les différences Windows/Linux.
Voilà par exemple ce qu'on obtient en lançant apps.ale.backwardExtrusion
(avec l'interface graphique et OMP_NUM_THREADS=1
):
[TSC-CPU] User CPU Time : 24.461 [TSC-REA] Real CPU Time : 17.442 [TSC-KER] Kernel CPU Time : 16.4269 [TSC-OMP] OMP CPU Time : 17.4738
Le calcul dure réellement 17.4 sec mais, à cause du thread graphique et vu que j'ai plusieurs coeurs sur ma machine, le temps “user” est beaucoup plus important. Remarquez également le temps “kernel” qui n'est pas négligeable. Selon moi, il s'agit du temps de gestion des communications entre les deux threads, mais je n'en suis pas sur).
Chronométrage du code
Pour savoir ce qu'il faut paralléliser en premier, il est important d'avoir une idée des endroits les plus coûteux dans le code. Bien sûr, il existe les outils de profiling (AQTime, Quantify, etc.) mais l'instrumentation du code produit des temps de calcul gigantesques et il est difficile de mesurer des temps de tests “réels” avec plusieurs dizaines de milliers d'éléments.
J'ai donc introduit des timers dans Metafor. Ils sont situés das la classe Metafor
et mesurent certaines opérations: prépro, calcul des forces, detection du contact, assemblage de la matrice de raideur, etc. A la fin de l'intégration, les valeurs de ces chronos se retrouvent dans un fichier texte nommé timers.txt
(dans le workspace). Voilà ce que ça donne par exemple pour la palplanche de GDTech:
user | real | kernel | omp | |
---|---|---|---|---|
GEN_EXT_FORC | 0.358802 | 0.351 | 0 | 0.349811 |
GEN_INERT_FORC | 0.483603 | 0.424 | 0 | 0.422559 |
GEN_INTER_FORC | 3.94683 | 3.991 | 0 | 3.99881 |
Metafor | 49.9515 | 50.478 | 0.280802 | 50.5735 |
buildK | 20.7013 | 20.836 | 0.109201 | 20.8759 |
contactDetection | 2.04361 | 1.993 | 0 | 2.00017 |
prepro | 5.97484 | 6.205 | 0.0312002 | 6.21771 |
solveK | 5.81884 | 5.873 | 0 | 5.88465 |
— Romain BOMAN 2010/08/10 09:59