Content from Esecuzione di comandi con Snakemake


Ultimo aggiornamento il 2025-04-05 | Modifica questa pagina

Tempo stimato: 60 minuti

Panoramica

Domande

  • “Come si esegue un semplice comando con Snakemake?”

Obiettivi

  • “Creare una ricetta di Snakemake (uno Snakefile)”

Qual è il flusso di lavoro che mi interessa?


In questa lezione faremo un esperimento che prende un’applicazione che gira in parallelo e ne studia la scalabilità. Per farlo dovremo raccogliere dati, in questo caso significa eseguire l’applicazione più volte con un numero diverso di core della CPU e registrare il tempo di esecuzione. Una volta fatto questo, dobbiamo creare una visualizzazione dei dati per vedere come si confronta con il caso ideale.

Dalla visualizzazione possiamo decidere a quale scala ha più senso eseguire l’applicazione in produzione per massimizzare l’uso della CPU allocata nel sistema.

Potremmo fare tutto questo manualmente, ma ci sono strumenti utili per aiutarci a gestire le pipeline di analisi dei dati come quelle del nostro esperimento. Oggi ne conosceremo uno: Snakemake.

Per iniziare a usare Snakemake, iniziamo con un semplice comando e vediamo come eseguirlo tramite Snakemake. Scegliamo il comando hostname che stampa il nome dell’host in cui viene eseguito il comando:

BASH

[ocaisa@node1 ~]$ hostname

OUTPUT

node1.int.jetstream2.hpc-carpentry.org

Questo stampa il risultato, ma Snakemake si affida ai file per conoscere lo stato del flusso di lavoro, quindi reindirizziamo l’output a un file:

BASH

[ocaisa@node1 ~]$ hostname > hostname_login.txt

Creazione di un file Snake


Modificare un nuovo file di testo chiamato Snakefile.

Contenuto di Snakefile:

PYTHON

rule hostname_login:
    output: "hostname_login.txt"
    input:  
    shell:
        "hostname > hostname_login.txt"

Punti chiave di questo file

  1. Il file si chiama Snakefile - con la maiuscola S e senza estensione.
  2. Alcune righe sono rientrate. I rientri devono essere fatti con caratteri di spazio, non con tabulazioni. Si veda la sezione di impostazione per sapere come far sì che il proprio editor di testo faccia questo.
  3. La definizione della regola inizia con la parola chiave rule seguita dal nome della regola, poi da due punti.
  4. Abbiamo chiamato la regola hostname_login. Si possono usare lettere, numeri o trattini bassi, ma il nome della regola deve iniziare con una lettera e non può essere una parola chiave.
  5. Le parole chiave input, output e shell sono tutte seguite da due punti (“:”).
  6. I nomi dei file e il comando di shell sono tutti in "quotes".
  7. Il nome del file di output viene dato prima del nome del file di input. In realtà, a Snakemake non interessa l’ordine in cui appaiono, ma in questo corso daremo prima l’output. Vedremo presto perché.
  8. In questo caso d’uso non c’è un file di input per il comando, quindi lo lasciamo vuoto.

Torniamo alla shell ed eseguiamo la nostra nuova regola. A questo punto, se ci sono virgolette mancanti, rientri errati e così via, potremmo vedere un errore.

BASH

snakemake -j1 -p hostname_login

bash: snakemake: command not found...

Se la shell vi dice che non riesce a trovare il comando snakemake, allora dobbiamo rendere il software disponibile in qualche modo. Nel nostro caso, ciò significa cercare il modulo che dobbiamo caricare:

BASH

module spider snakemake

OUTPUT

[ocaisa@node1 ~]$ module spider snakemake

--------------------------------------------------------------------------------------------------------
  snakemake:
--------------------------------------------------------------------------------------------------------
     Versions:
        snakemake/8.2.1-foss-2023a
        snakemake/8.2.1 (E)

Names marked by a trailing (E) are extensions provided by another module.


--------------------------------------------------------------------------------------------------------
  For detailed information about a specific "snakemake" package (including how to load the modules) use the module's full name.
  Note that names that have a trailing (E) are extensions provided by other modules.
  For example:

     $ module spider snakemake/8.2.1
--------------------------------------------------------------------------------------------------------

Ora vogliamo il modulo, quindi carichiamolo per rendere disponibile il pacchetto

BASH

[ocaisa@node1 ~]$ module load snakemake

e poi assicurarsi di avere a disposizione il comando snakemake

BASH

[ocaisa@node1 ~]$ which snakemake

OUTPUT

/cvmfs/software.eessi.io/host_injections/2023.06/software/linux/x86_64/amd/zen3/software/snakemake/8.2.1-foss-2023a/bin/snakemake

BASH

snakemake -j1 -p hostname_login

Esecuzione di Snakemake

Eseguire snakemake --help | less per vedere la guida per tutte le opzioni disponibili. Che cosa fa l’opzione -p nel comando snakemake di cui sopra?

  1. protegge i file di output esistenti
  2. stampa i comandi di shell che vengono eseguiti sul terminale
  3. Indica a Snakemake di eseguire solo un processo alla volta
  4. chiede all’utente il file di input corretto

È possibile cercare nel testo premendo /, e tornare alla shell con q.

  1. Stampa i comandi di shell che vengono eseguiti sul terminale

Questa è una cosa così utile che non sappiamo perché non sia l’opzione predefinita! L’opzione -j1 indica a Snakemake di eseguire solo un processo alla volta, e per ora ci atterremo a questa opzione perché rende le cose più semplici. La risposta 4 è un’assurdità, poiché Snakemake non richiede mai un input interattivo da parte dell’utente.

