Utilisateur:Kabyhaswell/ProgrammationParallèleMPI/Communiquer entre processus : point-à-point

Une page de Wikiversité, la communauté pédagogique libre.
Début de la boite de navigation du chapitre
Communiquer entre processus : point-à-point
Icône de la faculté
Chapitre no 2
Leçon : ProgrammationParallèleMPI
Chap. préc. :Modèle_d'exécution
Chap. suiv. :Communications_collectives

Exercices :

Communications de base
fin de la boite de navigation du chapitre
En raison de limitations techniques, la typographie souhaitable du titre, « ProgrammationParallèleMPI : Communiquer entre processus : point-à-point
Kabyhaswell/ProgrammationParallèleMPI/Communiquer entre processus : point-à-point
 », n'a pu être restituée correctement ci-dessus.

Dans ce chapitre, nous allons voir les fonctions de base des programmes MPI et les premières fonctions de communications inter-processus.

Le processus MPI[modifier | modifier le wikicode]

Nous avons vu au chapitre précédent que le support exécutif de MPI fournit un ensemble de fonctions de communication de haut niveau, apportant une abstraction sur les types de réseaux matériels disponibles, et un système de nommage des processus. Pour pouvoir le faire, les programmes MPI doivent suivre une certaine structure.

Les fonctions MPI[modifier | modifier le wikicode]

Les fonctions MPI ont des noms qui suivent la convention suivante :

  • MPI_ (MPI puis _) en majuscules
  • La première lettre suivante en majuscule
  • La suite en minucules

Par exemple, la fonction d'envoi bloquant est MPI_Send ; la fonction d'envoi non-bloquant est MPI_Isend.

Les données des communications (données envoyées et reçues) doivent être situées dans des buffers où elles sont sérialisées. On passe à la fonction MPI un pointeur vers le début du buffer d'envoi ou de réception, le nombre d'éléments à envoyer ou recevoir, et le type de données. Typiquement, pour envoyer N entiers, il faut les mettre dans un tableau d'entier et envoyer N entiers.

Types de données[modifier | modifier le wikicode]

Les types de données utilisés peuvent être des types prédéfinis ou dérivés (définis par le programmeur). Les types de base sont les suivants :

Type MPI Type C Type C++
MPI_CHAR char char
MPI_SHORT signed short signed short
MPI_INT signed int signed int
MPI_LONG signed long signed long
MPI_LONG_LONG signed long long signed long long
MPI_UNSIGNED_CHAR unsigned char unsigned char
MPI_UNSIGNED_SHORT unsigned short unsigned short
MPI_UNSIGNED unsigned int unsigned int
MPI_UNSIGNED_LONG unsigned long unsigned long
MPI_UNSIGNED_LONG_LONG unsigned long long unsigned long long
MPI_FLOAT float float
MPI_DOUBLE double double
MPI_LONG_DOUBLE long double long double
MPI_BOOL bool
MPI_COMPLEX Complex<float>
MPI_DOUBLE_COMPLEX Complex<double>
MPI_LONG_DOUBLE_COMPLEX Complex<long double>
MPI_WCHAR wchar_t wchar_t
MPI_BYTE
MPI_PACKED

Initialisation et finalisation[modifier | modifier le wikicode]

Nous avons vu au chapitre précédent que l'environnement d'exécution a pour rôle, entre autres, de permettre à la bibliothèque MPI de communiquer entre les processus. Pour cela, celle-ci doit être initialisée. Notamment, chaque processus doit découvrir de quels moyens de communications il dispose (quelles cartes réseau sont disponibles), de quelles adresses (IP, port, adressage spécifique…) il dispose sur ces cartes réseau…

Cette initialisation se fait avec la fonction MPI_Init. La fonction prend en paramètres un pointeur vers le nombre d'arguments passés à la ligne de commande qui a lancé le processus, et un pointeur vers le tableau de paramètres. Ces pointeurs permettent au processus de récupérer ses paramètres de ligne de commande sans décalage lié au lancement par l'environnement d'exécution.

Concrètement, imaginons un programme qui s'appelle programme et prenne trois arguments. Si on appelle

   ./programme A B C

