Volcano y Kueue: gang scheduling, colas y cuotas GPU para cargas distribuidas en Kubernetes

TL;DR

Volcano (volcano-sh, CNCF incubating) es un scheduler batch completo que reemplaza o complementa al kube-scheduler: coloca pods con semántica gang (todo-o-nada via PodGroup/minMember), gestiona colas con prioridad, fair-share DRF y preemption entre colas, y entiende topología de red y NUMA.

Kueue (kubernetes-sigs/kueue) es un gestor de colas y cuotas a nivel de Job: NO coloca pods (delega en kube-scheduler o en Volcano), pero decide cuándo un workload puede ser admitido según cuota disponible (ClusterQueue/LocalQueue/Cohort), con fair sharing, borrowing entre equipos y preemption por prioridad. Integra nativamente Job, JobSet, RayJob, todos los Kubeflow operators y más.

La combinación ganadora en producción para cargas GPU multi-tenant es: Kueue para cuota/colas + Volcano (o el plugin coscheduling de sig-scheduler) para el gang del job.


La analogía

Imagina un club con capacidad limitada y una sala de baile dentro.

Kueue es el portero y el gestor de reservas: comprueba si la cuota del equipo (aforo reservado) permite entrar al grupo, aplica la lista de espera justa, presta aforo de otros equipos si los hay ociosos, y recupera el sitio cuando el dueño lo necesita. Pero el portero no decide dónde se sienta cada persona dentro del local.

Volcano es el jefe de sala: cuando el grupo ya tiene permiso para entrar, él decide en qué mesas se sientan, asegura que todo el grupo se sienta a la vez o ninguno entra (gang), elige las mesas según topología (quién necesita hablar con quién) y expulsa grupos de menor prioridad para hacer hueco si es necesario.

Sin portero (Kueue), el jefe de sala no sabe cuántos grupos puede acoger a la vez ni si un equipo se está pasando de aforo. Sin jefe de sala (Volcano), el portero deja entrar al grupo pero los integrantes se dispersan solos por las mesas disponibles —y el grupo de 8 que necesita sentarse junto nunca lo consigue.


El problema que ninguno resuelve por defecto: el kube-scheduler

El kube-scheduler de Kubernetes es un scheduler de pods, no de jobs. Asigna pods de uno en uno al nodo más adecuado según recursos disponibles y constraints de afinidad. Para una carga de entrenamiento distribuido que necesita, por ejemplo, 8 pods con 4 GPUs cada uno (32 GPUs en total sobre 8 nodos 4×H100), el scheduler estándar hace lo siguiente:

  1. Busca un nodo con 4 GPUs disponibles. Lo encuentra. Programa el pod 1.
  2. Busca otro nodo con 4 GPUs. Lo encuentra. Programa el pod 2.
  3. Continúa hasta que llega al pod 6 y resulta que ya no hay nodos con 4 GPUs libres: el cluster tiene exactamente 32 GPUs y hay otros workloads usando algunas.
  4. Los pods 1–5 están Running. Los pods 6–8 están Pending.
  5. Los pods 1–5 no pueden hacer nada sin los demás: un trabajo PyTorch distribuido necesita que todos los workers arranquen para iniciar el proceso torchrun. Espera con los recursos ocupados. Deadlock.

Esto no es un bug, es el diseño: el kube-scheduler no tiene el concepto de “programa este grupo de pods solo si puedes programarlos todos”. En consecuencia:

  • Los recursos de los pods 1–5 están bloqueados sin producir trabajo.
  • Otros jobs que podrían correr con los recursos parciales también esperan.
  • Si hay varios jobs en esta situación, el cluster puede quedar con recursos fragmentados, ningún job corriendo y todo el mundo en deadlock circular.

Además, el kube-scheduler no tiene noción de:

  • Colas por equipo/proyecto con prioridad relativa.
  • Cuotas de recursos por equipo con capacidad de borrowing entre equipos ociosos.
  • Fair-share: si el equipo A lleva semanas usando el 80 % del cluster, debería esperar más que el equipo B que lleva semanas en idle.
  • Preemption inter-queue: expulsar un job de menor prioridad de otro equipo para hacer sitio al job urgente de este equipo.

Resolver cualquiera de estos problemas requiere añadir una capa sobre el scheduler. Volcano y Kueue son las dos soluciones OSS dominantes en 2026, con enfoques arquitectónicos complementarios.


Volcano: el scheduler batch

Qué es y qué reemplaza

Volcano (volcano-sh) es un scheduler batch Kubernetes-native aceptado por CNCF como su primer y único proyecto oficial de scheduling batch de contenedores (volcano.sh/en/docs). En versión v1.15.x a junio de 2026, con estado CNCF incubating.

Volcano no es un addon al kube-scheduler: es un scheduler alternativo (o complementario, según la configuración) que coloca pods. Se instala como un deployment y los jobs que quieran beneficiarse de sus capacidades deben usar la clase de scheduler volcano en su pod spec (schedulerName: volcano) o usar el CRD VolcanoJob.

La propuesta de valor central de Volcano es que trata grupos de pods como unidades atómicas de scheduling, no pods individuales. Esto es lo que permite resolver el deadlock descrito arriba.

