Ada : exécution MPMD d'un couplage de codes sous environnement Intel en batch

Le modèle d’exécution MPMD (Multiple Program Multiple Data) est supporté sur Ada sous l'environnement Intel. Différents exécutables sont lancés et communiquent entre eux en utilisant MPI. Tous les processus MPI sont inclus au sein du même communicateur MPI_COMM_WORLD.

Sur demande uniquement, il est possible d'accéder à un mode MPMD très peu restrictif qui permet de contourner certaines limites des modes MPMD “classiques” lors de l'exécution de programmes hétérogènes.

Couplage MPMD de codes MPI

A titre d'exemple, voici un travail batch qui réalise le couplage de 3 codes MPI, 8 processus MPI sont générés :

  • Le processus MPI de rang 0 est issu de l'exécutable ./a.out
  • Les 4 processus MPI de rang 1 à 4 sont issus de ./b.out
  • Les 3 processus MPI de rang 5 à 7 sont issus de ./c.out
intel_mpmd_mpi.ll
# @ job_name = intel_mpmd
# @ output   = $(job_name).$(jobid)
# @ error    = $(output)
# @ job_type = mpich
# @ total_tasks = 8
# @ wall_clock_limit = 0:30:00
# @ queue
 
# Recommandation : compilez et exécutez vos codes sous un même environnement Intel.
# Donc, si necessaire, utilisez la commande module pour charger l'environnement approprie.
# Par exemple, si votre code est compile avec Intel/2016.2, de-commenter la ligne suivante :
# module load intel/2016.2
 
# Exécution MPMD
mpirun -np 1 ./a.out : -np 4 ./b.out : -np 3 ./c.out

Couplage MPMD de codes hybrides MPI/OpenMP

Le couplage MPMD de codes hybrides MPI/OpenMP se fait de la même façon que ci-dessus mais il faut en plus préciser le nombre de threads OpenMP à utiliser pour chaque exécutable. Cela peut être réalisé à l'aide de l'option -env précédant chaque exécutable apparaissant dans la ligne de commande qui définit le couplage.

Le travail batch intel_mpmd_mpi_openmp.ll réalise le couplage de 3 codes MPI/OpenMP, 13 processus MPI sont générés :

  • Le processus MPI de rang 0 est issu de l'exécutable ./a.out
  • Les 4 processus MPI de rang 1 à 4 sont issus de ./b.out. Chaque processus génère 2 threads OpenMP.
  • Les 8 processus MPI de rang 5 à 12 sont issus de ./c.out. Chaque processus génère 4 threads OpenMP.

Les ressources réservées à l'aide des directives du gestionnaire de batch LoadLeveler sont les suivantes :

  • #@total_tasks=13, il y a 13 processus MPI
  • #@nb_threads=4, nombre de threads maximum par processus MPI

On réserve ainsi 13*4= 52 coeurs, soit 2 nœuds Ada.

intel_mpmd_mpi_openmp.ll
# @ job_name = intel_mpmd
# @ output   = $(job_name).$(jobid)
# @ error    = $(output)
# @ job_type = mpich
# @ total_tasks = 13
# @ nb_threads  = 4
# @ resources = ConsumableCpus($(nb_threads))
# @ wall_clock_limit = 0:30:00
# @ queue
 
# Recommandation : compilez et exécutez vos codes sous un même environnement Intel.
# Donc, si necessaire, utilisez la commande module pour charger l'environnement approprie.
# Par exemple, si votre code est compile avec Intel/2016.2, de-commenter la ligne suivante :
# module load intel/2016.2
 
# Exécution MPMD
mpirun -np 1 -env OMP_NUM_THREADS=1 ./a_mixte.out : -np 4 -env OMP_NUM_THREADS=2 ./b_mixte.out : -np 8  -env OMP_NUM_THREADS=4 ./c_mixte.out

Couplage MPMD avancé de codes hétérogènes

