Exemple d’utilisation de MPI

MPI (Message Passing Interface) est une bibliothèque et un standard de communications entre noeuds de calcul. Elle gère l’échange des messages entre les processus (transfert de données, synchronisation, opérations globales, etc.). Chaque processus dispose de ses propres données (calcul distribué). Les échanges entre 1 ou plusieurs processus se font dans un communicateur. Chaque processus est identifié pas son rang au sein du groupe.

De nombreuses implémentations différentes de la norme ont été produites, telles que OpenMPI, IntelMPI, MPICH et MVAPICH.

Dans ce qui suit, serot illustrés quelques éléments d’utilisation de cette norme, dans différents langages. Il faut noter que le standard pour C++ a été supprimé depuis MPI 3.0 et qu’il est remplacé par le standard C ou Boost MPI. Python utilise mpi4py.

Code séquentiel

L’objectif est de paralléliser le code de base, qui écrit « Hello! ».

en C

#include <stdio.h>
 
int main(int argc, char *argv[]) 
{
  printf("Hello!\n");

  return(0);
}

en C++

#include <iostream>

int main(int argc, char **argv)
{
  std::cout << "Hello!" << std::endl;

  return 0;
}

en FORTRAN

program hello
 
  implicit none

  print *, 'Hello!'
 
end program hello

en Python

#!/usr/bin/env python3 
# -*- coding: utf-8 -*-

print("Hello!")


en Julia

println("Hello!")

Initialisation de MPI

Tout programme MPI est composé d’un appel à la bibliothèque, puis d’une initialisation de l’environnement, généralement e, début de programme. En fin de programme, l’environnement est quitté.

L’appel à la bibliothèque se fait, suivant les langages, en ajoutant le fichier d’en-tête ou le module approprié, comme mpi.h pour C et use mpi ou use mpi_f08 (norme 2008) pour FORTRAN.

En FORTRAN, les subroutines renvoient un code d’erreur, facultatif avec mpi_f08. Attention, en C, les appels commencent par MPI_# où # est une majuscule. En Python, il n’est pas nécessaire d’initialiser l’environnement MPI (ni d’expliciter sa fin).

Les codes ci-dessous peuvent être compilés et exécutés en parallèle. Chaque processus exécute une copie du programme séquentiel.

en C

#include <stdio.h>
#include <mpi.h>
 
int main(int argc, char *argv[]) 
{
  // Initialize the MPI environment
  MPI _Init(&argc, &argv);

  printf("Hello!\n");

  // Quit the MPI environment
  MPI _Finalize();
  return(0);
}

en C++

#include <iostream>
#include <mpi.h>

int main(int argc, char **argv)
{
  // Initialize the MPI environment
  MPI_Init(&argc, &argv);

  std::cout << "Hello!" << std::endl;

  // Finalize MPI directives
  MPI_Finalize();

  return 0;
}

en FORTRAN

program hello
 
  use MPI

  implicit none

  integer :: ierr

  call MPI_INIT(ierr)

  print *, 'Hello!'
 
  call MPI_FINALIZE(ierr)

end program hello

en Python

#!/usr/bin/env python3 
# -*- coding: utf-8 -*-

import mpi4py.MPI as MPI

print("Hello!")


en Julia

using MPI

MPI.Init()

println("Hello!")

MPI.Finalize()

Identification des processus

Chaque processus a un rang parmi l’ensemble des processus en cours dans le communicateur. Les rangs dans MPI commencent à partir de 0. S’il y a N processus exécutés, le rang varie de 0 à N-1.

Modifions le programme pour que chaque processus donne son rang dans le communicateur et explicite la machine sur laquelle il travaille.

en C

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

int main(int argc, char *argv[]) {

  int size, rank, name_length; 
  char hostname[MPI_MAX_PROCESSOR_NAME];

  // Initialize the MPI environment
  MPI_Init(&argc, &argv);

  // Get the total number of tasks and the rank of the process within the communicator
  MPI_Comm_size(MPI_COMM_WORLD, &size);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);

  // Get the name of the processor
  MPI_Get_processor_name(hostname, &name_length);

  printf("Hello! I am process %d of %d on %s.\n", rank, size, hostname);

  // Quit the MPI environment
  MPI_Finalize();

  return 0;
}

en C++

#include <iostream>
#include <mpi.h>

int main(int argc, char **argv)
{
  int size, rank, name_length; 
  char hostname[MPI_MAX_PROCESSOR_NAME];

  // Initialize the MPI environment
  MPI_Init(&argc, &argv);

  // Get the process size and rank
  MPI_Comm_size(MPI_COMM_WORLD, &size);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);

  // Get the processor name
  MPI_Get_processor_name(hostname, &name_length);

  std::cout << "Hello! I am process " << rank << " of "<< size << " on " << hostname <<".\n" << std::endl;

  // Finalize MPI directives
  MPI_Finalize();

  return 0;
}