Gang scheduling via PodGroup

El mecanismo central de Volcano es el PodGroup: un CRD que agrupa los pods de un job y define cuántos deben poder programarse antes de que Volcano arranque cualquiera de ellos.

# PodGroup para un job de entrenamiento PyTorch distribuido: 8 workers, mínimo 8
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
  name: pytorch-train-pg
  namespace: ml-training
spec:
  minMember: 8          # all-or-nothing: si no hay sitio para 8, ninguno arranca
  minResources:
    nvidia.com/gpu: "32" # 8 pods × 4 GPUs = 32 GPUs mínimas en el cluster
  queue: team-datos      # a qué cola Volcano se asigna este job
  priorityClassName: high-priority

El parámetro minMember implementa la semántica all-or-nothing: Volcano solo asigna nodos a los pods del grupo cuando puede asignar al menos minMember pods simultáneamente. Si el cluster no tiene capacidad para 8 pods GPU en este momento, ningún pod del grupo se mueve de Pending. Nada se bloquea, nada se fragmenta.

minMember puede ser menor que el total de pods del job: esto permite elastic gang scheduling, donde el job puede arrancar con menos workers y escalar, útil para jobs tolerantes a workers reducidos.

Para que los pods de un job se asocien al PodGroup, llevan la anotación:

# Pod spec del worker PyTorch
metadata:
  annotations:
    scheduling.volcano.sh/pod-group-name: pytorch-train-pg
spec:
  schedulerName: volcano
  containers:
    - name: trainer
      image: pytorch/pytorch:2.5-cuda12.4
      resources:
        limits:
          nvidia.com/gpu: "4"

Queue: colas con cuotas y prioridad

Volcano introduce el CRD Queue para gestionar múltiples tenants con cuotas independientes:

apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: team-datos
spec:
  weight: 4           # peso relativo para fair-share entre colas (proportion plugin)
  capability:         # techo absoluto de recursos que puede usar esta cola
    nvidia.com/gpu: "16"
  guarantee:          # recurso garantizado, nunca prestado a otras colas
    resource:
      nvidia.com/gpu: "8"
  reclaimable: true   # si true, otros pueden reclamar los recursos que presta cuando los necesiten
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: team-ia
spec:
  weight: 2
  capability:
    nvidia.com/gpu: "8"
  guarantee:
    resource:
      nvidia.com/gpu: "4"
  reclaimable: true

El campo weight alimenta el plugin proportion: las colas compiten por los recursos disponibles del cluster en proporción a su peso. Un cluster con 32 GPUs y dos colas de peso 4 y 2 reparte las GPUs en proporción 4:2 (≈21 y 11 GPUs respectivamente) cuando ambas están saturadas.

Plugins del scheduler: DRF, binpack, topology-aware

Volcano implementa su lógica de scheduling como un pipeline de acciones y plugins:

Acciones (qué hace el scheduler en cada ciclo):

  • enqueue: mueve jobs de cola waiting a schedulable cuando hay cuota disponible.
  • allocate: asigna nodos a pods schedulable.
  • preempt: expulsa pods de menor prioridad para hacer sitio a los de mayor prioridad dentro de la misma cola.
  • reclaim: expulsa pods de otras colas que están usando por encima de su guarantee para devolver recursos al dueño.
  • backfill: rellena recursos ociosos con jobs best-effort que no interfieren con los demás.

Plugins relevantes para cargas GPU:

PluginQué hace
gangImplementa la semántica all-or-nothing del PodGroup
proportionFair-share por peso de cola (cuota proporcional)
capacityCuotas con guarantee/capability y reclaim; alternativa más expresiva a proportion
drfDominant Resource Fairness: fair-share multi-dimensional (CPU, memoria, GPU)
binpackCompacta pods en los nodos más llenos; reduce fragmentación GPU
priorityOrdena jobs por prioridad dentro de la misma cola
nodeorderPuntuación de nodos según múltiples criterios (afinidad, recursos, spread)
task-topologyAfinidad entre pods del mismo job (comunicación inter-GPU)
numa-awareNUMA affinity: alinea pods con el socket NUMA del nodo para reducir latencia de memoria

Topología de red y NUMA (v1.11+)

Volcano v1.11 (febrero 2025) introdujo Network Topology Aware Scheduling como feature de primera clase (CNCF blog, marzo 2025). Los jobs de entrenamiento distribuido en un datacenter con estructura de red jerárquica (spine/leaf, bloques de nodos con NVSwitch) pueden declarar restricciones de topología:

# VolcanoJob con restricción de topología de red
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: llm-pretrain
  namespace: ml-training
spec:
  minAvailable: 8
  schedulerName: volcano
  queue: team-datos
  plugins:
    ssh: []
    env: []
    svc: []
  networkTopology:
    mode: hard                 # hard: el job DEBE cumplir la restricción
    highestTierAllowed: block  # los pods no pueden spannear más allá de un "bloque" de red
  tasks:
    - replicas: 8
      name: worker
      template:
        spec:
          containers:
            - name: trainer
              image: nvcr.io/nvidia/pytorch:25.01-py3
              resources:
                limits:
                  nvidia.com/gpu: "4"

