Babel : optimisation des entrées/sorties

Sommaire

Introduction

Tout code de calcul a besoin de données en entrée et de sauvegarder des résultats (sorties). Les applications massivement parallèles ont généralement des besoins plus importants que les applications plus classiques. De plus en plus de logiciels commencent à être limité par les performances de leurs entrées/sorties.

Le but de ce document est de vous donner des conseils et des pistes qui vous aideront à améliorer la gestion de vos entrées/sorties en terme de performance, organisation et volume.

Méthodologie

Comme pour toute optimisation de performances, la première question à se poser est : est-ce que c'est utile ? Avant de vous lancer dans l'optimisation des entrées/sorties (I/O), il est nécessaire de mesurer le temps pris par ces opérations. Si elles ne consomment que 1% de votre temps d'horloge, ce travail est probablement inutile (sauf si vous pensez que cela deviendra critique dans le futur lorsque vous aurez multiplié par 100 la taille des problèmes étudiés).

Après toute modification, il est impératif de vérifier que les données sont toujours correctes et que les performances ne sont pas dégradées.

Attention : les mesures de performance des entrées/sorties sur Babel sont délicates à réaliser car vous n'avez pas un accès dédié aux disques. Les systèmes de fichiers WORKDIR et TMPDIR sont partagés entre tous les travaux s'exécutant en batch et en interactif sur Babel et Vargas. Il est donc tout à fait normal d'avoir des variations importantes entre 2 exécutions selon la charge sur les serveurs de fichiers et le réseau.

Modifier la façon de gérer les I/O peut aussi s'avérer nécessaire ou utile pour des questions de portabilité entre différentes architectures ou de gestion des fichiers (format des fichiers ou nombre trop important).

Avant de se lancer dans ces opérations, il est aussi essentiel de connaître les caractéristiques matérielles de la Blue Gene/P concernant les entrées/sorties. Celles-ci sont décrites sur notre site web dans la rubrique description matérielle détaillée.

Conception

Lors de la conception d'un code de calcul, les entrées/sorties sont souvent négligées. Pourtant, elles sont fondamentales pour toute application.

Il est important de déterminer dès le début quelles informations devront être lues et écrites. Pour les applications massivement parallèles, il est critique de ne lire et écrire que ce qui est nécessaire. L'approche utilisée pour les I/O (voir section suivante) est déterminante pour la portabilité et l'évolutivité de l'application.

Séparer les procédures d'I/O du reste du code est conseillé. Cela favorise la lisibilité du code source et la maintenabilité.

Approches

Pour réaliser des entrées/sorties dans des applications parallèles, de nombreuses méthodes sont possibles. Certaines ne sont adaptées qu'à certains problèmes, d'autres peuvent être plus simple à mettre en ?uvre, plus portables ou bien plus performantes.

Un fichier par processus

La méthode la plus simple consiste à ce que tous les processus lisent et écrivent leurs données indépendamment les uns des autres dans des fichiers séparés.

Utiliser cette approche apporte certains avantages comme la simplicité de l'implémentation des entrées/sorties dans l'application et une parallélisation triviale. Cependant, le revers de la médaille est la création d'un grand nombre de fichiers et le report des difficultés de la parallélisation des I/O vers les étapes de pré- et post-traitement.

La simplicité de cette méthode ne l'empêche pas d'être souvent performante voire même la plus performante pour un nombre de processus pas trop grand. Sur une machine telle que Babel, pour une application réellement massivement parallèle (au moins 1024 coeurs), le nombre de fichiers à manipuler est prohibitif et est donc à éviter absolument. De plus, si tous les processus écrivent simultanément, le risque de saturer les serveurs de fichiers est non-négligeable.

La portabilité des données d'une machine à une autre n'est pas assurée. La portabilité concerne principalement l'endianness des données ainsi que la taille des différents types de bases (entiers, réels simple précision,…).

Processus spécialisés pour les I/O

Une technique souvent utilisée est d'utiliser un ou plusieurs processus pour réaliser les entrées/sorties. Cela peut se faire via un processus collectant et distribuant toutes les données, via plusieurs processus gérant chacun les I/O d'un groupe de processus de calcul ou encore via un groupe de processus qui vont se partager les entrées/sorties des processus de calcul. Ces processus d'entrées/sorties peuvent également calculer ou pas (si elles prennent du temps, il faut faire très attention à l'équilibrage de charge).

