Gestion des processus et des jobs

Mesures et performances : le temps et l espace

Les deux critères principaux à prendre en compte dans l'évaluation de la consommation de ressources ou de la performance d'un programme sont le temps et l'espace c'est à dire le temps d'exécution et la mémoire vive utilisée.

En fonction du programme que l'on veut utiliser et des données, l'utilisation de la mémoire vive peut aller de quelques Ko à plusieurs Go. Cette consommation est très souvent prédictible et dépendante de la taille des données en entrée du programme.

Le temps d'exécution est fortement variable et dépend de la charge cpu provoquée par les autres processus sur la machine.

En général, quand on cherche à diminuer son temps d'exécution (par changement d'algorithme ou optimisation de paramètres), on augmente sa consommation de mémoire vive et inversement : si on cherche à diminuer sa consommation de mémoire vive, on augmente son temps d'exécution.

mesures de temps avec time

time est une commande qui prend en paramètre une autre commande pour mesurer son temps d'exécution.

time sleep 5

real    0m5.029s
user    0m0.001s
sys     0m0.003s

On peut observer 3 résultats à la commande time :

  • real : La commande "sleep 5" a mis 5 secondes avant de rendre la main à l'utilisateur
  • user : La commande "sleep 5" a utilisé le cpu pendant 1 ms
  • sys : Les appels systèmes effectués par la commande "sleep 5" ont duré au total 3 ms

ps / top / htop pour mesurer la mémoire

Avec ces outils, on peut voir en temps réel l'utilisation mémoire d'un processus. C'est assez peu pratique pour voir l'évolution globale de cette valeur dans le temps.

études de binaire avec l outil memcheck de valgrind

Si on programme dans un langage compilé et on ajoute l'option -g à la compilation, on peut utiliser valgrind en mode analyseur de mémoire.

valgrind --tool=memcheck ./prog

va produire un rapport montrant les erreurs d'affectation/allocation mémoire. On peut y ajouter de nombreuses options comme la recherche de zone mémoires non libérées et non référencées (fuites mémoire ou memory leaks).

exemple et exercice

Soit le programme C++ suivant :

#include <iostream>

using namespace std;

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

    // on fixe le nombre d'iterations
    int max_iter = 100;
    if(argc > 1){
        max_iter = atoi(argv[1]);
    }   
    cout << max_iter << "iterations vont etre effectuees\n";

    // on declare un pointeur sur une chaine
    string * s;
    // dans chaque iteration on lui affecte une chaine
    for (int i=0; i<max_iter; i++){
        s = new string("superchaine"+std::to_string(i));
        cout << "iteration no " << i << " : " << *s << "\n";
    }   

    // on libere la memoire pointee par le pointeur s
    delete s;
    return 0;
}

Vous pouvez le compiler à l'aide de la commande suivante :

g++ test_valgrind.cpp -o test_valgrind -std=c++11

puis regardez la consommation mémoire de plusieurs exécutions avec des paramètres différents :

valgrind --tool=memcheck ./test_valgrind 20
valgrind --tool=memcheck ./test_valgrind 200
valgrind --tool=memcheck ./test_valgrind 1000

Ce programme libère-t-il correctement la mémoire ? Peut-on l'exécuter avec un très grand paramètre ?

Que peut-on changer dans ce programme pour éliminer les fuites de la mémoire sans affecter le comportement du programme ?

REPONSES : Plus le nombre d'itération est grand, plus la mémoire perdue détectée par valgrind est grande. Ce programme ne libère pas correctement la mémoire et perd même les références vers la mémoire allouée (quand on fait le "s = new string").

On ne peut pas l'exécuter avec un très grand paramètre puisqu'à partir d'une certaine valeur, on aura rempli la mémoire vive avant la fin de l'exécution.

Pour corriger ce programme, il faudrait libérer la mémoire au moment où on en a plus besoin. Dans notre cas, on souhaite simplement afficher la string. Après chaque affichage on pourrait libérer la string avant d'écraser le pointeur par une nouvelle string.

#include <iostream>

using namespace std;

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

    // on fixe le nombre d'iterations
    int max_iter = 100;
    if(argc > 1){
        max_iter = atoi(argv[1]);
    }   
    cout << max_iter << "iterations vont etre effectuees\n";

    // on declare un pointeur sur une chaine
    string * s;
    // dans chaque iteration on lui affecte une chaine
    for (int i=0; i<max_iter; i++){
        s = new string("superchaine"+std::to_string(i));
        cout << "iteration no " << i << " : " << *s << "\n";
        // on libere la memoire pointee par le pointeur s
        delete s;
    }   

    return 0;
}