en FORTRAN

program hello
   
  use MPI

  implicit none

  integer                             :: nprocs, mypid, name_length, ierr = 0
  character(MPI_MAX_PROCESSOR_NAME)   :: hostname

  call MPI_INIT(ierr)

  call MPI_COMM_SIZE(MPI_COMM_WORLD, nprocs, ierr)
  call MPI_COMM_RANK(MPI_COMM_WORLD, mypid, ierr)

  call MPI_GET_PROCESSOR_NAME(hostname, name_length, ierr)

  write(*, '(2A, I2, A, I2, 3A)') &
       'Hello! ', &
       'I am process ', mypid, &
       ' of ', nprocs, &
       ' on ', hostname(1:name_length), '.'

  call MPI_FINALIZE(ierr)

end program hello

en Python

#!/usr/bin/env python 
# -*- coding: utf-8 -*-

import mpi4py.MPI as MPI

# No need to initialize the MPI environment
comm = MPI.COMM_WORLD

# Get the process size and rank
size = comm.Get_size()
rank = comm.Get_rank()

# Get the processor name
hostname = MPI.Get_processor_name()

print("Hello! I am process {:4d} of {:4d} on {} ".format(rank, size, hostname), flush=True)

# No need to finalize the MPI environment

en Julia

# get MPI module
using MPI

# initialize MPI environment
MPI.Init()

# define the MPI communicator which is of type MPI.Comm
comm = MPI.COMM_WORLD

# Get the process size and rank
size = MPI.Comm_size(comm::MPI.Comm)
rank = MPI.Comm_rank(comm::MPI.Comm)

# Get the processor name
hostname = MPI.Get_processor_name(comm::MPI.Comm)

print("Hello world, I am rank $(rank) of $(size) on $(hostname)\n")

MPI.Finalize()

Mesure du temps

Modifions le programme pour que chaque processus affiche son temps d’exécution. Il sera demandé à ce que l’affichage du temps se fasse après l’affichage des numéros et noms de machine, ce qui nécessite d’appeler une barrière de synchronisation.

en C

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

int main(int argc, char *argv[]) {

  int size, rank, name_length; 
  char hostname[MPI_MAX_PROCESSOR_NAME];
  double time_start, time_stop;

  // Initialize the MPI environment
  MPI_Init(&argc, &argv);

  // Get the total number of tasks and the rank of the process within the communicator
  MPI_Comm_size(MPI_COMM_WORLD, &size);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);

  // Get the name of the processor
  MPI_Get_processor_name(hostname, &name_length);

  // Synchronize before print and start tic chrono
  MPI_Barrier(MPI_COMM_WORLD);
  time_start = MPI_Wtime();
  printf("Hello! I am process %d of %d on %s.\n", rank, size, hostname);

  // Synchronize before stop tac chrono
  MPI_Barrier(MPI_COMM_WORLD);
  time_stop = MPI_Wtime();

  printf("Time spent by process %d : %f sec.\n", rank, time_stop - time_start);

  // Quit the MPI environment
  MPI_Finalize();

  return 0;
}

en C++

#include <iostream>
#include <mpi.h>

int main(int argc, char **argv)
{
  int size, rank, name_length; 
  char hostname[MPI_MAX_PROCESSOR_NAME];
  double time_start, time_stop;

  // Initialize the MPI environment
  MPI_Init(&argc, &argv);

  // Get the process size and rank
  MPI_Comm_size(MPI_COMM_WORLD, &size);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);

  // Get the processor name
  MPI_Get_processor_name(hostname, &name_length);

  // Synchronize before print and start tic chrono
  MPI_Barrier(MPI_COMM_WORLD);
  time_start = MPI_Wtime();
  std::cout << "Hello! I am process " << rank << " of "<< size << " on " << hostname <<".\n" << std::endl;

  // Synchronize before stop tac chrono
  MPI_Barrier(MPI_COMM_WORLD);
  time_stop = MPI_Wtime();
  std::cout << "Time spent by process " << rank << " : " << time_stop - time_start << " sec.\n" << std::endl;

  // Finalize MPI directives
  MPI_Finalize();

  return 0;
}

en FORTRAN

