Table des matières
Support avancé : RAMSES
Introduction
Ce projet de support avancé réalisé par l'IDRIS a permis d'augmenter l'extensibilité d'un code de calcul d'astrophysique jusqu'à 24.576 coeurs.
Objectifs/problématique
Le code RAMSES est utilisé dans le projet DEUSS. Il vise a réaliser une simulation en 2048^3 sur 6 racks Blue Gene/P (24.576 coeurs en mode VN).
A charge de travail équivalente par processus (en terme de nombre de mailles), l'application semble consommer de plus en plus de mémoire lorsque le nombre de processus augmente. Cette augmentation est telle qu'il n'est pas possible de réaliser la simulation avec un raffinement suffisant sur 6 racks de Babel.
Le but de ce support avancé est donc de tenter d'identifier pourquoi les besoins en mémoire augmentent et d'essayer de réduire l'empreinte mémoire de RAMSES.
Identification du problème
Type communicator
Nous avons utilisé la bibliothèque de mesure de la consommation mémoire libbgpidris (devenue libhpcidris). Le code a été instrumenté dans toutes les zones d'allocation importantes du code. Cela a permis d'identifier précisement que certaines structures utilisaient beaucoup de mémoire. Il s'agit principalement de 4 structures de données de type communicator et de dimensions (ncpu,nlevelmax).
La structure est definie dans amr/amr_commons.f90
! Communication structure type communicator integer ::ngrid integer ::npart integer ,dimension(:) ,pointer::igrid integer ,dimension(:,:),pointer::f integer(kind=i8b),dimension(:),pointer::f8 real(kind=8),dimension(:,:),pointer::u end type communicator
Les tableaux sont :
type(communicator),allocatable,dimension(:) ::active type(communicator),allocatable,dimension(:,:)::boundary type(communicator),allocatable,dimension(:,:)::emission type(communicator),allocatable,dimension(:,:)::reception type(communicator),allocatable,dimension(:,:)::active_mg type(communicator),allocatable,dimension(:,:)::emission_mg
Le problème est que les tableaux emission, reception, active_mg et emission_mg sont de taille ncpu*nlevelmax et donc lorsque le nombre de processus augmente la consommation mémoire augmente également.
Le type communicator consomme 160 octets, nlevelmax vaut 17 et ncpu vaut 24.576 dans le cas qui nous intéresse. Cela donne une consommation pour un tableau comme emission de 66 Mo et donc les quatres tableaux consomment 267 Mo.
Ce sont des tableaux qui sont utilisés pour les communications. Or, un processus ne communique qu'avec un nombre limité de processus. Ces tableaux sont globalement vides et utilisés très partiellement.
Tableaux statiques
En faisant un size sur l'application, on constate une taille de BSS assez élevée (plus de 65Mo).
text data bss dec hex filename 4088310 316144 68926796 73331250 45ef232 ramsesQ3d
Un size sur les differents fichiers objets (.o) a permis d'identifier les fichiers sources les plus consommateurs :
41912 988 10188000 10230900 9c1c74 godunov_fine.o 65308 6228 2778752 2850288 2b7df0 output_part.o 63364 2160 50192000 50257524 fede74 umuscl.o
Ces tailles importantes de BSS sont majoritairement dues à des tableaux à plusieurs dimensions avec l'attribut save.
Améliorations apportées
Modification du type communicator
Une optimisation faite a été de réduire la taille de la structure en utilisant une structure intermédiaire :
type point_comm integer ,dimension(:) ,pointer::igrid integer ,dimension(:,:),pointer::f integer(kind=i8b),dimension(:) ,pointer::f8 real(kind=8) ,dimension(:,:),pointer::u end type point_comm ! Communication structure type communicator integer ::ngrid integer ::npart type(point_comm), pointer ::pcomm end type communicator
Le type communicator ne consomme plus que 16 octets, et donc les 4 tableaux consomment 27 Mo. Le pointer pcomm n'est alloué que lorsque l'on a besoin d'utiliser la structure.
A noter que l'on ne désalloue pas le pointeur pcomm lorsque l'on désalloue l'une des composantes de point_comm (igrid,f,f8,u) même si les autres composantes ne sont pas allouées.
Remplacement des structures emission et emission_mg
Les structures emission et emission_mg contenaient beaucoup d'éléments inutilisés. Pour gagner de la mémoire, seules les valeurs utilisées sont allouées. Pour cela, il faut passer par une allocation plus dynamique et gérer la liste des processus avec qui il y a effectivement des communications (càd lorsqu'il y a au moins un élément à transmettre ou à recevoir). Deux nouveaux types ont été créés dans amr_commons.f90 :
type communicator_var integer :: nactive ! Number of processes sharing data with current process integer :: ngrids_tot ! Total number of octs to share integer, allocatable,dimension(:) :: cpuid ! Number of the active process integer, allocatable,dimension(:) :: ngrids ! Number of octs to share with each active process integer, allocatable,dimension(:) :: igrid real(kind=8),allocatable,dimension(:) :: u integer, allocatable,dimension(:) :: f end type communicator_var type communicator_var2 integer :: nactive ! Number of processes sharing data with current process integer :: nparts_tot ! Total number of particles to share integer, allocatable,dimension(:) :: cpuid ! Number of the active process integer, allocatable,dimension(:) :: nparts ! Number of particles to share with each active process real(kind=8),allocatable,dimension(:) :: u integer, allocatable,dimension(:) :: f integer(kind=8),allocatable,dimension(:):: f8 end type communicator_var2
La structure emission a été remplacée par 2 structures de dimension nlevelmax (au lieu de ncpu*nlevelmax) :
type(communicator_var),allocatable,dimension(:)::emission type(communicator_var2),allocatable,dimension(:)::emission_part
emission est principalement utilisé dans virtual_boundaries pour échanger des mailles. emission_part est utilisé dans particle_tree pour échanger des particules (emission y est utilisé aussi pour identifier les mailles auquelles appartiennent ces particules).
La structure emission_mg est maintenant de type communicator_var et de dimension nlevelmax (au lieu de ncpu*nlevelmax) :
type(communicator_var),allocatable, dimension(:) :: emission_mg
Ces structures sont créées en 3 phases. Un premier balayage permet de compter le nombre d'éléments qui doivent entrer dans celles-ci (nombre de processus actifs càd avec au moins un élément et nombre total d'éléments à échanger avec l'ensemble des processus). Ensuite, les tableaux de la structure sont alloués. Et finalement, ces structures sont remplies.
Le nombre de processus impliqué dans les échanges est bien moindre que le nombre de processus total et augmente très lentement. L'économie de mémoire est donc très importante.
Elimination des tableaux statiques
Les tableaux avec l'attribut save identifiés dans la section précédente ne requièrent pas d'être statiques. En effet, leur contenu est de toute facon entièrement écrasé à chaque appel. De plus, les procédures contenues dans godunov_fine et umuscl ne sont pas utilisées pour les calculs effectués ici. Retirer cet attribut permet donc de gagner 60 Mo.
Résultats
Sur 6 racks en mode VN (24.576 processus) :
Version | ngridmax | Mémoire (Mo) | Temps/timestep (s) |
---|---|---|---|
Originale | 95.000 | 460,8 | 543,708 |
Optimisée | 95.000 | 165,8 | 467,817 |
Originale | 450.000 | >512 | - |
Optimisée | 450.000 | 375,7 | 475,841 |
La mémoire est celle correspondant au processus le plus gros consommateur.
Sur le cas avec ngridmax=95.000, la mémoire consommée a été réduite de 295 Mo, soit d'un facteur 3 ! Ce gain est à peu près proportionnel au nombre de processus et devrait donc être à peu près stable quel que soit la valeur de ngridmax (ou de npartmax). RAMSES en sa version originale n'était pas capable de tourner sur 6 racks avec ngridmax=450.000 qui est la valeur estimée par l'utilisateur pour pouvoir mener à bien ses calculs. Il aurait fallu environ 760 Mo/coeur). Par contre, la nouvelle version n'a besoin que de 375 Mo pour le processus le plus gourmand. La réduction est telle qu'il est même encore possible d'augmenter la valeur de ngridmax à environ 650.000.
De plus, résultats indirects de nos modifications, le temps de calcul a été diminué de 14% par pas de temps. Nous pensons que cela est dû à des boucles plus courtes (en nombre de processus actifs au lieu du nombre de processus total pour les échanges de données) et à des structures de données plus compactes et contiguës en mémoire.
Pour aller plus loin
Le but de ce support avancé était de pouvoir réaliser les calculs souhaités sur 6 racks. Une optimisation complète n'a pas été réalisée car pas nécessaire au vu des améliorations obtenues.
Pour aller plus loin, plusieurs problématiques pourraient être traitées :
- tous les tableaux en ncpu devraient être éliminés pour obtenir une extensibilité en mémoire pratiquement optimale ;
- toutes les variables avec l'attribut save devraient être ré-évaluées pour vérifier qu'il est bien nécessaire (attention, un impact léger sur les performances pourrait apparaître dans les procédures appellées de très nombreuses fois) ;
- les pointeurs pcomm devraient être désalloués lorsque les composantes de la structure point_comm vers laquelle ils pointent sont toutes désallouées.
Conclusions
Les causes de la surconsommation de mémoire du code RAMSES ont pu être identifiées à l'aide de la bibliothèque libbgpidris (devenue libhpcidris)).
Des modifications du code ont été apportées qui ont permis de réduire drastiquement l'empreinte mémoire de l'application (gain de 295 Mo par processus pour un calcul sur 24.576 coeurs). Les gains obtenus devraient permettre à l'utilisateur de réaliser les calculs haute résolution envisagés.