MPI et programmation par passage de messages Matthieu EXBRAYAT Maîtrise d’informatique
Modèles mémoire Shared memory Shared disk Shared nothing Remarque Mémoire partagée, disques partagés Implantations « haut de gamme » Croissance limitée Shared disk Mémoire répartie, Implantation physique assez rare (ou ferme de disques) Shared nothing Mémoire et disques répartis Cluster de PC, implantation courante Remarque implantation physique vs implantation logique
Mémoire Répartie (shared nothing) Fonctionnement parallèle = envoi de messages Envoi / Réception Permet la synchronisation Avantages Portabilité, extensibilité Inconvénients Gestion explicite des appels distants = programmation à 2 niveaux
MPI : Message Passing Interface MPI = spécification standard (consortium) Plusieurs implantations Gratuites Portables Intérêt Cadre « simple » pour la programmation par envoi de messages Sémantique « maîtrisée » Portabilité et stabilité (protocole bas niveau pas géré par développeur) Permet architecture hétérogène
MPI Contient Constantes, types et fonctions pour C, C++, Fortran (77 et 90) Primitives de comm. point à point Primitives de comm. collectives Regroupement de processus Interface de suivi des performances
MPI ne contient pas Opérations en mémoire partagée EDI (programmation / débogage) Il existe des extensions Gestion de processus Lancement explicite au départ RPC Gestion des threads E/S Parallèles (disponible dans MPI 2)
Principe d’un programme MPI On écrit un programme contenant les opérations locales + les échanges via MPI (initialisation et échanges) On compile On lance par mpirun –np n monprog n=nombre de machines La liste des machines peut être définie explicitement Mpi crée n copies de l’exécutable sur les machines cibles Exécution asynchrone des processus (+synchro sur échange messages) Terminaison des processus
Différents types d’appel Local (manipulation locale d’objets MPI) Non-local : échange avec autre(s) processus Bloquant : le processus reprend la main après l’opération (ressources disponibles) Non-bloquant : le processus reprend la main avant la fin de l’opération (nécessite contrôle) Point-à-point : Echange entre deux processus Collectif : Tous les processus du groupe appellent la même fonction NB : nous utiliserons le groupe par défaut, qui regroupe tous les processus
MPI et C Les fonctions commencent par MPI_ Elles retournent un code d’erreur (qui vaut MPI_SUCCESS en cas de réussite) Les arguments de type tableau sont indexés à partir de 0 (C classique…)
Connection et déconnection à MPI MPI_Init(&argc,&argv); MPI_Comm_size(MPI_COMM_WORLD, &num_procs); MPI_Comm_rank(MPI_COMM_WORLD, &myid); MPI_Get_processor_name(processor_name, &namelen); MPI_Finalize();
MPI_Send : envoi bloquant int MPI_Send(void *buf,int count, MPI_Datatype datatype,int dest, int tag, MPI_Comm comm) buf : adresse mémoire (début des informations à transmettre) count : nombre de valeurs à envoyer datatype : type de valeur dest : numéro du processus destinataire tag : numéro associé au message comm : communicateur = groupe de processus. Nous utilisons le groupe global : MPI_COMM_WORLD
Des détails… Avant l’appel à MPI_Send, les infos à envoyer sont placées dans une zone de mémoire contigüe (tableau de taille count) Si count = 0 : pas de données (utilisable pour synchro) Les types de données : MPI_CHAR signed char MPI_SHORT signed short int MPI_INT signed int MPI_LONG signed long int MPI_FLOAT float MPI_DOUBLE double MPI_PACKED Comment envoyer un seul entier ?
MPI_Receive : réception bloquante int MPI_Recv (void* buf,int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status) buf : emplacement où stocker les valeurs reçues count : capacité de buf (erreur si débordement, placement au début si remplissage incomplet) source : machine émettrice (numéro ou MPI_ANY_SOURCE) tag : numéro ou MPI_ANY_TAG status : infos sur la réception (structure) : status.MPI_SOURCE status.MPI_TAG status.MPI_ERROR int MPI_Get_count(MPI_Status *status,MPI_DATATYPE datatype, int *count))
Bloquant ? Une opération est bloquante dans le sens où elle ne termine qu’après avoir terminé l’utilisation du buffer : send se termine après l’envoi effectif (!= réception) receive se termine après la réception effective un send peut-il être lancé avant le receive correspondant ?
Ordre d’acheminement MPI garanti l’ordre d’acheminement entre un émetteur et un destinataire donnés. L’ordre global n’est par contre pas garanti (plusieurs émetteurs vers un même destinataire, par exemple).
Envoi et réception combinés Si deux processus souhaitent échanger des informations, il est possible d’appeler MPI_Sendrecv int MPI_Sendrecv (void* sendbuf, int sendcount, MPI_Datatype sendtype, int dest, int sendtag, void* recvbuf, int recvcount, MPI_Datatype recvtype, int source, int recvtag, MPI_Comm comm, MPI_Status *status) Procède comme deux threads qui géreraient un envoi et une réception envoi et réception sont bloquants Les buffers sont nécessairement différents (disjoints) Les tags peuvent être différents.
Processus NULL On peut faire des envoi vers MPI_PROC_NULL (si destinataire inactif pour une raison donnée) Se termine dès que possible. Aucun effet Utilisable aussi en réception.
Envoi et réception non bloquants Le processus reprend la main dès que la demande d’envoi ou de réception est faite. Le processus contrôle ensuite si l’envoi ou la réception a été effectué Permet des recouvrement calcul- communication (particulièrement intéressant pour réception)
Syntaxe int MPI_ISend(void *buf,int count, MPI_Datatype datatype,int dest, int tag, MPI_Comm comm, MPI_Request *request) int MPI_IRecv (void* buf,int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Request *request) int MPI_Wait(MPI_Request * request, MPI_Status * status) int MPI_Test(MPI_Request * request, int *flag, MPI_Status * status)
Opérations collectives MPI définit certains algorithmes parallèles de base : diffusion, réduction… Définition cohérente avec send et receive, mais : correspondance entre message et buffer de réception (taille) opérations bloquantes pas de tag.
Barrière de synchronisation Permet de s’assurer d’un point de synchro (car pas de certitudes à partir des send et receive, même bloquants). int MPI_Barrier(MPI_Comm comm) Opération globale au groupe de processus
Broadcast int MPI_Bcast(void * buf, int count, MPI_Datatype datatype, int root, MPI_Comm comm) Envoi depuis le processus de rang root vers tous les autres
Gather Un processus collecte des informations venant de tous les processus et les assemble (dans un tableau) int MPI_Gather(void *sendbuf,int sendcount, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm) Tous les processus (root y-compris) envoient des infos vers root. Le placement se fait dans l’ordre des rangs de processus. en général, recvcount = sendcount
Scatter Scatter : un processus distribue des informations (stockées dans un tableau) vers tous les processus int MPI_Scatter(void *sendbuf,int sendcount, MPI_Datatype sendtype, void *recvbuf, int recvcount, MPI_Datatype recvtype, int root, MPI_Comm comm) Tous les processus (root y-compris) reçoivent des infos de root. La distribution se fait dans l’ordre des rangs de processus. en général, recvcount = sendcount
Reduce On dispose d’un certain nombre de calculs de préfixes pré-implantés int MPI_Reduce(void* sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, int root, MPI_Comm comm) MPI_MAX, MPI_MIN, MPI_SUM, MPI_PROD MPI_LAND, MPI_LOR, MPI_LXOR MPI_BAND, MPI_BOR, MPI_BXOR MPI_MAXLOC, MPI_MINLOC (paire valeur/index)
Scan On dispose d’un certain nombre de calculs de préfixes pré-implantés int MPI_Scan(void* sendbuf, void *recvbuf, int count, MPI_Datatype datatype, MPI_Op op, MPI_Comm comm) Mêmes opérateurs que pour Reduce