Punti Chiave

  • “Prima di eseguire Snakemake è necessario scrivere uno Snakefile”
  • “Uno Snakefile è un file di testo che definisce un elenco di regole”
  • “Le regole hanno ingressi, uscite e comandi di shell da eseguire”
  • “Si dice a Snakemake quale file creare e questo esegue il comando di shell definito nella regola appropriata”

Content from Esecuzione di Snakemake sul cluster


Ultimo aggiornamento il 2025-04-05 | Modifica questa pagina

Tempo stimato: 50 minuti

Panoramica

Domande

  • “Come si esegue la mia regola di Snakemake nel cluster?”

Obiettivi

  • “Definire le regole da eseguire localmente e sul cluster”

Cosa succede quando vogliamo che la nostra regola venga eseguita sul cluster piuttosto che sul nodo di accesso? Il cluster che stiamo usando usa Slurm e si dà il caso che Snakemake abbia un supporto integrato per Slurm, dobbiamo solo dirgli che vogliamo usarlo.

Snakemake utilizza l’opzione executor per consentire di selezionare il plugin che si desidera eseguire la regola. Il modo più rapido per applicarlo al proprio file Snake è definirlo sulla riga di comando. Proviamo

BASH

[ocaisa@node1 ~]$ snakemake -j1 -p --executor slurm hostname_login

OUTPUT

Building DAG of jobs...
Retrieving input from storage.
Nothing to be done (all requested files are present and up to date).

Non è successo nulla! Perché no? Quando gli viene chiesto di costruire un target, Snakemake controlla il “tempo di ultima modifica” del target e delle sue dipendenze. Se una dipendenza è stata aggiornata dopo il target, le azioni vengono rieseguite per aggiornare il target. Utilizzando questo approccio, Snakemake sa che deve ricostruire solo i file che, direttamente o indirettamente, dipendono dal file che è stato modificato. Questo è chiamato costruzione incrementale.

Le build incrementali migliorano l’efficienza

Ricostruendo i file solo quando necessario, Snakemake rende l’elaborazione più efficiente.

In esecuzione sul cluster

Ora abbiamo bisogno di un’altra regola che esegua la hostname sul cluster. Creare una nuova regola nel file Snake e provare a eseguirla sul cluster con le opzioni da --executor slurm a snakemake.

La regola è quasi identica alla regola precedente, tranne che per il nome della regola e il file di output:

PYTHON

rule hostname_remote:
    output: "hostname_remote.txt"
    input:
    shell:
        "hostname > hostname_remote.txt"

Si può quindi eseguire la regola con

BASH

[ocaisa@node1 ~]$ snakemake -j1 -p --executor slurm hostname_remote

OUTPUT

Building DAG of jobs...
Retrieving input from storage.
Using shell: /cvmfs/software.eessi.io/versions/2023.06/compat/linux/x86_64/bin/bash
Provided remote nodes: 1
Job stats:
job                count
---------------  -------
hostname_remote        1
total                  1

Select jobs to execute...
Execute 1 jobs...

[Mon Jan 29 18:03:46 2024]
rule hostname_remote:
    output: hostname_remote.txt
    jobid: 0
    reason: Missing output files: hostname_remote.txt
    resources: tmpdir=<TBD>

hostname > hostname_remote.txt
No SLURM account given, trying to guess.
Guessed SLURM account: def-users
No wall time information given. This might or might not work on your cluster.
If not, specify the resource runtime in your rule or as a reasonable default
via --default-resources. No job memory information ('mem_mb' or
'mem_mb_per_cpu') is given - submitting without.
This might or might not work on your cluster.
Job 0 has been submitted with SLURM jobid 326 (log: /home/ocaisa/.snakemake/slurm_logs/rule_hostname_remote/326.log).
[Mon Jan 29 18:04:26 2024]
Finished job 0.
1 of 1 steps (100%) done
Complete log: .snakemake/log/2024-01-29T180346.788174.snakemake.log

Notate tutti gli avvertimenti che Snakemake ci dà sul fatto che la regola potrebbe non essere in grado di essere eseguita sul nostro cluster, perché forse non abbiamo fornito informazioni sufficienti. Fortunatamente per noi, questa regola funziona sul nostro cluster e possiamo dare un’occhiata al file di output creato dalla nuova regola, hostname_remote.txt:

BASH

[ocaisa@node1 ~]$ cat hostname_remote.txt

OUTPUT

tmpnode1.int.jetstream2.hpc-carpentry.org

Profilo di Snakemake


L’adattamento di Snakemake a un particolare ambiente può comportare molti flag e opzioni. Pertanto, è possibile specificare un profilo di configurazione da utilizzare per ottenere le opzioni predefinite. Questo profilo si presenta come

BASH

snakemake --profile myprofileFolder ...

La cartella del profilo deve contenere un file chiamato config.yaml che è quello che memorizzerà le nostre opzioni. La cartella può contenere anche altri file necessari per il profilo. Creiamo il file cluster_profile/config.yaml e inseriamo alcune delle nostre opzioni esistenti:

YAML

printshellcmds: True
jobs: 3
executor: slurm

Ora dovremmo essere in grado di rilanciare il nostro flusso di lavoro puntando al profilo invece di elencare le opzioni. Per forzare l’esecuzione del nostro flusso di lavoro, dobbiamo prima rimuovere il file di output hostname_remote.txt e poi possiamo provare il nostro nuovo profilo

BASH

[ocaisa@node1 ~]$ rm hostname_remote.txt
[ocaisa@node1 ~]$ snakemake --profile cluster_profile hostname_remote

Il profilo è estremamente utile nel contesto del nostro cluster, poiché l’esecutore Slurm ha molte opzioni e a volte è necessario usarle per poter inviare lavori al cluster a cui si ha accesso. Sfortunatamente, i nomi delle opzioni in Snakemake non sono esattamente gli stessi di Slurm, quindi abbiamo bisogno dell’aiuto di una tabella di traduzione:

SLURM Snakemake Description
--partition slurm_partition the partition a rule/job is to use
--time runtime the walltime per job in minutes
--constraint constraint may hold features on some clusters
--mem mem, mem_mb memory a cluster node must
provide (mem: string with unit), mem_mb: int
--mem-per-cpu mem_mb_per_cpu memory per reserved CPU
--ntasks tasks number of concurrent tasks / ranks
--cpus-per-task cpus_per_task number of cpus per task (in case of SMP, rather use threads)
--nodes nodes number of nodes

Le avvertenze fornite da Snakemake suggeriscono che potrebbe essere necessario fornire queste opzioni. Un modo per farlo è fornirle come parte della regola di Snakemake usando la parola chiave resources, ad esempio,

PYTHON

rule:
    input: ...
    output: ...
    resources:
        partition = <partition name>
        runtime = <some number>

e possiamo anche usare il profilo per definire valori predefiniti per queste opzioni da usare con il nostro progetto, usando la parola chiave default-resources. Ad esempio, la memoria disponibile sul nostro cluster è di circa 4 GB per core, quindi possiamo aggiungerla al nostro profilo:

YAML

printshellcmds: True
jobs: 3
executor: slurm
default-resources:
  - mem_mb_per_cpu=3600

Sfida

Sappiamo che il nostro problema viene eseguito in un tempo molto breve. Cambiare la durata predefinita dei nostri lavori a due minuti per Slurm.

YAML

printshellcmds: True
jobs: 3
executor: slurm
default-resources:
  - mem_mb_per_cpu=3600
  - runtime=2

Esistono varie opzioni sbatch non supportate direttamente dalle definizioni delle risorse nella tabella precedente. È possibile utilizzare la risorsa slurm_extra per specificare uno qualsiasi di questi flag aggiuntivi a sbatch:

PYTHON

rule myrule:
    input: ...
    output: ...
    resources:
        slurm_extra="--mail-type=ALL --mail-user=<your email>"

Esecuzione di regole locali


La nostra regola iniziale era di ottenere il nome host del nodo di accesso. Vogliamo sempre eseguire la regola sul nodo di accesso perché abbia senso. Se diciamo a Snakemake di eseguire tutte le regole tramite l’esecutore Slurm (cosa che stiamo facendo tramite il nostro nuovo profilo), questo non accadrà più. Quindi come si fa a forzare l’esecuzione della regola sul nodo di login?

Beh, nel caso in cui una regola di Snakemake esegua un compito banale, l’invio di un lavoro potrebbe essere eccessivo (ad esempio, meno di un minuto di tempo di calcolo). Come nel nostro caso, sarebbe meglio che queste regole venissero eseguite localmente (cioè dove viene eseguito il comando snakemake) invece che come lavoro. Snakemake consente di indicare quali regole devono essere sempre eseguite localmente con la parola chiave localrules. Definiamo hostname_login come regola locale vicino alla parte superiore del nostro Snakefile.

PYTHON

localrules: hostname_login

Punti Chiave

  • “Snakemake genera e invia i propri script batch per lo scheduler”
  • “È possibile memorizzare le impostazioni di configurazione predefinite in un profilo Snakemake”
  • localrules definisce le regole che vengono eseguite localmente e non vengono mai inviate a un cluster”

Content from Segnaposto


Ultimo aggiornamento il 2025-04-05 | Modifica questa pagina

Tempo stimato: 70 minuti

Panoramica

Domande

  • “Come si crea una regola generica?”

Obiettivi

  • “Vedere come Snakemake gestisce alcuni errori”

Il nostro Snakefile presenta alcune duplicazioni. Per esempio, i nomi dei file di testo sono ripetuti in alcuni punti delle regole dello Snakefile. Gli Snakefile sono una forma di codice e, in qualsiasi codice, la ripetizione può portare a problemi (ad esempio, rinominiamo un file di dati in una parte dello Snakefile, ma dimentichiamo di rinominarlo altrove).

D.R.Y. (Don’t Repeat Yourself)

In molti linguaggi di programmazione, la maggior parte delle caratteristiche del linguaggio serve a consentire al programmatore di descrivere lunghe routine di calcolo come codice breve, espressivo e bello. Le caratteristiche di Python, R o Java, come le variabili e le funzioni definite dall’utente, sono utili in parte perché ci evitano di dover scrivere (o pensare) tutti i dettagli più volte. Questa buona abitudine di scrivere le cose solo una volta è nota come principio “Non ripetere te stesso” o D.R.Y.

Rimuoviamo alcune ripetizioni dal nostro Snakefile.

Segnaposto


Per creare una regola più generica abbiamo bisogno di placeholder. Vediamo l’aspetto di un segnaposto

PYTHON

rule hostname_remote:
    output: "hostname_remote.txt"
    input:
    shell:
        "hostname > {output}"

Come promemoria, ecco la versione precedente dell’ultimo episodio:

PYTHON

rule hostname_remote:
    output: "hostname_remote.txt"
    input:
    shell:
        "hostname > hostname_remote.txt"

La nuova regola ha sostituito i nomi di file espliciti con cose in {curly brackets}, in particolare {output} (ma avrebbe potuto essere anche {input}… se avesse avuto un valore e fosse stato utile).

{input} e {output} sono dei segnaposto

I segnaposto sono usati nella sezione shell di una regola e Snakemake li sostituisce con valori appropriati - {input} con il nome completo del file di input e {output} con il nome completo del file di output - prima di eseguire il comando.

anche {resources} è un segnaposto e si può accedere a un elemento nominato di {resources} con la notazione {resources.runtime} (per esempio).