La semántica highestTierAllowed: block instruye a Volcano a colocar los 8 pods dentro del mismo bloque de red (por ejemplo, todos los nodos bajo el mismo switch de acceso), minimizando el tráfico inter-bloque que degrada el all-reduce distribuido.

La NUMA awareness funciona de forma similar: con el plugin numa-aware, los pods solicitan una política NUMA (single-numa-node, restricted, best-effort) y Volcano selecciona nodos donde los recursos CPU/memoria/GPU solicitados están en el mismo dominio NUMA, evitando el overhead de acceso a memoria remota (NUMA-crossing) que puede degradar el throughput de entrenamiento un 15-30 % en nodos multi-socket.

GPU virtualization en v1.11+

Volcano v1.11 también introduce soporte para MIG dinámico y vCUDA: en lugar de declarar nvidia.com/gpu: 1 para una GPU entera, los workloads pueden declarar nvidia.com/gpu-memory: 20Gi y Volcano (con el device plugin correspondiente) provisiona dinámicamente la instancia MIG o la partición vCUDA. Esto es [marketing del proyecto sin benchmarks independientes publicados a junio 2026], pero la arquitectura del feature está documentada en el código.

Qué reemplaza/añade al scheduler por defecto

Capacidadkube-schedulerVolcano
Colocar pods en nodosSí (lo reemplaza para workloads marcados con schedulerName: volcano)
Gang scheduling (all-or-nothing)NoSí (PodGroup + minMember)
Colas con prioridadNoSí (Queue CRD)
Fair-share inter-queueNoSí (DRF, proportion, capacity plugins)
Preemption inter-queueNoSí (reclaim action)
Topology-aware (NUMA, network)Parcial (node affinity)Sí (plugins dedicados)
Elastic gang (minMember < total)No
Backfill best-effortNo
Integraciones de frameworksParcialMPI, PyTorch, Ray, TensorFlow, Spark, Flink, Horovod

Kueue: el gestor de colas y cuotas

Qué es y qué NO hace

Kueue (kubernetes-sigs/kueue) es un sistema kubernetes-native que gestiona cuotas y cómo los jobs las consumen (kueue.sigs.k8s.io). Kueue decide cuándo un job debe esperar, cuándo debe ser admitido (pods pueden crearse) y cuándo debe ser expulsado (pods activos deben borrarse).

El principio de diseño central de Kueue es explícito en su documentación: evitar duplicar funcionalidad madura de componentes Kubernetes. El autoscaling es responsabilidad del cluster-autoscaler. El scheduling de pod-a-nodo es responsabilidad del kube-scheduler. La gestión del ciclo de vida del job es responsabilidad del kube-controller-manager. Kueue no reemplaza ninguno de ellos: se coloca encima como una capa de admission control y gestión de cuotas a nivel de Job.

Esto es la distinción fundamental: Kueue no coloca pods en nodos. Cuando Kueue admite un workload, simplemente permite que el controller del job correspondiente cree los pods, y esos pods son programados por el kube-scheduler (o por Volcano, si está configurado como scheduler).

Los cuatro objetos nucleares

ResourceFlavor: mapea recursos abstractos a grupos de nodos físicos concretos.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: h100-sxm
spec:
  nodeLabels:
    accelerator: h100-sxm
    node-pool: gpu-training
  tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"

ClusterQueue: define la cuota de recursos por flavor para un tenant. Objeto cluster-scoped.

apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: cq-team-datos
spec:
  cohort: llm-platform           # cohort al que pertenece (puede prestar/pedir prestado)
  queueingStrategy: BestEffortFIFO
  namespaceSelector:
    matchLabels:
      kubernetes.io/metadata.name: ns-datos
  resourceGroups:
    - coveredResources: ["nvidia.com/gpu", "cpu", "memory"]
      flavors:
        - name: h100-sxm
          resources:
            - name: "nvidia.com/gpu"
              nominalQuota: 16        # GPUs garantizadas para este equipo
              borrowingLimit: 8       # puede tomar hasta 8 GPUs adicionales del cohort
              lendingLimit: 8         # puede prestar hasta 8 de sus 16 GPUs nominales
            - name: "cpu"
              nominalQuota: "128"
            - name: "memory"
              nominalQuota: "512Gi"
  preemption:
    reclaimWithinCohort: LowerPriority   # recupera quota prestada expulsando jobs de menor prioridad
    borrowWithinCohort:
      policy: LowerPriority
    withinClusterQueue: LowerPriority

LocalQueue: punto de entrada namespace-scoped para los workloads de un equipo. Los jobs apuntan a su LocalQueue; Kueue los mapea al ClusterQueue correspondiente.

apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: lq-datos
  namespace: ns-datos
spec:
  clusterQueue: cq-team-datos

Cohort: agrupa ClusterQueues que pueden prestarse quota entre sí. No es un CRD independiente; se declara como campo en el ClusterQueue (spec.cohort: nombre). Kueue agrega la quota disponible de todos los ClusterQueues en el cohort y permite que cualquiera tome prestado lo que otros no usan, respetando los borrowingLimit y lendingLimit.

Fair sharing y preemption

