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 parallèle de calcul 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, enchaînée avec une deuxième étape parallèle, elle-même enchaînée avec une dernière étape séquentielle, tout cela au sein d'un travail LoadLeveler unique. Les ressources qui, sinon, auraient été réservées mais non 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. De plus, il n'y pas d'héritage des caractéristiques d'une étape à l'autre (nombre de cœurs, mémoire, temps) : au début de chaque nouvelle étape, on hérite des limites par défaut de la classe dans laquelle on s'exécute.
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).
en attendant la résolution par IBM d'un bogue dans LoadLeveler, nous avons dû mettre en place un contournement qui pénalise l'exécution d'un job MPI précédé d'une exécution monoprocesseur (surcoût de 20 à 75%). Aussi, nous vous recommandons d'éviter ce type de job pour l'instant, au profit de l'exemple 2, qui fonctionne sans aucune pénalité. Ce deuxième exemple a pour caractéristique d'exclure toute phase de calcul dans la première étape séquentielle de type archive.
Vous pouvez contacter l'Assistance pour plus de détails, ou si vous avez un besoin urgent d'enchaînement de calcul mono et multiprocesseur.
Dans ce premier exemple, nous allons enchaîner la compilation d'un programme MPI dans une classe séquentielle, l'exécution de ce programme MPI sur 128 processus, puis la compilation et l'exécution d'un code séquentiel de post-traitement des résultats. Le fichier de soumission, appelons le multi-steps.ll, est le suivant :
#=========== 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 # @ data_limit = 2gb # @ queue #=========== Step 2 directives =========== #============= Parallel step ============= # @ step_name = parallel_step # @ dependency = (sequential_preprocessing == 0) # (executed only if previous step 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 previous step 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/coucou_MPI.f90 . mpxlf_r coucou_MPI.f90 -o coucou_MPI.exe ;; #============ Step 2 commands ============ #============= Parallel step ============= parallel_step ) set -x cd $TMPDIR ./coucou_MPI.exe ;; #============ Step 3 commands ============ #======= Sequential postprocessing ======= sequential_postprocessing ) set -x cd $TMPDIR cp $LOADL_STEP_INITDIR/coucou_post.f90 . xlf_r coucou_post.f90 -o coucou_post.exe ./coucou_post.exe *.dat ;; 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).
Dans ce deuxième exemple, la première étape consiste à recopier des fichiers depuis Gaya 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 Gaya. Les étapes 1 et 3, qui correspondent à des transferts de données depuis/vers Gaya 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 :
#=========== 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 # @ queue #=========== Step 2 directives =========== #============= Parallel step ============= # @ step_name = parallel_step # @ dependency = (get_file == 0) # (executed only if previous step 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 previous step completed with an error) # @ job_type = serial # @ class = archive # @ 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 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) , la deuxième étape ne s'exécutera que si les deux commandes mfget se sont correctement exécutées; il en va de même pour la dépendance dans la troisième étape. Si la commande set -ex n'est pas utilisée, alors le test de dépendance de l'étape en cours se fait uniquement sur le code de retour de la dernière commande de l'étape précédente.
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 :
#=========== 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.exe . ./code.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.exe venait à dépasser le temps Elapsed fixé à 3600 s, alors la première étape serait interrompue, mais les fichiers créés lors de cette première étape dans le TMPDIR ne seraient pas effacés pour autant grâce à la rémanence; il est dès lors possible de les recopier dans l'étape suivante sur le WORKDIR, espace disque permanent.
De façon générale, un batch doit être lancé à partir d'un espace disque permanent, sinon sa sortie serait donc 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 TMPDIR temporaire 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 .
> ls prog1 prog2 script1.ll script2.ll
> more script1.ll # Temps Elapsed en sec. par processus # @ wall_clock_limit = 400 # Taille memoire de la zone DATA ici 3 Go # @ data_limit = 3Gb # 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 dans le repertoire #de soumission du premier script nomme script1.ll cd $LOADL_STEP_INITDIR llsubmit script2.ll > more script2.ll # Temps Elapsed en s. par processus # @ wall_clock_limit = 400 # Taille memoire de la zone DATA ici 3 Go # @ data_limit = 3Gb # 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