Aplicaciones MPI y Snakemake

Última actualización: 2025-07-01 | Mejora esta página

Hoja de ruta

Preguntas

  • “¿Cómo puedo ejecutar una aplicación MPI a través de Snakemake en el cluster?”

Objetivos

  • “Definir reglas para ejecutar localmente y en el cluster”

Ahora es el momento de volver a nuestro flujo de trabajo real. Podemos ejecutar un comando en el cluster, pero ¿qué pasa con la ejecución de la aplicación MPI que nos interesa? Nuestra aplicación se llama amdahl y está disponible como módulo de entorno.

Desafío

Localiza y carga el módulo amdahl y luego reemplaza nuestra regla hostname_remote con una versión que ejecute amdahl. (No te preocupes por el MPI paralelo todavía, ejecútalo con una sola CPU, mpiexec -n 1 amdahl).

¿Su regla se ejecuta correctamente? Si no es así, revise los archivos de registro para averiguar por qué

BASH

module spider amdahl
module load amdahl

localizará y cargará el módulo amdahl. Podemos entonces actualizar/reemplazar nuestra regla para ejecutar la aplicación amdahl:

PYTHON

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

Sin embargo, cuando intentamos ejecutar la regla obtenemos un error (a menos que ya tengas una versión diferente de amdahl disponible en tu ruta). Snakemake informa de la ubicación de los logs y si miramos dentro podemos (eventualmente) encontrar

SALIDA

...
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
--------------------------------------------------------------------------
...

Así que, aunque cargamos el módulo antes de ejecutar el flujo de trabajo, nuestra regla Snakemake no encontró el ejecutable. Esto se debe a que la regla de Snakemake se ejecuta en un entorno de ejecución limpio, y tenemos que decirle de alguna manera que cargue el módulo de entorno necesario antes de intentar ejecutar la regla.

Snakemake y módulos de entorno


Nuestra aplicación se llama amdahl y está disponible en el sistema a través de un módulo de entorno, por lo que necesitamos decirle a Snakemake que cargue el módulo antes de que intente ejecutar la regla. Snakemake es consciente de los módulos de entorno, y estos pueden ser especificados a través de (otra) opción:

PYTHON

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

Sin embargo, añadir estas líneas no es suficiente para que la regla se ejecute. No sólo tienes que decirle a Snakemake qué módulos cargar, sino que también tienes que decirle que use módulos de entorno en general (ya que se considera que el uso de módulos de entorno hace que tu entorno de ejecución sea menos reproducible, ya que los módulos disponibles pueden diferir de un cluster a otro). Esto requiere que le des a Snakemake una opción adicional

BASH

snakemake --profile cluster_profile --use-envmodules amdahl_run

Desafío

Utilizaremos módulos de entorno durante el resto del tutorial, así que conviértalo en una opción por defecto de nuestro perfil (estableciendo su valor en True)

Actualiza el perfil de nuestro cluster a

YAML

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

Si quieres probarlo, tienes que borrar el fichero de salida de la regla y volver a ejecutar Snakemake.

Snakemake y MPI


En realidad no hemos ejecutado una aplicación MPI en la última sección, ya que sólo lo hemos hecho en un núcleo. ¿Cómo solicitamos que se ejecute en varios núcleos para una única regla?

Snakemake tiene soporte general para MPI, pero el único ejecutor que actualmente soporta explícitamente MPI es el ejecutor Slurm (¡por suerte para nosotros!). Si volvemos a mirar nuestra tabla de traducción de Slurm a Snakemake nos daremos cuenta de que las opciones relevantes aparecen cerca de la parte inferior:

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

La que nos interesa es tasks ya que sólo vamos a aumentar el número de rangos. Podemos definirlas en una sección resources de nuestra regla y referirnos a ellas usando marcadores de posición:

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}"

Eso funcionó pero ahora tenemos un pequeño problema. Queremos hacer esto para algunos valores diferentes de tasks lo que significaría que necesitaríamos un archivo de salida diferente para cada ejecución. Sería genial si de alguna manera podemos indicar en el output el valor que queremos utilizar para tasks … y que Snakemake lo recoja.

Podríamos utilizar un wildcard en output para poder definir el tasks que deseamos utilizar. La sintaxis de este comodín es la siguiente

