Optimiser les implémentations MPI (1ère partie)
By   |  July 11, 2013

Souvent ignorées des scientifiques, les techniques d’optimisation des couches MPI sous-jacentes aux applications peuvent s’avérer décisives en termes de performances. Ce mois-ci, deux illustrations concrètes montrent qu’avec un léger effort d’appropriation, les résultats peuvent changer du tout au tout…

Jérôme Vienne, PhD
HPC Software Tools Group
Texas Advanced Computing Center (TACC)

L’une de mes missions au Texas Advanced Computing Center (TACC) d’Austin est d’aider les scientifiques qui utilisent nos clusters à obtenir les meilleures performances pour leurs applications. Cette fonction de support se concrétise par quelques jours, chaque trimestre, pendant lesquels je suis amené à répondre aux requêtes/tickets qui nous sont soumis. Durant ces journées, il m’arrive d’avoir à traiter des tickets qui montrent que les chercheurs considèrent les implémentations MPI comme des blocs immuables, des boîtes noires avec lesquelles on doit composer sans possibilité d’intervention ou d’optimisation.

Cette erreur fondamentale a pour conséquence directe un certain nombre de frustrations par rapport aux performances obtenues. Et concrètement, les messages traduisent une méconnaissance des mécanismes en jeu dans les implémentations concernées. Au travers de cet article, qui s’adresse plutôt aux scientifiques qu’aux purs développeurs MPI, je vais donc essayer de clarifier l’importance du placement des processus MPI, les possibilités qui en découlent, puis aborder la question délicate du choix du benchmark le plus approprié dans une optique d’optimisation applicative mesurable. Notez que cette première livraison sera suivie d’une seconde, qui se consacrera aux bonnes pratiques à adopter pour le passage à l’échelle et à l’exploitation des nombreuses voies de gain en performances offertes par les connexions InfiniBand.    

Chaque implémentation MPI est différente

Bien que toutes les implémentations de la bibliothèque MPI suivent le même standard, chacune doit être considérée dans son individualité. Car rien dans le standard ne définit les algorithmes à utiliser pour les communications collectives ou l’ordre de placement des processus MPI au sein d’un nœud, par exemple. Ces décisions sont laissées au libre choix des développeurs et ce sont elles, précisément, qui font la différence. Compte tenu de l’infinie variété des architectures existantes et des processus applicatifs en jeu, il est bien sûr illusoire d’espérer trouver les réglages optimaux pour tous les cas. A cet égard, certaines implémentations propriétaires sont avantagées lorsqu’elles sont utilisées sur des couches matérielle plus ou moins propriétaires (Cray, IBM…). Mais, même dans ces cas, une implémentation MPI peut nécessiter des réglages différents de ceux qui sont proposés par défaut.

En pratique, il est souvent possible d’optimiser certains comportements de l’implémentation au travers des variables d’environnements ou d’options globales. On parle ici de petits changements pouvant avoir de grandes répercussions sur la performance de vos applications. Pour cela, il vous faut connaître l’éventail des possibilités dont vous disposez en fonction de l’implémentation avec laquelle vous travaillez, ce qui implique une minimum de lecture de la documentation technique. Mais pour fastidieuse que soit cette lecture, l’effort est généralement payant. Voici deux exemples concrets pour vous en convaincre.

Le placement des processus MPI

Il est facile de concevoir qu’une communication entre deux nœuds est plus coûteuse qu’une communication à l’intérieur d’un seul. Mais il faut aussi savoir que, dans un nœud, les performances sont différentes selon que la communication se fait à l’intérieur du même socket ou entre deux sockets.

Pour illustrer la chose, nous avons mesuré la latence en utilisant le test inclus dans les OSU Micro-benchmarks de MVAPICH2 1.9 sur un nœud du cluster Stampede. Stampede disposant de deux sockets avec 8 cœurs chacun, les cœurs numerotés 0 à 7 résident sur le premier socket, tandis que les cœurs numerotés 8 à 15 résident sur le second. La figure 1 montre clairement que la latence d’une communication sur un même socket est nettement inférieure à celle d’une communication entre deux sockets. L’explication est simple : lorsque les communications se font sur le même socket, les données restent dans le même cache L3, ce qui augmente la vitesse d’échange des données.

Fig. 1 – Comparaison de latence à l’intérieur d’un noeud composé de 2 Xeon E5-2680 2.7 GHz selon que les communications restent ou non sur le même socket.

Le placement par défaut des processus MPI dépend entièrement des choix faits par les développeurs des différentes implémentations. Or, un bon placement des processus MPI est toujours indiqué pour optimiser l’utilisation du cache. Donc, pour y parvenir, il faut pouvoir répondre à deux questions :

– Quel est le placement par défaut utilisé par mon implémentation ?

– Comment le modifier ?

Beaucoup d’implémentations MPI peuvent indiquer où elles placent les processus MPI. Si ce n’est pas le cas de la vôtre, utilisez le code du listing 1, dont l’exécution renvoie la séquence de correspondance processus > cœur (cpuset). Pour mettre en évidence les différences de placement par défaut entre les implémentations, nous avons utilisé ce programme avec quatre processus MPI sur Stampede en utilisant Intel MPI et MVAPICH2.

Le listing 2 montre le résultat obtenu d’abord avec Intel MPI. Sans explication, ce résultat peut paraître déroutant : on voit qu’il y a 16 lignes et que chaque rang MPI est répété 4 fois avec un cpuset différent… En fait, il faut comprendre que chaque processus MPI est libre de migrer sur certains cœurs. Le rank 0 peut aller sur les cœurs 0 à 3, le rank 1 sur les cœurs 4 à 7, et ainsi de suite. En programmation hybride (MPI + OpenMP), ce choix est judicieux car si l’on a quatre processus MPI avec chacun quatre threads OpenMP, on sera sûr que les threads utiliseront le même cache. En revanche, dans le cas d’une implémentation purement MPI, la migration éventuelle des processus MPI peut introduire une légère perturbation. 