program hello
   
  use MPI

  implicit none

  integer                             :: nprocs, mypid, name_length, ierr = 0
  character(MPI_MAX_PROCESSOR_NAME)   :: hostname
  real(kind = 8):: time_start, time_stop

  call MPI_INIT(ierr)

  call MPI_COMM_SIZE(MPI_COMM_WORLD, nprocs, ierr)
  call MPI_COMM_RANK(MPI_COMM_WORLD, mypid, ierr)

  call MPI_GET_PROCESSOR_NAME(hostname, name_length, ierr)

  ! synchronization point
  call MPI_BARRIER(MPI_COMM_WORLD, ierr)
  time_start = MPI_WTIME()

  write(*, '(2A, I2, A, I2, 3A)') &
       'Hello! ', &
       'I am process ', mypid, &
       ' of ', nprocs, &
       ' on ', hostname(1:name_length), '.'

  call MPI_BARRIER(MPI_COMM_WORLD, ierr)
  time_stop = MPI_WTIME()

  write(*, '(A, I2, A, E14.7, A)') 'Time spent by process ',mypid, ' : ',time_stop-time_start, ' sec.'

  call MPI_FINALIZE(ierr)

end program hello

en Python

#!/usr/bin/env python 
# -*- coding: utf-8 -*-

import mpi4py.MPI as MPI

# No need to initialize the MPI environment
comm = MPI.COMM_WORLD

# Get the process size and rank
size = comm.Get_size()
rank = comm.Get_rank()

# Get the processor name
hostname = MPI.Get_processor_name()

# Synchronize before print and start tic chrono
comm.Barrier()
time_start = MPI.Wtime()
print("Hello! I am process {:4d} of {:4d} on {} ".format(rank, size, hostname), flush=True)

# Synchronize before stop tac chrono
comm.Barrier()
time_stop = MPI.Wtime() 
print("Time spent by process {:4d} : {:4f} ".format(rank, time_stop-time_start), flush=True)

# No need to finalize the MPI environment

en Julia

# get MPI module
using MPI

# initialize MPI environment
MPI.Init()

# define the MPI communicator which is of type MPI.Comm
comm = MPI.COMM_WORLD

# Get the process size and rank
size = MPI.Comm_size(comm::MPI.Comm)
rank = MPI.Comm_rank(comm::MPI.Comm)

# Get the processor name
hostname = MPI.Get_processor_name(comm::MPI.Comm)

# Synchronize before print and start tic chrono
MPI.Barrier(comm::MPI.Comm)
time_start = MPI.Wtime()
print("Hello world, I am rank $(rank) of $(size) on $(hostname)\n")

# Synchronize before stop tac chrono
MPI.Barrier(comm::MPI.Comm)
time_stop = MPI.Wtime()
print("Time spent by process $(rank) : $(time_stop-time_start) src.\n")

MPI.Finalize()

Communication

Dans les programmes précédents, les processus n’échangent pas d’information.

Modifions le programme de base pour que chaque processus envoie son « Hello » au processus de rang suivant dans le communicateur. Il existe de nombreuses façon pour échanger des informations entre chacun des processus. Ici, l’échange se fera point à point, en construisant un anneau de communication.

Pour envoyer (ou recevoir) un message, il est nécesaire d’en connaître la taille et le type.

en C

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

#define BUFMAX 81

int main(int argc, char *argv[]) {

  int size, rank, name_length; 
  char outbuf[BUFMAX], inbuf[BUFMAX];
  int sendto, recvfrom;
  int tag_s=1, tag_r=2;
  MPI_Status status;

  // Initialize the MPI environment
  MPI_Init(&argc, &argv);

  // Get the total number of tasks and the rank of the process within the communicator
  MPI_Comm_size(MPI_COMM_WORLD, &size);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);

  // Should have an odd number of processus
  if ((size % 2)) {
    if (rank == 0) {
      printf("Error! The numper of porcesses is %d\n", size);
      printf("An odd number of processes is required\n");
    }
    exit(0);
  }

  sprintf(outbuf, "Hello");

  // Find next and previous process
  sendto = (rank + 1) % size ;
  recvfrom = (rank + size - 1) % size ;

  if (!(rank % 2)) {
    MPI_Send(outbuf, BUFMAX, MPI_CHAR, sendto, tag_s, MPI_COMM_WORLD);
    MPI_Recv(inbuf, BUFMAX, MPI_CHAR, recvfrom, tag_r, MPI_COMM_WORLD, &status);
  } else {
    MPI_Recv(inbuf, BUFMAX, MPI_CHAR, recvfrom, tag_s, MPI_COMM_WORLD, &status);
    MPI_Send(outbuf, BUFMAX, MPI_CHAR, sendto, tag_r, MPI_COMM_WORLD);
  }

  printf("I am, %d, and recieved from %d the message: \"%s\"\n", rank, recvfrom, inbuf);

  // Quit the MPI environment
  MPI_Finalize();

  return 0;
}