Punti Chiave

  • “Le regole di Snakemake sono rese più generiche con i segnaposto”
  • “I segnaposto nella parte shell della regola vengono sostituiti con valori basati sui caratteri jolly scelti”

Content from Applicazioni MPI e Snakemake


Ultimo aggiornamento il 2025-04-05 | Modifica questa pagina

Tempo stimato: 50 minuti

Panoramica

Domande

  • “Come si esegue un’applicazione MPI tramite Snakemake sul cluster?”

Obiettivi

  • “Definire le regole da eseguire localmente e sul cluster”

Ora è il momento di tornare al nostro flusso di lavoro reale. Possiamo eseguire un comando sul cluster, ma che dire dell’esecuzione dell’applicazione MPI che ci interessa? La nostra applicazione si chiama amdahl ed è disponibile come modulo d’ambiente.

Sfida

Individuare e caricare il modulo amdahl e quindi sostituire la nostra regola hostname_remote con una versione che esegue amdahl. (Non preoccupatevi ancora dell’MPI parallelo, ma eseguitelo con una sola CPU, mpiexec -n 1 amdahl).

La regola viene eseguita correttamente? In caso contrario, cercare nei file di log per scoprirne il motivo?

BASH

module spider amdahl
module load amdahl

individuerà e caricherà il modulo amdahl. Possiamo quindi aggiornare/sostituire la nostra regola per eseguire l’applicazione amdahl:

PYTHON

rule amdahl_run:
    output: "amdahl_run.txt"
    input:
    shell:
        "mpiexec -n 1 amdahl > {output}"

Tuttavia, quando si tenta di eseguire la regola si ottiene un errore (a meno che non sia già disponibile una versione diversa di amdahl nel proprio percorso). Snakemake riporta la posizione dei log e se guardiamo all’interno possiamo (alla fine) trovare

OUTPUT

...
mpiexec -n 1 amdahl > amdahl_run.txt
--------------------------------------------------------------------------
mpiexec was unable to find the specified executable file, and therefore
did not launch the job.  This error was first reported for process
rank 0; it may have occurred for other processes as well.

NOTE: A common cause for this error is misspelling a mpiexec command
      line parameter option (remember that mpiexec interprets the first
      unrecognized command line token as the executable).

Node:       tmpnode1
Executable: amdahl
--------------------------------------------------------------------------
...

Quindi, anche se abbiamo caricato il modulo prima di eseguire il flusso di lavoro, la nostra regola Snakemake non ha trovato l’eseguibile. Questo perché la regola Snakemake viene eseguita in un ambiente runtime pulito e dobbiamo dirgli in qualche modo di caricare il modulo ambiente necessario prima di provare a eseguire la regola.

Snakemake e moduli di ambiente


La nostra applicazione si chiama amdahl ed è disponibile sul sistema tramite un modulo d’ambiente, quindi dobbiamo dire a Snakemake di caricare il modulo prima di provare a eseguire la regola. Snakemake conosce i moduli d’ambiente, che possono essere specificati tramite (un’altra) opzione:

PYTHON

rule amdahl_run:
    output: "amdahl_run.txt"
    input:
    envmodules:
      "mpi4py",
      "amdahl"
    input:
    shell:
        "mpiexec -n 1 amdahl > {output}"

L’aggiunta di queste righe non è tuttavia sufficiente a far eseguire la regola. Non solo bisogna dire a Snakemake quali moduli caricare, ma bisogna anche dirgli di usare i moduli d’ambiente in generale (poiché si ritiene che l’uso dei moduli d’ambiente renda l’ambiente di runtime meno riproducibile, dato che i moduli disponibili possono differire da cluster a cluster). Per questo è necessario dare a Snakemake un’opzione aggiuntiva

BASH

snakemake --profile cluster_profile --use-envmodules amdahl_run

Sfida

Utilizzeremo i moduli d’ambiente per tutto il resto del tutorial, quindi rendiamola un’opzione predefinita del nostro profilo (impostando il suo valore a True)

Aggiornare il profilo del cluster a

YAML

printshellcmds: True
jobs: 3
executor: slurm
default-resources:
  - mem_mb_per_cpu=3600
  - runtime=2
use-envmodules: True

Se si vuole testare, è necessario cancellare il file di output della regola e rieseguire Snakemake.

Snakemake e MPI


Nell’ultima sezione non abbiamo eseguito un’applicazione MPI, in quanto abbiamo eseguito solo su un core. Come si fa a richiedere l’esecuzione su più core per una singola regola?

Snakemake ha un supporto generale per MPI, ma l’unico esecutore che attualmente supporta esplicitamente MPI è l’esecutore Slurm (per nostra fortuna!). Se guardiamo alla nostra tabella di traduzione da Slurm a Snakemake, notiamo che le opzioni rilevanti appaiono in fondo:

SLURM Snakemake Description
--ntasks tasks number of concurrent tasks / ranks
--cpus-per-task cpus_per_task number of cpus per task (in case of SMP, rather use threads)
--nodes nodes number of nodes

L’opzione che ci interessa è tasks, poiché aumenteremo solo il numero di ranghi. Possiamo definirli in una sezione resources della nostra regola e fare riferimento ad essi usando dei segnaposto:

PYTHON

rule amdahl_run:
    output: "amdahl_run.txt"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi='mpiexec',
      tasks=2
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl > {output}"

Questo ha funzionato, ma ora abbiamo un piccolo problema. Vogliamo farlo per alcuni valori diversi di tasks, il che significa che abbiamo bisogno di un file di output diverso per ogni esecuzione. Sarebbe fantastico se potessimo indicare in qualche modo nel file output il valore che vogliamo usare per tasks… e far sì che Snakemake lo prenda.

Potremmo usare un wildcard nel output per permetterci di definire il tasks che vogliamo usare. La sintassi di questo carattere jolly è la seguente

