Aller au contenu

3 · MPI en profondeur

Complément de PRPA23. Ce chapitre traite ce que le cours ne peut pas couvrir en quarante-deux heures : le réglage d'une implémentation, la topologie réseau, le placement, et les motifs de communication qui limitent le passage à l'échelle.

Difficulté : ★★★ · ⏱ 25 h

Ce qui se passe réellement dans un MPI_Send

Comprendre les deux protocoles est la clé de la moitié des surprises de performance et de la totalité des interblocages mystérieux.

Protocole eager — pour les petits messages. L'émetteur copie les données dans un tampon interne et rend la main immédiatement. Le récepteur les récupérera plus tard. Coût : une copie mémoire supplémentaire, mais aucune attente.

Protocole rendezvous — pour les gros messages. L'émetteur envoie un en-tête, attend que le récepteur signale qu'il est prêt et où déposer les données, puis transfère directement. Coût : un aller-retour de latence, mais pas de copie intermédiaire.

Le seuil de bascule est un paramètre de l'implémentation, typiquement de l'ordre de quelques kilooctets à quelques dizaines de kilooctets.

L'interblocage qui n'apparaît qu'en production

// Deux rangs qui s'échangent des données
MPI_Send(envoi, n, MPI_DOUBLE, voisin, 0, comm);
MPI_Recv(recep, n, MPI_DOUBLE, voisin, 0, comm, &st);

Pour n = 100 (800 octets) : protocole eager, les deux Send rendent la main, tout fonctionne. Pour n = 10^6 (8 Mo) : protocole rendezvous, les deux rangs attendent que l'autre poste son Recv, interblocage définitif.

Le code passe tous les tests à petite taille et se bloque sur le cas de production. C'est le bug le plus coûteux de MPI.

Les trois remèdes, par ordre de qualité :

  1. MPI_Sendrecv, qui est conçu exactement pour ce motif ;
  2. MPI_Isend + MPI_Irecv + MPI_Waitall, qui permet en plus le recouvrement ;
  3. Alterner l'ordre selon la parité du rang — correct mais fragile.

Jamais MPI_Bsend pour contourner le problème : cela masque l'erreur de conception et consomme de la mémoire.

Le réglage d'une implémentation MPI

Chaque implémentation choisit dynamiquement un algorithme par collective, selon la taille du message et le nombre de rangs. Ces choix sont réglables, et le réglage par défaut n'est pas toujours le bon pour votre machine et votre code.

Sous OpenMPI, les paramètres se passent par --mca ou par variables d'environnement OMPI_MCA_*. Les familles utiles :

Famille Rôle
coll Choix des algorithmes de collectives (tuned, basic, hcoll, ucc)
btl / pml / mtl Couches de transport : mémoire partagée, TCP, InfiniBand via UCX
rmaps Placement des processus
btl_openib_*, osc_ucx_* Réglages fins du réseau rapide

Sous MPICH et ses dérivés (dont Intel MPI, Cray MPICH), les variables MPICH_* ou I_MPI_* jouent le même rôle.

Les cinq vérifications à faire sur un cluster inconnu

  1. MPI utilise-t-il le réseau rapide ? Si le réseau InfiniBand est mal configuré, MPI retombe silencieusement sur TCP et vous perdez un facteur dix. Vérifier avec les OSU Micro-Benchmarks : une latence supérieure à 10 microsecondes sur un réseau HPC signale un problème. Sous OpenMPI, --mca btl_base_verbose 100 indique la couche retenue.
  2. Le placement est-il correct ? --map-by, --bind-to, et vérification par --report-bindings. Deux rangs sur le même cœur, c'est un facteur deux perdu.
  3. Les collectives sont-elles réglées ? Comparer --mca coll_tuned_* avec les valeurs par défaut sur vos tailles de message réelles. Sur certaines machines, changer d'algorithme d'Allreduce donne 20 à 50 %.
  4. Le nombre de rangs par nœud est-il optimal ? Un rang par cœur, un rang par domaine NUMA avec OpenMP dedans, un rang par socket : trois configurations à mesurer. La réponse dépend du code.
  5. Les tampons sont-ils enregistrés ? Sur InfiniBand, les transferts RDMA nécessitent l'enregistrement des pages mémoire, opération coûteuse. Les implémentations maintiennent un cache d'enregistrement dont la taille est réglable. Un code qui alloue et libère ses tampons de communication à chaque itération paie ce coût en permanence.

