Amélioration de la scalabilité en mémoire de la dynamique de LMDZ Contexte... (Milestone CONVERGENCE MS 2.1a) La dynamique de LMDZ résout les équations de Naviers-Stoke sur la sphère en milieu tournant ainsi que les équations de transport. Après discrétisation, ces équations sont résolues par des schémas explicites en différences et volumes finis. Cela implique, pour le calcul de chaque maille, qu il est nécessaire d'avoir connaissance des valeurs des mailles voisines, soit premier, soit voisin suivant les cas. La parallélisation de la dynamique impose de nombreuses communications entre les différents domaines voisins pour échanger les valeurs manquantes aux frontières. A contrario, la physique de LMDZ résout l'ensemble des processus atmosphérique au sein d'une même colonne (rayonnement, convection, précipitation, etc...). Chaque colonne d'atmosphère étant indépendante, une parallélisation de type «embarassingly parallel» est implémentée, ne posant aucun problème de scalabilité, que ce soit sur le calcul ou sur la mémoire. La dynamique est donc le principal goulet d étranglement pour la scalabilité du modèle, en particulier lors de la montée vers les hautes résolutions, car la diminution du pas de temps, dû au respect des critères de convergence CFL, implique un ratio de calcul dynamique/physique de plus en plus important. La dynamique de LMDZ a été initialement parallélisée avec MPI (Messsage Passing Interface) en découpant puis en distribuant le domaine horizontal sur les différents processus. Le resserrement des méridiens aux pôles pose des problèmes de stabilité qui sont résolus par un processus de filtrage des ondes les plus rapides appliqué à chaque opérateur différentiel, pour des raisons de conservation. Ce filtrage, appliqué sur 1/3 du globe, couple l'ensemble des mailles d'une même latitude. Il en résulte deux conséquences importantes : d'une part le découpage du domaine horizontal se fait uniquement en bande de latitude, un découpage en longitude serait contre-productif. D'autre part, le calcul du filtrage étant extrêmement couteux, les domaines près des pôles calculent beaucoup plus de les domaines près de l'équateur. Il en résulte un déséquilibrage de charge très important. Ce problème est résolu en attribuant moins de bandes de latitude aux domaines près des pôles pour obtenir un bon équilibrage. Malheureusement, dans la dynamique il existe plusieurs processus pour lesquelles le poids du filtrage est très différent, et une distribution optimum ne peut être trouvée pour l'ensemble de la dynamique. On a ainsi 4 distributions différentes: distribution Caldyn : résolutions des équations Navier-Stocke, dérivée simple : filtrage moyen. distribution Van-Leer : transport : pas de filtre. distribution dissipation : viscosité, Laplacien itéré : filtrage intensif. distribution physique : équilibrage optimum pour la physique. Au passage de chacun de ses processus, il est nécessaire de redistribuer les données de manière à garantir un équilibrage de charge optimum. Pour simplifier les opérations de transfert de distribution, l'ensemble des tableaux sont déclarés sur le domaine global, mais les calculs ne sont effectués que sur une partie du tableau, en modifiant les indices de départ et de
fin des boucles de calcul. Changer de distribution revient donc à modifier les indices des boucles et à transférer les données manquantes. Pour déterminer l'équilibrage optimum de chaque distribution, on utilise une procédure itérative d'ajustement. Pour chaque distribution, le modèle part d'une configuration équirépartie en bande de latitude. On calcule le temps de passage pour chacune des distributions, puis en fonction des temps relevés, on enlève une bande au processus le plus lent et on en rajoute une au processus le plus rapide. On recalcule ensuite la nouvelle distribution et on transfère les données manquantes à chaque domaine. On prend également en compte les fluctuations des temps de passage en calculant les écarts types. En itérant successivement et suffisamment longtemps, on finit par converger vers les distributions équilibrées de manière optimum. Les distributions sont ensuite écrites dans le fichier «bands.dat» qui sera relu lors d une prochaine exécution, ce qui évite de refaire l'ajustement à chaque redémarrage. On comprend donc assez facilement qu'avec des tailles de domaines qui évoluent dynamiquement au gré des différents processus de calcul, il était beaucoup plus simple de déclarer l'ensemble des variables sur le domaine global mais en ne travaillant que sur la portion du tableau correspondant à la distribution en cours. L inconvénient est la consommation de la mémoire qui n'est pas distribuée sur les différents processus MPI de la simulation. A l'origine, la consommation mémoire n'était pas le problème prioritaire (calculateur vectoriel à grosse mémoire, faible résolution). Avec des résolutions plus élevées, le problème a été contourné en ajoutant un niveau de parallélisme OpenMP sur les niveaux verticaux. La mémoire étant partagée par l'ensemble des threads, sa consommation par cœur de calcul est donc beaucoup moins importante. Néanmoins pour des résolutions très élevées (1/2, 1/3 ) ou des configurations à grand nombre de traceurs transportés (activation de la chimie et des aérosols d'inca, ~121 traceurs) le problème des limites mémoires se pose à nouveau. Certaines de ces configurations ne peuvent tourner sur les calculateurs actuels moins pourvu en mémoire que les machines vectorielle d ancienne génération, à moins de dépeupler considérablement les cœurs de calculs par nœuds. Le travail décrit dans la suite de ce rapport présente le travail effectuer sur la nouvelle version de la dynamique de LMDZ (dyn3dmem) permettant de distribuer la mémoire entre les différents processus MPI afin de lever les limitations actuelles. Idée générale Il est nécessaire de déclarer les tableaux des variables en fonction de la taille des domaines locaux. Les dimensions locales doivent également intégrer la taille des halos de recouvrement (au maximum 2 au nord et 2 au sud). Pour les champs utilisés dans plusieurs distributions, afin d éviter de les allouer puis de les désallouer à chaque changement, on doit dupliquer les tableaux pour chacune des distributions concernée. Lors du changement, les données ainsi que les halos de recouvrement seront copiés d un processus à l autre si nécessaire, afin de compléter les données manquantes de la nouvelle distribution. La procédure d'ajustement automatique complique ce fonctionnement, car on va changer dynamiquement de distribution, donc de taille des tableaux pour une même distribution suivant les pas de temps. Il faut donc allouer un tableau avec la nouvelle taille, copier les données communes de l'ancien tableau vers le nouveau, transférer les données manquantes à partir des autres processus, désallouer l'ancien tableau, puis associer le champ au nouveau tableau grâce au mécanisme des pointeurs Fortran.
Implémentation Afin d'implémenter cette nouvelle version, il a été nécessaire de modifier environ 60 routines de la dynamique et de recoder les routines de transfert de halo. Cette nouvelle version de la dynamique a donné lieu à la création d'une nouvelle branche dyn3dmem qui peut être consultée dans le répertoire libf/dy3dmem des sources de LMDZ sous SVN. Déclaration des tableaux Les dimensions des tableaux dépendent des indices globaux iim, jjm et llm: nombre de longitude et de latitude. On a deux types de champs en fonction de la déclaration de leur dimension : Champ de type «u» : REAL :: ucov(iim+1, jjm+1,llm) (2 dimensions pour l'horizontale) REAL :: ucov(ijmp1,llm) avec ijmp1=(iim+1)*(jjm+1) (1 dimensions pour l'horizontale) Champ de type «v» : REAL :: vcov(iim+1, jjm,llm) ou REAL :: vcov(ijm,llm) avec ijmp1=(iim+1)*jjm Dans la nouvelle version, pour déclarer les tableaux localement, on définit les bornes hautes et basse pour les indices de latitude correspondant à la taille du domaine local plus la taille des halos de recouvrement (de taille 2). On a donc l'évolution suivante pour tous les tableaux déclarés sur la grille de la dynamique : REAL :: ucov(iim+1, jjm+1,llm) => REAL :: ucov(iim+1, jjb_u:jje_u,llm) REAL :: ucov(ijmp1,llm) => REAL :: ucov(ijb_u:ije_u,llm) REAL :: vcov(iim+1, jjm,llm) => REAL :: vcov(jjb_v:jje_v, jjm,llm) REAL :: vcov(ijm,llm) => REAL :: vcov(ijb_v:ije_v,llm) Indice des boucles sur le domaine horizontal Les domaines MPI horizontaux sont découpés en bande de latitudes, on définit donc : jj_begin: indice de la première bande de latitude. jj_end : indice de la dernière bande de latitude. ij_begin : premier point du domaine local horizontal de taille global ijm. ij_end : dernier point du domaine local horizontal de taille ijm. Les boucles sont donc de la forme : Pour 2 dimensions sur l'horizontale DO j=jj_begin,jj_end
DO i=1,iim+1 END DO END DO Pour 1 dimension sur l'horizontale DO ij=ij_begin,ij_end END DO On doit donc avoir jjb_u et jjb_v < jj_begin, ijb_u et ijb_v < ij_begin, jje_u et jje_v > jj_end, ije_u et ije_v > ij_end, pour intégrer les halos de recouvrement dans la taille des tableaux. Création et changement de distribution Une distribution est définie par le nombre bandes de latitude assigné à chacun des processus MPI. Elle est représenté par un type dérivé : TYPE(distrib), contenant ces informations en interne. Création d une distribution Une distribution est créée à partir d'un tableau (de la taille du communicateur MPI) contenant le nombre de bande latitude assigné à chaque processus. SUBROUTINE create_distrib(nb_bands, distrib) INTEGER :: nb_bands(mpi_size)! Bande de latitude TYPE(distrib) :: distrib! Nouvelle distribution Passage d'une distribution à l'autre Le changement de distribution se fait par l'appel à la routine «set_distrib» qui redéfini les indices définissant le domaine local pour la nouvelle distribution : jj_begin, jjb_u, jjb_v, ij_begin, ijb_u, ijb_v, jj_end, jje_u, jje_v,ij_end, ije_u et ije_v : CALL set_distrib(new_distrib) Transferts des champs d'une distribution à l'autre (Défini dans le fichier allocate_field.f90) SUBROUTINE switch_u(field, old_distrib, new_distrib, up, down) SUBROUTINE switch_v(field, old_distrib, new_distrib, up, down) REAL,POINTER :: field(:)! Champ a redistribuer (version 1D, 2D, 3D) TYPE(distrib),INTENT(IN) :: old_distrib! Ancienne distribution TYPE(distrib),INTENT(IN) :: new_distrib! Nouvelle distribution INTEGER, OPTIONAL,INTENT(IN) :: up! Taille du halo au nord (optionnel) INTEGER, OPTIONAL,INTENT(IN) :: down! Taille du halo au sud (optionnel)
=> Transfère les champs déclarés en 1D sur l'horizontale d'une distribution à l'autre. Le champ est désalloué puis réalloué en fonction de la nouvelle taille du domaine local. Il existe également la variante pour les champs déclarés en 2D sur l'horizontal (iim+1, jjm+1) : SUBROUTINE switch2d_u(field, old_distrib, new_distrib, up, down) SUBROUTINE switch2d_v(field, old_distrib, new_distrib, up, down) Transfert de halos (Défini dans le fichier mod_hallo.f90) Routines permettant l'enregistrement des halos des différents champs. L'information est stockée dans un objet de type «request». Plusieurs requêtes de transfert peuvent être enregistrées dans le même objet. La routine permet également de transférer les données manquantes en cas de changement de distribution. SUBROUTINE Register_SwapField_u(FieldS, FieldR, new_dist, a_request,& SUBROUTINE Register_SwapField_v(FieldS, FieldR, new_dist, a_request,& REAL, INTENT(IN) :: FieldS(:)! Champ en envoi (version 1D, 2D ou 3D) REAL, INTENT(OUT) :: FieldR(:)! Champ en reception (version 1D, 2D ou 3D) TYPE(distrib),OPTIONAL,INTENT(IN) :: old_dist! (distribution du champ! d'envoi) TYPE(distrib),INTENT(IN) :: new_dist! (distribution du champ de réception) INTEGER,OPTIONAL,INTENT(IN) :: up! (taille du halo au nord) INTEGER,OPTIONAL,INTENT(IN) :: down! (taille du halo au sud) TYPE(request),INTENT(INOUT) :: a_request Il existe également la variante pour les champs déclarés en 2D sur l'horizontale (iim+1, jjm+1) : SUBROUTINE Register_SwapField2d_u(FieldS, FieldR, new_dist, a_request,& SUBROUTINE Register_SwapField2d_v(FieldS, FieldR, new_dist, a_request,& Routine effectuant l'envoi des halos enregistrés dans la requête. L'envoie est effectué de manière asynchrone en utilisant des appels MPI non bloquants : MPI_ISsend et MPI_IRecv. SUBROUTINE SendRequest(a_Request) TYPE(request),INTENT(IN) :: a_request Routine permettant de compléter l'envoi asynchrone précédent et s'assurant que les buffers peuvent être réutilisés : SUBROUTINE WaitRequest(a_Request) TYPE(request),INTENT(IN) :: a_request
Performances Pour mesurer les gains apportés par cette nouvelle implémentation, nous avons choisi de comparer une configuration de LMDZ de 280x192 (longitude x latitude) sur 39 niveaux verticaux avec 121 traceurs atmosphériques, typiques d'un couplage avec le code de chimie atmosphérique INCA. Nous avons comparé la mémoire consommée par la partie dynamique pour les deux versions parallèles dyn3dpar (ancienne version) et dyn3dmem (nouvelle version). Dans cette figure nous présentons la mémoire consommée par processus MPI (en Mo), en faisant varier le nombre de processus. La mémoire consommée par la partie physique étant distribuée et scalable, elle n a pas été prise en compte dans cette étude car elle est identique pour les deux versions. Mais elle devrait bien évidement s'ajouter au coût de la partie dynamique. Pour la version dyn3dpar, les résultats ont été obtenus en dépeuplant les nœuds (1 processus par nœud), car sans cela la mémoire n'aurait pas été suffisante pour faire tourner le modèle. A plus haute résolution il devient impossible de faire tourner le modèle sur une architecture classique, la mémoire étant, dans tous les cas, insuffisante. Pour cette version, on voit clairement sur la courbe que le coût en mémoire par processus reste approximativement constant (~ 14 Go) même en augmentant le nombre de processus MPI. Pour la version dyn3dmem, la mémoire est beaucoup mieux distribuée et montre une bonne scalabilité. Avec 64 processus, la mémoire consommée par processus est d'environ 2.5 Go ce qui permet de tenir aisément en mémoire sur un nœud de c alcul, une architecture typique
disposant de 3 à 4 Go par cœur. Le gain obtenu entre les deux configurations pour 64 processus MPI est de près d'un facteur 5. Avec ce nouveau développement, nous pensons avoir résolu le problème de mémoire de LMDZ, même à très haute résolution. En effet, même si la consommation par processus devient plus importante en augmentant la résolution, le niveau de parallélisme OpenMP en mémoire partagée permet d'augmenter amplement la mémoire consommée par processus MPI sans avoir à dépeupler les nœuds de calcul, chaque thread se partageant la même mémoire.