Planificación de trabajos en clústeres de IA: SLURM, Kubernetes, Ray y cuándo no necesita ninguno de ellos

Un planificador decide qué trabajo se ejecuta en qué GPU, cuándo y con cargo a quién. Sin uno, un clúster compartido degenera en gente preguntándose por Slack si alguien está usando el nodo 3. Con el planificador equivocado, dedicará más horas de ingeniería a YAML que al trabajo con modelos.

Este artículo compara los planificadores que la gente elige de verdad —SLURM, Kubernetes (con Kueue o Volcano), Ray y KubeRay, y las opciones comerciales Run:ai y Determined— y es honesto sobre el caso en el que la respuesta correcta es «nada, solo SSH». El lector ya ha leído K01, K02 y K03 y ahora decide qué montar encima de un clúster de 1–32 nodos.

Qué hace realmente un planificador

Tres tareas, más o menos en orden de dificultad: colocar el trabajo en los recursos libres, poner en cola el trabajo que todavía no cabe y coordinar los trabajos multinodo: el entrenamiento distribuido necesita que los N rangos arranquen a la vez (gang scheduling), porque un dist.barrier() de PyTorch se bloquea indefinidamente si un rango nunca aparece (véase K06).

Cualquier cosa que vaya más allá de un clúster de un solo equipo necesita además cuotas multiinquilino con préstamo, decaimiento de fair-share entre equipos, expropiación y una contabilidad de GPU correcta (para que el trabajo vea CUDA_VISIBLE_DEVICES bien configurado). Todos los planificadores que siguen cubren las tres primeras tareas. Donde divergen es en la segunda lista y en el tipo de trabajo que dan por hecho que usted ejecuta.

La gran división: trabajos por lotes frente a servicios de larga ejecución

Este es, con diferencia, el marco más importante. Los trabajos por lotes entrenan durante 8 horas y terminan, o ejecutan un barrido de hiperparámetros de 200 trabajos cortos, o procesan un conjunto de datos durante la noche: empiezan, acaban, dan un resultado, nada responde por HTTP. Los servicios de larga ejecución son un endpoint de vLLM que sirve inferencia 24/7, una base de datos de memoria de escena, una pila de monitorización: deben permanecer activos indefinidamente y reiniciarse si fallan.

SLURM se creó para lotes. Kubernetes se creó para servicios de larga ejecución. Todo lo demás es uno de los dos estirándose para cubrir la otra mitad.

Carga de trabajo SLURM Kubernetes
Entrenamiento de 8 horas Nativo Necesita Kueue o Volcano
Barrido de hiperparámetros de 200 trabajos Nativo Incómodo
Endpoint de inferencia vLLM 24/7 Incómodo Nativo
Mixto: entrenamiento + inferencia + herramientas Doloroso Nativo (con Kueue)
Entrenamiento distribuido multinodo Nativo (gang) Necesita plugin de gang
Reinicio automático de servicios caídos No Nativo

Si su clúster hace solo una de esas cosas, la elección es fácil. Si hace las dos —algo cada vez más habitual—, la pregunta es sobre qué planificador está dispuesto a pagar el impuesto operativo.

SLURM: el estándar de HPC que sigue ganando en entrenamiento puro

SLURM mueve más del 65% de los supercomputadores del TOP500 y la mayoría de los grandes entrenamientos publicados. Para cargas del tipo «envíe un trabajo de entrenamiento, espere los resultados, recoja el checkpoint», ha sido la respuesta correcta durante veinte años.

El modelo mental es sencillo: una partición es un grupo de nodos con nombre (gpu-5090, cpu-only, bigmem); un trabajo es un script de shell con directivas #SBATCH, enviado con sbatch; GRES (Generic RESources) lleva la cuenta de las GPU: --gres=gpu:rtxpro6000:2 pide dos tarjetas concretas; el gang scheduling viene integrado.

