Snakemake est un moteur de workflow, inspiré de Make, écrit en Python, conçu pour l’automatisation reproductible et traçable de pipelines. Il permet de définir des règles (“rules”) décrivant comment générer des fichiers de sortie à partir de fichiers d’entrée à l’aide de scripts ou de commandes. Le workflow est exprimé dans un fichier Snakefile, en syntaxe Python, et Snakemake construit automatiquement le graphe des dépendances pour exécuter les étapes dans le bon ordre, en parallèle si possible.
Cette page constitue une notice permettant de prendre en main Snakemake et de l’utiliser pour vos projets. Snakemake est toutefois un projet qui possède une communauté très active, et des nouvelles versions proposant de nouvelles fonctionnalités sortent très régulièrement. Plus d’informations, notamment sur les dernières mises à jours, sont disponibles sur https://snakemake.readthedocs.io/en/stable/#.
Papier de référence : <a href=https://doi.org/10.12688/f1000research.29032.1 >Mölder, F., Jablonski, K.P., Letcher, B., Hall, M.B., Tomkins-Tinch, C.H., Sochat, V., Forster, J., Lee, S., Twardziok, S.O., Kanitz, A., Wilm, A., Holtgrewe, M., Rahmann, S., Nahnsen, S., Köster, J., 2021. Sustainable data analysis with Snakemake. F1000Res 10, 33.
Snakemake peut facilement être installé sur votre machine locale ou dans un environnement virtuel (Conda, Venv, Poetry, etc.) grâce à la commande :
pip install snakemake
Si la commande pip n’est pas reconnue, vérifiez que Python est correctement installé sur votre machine ou dans votre environnement virtuel et, pour ce dernier cas, que votre environnement virtuel est correctement activé.
La gestion de workflow par Snakemake est contrôlée par des règles (“rules”).
Une règle est définie (au minimum) par :
shell permettant de générer les sorties à partir des entrées.Une règle est correcte lorsque l’exécution de la commande shell permet de générer les fichiers de sortie spécifiés à partir des fichiers d’entrée spécifiés.
Pour créer une règle, il faut commencer par créer un fichier appelé Snakefile, qui possède une extension .smkpropre à Snakemake, dans votre répertoire de travail (par exemple, vous pouvez créer un fichier my_snakefile.smk à la racine de ce répertoire). Ce fichier va consigner toutes les règles qui vont régir votre workflow. Le Snakefile est basé en langage Python, ce qui permet éventuellement d’y écrire des scripts annexes en langage Python.
La syntaxe d’une règle Snakemake est la suivante :
rule my_rule:
input:
"path/to/my/input/file1",
"path/to/my/input/file2",
...
output:
"path/to/my/output/file1",
"path/to/my/output/file2",
...
shell:
"myshellcommand"
Dans cet exemple de règle my_rule, la commande myshellcommand est déclenchée par l’existence de tous les fichiers précisés dans le champ input et permet de générer les fichiers précisés dans le champ output.
L’absence d’un ou plusieurs fichiers spécifiés dans le champ input empêchera la règle de s’exécuter.
L’absence d’un ou plusieurs fichiers spécifiés dans le champ output après exécution de la commande donnée dans le champ shell provoquera une erreur qui arrêtera l’exécution du workflow? De plus, tous les autres fichiers de sorties générés seront automatiquement supprimés, même s’ils sont valides.
Un workflow va être défini par une ou plusieurs règles Snakemake, toutes consignées dans le Snakefile, qui vont s’enchaîner les unes aux autres jusqu’à ce que les sorties finales soient créées.
Quatre principes sont à retenir impérativement pour concevoir votre workflow à partir de règles :
input existent ;all).Dès lors, en cascadant des entrées et des sorties de règles, il est possible de concevoir un réseau dont Snakemake saura déduire l’ordre d’exécution des règles.
L’ordre d’écriture des règles dans le Snakefile n’a aucune importance : l’ordre réel sera automatiquement déduit à partir des entrées et des sorties de chaque règle. Il faut toutefois penser à créer une règle all ne comprenant qu’un champ input, dans lequel on donne les fichiers finaux générés par le workflow. Cette règle all doit impérativement être placée au début du Snakefile.
Prenons un exemple composé de quatres règles.
Nous disposons d’un répertoire de travail dans lequel est installé Snakemake. Dans ce répertoire, nous trouvons un fichier file1 ainsi qu’un Snakefile nommé ABCD.smk dont le contenu est affiché ci-dessous :
# Régle all : le fichier final généré par ce workflow est file5
rule all:
input:
"file5"
# Copie de file1 en file2
rule A:
input:
"file1"
output:
"file2"
shell:
"cp file1 file2"
# Copie de file2 en file3
rule B:
input:
"file2"
output:
"file3"
shell:
"cp file2 file3"
# Copie de file2 en file4
rule C:
input:
"file2"
output:
"file4"
shell:
"cp file2 file4"
# Création de file5 grâce à l'existence de file3 et file4
rule D:
input:
"file3",
"file4"
output:
"file5"
shell:
"touch file5"
Analysons le Snakefile précédent en parallèle d’une simulation d’exécution du workflow :
file1 déclenchera la règle A, puisque ce fichier constitue la totalité des entrées requises pour cette règle. Aucune autre règle du Snakefile ne requiert file1 en entrée. Le fichier file2 est créé par la commande donnée dans le champ shell de la règle A. Aucune erreur n’est renvoyée, puisque le champ output spécifie bien que seul le fichier file2 sera créé par la règle A.file2 va déclencher les règles B et C. Si plusieurs jobs sont mis à disposition de Snakemake, ces règles pourront être exécutées en parallèle (voir la section sur la gestion des jobs). Sinon, elles seront exécutées séquentiellement. Leur exécution entraîne l’apparition des fichiers file3 et file4.file3 et file4 constituent les entrées de la règle D, cette dernière règle est finalement exécutée et le fichier file5 est créé.file5 n’est une entrée pour aucune règle parmi celles spécifiées dans le Snakefile. Il ne reste plus aucune règle à exécuter. L’exécution générale du workflow s’arrête sans erreur.À partir de ce Snakefile, Snakemake est donc capable de déduire l’ordre des étapes à exécuter, ce qui peut être représenté sous forme de graphe :
flowchart TD
S{Start} -->|file1 exists| A
A -->|cp file1 file2| B
B -->|cp file2 file3| D
A -->|cp file1 file2| C
C -->|cp file2 file4| D
D -->|touch file5| stop{Stop}
Comme nous l’avons vu ci-dessous, l’exécution de l’ensemble du workflow est conditionnée par la bonne génération des sorties de chaque étape, qui servent d’entrées aux étapes suivantes. Mais alors, que faire en cas d’erreur au cours de l’exécution du workflow, à une étape intermédiaire ?
C’est là l’une des forces de Snakemake : si le workflow est interrompu en cours d’exécution, Snakemake est capable d’analyser les fichiers présents dans le répertoire de travail et de relancer le workflow à partir de l’étape interrompue. De plus, si certaines sorties d’étapes déjà exécutées sont modifiées, Snakemake relance le workflow à partir de l’étape la plus en amont de toute modification.
Reprenons le workflow précédent, et imaginons une interruption du workflow en raison d’une erreur pendant l’exécution de la règle C. Comme la règle C échoue, le fichier file4 n’est pas créé et la règle D ne peut pas être lancée, puisque tous ses fichiers d’entrée ne sont pas présents. Le workflow s’arrête.
On trouve l’origine de l’erreur dans l’exécution de la règle C, et on relance le workflow.
Le “raisonnement” de Snakemake sera le suivant :
file1 et file2 existent, et le fichier file2 est plus récent que le fichier file1. La règle A n’est donc pas relancée.file2 et file3 existent, et le fichier file3 est plus récent que le fichier file2. La règle B n’est donc pas relancée.file2 existe, mais le fichier file4 n’existe pas : il faut relancer l’étape C.file4 n’existe pas : la règle D ne peut pas être lancée.De cette analyse, Snakemake déduit qu’il faut relancer le workflow à partir de la règle C, et la règle D sera lancée par la création du fichier file4. Les étapes A et B ne seront pas relancées.
Notons que si le fichier file1 a été modifié après l’exécution interrompue, il devient de fait plus récent que le fichier file2. La règle A et tout le workflow seront alors relancés lors de la prochaine exécution du workflow.
Snakemake permet de faire l’interpolation dans l’écriture des règles, pour les rendres plus génériques. La référence à un champ au sein d’une règle doit être placée {entre accolades}.
# Copie de file1 en file2
rule A:
input:
"file1"
output:
"file2"
shell:
"cp {input} {output}"
L’interpolation ne fonctionne qu’au sein d’une règle. Il n’est pas possible de faire référence aux entrées ou aux sorties d’une autre règle.
Snakemake permet également de faire de l’abstration dans les noms des fichiers, afin d’appliquer une même règle plusieurs fois à différents fichiers.
Prenons un exemple. Voici ci-dessous un Snakefile :
# Nombre de fichiers
nb_files = 4
# Définition de la sortie finale
rule all:
input:
"final_output"
# Création de 'nb_files' fichiers input_file
rule A:
input:
"initial_file"
output:
[expand("input_file{file_id}", file_id=range(1, nb_files+1))]
shell:
"touch {output}"
# Les input_files sont copiés en autant d'output_files
rule B:
input:
"input_file{file_id}"
output:
"output_file{file_id}"
shell:
"cp input_file{wildcards.file_id} output_file{wildcards.file_id}"
# Si tous les output_files sont présents, on génère le fichier final_output
rule C:
input:
[expand("output_file{file_id}", file_id=range(1, nb_files+1))],
output:
"final_output"
shell:
"touch final_output"
Dans cet exemple, on utilise des wildcards dans la règle B pour copier les fichiers input_files en fichiers output_files indexés de la même manière (input_file1 est copié en output_file1, input_file2 est copié en output_file2, etc.). On nomme l’indice file_id dans les champs input et output, et on y fait référence dans le champ shell grâce à wildcards.file_id.
Il peut y avoir plusieurs wildards dans une même règle. Il suffit de les nommer différement et d’y faire référence dans le champ shell par leurs noms respectifs.
La commande expand permet d’aggréger des fichiers et de les paramétrer, comme dans l’exemple ci-dessus.
Comme évoqué précédemment, le Snakefile est un fichier basé en langage Python. Il est donc possible d’écrire dans le Snakefile des fonctions en Python et de les appeler dans les champs des règles (sauf dans le champ shell).
Voici ci-dessous un exemple issu de la documentation officielle de Snakemake (https://snakemake.readthedocs.io/en/stable/snakefiles/rules.html) :
def myfunc(wildcards):
return [... a list of input files depending on given wildcards ...]
rule:
input:
myfunc
output:
"someoutput.{somewildcard}.txt"
shell:
"..."
Plus d’informations sur l’utilisation des fonctions dans le Snakefile sont disponibles dans la documentation officielle de Snakemake.
Une fois le Snakefile rédigé, il convient de savoir lancer une exécution du workflow géré par Snakemake.
Cette exécution peut être réalisée depuis un terminal grâce à la commande snakemake, assortie des options que nous allons détailler ici.
Pour lancer une exécution du workflow avec Snakemake, il faut préciser un Snakefile et des ressources à utiliser (jobs, cœurs, etc.)
Si vous allouez plusieurs jobs à l’exécution de votre workflow, Snakemake se chargera de paralléliser efficacement les étapes qui peuvent l’être.
Lancement du workflow avec 1 coeur :
snakemake -s my_snakefile.smk --cores 1
Lancement du workflow avec 4 jobs :
snakemake -s my_snakefile.smk --jobs 4
Snakemake propose également une commande pour générer un graphe à partir des différentes règles que vous avez précisées dans le Snakefile, sans exécuter le workflow. Ce graphe est renvoyé en langage Graphviz Dot.
Ce graphe peut être généré simplement avec la commande :
snakemake -s my_snakefile.smk --dag
Des cas d’usage peuvent nécessiter d’exécuter un workflow totalement ou partiellement sur un cluster de calcul (par exemple, sur Bigfoot si votre workflow comprend des étapes d’inférence IA).
L’objectif de cette section est d’expliquer comment mettre en place la communication entre votre machine local et un cluster de calcul dans le cadre d’un workflow géré par Snakemake.
Avant de commencer, assurez-vous de disposer d’un espace personnel accessible sur votre cluster de calcul. Vous devez disposer des autorisations nécessaires afin de pouvoir accéder aux infrastructures de calcul. Vérifiez donc vos autorisations et vos configurations SSH avant de passer à la suite. Vous devez également créer un compte Perseus et éventuellement demander l’accès à un projet pour pouvoir poursuivre. Plus d’informations sont disponibles sur https://gricad-doc.univ-grenoble-alpes.fr/hpc/connexion/.
Admettons que vous disposiez d’un workflow décrit par le Snakefile suivant :
# Nombre de fichiers
nb_files = 4
# Définition de la sortie finale (local)
rule all:
input:
"final_output"
# Création de 'nb_files' fichiers input_file (local)
rule preprocessing:
input:
"initial_file"
output:
[expand("input_file{file_id}", file_id=range(1, nb_files+1))]
shell:
"touch {output}"
# Génération d'output_files à partir des input_files via un calcul sur cluster (cluster)
rule cluster_inference:
input:
"input_file{file_id}"
output:
"output_file{file_id}"
shell:
"calcul_cluster.sh" # Inférence sur le cluster avec oarsub
# Si tous les output_files sont présents, on génère le fichier final_output (local)
rule postprocessing:
input:
[expand("output_file{file_id}", file_id=range(1, nb_files+1))],
output:
"final_output"
shell:
"touch final_output"
Le workflow ressemble donc à la figure suivante :
flowchart TD
S{Start} --> PreProcessing
PreProcessing --> ClusterInference
ClusterInference -->PostProcessing
PostProcessing -->stop{Stop}
Dans ce workflow, on souhaite à terme que les étapes preprocessing et postprocessing soient exécutées en local, et que l’étape cluster_inference soit exécutée sur le cluster (avec oarsub).
En l’état, toutes les étapes sont effectuées en local. Nous allons donc adapter notre workflow et notre Snakefile pour pouvoir exécuter la partie cluster_inference sur le cluster de calcul.
La communauté Snakemake est très active et plusieurs packages complémentaires ont été développés. En particulier, le package snakemake-executor-plugin-cluster-generic permet de faciliter l’utilisation de clusters de calcul dans un workflow géré par Snakemake.
Commencez par installer le package dans votre environnement de travail :
pip install snakemake-executor-plugin-cluster-generic
Au début de votre Snakefile, vous devez ajouter une ligne qui va préciser à Snakemake quelles règles devront être exécutées en local. Par défaut, les autres règles seront exécutées sur le cluster.
En s’adaptant à l’exemple ci-dessus, la ligne a ajouter au début de votre Snakefile est la suivante :
workflow._localrules = set(["all", "preprocessing", "postprocessing"])
Grâce à cette ligne, Snakemake comprendra que les règles all, preprocessing et postprocessing doivent être exécutées en local, et que les autres règles (cluster_inference, ici) devront être exécutées sur le cluster.
Nous allons encadrer notre étape cluster_inference d’une étape de transfert des données de votre machine locale vers le cluster de calcul (transfer) et d’une étape de rappatriement des données inférées sur le cluster vers votre machine local (transfer_back).
Notre workflow prend donc la forme suivante :
flowchart TD
S{Start} --> PreProcessing
PreProcessing --> TransferData
TransferData --> ClusterInference
ClusterInference --> TransferBackData
TransferBackData --> PostProcessing
PostProcessing -->stop{Stop}
Ces étapes de transfert de données peuvent être réalisées avec rsync via l’outil GRICAD de transfert de données vers les clusters cargo.
Le but de l’étape TransferData est de transférer les données et fichiers nécessaires à l’inférence sur l’infrastructure de calcul. La règle résultante peut donc s’écrire sous la forme suivante :
rule transfer:
input:
[expand("input_file{file_id}", file_id=range(1, nb_files+1))]
output:
"transfer_OK"
shell:
"transfer_to_cluster.sh && touch transfer_OK"
Dans la règle ci-dessus, les transferts de données et de fichiers sont réalisés grâce au script transfer_to_cluster.sh. Ce script peut ressembler à :
rsync -avxH "$PWD/data/" "$USER@cargo.univ-grenoble-alpes.fr:/path/to/your/cluster/directory/data/"
Une fois que ce transfert est réalisé, on applique la commande touch transfer_OK pour signifier en local que le transfert sur le cluster a été réalisé, et déclencher l’étape suivante (pour laquelle le fichier transfer_OK sera l’une des entrées).
Parmi les données transférées, il doit y avoir les jobs à exécuter avec oarsub ainsi que les éventuels wrappers qsub et qstat permettant de passer plus facilement des options à l’exécution.
Le but de l’étape TransferBackData est de rapatrier en local les sorties générées après l’inférence sur l’infrastructure de calcul. La règle résultante peut donc s’écrire sous la forme suivante :
rule transfer_back:
input:
"inference_cluster_OK"
output:
[expand("output_file{file_id}", file_id=range(1, nb_files+1))]
shell:
"transfer_back_from_cluster.sh && touch transfer_back_OK"
Dans la règle ci-dessus, le rapatriement des sorties est réalisé grâce au script transfer_back_from_cluster.sh. Ce script peut ressembler à :
rsync -avxH "$USER@cargo.univ-grenoble-alpes.fr:/path/to/your/cluster/directory/outputs/" "$PWD/outputs/"
Une fois que ce transfert est réalisé, on applique la commande touch transfer_back_OK pour signifier en local que le rapatriement des sorties en local a été réalisé, et déclencher l’étape suivante (pour laquelle le fichier transfer_back_OK sera l’une des entrées).
Les règles transfer et transfer_back doivent être exécutées en local. Il faut donc penser à les rajouter dans la liste des règles à exécuter en local au début de votre Snakefile.
Pour signifier à Snakemake qu’une partie du workflow sera exécutée sur un cluster de calcul, il faut lui préciser dans la commande de lancement du workflow.
snakemake -s my_snakefile.smk --jobs 1 --executor cluster-generic
--cluster-generic-submit-cmd "local_qsub.sh"
--cluster-generic-status-cmd "local_qstat.sh"
Le flag --executor cluster-generic permet de préciser à Snakemake que le workflow sera partiellement exécuté sur un cluster de calcul.
Le flag --cluster-generic-submit-cmd "local_qsub.sh" indique à Snakemake le script à exécuter pour soumettre les jobs sur le cluster. local_qsub.sh est un wrapper local qui va lui-même aller lancer le wrapper qsub.sh sur le cluster, qui soumettra les jobs avec oarsub.
Le flag --cluster-generic-submit-cmd "local_qsub.sh" indique à Snakemake le script à exécuter pour soumettre les jobs sur le cluster. local_qsub.sh est un wrapper local qui va lui-même aller lancer le wrapper qsub.sh précédemment transféré sur le cluster, qui soumettra les jobs avec oarsub.
Le flag --cluster-generic-status-cmd "local_qstat.sh" indique à Snakemake le script à exécuter pour contrôler l’état des jobs sur le cluster. local_qstat.sh est un wrapper local qui va lui-même aller lancer le wrapper qstat.sh précédemment transféré sur le cluster, qui contrôlera l’état des jobs avec oarstat.
Dans le Snakefile, la règle cluster_inference sera de la forme :
rule cluster_inference:
input:
"transfer_OK", # Fichier de sortie de l'étape de transfert des données sur le cluster
output:
"inference_cluster_OK",
shell:
"job.sh" # Script à lancer avec oarsub sur le cluster
Cette règle sera lancée sur le cluster par qsub.sh, lui-même lancé par Snakemake avec local_qsub.sh.
Le contenu du fichier local_qsub.sh sera de la forme (à compléter selon vos besoins) :
ssh cluster_name "/path/to/run/directory/qsub.sh $*"
Ce fichier se connecte donc au cluster par SSH et lance le qsub.sh préalablement transféré, puis créé un fichier job_running pour signifier en local que le job a été lancé sur le cluster.
Le contenu du fichier local_qstat.sh sera de la forme (à compléter selon vos besoins) :
tmp=$(ssh cluster_name "/path/to/run/directory/qstat.sh $*)"
oar_state=$(echo "$tmp" | cut -d' ' -f 2)
case ${oar_state} in
Terminated) touch "inference_cluster_OK"; echo "success";;
Running|Finishing|Waiting|toLaunch|Launching|Hold|toAckReservation) echo "running";;
Error|toError) echo "failed";;
Suspended|Resuming) echo "suspended";;
*) echo "unknown";;
esac
Le local_qstat.sh sera itérativement lancé par Snakemake jusqu’à ce que le job soit terminé. En cas de succès, le fichier inference_cluster_OK sera créé et permettra de lancer la suite du workflow (rapatriement des données avec la règle transfer_back). Sinon, le workflow s’arrêtera avec une erreur “fichier de sortie non créé”.
La réalisation des étapes précédentes peut être assez complexe et chronophage. Un template permettant de faciliter la prise en main de cet outil est en cours de conception.