PYTHON

output: "amdahl_run_{parallel_tasks}.txt"

donde parallel_tasks es nuestro comodín.

Comodines

Los comodines se utilizan en las líneas input y output de la regla para representar partes de nombres de archivo. Al igual que el patrón * del intérprete de comandos, el comodín puede sustituir a cualquier texto para formar el nombre de archivo deseado. Al igual que con el nombre de sus reglas, puede elegir cualquier nombre que desee para sus comodines, así que aquí hemos utilizado parallel_tasks. El uso de los mismos comodines en la entrada y la salida es lo que le dice a Snakemake cómo hacer coincidir los archivos de entrada con los archivos de salida.

Si dos reglas usan un comodín con el mismo nombre entonces Snakemake las tratará como entidades diferentes - las reglas en Snakemake son autocontenidas de esta manera.

En la línea shell puede hacer referencia al comodín con {wildcards.parallel_tasks}

Orden de operaciones de Snakemake


Sólo estamos empezando con algunas reglas simples, pero vale la pena pensar en lo que Snakemake está haciendo exactamente cuando lo ejecutas. Hay tres fases distintas:

  1. Prepara la ejecución:
    1. Lee todas las definiciones de reglas del archivo Snakefile
  2. Planea qué hacer:
    1. Ve qué fichero(s) le estás pidiendo que haga
    2. Busca una regla coincidente mirando las outputs de todas las reglas que conoce
    3. Rellena los comodines para calcular el input de esta regla
    4. Comprueba que este fichero de entrada (si es necesario) está realmente disponible
  3. Ejecuta los pasos:
    1. Crea el directorio para el fichero de salida, si es necesario
    2. Elimina el fichero de salida antiguo si ya está ahí
    3. Sólo entonces, ejecuta el comando shell con los marcadores de posición sustituidos
    4. Comprueba que el comando se ejecuta sin errores y crea el nuevo fichero de salida como se esperaba

Modo de ejecución en seco (-n)

A menudo es útil ejecutar sólo las dos primeras fases, de modo que Snakemake planificará los trabajos a ejecutar, y los imprimirá en la pantalla, pero nunca los ejecutará realmente. Esto se hace con la bandera -n, eg:

BASH

> $ snakemake -n ...

La cantidad de comprobaciones puede parecer pedante ahora mismo, pero a medida que el flujo de trabajo gane más pasos esto nos resultará realmente muy útil.

Usando comodines en nuestra regla


Nos gustaría utilizar un comodín en output para permitirnos definir el número de tasks que deseamos utilizar. Basándonos en lo que hemos visto hasta ahora, se podría imaginar que esto podría tener el siguiente aspecto

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}"

pero hay dos problemas con esto:

  • La única forma de que Snakemake conozca el valor del comodín es que el usuario solicite explícitamente un archivo de salida concreto (en lugar de llamar a la regla):

BASH

  snakemake --profile cluster_profile amdahl_run_2.txt

Esto es perfectamente válido, ya que Snakemake puede averiguar que tiene una regla que puede coincidir con ese nombre de archivo.

  • El mayor problema es que incluso haciendo eso no funciona, parece que no podemos usar un comodín para tasks:

    SALIDA

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

Desafortunadamente para nosotros, no hay forma directa de acceder a los comodines para tasks. La razón de esto es que Snakemake intenta utilizar el valor de tasks durante su etapa de inicialización, que es antes de que sepamos el valor del comodín. Necesitamos aplazar la determinación de tasks para más adelante. Esto se puede conseguir especificando una función de entrada en lugar de un valor para este escenario. La solución entonces es escribir una función de un solo uso para manipular Snakemake para que haga esto por nosotros. Dado que la función es específicamente para la regla, podemos utilizar una función de una sola línea sin nombre. Este tipo de funciones se llaman funciones anónimas o funciones lamdba (ambas significan lo mismo), y son una característica de Python (y otros lenguajes de programación).

Para definir una función lambda en python, la sintaxis general es la siguiente:

PYTHON

lambda x: x + 54

Dado que nuestra función puede tomar los comodines como argumentos, podemos usarlos para establecer el valor de tasks:

PYTHON