Un slurm.conf mínimo para un clúster de 4 nodos con 8× RTX Pro 6000 Blackwell por nodo:

ClusterName=kentino-lab
SlurmctldHost=head01

SchedulerType=sched/backfill
SelectType=select/cons_tres
SelectTypeParameters=CR_Core_Memory
GresTypes=gpu

# Contabilidad de GPU — sin estas dos líneas, el fair-share se calcula sobre segundos de CPU y no significa nada
AccountingStorageType=accounting_storage/slurmdbd
AccountingStorageTRES=gres/gpu
PriorityType=priority/multifactor
PriorityDecayHalfLife=7-0
PriorityWeightFairshare=10000
PriorityWeightAge=1000

# los cgroups aíslan las GPU que un trabajo puede ver
ProctrackType=proctrack/cgroup

NodeName=node[01-04] CPUs=128 RealMemory=1024000 Sockets=2 \
    CoresPerSocket=64 ThreadsPerCore=1 Gres=gpu:rtxpro6000:8 State=UNKNOWN

PartitionName=train Nodes=node[01-03] Default=YES MaxTime=48:00:00 State=UP
PartitionName=interactive Nodes=node04 MaxTime=04:00:00 \
    PriorityTier=10 State=UP

El gres.conf correspondiente en cada nodo:

AutoDetect=nvml
NodeName=node[01-04] Name=gpu Type=rtxpro6000 File=/dev/nvidia[0-7]

AutoDetect=nvml consulta directamente la NVIDIA Management Library, detecta las particiones MIG de forma automática y le ahorra escribir a mano las rutas de los ficheros de dispositivo.

Un usuario envía:

#!/bin/bash
#SBATCH --job-name=qwen-finetune
#SBATCH --partition=train
#SBATCH --nodes=2
#SBATCH --ntasks-per-node=8
#SBATCH --gres=gpu:rtxpro6000:8
#SBATCH --time=12:00:00
#SBATCH --output=logs/%j.out

srun python -m torch.distributed.run \
    --nnodes=2 --nproc-per-node=8 \
    --rdzv-backend=c10d \
    --rdzv-endpoint=$(scontrol show hostnames | head -1):29500 \
    train.py --config configs/qwen72b.yaml

PyTorch Elastic y SLURM encajan sin fricción: SLURM es el dueño de la asignación, srun lanza un proceso por tarea y torch.distributed.run junto con el backend de rendezvous se encarga de asignar los rangos. Si cae un nodo, --max-restarts en torchrun más --requeue en #SBATCH permite recuperar desde el último checkpoint. Todo el mundo lo hace así.

Lo que obtiene gratis: gang scheduling, decaimiento de fair-share, backfill, contabilidad (cada segundo de GPU registrado en sacct) y una semántica sencilla: sin CRD, sin controlador, sin grafo de objetos YAML. Lo que no obtiene: servicios de larga ejecución con reinicio automático, ingress HTTP, actualizaciones progresivas, contenedores sidecar, estado declarativo. SLURM es un planificador por lotes. Tratarlo como si fuera Kubernetes duele.

SLURM es la opción correcta cuando: ejecuta entrenamiento, inferencia por lotes o simulación; los usuarios escriben scripts de shell; un SRE a tiempo parcial puede operarlo.

Dónde toca techo SLURM

En tres sitios. Servicios de tipo web: no existe el concepto de «ejecutar siempre, reiniciar al morir, exponer el puerto 8000». Puede meter vLLM a la fuerza en un trabajo SLURM de larga duración, pero las comprobaciones de salud, las actualizaciones progresivas y el balanceo de carga pasan a ser problema suyo. Cargas heterogéneas: SLURM asume un único conjunto de recursos; un clúster con entrenamiento + inferencia + Jupyter + monitorización + preparación de datos pide controles más finos. Aislamiento multiinquilino estricto: los cgroups aíslan CPU y memoria; el aislamiento de GPU depende de CUDA_VISIBLE_DEVICES y de que el código del usuario lo respete. Sin espacios de nombres, sin política de red por trabajo, sin tenencia de nivel contenedor. Suficiente para laboratorios; insuficiente para un servicio alojado con varios clientes.

