Courses

Introduction à MPI

Programmation parallèle sur architecture à mémoire distribuée avec MPI

Le but de cette première série est de vous faire découvrir le principe de la programmation parallèle sur architecture à mémoire distribuée à l'aide de la librairie MPI. A l'issue de cette série, vous devriez avoir bien compris la notion de rang d'un processus MPI et les bases de la communication point à point. Nous regarderons également comment accéder et exécuter des programmes parallèles sur des machines partagées.

Considérations techniques

Afin de pouvoir compiler et exécuter des programmes MPI sur votre machine personnelle, vous aurez besoin d'un compilateur MPI (mpiccmpiCC ou mpicxx) et du runtime MPI (mpirun ou mpiexec).

Afin de vous connecter au cluster Baobab, vous aurez besoin d'un client ssh. Un tel client est disponible par défaut sur l'immense majorité des distributions GNU/Linux et sur Mac OS. Sur Windows, vous pouvez utiliser putty.

Compilation et exécution locale d'un programme MPI simple

# Compilez le programme hello ci-dessous
# à l'aide de la commande:
# mpiCC or mpicxx depending the operating system
mpiCC hello.cpp -o hello -std=c++11
 
# Puis exécutez le plusieurs fois à l'aide de la commande :
mpirun -np 10 ./hello

Quelles conclusions tirez-vous sur le modèle d'exécution de MPI ?

Programme hello.cpp

#include <mpi.h>
#include <iostream>
 
int main(int argc, char **argv) {
  int myRank, nProc;
 
  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &myRank);
  MPI_Comm_size(MPI_COMM_WORLD, &nProc); 
 
  std::cout << "Hello, I'm process" << myRank << std::endl;
  std::cout << "There is " << nProc << " processes" << std::endl;   
 
  MPI_Finalize();
}

Compilation et exécution d'un programme MPI sur une machine partagée dotée d'un système de queuing

En général, les programmes MPI sont exécutés sur des machines parallèles disposant d'un grand nombre de coeurs de calcul et de beaucoup de mémoire. MPI générant alors autant de processus que voulu du programme exécuté. Ces machines étant utilisées par plusieurs utilisateurs, il est important de pouvoir s'assurer qu'à un moment donné, il existe au maximum un flux d'exécution (thread ou processus) par coeur de calcul disponible sur la machine. Pour cela, on utilise des systèmes de queuingSLURM est un système de queuing très répandu, c'est celui que nous utiliserons.

Le principe de ce type de système est le suivant : on décrit les paramètres d'exécution du programme dans un script et on soumet ce script au système de queuing. Ceci à pour effet de créer un job qui sera exécuté sur la machine dès que les ressources nécessaires sont disponibles.

Nous utiliserons le cluster baobab dès la semaine prochaine. Pour l'instant, nous exécuterons les programmes localement.

Somme locale de vecteurs

Maintenant que vous savez exécuter des programmes MPI, effectuez une opération en parallèle en modifiant votre programme hello.cpp comme suit:

#include <mpi.h>
#include <iostream>
#include <vector>
 
int main(int argc, char **argv) {
  int myRank, nProc;
 
  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &myRank);
  MPI_Comm_size(MPI_COMM_WORLD, &nProc); 
 
  std::vector<int> v(10, myRank);
  int sum = 0;
  for(auto i: v) sum += i;
 
  std::cout << "Hello, I'm process" << myRank << " and the sum of my vector is " << sum << std::endl; 
 
  MPI_Finalize();
}

Exécutez ensuite ce programme. Assurez vous de comprendre le résultat de l'exécution.

Communication point à point

Comme son nom l'indique, MPI (Message Passing Interface) est une librairie avant tout faite pour échanger des messages entre processus. Les deux fonctions les plus basique pour cela sont MPI_Send et MPI_Recv, qui comme leur noms l'indiquent servent respectivement à envoyer un buffer et à recevoir un buffer. La documentation de ces fonctions se trouve respectivement ici pour MPI_Send et là pour MPI_Recv.

Le programme suivant illustre une communication point à point basique. Exécutez le avec 3 (ou plus), 2 puis 1 processus et assurez vous d'avoir compris le résultat obtenu.

#include <mpi.h>
#include <iostream>
#include <vector>
 
int main(int argc, char **argv) {
  int myRank, nProc;
 
  MPI_Init(&argc, &argv);
  MPI_Comm_rank(MPI_COMM_WORLD, &myRank);
  MPI_Comm_size(MPI_COMM_WORLD, &nProc); 
 
  std::vector<int> v(10, 0);
 
  if(myRank == 0){
    for(int i=0; i<v.size(); i++) v[i] = i;
    std::cout << "I'm process 0 and I'm sending my vector" << std::endl;
    MPI_Send(v.data(), v.size(), MPI_INT, 1, 0, MPI_COMM_WORLD);
  }
  else if(myRank == 1){
    MPI_Status status;
    std::cout << "I'm process 1, last element of my vector before receive is " << v.back() << std::endl;
    MPI_Recv(v.data(), v.size(), MPI_INT, 0, 0, MPI_COMM_WORLD, &status);
    std::cout << "I'm process 1, last element of my vector after receive is " << v.back() << std::endl;
  }
  else{
    std::cout << "I'm process " << myRank << " I have nothing to do" << std::endl;
  }
 
  MPI_Finalize();
}

Exercice : somme globale de vecteurs

Nous avons vu comment créer des processus MPI et comment les faire communiquer. Il est maintenant temps d'essayer de faire communiquer les processus afin de leur faire résoudre un problème collectivement en parallèle. Ecrivez un programme qui effectue la somme d'un vecteur en parallèle. Pour ce faire, écrivez une première version qui effectuera les opérations suivantes:

  • chaque processus alloue et initialise un vecteur local de 10 éléments, chaque élément du vecteur étant égal au rang du processus
  • chaque processus effectue la somme locale de son vecteur et affiche le résultat
  • chaque processus envoie sa somme locale au processus 0
  • le processus 0 reçoit toutes les sommes locales et en effectue la somme, obtenant ainsi la somme globale, et affiche le résultat

Une fois que vous avez terminé, écrivez une deuxième version dont la première étape est remplacée par :

  • le processus 0 alloue et initialise un vecteur de taille 10 x nombre de processus (10*nProc), chaque élément du vecteur étant égal à son indice
  • le processus 0 envoie 10 éléments du vecteurs à chaque processus

N'oubliez pas qu'à un Send doit toujours correspondre un Recv et que le processus 0 doit également effectuer sa part du travail.