Ada, Adapp : exécution de travaux multi-étapes et en cascade

Utilisation de la notion d'étape de LoadLeveler

Certains utilisateurs ont développé des chaînes de traitement complexes (flux de données) qui consistent à enchaîner des travaux dont les caractéristiques sont différentes (nombre de cœurs, temps et mémoire nécessaires). Les fichiers de sortie d'un travail sont souvent utilisés comme fichiers d'entrée du travail suivant, ce qui ajoute des relations d'interdépendance entre les travaux. LoadLeveler permet de gérer cette problématique d'une façon simple et efficace en utilisant la notion d'étape (step) et de travail multi-étapes (multi-steps). Chaque étape est définie dans un sous-travail à exécuter auquel sont associées des ressources propres (cœurs, mémoire, temps, donc une classe). Un travail multi-étapes consistera à définir autant d'étapes que de sous-travaux à exécuter, ainsi que les relations d'interdépendance entre ces étapes : de cette façon, à chaque étape, les ressources réservées correspondent exactement aux ressources utilisées.

Prenons un exemple concret : un utilisateur a besoin de lancer un premier travail séquentiel (compilation par exemple), puis un second travail de calcul en parallèle et enfin un troisième travail séquentiel (post-traitement des fichiers générés précédemment). Il serait dommage de réserver les cœurs nécessaires à l'exécution parallèle pour l'ensemble des travaux : cela pénaliserait non seulement la bonne exploitation de la machine, mais aussi l'utilisateur lui-même, qui serait facturé du nombre de cœurs réservés multiplié par le temps Elapsed de l'ensemble des travaux … Avec un travail multi-étapes, il suffit de spécifier une première étape séquentielle, suivie d'une deuxième étape parallèle, elle-même suivie d'une dernière étape séquentielle, tout cela au sein d'un travail LoadLeveler unique. Les ressources réservées qui, sinon, n'auraient pas été utilisées ont disparu, elles ne sont donc plus facturées (exemple 1).

Dans un travail multi-étapes, le répertoire TMPDIR est rémanent : le contenu de ce répertoire est conservé entre les différentes étapes du travail. ATTENTION, le tmpdir est effacé lorsque la dernière étape est terminée et que s'il n'y a pas de “dependency” entre les étapes, la dernière peut se finir avant les autres et le tmpdir est perdu… De plus, il n'y pas d'héritage des caractéristiques d'une étape à l'autre (nombre de cœurs, mémoire, temps Elapsed) : au début de chaque nouvelle étape, on hérite des limites par défaut de la classe dans laquelle on s'exécute.

Remarques :

  • Si l'étape séquentielle ne consiste qu'à réaliser des transferts de fichiers depuis ou vers Ergon avec les commandes mfget / mfput, alors il faut utiliser la classe dédiée  archive  créé spécifiquement pour cela. L'aiguillage vers cette classe se fait avec l'utilisation du mot-clef LoadLeveler  # @ class = archive  dans le script de soumission. Il est recommandé de conserver les valeurs des limites par défaut de la classe archive : en particulier, aucune limite de temps n'est à spécifier. On hérite alors par défaut des limites maximum de la classe (30 minutes en temps CPU et 20 heures en temps Elapsed); grâce à la sécurisation des commandes mfput / mfget, on s'affranchit de tout problème ponctuel sur Ergon. L'utilisation de la classe archive n'est pas facturée aux utilisateurs; un suivi permanent est mis en place pour éviter toute dérive dans son utilisation (exemple 2).
  • Dans tous les autres cas, il suffit d'utiliser les classes standard disponibles.