Cette méthode a pour avantage de regrouper toutes les données dans un seul (ou quelques) fichier(s) et donc d'avoir les mêmes fichiers que si l'application était séquentielle (dans le cas d'un seul processus gérant les I/O). Cela peut simplifier les étapes de pré/post-traitements et le travail de parallélisation.

Cependant, dans le cas d'un seul processus d'I/O, cette approche n'utilise pas du tout les possibilités des systèmes de fichiers parallèles et sera donc peu performante. Il faut donc éviter de l'utiliser pour des applications écrivant (ou lisant) des quantités de données importantes. Dans le cas de plusieurs processus d'I/O, les performances peuvent être bonnes quand leur nombre est suffisant pour saturer la vitesse d'écriture/lecture du système de fichiers. Cette approche peut également donner d'excellentes performances lorsque chaque processus d'I/O utilise des noeuds d'I/O différents. La difficulté est alors de placer correctement ces processus sur la topologie de la Blue Gene/P. Certaines extensions MPI sont proposées par IBM (voir Blue Gene/P Application Development). Faites attention à la portabilité sur d'autres architectures si vous les utilisez.

Dans le cas où ces processus ne participent pas aux calculs, il ne faut pas oublier qu'ils sont gaspillés de ce point de vue. L'implémentation n'est pas non plus toujours triviale (surtout dans le cas de multiples processus d'I/O). Il faut en effet gérer les communications pour les transferts de données depuis et vers ces processus, faire attention à leur occupation mémoire (il est dangereux d'allouer des tableaux contenant l'ensemble des données de tous les processus sur un seul),…

Cette méthode ne garantit pas non plus la portabilité des données comme pour les fichiers séparés.

Un seul processus pour les I/O

Il s'agit de la même approche que dans la section précédente mais utilisant un seul processus spécialisé. Cette méthode bien qu'ayant l'avantage de ne générer que quelques fichiers séquentiels est à proscrire sur Babel (sauf pour des applications très légères en entrées/sorties) car elle est intrinséquement non-parallèle et incapable de lire/écrire des données à des débits soutenus (un seul noeud d'I/O est utilisé).

Un fichier partagé entre tous

Il est possible de ne créer qu'un seul fichier (ou quelques-uns) partagé entre tous les processus. De cette façon, le problème d'un très grand nombre de fichiers est évité.

Cette approche est assez délicate car le système doit gérer un nombre important de requêtes simultanées et cela risque de limiter fortement la performance. De plus, il risque d'y avoir des problèmes de gestion des verrous (pour éviter que deux processus écrivent en même temps au même endroit) et donc également des pertes de performances.

La difficulté réside aussi dans l'écriture de l'application qui doit déterminer correctement où chaque processus doit écrire/lire ses données.

La portabilité n'est évidemment pas non plus assurée.

Cette méthode n'est donc pas conseillée. Si vous souhaitez une approche avec un seul fichier partagé, intéressez-vous à ce qui est proposé dans les sections suivantes (MPI-I/O, Parallel NetCDF et HDF5).

MPI-I/O

MPI-I/O permet d'obtenir des performances élevées pour les entrées-sorties parallèles. Pour se faire, les accès aux fichiers se font de manière parallèle par tous les processus MPI impliqués soit de manière individuelle, soit collective. Les opérations collectives fournissent généralement de meilleures résultats car de nombreuses optimisations deviennent possible (regroupement de requêtes, mécanisme de passoire, accès en deux phases,…).

L'utilisation de MPI-I/O se rapproche de celle des communications par messages MPI avec la possibilité d'utiliser des types dérivés et de réaliser des appels non-bloquants (entrées-sorties asynchrones) rendant possible le recouvrement I/O-calcul.

MPI-I/O permet de rassembler les données de plusieurs ou de tous les processus dans un seul fichier. Cela facilite grandement la gestion des fichiers et simplifie aussi les étapes de pré- et post-traitement.

Les fichiers générés ne sont pas portables sauf lorsqu'ils sont écrits en mode external32 (non supporté sur Babel actuellement).

Vous trouverez de plus amples informations sur l'utilisation de MPI-I/O dans le support de cours MPI-2 de l'IDRIS.

Parallel NetCDF

Parallel NetCDF est une bibliothèque d'entrées/sorties parallèles hautes-performances compatible avec le format de fichier NetCDF conçu par Unidata. Ce format est très utilisé dans le monde scientifique et particulièrement par la communauté climatologique.

Parallel NetCDF a été dévelopé, indépendemment du groupe NetCDF, pour combler l'absence de parallélisme dans la bibliothèque originale (ce qui n'est plus vrai dans NetCDF-4). Elle est une surcouche au-dessus de MPI-I/O et dépend donc des performances de cette dernière.

Cette bibliothèque donne de très bonnes performances en parallèle sur Babel si elle est utilisée correctement. Le format de fichier est assez simple limitant les surcoûts et est ainsi théoriquement performant.