dans le code de programme, on accède à A comme argv[1], à B comme argv[2] et à C comme argv[3]. Maintenant, si programme est un programme parallèle, on l'appelle comme suit :

   mpiexec --machinefile machinefile -n 64 <autres options> ./programme A B C

Ainsi, le tableau des arguments de la ligne de commande contient tous les arguments passés à mpiexec, et programme, A, B et C ne sont pas du tout au début de ce tableau. La fonction MPI_Init fait donc un décalage du pointeur vers le début du tableau d'arguments afin que programme accède bien à son premier argument comme argv[1].

Par ailleurs, la bibliothèque doit être finalisée, notamment pour que l'environnement d'exécution sache que la fin de l'exécution d'un processus est une fin normale et pas une défaillance. Cette finalisation se fait avec la fonction MPI_Finalize.

Il est extrêmement important de ne pas oublier que tout programme MPI doit commencer par MPI_Init et terminer par MPI_Finalize. Aucune fonction MPI ne peut être appelée avant MPI_Init ni après MPI_Finalize. Le programme peut faire d'autres choses, mais pas en utilisant les fonctions MPI.

Un programme minimal MPI est alors :

#include <stdlib.h>
#include <mpi.h>

int main( int argc, char** argv ) {
  /* Initialisation (obligatoire) */
  MPI_Init( &argc, &argv );
  /* Finalisation (obligatoire) */
  MPI_Finalize();
  return EXIT_SUCCESS;
}

Ce programme est compilé avec

    mpi@thrall:$ mpicc -Wall -o exemple2.1 exemple2.1.c 

Il ne fait pas grand chose : il initialise et finalise simplement la bibliothèque MPI. Il ne fait même pas d'affichage. Il s'exécute avec

    moi@thrall:$ mpiexec -n 4 ./exemple2.1

Et en Python[modifier | modifier le wikicode]

Il existe des bindings non-officiels pour Python dans le module mpi4py.

Les communicateurs de base sont :

  • MPI.COMM_WORLD
  • MPI.COMM_SELF
  • MPI.COMM_NULL

Et les fonctions MPI se trouvent dans le module MPI.

La particularité de Python est qu'il n'y a pas de fonctions d'initialisation et de finalisation à appeler explicitement. Elles sont appelées par le module lui-même dans le constructeur et le destructeur.

Un programme équivalent au programme ci-avant est donc le suivant.

#!/usr/bin/env python
from mpi4py import MPI

def main():
	return

if __name__ == "__main__":
	main()

Si le script est exécutable, on le lance comme suit :

   moi@thrall:$ mpiexec -n 4 ./exemple2.1.py 

Sinon, on lance l’interpréteur avec mpiexec :

   moi@thrall:$ mpiexec -n 4 python ./exemple2.1.py 

Savoir qui on est[modifier | modifier le wikicode]

Nous avons vu que les processus sont identifiables de façon unique dans un communicateur par son rang dans ce communicateur. On peut également obtenir la taille du communicateur.

Ce rang n’est unique que dans un communicateur : deux processus différents peuvent avoir le même rang dans deux communicateurs différents. Ainsi, désigner un processus par son rang n’a de sens que si ce rang est associé au communicateur correspondant.

On dispose de fonctions pour obtenir

  • son rang dans un communicateur : MPI_Comm_rank
  • la taille (le nombre de processus) de ce communicateur : MPI_Comm_size
#include <stdio.h>
#include <stdlib.h>
#include <mpi.h>

int main( int argc, char** argv ) {
  int rank, size;
  MPI_Init( &argc, &argv );
  /* On obtient son rang dans MPI_COMM_WORLD et la taille du communicateur */
  MPI_Comm_size( MPI_COMM_WORLD, &size );
  MPI_Comm_rank( MPI_COMM_WORLD, &rank );
  printf( "Je suis le processus %d sur %d\n", rank, size );
  MPI_Finalize();
  return EXIT_SUCCESS;
}

L'équivalent en Python est le programme suivant.

#!/usr/bin/env python
from mpi4py import MPI

