Utilisateur:Kabyhaswell/ProgrammationParallèleMPI/Modèle d'exécution
Dans ce chapitre, nous allons présenter le modèle d'exécution de MPI.
Modèle de système distribué
modifierUnités de calcul et mémoire distribuée
modifierAvec MPI, chaque unité de calcul est un processus. Chaque processus possède son propre espace d'adressage, accessible uniquement par lui (ce fait est assoupli avec les communications unilatérales). Ainsi, lorsqu'un processus a besoin d'accéder à des données localisées dans l'espace mémoire d'un autre processus, le programmeur doit émettre une communication explicite entre les processus pour déplacer les données.
Communications inter-processus
modifierIl existe deux types de communications entre les processus :
- les communications point-à-point, entre deux processus
- les communications collectives, impliquant un ensemble de processsus
Communications point-à-point
modifierLes communications point-à-point sont effectuées entre deux processus. Dans le modèle de base, le processus source appel une primitive de type send et le processus destination appelle une primitive de type receive. Chacun dispose d'un buffer (tampon), où le processus source prend les données qu'il envoie et le processus destination dépose les données qu'il reçoit.
Il est très important qu'à chaque send corresponde un receive (et inversement). Dans le cas contraire, des données excédentaires peuvent être mises dans un tampon de réception et être utilisées à la réception suivante, ce qui est source de bug très difficile à diagnostiquer (car il provoque une erreur numérique dans le calcul), ou un processus peut rester bloqué en attente de données qui n’arriveront pas (ou dans la communication suivante).
La norme MPI propose principalement deux sortes de communications point-à-point : bloquantes ou non-bloquantes. Les communications bloquantes retournent une fois que le processus a effectué la communication (émission sur le réseau ou réception des données). Les communications non-bloquantes sont émises et retournent immédiatement. Le calcul peut alors reprendre son cours pendant que le système fait avancer les communications en arrière-plan. Des fonctions d'attente (ou de test) permettent de s'assurer que la communication est terminée (ou de tester si c'est le cas), lorsque, par exemple, les données doivent être utilisées.
Communications collectives
modifierLes communications collectives sont effectuées par un ensemble de processus. Il est important que tous les processus concernés appellent la fonction de communication. Si ils n'ont pas tous le même rôle (par exemple, si certains envoient des données et si d'autres en reçoivent), la distinction sera faite dans la fonction elle-même. Le programmeur doit simplement appeler la fonction depuis tous les processus.
Les communications collectives ont longtemps été uniquement bloquantes. Un processus sort de la fonction d'appel à la communication collective lorsque sa participation à celle-ci est terminée.
Il est très important de distinguer le moment où un processus sort de l'appel et celui où la fonction a terminé : en effet, seules quelques communications collectives ont un effet de synchronisation entre les processus. Ainsi, le fait qu'un processus ait terminé sa participation à la communication collective ne veut pas nécessairement dire que celle-ci est terminée. Il est parfois même possible que certains processus n'y soient pas encore entrés.
Depuis la norme MPI 3, il existe des communications collectives non bloquantes, à l'image des communications point-à-point non bloquantes.
Notion de communicateur
modifierDes processus pouvant communiquer ensemble sont situés dans un même communicateur. Le programmeur peut créer des nouveaux communicateurs lors de l’exécution, pour rassembler des processus communiquant entre eux. En particulier, si seuls certains processus doivent participer à une communication collective, il peut être intéressant de les rassembler dans un communicateur spécifique, pour ne pas faire participer tous les processus à cette communication collective.
Outre les communicateurs créés par le programmeur, il existe trois communicateurs qui sont toujours créés par le système au lancement d'un programme MPI :
MPI_COMM_WORLD
, qui rassemble tous les processus ;MPI_COMM_SELF
, dont chaque processus a une instance dans laquelle il est seul ;MPI_COMM_NULL
, qui ne contient aucun processus et qui est utilisé pour tester des valeurs de retour de certaines fonctions.
Nommage des processus
modifierLes processus sont distinguables les uns des autres par un système de nommage par rang. C'est ce rang qui est utilisé pour les communications. Il est unique sur un communicateur : ainsi, il est indissociable du communicateur auquel il est associé.
Par exemple, si on a une application composée de N processus, les processus ont un rang entre 0 et N-1 dans MPI_COMM_WORLD, mais ils ont tous le rang 0 dans MPI_COMM_SELF. Si on crée des communicateurs contenant, pour l'un les processus de rang pair, et pour l'autre les processus de rang impair, les processus qui ont le rang 0 et 1 dans MPI_COMM_WORLD auront tous les deux le rang 0 dans ces deux communicateurs, ceux de rang 2 et 3 dans MPI_COMM_WORLD auront le rang 1, et ainsi de suite (si l’ordre des processus est conservé à la création de ces sous-communicateurs).
Le support exécutif
modifierLes programmes parallèles programmés avec MPI ont vocation à s'exécuter sur des ressources de calcul distribuées. Ils peuvent s'exécuter sur un seul nœud composé de plusieurs cœurs : ils s'exécuteront alors de la même façon que si les processus étaient situés sur des nœuds distincts.
Pour pouvoir lancer et contrôler des processus sur des nœuds distants, on a besoin d'un middleware (intergiciel) : le support exécutif. Celui-ci est composé de deux parties :
- l'environnement d'exécution, qui orchestre les processus distants sur les ressources de calcul disponible.
- la bibliothèque de communications, qui effectue les communications, rendues possibles par l'environnement d'exécution.
L'environnement d'exécution
modifierDans le cas d'une application mono-processus, s'exécutant sur une machine, le lancement de cette application est simple : l'environnement d'exécution lance un nouveau processus et exécute la commande demandée. Lorsque cette application a besoin d'accéder à des ressources, elle s'adresse au système d'exploitation, qui a la charge d'orchestrer l'utilisation des ressources. Dans le cas d'une application parallèle s'exécutant sur plusieurs machines, il n'y a pas une instance de système d'exécution mais plusieurs, potentiellement pas le même sur tous les nœuds de calcul… Pour cela, on utilise un middleware qui a la charge de cette orchestration : l'environnement d'exécution.
Concrètement, l’environnement d'exécution :
- lance les processus sur les ressources distantes, éventuellement en interaction avec un système de lancement, depuis une liste des ressources disponibles
- surveille ces processus pendant leur exécution, ce qui peut se résumer à détecter les pannes, et, le cas échéant, commande le comportement à adopter (par défaut, en cas de panne d'un processus, l'application est arrêtée)
- transmet les entrées-sorties et les signaux : si un processus effectue un affichage, celui-ci est transmis jusqu'à l'utilisateur par l'environnement d'exécution
- permet les communications entre les processus (effectuées par la bibliothèque de communications) et, plus généralement, la portabilité.
Par exemple, le nommage des processus est un élément de portabilité. Sans ce système de nommage (rang, communicateur), les processus pourraient être identifiés de manière unique, par exemple, par l'adresse IP de la machine sur laquelle ils sont exécutés et leur PID. Mais, d'une exécution à l'autre, ils n'auraient pas le même PID. et en changeant de machines, les machines utilisées n’auraient pas la même adresse IP. De même, pour communiquer entre deux processus, ils pourraient utiliser l'adresse utilisée par le réseau utilisé, par exemple dans le cas de TCP/IP, l'adresse IP et le port utilisés par le processus distant. Cependant, d'une exécution à une autre les processus n'utiliseraient pas les mêmes ports, et, en changeant de machines, ils n’auraient pas la même adresse IP voire n'utiliseraient pas le même type de réseau.
L'environnement d'exécution attribue aux processus des numéros uniques dans le communicateur associé, et c'est cette paire (rang, communicateur) qui est utilisée pour désigner un processus distant pour communiquer avec lui. Ainsi, d'une exécution à une autre, ce nommage reste identique. De plus, lorsque le programmeur appelle des fonctions de communication sur le réseau, c'est l'environnement d'exécution qui choisit quel type de réseau va être utilisé. Par exemple, l'environnement d'exécution sait si les deux processus peuvent communiquer en InfiniBand, ou en TCP, ou sur mémoire partagée.
C'est l'environnement d'exécution qui est utilisé pour lancer et suivre l’exécution d'une application parallèle.
La bibliothèque de communications
modifierLa bibliothèque de communications fournit un ensemble de fonctions de haut niveau qui permettent les communications. Dans le cas de MPI, c'est une implémentation de l'interface normalisée, qui suit la sémantique définie par la norme.
Elle assure la portabilité des communications. Par exemple, si, pour une exécution d'un programme parallèle, les processus peuvent communiquer sur réseau InfiniBand et, l'exécution suivante étant effectuée sur d'autres ressources, ils doivent communiquer sur réseau TCP/IP, l'application n’a pas à être modifiée : l'implémentation de la bibliothèque de communication permet d'utiliser l’un ou l'autre selon ce qui est disponible.
Elle interagit avec l'environnement d'exécution, pour utiliser ce système de nommage des processus et pour pouvoir mettre en place ces communications. Par exemple, c'est l’environnement d'exécution qui lui permet de connaitre quelles sont les informations de contact d'un processus distant (type de réseau disponible, adressage). C'est une fois ces informations obtenues que la bibliothèque de communications peut transmettre des données entre les processus.
Un premier programme MPI
modifierCopiez le code suivant dans un fichier texte :
#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>
int main( int argc, char** argv ) {
int rank, size;
/* Initialisation (obligatoire) */
MPI_Init( &argc, &argv );
/* On détermine qui on est et combien il y a de processus, et on l’affiche) */
MPI_Comm_size( MPI_COMM_WORLD, &size );
MPI_Comm_rank( MPI_COMM_WORLD, &rank );
printf( "Bonjour, je suis le processus %d/%d\n", rank, size );
/* Finalisation (obligatoire) */
MPI_Finalize();
return EXIT_SUCCESS;
}
Compilation
modifierPour compiler, on utilise le programme mpicc
. On peut le voir comme un wrapper autour du compilateur local, même si dans l'absolu, il peut faire davantage de choses. A minima, il permet d'aller chercher les fichiers d'en-têtes mpi.h
et la bonne bibliothèque MPI (qui peut être libmpi
, libmpich
ou autre selon l’implémentation utilisée) et de faire l’édition des liens avec la bibliothèque MPI. Il peut faire d'autres choses, mais c'est principalement une surcouche qui ensuite appelle le compilateur local pour la compilation elle-même.
Par conséquent, il s'utilise comme le compilateur local. Par exemple :
mpicc -Wall -o exemple1 exemple1.c
On pourra également passer des options d'optimisation, utiliser des compilations séparées dans le cas de fichiers multiples… exactement comme avec le compilateur local.
Exécution
modifierLe lancement de l’exécution se fait avec le point d'entrée de l’environnement d'exécution : mpiexec
. On doit lui passer, a minima, le nombre de processus à lancer avec l'option -n
et le chemin vers l'exécutable à lancer. Si on veut s'exécuter sur plusieurs machines, on passe un fichier appelé machinefile qui contient la liste des machines à utiliser.
Pour exécuter le programme que nous venons de compiler avec 4 processus, on peut donc appeler :
moi@thrall:$ mpiexec -n 4 ./exemple1 Bonjour, je suis le processus 2/4 Bonjour, je suis le processus 0/4 Bonjour, je suis le processus 1/4 Bonjour, je suis le processus 3/4
On constate que les affichages, effectués dans le code avec printf
, sont faits par le programme mpiexec
: c'est en cela que l'environnement d'exécution redirige les entrées-sorties faites par les processus. On constate également qu'ils sont faits dans un ordre ne correspondant pas à l'ordre des rangs des processus. Chaque processus envoie son affichage au mpiexec
, il n'y a pas d'ordre particulier, il n'y a donc pas de raison pour que les affichages se fassent dans un ordre ou dans un autre.
À noter que l'environnement d'exécution permet de lancer n’importe quelle application, pas nécessairement un programme MPI. Par exemple, on peut lancer une commande :
moi@thrall:$ mpiexec -n 4 date mercredi 15 août 2018, 22:12:15 (UTC+0200) mercredi 15 août 2018, 22:12:15 (UTC+0200) mercredi 15 août 2018, 22:12:15 (UTC+0200) mercredi 15 août 2018, 22:12:15 (UTC+0200)
Dans le cas de l’utilisation d'un machinefile, celui-ci est passé avec l'option --machinefile <fichier>
. Sa syntaxe est très simple : on liste simplement les machines à utiliser. Par exemple, pour utiliser 3 machines appelées vampyr01
, vampyr02
et vampyr03
:
vampyr01 vampyr02 vampyr03
Si ces machines disposent de plusieurs cœurs, ceux-ci vont être utilisés soit en remplissant d'abord la première machine puis la deuxième puis la troisième, soit en utilisant un round-robin : le premier processus sur la première machine, le deuxième processus sur la deuxième machine, le troisième processus sur la troisième machine, le quatrième processus sur la première machine, le cinquième processus sur la deuxième machine, et ainsi de suite. La première façon est l'option par défaut, la deuxième s'active avec l'option --bynode
.
On peut préciser dans le machinefile le nombre de processus à lancer sur une machine et le nombre maximum à lancer sur cette machine avec, respectivement, les options slots=<N>
et
maxslots=<N>
. Par exemple :
vampyr01 slots=2 max-slots=4 vampyr02 slots=2 max-slots=4 vampyr03 slots=2 max-slots=4
Dans ce cas, on commencera par mettre 2 processus sur chaque machine et, si c'est insuffisant, on en mettra davantage mais sans dépasser 4. Si l'utilisateur a demandé, avec l'option -p
, plus de processus que le machinefile ne permet d'en lancer avec la somme des options max-slots
, on a alors une situation d'oversubscription : l'application demande plus de ressources qu'il n(y en a de disponibles. Selon si l'oversubscription est autorisée ou non, l'environnement passera outre la limite ou l'application ne sera pas lancée et mpiexec
affichera un message d'erreur plus ou moins explicite.
Python
modifierEn Python, le même exercice peut se réaliser en sauvegardant le code suivant dans un fichier cours-01.py
:
#!/usr/bin/env python3
from mpi4py import MPI
comm = MPI.COMM_WORLD
rank = comm.Get_rank()
size = comm.Get_size()
print('Processus',rank+1,'/',size)
On exécute alors le programme par la commande mpiexec -n 2 python3 ./cours-01.py