Elle a l'avantage de créer des fichiers parfaitement portables entre architectures. Les fichiers sont également auto-documentés (par exemple, toutes les données sont nommées et ont des dimensions bien connues). Les outils NetCDF standards sont utilisables sur les fichiers générés et facilitent grandement les opérations de pré/post-traitements. Attention : les outils NetCDF actuels ne sont pas encore compatible avec le nouveau format de fichier totalement 64 bits introduit dans Parallel NetCDF 1.1.0 (ce format n'est pas créé par défaut, il faut l'imposer vous-même).

Malheureusement, son utilisation est un peu lourde (déclaration de toutes les dimensions et variables avant de pouvoir commencer à écrire). La documentation disponible est actuellement très limitée; particulièrement pour l'interface Fortran.

HDF5

HDF5 est tout comme Parallel NetCDF une bibliothèque d'entrées/sorties parallèles hautes-performances. Elle est une surcouche au-dessus de MPI-I/O et dépend donc des performances de cette dernière.

Le format de fichier est bien plus complexe que celui de Parallel NetCDF et apporte beaucoup plus de fonctionnalités et de souplesse au détriment de surcoûts plus importants. Ces surcoûts restent néanmoins raisonnables voire négligeables pour les fichiers de tailles importantes.

HDF5 crée des fichiers auto-documentés et portables entre tout type d'architecture.

Le nombre important de fonctionnalités de ce format le rend assez complexe à utiliser.

Des outils de pré/post-traitement simples sont fournis qui permettent par exemple de retrouver la valeur d'une variable donnée en une simple ligne de commande.

Conseils divers

Voici quelques conseils et pistes qui peuvent mener à des performances accrues sur Babel :

  • Ne pas ouvrir et fermer des fichiers trop fréquemment car cela implique de nombreuses opérations sur le système. La meilleure façon de procéder est d'ouvrir le fichier au début et de ne le fermer que lorsque son usage n'est pas nécessaire pendant un laps de temps suffisamment long.
  • Limiter le nombre de fichiers ouverts simultanément car pour chaque fichier ouvert, le système du côté des noeuds d'I/O mais aussi des serveurs de fichiers doit réserver et maintenir certaines ressources.
  • Ouvrir les fichiers dans le bon mode. Si un fichier n'est destiné qu'à être lu, il faut l'ouvrir en mode read-only car le choix du bon mode permet au système d'appliquer certaines optimisations et de n'allouer que les ressources nécessaires.
  • Ne faire des flushs (vidange des buffers) que si cela est nécessaire. Les flushs sont des opérations coûteuses.
  • Ecrire/lire les tableaux/structures de données en une fois plutôt qu'élément par élément. Ne pas respecter cette rêgle aura un impact négatif très important sur les performances des I/O.
  • Séparer les procédures faisant les I/O du reste du code source pour une meilleure lisibilité et maintenabilité du code.
  • Séparer les métadonnées des données. Les métadonnées sont tout ce qui décrit les données. Il s'agit généralement des paramètres des calculs effectués, les tailles des tableaux,… Il est souvent plus simple de séparer le contenu des fichiers en une première partie contenant les métadonnées suivie par les données proprement dites.
  • Créer des fichiers indépendants du nombre de processus. Cela rendra beaucoup plus simple les post-traitements ainsi que les redémarrages (restarts) avec un nombre différent de processus.

Pour ceux qui utilisent des I/O parallèles avec MPI-I/O et les bibliothèques qui en dérivent (Parallel NetCDF et HDF5) :

  • Toujours faire les entrées/sorties en utilisant les fonctions collectives (si tous les processus en font). L'écart de performance avec les fonctions indépendantes est généralement très important et croît avec le nombre de processus.
  • Evitez d'utiliser les procédures avec pointeurs implicites partagés (MPI_File_read_ordered par exemple). Il faut privilégier les appels collectifs avec adresses explicites (comme MPI_File_read_at_all) ou avec pointeurs implicites individuels (comme MPI_File_read_all).
  • Utiliser des appels non-bloquants pour faire du recouvrement calculs-I/O est une piste à tester.
  • Sur Babel, ne pas utiliser la procédure MPI_File_get_position_shared qui est extrêmement lente (2 minutes pour 1024 processus par exemple). Il est souvent possible de calculer manuellement la valeur du pointeur partagé.

Hints MPI-I/O

La bibliothèque MPI-I/O est portable entre de nombreux systèmes (interface commune). Cependant, chaque système peut être optimisé de façon différente. Selon le type d'entrées/sorties effectuées des optimisations différentes peuvent améliorer ou réduire les performances.

Les caractéristiques et optimisations de la bibliothèque MPI-I/O peuvent être modifiées en utilisant des hints. Dans certains cas, des variations très importantes peuvent apparaître. Les valeurs par défaut sont généralement assez performantes mais ce n'est pas toujours le cas. Comme avec toutes les optimisations, il faut faire des essais avec des jeux de données caractéristiques de votre application.