Kueue implementa Fair Sharing como política de ordenación de la cola de workloads pendientes (kueue.sigs.k8s.io/docs/concepts/fair_sharing): cuando hay varios workloads compitiendo por quota en el cohort, los que pertenecen a ClusterQueues con mayor uso histórico acumulado tienen menor prioridad de admisión. Esto implementa reparto equitativo sin bloquear permanentemente a ningún equipo.

La preemption en Kueue opera en dos dimensiones:

  • reclaimWithinCohort: el ClusterQueue que presta cuota la recupera expulsando workloads que la están usando prestada, según política de prioridad.
  • withinClusterQueue: dentro del mismo ClusterQueue, workloads de menor prioridad son expulsados para dar paso a workloads de mayor prioridad del mismo equipo.

Gang semantics en Kueue: All-or-nothing con ready Pods

Kueue proporciona admisión gang a nivel de Job: admite el workload completo solo cuando toda la cuota necesaria está disponible. Si un RayJob necesita 8 GPUs (1 head + 7 workers), Kueue no admite el workload hasta que haya 8 GPUs disponibles en el ClusterQueue (o prestadas del cohort). El waitForPodsReady con timeout añade una segunda garantía: si los pods creados no pasan a Ready en el tiempo configurado, Kueue re-encola el workload liberando la cuota (kueue.sigs.k8s.io/docs/tasks/manage/setup_wait_for_pods_ready).

Esta es una semántica gang a nivel de admisión, no a nivel de placement de pod. Garantiza que la cuota esté disponible antes de crear los pods, pero no garantiza que el kube-scheduler pueda colocarlos todos en nodos concretos al mismo tiempo. Para esa segunda garantía se necesita Volcano o el plugin coscheduling.

Topology-Aware Scheduling (TAS)

Kueue v0.10+ introduce Topology-Aware Scheduling (kueue.sigs.k8s.io/docs/concepts/topology_aware_scheduling): permite definir topologías de nodos (bloques, subblocks, hosts) y que los workloads soliciten niveles de co-localización. Kueue solo admite el workload cuando puede satisfacer la restricción topológica, y añade node selectors/taints al momento de la admisión para que el scheduler coloque los pods en la topología correcta.

TAS se configura con el CRD Topology:

apiVersion: kueue.x-k8s.io/v1beta1
kind: Topology
metadata:
  name: datacenter-topology
spec:
  levels:
    - nodeLabel: "topology.kubernetes.io/block"
    - nodeLabel: "topology.kubernetes.io/rack"
    - nodeLabel: "kubernetes.io/hostname"

Y el ResourceFlavor referencia la topología:

apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: h100-sxm
spec:
  nodeLabels:
    accelerator: h100-sxm
  topologyName: datacenter-topology

Integraciones de frameworks

Kueue tiene integración built-in (sin código adicional) para los siguientes tipos de workload, activadas con una anotación en el job:

metadata:
  labels:
    kueue.x-k8s.io/queue-name: lq-datos  # apunta a la LocalQueue del equipo

Los tipos de workload soportados incluyen: batch/Job, JobSet, RayJob, RayCluster, PytorchJob, TFJob, MPIJob, JAXJob, PaddleJob, XGBoostJob, TrainJob, AppWrapper, LeaderWorkerSet, Deployment, StatefulSet, y plain Pod/PodGroup.

Para cargas LLM, los casos directamente relevantes:

  • RayJob (entrenamiento distribuido con Ray Train): Kueue admite el RayJob cuando hay cuota para todo el cluster Ray (head + workers). Documentado en docs.ray.io.
  • PyTorchJob (Kubeflow Training Operator): admisión gang del job completo.
  • JobSet: para jobs multi-réplica coordinados (LWS, pipelines multi-step).
  • Deployment/StatefulSet: para inferencia continua, permitiendo gestionar la cuota de GPUs de inferencia igual que las de entrenamiento.

La distinción clave: Volcano coloca, Kueue admite

Esta tabla resume la diferencia arquitectónica fundamental:

DimensiónVolcanoKueue
Rol principalScheduler (coloca pods en nodos)Admission controller + gestor de cuotas (decide cuándo crear pods)
Gang schedulingSí, nivel de placement (PodGroup/minMember)Sí, nivel de admisión (all-or-nothing de cuota)
Cuotas multi-tenantSí (Queue con capability/guarantee)Sí (ClusterQueue con nominalQuota/borrowingLimit)
Cohorts / borrowingParcial (reclaimable entre queues)Sí (Cohort con lendingLimit/borrowingLimit explícito)
Fair sharingSí (DRF plugin)Sí (Fair Sharing basado en uso histórico)
PreemptionSí (preempt + reclaim actions)Sí (reclaimWithinCohort, withinClusterQueue)
Topology/NUMASí (plugins dedicados, network topology, NUMA-aware)Sí (TAS, niveles de topología en ResourceFlavor)
Integraciones de frameworksVolcano Job (MPI, PyTorch, Ray, TF, Spark, Flink)Nativo: Job, JobSet, RayJob, Kubeflow, LWS, AppWrapper, Deployment, StatefulSet
Quién coloca los podsVolcanokube-scheduler (o Volcano si está configurado)
Huella de instalaciónMedia-alta (scheduler propio, CRDs, webhook, metrics)Ligera (controller, CRDs, webhook; no reemplaza scheduler)
Madurez / estadoCNCF incubating; v1.15 (junio 2026); producción en Huawei, Baidu, DiDikubernetes-sigs; API v1beta2; producción en Google GKE, Red Hat OpenShift 4.20, Runway ML
Curva de adopciónMayor (requiere cambiar schedulerName o usar VolcanoJob CRD)Menor (añade labels a jobs existentes; no cambia el scheduler)