def main():
    comm = MPI.COMM_WORLD
    rank = comm.Get_rank()
    size = comm.Get_size()
    print "Je suis le processus", rank, "sur", size

if __name__ == "__main__":
	main()

Communications bloquantes[modifier | modifier le wikicode]

Les fonctions de communications bloquantes de base sont MPI_Send et MPI_Recv. Elles servent à communiquer des données entre deux processus. Ces données doivent être sérialisées dans un tableau.

Il est absolument impératif qu'à chaque envoi corresponde une réception. Si un processus A envoie X octets à un processus B, B doit recevoir X octets du processus A.

Elles retournent une fois qu'elles ont terminé ce qu'elles ont à faire. Dans le cas de MPI_Recv, cela signifie que les données sont bien dans le buffer de réception. Dans le cas de MPI_Send, cela signifie que les données sont prises en charge par la bibliothèque, pas forcément qu'elles sont arrivées au destinataire (nous en reparlerons plus tard).

Le tag[modifier | modifier le wikicode]

Les communications sont identifiées par un paramètre : le tag. Ce paramètre sert à différencier les messages envoyés entre les processus, à des fins de signalisation notamment : on pourra traiter différemment des données selon le type de communication, par exemple.

Premier exemple[modifier | modifier le wikicode]

Dans l'exemple suivant, le processus 0 récupère une information (son PID), l'affiche et l’envoie au processus 1. Le processus 1 reçoit un entier et l’affiche. Les autres processus ne font rien.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <mpi.h>

#define TAG 1664

int main( int argc, char** argv ) {
  int rank, pid;
  MPI_Status status;
  MPI_Init( &argc, &argv );
  MPI_Comm_rank( MPI_COMM_WORLD, &rank );
  if( 0 == rank ) { /* Le processus 0 récupère son PID et l’envoie au processus 1 */
    pid = (int) getpid();
    printf( "%d: j\'envoie %d\n", rank, pid );
    MPI_Send( &pid, 1, MPI_INT, 1, TAG, MPI_COMM_WORLD );
  } else {
    if( 1 == rank ) { /* Le processus 1 reçoit un entier et l’affiche */
      MPI_Recv( &pid, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD, &status );
      printf( "%d: J\'ai reçu %d\n", rank, pid );
    } 
  } 
  MPI_Finalize();
  return EXIT_SUCCESS;
}

Recevoir de n’importe qui[modifier | modifier le wikicode]

Il est évident que, pour envoyer des données, il faut savoir à qui les envoyer. La fonction d'envoi dont donc recevoir comme paramètre le rang du processus destinataire et le communicateur sur lequel s'effectue la communication. Cependant, en réception, on peut vouloir recevoir de n’importe quel processus. C'est utile par exemple si plusieurs processus envoient des données à un seul processus : ainsi, il va recevoir de tous les processus dans l'ordre dans lequel les communications arrivent.

Pour cela, on utilise un rang spécial : MPI_ANY_SOURCE.

De façon similaire, on peut recevoir avec n'importe quel tag, en utilisant le tag spécial : MPI_ANY_TAG.

Dans le dernier argument de la fonction MPI_Recv, le status, on peut obtenir le rang du processus depuis lequel on a reçu des données, et le tag associé.

Exemple avec plusieurs envoyeurs vers un même destinataire[modifier | modifier le wikicode]

Dans l'exemple suivant, les processus de rang différent de 0 obtiennent leur PID et l’envoient au processus de rang 0. Celui-ci doit donc effectuer autant de réception qu’il y a de processus dans le communicateur MPI_COMM_WORLD en plus de lui. Mais ces réceptions peuvent arriver dans n'importe quel ordre. Comme l'ordre de réception n’a pas d'importance, il reçoit de MPI_ANY_SOURCE et il récupère l’émetteur du message dans la variable status en sortie du MPI_Recv. Pour illustrer l'utilisation de MPI_ANY_TAG, on reçoit également avec n’importe quel tag.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <mpi.h>

#define TAG 1664

