Maître de Conférences en Informatique à l'Université d'Angers
Ce site est en cours de reconstruction certains liens peuvent ne pas fonctionner ou certaines images peuvent ne pas s'afficher.
MPI (The Message Passing Interface), conçue en 1993-94, est une norme (ou API - Application Programming Interface) définissant une bibliothèque de fonctions, utilisable avec les langages C, C++ et Fortran. Elle permet d'exploiter des ordinateurs distants ou multiprocesseurs par passage de messages (Wikipedia).
La technique de passage de message consiste à transmettre au travers du réseau les données à échanger. Initialement MPI a été conçu pour des systèmes à mémoire distribuée très populaires dans les années 80-90. MPI fut ensuite adapté pour les systèmes à mémoire partagée et fonctionne à présent pour des systèmes hybrides (distribué + partagé).
L'ensemble des fonctions peut être trouvé à cette adresse OpenMPI Doc.
MPI étant une API, il existe plusieurs implantations come MPICH ou OpenMPI (voir ce site pour un aperçu de l'architecture MPI).
Sous Ubuntu 24.04, il n'est plus possible d'utiliser la partie C++ de MPI. Deux possibilités s'offrent à vous :
Récupérez openmpi-4.1.8.tar.gz sur le site d'OpenMPI. Préférer la version 4.1 à la dernière version (5.X) car les fichiers sont moins volumineux.
Décompressez, compilez le code source et installez les librairies, il s'agit d'une installation locale dans votre home directory :
> tar -xvzf openmpi-4.1.8.tar.gz
> cd openmpi-4.1.8/
# en local (pour l'utilisateur courant)
> ./configure --prefix=$HOME/openmpi-4.1.8 --disable-mpi-fortran \
--disable-oshmem CC=gcc CXX=g++ CFLAGS="-O3 -march=native" \
CXXFLAGS="-O3 -march=native -std=c++20" --enable-mpi-cxx
> make -j$(nproc)
> make install
# ou pour une installation globale (tous les utilisateurs)
> ./configure prefix=/usr/local/openmpi-4.1.8 --disable-mpi-fortran \
--disable-oshmem CC=gcc CXX=g++ CFLAGS="-O3 -march=native" \
CXXFLAGS="-O3 -march=native -std=c++20" --enable-mpi-cxx
> make -j$(nproc)
> sudo make install
Note: l'utilisation de -std=c++20 n'est pas forcément nécessaire.
Modifiez ensuite votre fichier
# pour une installation locale
export PATH=\$HOME/openmpi-4.1.8/bin:\$PATH
export LD_LIBRARY_PATH=\$HOME/openmpi-4.1.8/lib:\$LD_LIBRARY_PATH
# ou pour une installation globale
export PATH=/usr/local/openmpi-4.1.8/bin:\$PATH
export LD_LIBRARY_PATH=/usr/local/openmpi-4.1.8/lib:\$LD_LIBRARY_PATH
et faire un source du
> source .bashrc
# vérifier que mpi fonctionne :
> mpirun --version
mpirun (Open MPI) 4.1.8
Report bugs to http://www.open-mpi.org/community/help/
Commencez par installer Docker s'il n'est pas déjà installé :
> sudo apt-get update
# ne pas oublier docker-buildx pour les versions récentes de docker
> sudo apt-get install docker.io docker-buildx
> sudo usermod -aG docker $USER
> sudo systemctl start docker
Créez ensuite un répertoire dans lequel vous placez un fichier
> mkdir mpi
> cd mpi
> nano Dockerfile
Le fichier
# Utiliser Ubuntu 20.04 comme image de base
FROM ubuntu:20.04
# Empêcher les invites interactives lors de l'installation des paquets
ARG DEBIAN_FRONTEND=noninteractive
# Mettre à jour le système et installer les dépendances nécessaires
# Utilisation de fte pour avoir l'éditeur de texte sfte plus sympathique
# que nano
RUN apt-get update && apt-get install -y \
build-essential \
openmpi-bin \
openmpi-common \
libopenmpi-dev \
sudo \
nano fte fte-console fte-terminal \
&& rm -rf /var/lib/apt/lists/*
# Créer un utilisateur non-root 'user'
RUN useradd -m -s /bin/bash user \
&& echo "user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# Passer à l'utilisateur 'user'
USER user
# Créer un répertoire de travail pour les programmes
WORKDIR /home/user
# Configurer les variables d'environnement MPI
ENV PATH="/usr/lib64/openmpi/bin:\$PATH" \
LD_LIBRARY_PATH="/usr/lib64/openmpi/lib:\$LD_LIBRARY_PATH"
# L'utilisateur 'user' peut maintenant compiler et exécuter des programmes MPI
# Vous pouvez ajouter vos fichiers ou configurations ici, par exemple :
# COPY . /home/user/
# Commande par défaut (exemple pour démarrer un shell interactif)
CMD ["/bin/bash"]
Compilez ensuite le Dockerfile afin de créer le container MPI :
> sudo docker build -t mpi_cpp .
> sudo docker run -it mpi_cpp
Ou bien si on veut mapper (mettre en correspondance) le répertoire
> docker run -it \
--mount type=bind,source=/home/richer/exchange,target=/home/user/exchange mpi_cpp
Il est nécessaire de modifier substantiellement son programme si on désire le paralléliser avec MPI car on exécute le même programme sur plusieurs machines/coeurs différents.
MPI est disponible pour Fortran, C et C++ et également avec Python (pyMPI).
Pour pouvoir disposer de MPI sur sa machine il faut installer les librairies suivantes sous Ubuntu :
> sudo apt-get install libopenmpi-dev openmpi-bin openmpi-doc
Pour C et C++ on inclura le fichier mpi.h
Pour débuguer il est recommandé d'installer MPE (Multi-Processing Environment), difficile à trouver, qui doit être compilé lors de l'installation de Python Anaconda. On réalisera alors l'édition de liens avec -llmpe -lmpe
On peut également utiliser la commande suivante (ici dans le cas de deux programmes avec gdb):
> mpirun -n 2 xterm -e gdb ./a.exe
Deux terminaux sont alors ouverts et il faut lancer l'exécution du programme dans le débogueur avec run.
Les opérations MPI portent sur des communicateurs qui sont en quelque sorte des ports de communication inter processus. Le communicateur par défaut est COMM_WORLD qui comprend tous les processus actifs.
On peut créer de nouveaux communicateurs mais cela se révèle complexe.
Le modèle de communication est un modèle point à point basé sur deux opérations élémentaires send et receive qui sont ensuite déclinés de plusieurs manières différentes.
Le schéma classique d'utilisation de MPI consiste à :
Voici deux exemples simples qui affichent un message avec un temps de latence (instruction sleep) pour chaque thread / processeur. Le premier utilise des fonctions C, le second utilise des espaces de noms et des objets
Afin de compiler un programme MPI, on utilise le compilateur mpicc pour le C, mpic++ pour le C++ :
# pour le code source C
> mpicc -o exe src.c -O3 ...
# ou alors pour les sources en C++ :
> mpic++ -o exe src.cpp -O3 ...
# ou alors si cela ne fonctionne pas :
> mpicxx -o exe src.cpp -O3 ...
Pour exécuter un programme compilé avec MPI, il faut utiliser mpirun (ou mpiexec) en spécifiant le nombre de processus (=machines, processeurs, coeurs) grâce à l'option -n (number) ou -np (number of processes) suivant les systèmes :
> mpirun -n 4 ./c3_mpi_ex_1.exe
Dans le cas où on voudrait lancer plus d'instances de programme que de threads disponibles,
> mpirun -n 20 ./ezmpi_broadcast.exe
--------------------------------------------------------------------------
There are not enough slots available in the system to satisfy the 20
slots that were requested by the application:
...
Alternatively, you can use the --oversubscribe option to ignore the
number of available slots when deciding the number of processes to
launch
--------------------------------------------------------------------------
Il faudra alors utiliser l'option
Voici le résultat à l'affichage du programme précédent si on l'exécute sur une seule machine Intel Core i3-2375M CPU @ 1.50GHz (Dual Core + HyperThreading, soit 4 threads) :
running on inspiron with id=0/4
running on inspiron with id=1/4
running on inspiron with id=2/4
running on inspiron with id=3/4
Voici un autre exemple qui lance 16 processus sur 4 noeuds (h1 à h4) en réservant 4 processus (coeurs) sur chacune des machines :
> mpiexec -hosts h1:4,h2:4,h3:4,h4:4 -n 16 ./test
Référez vous à la section 5.10 pour voir réellement comment faire.
Sur un cluster on utilisera par exemple (-pe = parallel environment):
> qsub -pe mpi 16 test_mpi.sh
Pour obtenir une section critique pour l'affichage on utilise la fonction MPI_Barrier ou MPI::COMM_WORLD.Barrier() en C++ qui bloque l'appelant jusqu'à ce que tous les autres programmes aient appelé cette fonction :
Dans l'exemple qui suit, les 6 processus commencent par travailler (travail remplacé ici par un sleep) en parallèle. Le premier (identifiant 0) suspend son exécution pendant 1 seconde, le second pendant 2 secondes, etc. Au final, après 6 secondes, l'ensemble des processus exécutent le code qui suit l'appel à MPI::COMM_WORLD.Barrier();.
> mpirun -n 6 ./mpi_cpp_syncrho.exe
Hello, from 3
Hello, from 4
Hello, from 5
Hello, from 2
Hello, from 0
Hello, from 1
.... wait 6 seconds here until you finally get ....
Bye, from 0
Bye, from 5
Bye, from 3
Bye, from 4
Bye, from 1
Bye, from 2
Pour transmettre des données entre les processeurs, on utilise deux fonctions MPI_Send et MPI_Recv, ou éventuellement MPI_Sendrecv :
// envoi d'un processus vers un autre processus
int MPI_Send(void* data,
int count,
MPI_Datatype datatype,
int destination,
int tag,
MPI_Comm communicator)
// réception d'un processus vers un autre processus
int MPI_Recv(void* data,
int count,
MPI_Datatype datatype,
int source,
int tag,
MPI_Comm communicator,
MPI_Status* status)
// envoi suivi d'une réception
int MPI_Sendrecv(const 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)
Pour la partie C++ on utilisera :
MPI::COMM_WORLD.Send(const void* buf,
int count,
MPI::Datatype& datatype,
int dest,
int tag) const
void MPI::COMM_WORLD.Recv(void* buf,
int count,
MPI::Datatype& datatype,
int source,
int tag,
MPI::Status* status) const
void MPI::COMM_WORLD::Sendrecv(const void* sendbuf,
int count,
MPI::Datatype& datatype,
int dest, int sendtag,
void* recvbuf, int recvcount,
MPI::Datatype recvtype, int source, int recvtag,
MPI::Status* status) const
Voici quelques exemples :
Dans le premier exemple, on utilise deux processeurs. Le maître (identifiant 0) et l'esclave (identifiant 1) :
| Maître | Esclave |
|---|---|
| Création d'un tableau et initialisation | |
| Envoi de la taille → | |
| Réception de la taille, création du tableau | |
| Envoi du tableau → | |
| Réception des données dans le tableau | |
| Calcul de la somme des éléments | |
| ← Envoi de la somme | |
| Réception de la somme | |
| Affichage de la somme |
Le deuxième exemple utilise sendrecv pour échanger une donné entre maître et esclave.
Attention la fonction Sendrecv permet uniquement d'échanger deux données de même type et de même nombre d'occurrence.
On ne peut pas, par exemple, envoyer un tableau de données et attendre la somme en retour.
Afin de simplifier l'utilisation de MPI on peut créer une interface (wrapper) : j'ai créé, pour ma part, EZMPI.
EZMPI est constitué d'une seule classe Process qui représente un processus et qui permet de récupérer lors de l'initialisation l'identifiant du processeur, le nombre de processus lancés, ... On dispose également de méthodes qui permettent d'envoyer et recevoir un élément (quel que soit le type) ou un tableau d'élements.
On dispose en outre d'un système de log qui permet de consulter le déroulement des échanges entre processus (réception et envoi de données).
Le fichier principal à inclure est le suivant (il comprend interface et implantation) :
On peut alors réécrire les programes précédents de manière plus simple :
Certains traitements comme la réduction ou le scan sont implantés spécifiquement pour MPI.
Par exemple pour la réduction :
MPI propose deux opérations inverses :

Voici un exemple pour lequel $K$ processus créent des tableaux de 10 entiers qui sont envoyés au master (processus de rang 0) :
> mpirun -n 4 ./ezmpi_scatter.exe
====================
=== FINAL RESULT ===
====================
---------------------
CPU 0
---------------------
14:15:05 cpu 0/4: global array = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 ]
14:15:05 cpu 0/4: scatter
14:15:05 cpu 0/4: local array=[1 2 3 4 5 6 7 8 9 10 ]
---------------------
CPU 1
---------------------
14:15:05 cpu 1/4: scatter
14:15:05 cpu 1/4: local array=[11 12 13 14 15 16 17 18 19 20 ]
---------------------
CPU 2
---------------------
14:15:05 cpu 2/4: scatter
14:15:05 cpu 2/4: local array=[21 22 23 24 25 26 27 28 29 30 ]
---------------------
CPU 3
---------------------
14:15:05 cpu 3/4: scatter
14:15:05 cpu 3/4: local array=[31 32 33 34 35 36 37 38 39 40 ]
> mpirun -n 4 ./ezmpi_gather.exe
====================
=== FINAL RESULT ===
====================
---------------------
CPU 0
---------------------
14:19:02 cpu 0/4: local array=[1 2 3 4 5 6 7 8 9 10 ]
14:19:02 cpu 0/4: gather
14:19:02 cpu 0/4: global array = [1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 ]
---------------------
CPU 1
---------------------
14:19:02 cpu 1/4: local array=[11 12 13 14 15 16 17 18 19 20 ]
14:19:02 cpu 1/4: gather
---------------------
CPU 2
---------------------
14:19:02 cpu 2/4: local array=[21 22 23 24 25 26 27 28 29 30 ]
14:19:02 cpu 2/4: gather
---------------------
CPU 3
---------------------
14:19:02 cpu 3/4: local array=[31 32 33 34 35 36 37 38 39 40 ]
14:19:02 cpu 3/4: gather
Enfin, l'opération broadcast envoie à tous les esclaves une copie d'un tableau détenu par le maître par exemple :
> mpirun -n 4 ./ezmpi_broadcast.exe
====================
=== FINAL RESULT ===
====================
---------------------
CPU 0
---------------------
17:08:27 cpu 0/4: global array on master = [1 2 3 4 5 6 7 8 9 10 ]
17:08:27 cpu 0/4: broadcast
17:08:27 cpu 0/4: global array (after broadcast) =[1 2 3 4 5 6 7 8 9 10 ]
---------------------
CPU 1
---------------------
17:08:27 cpu 1/4: global array on slave = [0 0 0 0 0 0 0 0 0 0 ]
17:08:27 cpu 1/4: broadcast
17:08:27 cpu 1/4: global array (after broadcast) =[1 2 3 4 5 6 7 8 9 10 ]
---------------------
CPU 2
---------------------
17:08:27 cpu 2/4: global array on slave = [0 0 0 0 0 0 0 0 0 0 ]
17:08:27 cpu 2/4: broadcast
17:08:27 cpu 2/4: global array (after broadcast) =[1 2 3 4 5 6 7 8 9 10 ]
---------------------
CPU 3
---------------------
17:08:27 cpu 3/4: global array on slave = [0 0 0 0 0 0 0 0 0 0 ]
17:08:27 cpu 3/4: broadcast
17:08:27 cpu 3/4: global array (after broadcast) =[1 2 3 4 5 6 7 8 9 10 ]
Exécuter un programme MPI sur plusieurs machines relève de la gageure, j'ai passé 4 heures pour pouvoir y parvenir après moult problèmes :
Pour que cela fonctionne il faut, sur toutes les machines :
Dans l'exemple qui suit, on dispose de deux machines :
Eventuellement, modifiez
192.168.1.194 solaris
192.168.1.109 jupiter
Sur solaris :
solaris> ssh-keygen -t ed25519 -N "" -f ~/.ssh/id_ed25519
solaris> ssh-copy-id richer@192.168.1.109
Se connecter une première fois afin d'activer la connexion ssh et la clé :
solaris> ssh richer@192.168.1.109
Welcome to Ubuntu 24.04 LTS (GNU/Linux 6.8.0-31-generic x86_64)
...
jupiter> exit
Sur chaque machine compiler le programme à exécuter :
> mpic++ -o equation_mpi.exe equation_mpi.cpp -O3
On peut probablement le compiler sur la machine principale et le copier sur les machines secondaires par scp si on dispose des mêmes OS et compilateurs.
Ce fichier décrit le nombre de slots (threads / processus) sur chaque machine, il faut le créer sur la machine principale :
solaris@~dev/cpp/parallelism> cat hosts.txt
solaris slots=12
jupiter slots=12
Pour exécuter le programme avec 16 processus, il faut procéder ainsi sur la machine principale :
solaris@~/dev/cpp/parallelism> time mpirun -np 16 --hostfile hosts.txt \
--host solaris:8,jupiter:8 --prefix /usr/local/openmpi-4.1.8 --map-by ppr:8:node \
--mca btl tcp,self --mca btl_tcp_if_include 192.168.1.0/24 \
--mca oob_tcp_if_include 192.168.1.0/24 \
~/dev/cpp/parallelism/equation_mpi.exe -n 26
2 0 0 0 0 4 0 # 1*2 + 6*4 = 26
4 0 0 0 2 2 0 # 1*4 + 5*2 + 6*2 = 26
6 0 0 0 0 1 2 # ...
8 0 0 0 0 3 0
10 0 0 0 2 1 0
12 0 0 0 0 0 2
14 0 0 0 0 2 0
16 0 0 0 2 0 0
18 0 0 2 0 0 0
20 0 0 0 0 1 0
22 0 0 1 0 0 0
23 0 1 0 0 0 0
0 0 0 0 0 2 2
26 0 0 0 0 0 0
24 1 0 0 0 0 0
real 0m6,095s
user 0m18,259s
sys 0m2,002s
Exercice 5.1
Une suite de Syracuse est une suite d'entiers naturels définie de la manière suivante : on part d'un nombre entier strictement positif
Cette suite possède la propriété de converger vers 1 après un certain nombre d'étapes.
Mettre en place une solution MPI avec trois instances :
Le maître comptera le nombre d'appels à l'esclave n°1 et l'esclave n°2.
Lorsque le maître recoît la valeur 1, il s'arrête et envoie un code d'arrêt aux esclaves (un nombre négatif par exemple). Puis il affiche le nombre d'appels à chaque esclave.
Exercice 5.2
Objectif : Écrire un programme MPI pour rechercher un élément dans un tableau.
Description :