Écoulements fluides incompressibles
Parallélisation
Les approximations des équations de Stokes ou de Navier-Stokes décrivant un fluide incompressible résolues avec l’algorithme de projection que nous avons exposé dans ce chapitre reposent sur la résolution de deux équations elliptiques. La première est pour la prédiction de la vitesse et la second seconde pour la solution de la pression. Ceci revient à résoudre à chaque étape des systèmes d’équations linéaires de type : Ax = b (4.35) avec A une matrice carrée donnée définie positive non singulière et b un vecteur donné. L’inconnue x peut représenter soit la vitesse soit la pression. Nous utilisons, pour la résolution de ce système, le schéma itératif Bi-CGStab (Bi-Gradient Conjugué Stabilisé) [89, 94]. Pour réduire le temps de calcul, nous avons eu recours à la parallélisation des solveurs en utilisant ce schéma à l’aide la bibliothèque OpenMP1
Mise en œuvre avec OpenMP
La bibliothèque OpenMP est une interface de programmation contenant un ensemble de directives pour paralléliser un code sur une architecture à mémoire partagée. Ces directives permettent de faire les calculs plus rapidement en utilisant plusieurs processeurs. En effet, chaque processeur exécute un bout de programme avec un unique jeu de variables et les directives OpenMP définissent les zones parallèles et les attribuent à des différents threads. Chaque threads exécute des tâches en parallèle sur un processeur ou cœur (core) indépendant. Tâches parallèles Programme principal Figure 4.9: Programme principal et tâches parallèles. Dans les régions parallélisées, le programme principal via un master thread lance des threads (dont le nombre a été précisé auparavant par l’utilisateur) pour répartir la charge de calcul. Chaque thread exécute un ensemble d’instructions puis se met en attente jusqu’à la fin de toutes les tâches. Après synchronisation, le master thread reprend la main (cf. figure 4.9). Les variables partagées sont utilisées par tous les threads sur la même zone mémoire tandis que chaque copie des variables privées utilise une zone mémoire accessible uniquement par le thread concerné. Cette forme de parallélisation permet de développer rapidement en restant proche du code séquentiel sans avoir à tout reconstruire. 108 4.5 Parallélisation Dans le cas qui nous intéresse, on peut par exemple répartir une boucle for i =1: n sur plusieurs threads avec une boucle de taille n+, définie pour chaque thread tel que Dn+ = n. Ceci nous permet donc de diminuer le temps d’exécution de la boucle en question en le divisant au mieux par le nombre de processeurs. Sur la figure suivante, nous présentons un exemple d’exécution de la boucle avec m processeurs partageant deux mémoires comme sur notre cluster 2 Pour ne pas avoir des conflits par rapport à la mémoire partagée, il est nécessaire d’initialiser les variables utilisées dans la zones parallélisées. Dans notre code, les variables qui sont utiles seulement à l’itération sont déclarées privées. Les indices des boucles (internes à la zone parallèle), les indices dans les tableaux et toutes leurs correspondances locales ou globales sont aussi privés puisqu’ils sont propres aux itérations de la boucle parallélisée.
Tests de performance
La parallélisation doit permette de réduire le temps de calcul lors des appels du solveur algébrique. Ainsi, pour mesurer les performances de l’algorithme parallélisé, on considère sur le domaine Ω = [0, 1]3 le problème de Poisson suivant : ! −∆u = f dans Ω, u = 0 sur ∂Ω. (4.37) On prend pour le second membre la fonction f = sin(πx) sin(πy) sin(πz) et on utilise la discréditation DDFV en appliquant l’opérateur ∆T (cf. la section 3.5.3 du chapitre précédent). On résout ce problème sur deux maillages différentes qui correspondent à : – Cas 1 : taille de grille de 803, soit 1.081.841 inconnues. – Cas 2 : taille de grille de 503, soit 272.651 inconnues Dans les deux cas, on lance plusieurs tests en exécutant le programme sur la même machine CPU du cluster. On commence par un premier test sans parallélisation, ce qui nous donne un temps d’exécution T0. Puis on lance une série de tests en utilisant la parallélisation avec 2, 4, 6, … jusqu’à 20 cœurs et à chaque fois on calcule le temps d’exécution Tp. Ceci nous permet de calculer pour chaque test l’accélération du programme, le speedup : T0/Tp. Il s’agit donc du ratio du temps de calcul en séquentiel (sur 1 cœur) sur le temps de calcul en parallèle sur p cœurs. Plus ce ratio se rapproche du nombre des cœurs utilisés pour le calcul parallèle, plus l’efficacité parallèle du code est bonne.