Cómo coexisten: el patrón producción

En producción, Kueue y Volcano son complementarios, no excluyentes. El patrón más común en 2026 para clusters GPU multi-tenant es:

  1. Kueue gestiona la cuota global: qué equipo puede usar cuántas GPUs, cuánto puede pedir prestado, cuándo un job entra en cola vs. se admite.
  2. Volcano hace el gang scheduling a nivel de pod: una vez que Kueue admite el job (la cuota está disponible), Volcano coloca los pods asegurando que todos se colocan simultáneamente en nodos compatibles.

La integración se configura especificando schedulerName: volcano en los pod specs de los workloads gestionados por Kueue. Kueue ve el Job/RayJob/PyTorchJob y gestiona su cuota; cuando lo admite, los pods se crean y Volcano los coloca con semántica gang. Los PodGroups de Volcano se crean automáticamente por el Volcano Job controller o por el propio Kubeflow Training Operator cuando detecta que el scheduler es Volcano.

# PyTorchJob gestionado por Kueue (cuota) + Volcano (gang placement)
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: llm-finetune-70b
  namespace: ns-datos
  labels:
    kueue.x-k8s.io/queue-name: lq-datos   # Kueue gestiona la cuota
  annotations:
    scheduling.volcano.sh/queue-name: team-datos  # Volcano usa su Queue
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      restartPolicy: OnFailure
      template:
        spec:
          schedulerName: volcano             # Volcano hace el placement
          containers:
            - name: pytorch
              image: nvcr.io/nvidia/pytorch:25.01-py3
              resources:
                limits:
                  nvidia.com/gpu: "4"
    Worker:
      replicas: 7
      restartPolicy: OnFailure
      template:
        spec:
          schedulerName: volcano
          containers:
            - name: pytorch
              image: nvcr.io/nvidia/pytorch:25.01-py3
              resources:
                limits:
                  nvidia.com/gpu: "4"

La tercera vía: plugin coscheduling de sig-scheduler

Si no quieres desplegar un scheduler alternativo completo pero necesitas gang scheduling, existe el plugin coscheduling de kubernetes-sigs/scheduler-plugins. Este plugin extiende el kube-scheduler con un mecanismo de PodGroup similar al de Volcano, implementado como plugin de scheduling framework (permit plugin). La ventaja es que no reemplaza el scheduler; la desventaja es que tiene menos funcionalidad que Volcano (sin DRF, sin Queue/fair-share, sin network topology). Es la opción correcta para clusters simples que solo necesitan gang y no quieren la complejidad operativa de Volcano. Kueue puede funcionar también junto a este plugin.


Ejemplos YAML completos

Volcano: Queue + PodGroup + VolcanoJob

# 1. Queue para equipo datos (16 GPUs nominales, techo en 24)
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: team-datos
spec:
  weight: 4
  capability:
    nvidia.com/gpu: "24"
    cpu: "192"
    memory: "768Gi"
  guarantee:
    resource:
      nvidia.com/gpu: "16"
  reclaimable: true
---
# 2. Queue para equipo ia (8 GPUs nominales, techo en 16)
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: team-ia
spec:
  weight: 2
  capability:
    nvidia.com/gpu: "16"
    cpu: "64"
    memory: "256Gi"
  guarantee:
    resource:
      nvidia.com/gpu: "8"
  reclaimable: true
---
# 3. PodGroup: job de fine-tuning de 70B, 8 workers × 4 GPUs = 32 GPUs
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
  name: finetune-70b-pg
  namespace: ns-datos
spec:
  minMember: 8
  minResources:
    nvidia.com/gpu: "32"
  queue: team-datos
  priorityClassName: training-high
---
# 4. VolcanoJob (wrapper que Volcano entiende nativamente)
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: finetune-70b
  namespace: ns-datos
spec:
  minAvailable: 8
  schedulerName: volcano
  queue: team-datos
  priorityClassName: training-high
  plugins:
    env: []
    svc: []
  policies:
    - event: PodEvicted
      action: RestartJob
  tasks:
    - replicas: 8
      name: worker
      policies:
        - event: TaskCompleted
          action: CompleteJob
      template:
        metadata:
          annotations:
            scheduling.volcano.sh/pod-group-name: finetune-70b-pg
        spec:
          schedulerName: volcano
          containers:
            - name: trainer
              image: nvcr.io/nvidia/pytorch:25.01-py3
              command: ["torchrun", "--nproc_per_node=4", "--nnodes=8",
                        "--node_rank=$(RANK)", "--master_addr=$(MASTER_ADDR)",
                        "--master_port=23456", "train.py"]
              env:
                - name: NCCL_DEBUG
                  value: "INFO"
              resources:
                requests:
                  nvidia.com/gpu: "4"
                  cpu: "16"
                  memory: "64Gi"
                limits:
                  nvidia.com/gpu: "4"
                  cpu: "16"
                  memory: "64Gi"
          restartPolicy: Never