La parallélisation

La parallélisation est l'action de rendre une exécution, un traitement, une tâche parallèle, c'est à dire de la découper en plusieurs morceaux capables de fonctionner en même temps sans se perturber ou se nuire.

Une petite introduction théorique

Un processeur (cpu) monocoeur n'est capable d'exécuter qu'une seule opération à la fois. On appelle cela exécution séquentielle. Une machine disposant de ce type de processeur peut tout de même exécuter plusieurs processus à la fois grâce au système d'exploitation qui partage l'utilisation du processeur entre les processus. Ainsi on a l'impression que les traitements sont effectués en même temps mais en réalité, le cpu est alloué à un seul processus à un instant T. Ce partage temporel de cpu induit une augmentation du temps d'exécution d'un processus si un autre processus occupe aussi le cpu.

Donc, l'exécution pseudo parallèle de deux processus ne permet pas de réduire le temps total d'exécution.

Depuis quelques années, les processeurs sont multicoeurs, ce qui signifie qu'il disposent de plusieurs unités de calcul indépendantes. Une machine pourvue de ce type de processeur peut donc réellement exécuter plusieurs processus en même temps. Il auront tous accès à la mémoire et éventuellement à des espaces partagés.

Pourquoi n'augmente-t-on pas simplement la fréquence des processeurs ? Les processeurs évolue lentement. Ces dernières années ils n'ont pas gagné beaucoup de Hz. Lorsqu'on arrive à paralléliser efficacement un algorithme qui s'applique à des grosses données, on est sur de pouvoir diviser son temps d'exécution par le nombre de coeurs ou de machines qui vont le faire fonctionner. Ce gain est bien plus intéressant qu'une augmentation de 10% de vitesse sur un processeur mono coeur.

La parallélisation par les données

principe

La parallélisation par les données est la manière la plus simple de paralléliser un traitement. Le principe de cette technique est de lancer plusieurs fois le programme, créant ainsi plusieurs processus, pour traiter des sous parties des données. Une fois toutes les sous parties des données traitées, on rassemble les résultats pour obtenir le même résultat qu'aurait produit une exécution simple non parallèle.

Cette technique ne peut pas être employée dans tous les cas de figure car elle dépend de certaines conditions :

  • on doit pouvoir découper les données ou au moins être capable d'en traiter seulement une sous partie
  • on doit pouvoir assembler les résultats
  • l'assemblage des résultats "intermédiaires" doit être identique ou équivalent au résultat qu'aurait produit une exécution séquentielle non parallèle.

problèmes liés : Si le traitement effectué passe la majorité de son temps en lecture/écriture sur un disque dur ou une mémoire lente, la parallélisation par les données va générer une forte concurrence au niveau des accès sur cette mémoire ce qui risque de ne pas être très bénéfique sur le temps d'exécution global du traitement, voire même qui pourra augmenter le temps de calcul.

avantages de la parallélisation par les données : elle est parfaitement adaptée à l'utilisation d'un cluster de calcul puisqu'elle ne nécessite pas que les processus soient exécutés sur une seule machine. On peut donc les distribuer sur les machines du cluster en prenant soin de les encapsuler dans des tâches autonomes et indépendantes.

exemples

Un exemple simple :

split -l 50 fichier.txt;
for i in `ls x*`
do
  analyser $i &
done

Autre exemple dans le cadre d'un cluster : Voila un script à exécuter sur le noeud maitre qui va soumettre les sous-parties de l'analyse :

split -l 50 fichier.txt;
for i in `ls x*`
do
  qsub -N analyse_$i -cwd -j y -b y analyser $i;
done

Si cette analyse est ralentie par l'accès lent au système de fichier partagé (NFS sur le cluster MBB), on peut faire du file staging, concept abordé plus tard, pour transférer chaque sous-partie des données sur un système de fichier local au noeud de calcul.

On peut également utiliser les array jobs, abordés plus tard, dans le cadre de la parallélisation par les données. On fera ainsi un grand nombre de jobs qui traitent chacun une sous-partie des données.

exercices data //

Pour créer une base de données blast à partir d'un fichier de séquences fasta (qrsh pour avoir une session interactive sur un noeud) :