Les topologies réseau

Fat-tree. Une arborescence à plusieurs niveaux de commutateurs, dimensionnée pour que la bande passante soit constante à chaque niveau (full bisection bandwidth) ou réduite d'un facteur connu (blocking ratio). Propriété : le nombre de sauts entre deux nœuds quelconques est borné et petit ; le coût de communication dépend peu du placement, ce qui simplifie la vie. Inconvénient : le nombre de commutateurs croît vite avec la taille.

Dragonfly. Des groupes de nœuds densément connectés à l'intérieur, et des liens globaux entre groupes. Propriété : beaucoup moins de câbles et de commutateurs à grande échelle. Inconvénient : le coût de communication dépend fortement de la position relative des rangs, et la congestion sur les liens globaux peut dégrader brutalement les performances. Référence : Kim, Dally, Scott, Abts, Technology-Driven, Highly-Scalable Dragonfly Topology (ISCA 2008).

Tore 3D, 4D, 5D ou 6D. Chaque nœud est connecté à ses voisins dans plusieurs dimensions. Excellent pour les communications entre voisins proches (stencils), mauvais pour les communications globales. Utilisé historiquement par plusieurs grandes machines.

Pourquoi c'est pratique. Le placement des rangs MPI sur la topologie change les performances d'un facteur significatif. Un code de stencil 3D placé de sorte que les voisins logiques soient voisins physiques va nettement plus vite. MPI fournit un mécanisme pour cela : MPI_Cart_create avec reorder = 1 autorise l'implémentation à renuméroter les rangs selon la topologie physique, et MPI_Dist_graph_create permet de déclarer un graphe de communication arbitraire. Peu de gens l'utilisent.

Les motifs de communication et leur coût

Motif Coût typique Où ça se voit
Point à point voisins \(O(1)\) sauts, constant Stencils, décomposition de domaine
Broadcast, Reduce \(O(\log p)\) Distribution de paramètres
Allreduce \(O(\log p)\) Produit scalaire à chaque itération d'un Krylov
Alltoall \(O(p)\) messages par rang Transposition, FFT distribuée, tri
Gather vers un rang \(O(p)\) sur un seul rang, goulot Écriture naïve de résultats

Le motif qui tue le passage à l'échelle est l'Allreduce. Son coût en \(\log p\) paraît bénin, mais il est synchronisant : tous les rangs attendent le plus lent. À dix mille rangs, avec un déséquilibre de charge de 1 %, chaque Allreduce coûte le pire cas. C'est la raison pour laquelle HPCG passe moins bien à l'échelle que HPL.

Les remèdes, dans l'ordre de puissance :

  1. Réduire le nombre d'Allreduce. Les méthodes de Krylov dites communication-avoiding (s-step) regroupent plusieurs itérations pour ne faire qu'une réduction. C'est un sujet de recherche actif et un vrai gain.
  2. Recouvrir. MPI_Iallreduce existe depuis MPI-3 : on lance la réduction et on fait autre chose pendant. Encore trop peu utilisé.
  3. Réduire le déséquilibre de charge, qui est la vraie cause du coût.

Les RMA, ou communications unidirectionnelles

Le modèle où un processus lit ou écrit directement dans la mémoire d'un autre, sans que celui-ci participe. Introduit dans MPI-2, considérablement amélioré dans MPI-3.

Pourquoi c'est important : les réseaux modernes font du RDMA en matériel. Le modèle RMA s'y applique naturellement, alors que le passage de messages classique impose une synchronisation qui n'a pas de contrepartie matérielle.