Les couplages présentés ci-dessus sont relativement simples à mettre en place mais ils possèdent deux inconvénients majeurs dès lors que les codes à coupler sont hétérogènes :

  • la même quantité de mémoire (par défaut 3.5 Go par threads) est allouée à tous les exécutables sans possibilité de dépassement et cela même s'il reste de la mémoire non utilisée.
  • le même nombre de cœurs est réservé pour chaque exécutable ce qui cause parfois une surréservation de ressources. Par exemple, pour coupler 16 processus purement MPI avec 8 processus hybrides (MPI/OpenMP) utilisant chacun 2 threads OpenMP, il va être nécessaire de réserver (16 + 8) x 2 = 48 cœurs soit 2 nœuds Ada alors qu'un seul nœud aurait pu suffire en théorie (16 + 8 x 2 = 32 cœurs).

Afin de contourner ses limitations, nous offrons (sur demande uniquement) la possibilité d'accéder à un mode MPMD avancé permettant de réserver des nœuds complets d'Ada. L'utilisateur est ensuite responsable

  • de la répartition de ses processus sur les différents nœuds qui lui sont affectés
  • du placement des processus (et des threads éventuels) sur les cœurs de chaque nœud.

Dans ce mode d'exécution, l'intégralité de la mémoire du nœud (128 Go sauf pour les nœuds “grosse mémoire” qui possèdent 256 Go de mémoire vive) est disponible sans qu'aucune restriction ne soit appliquée par processus. Il est de la responsabilité de l'utilisateur de s'assurer que la somme de la mémoire utilisée par chaque processus est inférieure à la quantité de mémoire disponible sur le nœud.

Les demandes d'accès sont à adresser à l'équipe assistance de l'IDRIS ().

Réservation des nœuds

L'instruction LoadLeveler # @ node = N permet de réserver N nœuds complets. La variable d'environnement LOADL_PROCESSOR_LIST définie par LoadLeveler permet de connaitre la liste des nœuds ayant été affectés au travail en cours.

Pour réserver des nœuds “grosse mémoire”, il est nécessaire d'ajouter l'instruction # @ requirements = (Memory > 200000) dans le script de soumission.

Lancement des processus

Le lancement des processus se fait via la construction d'un fichier de configuration qui va ensuite être utilisé par la commande mpirun. Dans ce cas, la seule et unique option de la commande mpirun doit être -configfile chemin/vers/le/fichier/de/config.

Chaque ligne du fichier de configuration décrit l'exécution d'un exécutable sur un nœud bien défini :

  • l'option -host nom_de_nœud permet de définir sur quel nœud l'exécutable doit être lancé
  • l'option -n M permet de définir le nombre M de processus MPI à démarrer sur le nœud choisi pour cet exécutable.

