Saltar a contenido

Cómo crear un solver de HPC#

Introducción#

Un solver de HPC ejecuta su trabajo como un job por lotes en un clúster de computación de altas prestaciones (HPC) en lugar de dentro del contenedor del solver. Desde tu función run() envías un job al planificador del clúster (Slurm), esperas a que termine y devuelves los resultados, todo a través de un cliente que QCentroid te proporciona.

Es la forma de acceder a un clúster HPC conectado a la plataforma mediante un provider de HPC: tus jobs se ejecutan en los nodos de cómputo del clúster a través de la API REST de Slurm.

Nota

Esta página da por hecho que ya conoces lo básico para construir un solver. Si no es así, empieza primero por Cómo crear un solver.

Cómo funciona#

No gestionas tú la conexión con el clúster. Cuando seleccionas el provider de HPC para tu solver, la plataforma, antes de que se llame a tu run():

  1. Abre una VPN segura hacia el clúster (si el provider la necesita).
  2. Inicia sesión y prepara el acceso a la API REST de Slurm.
  3. Inyecta las credenciales y los valores por defecto del clúster como variables de entorno, y deja el cliente de HPC disponible para tu código.

Así, tu solver solo tiene que centrarse en qué job ejecutar. Enviarlo, esperarlo y leer la salida se reduce a un puñado de llamadas al cliente:

Tu run() → envía un script → cliente de HPC → Slurm REST a través de la VPN → el clúster lo ejecuta en un nodo de cómputo → la salida del job vuelve a tu run().

Antes de empezar#

  • Habilita el provider de HPC desde la sección Providers del panel. Consulta Cómo configurar proveedores.
  • Al registrar tu solver, selecciona ese provider y elige el lenguaje de HPC (python_hpc) en el desplegable de lenguaje de programación. Esto es lo que le indica a la plataforma que construya tu solver con el cliente de HPC (y con la VPN, si el clúster la necesita).

Solo se ejecuta en la plataforma

Los solvers de HPC no se pueden ejecutar en local con python app.py. La VPN, las credenciales del clúster y el cliente de HPC solo existen dentro del entorno de ejecución de QCentroid. Puedes desarrollar la lógica del job en local si quieres, pero prueba el solver completo ejecutándolo en la plataforma.

El cliente de HPC#

El cliente viene con QCentroid y ya está instalado en tu solver en tiempo de ejecución; no lo añadas a requirements.txt. Impórtalo así:

from qcentroid_solver_runner.hpc import QCentroidHPCClient

Se encarga de todo el ciclo: iniciar sesión en el clúster, hablar con Slurm, enviar el job, sondear su estado y leer la salida de vuelta. Lee el host del clúster, las credenciales y los valores por defecto del entorno que preparó el provider, así que nunca los escribes en el código.

Uso típico#

import logging
from qcentroid_solver_runner.hpc import QCentroidHPCClient

logger = logging.getLogger("qcentroid-user-log")   # para que el progreso salga en el panel

with QCentroidHPCClient(logger=logger) as client:
    client.login()
    job_id = client.run_job(script, name="my-job", time_limit=5, environment="login")
    client.wait_for_completion(job_id, poll_interval=5)
    result = client.get_job_results(job_id)

Métodos principales#

Método Qué hace
login() Conecta con el clúster y prepara el acceso a Slurm. Los demás métodos lo llaman por ti si se te olvida
run_job(script, **options) Envía un script por lotes y devuelve su job_id. Consulta las opciones más abajo
wait_for_completion(job_id, *, poll_interval=10, queue_timeout=None, run_timeout=None) Bloquea hasta que el job alcanza un estado final y lo devuelve. Registra el progreso en cada sondeo (motivo de cola, nodo, tiempos en cola/ejecución). Ambos timeouts son ilimitados por defecto; al alcanzar cualquiera de ellos el job se cancela y vuelve como CANCELLED
get_job_results(job_id) Un JobResult con state, node, exit_code, succeeded, stdout, stderr e info (el registro completo de Slurm)
run_and_wait(script, **options) Enviar + esperar + obtener resultados en una sola llamada
get_job_status(job_id) Estado actual (PENDING, RUNNING, COMPLETED, …) sin bloquear
cancel_job(job_id) / cancel_all() Cancelar un job, o todos los jobs activos
list_partitions() / get_user_associations() Descubrir qué particiones existen y qué combinaciones de account/partition/qos puedes usar; útil si un envío es rechazado

