Turing : optimisation des entrées/sorties

Introduction

Tout code de calcul a besoin de données en entrée, mais aussi de sauvegarder des résultats (sorties). Dans ce domaine, les applications massivement parallèles ont généralement des besoins plus importants que les applications plus classiques. De plus en plus de logiciels sont maintenant limités 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 termes 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 (Input/Output), il est nécessaire de mesurer le temps pris par ces opérations. Si elles ne consomment qu'1% de votre temps Elapsed (ou temps d'horloge), ce travail d'optimisation est probablement inutile ; sauf si vous pensez que cela deviendra critique dans le futur lorsque vous aurez multiplié par 100 ou 1000 la taille du problème que vous souhaitez calculer.

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 Turing 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 Turing et Ada : il est donc tout à fait normal d'avoir des variations importantes entre deux exécutions, selon la charge sur le réseau et sur les serveurs de fichiers.

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

Avant de se lancer dans ces opérations, il est essentiel de connaître les caractéristiques matérielles de la Blue Gene/Q concernant les entrées/sorties. Celles-ci sont décrites sur 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 de la conception quelles informations devront être lues et écrites. Pour les applications massivement parallèles, il est critique de ne lire et de n'é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.

Il est fortement conseillé de séparer les procédures d'I/O du reste du code : cela favorise la lisibilité du code source et sa maintenabilité.

Approches

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

Un seul processus pour les I/O

On utilise un seul processus pour les entrées-sorties.

  • Avantages :
    • la méthode est simple,
    • On ne génère que quelques fichiers séquentiels.
  • Inconvénients :
    • elle est intrinsèquement non parallèle,
    • elle n'utilise pas du tout les possibilités des systèmes de fichiers parallèles,
    • elle est peu performante,
    • on est incapable de lire/écrire des données à des débits soutenus.

Cette méthode est donc à proscrire sur Turing sauf pour des applications qui écrivent ou lisent peu de données.

Un fichier par processus

Tous les processus lisent et écrivent leurs données indépendamment les uns des autres dans des fichiers séparés.

  • Avantages :
    • Cette méthode est simple à mettre en œuvre,
    • la parallélisation est évidente,
    • cette méthode est souvent performante, voire même la plus performante pour un nombre de processus pas très grand.
  • Inconvénients :
    • Un grand nombre de fichiers est généré,
    • les étapes de pré- et post-traitement vont devoir s'adapter au grand nombre de fichiers utilisés.
    • si tous les processus écrivent simultanément, le risque est grand de saturer les serveurs de fichiers, donc d'impacter aussi tous les autres jobs sur Ada et Turing.

Sur une machine telle que Turing, pour une application massivement parallèle avec plusieurs milliers de processus, le nombre de fichiers à manipuler devient prohibitif : cette technique est donc à éviter.

Un fichier partagé entre tous

Il est possible de ne créer qu'un seul fichier (ou quelques-uns) mais partagé entre tous les processus

  • Avantages :
    • la génération d'un très grand nombre de fichiers est évitée,
  • Inconvénients :
    • Cette approche est assez délicate car le système doit gérer un nombre important de requêtes simultanées qui risquent de limiter fortement la performance,
    • il peut aussi y avoir des conflits de gestion des verrous pour éviter que deux processus écrivent en même temps au même endroit, donc également des pertes de performances de ce point de vue.
    • Il est difficile de savoir, lors de l'écriture de l'application, où chaque processus doit écrire/lire ses données.

Cette méthode n'est pas conseillée, compte tenu de ses inconvénients multiples : si vous souhaitez une approche avec un seul fichier partagé, intéressez-vous à MPI-I/O, Parallel NetCDF ou HDF5, qui sont proposés dans les sections suivantes.

Processus spécialisés pour les I/O

Le meilleur compromis est d'utiliser un groupe de processus pour réaliser les entrées-sorties. Cela peut se faire via un groupe de 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 de tous les processus de calcul. Ce groupe de processus, réalisant les entrées-sorties, peut également calculer, pour améliorer l'équilibrage de charge.

Cette méthode a pour avantage de regrouper toutes les données dans un seul, ou quelques fichiers. Il peut donc être possible d'avoir en sortie le même nombre de fichiers que l'application exécutée séquentiellement. Cela simplifie notamment les étapes de pré/post-traitement.

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 nœuds d'I/O différents; la difficulté est alors de placer correctement ces processus sur la topologie de la Blue Gene/Q : pour cela IBM propose certaines extensions MPI (voir Blue Gene/Q Application Development). Dans ce cas, faites attention à la portabilité du code sur d'autres architectures que Blue Gene (si vous en 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. Par exemple, il est dangereux d'allouer des tableaux contenant l'ensemble des données de tous les processus sur un seul…

De plus, comme pour les méthodes précédentes, 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 base (entiers, réels simple précision…).

MPI-I/O

MPI-I/O permet d'obtenir des performances élevées pour les entrées-sorties parallèles. En effet, 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 performances car de nombreuses optimisations deviennent possibles (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.

MPI-I/O permet de rassembler les données de tous les processus (ou seulement de certains) 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, pas encore supporté sur Turing actuellement.

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

Parallel NetCDF

Parallel NetCDF est une bibliothèque d'entrées/sorties parallèles haute-performance, partiellement 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é Climatologie.

Parallel NetCDF a été développé indépendamment 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 consiste en 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 Turing si elle est utilisée correctement; le format de fichier est assez simple et limite les surcoûts, il est ainsi théoriquement performant.

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

Malheureusement, l'utilisation de Parallel NetCDF est un peu lourde (déclaration de toutes les variables et des dimensions 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 haute-performance. Elle consiste aussi en 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; il apporte beaucoup plus de fonctionnalités et de souplesse au prix de surcoûts plus importants. Ces surcoûts restent néanmoins raisonnables voire négligeables pour des fichiers de taille importante.

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

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

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

Autres bibliothèques

A l'IDRIS, plusieurs autres bibliothèques d'entrées/sorties sont disponibles :

Conseils divers

Voici quelques pistes et conseils qui peuvent améliorer les performances de vos entrées/sorties sur Turing :

  • ne pas ouvrir et fermer des fichiers trop fréquemment car cela implique de nombreuses opérations sur le système de fichiers. La meilleure façon de procéder est d'ouvrir un fichier au début du programme 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 : pour chaque fichier ouvert, des ressources sont réservées et gérées par le système, aussi bien du côté des nœuds d'I/O que du côté des serveurs de fichiers;
  • ouvrir les fichiers dans le mode adéquat : si un fichier n'est destiné qu'à être lu, il faut l'ouvrir en mode read-only. 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;
  • écrire/lire les tableaux/structures de données en une seule fois plutôt qu'élément par élément : ignorer cette règle a un impact négatif très important sur les performances;
  • 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 comprennent tout ce qui décrit les données : il s'agit généralement des paramètres des calculs effectués, des tailles des tableaux, etc. Il est souvent plus simple de séparer le contenu des fichiers en une première partie contenant les métadonnées, suivie par une deuxième partie contenant les données proprement dites;
  • créer des fichiers indépendants du nombre de processus : cela rendra beaucoup plus simple les post-traitements, mais aussi les redémarrages éventuels (restarts) avec un nombre différent de processus.

Pour en savoir plus

Les principaux documents concernant la Blue Gene/Q traitent également des entrées/sorties; ils peuvent vous aider à mieux comprendre le fonctionnement de Turing.