int main( int argc, char** argv ) {
  int rank, size, pid, i;
  MPI_Status status;
  MPI_Init( &argc, &argv );
  MPI_Comm_size( MPI_COMM_WORLD, &size );
  MPI_Comm_rank( MPI_COMM_WORLD, &rank );
  if( 0 == rank ) { /* Le processus 0 recoit depuis les autres */
    for( i = 0 ; i < size - 1 ; i++ ) {
        MPI_Recv( &pid, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &status );
        printf( "%d : J\'ai reçu %d depuis %d et le tag %d\n", rank, pid, status.MPI_SOURCE, status.MPI_TAG );
    }
  } else { /* Tous les processus autres que 0 envoient leur pid a 0 */
    pid = (int) getpid();
    printf( "%d : j\'envoie %d\n", rank, pid );
    MPI_Send( &pid, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD );
  } 
  MPI_Finalize();
  return EXIT_SUCCESS;
}

L'affichage obtenu est donc :

   moi@thrall:$ mpiexec -n 6 ./exemple2.4
   1 : j'envoie 12414
   5 : j'envoie 12422
   2 : j'envoie 12415
   4 : j'envoie 12419
   3 : j'envoie 12416
   0 : J'ai reçu 12414 depuis 1 et le tag 1664
   0 : J'ai reçu 12422 depuis 5 et le tag 1664
   0 : J'ai reçu 12419 depuis 4 et le tag 1664
   0 : J'ai reçu 12415 depuis 2 et le tag 1664

Modes de communication[modifier | modifier le wikicode]

Concrètement, les communications faites par MPI_Send et MPI_Recv ne sont pas forcément synchrones. Ainsi, il est possible que MPI_Send retourne alors que le processus destinataire n’est pas encore dans son MPI_Recv. C'est un fait très important à garder à l’esprit lorsqu'on débug un programme MPI : ce n’est pas parce qu'on a envoyé un message que le destinataire va le recevoir, par exemple si il manque un MPI_Recv.

MPI implémente ses communications selon deux modes : eager (pour les petits messages) et rendez-vous (pour les gros messages). La taille de messages limite entre les deux modes est un réglage de la bibliothèque MPI, et son optimum est spécifique aux caractéristiques de la machine.

Le mode eager fonctionne selon le principe que la couche de communications stocke temporairement le message dans un buffer interne d'envoi. Dès que le message est copié dans la couche de communications, le buffer d'envoi du programme peut être réutilisé et MPI_Send retourne. Le message est alors dans la couche de communications, qui le prend en charge, mais il n'est pas forcément arrivé à destination.

Côté destination, si la bibliothèque reçoit un message sans être dans un MPI_Recv correspondant, ce message est mis dans un buffer de réception. Plus tard, quand le programme entre dans un MPI_Recv, la bibliothèque commence par regarder dans les buffers de réception si un message correspondant (en regardant notamment l’émetteur, le communicateur et le tag) n'a pas déjà été reçu.

Étant donné les stockages intermédiaires qu'il implique, le mode eager est plutôt utilisé pour des messages de petite taille.

À l'inverse, le mode rendez-vous évite ces stockages intermédiaires, mais il utilise un protocole de synchronisation entre l'émetteur et le destinataire. Il implémente une poignée de main :

  • l'émetteur envoie un premier fragment
  • le destinataire reçoit ce fragment et, quand il dispose d'assez de place pour stocker le message entier, il envoie un acquittement
  • une fois que l'émetteur reçoit cet acquittement, il envoie le reste des données.

Ainsi, en mode rendez-vous, la communication n’a lieu que si le destinataire est en mesure de recevoir le message. IL implique une synchronisation entre les deux processus : code>MPI_Send retourne quand la communication a effectivement eu lieu. Il évite également de parcourir les buffers de réception en entrant dans MPI_Recv. Cependant, la poignée de main introduit une latence à l'initialisation de la communication : ainsi, on l'utilise plutôt pour des messages de taille moyenne à grande, plus limités par la bande passante que par la latence.


Communications non-bloquantes[modifier | modifier le wikicode]

Superposition communication et calcul[modifier | modifier le wikicode]