Le concept de travail multi-étapes peut également se révéler utile pour récupérer les fichiers générés dans le TMPDIR lors d'une exécution susceptible de dépasser les limites en temps Elapsed, donc d'être interrompue brutalement. Dans un travail classique constitué d'une étape unique, les fichiers sont effacés avec le TMPDIR dès que le travail est tué : ils sont définitivement perdus. Une façon de remédier à ce problème est de créer un travail constitué de deux étapes : une première étape associée à l'exécution du code, puis une seconde étape inconditionnelle (exécutée que l'étape précédente se soit bien déroulée ou non) qui aura la charge de recopier des fichiers vers un espace disque permanent (HOME ou WORKDIR). Le TMPDIR étant rémanent dans un job multi-étapes, les fichiers créés lors de la première étape seront toujours accessibles dans la seconde (exemple 3).

Exemple 1 : Travail multi-étapes standard

Dans ce premier exemple, nous allons enchaîner l'exécution d'un programme séquentiel (dans une classe séquentielle), l'exécution d'un programme MPI sur 128 processus (dans une classe parallèle), puis l'exécution d'un code séquentiel de post-traitement des résultats (dans une classe séquentielle). Le fichier de soumission, appelons le multi-steps.ll, est le suivant :

multi-steps.ll
#=========== Global directives ===========
# @ job_name = multi-steps
# @ output = $(job_name).$(step_name).$(jobid)
# @ error = $(output)
 
#=========== Step 1 directives ===========
#======= Sequential preprocessing ========
# @ step_name = sequential_preprocessing
# @ job_type = serial
# @ wall_clock_limit = 600
# @ queue
 
#=========== Step 2 directives ===========
#============= Parallel step =============
# @ step_name = parallel_step
# @ dependency = (sequential_preprocessing == 0)
# (executed only if step 1 completed without error)
# @ job_type = parallel
# @ total_tasks = 128
# @ wall_clock_limit = 3600
# @ queue
 
#=========== Step 3 directives ===========
#======= Sequential postprocessing =======
# @ step_name = sequential_postprocessing
# @ dependency = (parallel_step == 0)
# (executed only if step 2 completed without error)
# @ job_type = serial
# @ wall_clock_limit = 1200
# @ queue
 
case ${LOADL_STEP_NAME} in
 
  #============ Step 1 commands ============
  #======= Sequential preprocessing ========
  sequential_preprocessing )
    set -x
    cd $TMPDIR
    cp ${LOADL_STEP_INITDIR}/input.data .
    cp ${LOADL_STEP_INITDIR}/prog_parallel.exe .
    cp ${LOADL_STEP_INITDIR}/preprocess.exe .
    ./preprocess.exe
  ;;
 
  #============ Step 2 commands ============
  #============= Parallel step =============
  parallel_step )
    set -x
    cd $TMPDIR
    poe ./prog_parallel.exe
  ;;
 
  #============ Step 3 commands ============
  #======= Sequential postprocessing =======
  sequential_postprocessing )
    set -x
    cd $TMPDIR
    cp ${LOADL_STEP_INITDIR}/postprocess.exe .
    ./postprocess.exe
    cp output.dat ${LOADL_STEP_INITDIR}/.
  ;;
 
esac

Ce travail est organisé en deux sections : la première contient l'ensemble des directives destinées au gestionnaire de files d'attente LoadLeveler, tandis que la deuxième rassemble les commandes batch à exécuter par les différentes étapes. Les deux parties peuvent être mélangées, mais cela nuit à la lisibilité, donc à la compréhension du travail.

Les directives LoadLeveler de chaque étape doivent se terminer par une directive  # @ queue. D'autre part il est indispensable de spécifier des directives de dépendance si l'on veut s'assurer que les différentes étapes s'exécutent bien les unes après les autres : sans ces directives, toutes les étapes démarrent indépendamment dès que les ressources nécessaires sont disponibles. Dans cet exemple, la directive # @ dependency des étapes 2 et 3 indique que ces étapes ne doivent s'exécuter que si l'étape précédente s'est terminée sans erreur (valeur de retour égale à zéro).

La soumission de ce travail crée en réalité trois sous-travaux contenant le même script shell, mais qui utilisent des ressources (cœurs, mémoire, temps) différentes; ces sous-travaux ne se distinguent que par leur nom d'étape. Pour qu'ils exécutent chacun des commandes différentes, il est nécessaire d'effectuer un branchement spécifique pour chaque étape, en utilisant leur nom d'étape (stocké dans la variable LOADL_STEP_NAME) dans une structure case. L'utilisation du case est simple, il suffit de vous inspirer de l'exemple fourni; cependant n'oubliez pas d'ajouter un ;; (double point-virgule) pour séparer chaque branche et un esac à la fin.