Avec MVAPICH2, le listing 3 montre un résultat très différent. Ici, les choses sont plus claires : chaque processus MPI est attaché à un seul cœur. L’avantage est que, dans le cadre de communications collectives de petites tailles, les échanges seront plus rapides. En revanche, pour des communications collectives de grande taille, des défauts de cache pourraient apparaître. Et dans l’exemple de programmation hybride qui est le nôtre (quatre processus MPI avec chacun quatre threads OpenMP), les résultats seront catastrophiques car les quatre threads utiliseront le même cœur.

Voilà qui démontre combien il est important d’utiliser les options disponibles sur chaque implémentation pour optimiser le comportement de base d’une application. Grâce à ces options, on arrive assez facilement à calquer le placement des tâches de Intel MPI sur celui de MVAPICH2 – ou inversement. Et ce qui est valable pour ces deux implémentations l’est aussi pour la plupart des autres…

Le choix du benchmark

Les architectures des clusters étant par nature assez différentes les unes des autres, il est impossible pour les implémentations Open Source tel que Open MPI, MPICH ou MVAPICH2 de proposer des réglages qui puissent satisfaire toutes les configurations et toutes les applications possibles.

Il faut donc faire des choix dans les réglages de l’implémentation en fonction de la nature de l’application et de la plateforme d’exécution cible. Pour simplifier les choses, on peut considérer que les réglages en question reviennent à optimiser deux caractéristiques principales :

Le point de changement entre l’eager et le rendez-vous protocol pour les communications point à point.

Le choix des algorithmes à utiliser pour chaque taille de message à l’intérieur de chaque communication collective.

Pour autant, le problème n’est pas simple. D’une part, on travaille toujours dans un contexte de pile complexe (spécificités matérielles > couche MPI variable ou pas > spécificités applicatives). D’autre part, ces deux caractéristiques sont elles-mêmes fortement variables. Pour illustrer cela, prenons un exemple basique en nous posant la question suivante : quel peut être le tuning idéal pour MPI_GATHER selon que l’on utilise les Intel MPI Benchmarks (IMB) ou les OSU Micro-Benchmarks (OMB) avec MVAPICH2 1.9 sur Stampede ?

MVAPICH2 utilise trois algorithmes pour MPI_GATHER : Binomial, Direct et Two-Level. Pour évaluer chacun des algorithmes avec IMB et OMB, nous avons forcé MVAPICH2 à n’utiliser qu’un seul algorithme à la fois pour l’ensemble des tailles de message (indiquées en octets). La figure 2-A montre le résultat obtenu avec IMB en utilisant 64 cœurs (4 nœuds). La lecture du graphique met en évidence les différences de comportement des algorithmes. Plus précisément, on constate que Two-Level donne les meilleurs résultats jusqu’à 64 octets avant de passer la main à Binomial jusqu’à 1024 Octets, qui cède ensuite le pas devant l’algorithme Direct.

Fig. 2-A et 2-B – Benchmarks IMB 64 coeurs (en haut) et OMB 64 coeurs (en bas).

Regardons maintenant la figure 2-B, qui présente les résultats obtenus avec OMB sur les mêmes 64 cœurs. Clairement, les performances changent du tout au tout. L’algorithme Direct qui était le meilleur pour les messages de grande taille se révèle ici le plus mauvais. Quant aux temps mesurés, ils sont presque dix fois inférieurs.

Pourquoi une telle différence entre IMB et OMB sur le même test exactement ? Parce que les protocoles utilisés pour mesurer la performance de MPI_GATHER sont structurellement divergents. IMB change le processus MPI émetteur du message à chaque itération (rang 0, puis 1, puis 2…), de sorte que ce benchmark ne peut pas profiter des effets de cache – contrairement à OMB qui, pour sa part, garde toujours le rang 0 comme source émettrice.

Qui a raison entre les deux ? Cette question n’a pas de réponse définitive. Ce qui est plutôt une bonne chose compte tenu des possibilités de variation des applications. La vôtre se comporte-t-elle plutôt comme IMB, comme OMB, ou de façon encore différente ? Ce qu’il faut retenir, c’est qu’Intel MPI utilise par défaut IMB pour définir le réglage de ses collectives alors que MVAPICH2, elle, utilise OMB. Voilà qui suffit à expliquer des comportements applicatifs en apparence aberrants. Et qui justifie le léger effort que représente d’abord la lecture des user’s guides propres à chaque implémentation et ensuite le benchmarking puis l’optimisation des réglages les plus appropriés au code concerné.

Lorsqu’on utilise une implémentation MPI, il est important de comprendre les choix des développeurs. Et lorsqu’on souhaite comparer deux implémentations, il faut le faire de façon judicieuse, en utilisant le même placement des processus MPI tout en essayant d’obtenir les meilleures performances sur l’application cible. Nous avons traité ici de deux problèmes qui, bien qu’assez simples, restent généralement inconnus pour beaucoup d’utilisateurs au profil plutôt scientifique que purement informatique. Dans la seconde partie de cet article, nous aborderons un sujet un peu plus avancé : les techniques de réduction d’empreinte mémoire et les possibilités offertes par le réseau InfiniBand pour améliorer la scalabilité des implémentations MPI. 

Bons développements !

© HPC Today 2024 - All rights reserved.

Thank you for reading HPC Today.

Express poll

Do you use multi-screen
visualization technologies?

Industry news

Brands / Products index