Cuando se topa con alguno de esos límites, lo normal es añadir una segunda capa —Kubernetes para los servicios— en lugar de torturar a SLURM. SUNK, de CoreWeave, y las ofertas gestionadas de 2026 ejecutan SLURM sobre Kubernetes para que el mismo clúster haga las dos cosas. A la escala de Kentino, dos clústeres pequeños separados suelen salir más simples.

Kubernetes para IA: la vía cloud-native

Kubernetes se creó para mantener en marcha servicios web sin estado. Para los clústeres de IA, «todo lo demás» se le acopla encima. La pila de 2026: NVIDIA GPU Operator (controladores, CUDA, NCCL, DCGM, device plugin, configuración MIG), Kueue (cola, cuota, admisión), Volcano (planificador orientado a lotes), Kubeflow Trainer (CRD PyTorchJob, TFJob, MPIJob), KubeRay (Ray sobre K8s) y Karpenter o Cluster Autoscaler para aprovisionar nodos.

Por qué Kubernetes a secas es una mala idea para IA: el kube-scheduler por defecto es FIFO y no tiene semántica de gang. Arranca tan tranquilo 7 de los 8 pods de un trabajo de entrenamiento distribuido y deja el octavo en Pending mientras los 7 en marcha mantienen sus GPU ociosas. Esto no es teórico: es el error más frecuente de los equipos que intentan entrenar en un clúster que construyeron para inferencia. Las soluciones son Kueue (admisión a nivel de trabajo) y Volcano (colocación gang a nivel de pod). La buena práctica actual es usar los dos: Kueue arriba, Volcano debajo.

Volcano

Volcano es un planificador por lotes de la CNCF que se instala junto a kube-scheduler o en su lugar. Añade gang scheduling real con semántica minAvailable (admitir cero o N, nunca parcial), prioridades de cola y expropiación, estrategias de fair-share / binpack / topology-aware y soporte de primer nivel para PyTorchJob, TFJob, MPIJob, RayJob y SparkApplication.

Una cola mínima más un trabajo de entrenamiento PyTorch con gang scheduling:

apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: training
spec:
  weight: 4
  capability:
    nvidia.com/gpu: 32
---
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: qwen-finetune
spec:
  schedulerName: volcano
  minAvailable: 16          # gang: los 16 rangos o ninguno
  queue: training
  policies:
    - event: PodEvicted
      action: RestartJob
  tasks:
    - replicas: 16
      name: worker
      template:
        spec:
          containers:
            - name: pytorch
              image: kentino/pytorch:2.5-cuda13
              resources:
                limits:
                  nvidia.com/gpu: 1
              command: ["torchrun", "--nnodes=16", "--nproc-per-node=1", "train.py"]

Cuándo no necesita Volcano: un clúster de inferencia puro con Deployments de vLLM independientes. Cada pod es dueño de sus GPU, no hay coordinación entre pods y el kube-scheduler por defecto basta. Volcano empieza a merecer la pena en cuanto mezcla entrenamiento distribuido en el clúster.

Kueue y el préstamo de cuota

Kueue cubre una capa que Volcano no toca: quién puede consumir cuánto del clúster y qué ocurre cuando un equipo está parado.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: rtxpro6000
spec:
  nodeLabels:
    nvidia.com/gpu.product: "RTX-PRO-6000-Blackwell"
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: team-research
spec:
  cohort: "shared-gpu"
  resourceGroups:
    - coveredResources: ["nvidia.com/gpu"]
      flavors:
        - name: rtxpro6000
          resources:
            - name: "nvidia.com/gpu"
              nominalQuota: 16
              borrowingLimit: 8