Les trois modes de synchronisation, du plus simple au plus fin :

  • Actif avec MPI_Win_fence : une barrière collective sur la fenêtre. Simple, peu performant.
  • Actif avec MPI_Win_post/start/complete/wait : synchronisation avec un groupe de voisins. Plus fin.
  • Passif avec MPI_Win_lock/unlock ou MPI_Win_lock_all : le processus cible ne participe pas du tout. C'est le mode qui exprime vraiment le RMA, et celui qui permet des structures partagées (compteurs, files de tâches).

Les opérations atomiques MPI_Fetch_and_op, MPI_Compare_and_swap et MPI_Accumulate permettent de construire des algorithmes avec vol de travail distribué.

La sémantique RMA est subtile

Les garanties d'ordre et de visibilité des RMA sont beaucoup plus faibles qu'on ne l'imagine. Deux MPI_Put vers la même adresse peuvent s'appliquer dans n'importe quel ordre. Une donnée écrite par MPI_Put n'est visible qu'après une synchronisation appropriée (MPI_Win_flush, MPI_Win_unlock…).

Lisez le chapitre correspondant de Using Advanced MPI avant d'écrire du RMA, et testez avec MUST. C'est la partie de MPI où l'on se trompe le plus facilement, avec des bugs non déterministes.

La programmation hybride MPI + OpenMP

Le modèle dominant en production : un rang MPI par domaine NUMA ou par socket, et des threads OpenMP à l'intérieur.

Pourquoi. Un rang MPI par cœur sur un nœud à 128 cœurs signifie 128 rangs par nœud, donc une explosion du nombre de messages dans les collectives et une duplication de toutes les structures de données répliquées. Avec 2 rangs par nœud et 64 threads chacun, on divise par 64 le nombre de messages.

Les quatre niveaux de support des threads dans MPI, à demander explicitement avec MPI_Init_thread :

Niveau Signification
MPI_THREAD_SINGLE Un seul thread dans le programme
MPI_THREAD_FUNNELED Plusieurs threads, mais seul le principal appelle MPI
MPI_THREAD_SERIALIZED Plusieurs threads peuvent appeler MPI, mais pas simultanément
MPI_THREAD_MULTIPLE Appels MPI concurrents autorisés

Attention : MPI_THREAD_MULTIPLE est souvent nettement plus lent, parce que l'implémentation doit ajouter des verrous internes. Et MPI_Init_thread renvoie le niveau réellement fourni, qui peut être inférieur à celui demandé. Vérifiez toujours la valeur retournée — beaucoup de codes ne le font pas et se comportent de manière incorrecte sur certaines installations.

Le modèle recommandé : MPI_THREAD_FUNNELED, avec les communications hors des régions parallèles, ou dans une région master/single. C'est plus simple et plus rapide.

Exercices

E1 · ★★ ⏱ 3 h — Trouver le seuil eager. Mesurer une courbe de ping-pong très finement autour de la région de transition, et localiser la discontinuité. Retrouver la valeur dans la documentation de votre implémentation, puis la modifier par variable d'environnement et vérifier que la discontinuité se déplace.

E2 · ★★★ ⏱ 4 h — Le réglage des collectives. Mesurer MPI_Allreduce sur plusieurs tailles et plusieurs nombres de rangs, avec trois algorithmes forcés différents. Tracer une carte des régions où chaque algorithme gagne. Comparer au choix automatique de l'implémentation : il est généralement bon, mais pas toujours.

E3 · ★★★ ⏱ 4 h — Le placement et la topologie. Sur un stencil 3D à 64 rangs, comparer trois placements : par défaut, MPI_Cart_create avec reorder = 1, et un placement explicite par la carte de rangs (--rankfile sous OpenMPI). Mesurer. Puis, si le cluster a plus de nœuds que nécessaire, comparer une allocation sur des nœuds contigus et sur des nœuds dispersés.