PYTHON

output: "amdahl_run_{parallel_tasks}.txt"

dove parallel_tasks è il nostro carattere jolly.

Caratteri jolly

I caratteri jolly sono usati nelle righe input e output della regola per rappresentare parti di nomi di file. Come lo schema * nella shell, il carattere jolly può sostituire qualsiasi testo per comporre il nome del file desiderato. Come per la denominazione delle regole, si può scegliere un nome a piacere per i caratteri jolly, qui abbiamo usato parallel_tasks. L’uso degli stessi caratteri jolly nell’input e nell’output indica a Snakemake come abbinare i file di input a quelli di output.

Se due regole utilizzano un carattere jolly con lo stesso nome, Snakemake le tratterà come entità diverse - le regole in Snakemake sono autocontenute in questo modo.

Nella riga shell si può fare riferimento al carattere jolly con {wildcards.parallel_tasks}

Ordine delle operazioni di Snakemake


Abbiamo appena iniziato con alcune semplici regole, ma vale la pena di pensare a cosa fa esattamente Snakemake quando lo si esegue. Ci sono tre fasi distinte:

  1. Prepara l’esecuzione:
    1. Legge tutte le definizioni delle regole dal file Snakefile
  2. Pianifica cosa fare:
    1. vede quale/i file si sta chiedendo di creare
    2. Cerca una regola corrispondente guardando le output di tutte le regole che conosce
    3. Compila i caratteri jolly per ottenere il valore input per questa regola
    4. verifica che questo file di input (se richiesto) sia effettivamente disponibile
  3. Esegue i passi:
    1. Crea la directory per il file di output, se necessario
    2. Rimuove il vecchio file di output, se è già presente
    3. Solo allora, esegue il comando di shell con i segnaposto sostituiti
    4. controlla che il comando sia stato eseguito senza errori e che il nuovo file di output sia stato creato come previsto

Modalità di esecuzione a secco (-n)

Spesso è utile eseguire solo le prime due fasi, in modo che Snakemake pianifichi i lavori da eseguire e li stampi sullo schermo, ma non li esegua mai. Questo viene fatto con il flag -n, ad esempio:

BASH

> $ snakemake -n ...

La quantità di controlli può sembrare pedante in questo momento, ma con l’aumentare dei passi del flusso di lavoro questo diventerà davvero molto utile.

Usando i caratteri jolly nella nostra regola


Vorremmo usare un carattere jolly nel output per permetterci di definire il numero di tasks che vogliamo usare. Sulla base di quanto visto finora, si potrebbe immaginare che questo potrebbe essere simile a

PYTHON

rule amdahl_run:
    output: "amdahl_run_{parallel_tasks}.txt"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      tasks="{parallel_tasks}"
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl > {output}"

ma ci sono due problemi:

  • L’unico modo per Snakemake di conoscere il valore del carattere jolly è che l’utente richieda esplicitamente un file di output concreto (invece di chiamare la regola):

BASH

  snakemake --profile cluster_profile amdahl_run_2.txt

questo è perfettamente valido, poiché Snakemake è in grado di capire che ha una regola che può corrispondere a quel nome di file.

  • Il problema maggiore è che anche così non funziona, sembra che non si possa usare un carattere jolly per tasks:

    OUTPUT

    WorkflowError:
    SLURM job submission failed. The error message was sbatch:
    error: Invalid numeric value "{parallel_tasks}" for --ntasks.

Purtroppo per noi, non c’è un modo diretto per accedere ai caratteri jolly per tasks. Il motivo è che Snakemake cerca di usare il valore di tasks durante la fase di inizializzazione, quindi prima di conoscere il valore del carattere jolly. È necessario rimandare la determinazione di tasks a un momento successivo. Questo si può ottenere specificando una funzione di input invece di un valore per questo scenario. La soluzione è quindi quella di scrivere una funzione da usare una sola volta per manipolare Snakemake in modo che lo faccia per noi. Poiché la funzione è specifica per la regola, possiamo usare una funzione di una riga senza nome. Questo tipo di funzioni sono chiamate funzioni anonime o funzioni lamdba (entrambe hanno lo stesso significato) e sono una caratteristica di Python (e di altri linguaggi di programmazione).

Per definire una funzione lambda in python, la sintassi generale è la seguente:

PYTHON

lambda x: x + 54

Poiché la nostra funzione può accettare i caratteri jolly come argomenti, possiamo usarli per impostare il valore di tasks:

PYTHON

rule amdahl_run:
    output: "amdahl_run_{parallel_tasks}.txt"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      # No direct way to access the wildcard in tasks, so we need to do this
      # indirectly by declaring a short function that takes the wildcards as an
      # argument
      tasks=lambda wildcards: int(wildcards.parallel_tasks)
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl > {output}"

Ora abbiamo una regola che può essere usata per generare output da esecuzioni di un numero arbitrario di task paralleli.

Commenti in Snakefiles

Nel codice precedente, la riga che inizia con # è una riga di commento. Si spera che abbiate già l’abitudine di aggiungere commenti ai vostri script. Un buon commento rende qualsiasi script più leggibile, e questo vale anche per Snakefiles.

Poiché la nostra regola è ora in grado di generare un numero arbitrario di file di output, le cose potrebbero diventare molto affollate nella nostra directory corrente. Probabilmente è meglio mettere le esecuzioni in una cartella separata per mantenere l’ordine. Possiamo aggiungere la cartella direttamente al nostro output e Snakemake si occuperà della creazione della directory per noi:

PYTHON

rule amdahl_run:
    output: "runs/amdahl_run_{parallel_tasks}.txt"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      # No direct way to access the wildcard in tasks, so we need to do this
      # indirectly by declaring a short function that takes the wildcards as an
      # argument
      tasks=lambda wildcards: int(wildcards.parallel_tasks)
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl > {output}"