Los dos equipos de la cohorte tienen 16 GPU garantizadas y pueden tomar prestadas hasta 8 del conjunto ocioso del otro. El trabajo de alta prioridad expropia la capacidad prestada. La hoja de ruta de Kueue para 2026 se centra en MultiKueue (despacho multiclúster) y en la expropiación cooperativa: a un trabajo con checkpointing se le puede decir «guarde el estado y ceda en 60 segundos» en lugar de matarlo sin más.

MIG y uso compartido de GPU

El device plugin de NVIDIA asigna por defecto la GPU entera. Para entrenamiento e inferencia grande eso es lo correcto; para inferencia pequeña, notebooks o desarrollo es un desperdicio. Hay tres modos de uso compartido: MIG (aislamiento de memoria y de fallos por hardware, inferencia multiinquilino en producción), MPS (cooperativo, sin aislamiento) y time-slicing (sin aislamiento, solo desarrollo).

MIG existe en H100/H200, A100 y B200, en ninguna GPU de la gama Kentino. RTX 5090, RTX 4090, RTX Pro 6000 Blackwell, L40 y L4 no admiten MIG. Si su diseño exige particiones de GPU aisladas por hardware, eso limita el hardware a piezas SXM/de centro de datos que Kentino no fabrica. El time-slicing anuncia N GPU virtuales por cada GPU física y el planificador no tiene ni idea de que están sobresuscritas: úselo solo en desarrollo.

En qué es bueno Kubernetes: servicios de inferencia de larga ejecución (Deployments de vLLM escalados por HPA según la profundidad de la cola, actualizaciones progresivas, comprobaciones de salud; SLURM no tiene nada comparable), cargas heterogéneas en un solo clúster, ecosistema (Prometheus, Grafana, Argo). Lo que paga a cambio: una pila funcional de K8s + GPU Operator + Kueue + Volcano + Kubeflow son 20 componentes como mínimo. Estimación realista: Kubernetes supone 5–10× la carga operativa de SLURM para entrenamiento por lotes puro.

Kubernetes es la elección correcta cuando: el clúster ejecuta servicios de inferencia junto al entrenamiento, usted cuenta con un equipo de plataforma, lo declarativo importa de verdad o necesita portabilidad entre clústeres.

Ray y KubeRay: computación distribuida nativa de Python

Ray es otra bestia distinta. No es realmente un planificador de clúster en el sentido de SLURM o K8s: es un runtime distribuido de Python que incluye un planificador, un almacén de objetos y un autoescalador. Usted escribe Python con decoradores @ray.remote y Ray decide dónde ejecutar cada tarea.

Dónde encaja Ray: ajuste de hiperparámetros (Ray Tune: cientos de pruebas en paralelo, podando pronto las malas; el caso de uso para el que nació Ray y en el que sigue siendo el mejor), aprendizaje por refuerzo (RLlib: entornos y aprendices como actores, algo que encaja bien con Ray y mal con los trabajos por lotes de SLURM), entrenamiento distribuido (Ray Train, envolviendo PyTorch DDP / FSDP / DeepSpeed), servicio de modelos (Ray Serve, nativo de Python, con pipelines multimodelo a medida) y preprocesado de datos (Ray Data). Lo que Ray no es: un planificador por lotes multiinquilino para un clúster compartido. Ray da por hecho que una sola aplicación es dueña del clúster.

KubeRay es el operador que ejecuta Ray sobre Kubernetes. Tres CRD: RayCluster (de larga vida), RayJob (de un solo uso) y RayService (Ray Serve, actualizaciones progresivas). Un RayCluster mínimo:

apiVersion: ray.io/v1
kind: RayCluster
metadata:
  name: rl-cluster