Kueue: ResourceFlavor + ClusterQueue + LocalQueue + Job anotado

# 1. ResourceFlavor: nodos con H100 SXM
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: h100-sxm
spec:
  nodeLabels:
    accelerator: h100-sxm
  tolerations:
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"
---
# 2. ClusterQueue equipo datos: 16 GPUs nominales, puede pedir 8 prestadas
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: cq-team-datos
spec:
  cohort: llm-platform
  queueingStrategy: BestEffortFIFO
  namespaceSelector:
    matchLabels:
      kubernetes.io/metadata.name: ns-datos
  resourceGroups:
    - coveredResources: ["nvidia.com/gpu", "cpu", "memory"]
      flavors:
        - name: h100-sxm
          resources:
            - name: "nvidia.com/gpu"
              nominalQuota: 16
              borrowingLimit: 8
              lendingLimit: 8
            - name: "cpu"
              nominalQuota: "128"
            - name: "memory"
              nominalQuota: "512Gi"
  preemption:
    reclaimWithinCohort: LowerPriority
    borrowWithinCohort:
      policy: LowerPriority
    withinClusterQueue: LowerPriority
---
# 3. ClusterQueue equipo ia: 8 GPUs nominales
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: cq-team-ia
spec:
  cohort: llm-platform
  queueingStrategy: BestEffortFIFO
  namespaceSelector:
    matchLabels:
      kubernetes.io/metadata.name: ns-ia
  resourceGroups:
    - coveredResources: ["nvidia.com/gpu", "cpu", "memory"]
      flavors:
        - name: h100-sxm
          resources:
            - name: "nvidia.com/gpu"
              nominalQuota: 8
              borrowingLimit: 8
              lendingLimit: 4
            - name: "cpu"
              nominalQuota: "64"
            - name: "memory"
              nominalQuota: "256Gi"
  preemption:
    reclaimWithinCohort: LowerPriority
    withinClusterQueue: LowerPriority
---
# 4. LocalQueue en el namespace del equipo datos
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: lq-datos
  namespace: ns-datos
spec:
  clusterQueue: cq-team-datos
---
# 5. LocalQueue en el namespace del equipo ia
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: lq-ia
  namespace: ns-ia
spec:
  clusterQueue: cq-team-ia
---
# 6. RayJob de inferencia batch gestionado por Kueue
apiVersion: ray.io/v1
kind: RayJob
metadata:
  name: batch-eval-llama70b
  namespace: ns-datos
  labels:
    kueue.x-k8s.io/queue-name: lq-datos    # Kueue gestiona la admisión
spec:
  entrypoint: "python batch_eval.py --model /models/llama-70b"
  rayClusterSpec:
    headGroupSpec:
      rayStartParams:
        num-gpus: "4"
      template:
        spec:
          containers:
            - name: ray-head
              image: rayproject/ray-ml:2.40.0-gpu
              resources:
                limits:
                  nvidia.com/gpu: "4"
                  cpu: "16"
                  memory: "64Gi"
    workerGroupSpecs:
      - replicas: 3
        minReplicas: 3        # gang: Kueue no admite si no hay cuota para 3 workers
        maxReplicas: 3
        groupName: gpu-worker
        rayStartParams:
          num-gpus: "4"
        template:
          spec:
            containers:
              - name: ray-worker
                image: rayproject/ray-ml:2.40.0-gpu
                resources:
                  limits:
                    nvidia.com/gpu: "4"
                    cpu: "16"
                    memory: "64Gi"

Kueue + Volcano juntos: PyTorchJob con cuota Kueue y gang placement Volcano

# PyTorchJob: Kueue controla la cuota, Volcano hace el gang placement
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: distributed-finetune
  namespace: ns-datos
  labels:
    kueue.x-k8s.io/queue-name: lq-datos      # Kueue: cuota y admisión
  annotations:
    # Volcano crea automáticamente el PodGroup cuando schedulerName=volcano
    scheduling.volcano.sh/queue-name: team-datos
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      restartPolicy: OnFailure
      template:
        spec:
          schedulerName: volcano              # Volcano: placement gang
          containers:
            - name: pytorch
              image: nvcr.io/nvidia/pytorch:25.01-py3
              resources:
                limits:
                  nvidia.com/gpu: "4"
                  cpu: "16"
                  memory: "64Gi"
    Worker:
      replicas: 7
      restartPolicy: OnFailure
      template:
        spec:
          schedulerName: volcano
          containers:
            - name: pytorch
              image: nvcr.io/nvidia/pytorch:25.01-py3
              resources:
                limits:
                  nvidia.com/gpu: "4"
                  cpu: "16"
                  memory: "64Gi"

Para cargas LLM: entrenamiento, fine-tuning e inferencia batch

Entrenamiento y fine-tuning distribuido multi-GPU