rule amdahl_run:
    output: "amdahl_run_{parallel_tasks}.txt"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      # No hay una forma directa de acceder al comodín en las tareas, así que necesitamos hacerlo
      # de forma indirecta declarando una función breve que reciba los comodines como argumento
      tasks=lambda wildcards: int(wildcards.parallel_tasks)
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl > {output}"

Ahora tenemos una regla que puede utilizarse para generar resultados de ejecuciones de un número arbitrario de tareas paralelas.

Comentarios en Snakefiles

En el código anterior, la línea que empieza por # es una línea de comentario. Esperemos que ya tenga el hábito de añadir comentarios a sus propios scripts. Los buenos comentarios hacen que cualquier script sea más legible, y esto es igualmente cierto con Snakefiles.

Como nuestra regla es ahora capaz de generar un número arbitrario de ficheros de salida las cosas podrían llenarse mucho en nuestro directorio actual. Probablemente sea mejor entonces poner las ejecuciones en una carpeta separada para mantener las cosas ordenadas. Podemos añadir la carpeta directamente a nuestro output y Snakemake se encargará de crear el directorio por nosotros:

PYTHON

rule amdahl_run:
    output: "runs/amdahl_run_{parallel_tasks}.txt"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      # No hay una forma directa de acceder al comodín en las tareas, así que necesitamos hacerlo
      # de forma indirecta declarando una función breve que reciba los comodines como argumento
      tasks=lambda wildcards: int(wildcards.parallel_tasks)
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl > {output}"

Desafío

Crea un fichero de salida (en la carpeta runs) para el caso en que tengamos 6 tareas paralelas

(SUGERENCIA: Recuerde que Snakemake tiene que ser capaz de hacer coincidir el archivo solicitado con el output de una regla)

BASH

snakemake --profile cluster_profile runs/amdahl_run_6.txt

Otra cosa sobre nuestra aplicación amdahl es que en última instancia queremos procesar la salida para generar nuestro gráfico de escalado. La salida en este momento es útil para la lectura, pero hace que el procesamiento más difícil. amdahl tiene una opción que realmente hace esto más fácil para nosotros. Para ver las opciones de amdahl podemos utilizar

BASH

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

SALIDA

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

La opción que estamos buscando es --terse, y eso hará que amdahl imprima la salida en un formato que es mucho más fácil de procesar, JSON. El formato JSON en un archivo normalmente utiliza la extensión de archivo .json así que vamos a añadir esa opción a nuestro comando shell y cambiar el formato de archivo de la output para que coincida con nuestro nuevo comando:

PYTHON

rule amdahl_run:
    output: "runs/amdahl_run_{parallel_tasks}.json"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      # No hay una forma directa de acceder al comodín en las tareas, así que necesitamos hacerlo
      # de forma indirecta declarando una función breve que reciba los comodines como argumento
      tasks=lambda wildcards: int(wildcards.parallel_tasks)
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl --terse > {output}"

Había otro parámetro para amdahl que me llamó la atención. amdahl tiene una opción --parallel-proportion (o -p) que puede interesarnos cambiar, ya que modifica el comportamiento del código y, por tanto, influye en los valores que obtenemos en nuestros resultados. Vamos a añadir otra capa de directorio a nuestro formato de salida para reflejar una elección particular para este valor. Podemos utilizar un comodín para no tener que elegir el valor de inmediato:

PYTHON

rule amdahl_run:
    output: "p_{parallel_proportion}/runs/amdahl_run_{parallel_tasks}.json"
    input:
    envmodules:
      "amdahl"
    resources:
      mpi="mpiexec",
      # No hay una forma directa de acceder al comodín en las tareas, así que necesitamos hacerlo
      # de forma indirecta declarando una función breve que reciba los comodines como argumento
      tasks=lambda wildcards: int(wildcards.parallel_tasks)
    input:
    shell:
        "{resources.mpi} -n {resources.tasks} amdahl --terse -p {wildcards.parallel_proportion} > {output}"

Desafío

Crear un fichero de salida para un valor de -p de 0.999 (el valor por defecto es 0.8) para el caso en que tengamos 6 tareas paralelas.

BASH

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

Puntos Clave

  • “Snakemake elige la regla apropiada sustituyendo los comodines de forma que la salida coincida con el objetivo”
  • “Snakemake comprueba varias condiciones de error y se detendrá si ve un problema”