Pour soumettre ce travail à trois étapes, placez-vous dans le répertoire contenant multi-steps.ll et tapez :

$ llsubmit multi-steps.ll

Notez que, lors du démarrage de chaque étape, le répertoire par défaut est celui d'où a été soumis le travail ; c'est là que seront écrits les fichiers de sortie de LoadLeveler. D'autre part, il est indispensable de spécifier un fichier de sortie différent pour chaque étape, faute de quoi la sortie de la dernière étape écrase les sorties des précédentes (ligne output dans le fichier de soumission).

Exemple 2 : Travail multi-étapes pour le transfert de fichiers avec Ergon

Dans ce deuxième exemple, la première étape consiste à recopier des fichiers depuis Ergon dans le répertoire d'exécution, la seconde à utiliser ces fichiers pour l'exécution parallèle d'un code sur 32 processus, enfin la troisième archive le fichier résultat sur Ergon. Les étapes 1 et 3, qui correspondent à des transferts de données depuis/vers Ergon avec les commandes mfget / mfput se font dans la classe dédiée archive. Le fichier de soumission, appelons le multi-steps-archive.ll, est le suivant :

multi-steps-archive.ll
#=========== Global directives ===========
# @ job_name = multi-steps-archive
# @ output = $(job_name).$(step_name).$(jobid)
# @ error = $(output)
 
#=========== Step 1 directives ===========
#============== get_file =================
# @ step_name = get_file
# @ job_type = serial
# @ class = archive
# (don't specify wall_clock_limit with this class)
# @ queue
 
#=========== Step 2 directives ===========
#============= Parallel step =============
# @ step_name = parallel_step
# @ dependency = (get_file == 0)
# (executed only if step 1 completed without error)
# @ job_type = parallel
# @ total_tasks = 32
# @ wall_clock_limit = 3600
# @ queue
 
#=========== Step 3 directives ===========
#======= Sequential postprocessing =======
# @ step_name = put_file
# @ dependency = (parallel_step >= 0) && (get_file == 0)
# (executed even if step 2 completed with an error but only if step 1 is successful)
# @ job_type = serial
# @ class = archive
# (don't specify wall_clock_limit with this class)
# @ queue
 
case $LOADL_STEP_NAME in
 
  #============ Step 1 commands ============
  #======= Sequential preprocessing ========
  get_file )
    set -ex
    cd $TMPDIR
    mfget dataset1.in dataset.in
    mfget dataset2.in dataset_bis.in
  ;;
 
  #============ Step 2 commands ============
  #============= Parallel step =============
  parallel_step )
    set -x
    cd $TMPDIR
    cp ${LOADL_STEP_INITDIR}/code_MPI.exe .
    ls -al
    ./code_MPI.exe
  ;;
 
  #============ Step 3 commands ============
  #======= Sequential postprocessing =======
  put_file )
    set -x
    cd $TMPDIR
    mfput dataresult.out result.out
  ;;
 
esac

Notez l'utilisation de la commande set -ex (Bash shell) dans la première étape. Elle permet d'interrompre l'étape en cours dès qu'une commande renvoie une erreur (code de retour différent de zéro).
Ainsi, en prenant en compte la relation de dépendance # @ dependency = (get_file == 0) dans la deuxième étape, la step 2 ne s'exécutera que si les deux commandes mfget se sont correctement exécutées. Si la commande set -ex n'est pas utilisée, alors le test de dépendance de l'étape 2 se fait uniquement sur le code de retour de la dernière commande de l'étape 1.
Pour la troisième étape, la dépendance choisie est telle que l'étape s'exécutera même en cas d'erreur lors de la seconde étape (cela peut être très utile pour récupérer des résultats partiels même après un crash de l'application ou un dépassement de temps) mais uniquement si la première étape s'est bien terminée (dans le cas contraire, il n'y a pas d'intérêt à exécuter cette troisième étape).

Exemple 3 : Travail multi-étapes pour gérer la recopie inconditionnelle des fichiers résultats en fin d'exécution