On a vu que, dans le cas général, les communications avec MPI_Send et MPI_Recv sont bloquantes : la fonction retourne quand la communication a été effectuée ou, au moins, transmise à la couche de communications. Cependant, parfois, on peut vouloir faire du calcul pendant que les communications soient effectuées par la couche de communications d'une part, et les calculs progressent en même temps. En particulier, certaine cartes réseau sont capable de gérer les communications de façon autonome : par exemple, les cartes InfiniBand, Myrinet…

Pour cela, la norme MPI dispose de fonctions dites non-bloquantes. Les communications sont postées, on récupère une requête vers la communication, et la communication progresse en arrière-plan. C'est en utilisant cette requête que le programmeur peut interroger l'avancée de la communication ou attendre sa complétion. Ces fonctions de communications ont un nom qui commence par un i, comme immediate : la fonction retourne immédiatement, pour que l'on puisse effectuer du calcul et, ainsi, superposer communications et calculs. Bien entendu, les calculs doivent être effectués sur des données qui ne sont pas concernées par les communications en cours.

Exemple[modifier | modifier le wikicode]

Prenons par exemple le calcul suivant. Nous avons deux processus (seuls les processus 0 et 1 participent) qui ont chacun un tableau initialisé à des entiers aléatoires dans la fonction fillTable. Ces deux processus vont s'échanger le premier élément de leur tableau, effectuer un calcul sur les valeurs contenues dans leur tableau dans la fonction computeTab (ici, une simple multiplication par 2) et effectuer un calcul utilisant le premier élément (initial, avant multiplication) du tableau de l’autre processus dans la fonction divTab (une division puis la partie entière du résultat).

L'élément reçu de l’autre processus est un diviseur : le programme le reçoit dans une variable div. Ce diviseur n’est pas nécessaire au calcul de la deuxième fonction computeTab. Donc on peut effectuer ce calcul pendant que les communications progressent. Pour cette raison, on poste une communication MPI_Irecv pour que cette réception soit non-bloquante. De même, on poste une communication MPI_Isend pour que l’envoi de cette variable soit non-bloquant. Pour pouvoir modifier les valeurs du tableau, on met la variable envoyée (le premier élément du tableau) dans une variable spécifique.

On attend que la réception du message qui contient la variable div soit effectuée avant d'appeler la fonction qui l'utilise avec MPI_Wait. Ainsi, cette communication a pu avoir lieu pendant que le programme n’avait pas besoin de cette donnée, et on s'assure qu'elle est arrivée avant de l'utiliser.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <math.h>
#include <mpi.h>

#define TAG 1664
#define TABSIZE 512

void fillTable( int*, int );
void computeTab( int*, int );
void divTab( int*, int, int );

int main( int argc, char** argv ) {
    int rank, size, div, tosend;
    MPI_Status stat[2];
    MPI_Request req[2];
    int* tab;
    /* Initialisation MPI */
    MPI_Init( &argc, &argv );
    MPI_Comm_size( MPI_COMM_WORLD, &size );
    MPI_Comm_rank( MPI_COMM_WORLD, &rank );
    if( rank > 1 ) { /* Seuls les processus 0 et 1 participent */
        MPI_Finalize();
        return EXIT_SUCCESS;
    }
    /* Initialisation du tableau */
    tab = (int*) malloc( TABSIZE * sizeof( int ) );
    fillTable( tab, TABSIZE );
    /* On poste un envoi du premier élément du tableau */
    tosend = tab[0];
    printf( "%d envoie %d à %d\n", rank, tosend, rank ^ 0x1 );
    MPI_Isend( &tosend, 1, MPI_INT, rank ^ 0x1, TAG, MPI_COMM_WORLD, &req[0] ) ;
    /* On poste la réception du diviseur */
    MPI_Irecv( &div, 1, MPI_INT, rank ^ 0x1, TAG, MPI_COMM_WORLD, &req[1] ) ;
    /* On fait un calcul sur le tableau */
    computeTab( tab, TABSIZE );
    /* On a besoin du diviseur : on attend que la communication soit terminée */
    MPI_Wait( &req[1], &stat[1] );
    printf( "%d a reçu comme diviseur %d\n", rank, div );
    /* On l'utilise pour le calcul */
    divTab( tab, TABSIZE, div );
    /* On a fini, on attend que la communication du premier élément du tableau soit terminée */
    MPI_Wait( &req[0], &stat[0] );
    /* Finalisation */
    free( tab );
    MPI_Finalize();
    return EXIT_SUCCESS;
}