makeblastdb -in omm.fasta -dbtype nucl

Pour soumettre un blastn sur une base de données blast locale via SGE sur un fichier de séquences fasta :

qsub -cwd -N testblast -b y "blastn -db locale.fasta -num_alignments 1 -outfmt 6 -query fichier.fasta > resublast.txt"

ou en utilisant un script de soumission blastn.sge:

#!/bin/bash
# nom du job:
#$ -N test_blast
#$ -S /bin/bash
# utilise le repertoire courant pour lancer le job
#$ -cwd

fichierquery=$1
blastn -db locale.fasta -num_alignments 3 -outfmt 6 -query $fichierquery > resublast.txt

puis

qsub  blastn.sge test.fasta
  • Tester une des solutions sur le fichier de query test.fasta contre la base omm.fasta

  • Ecrivez une commande qui découpe le fichier test.fasta (obtenu depuis test.fastq) en paquets de 100 séquences (split) avant de soumettre des blastn via SGE. Pensez à gérer les sorties. Si vous n'avez pas le fichier fasta, vous pouvez l'obtenir à partir du fastq avec la commande suivante :

cat test.fastq | awk '{if (NR%4 == 1 || NR%4 == 2) {print $0 ;} }' | sed 's/^@/>/' > test.fasta
# on peut aussi procéder comme ça (getline force la lecture d'une ligne supplémentaire) :
cat test.fastq | awk '{print; getline; print; getline;getline }' | sed 's/^@/>/' > 2

REPONSE : On split le fichier test.fasta. Ensuite on fait un boucle : pour chaque fichier qui commence par "x" dans le dossier courant, on lance un job blastn.

split -a 1 -d -l 200 test.fasta
for i in x*; do
    qsub blastn.sge $i
done
  • Ecrivez un script simple qui soumet 10 jobs qui vont chacun générer un fichier contenant 10000 nombres aléatoires, un nombre par ligne Voila la partie qui génère les nombres aléatoires(*) que vous pouvez mettre dans un script SGE :
for i in `seq 1 10000`; do
    echo $RANDOM >> $JOB_ID.random_numbers
done

REPONSE : Voila le script sge (appelé ensuite random.sge)

#!/bin/bash
#$ -N random_job
#$ -S /bin/bash
#$ -cwd

for i in `seq 1 10000`; do
    echo $RANDOM >> $JOB_ID.random_numbers
done

Puis voila le script de lancement des jobs :

for i in `seq 1 10`; do
    qsub random.sge
done

