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.