Voici la liste de ce qui est supporté sur Babel.

Pour les optimisations sur les entrées/sorties collectives utilisant des mécanismes en deux phases (collective buffering) :

  • romio_cb_read et romio_cb_write (valeur par défaut : enable) : utilisation du buffering collectif (mettre à enable pour l'utiliser pour toutes les entrées/sorties collectives, disable pour le désactiver et automatic pour laisser MPI choisir). Modifier cette valeur peut améliorer ou détériorer (selon les cas) fortement les performances.
  • bgl_nodes_pset (valeur par défaut : 8) : nombre de processus qui réalisent les opérations d'entrées/sorties. Il s'agit du nombre de processus qui écrivent ou lisent réellement les données sur le système de fichiers, les autres communiquant avec ces derniers. Ce nombre est donné par pset. Sur Babel, un pset correspond à un bloc de 256 coeurs qui partagent un même noeud d'I/O.
  • cb_buffer_size (valeur par défaut : 16777216) : taille du buffer pour les optimisations collectives en deux phases.

Les autres hints disponibles sont :

  • romio_ds_read et romio_ds_write (valeur par défaut : automatic) : utilisation du mécanisme de passoire (data sieving) qui regroupe en une requête un ensemble de petites opérations dans des blocs non contigus (mettre à enable pour l'utiliser pour toutes les entrées/sorties indépendantes, disable pour le désactiver et automatic pour laisser MPI choisir).
  • ind_rd_buffer_size (valeur par défaut : 4194304) : taille du buffer pour les lectures non collectives.
  • ind_wr_buffer_size (valeur par défaut : 4194304) : taille du buffer pour les écritures non collectives.

Utilisation avec MPI-I/O

Pour utiliser les hints dans MPI-I/O, il suffit de passer à la procédure MPI_File_open un descripteur d'infos (créé à l'aide de MPI_Info_create) au lieu de mettre MPI_INFO_NULL. Chaque hint est valorisé à l'aide d'appels à MPI_Info_set. Une fois le descripteur passé à l'ouverture du fichier, il ne faut pas oublier de le libérer (MPI_Info_free). Attention : tous les hints sont vus comme des chaînes de caractères (y compris ceux qui prennent des valeurs entières).

Voici un exemple en Fortran pour ouvrir un fichier en écriture et en positionant 3 hints différents :

integer :: ierr,infos

call MPI_Info_create(infos,ierr)
call MPI_Info_set(infos,''bgl_nodes_pset'',''8'',ierr)
call MPI_Info_set(infos,''romio_cb_write'',''enable'',ierr)
call MPI_Info_set(infos,''romio_ds_write'',''automatic'',ierr)

call MPI_File_open(MPI_COMM_WORLD,filename,MPI_MODE_WRONLY+MPI_MODE_CREATE+MPI_MODE_UNIQUE_OPEN,&
                   infos,f_id,ierr)

call MPI_Info_free(infos,ierr)

Utilisation avec Parallel NetCDF

L'utilisation des hints dans Parallel NetCDF se fait exactement de la même façon que pour MPI-I/O. Il suffit de passer le descripteur d'info dans l'appel d'ouverture du fichier. Par exemple :

status = nfmpi_create(MPI_COMM_WORLD,filename,mode,info,f_id)

Utilisation avec HDF5

L'utilisation des hints dans HDF5 passe par l'utilisation d'une liste de propriétés dans laquelle est intégrée un descripteur d'infos similaire à celui utilisé dans MPI-I/O.

Un exemple d'utilisation est illustré ici :

! Create property list
call H5Pcreate_f(H5P_FILE_ACCESS_F,prp_id,status)

call MPI_Info_create(info,status)
call MPI_Info_set(info,''bgl_nodes_pset'',''8'',ierr)
call MPI_Info_set(info,''romio_cb_write'',''enable'',ierr)
call MPI_Info_set(info,''romio_ds_write'',''automatic'',ierr)

! Use parallel HDF5
call H5Pset_fapl_mpio_f(prp_id,MPI_COMM_WORLD,info,status)

! Open file
call H5Fcreate_f(filename,H5F_ACC_TRUNC_F,f_id,status,access_prp=prp_id)

! Free ressources
call H5Pclose_f(prp_id,status)
call MPI_Info_free(info,ierr)

Pour en savoir plus

Les principaux documents concernant la Blue Gene/P traitent également des entrées/sorties et peuvent vous aider à mieux comprendre le fonctionnement de Babel.

Des tests de performances réalisés sur Babel sont également disponible dans la rubrique outils et retours d'expériences IDRIS.