El gang scheduling es imprescindible para cualquier job de entrenamiento distribuido que use NCCL all-reduce (PyTorch DDP, FSDP, DeepSpeed ZeRO). Si un solo worker del grupo no arranca, el torchrun coordinator espera indefinidamente; con el kube-scheduler estándar este escenario ocurre cada vez que el cluster está bajo contención.

En un cluster genérico de 4 nodos 4×H100 SXM (16 GPUs totales), un job de fine-tuning de un modelo de 70B requiere típicamente 8 GPUs en tensor-parallel 8 (TP=8) o 16 GPUs en TP=4 + data-parallel 4. Con Volcano, el PodGroup con minMember: 8 garantiza que o se colocan los 8 pods a la vez o ninguno bloquea recursos. Con Kueue encima, la cuota garantiza que el equipo no supera sus 16 GPUs nominales y que otros equipos con cuota disponible no quedan bloqueados por un job esperando.

El cross-link con capacity planning de inferencia es directo: el presupuesto de VRAM del modelo (pesos + KV-cache) determina el TP mínimo y por tanto el minMember del PodGroup.

Inferencia batch y evaluaciones (evals)

Los jobs de inferencia batch —generar respuestas para un dataset de evaluación, procesar embeddings masivos, re-ranking offline— son cargas naturalmente paralelas que no necesariamente requieren gang scheduling estricto (cada request es independiente), pero sí se benefician de cuota y fair-share.

Para estas cargas, Kueue solo es suficiente: un batch/Job con múltiples pods independientes se gestiona con la cuota del ClusterQueue sin necesidad de Volcano. Si hay varios equipos enviando jobs de evaluación simultáneamente, Kueue ordena la admisión por fair-share y prioridad, y con borrowing del cohort los jobs de equipos con cuota libre no tienen que esperar la cuota de otros equipos ocupados.

El chargeback de estas cargas se conecta directamente con lo descrito en chargeback y showback de GPU: el nominalQuota del ClusterQueue es la expresión del presupuesto de GPU en Kubernetes, y OpenCost puede atribuir el coste por namespace/label para el informe mensual.

Cuota GPU multi-tenant y chargeback

La alineación entre Kueue y el sistema de chargeback es directa:

Concepto FinOpsMecanismo Kueue
Presupuesto garantizado de GPUnominalQuota por ClusterQueue
Techo de gasto máximonominalQuota + borrowingLimit
Préstamo de capacidad ociosaCohort + lendingLimit
Recuperar la cuota propiapreemption.reclaimWithinCohort: LowerPriority
Fair-share entre equiposFair Sharing policy en ClusterQueue
Chargeback del préstamohoras de GPU prestada × coste/GPU-hora (OpenCost)

Para la dimensión de utilización como palanca FinOps, ver la utilización de GPU como palanca FinOps: Kueue + Volcano juntos permiten maximizar la utilización sin sacrificar las garantías de cuota, que es exactamente el objetivo FinOps.

La gestión de particiones MIG dentro de este sistema (declarar nvidia.com/mig-4g.40gb como recurso en el ClusterQueue) se integra naturalmente: el ResourceFlavor puede mapear a nodos con perfil MIG específico, como se explica en compartir GPU: time-slicing, MPS y MIG.


Diagrama: flujo de un job de entrenamiento con Kueue + Volcano

Job de entrenamiento distribuido: Kueue (cuota) + Volcano (gang placement)Usuario / CIkubectl apply PyTorchJobKueue controller¿cuota disponible en CQ?Cola de esperafair-share / prioridadAdmisión (cuota OK)pods permitidos; CQ reserva GPUVolcano schedulerPodGroup: minMember=8 gangsNodos 4×H100 SXM8 pods × 4 GPU — todos a la vezSi no hay nodosningún pod se coloca → esperaKueue: gestiona cuota, cohorts, fair-share, preemption entre colasVolcano: gang placement (todo-o-nada), topology-aware, NUMA, DRF entre QueuesLos dos niveles son ortogonales: Kueue no ve nodos, Volcano no ve cuota de equipo

Tabla comparativa completa

CriterioVolcanoKueuePlugin coscheduling
RolScheduler (placement)Admission + cuota (no placement)Plugin kube-scheduler (placement)
Gang schedulingSí, nivel pod (PodGroup/minMember)Sí, nivel admisión (quota gang)Sí, nivel pod (PodGroup)
Cuotas multi-tenantSí (Queue capability/guarantee)Sí (ClusterQueue nominalQuota)No
Cohorts / borrowingLimitado (reclaimable)Sí (Cohort con borrowingLimit/lendingLimit)No
Fair-shareSí (DRF, proportion)Sí (Fair Sharing por uso histórico)No
Preemption inter-queueSí (reclaim action)Sí (reclaimWithinCohort)No
Topología red / NUMASí (v1.11+, plugins dedicados)Sí (TAS, Topology CRD)No
Integraciones nativasMPI, PyTorch, Ray, TF, Spark, Flink, HorovodJob, JobSet, RayJob, Kubeflow, LWS, AppWrapper, Deployment, StatefulSetCualquier job con PodGroup
Quién coloca los podsVolcanokube-scheduler (o Volcano)kube-scheduler
Elastic gangSí (minMember < total replicas)Parcial (partial admission en batch/Job)Limitado
HuellaMedia-alta (scheduler propio)Ligera (controller adicional)Mínima (plugin del scheduler)
Compatibilidad con KueueSí (como scheduler bajo Kueue)Sí (opción complementaria)
Estado CNCF / madurezCNCF incubating, v1.15kubernetes-sigs, v1beta2, adoptado en GKE/OpenShiftkubernetes-sigs/scheduler-plugins, experimental
Cuándo elegirloEntrenamiento distribuido HPC-like, NUMA, topología red, MPIMulti-tenancy con cuota flexible, cargas heterogéneas, inferencia + batch juntosClusters simples que solo necesitan gang sin scheduler propio