void fillTable( int* tab, int N ) {
    int i;
    srand( (unsigned int) getpid() );
    for( i = 0 ; i < N ; i++ ) {
        tab[i] = rand( );
    }
}

void computeTab( int* tab, int N ) {
    int i;
    for( i = 0 ; i < N ; i++ ) {
        tab[i] *= 2;
    }
}

void divTab( int* tab, int N, int div ) {
    int i;
    for( i = 0 ; i < N ; i++ ) {
        tab[i] = floor( tab[i] / div );
    }
}

Envois et réceptions[modifier | modifier le wikicode]

Les fonctions d'envoi et réception sont donc MPI_Isend et MPI_Irecv. Elles s'utilisent presque comme leurs homologues bloquantes, à ceci près qu'elles ont une variable de sortie de type MPI_Request : c'est ce handle qui nous permet d'identifier la communication et de faire des opérations sur celle-ci.

De façon similaire, on constate que MPI_Irecv n'a pas de variable de sortie de type MPI_Status : en effet, comme cette information n’a de sens qu'une fois la communication effectuée, on la récupère dans les fonctions de test et d'attente de complétion.

Attente[modifier | modifier le wikicode]

On attend la complétion d'une ou plusieurs communications non-bloquantes avec des fonctions d'attente. Ce sont des fonctions bloquantes : elles ne rendent la main qu'une fois que les communications qu'elles attendent sont terminées.

Elles prennent en entrée les handles de type MPI_Request qui, comme on l’a vu, permettent d'identifier les communications non-bloquantes en cours. Elles prennent les statuts des communications (de type MPI_Status) concernées.

On peut attendre une seule communication avec MPI_Wait, un ensemble de communications avec MPI_Waitall, une communication quelconque (la première qui termine) parmi un ensemble de communications avec MPI_Waitany et un sous-ensemble avec MPI_Waitsome.

Par exemple, dans le code suivant, le processus 0 reçoit un entier (le PID) depuis chaque autre processus et il fait une opération xor entre tous les entiers qu'il reçoit. Pour cela, il poste des MPI_Irecv depuis tous les autres processus et il attend que ces communications soient terminées l'une après l’autre avec MPI_Waitany. En valeur de sortie du MPI_Waitany, on sait quel est l'indice de la communication qui a terminé dans le tableau de requêtes. Une fois la communication terminée, la requête correspondante est invalidée, donc on ne sortira pas à nouveau dessus.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <mpi.h>

#define TAG 1664

int main( int argc, char** argv ) {
    int rank, size, pid, i, k, res;
    int* tab;
    MPI_Status* stat;
    MPI_Request* req;
    MPI_Init( &argc, &argv );
    MPI_Comm_size( MPI_COMM_WORLD, &size );
    MPI_Comm_rank( MPI_COMM_WORLD, &rank );
    if( 0 == rank ) {
        stat = (MPI_Status*) malloc( ( size - 1 ) * sizeof( MPI_Status ) ); 
        req = (MPI_Request*) malloc( ( size - 1 ) * sizeof( MPI_Request ) );
        tab = (int*)  malloc( ( size - 1 ) * sizeof( int ) );
    }
    pid = (int) getpid();
    if( 0 == rank ) { /* Le processus 0 recoit depuis les autres */
        for( i = 0 ; i < size - 1 ; i++ ) { /* On poste autant de réception qu'on en veut */
            MPI_Irecv( tab + i, 1, MPI_INT, i + 1, TAG, MPI_COMM_WORLD, req + i );
        }
        res = pid; /* Ici on peut faire un calcul */
        for( i = 0 ; i < size - 1 ; i++ ) { /* On attend chaque réception */
            MPI_Waitany( size - 1, req, &k, stat );
            /* On sait quelle communication a terminé avec l'indice k */
            printf( "reçu %d depuis %d\n", tab[k], k+1 );
            res ^= tab[k] ;
        }
        
    } else { /* Tous les processus autres que 0 envoient leur pid a 0 */
        printf( "%d : j\'envoie %d\n", rank, pid );
        MPI_Send( &pid, 1, MPI_INT, 0, TAG, MPI_COMM_WORLD );
    }
    /* Finalisation */
    if( 0 == rank ) {
        free( stat );
        free( req );
        free( tab );
    }
    MPI_Finalize();
    return EXIT_SUCCESS;
}