Sfida

Creare un file di output (sotto la cartella runs) per il caso in cui si abbiano 6 task paralleli

(SUGGERIMENTO: ricordate che Snakemake deve essere in grado di far corrispondere il file richiesto al output di una regola)

BASH

snakemake --profile cluster_profile runs/amdahl_run_6.txt

Un’altra cosa della nostra applicazione amdahl è che alla fine vogliamo elaborare l’output per generare il nostro grafico in scala. L’output in questo momento è utile per la lettura, ma rende più difficile l’elaborazione. in amdahl c’è un’opzione che ci facilita questo compito. Per vedere le opzioni di amdahl si può usare

BASH

[ocaisa@node1 ~]$ module load amdahl
[ocaisa@node1 ~]$ amdahl --help

OUTPUT

usage: amdahl [-h] [-p [PARALLEL_PROPORTION]] [-w [WORK_SECONDS]] [-t] [-e]

options:
  -h, --help            show this help message and exit
  -p [PARALLEL_PROPORTION], --parallel-proportion [PARALLEL_PROPORTION]
                        Parallel proportion should be a float between 0 and 1
  -w [WORK_SECONDS], --work-seconds [WORK_SECONDS]
                        Total seconds of workload, should be an integer greater than 0
  -t, --terse           Enable terse output
  -e, --exact           Disable random jitter

L’opzione che stiamo cercando è --terse, che farà stampare l’output di amdahl in un formato molto più facile da elaborare, JSON. Il formato JSON in un file usa tipicamente l’estensione .json, quindi aggiungiamo questa opzione al nostro comando shell e cambiamo il formato del file output per adattarlo al nostro nuovo comando:

PYTHON

rule amdahl_run:
    output: "runs/amdahl_run_{parallel_tasks}.json"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      # No direct way to access the wildcard in tasks, so we need to do this
      # indirectly by declaring a short function that takes the wildcards as an
      # argument
      tasks=lambda wildcards: int(wildcards.parallel_tasks)
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl --terse > {output}"

C’era un altro parametro per amdahl che ha attirato la mia attenzione. amdahl ha un’opzione --parallel-proportion (o -p) che potremmo essere interessati a cambiare perché modifica il comportamento del codice e quindi ha un impatto sui valori che otteniamo nei nostri risultati. Aggiungiamo un altro livello di directory al nostro formato di output per riflettere una scelta particolare per questo valore. Possiamo usare un carattere jolly in modo da non dover scegliere subito il valore:

PYTHON

rule amdahl_run:
    output: "p_{parallel_proportion}/runs/amdahl_run_{parallel_tasks}.json"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      # No direct way to access the wildcard in tasks, so we need to do this
      # indirectly by declaring a short function that takes the wildcards as an
      # argument
      tasks=lambda wildcards: int(wildcards.parallel_tasks)
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl --terse -p {wildcards.parallel_proportion} > {output}"

Sfida

Crea un file di output per un valore di -p di 0,999 (il valore predefinito è 0,8) per il caso in cui abbiamo 6 task paralleli.

BASH

snakemake --profile cluster_profile p_0.999/runs/amdahl_run_6.json

Punti Chiave

  • “Snakemake sceglie la regola appropriata sostituendo i caratteri jolly in modo che l’output corrisponda all’obiettivo”
  • “Snakemake controlla varie condizioni di errore e si ferma se vede un problema”

Content from Concatenazione di regole


Ultimo aggiornamento il 2025-04-05 | Modifica questa pagina

Tempo stimato: 70 minuti

Panoramica

Domande

  • “Come si combinano le regole in un flusso di lavoro?”
  • “Come faccio a creare una regola con più ingressi e uscite?”

Obiettivi

  • “”

Una pipeline di regole multiple


Ora abbiamo una regola che può generare output per qualsiasi valore di -p e per qualsiasi numero di task, dobbiamo solo chiamare Snakemake con i parametri che vogliamo:

BASH

snakemake --profile cluster_profile p_0.999/runs/amdahl_run_6.json

Questo però non è esattamente comodo, per generare un set di dati completo dobbiamo eseguire Snakemake molte volte con diversi target di file di output. Creiamo invece una regola che generi questi file per noi.

Il concatenamento delle regole in Snakemake è una questione di scelta di modelli di nomi di file che collegano le regole. Si tratta di un’arte: nella maggior parte dei casi ci sono diverse opzioni che funzionano:

PYTHON

rule generate_run_files:
    output: "p_{parallel_proportion}_runs.txt"
    input:  "p_{parallel_proportion}/runs/amdahl_run_6.json"
    shell:
        "echo {input} done > {output}"

Sfida

La nuova regola non fa nulla, si assicura solo che venga creato il file desiderato. Non vale la pena eseguirla sul cluster. Come assicurarsi che venga eseguita solo sul nodo di accesso?

Dobbiamo aggiungere la nuova regola alla nostra localrules:

PYTHON

localrules: hostname_login, generate_run_files

Ora eseguiamo la nuova regola (ricordiamo che dobbiamo richiedere il file di output per nome, poiché il output nella nostra regola contiene un pattern jolly):

BASH

[ocaisa@node1 ~]$ snakemake --profile cluster_profile/ p_0.999_runs.txt

OUTPUT

Using profile cluster_profile/ for setting default command line arguments.
Building DAG of jobs...
Retrieving input from storage.
Using shell: /cvmfs/software.eessi.io/versions/2023.06/compat/linux/x86_64/bin/bash
Provided remote nodes: 3
Job stats:
job                   count
------------------  -------
amdahl_run                1
generate_run_files        1
total                     2

Select jobs to execute...
Execute 1 jobs...