E4 · ★★★ ⏱ 3 h — Hybride, la bonne granularité. Un même code sur un nœud à \(N\) cœurs, avec toutes les combinaisons \(r \times t = N\) de rangs et de threads : \(N \times 1\), \(N/2 \times 2\), …, \(1 \times N\). Tracer le temps en fonction du nombre de rangs. La courbe a généralement un minimum autour d'un rang par domaine NUMA. Vérifier que le placement suit (--map-by numa ou équivalent).

E5 · ★★★★ ⏱ 6 h — Une file de tâches distribuée en RMA. Implémenter un vol de travail entre rangs avec MPI_Win_lock_all et MPI_Fetch_and_op sur un compteur global, appliqué à un problème irrégulier (par exemple un ensemble de tâches de durées très variables). Comparer à un schéma maître-esclave par messages. Mesurer le déséquilibre de charge résiduel dans les deux cas.

E6 · ★★★ ⏱ 3 h — MUST sur un code cassé. Écrire délibérément cinq erreurs MPI — types incohérents entre envoi et réception, communicateur mal apparié, collective appelée par un sous-ensemble de rangs, tampon modifié avant le Wait, fuite de requête — et vérifier que MUST les détecte toutes. Puis lancer MUST sur un vrai code : vous trouverez probablement quelque chose.

Ressources

Priorité 1 :

  • Gropp, Hoefler, Thakur, Lusk, Using Advanced MPI, MIT Press, 2014. Le volume qui couvre RMA, MPI-IO, topologies, types dérivés avancés, interopérabilité avec les threads. C'est le livre de ce chapitre.
  • Les supports MPI de l'IDRIS (idris.fr/formations/mpi/), en français, y compris le cours de programmation hybride MPI/OpenMP.
  • La documentation d'OpenMPI et de MPICH sur le réglage, et les pages de manuel ompi_info, mpirun.

Priorité 2 :

  • Les OSU Micro-Benchmarks : la suite de référence.
  • MUST (itc.rwth-aachen.de/must/) : vérificateur d'exécution MPI.
  • Score-P / Vampir, Extrae / Paraver, mpiP pour le profilage MPI. Voir Outils de mesure.
  • UCX (openucx.org) : la couche de communication sous-jacente à la plupart des implémentations MPI modernes sur InfiniBand. Sa documentation explique ce qui se passe réellement.

Priorité 3 :

  • Le standard MPI sur mpi-forum.org. Les chapitres sur les RMA et sur MPI-IO sont les plus utiles à lire directement.
  • Kim, Dally, Scott, Abts, « Technology-Driven, Highly-Scalable Dragonfly Topology », ISCA 2008.
  • Sur les méthodes communication-avoiding : les travaux de Demmel, Ballard, Hoemmen et Grigori, et le cours CS267 de Berkeley.
  • Sur les modèles alternatifs : GASPI/GPI, OpenSHMEM, et les modèles à espace d'adressage global partitionné (PGAS). Culture utile : MPI n'est pas le seul modèle, même s'il est de loin le plus répandu.

À retenir

Les cinq choses à savoir au-delà du cours

  1. Les deux protocoles, eager et rendezvous : ils expliquent les interblocages qui n'apparaissent qu'à grande taille. Utilisez MPI_Sendrecv ou les non bloquantes.
  2. Vérifiez toujours que MPI utilise le réseau rapide. Une latence supérieure à dix microsecondes signale un problème de configuration, et un facteur dix perdu.
  3. L'Allreduce est le motif qui limite le passage à l'échelle, parce qu'il synchronise. Réduire leur nombre ou les recouvrir est le levier principal à grande échelle.
  4. Le placement compte : un rang par domaine NUMA avec OpenMP dedans est souvent le bon point de fonctionnement, et il faut vérifier les liaisons.
  5. MPI_THREAD_MULTIPLE est coûteux, et le niveau retourné par MPI_Init_thread peut être inférieur au niveau demandé. Vérifiez-le.

Chapitre suivant : OpenMP et threads.