Test de progression[modifier | modifier le wikicode]

Sans attendre la fin de la communication, on peut également tester si elle est terminée. Pour cela, on dispose des fonctions MPI_Test et, de manière similaire aux fonctions d'attente de fin, MPI_Testall, MPI_Testany et MPI_Testall.

Elles n'attendent pas la fin de la communication : elles permettent uniquement de savoir si la communication est terminée, en retournant un drapeau à vrai si c'est le cas.

On peut également les utiliser pour redonner la main à la bibliothèque pour faire avancer les communications. Certains réseaux (comme Ethernet) ne peuvent pas faire avancer les communications de façon indépendante : lorsque le processus est en train de calculer, les communications ne peuvent pas avancer. Par conséquent, l'intégralité de la communication se fait dans le MPI_Wait. À l'inverse, les réseaux dits "rapides" ont une carte pouvant faire progresser les communications de façon autonome, et permettent une réelle superposition des communications et du calcul. Dans les cas où ce n'est pas possible, on peut réentrer de temps en temps dans la bibliothèque (par exemple à chaque tour de boucle, dans le cas d'un calcul itératif) avec MPI_Test pour lui permettre de faire progresser la communication.

Annulation[modifier | modifier le wikicode]

On peut également annuler une communication en cours, avec la fonction MPI_Cancel. Ici encore, on identifie la communication concernée par son handle de type MPI_Request.

Autres types de communications[modifier | modifier le wikicode]

Si les fonctions de communications bloquantes et non-bloquantes citées ci-dessus sont les plus utilisées, il en existe d'autres, qui s'utilisent selon le même principe. Leur sémantique change et assure des propriétés différentes. Les fonctions de réceptions correspondantes existent.

  • Nous avons vu que MPI_Send ne rend la main qu'une fois que le buffer d'émission peut être réutilisé. Ainsi, il peut retourner dès que les données ont été copiées dans un autre buffer d'envoi, intermédiaire, de la couche de communications. Il n'assure pas que les données ont été transmises, ni même que le destinataire est dans une fonction de réception.
  • MPI_Isend est une fonction d'envoi non-bloquante. Le buffer d'envoi ne doit pas être réutilisé tant que l’on ne s'est pas assuré de la complétion de la communication.
  • MPI_Bsend est un envoi bufferisé. Elle copie les données dans un buffer d'envoi et retourne dès que la copie est terminée.
  • MPI_Ibsend est un envoi bufferisé non-bloquant : on récupère un handle de type MPI_Request qui permet d'attendre ou de tester la complétion de la requête. Le buffer d'envoi peut être réutilisé, car les données ont été copiées dans un buffer intermédiaire de la couche de communications.
  • MPI_Ssend est un envoi synchrone : la fonction ne retourne qu'une fois que le destinataire a appelé une fonction de réception correspondante.
  • MPI_Issend est un envoi synchrone non bloquant : les fonctions de test ou d'attente de la complétion de l’envoi ne retournent qu'une fois que le destinataire a appelé une fonction de réception correspondante.
  • MPI_Rsend est un envoi "prêt" (ready send). Elle ne peut être appelée que si le destinataire est déjà dans une fonction de réception correspondante, ce qui la rend relativement délicate à utiliser.
  • MPI_Rsend est un envoi "prêt" non bloquant, permettant de transférer les données pendant le calcul, tout en étant en mode "prêt".