[Tue Jan 30 17:39:29 2024]
rule amdahl_run:
    output: p_0.999/runs/amdahl_run_6.json
    jobid: 1
    reason: Missing output files: p_0.999/runs/amdahl_run_6.json
    wildcards: parallel_proportion=0.999, parallel_tasks=6
    resources: mem_mb=1000, mem_mib=954, disk_mb=1000, disk_mib=954,
               tmpdir=<TBD>, mem_mb_per_cpu=3600, runtime=2, mpi=mpiexec, tasks=6

mpiexec -n 6 amdahl --terse -p 0.999 > p_0.999/runs/amdahl_run_6.json
No SLURM account given, trying to guess.
Guessed SLURM account: def-users
Job 1 has been submitted with SLURM jobid 342 (log: /home/ocaisa/.snakemake/slurm_logs/rule_amdahl_run/342.log).
[Tue Jan 30 17:47:31 2024]
Finished job 1.
1 of 2 steps (50%) done
Select jobs to execute...
Execute 1 jobs...

[Tue Jan 30 17:47:31 2024]
localrule generate_run_files:
    input: p_0.999/runs/amdahl_run_6.json
    output: p_0.999_runs.txt
    jobid: 0
    reason: Missing output files: p_0.999_runs.txt;
            Input files updated by another job: p_0.999/runs/amdahl_run_6.json
    wildcards: parallel_proportion=0.999
    resources: mem_mb=1000, mem_mib=954, disk_mb=1000, disk_mib=954,
               tmpdir=/tmp, mem_mb_per_cpu=3600, runtime=2

echo p_0.999/runs/amdahl_run_6.json done > p_0.999_runs.txt
[Tue Jan 30 17:47:31 2024]
Finished job 0.
2 of 2 steps (100%) done
Complete log: .snakemake/log/2024-01-30T173929.781106.snakemake.log

Osservate i messaggi di log che Snakemake stampa nel terminale. Che cosa è successo?

  1. Snakemake cerca una regola per creare p_0.999_runs.txt
  2. Determina che “generate_run_files” può fare questo se parallel_proportion=0.999
  3. Vede che l’input necessario è quindi p_0.999/runs/amdahl_run_6.json
  4. Snakemake cerca una regola per creare p_0.999/runs/amdahl_run_6.json
  5. Determina che “amdahl_run” può fare questo se parallel_proportion=0.999 e parallel_tasks=6
  6. Ora Snakemake ha raggiunto un file di input disponibile (in questo caso, non è richiesto alcun file di input), esegue entrambi i passaggi per ottenere l’output finale

Questo, in breve, è il modo in cui costruiamo i flussi di lavoro in Snakemake.

  1. Definizione delle regole per tutte le fasi di elaborazione
  2. Scegliere modelli di denominazione input e output che consentano a Snakemake di collegare le regole
  3. Indica a Snakemake di generare i file di output finali

Se si è abituati a scrivere script regolari, ci vuole un po’ di tempo per abituarsi. Invece di elencare i passi in ordine di esecuzione, si lavora sempre a ritroso** dal risultato finale desiderato. L’ordine delle operazioni è determinato dall’applicazione delle regole di corrispondenza dei pattern ai nomi dei file, non dall’ordine delle regole nel file Snake.

Prima le uscite?

L’approccio di Snakemake di lavorare a ritroso partendo dall’output desiderato per determinare il flusso di lavoro è il motivo per cui mettiamo le righe output per prime in tutte le nostre regole - per ricordarci che sono queste le prime cose che Snakemake guarda!

Molti utenti di Snakemake, e in effetti la documentazione ufficiale, preferiscono avere il input per primo, quindi in pratica si dovrebbe usare l’ordine che si ritiene più opportuno.

Uscite log in Snakemake

Snakemake ha un campo di regole dedicato per le uscite che sono file di log, e queste sono trattate per lo più come uscite normali, tranne per il fatto che i file di log non vengono rimossi se il lavoro produce un errore. Ciò significa che è possibile consultare il log per diagnosticare l’errore. In un flusso di lavoro reale questo può essere molto utile, ma in termini di apprendimento dei fondamenti di Snakemake ci atterremo ai normali campi input e output.

Gli errori sono normali

Non scoraggiatevi se vedete degli errori quando testate per la prima volta le vostre nuove pipeline di Snakemake. Ci sono molte cose che possono andare storte quando si scrive un nuovo flusso di lavoro e di solito sono necessarie diverse iterazioni per ottenere le cose giuste. Un vantaggio dell’approccio di Snakemake rispetto ai normali script è che Snakemake fallisce rapidamente quando c’è un problema, invece di continuare e potenzialmente eseguire calcoli inutili su dati parziali o corrotti. Un altro vantaggio è che quando un passo fallisce si può riprendere tranquillamente da dove si era interrotto.

Punti Chiave

  • “Snakemake collega le regole cercando iterativamente le regole che hanno ingressi mancanti”
  • “Le regole possono avere più ingressi e/o uscite con nome”
  • “Se un comando di shell non produce l’output previsto, Snakemake lo considererà un fallimento”

Content from Elaborazione di elenchi di input


Ultimo aggiornamento il 2025-04-05 | Modifica questa pagina

Tempo stimato: 80 minuti

Panoramica

Domande

  • “Come posso elaborare più file contemporaneamente?”
  • “Come si combinano più file insieme?”

Obiettivi

  • “Usare Snakemake per elaborare tutti i campioni in una volta sola”
  • “Creare un grafico di scalabilità che riunisca i nostri risultati”

Abbiamo creato una regola che può generare un singolo file di output, ma non abbiamo intenzione di creare più regole per ogni file di output. Vogliamo generare tutti i file di esecuzione con una sola regola, se possibile, ma Snakemake può accettare un elenco di file di input:

PYTHON

