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():
- Abre una VPN segura hacia el clúster (si el provider la necesita).
- Inicia sesión y prepara el acceso a la API REST de Slurm.
- 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 turun().
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:
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
PENDINGpara siempre. Si tus jobs no arrancan, fijamemory_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 conqueue_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.