L'épinglage des processus à un (code MPI pur) ou plusieurs (code hybride) cœurs se fait via l'utilisation de la variable d'environnement I_MPI_PIN_DOMAIN. Cette variable d'environnement peut être définie pour tous les nœuds via l'utilisation d'un export préalable à l'appel à mpirun ou indépendamment pour chaque nœud en utilisant l'option -env de mpirun (il n'est pas nécessaire de répéter cette option pour tous les exécutables lancés sur un même nœud).

La variable d'environnement I_MPI_PIN_DOMAIN est un tableau de la forme [Mask1, Mask2, …, MaskP] où :

  • P est le nombre total de processus MPI lancés sur le nœud
  • MaskI est un masque binaire hexadécimal qui définit l'ensemble de cœurs auxquels le I-ième processus MPI du nœud est épinglé : le j-ième cœur fait partie du I-ième ensemble si le j-ième bit de MaskI est positionné à 1. Le nombre de bits dont la valeur est 1 définit le nombre de threads OpenMP utilisés dans le cas d'un code hybride. Par exemple, si le masque vaut A soit 1010 en écriture binaire alors l'exécutable sera attaché aux cœurs 1 et 3.

Attention, la variable I_MPI_PIN_DOMAIN adresse des cœurs logiques et non physiques. Il est donc possible d'adresser 64 cœurs logiques sur chaque nœud de calcul possédant 32 cœurs physiques puisque l'HyperThreading est activé sur la machine Ada.

Exemples de script de soumission

  • Exemple utilisant des répartitions différentes des exécutables sur les nœuds affectés au travail :
intel_mpmd_avance.ll
# @ job_name = intel_mpmd
# @ output   = $(job_name).$(jobid)
# @ error    = $(output)
# @ job_type = mpich
# @ node = 2
# @ wall_clock_limit = 0:30:00
# @ queue
 
# Recommandation : compilez et exécutez vos codes sous un même environnement Intel.
# Donc, si necessaire, utilisez la commande module pour charger l'environnement approprie.
# Par exemple, si votre code est compile avec Intel/2016.2, de-commenter la ligne suivante :
# module load intel/2016.2
 
# Construction d'un tableau contenant le nom des différents nœuds
IFS=', ' read -r -a noeuds <<< "$LOADL_PROCESSOR_LIST"
 
# Création du fichier de configuration pour lancer :
# - 46 processus MPI pur "a.out" : 31 sur le premier nœud (cœurs 0 à 30) et 15 sur le deuxième nœud (cœurs 0 à 14)
# - 2 processus MPI pur "b.out" : 1 sur le premier nœud (cœur 31) et 1 sur le deuxième nœud (cœur 15)
# - 2 processus MPI hybrides "c_mixte.out" sur le deuxième nœud avec chacun 8 threads OpenMP (respectivement sur les cœurs 16 à 23 et 24 à 31)
cat <<EOF > $TMPDIR/configfile
-host ${noeuds[0]} -n 31 -env I_MPI_PIN_DOMAIN=[1,2,4,8,10,20,40,80,100,200,400,800,1000,2000,4000,8000,10000,20000,40000,80000,100000,200000,400000,800000,1000000,2000000,4000000,8000000,10000000,20000000,40000000,80000000] ./a.out
-host ${noeuds[0]} -n 1 ./b.out
-host ${noeuds[1]} -n 15 -env I_MPI_PIN_DOMAIN=[1,2,4,8,10,20,40,80,100,200,400,800,1000,2000,4000,8000,FF0000,FF000000] ./a.out
-host ${noeuds[1]} -n 1 ./b.out
-host ${noeuds[1]} -n 2 ./c_mixte.out
EOF
 
mpirun -configfile $TMPDIR/configfile
  • Exemple simple utilisant la même répartition des exécutables sur tous les nœuds affectés au travail :
intel_mpmd_avance.ll
# @ job_name = intel_mpmd
# @ output   = $(job_name).$(jobid)
# @ error    = $(output)
# @ job_type = mpich
# @ node = 2
# @ wall_clock_limit = 0:30:00
# @ queue
 
# Création du fichier de configuration pour lancer :
# - 2 processus MPI hybrides "a_mixte.out",
# - 1 processus MPI pur "b.out",
# - 1 processus MPI pur "c.out"
# par nœud de calcul.
 for host in $LOADL_PROCESSOR_LIST
 do
 	echo "-host $host -n 2 ./a_mixte.out" >> $TMPDIR/configfile
 	echo "-host $host -n 1 ./b.out" >> $TMPDIR/configfile
 	echo "-host $host -n 1 ./c.out" >> $TMPDIR/configfile
 done
 
# On utilise le même placement pour tous les nœuds de calcul :
# - le premier processus "a_mixte.out" du nœud possède 2 threads attachés aux cœurs 0 et 2 (0x5 = 101 en binaire)
# - le deuxième processus "a_mixte.out" du nœud possède 2 threads attachés aux cœurs 1 et 3 (0xA = 1010 en binaire)
# - le processus "b.out" du nœud est attaché au cœur 4 (0x10 = 10000 en binaire)
# - le processus "c.out" du nœud est attaché au cœur 5 (0x20 = 100000 en binaire)
export I_MPI_PIN_DOMAIN=[5,A,10,20]
 
mpirun -configfile $TMPDIR/configfile