Pitfalls operativos y escepticismo honesto

1. Deadlock por gang mal configurado

El escenario más frecuente: minMember configurado igual al total de réplicas en un cluster donde varios jobs compiten por los mismos recursos. Si dos jobs de 8 pods cada uno intentan usar un cluster con 8 nodos GPU y el job A tiene 4 pods colocados (no son 8, Volcano los retiene en pending), y el job B también tiene 4 pods retenidos, nadie avanza. Volcano hace bien su trabajo: no coloca ninguno hasta que haya sitio para los 8. Pero si los nominalQuota de las colas están mal dimensionados respecto a la capacidad real del cluster, esto genera esperas indefinidas.

Solución: dimensionar las cuotas de las colas para que la suma de guarantee no supere la capacidad real, y que los minMember de los jobs activos quepan en la cuota disponible. El autoscaling de nodos con ProvisioningRequest (Kueue + cluster-autoscaler) ayuda, pero introduce latencia de provisioning que hay que contemplar en el SLA del job.

2. Cuota vs capacidad real: el drift silencioso

El nominalQuota de Kueue o el guarantee de Volcano son declaraciones administrativas. No garantizan que los nodos con esas GPUs estén disponibles, saludables o que el device plugin las haya registrado correctamente. Un nodo en NotReady con 4 GPUs reduce la capacidad real sin que Kueue lo sepa: el ClusterQueue seguirá admitiendo workloads que luego no podrán colocarse.

Monitorización recomendada: cruzar las métricas de Kueue (kueue_admitted_workloads_total, kueue_pending_workloads) con las métricas de capacidad real del cluster (GPUs registradas en el device plugin) para detectar el drift. Kueue expone métricas Prometheus nativas; Volcano también.

3. Naming de recursos GPU: MIG, time-slicing y ResourceFlavor

Si el cluster usa MIG, los recursos en los pod specs cambian de nvidia.com/gpu a nvidia.com/mig-Xg.Ygb (por ejemplo, nvidia.com/mig-3g.40gb). Los ResourceFlavor de Kueue y las Queue de Volcano deben declarar el recurso correcto, o la cuota no matcheará con los pods. Con time-slicing, el recurso sigue siendo nvidia.com/gpu pero el device plugin anuncia más instancias de las GPUs físicas; la cuota se expresa en réplicas virtuales, lo que puede llevar a sobre-admisión si no se contempla el presupuesto de VRAM (ver compartir GPU: time-slicing, MPS y MIG).

4. Preemption en producción: el workload expulsado pierde progreso

Cuando Kueue o Volcano expulsan un job de entrenamiento que ha corrido durante horas, ese job pierde el progreso si no tiene checkpointing configurado. La preemption es correcta desde el punto de vista de la cuota, pero destruye trabajo si el job no está preparado. Antes de habilitar preemption agresiva, verificar que todos los jobs de entrenamiento tienen checkpointing periódico con restauración automática. PyTorch + Torchrun tienen soporte nativo; DeepSpeed también. Los jobs de inferencia batch sin estado no tienen este problema.

5. Volcano como scheduler único vs. coexistencia con kube-scheduler

Volcano puede configurarse como scheduler por defecto del cluster (todos los pods pasan por él) o como scheduler alternativo (solo los pods con schedulerName: volcano). La primera opción simplifica la configuración pero rompe pods del sistema que asumen comportamientos del kube-scheduler. La segunda (recomendada) requiere que los jobs de ML marquen explícitamente schedulerName: volcano, lo que puede ser un cambio de operator/chart no trivial para cargas existentes. Kueue resuelve esto de forma más transparente: solo requiere un label en el job, sin cambiar el scheduler.

6. Complexidad operativa real en 2026

Ejecutar Kueue + Volcano + el Training Operator + el GPU Operator en producción son cuatro componentes con sus propios CRDs, webhooks, versiones y ciclos de release. Una actualización de Kubernetes puede requerir actualizar los cuatro en secuencia. El debt operativo es real. Para un equipo pequeño sin capacidad de mantener este stack, la opción de un proveedor de Kubernetes gestionado (GKE con Kueue nativo, OpenShift con Red Hat build of Kueue) puede ser más pragmática que montar el stack completo desde cero.

El plugin coscheduling de sig-scheduler es una opción deliberadamente más simple cuando solo se necesita gang: menos features, menos complejidad, menos cosas que mantener.


Ver también


Fuentes