Escribir tu qcentroid.py#

Pon el cliente a trabajar dentro de run(). Este ejemplo envía un job pequeño que carga un módulo del clúster y ejecuta una línea de Python, y luego devuelve su salida:

qcentroid.py
import logging
from qcentroid_solver_runner.hpc import QCentroidHPCClient

logger = logging.getLogger("qcentroid-user-log")

# El script por lotes a ejecutar en el clúster. Usa "#!/bin/bash -l" (shell de login)
# cuando el script necesite `module`. Los nombres de los módulos dependen del clúster.
BATCH_SCRIPT = """#!/bin/bash -l
module load python/3.11

echo "Running on $(hostname)"
python3 -c 'print("Hello world from the HPC")'
"""


def run(input_data: dict, solver_params: dict, extra_arguments: dict) -> dict:
    logger.info("Enviando el job de HPC...")

    with QCentroidHPCClient(logger=logger) as client:
        client.login()
        job_id = client.run_job(
            BATCH_SCRIPT,
            name="my-hpc-job",
            time_limit=5,          # minutos
            environment="login",   # para que `module load` funcione (ver más abajo)
        )
        client.wait_for_completion(job_id, poll_interval=5)
        result = client.get_job_results(job_id)

    logger.info("Job %s terminado: %s (código de salida %s)", result.job_id, result.state, result.exit_code)

    return {
        "job_id": result.job_id,
        "state": result.state,
        "succeeded": result.succeeded,
        "stdout": result.stdout,
        "stderr": result.stderr,
    }

Todo lo que metas en input_data / solver_params puede usarse para moldear el script o las opciones del job, igual que en un solver normal.

Opciones de run_job()#

Las opciones de planificación que no pases caen a los valores por defecto que configuró el provider, así que en la práctica solo fijas lo específico de tu job.

Opción Notas
name Nombre del job
time_limit Límite de tiempo de reloj en minutos
memory_per_node Memoria en MB
tasks, cpus_per_task, nodes Tamaño de la asignación
partition, account, qos Contexto de planificación
gres Recursos genéricos por nodo, p. ej. "gpu:1"
gpus GPUs por job
exclusive Solicitar el nodo entero
environment "login" propaga el entorno de login del clúster, que es lo que hace funcionar module load. También se acepta un dict o una lista ["CLAVE=valor", ...]
working_directory Por defecto, tu directorio home en el clúster
job_overrides Campos crudos del job de la API REST de Slurm, para lo que no cubran las opciones anteriores

Las directivas #SBATCH se ignoran

A diferencia de sbatch, la API REST de Slurm no lee las líneas #SBATCH del script. Pasa las opciones de planificación como argumentos de run_job(), no como comentarios en el script.

Resultados y assets#

get_job_results() devuelve un JobResult:

Campo Significado
state Estado final de Slurm (COMPLETED, FAILED, CANCELLED, …)
succeeded True solo cuando el job terminó con código de salida 0
exit_code El código de salida del job
node El nodo de cómputo donde se ejecutó
stdout / stderr La salida capturada del job
info El registro crudo completo del job de Slurm (timestamps, recursos asignados, …)

El solver decide qué significa un job fallido

Un job del clúster que termina en FAILED o CANCELLED no es automáticamente un error del solver: tu run() devuelve con normalidad, reportando el estado. Lanza una excepción tú mismo solo si quieres marcar toda la ejecución como fallida.

Como en cualquier solver, puedes dejar ficheros en una carpeta additional_output para publicarlos como assets descargables (el stdout crudo, un informe, gráficas, …). Consulta Generar assets de salida.

Cosas a tener en cuenta#

  • Normalmente hay que fijar la memoria. En algunos clústeres, un job sin memoria explícita no llega a planificarse nunca y se queda en PENDING para siempre. Si tus jobs no arrancan, fija memory_per_node (o confía en el valor por defecto del provider).
  • Los tiempos en cola pueden ser largos. El clúster es compartido, así que un job puede pasar un rato encolado antes de ejecutarse. wait_for_completion() registra el motivo de la cola en cada sondeo, y puedes acotar la espera con queue_timeout / run_timeout.
  • Nunca escribas las credenciales en el código. Llegan por el entorno que preparó el provider; el cliente las lee por ti.

Qué sigue#

Ejecuta tu solver

Edita o actualiza este solver