rule generate_run_files:
    output: "p_{parallel_proportion}_runs.txt"
    input:  "p_{parallel_proportion}/runs/amdahl_run_2.json", "p_{parallel_proportion}/runs/amdahl_run_6.json"
    shell:
        "echo {input} done > {output}"

È fantastico, ma non vogliamo dover elencare singolarmente tutti i file che ci interessano. Come possiamo fare?

Definizione di un elenco di campioni da elaborare


Per fare questo, possiamo definire alcuni elenchi come variabili globali di Snakemake.

Le variabili globali devono essere aggiunte prima delle regole nel file Snake.

PYTHON

# Task sizes we wish to run
NTASK_SIZES = [1, 2, 3, 4, 5]
  • A differenza di quanto avviene con le variabili negli script di shell, possiamo mettere degli spazi intorno al segno =, ma non sono obbligatori.
  • Gli elenchi di stringhe quotate sono racchiusi tra parentesi quadre e separati da virgole. Se conoscete un po’ di Python, riconoscerete che si tratta della sintassi delle liste di Python.
  • Una buona convenzione è quella di usare nomi in maiuscolo per queste variabili, ma non è obbligatorio.
  • Sebbene questi elenchi siano definiti variabili, non è possibile modificare i valori una volta che il flusso di lavoro è in esecuzione, quindi gli elenchi definiti in questo modo sono più simili a costanti.

Utilizzo di una regola di Snakemake per definire un gruppo di uscite


Ora aggiorniamo il nostro Snakefile per sfruttare la nuova variabile globale e creare un elenco di file:

PYTHON

rule generate_run_files:
    output: "p_{parallel_proportion}_runs.txt"
    input:  expand("p_{{parallel_proportion}}/runs/amdahl_run_{count}.json", count=NTASK_SIZES)
    shell:
        "echo {input} done > {output}"

La funzione expand(...) di questa regola genera un elenco di nomi di file, prendendo la prima cosa tra le parentesi singole come modello e sostituendo {count} con tutti gli NTASK_SIZES. Poiché ci sono 5 elementi nell’elenco, si otterranno 5 file che vogliamo creare. Si noti che abbiamo dovuto proteggere il nostro carattere jolly in una seconda serie di parentesi, in modo che non venisse interpretato come qualcosa che doveva essere espanso.

Nel nostro caso attuale ci basiamo ancora sul nome del file per definire il valore del carattere jolly parallel_proportion, quindi non possiamo chiamare direttamente la regola, ma dobbiamo richiedere un file specifico:

BASH

snakemake --profile cluster_profile/ p_0.999_runs.txt

Se non si specifica un nome di regola di destinazione o un nome di file sulla riga di comando quando si esegue Snakemake, l’impostazione predefinita è di usare la prima regola nel file Snake come destinazione.

Regole come target

Dare il nome di una regola a Snakemake sulla riga di comando funziona solo se quella regola non ha parole jolly in uscita, perché Snakemake non ha modo di sapere quali potrebbero essere le parole jolly desiderate. Verrà visualizzato l’errore “Le regole di destinazione non possono contenere caratteri jolly” Questo può accadere anche quando non si fornisce alcun target esplicito sulla riga di comando e Snakemake cerca di eseguire la prima regola definita nel file Snake.

Regole che combinano più input


La nostra regola generate_run_files è una regola che accetta un elenco di file di input. La lunghezza di tale elenco non è fissata dalla regola, ma può cambiare in base a NTASK_SIZES.

Nel nostro flusso di lavoro il passo finale consiste nel prendere tutti i file generati e combinarli in una trama. Per fare questo, forse avete sentito che alcune persone usano una libreria python chiamata matplotlib. Non è compito di questo tutorial scrivere lo script python per creare un grafico finale, quindi vi forniremo lo script come parte di questa lezione. È possibile scaricarlo con

BASH

curl -O https://ocaisa.github.io/hpc-workflows/files/plot_terse_amdahl_results.py

Lo script plot_terse_amdahl_results.py ha bisogno di una riga di comando che assomiglia a:

BASH

python plot_terse_amdahl_results.py --output <output image filename> <1st input file> <2nd input file> ...

Introduciamo questa funzione nella nostra regola generate_run_files:

PYTHON

rule generate_run_files:
    output: "p_{parallel_proportion}_runs.txt"
    input:  expand("p_{{parallel_proportion}}/runs/amdahl_run_{count}.json", count=NTASK_SIZES)
    shell:
        "python plot_terse_amdahl_results.py --output {output} {input}"

Sfida

Questo script si basa su matplotlib, è disponibile come modulo d’ambiente? Aggiungere questo requisito alla regola.

PYTHON

rule generate_run_files:
    output: "p_{parallel_proportion}_scalability.jpg"
    input:  expand("p_{{parallel_proportion}}/runs/amdahl_run_{count}.json", count=NTASK_SIZES)
    envmodules:
      "matplotlib"
    shell:
        "python plot_terse_amdahl_results.py --output {output} {input}"

Ora finalmente possiamo generare un grafico in scala! Eseguire il comando finale di Snakemake:

BASH

snakemake --profile cluster_profile/ p_0.999_scalability.jpg

Sfida

Generare il grafico della scalabilità per tutti i valori da 1 a 10 core.

PYTHON

NTASK_SIZES = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Sfida

Eseguire nuovamente il flusso di lavoro per un valore p di 0,8

BASH

snakemake --profile cluster_profile/ p_0.8_scalability.jpg

Bonus round

Creare una regola finale che possa essere richiamata direttamente e che generi un grafico in scala per 3 diversi valori di p.

Punti Chiave

  • “Utilizzare la funzione expand() per generare elenchi di nomi di file da combinare”
  • “Ogni {input} a una regola può essere un elenco di lunghezza variabile”