spec:
  rayVersion: '2.55.0'
  headGroupSpec:
    rayStartParams: { dashboard-host: '0.0.0.0' }
    template:
      spec:
        containers:
          - name: ray-head
            image: rayproject/ray:2.55.0-py311-cu125
            resources:
              limits: { cpu: 8, memory: 32Gi }
  workerGroupSpecs:
    - groupName: gpu-workers
      replicas: 4
      minReplicas: 0
      maxReplicas: 8
      rayStartParams: {}
      template:
        spec:
          containers:
            - name: ray-worker
              image: rayproject/ray:2.55.0-py311-cu125
              resources:
                limits:
                  cpu: 16
                  memory: 128Gi
                  nvidia.com/gpu: 4

KubeRay autoescala el grupo de workers entre minReplicas y maxReplicas según la visión que tiene Ray de las tareas pendientes. Combinado con Kueue (RayJob + gang scheduling de Kueue está documentado y funciona), obtiene un Ray multiinquilino sobre un clúster K8s multiinquilino: el montaje de producción que usan de verdad la mayoría de las «casas Ray».

Cuándo es adecuado Ray por sí solo, sin K8s: un laboratorio de investigación pequeño donde una sola persona es dueña del clúster, donde el dinamismo importa más que la multiinquilinidad y donde la carga es intensiva en RL o en búsqueda de hiperparámetros. El híbrido al que converge la mayoría de los equipos: SLURM o K8s como capa base que posee los nodos y la multiinquilinidad; Ray lanzado dentro de un trabajo SLURM o de un espacio de nombres de K8s mientras dure la carga de un usuario. No instale Ray como planificador de primer nivel.

Run:ai y Determined: la gama comercial

Hay dos productos de pago que salen lo bastante a menudo como para tratarlos. Ambos atacan el mismo dolor: K8s + Volcano + Kueue + GPU Operator es mucho YAML, y algunas organizaciones prefieren comprar antes que construir.

NVIDIA Run:ai (adquirido en 2024, renombrado en 2025) es un planificador de GPU nativo de K8s. El fraccionamiento de GPU reparte una sola GPU entre varias cargas a nivel de memoria: peticiones de 0.5 GPU, redimensionado dinámico, bin-packing. Run:ai justifica su precio en entornos empresariales con más de 10 equipos de ML compitiendo por GPU compartidas. Por debajo de eso, Kueue + GPU Operator cubre casi el mismo terreno gratis.

Determined.AI (HPE, 2021) es una plataforma de entrenamiento gestionada: seguimiento de experimentos, búsqueda de hiperparámetros, gestión de checkpoints y entrenamiento distribuido en un solo producto. Encaja mejor en equipos de investigación que quieren un panel pulido sin tener que integrar cinco herramientas por su cuenta.

El problema de los notebooks interactivos

Un patrón que pilla a casi todos los clústeres de GPU compartidos: los investigadores quieren acceso a GPU por JupyterHub para desarrollar, y eso tiene que convivir con trabajos de entrenamiento largos que ocupan nodos enteros. La respuesta ingenua —una GPU dedicada por notebook— desperdicia el 80% del clúster. La respuesta estricta —obligar a los investigadores a hacer sbatch de todo— los empuja a trabajar en portátiles con modelos de juguete.

Tres patrones que funcionan: salloc de SLURM + Jupyter sobre la asignación (salloc --gres=gpu:1 --time=4:00:00, arrancar un servidor Jupyter en el nodo asignado y hacer túnel; límite de tiempo estricto y GPU bien contabilizada: la respuesta más limpia para un clúster SLURM), Kubernetes + JupyterHub + Kueue (pods de notebook en una cola de baja prioridad, los trabajos de entrenamiento expropian los notebooks ociosos) o Run:ai con fraccionamiento de GPU (cada notebook recibe 0.25 / 0.5 GPU). Elija el que elija, ponga un límite de tiempo a las sesiones interactivas: un notebook sin tiempo máximo es una GPU retirada para siempre del clúster.

La realidad honesta de un equipo pequeño