(*) Concernant la génération de nombres aléatoires, si vous en avez besoin dans des calculs de simulation, je vous invite à lire cette note: http://stackoverflow.com/questions/1194882/generate-random-number

  • Reprenez le script précédent et ajoutez un job qui concatène ensuite les fichiers obtenus en un fichier de résultat final. (indice : utilisez l'option -hold_jid de qsub expliquée dans la première formation ). Vous pouvez donner le même nom à tous vos jobs de génération et donner ce nom à hold_jid. REPONSE : Voila le script de lancement des jobs :
for i in `seq 1 10`; do
    qsub random.sge
done

qsub -N "concatenation" -cwd -hold_jid "random_job" -b y "cat *.random_numbers > random_total.txt"

L'option "-hold_jid random_job" permet de demander à SGE d'attendre que tous les jobs nommés "random_job" se terminent avant de lancer le job "concatenation".

La parallélisation par le code

Beaucoup de programmes sont capables de s'exécuter en plusieurs sous parties parallélisables appelées les threads. Une telle exécution est appelée multithread. Sur un processeur monocoeur, une exécution multithread a peu d'intérêt au niveau performance.

Exemple d'intérêt du multithread sur un processeur monocoeur :

  • firefox peut charger deux onglets en pseudo parallèle
  • on peut lire une vidéo et changer les paramètres de son lecteur vidéo en même temps
  • etc...

Le multithread prend tout son sens sur un processeur multicoeur où plusieurs threads peuvent s'exécuter en même temps, réellement en parallèle.

Toute la difficulté de la programmation multithread est de gérer les accès concurrentiels à la mémoire.

Les 2 problèmes principaux inhérents au multithread au niveau programmation sont :

  • l'interblocage (deadlock) : le thread 1 bloque le verrou 1 et attend que le verrou 2 soit libre pour débloquer le verrou 1. Le thread 2 bloque le verrou 2 et attend que le verrou 1 soit débloqué pour débloquer le verrou 1.
  • la famine (livelock) : un thread tente d'accéder à une ressource (variable ou section critique) mais elle n'est jamais disponible au moment où ce thread est actif.

Nous ne détaillerons pas ici les techniques de programmation multithread.

Cette page donne des points de départ pour l'apprentissage de ces techniques : http://fr.wikipedia.org/wiki/Programmation_concurrente#Probl.C3.A8mes

OpenMP

OpenMP est une norme implémentée par plusieurs compilateurs. Son but est d'automatiser facilement la parallélisation de boucles quand cela est possible.

Le principe d'OpenMP est d'interprêter des pragmas (commentaires écrits au bon endroit par le programmeur) pour transformer certaines boucles séquentielles en boucle parallèles. Il faut que les itérations de la boucle ne soient pas interdépendants pour avoir un réel gain de temps. Dans le pragma, on spécifie quelles variables doivent être rendues locales à chaque itération et lesquelles sont publiques. On peut aussi indiquer des sections critiques dans la boucle pour éviter que plusieurs itérations n'y pénètrent en même temps.

OpenMP est implémenté dans :

  • gcc/g++
  • icc
  • java (avec une librairie supplémentaire)
  • etc...

opemp

exemple :

main ()
{

int i;
float a[N], b[N], c[N], d[N], e[N];

/* Some initializations */
for (i=0; i < N; i++) {
  a[i] = i * 1.5;
  b[i] = i + 22.35;
  }

#pragma omp parallel ...
  {
  #pragma omp sections nowait
    {
    #pragma omp section
    for (i=0; i < N; i++)
      c[i] = a[i] + b[i];

    #pragma omp section
    for (i=0; i < N; i++)
      d[i] = a[i] * b[i];

    #pragma omp section
    for (i=0; i < N; i++)
      e[i] = a[i] - b[i];
    }  /* end of sections */
  }  /* end of parallel section */
}

Plus d'informations : https://computing.llnl.gov/tutorials/openMP/

multithread en CPP/Python

En C++ et Python et tout autre langage objet, on a souvent deux manières principales de faire de la programmation multithread :

pthread ou équivalent : le principe est d'appeler une fonction dans un thread

exemple : int pthread_create(pthread_t thread, const pthread_attr_t attr, void (start_routine) (void ), void arg); Dans la fonction précédente :

  • thread est une référence vers le thread
  • attr sert à configurer le thread
  • start_routine est la fonction à appeler dans le thread
  • arg est la liste de paramètres à passer à start_routine

Object thread : On doit écrire une classe qui dérive de la classe THREAD (différente dans chaque langage) et qui redéfini la méthode "run" (ou autre selon le langage) dans laquelle se trouve les opération à faire dans le thread. Ensuite il suffit d'instancier cette classe et d'appeler la méthode start sur l'instance.

MPI

Parallélisation avec R

Pour des exemples adaptés à l'utilisation sur le cluster : http://kimura.univ-montp2.fr/calcul_isem/2013/11/parallelisation-avec-r-sur-le-cluster/

GNU Parallel

Site officiel: http://www.gnu.org/software/parallel/ Tutoriel officiel: http://www.gnu.org/software/parallel/parallel_tutorial.html Application aux outils bioinformatiques: https://www.biostars.org/p/63816/

GNU parallel est un outil écrit en Perl qui permet de paralléliser l'exécution d'une suite d'instructions en ligne de commande.

exemple de base :

for x in `cat list` ; do
    do_something "$x"
done
process_output

Peut être remplacé par :

cat list | parallel --pipe do_something

exemple de découpage par blocs de 20k suivi de comptage de nombre de séquences:

cat test.fasta | parallel --block 20k  --pipe " grep -c '>' "

exemple de découpage par blocs de 100 lignes suivi de comptage de nombre de séquences:

cat test.fasta | parallel -N 100  --pipe " grep -c '>' "

exemple de découpage par blocs de 100 records délimités pas ">" suivi de comptage de nombre de séquences:

cat test.fasta | parallel -N 100 --recstart '>' --pipe " grep -c '>' "

exemple du blast

blast du fichier test.fasta en entier sur la base de gènes issus d'Orthomam

blastn -db omm.fasta -num_alignments 3 -outfmt 6 -query test.fasta > output.txt

blast après découpage en morceaux de 100 séquences avec respect des limites entres séquences (option recstart)

cat test.fasta | parallel -N 100 --recstart '>' --pipe blastn  -db omm.fasta -num_alignments 3 -outfmt 6 -query - > output.txt

Notez ici que l'on ne se souci pas des formats des séquences (sur une ou plusieurs lignes) ni de gérer la fusion des multiples sorties.

Comment distribuer les différents jobs de 'parallel' sur différents noeuds du cluster :

  • Pour dire à parallel de distribuer des sous tâches sur des noeuds spécifiques :
cat test.fasta | parallel --workdir . -S compute-0-4,compute-0-15 -N 100 --recstart '>' --pipe blastn -db omm.fasta -num_alignments 3 -outfmt 6 -query - > output.txt
  • On peut également utiliser une liste de noeuds depuis un fichier
>cat listenodes
compute-0-4
compute-0-15
compute-0-3

>cat test.fasta | parallel --workdir . --sshloginfile listenodes -N 100 --recstart '>' --pipe blastn -evalue 0.01  -db omm.fasta -num_alignments 3 -outfmt 6 -query - > output.txt

RECAPITULATIF: vue d ensemble // parallélisme

Dans ce schéma, on a un job qui utilise un seul slot. C'est le cas le plus courant. Pas de parallélisme. cluster_form2_monothread

Dans ce schéma, on peut voir un job avec 8 threads qui ne peut s'exécuter que sur un noeud de calcul. cluster_form2_multithread

Dans ce schéma, on a parallélisé notre traitement par les données. On n'est plus limité à un noeud de calcul. On peut donc augmenter la parallélisation en augmentant le nombre de jobs. cluster_form2_para_data

On peut aussi lancer des jobs multithreads dans le cadre de la parallélisation par les données : cluster_form2_para_data_mthread

Dans ce schéma on peut voir qu'un job MPI peut s'exécuter sur plusieurs noeuds à la fois : cluster_form2_mpi

Le file staging

Le file staging, dans le cadre de l'utilisation d'un cluster de calcul, est une technique visant à accélérer les accès aux fichiers de données.

En effet, plusieurs cas de figure peuvent provoquer un accès lent aux données, par exemple :

  • Accès concurrent par plusieurs jobs au même fichier sur le home partagé
  • Grand nombre d'accès rapides à de petites parties d'un gros fichier sur le home partagé

principe

Nous parlerons uniquement de file staging dans le cadre d'un job sur un cluster de type SGE avec des homes partagés et un systèmes de fichier local sur les noeuds accessible en lecture/écriture.

Le principe du file staging est simple. Il consiste à copier les fichiers de données au début d'un JOB sur un système de fichier local au noeud de calcul. Cet espace local peut être choisi par l'utilisateur s'il connait la configuration des noeuds. Il est tout de même préférable d'utiliser l'espace local créé par le gestionnaire de JOB : le dossier temporaire alias TMPDIR. Ce TMPDIR est créé en début d'exécution de job et supprimé en fin de job. Le chemin du TMPDIR est accessible par la variable d'environnement $TMPDIR au sein du job.

Voir cet article pour l'utilisation sur le cluster Mbb : http://kimura.univ-montp2.fr/calcul_isem/2011/05/allegement-cluster-travailler-sur-les-disques-des-noeuds-de-calcul/

exemple de job avec file staging

#!/bin/bash
#$ -S /bin/bash
#$ -cwd
#$ -N mon_job
#$ -j y

# on copie le fichier de données dans le TMPDIR
cp /home/toto/data.fa  $TMPDIR/

# on effectue le traitement sur ce fichier et on écrit le résultat dans son home
process --input $TMPDIR/data.fa --output /home/toto/result.txt

Si l'écriture du fichier de résultat provoque aussi un grand nombre de petits accès disque, on peut aussi l'effectuer dans le TMPDIR et copier ensuite en fin de job le résultat vers le home :

#!/bin/bash
#$ -S /bin/bash
#$ -cwd
#$ -N mon_job
#$ -j y

# on copie le fichier de données dans le TMPDIR
cp /home/toto/data.fa  $TMPDIR/

# on effectue le traitement sur ce fichier et on écrit le résultat dans le TMPDIR
process --input $TMPDIR/data.fa --output $TMPDIR/result.txt

# on copie (rapatrie) le résultat dans son home
cp $TMPDIR/result.txt  /home/toto/

exercice (file staging)

Utiliser le file staging sur la base de données omm.fasta et estimez le gain de performances du blast par rapport à la version sans file staging. (faire plusieurs essais et expliquer les différences de temps obtenus)

REPONSE Voila un script SGE qui fait du file staging :

#!/bin/bash
#$ -S /bin/bash
#$ -cwd
#$ -N file_staging
#$ -j y

# on copie le fichier de données dans le TMPDIR
cp ~/formation/omm.fasta  $TMPDIR/

# on effectue le traitement sur ce fichier et on écrit le résultat dans son home
blastn -db $TMPDIR/omm.fasta -num_alignments 3 -outfmt 6 -query ./test.fasta > ./resublast_file_staging.txt

Pour mesurer l'écart de temps, ajoutez un "time" avant la commande blastn dans le cas avec et sans file staging. Vous pouvez ensuite consulter le résultat de ce "time" dans le fichier de sortie généré par le job. AUTRE REPONSE POSSIBLE : Le file staging classique dans un TMPDIR a ses limites quand le fichier à transférer est très gros. On va passer la majorité de son temps à copier des données sur les noeuds de calcul. On va gaspiller du temps parce qu'on va copier le fichier à chaque job. Pour améliorer cet aspect, on peut copier une fois le fichier sur chaque noeud dans un espace où le fichier va pouvoir persister. Sur le cluster MBB, on peut utiliser /export/scratch .

Les array jobs

Si on doit lancer un grand nombre de fois des jobs presque identiques, on peut utiliser les array jobs qui offrent des avantages par rapport à un lancement "à la main".

Une courte introduction se trouve dans la précédente formation.

https://wiki.duke.edu/display/SCSC/SGE+Array+Jobs

http://wiki.gridengine.info/wiki/index.php/Simple-Job-Array-Howto


exercice (array jobs)

Découper le fichier test.fasta en paquets de 200 lignes (100 séquences)

split -a 1 -d -l 200 test.fasta

Utiliser un array de jobs pour blaster les fichiers (x0, x1 ... x9) sur la base omm.fasta . REPONSE : Voila le script sge :

#!/bin/bash
#$ -N array_blast
#$ -S /bin/bash
#$ -cwd
#$ -t 1-10

((numfichier=$SGE_TASK_ID - 1))
fichierquery=x$numfichier
blastn -db omm.fasta -num_alignments 3 -outfmt 6 -query $fichierquery > resublast_$numfichier.txt

Ensuite n'oubliez pas de concaténer les résultats. On peut le faire avec un job qui attend que l'array job soit terminé :

qsub -N concat_array -cwd -hold_jid array_blast -b y "cat resublast_* > resublast_total.txt"

Les environnements parallèles du cluster

principe

La notion d'environnements parallèles est très importante dans SGE, le système de gestion de jobs du cluster MBB. Un environnement parallèle est un ensemble de paramètres qui définissent des règles de fonctionnement des files d'attente lui appartenant. Du point de vue utilisateur, la partie des environnements parallèles qui nous intéresse est la définition des options d'exécution parallèle utilisée par les jobs MPI ou multithreads.

En bref, à la soumission d'un job sur le cluster, si on ne précise pas d'environnement parallèle, c'est l'environnement par défaut qui est choisi. Celui ci fait en sorte qu'un seul SLOT soit réservé sur le noeud de calcul qui va recevoir le job.

Si on change l'environnement parallèle avec l'option -pe, on peut réserver plusieurs SLOTS et donc être sur que l'on va disposer de plusieurs coeurs sur le noeud de calcul.

ATTENTION : On peut réserver un seul SLOT et lancer un programme qui utilise plusieurs coeurs. La réservation de SLOTS ne restreint pas réellement l'utilisation des coeurs du processeur. Si on réserve moins de SLOTS que de coeurs utilisés, les indications fournies par SGE sont faussées et cela nuit au bon fonctionnement du cluster. Nous vous demandons alors de jouer le jeu et de demander autant de SLOTS que le nombre de coeur que va réellement utiliser votre programme.

stratégies d'attribution des slots et des noeuds

L'utilisateur doit choisir son environnement parallèle en fonction de ses besoins et de son type de job.

En fonction des paramètres qui définissent l'environnement parallèle choisi, SGE va changer sa stratégie d'allocation de ressources. Ces stratégies sont définies par l'administrateur du cluster et sont accessibles dans la documentation associée.

Pour charger le moins possible les noeuds, une stratégie peut être d'allouer en priorité les slots sur les noeuds les moins chargés, ceux qui ont le plus de slots libres par exemple.

Pour un job MPI qui nécessite de grouper les slots demandés au maximum sur les mêmes noeuds, une stratégie peut être de choisir le noeud qui a le plus de slots libre et d'allouer tous les slots dont on a besoin sur ce noeud. Si on a besoin de plus de slots, on peut répéter cette stratégie.

exemples

Exemple de job qui demande 4 SLOTS :

#!/bin/bash
#$ -S /bin/bash
#$ -cwd
#$ -N superjob_4_coeurs
#$ -q cemeb.q
#$ -pe multithread60 4

# lancement d'un programme qui utilise 4 coeurs
prog4cores --data /home/toto/mydata.txt
# lancement d'un programme dont on peut ajuster le nombre de threads
./progNcores --nb-cores 4 --data /home/toto/mydata.txt

On peut demander un nombre variable de SLOTS. Ce qui nous sera effectivement alloué au moment du démarrage du job dépend de la place disponible au moment de l'acceptation du job. IMPORTANT : Dans ce cas on ne connait pas à priori le nombre de SLOTS dont on va disposer pendant l'exécution du job. On peut tout de même connaître ce nombre de SLOTS réellement alloués avec la variable $NSLOTS pendant l'exécution du job.

#!/bin/bash
#$ -S /bin/bash
#$ -cwd
#$ -N superjob_nb_coeurs_variable
#$ -q cemeb.q
#$ -pe multithread60 4-16

# lancement d'un programme dont on peut ajuster le nombre de threads
./progNcores --nb-cores $NSLOTS --data /home/toto/mydata.txt

Dans le cas précédent on a demandé entre 4 et 16 SLOTS. On peut aussi donner un nombre de SLOTS minimum sans préciser de maximum comme 4- qui signifie "au moins quatre".

option signification
-pe multithread60 4 demande 4 slots
-pe multithread60 4-16 demande entre 4 et 16 slots
-pe multithread60 4- demande au moins 4 slots

Voila la liste des environnments parallèles du cluster MBB :

  • mpi
  • mpich
  • multithread60
  • multithread8
  • orte
  • robin
  • thread

exercice (environnements parallèles)

Ensuite lancez un job avec environnement parallèle (le nombre de slots que vous souhaitez) qui effectue un blastn du fichier test.fasta sur la base de gènes omm.fasta (indice : utilisez l'option -num_threads de blastn pour préciser le nombre de coeurs à utiliser).

  • Intégration de GNU parallel avec SGE sur le cluster :

SGE fourni les moyens d’exécuter les jobs parallèles en utilisant un des environnements pré-configurés et adaptés pour la parallélisation à passage de message (mpi) ou à mémoire partagée (openmp …).

L’option à utiliser dans ce cas est –pe suivi du nom de l’environnement parallèle et du nombre de slots désiré.

En ligne de commande : qsub –pe robin 20

Dans un script de soumission : #$ -pe robin 20

Quand le job est soumis avec cette option, SGE lui attribue une liste de noeuds qu’il stocke dans un fichier accessible via la variable d’environnement $PE_HOSTFILE. Ce fichier contient en prmière colonne le nombre de slots et en 2° le nom du noeud. awk '{print $2"/"$1}' $PE_HOSTFILE > listenodes cat test.fasta | parallel --workdir . --sshloginfile listenodes ....

Ecrire le script de soumission SGE qui permet de distribuer le blast du fichier test.fasta sur le cluster par paquets de 100 séquences.

REPONSE

#!/bin/bash
# nom du job:
#$ -N Gnu_parallel_sge
#
# utilise le repertoire courant pour lancer le job
#$ -cwd
#
# utiliser l'environnement parallèle multithread avec 10 slots
#$ -pe multithread 10

#cat $PE_HOSTFILE
awk '{print $2"/"$1}' $PE_HOSTFILE > listenodes

#cat listenodes
cat test.fasta | parallel --workdir . --sshloginfile listenodes -N 100 --recstart '>' --pipe blastn -db omm.fasta -num_alignments 3 -outfmt 6 -query - > output.txt
rm -f listenodes

Si vous êtes arrivé jusque là vous êtes prêt pour charger le cluster avec vos propres traitements !