Ce troisième exemple va utiliser la propriété spécifique de rémanence du TMPDIR dans les travaux multi-étapes pour recopier dans un système de fichiers permanents les fichiers créés lors d'une première étape exécutée dans le TMPDIR (temporaire). Cette recopie se fera toujours, que l'étape précédente se soit déroulée correctement ou non, comme par exemple lors d'un arrêt brutal pour dépassement de la limite en temps Elapsed. Le fichier de soumission, appelons le multi-steps-copy.ll, est le suivant :

multi-steps-copy.ll
#=========== Global directives ===========
# @ job_name = multi-steps-copy
# @ output = $(job_name).$(step_name).$(jobid)
# @ error = $(output)
 
#=========== Step 1 directives ===========
#============= Execution step =============
# @ step_name = execution_step
# @ job_type = serial
# @ parallel_threads = 8
# @ wall_clock_limit = 3600
# @ queue
 
#=========== Step 2 directives ===========
#=============== Copy step ===============
# @ step_name = copy_file
# @ dependency = (execution_step >= 0)
# (executed even if previous step completed with an error)
# @ job_type = serial
# @ queue
 
case ${LOADL_STEP_NAME} in
  #============ Step 1 commands ============
  #============= Execution step =============
  execution_step )
    set -x
    cd $TMPDIR
    cp ${LOADL_STEP_INITDIR}/code_OpenMP.exe .
    ./code_OpenMP.exe
  ;;
 
  #============ Step 2 commands ============
  #=============== Copy step ===============
  copy_file )
    set -x
    cd $TMPDIR
    cp *.res ${LOADL_STEP_INITDIR}/Output
  ;;
 
esac

Dans cet exemple, si dans l'étape 1 l'exécution de code_OpenMP.exe venait à dépasser le temps Elapsed fixé à 3600 s, alors la première étape serait interrompue, mais les fichiers (créés dans le TMPDIR) ne seraient pas effacés pour autant grâce à la rémanence du TMPDIR dans ce cas. Il est dès lors possible de les recopier dans l'étape suivante sur un espace disque permanent comme le WORKDIR.

Lancement d'un travail LoadLeveler par un autre travail LoadLeveler

De façon générale, un batch doit être lancé à partir d'un espace disque permanent, sinon sa sortie serait perdue faute de pouvoir être écrite à l'endroit où la soumission a été faite. Or, dans tout travail LoadLeveler, il est recommandé de se positionner dans l'espace disque temporaire TMPDIR pour exécuter les binaires (pour des raisons de performance). De ce fait, si un tel travail désire en soumettre un autre, il sera nécessaire de changer d'espace disque au préalable : une solution consiste à soumettre l'ensemble des travaux de la chaîne à partir du lieu de soumission initial du premier travail, que l'on peut récupérer via la variable LOADL_STEP_INITDIR .

Exemple avec deux scripts de soumission permettant d'exécuter deux binaires.

$ ls
  prog1 prog2 script1.ll script2.ll
script1.ll
# Temps Elapsed en sec. par processus
# @ wall_clock_limit = 400
# Nom du travail LoadLeveler
# @ job_name = Sortie1
# Fichier de sortie standard du travail
# @ output = $(job_name).$(jobid)
# Fichier de sortie d'erreur du travail
# @ error = $(job_name).$(jobid)
# @ queue
 
set -x
cd $TMPDIR
cp ${LOADL_STEP_INITDIR}/prog1 .
./prog1 > resultat1
cp resultat1 ${LOADL_STEP_INITDIR}
 
# Lancement du deuxieme script qui se trouve
# aussi dans le repertoire de soumission
cd ${LOADL_STEP_INITDIR}
llsubmit script2.ll
script2.ll
# Temps Elapsed en s. par processus
# @ wall_clock_limit = 400
# Nom du travail LoadLeveler
# @ job_name = Sortie2
# Fichier de sortie standard du travail
# @ output = $(job_name).$(jobid)
# Fichier de sortie d'erreur du travail
# @ error = $(job_name).$(jobid)
# @ queue
 
set -x
cd $TMPDIR
cp ${LOADL_STEP_INITDIR}/prog2 .
./prog2 > resultat2
cp resultat2 ${LOADL_STEP_INITDIR}

Pour soumettre l'ensemble :

$ llsubmit script1.ll