Casi todo el contenido sobre planificadores que circula por internet da por supuesto un clúster de 256 GPU y un equipo de plataforma. El cliente real de Kentino está más cerca de 1–4 nodos, 4–32 GPU en total y 2–6 usuarios que se conocen y se coordinan por Slack. Para ese montaje, no necesita ningún planificador.

# usuario 1
ssh node01
tmux new -s my-training
CUDA_VISIBLE_DEVICES=0,1,2,3 python train.py
# Ctrl-B D para desacoplar

# usuario 2 — antes se coordina por Slack
ssh node01
nvidia-smi                              # ¿qué GPU están libres?
tmux new -s other-training
CUDA_VISIBLE_DEVICES=4,5,6,7 python train.py

Ese es todo el sistema de planificación de trabajos. SLURM empieza a amortizarse en torno a las 16 GPU o los 8 usuarios, lo que llegue antes. Por debajo de ahí, la sobrecarga operativa supera al valor que aporta. Instale un planificador cuando la coordinación humana esté fallando, no antes.

Cuándo encaja cada herramienta: el resumen

Escenario Recomendación
1–2 nodos, 4–16 GPU, 2–6 usuarios que hablan entre sí SSH + tmux + nvidia-smi. Sin planificador.
Laboratorio de investigación, 4–32 nodos, entrenamiento por lotes SLURM. Aburrido, probado, encaja.
Plataforma de inferencia que sirve tráfico de clientes Kubernetes + GPU Operator. Sin planificador por lotes.
Clúster mixto: entrenamiento + inferencia + herramientas Kubernetes + Kueue + Volcano + Kubeflow.
Python distribuido intensivo: RL, búsqueda de hiperparámetros Ray (o KubeRay) sobre SLURM o K8s.
Más de 10 equipos de ML compitiendo por GPU, con presupuesto para herramientas Run:ai sobre Kubernetes.
Experimentos de entrenamiento gestionados + seguimiento Determined.AI.
Más de 200 GPU, multiequipo, multicarga Federado: SLURM para lotes, K8s para servicios.

Qué hacer a continuación: el árbol de decisión

Recórralo en orden. El primer «sí» cierra la conversación.

  1. ¿Menos de 16 GPU y menos de 8 usuarios que hablan entre sí? Sí → ningún planificador. Documente las convenciones en un fichero markdown. Revíselo cuando la coordinación se rompa.
  2. ¿El clúster ejecuta servicios de inferencia de larga ejecución además de entrenamiento? Sí → Kubernetes es la base. Añada GPU Operator, luego Kueue y luego Volcano si hay entrenamiento distribuido en juego. No → SLURM es la base.
  3. ¿Tiene un equipo de plataforma o presupuesto para montarlo (0.5–1.0 FTE durante seis meses, 0.25 FTE en régimen estable)? No → quédese en SLURM sea cual sea la mezcla de cargas. El impuesto de K8s es real.
  4. ¿>10 equipos compitiendo por tiempo de GPU con necesidades estrictas de cuota? Sí → evalúe Run:ai. No → Kueue + cohortes le lleva el 80% del camino.
  5. ¿Carga intensiva en RL, búsqueda de hiperparámetros o Python distribuido? Sí → Ray (KubeRay sobre K8s, o salloc + Ray sobre SLURM). No → deje Ray fuera.
  6. ¿Hardware compatible con MIG (tarjetas de centro de datos)? No en la gama Kentino → planifique asignación de GPU entera, o time-slicing solo en desarrollo. No diseñe alrededor de MIG.

La mayoría de las conversaciones con Kentino terminan en el paso 1, 2 o 3. El resto es decoración.

Artículos complementarios: entrenamiento distribuido en K02, clústeres de inferencia en K03, almacenamiento de clúster en K04, gestión de fallos en K06, el techo de ancho de banda de PCIe en K07.


Esto forma parte de la Kentino Wiki, una serie de referencia sobre computación de IA, robótica y los sistemas que las conectan. Comentarios y correcciones, bienvenidos en info@kentino.com.