Lesson  in  Kubernetes 101

Dispositivos y Dynamic Resource Allocation

Una GPU no se pide con requests: se reclama. Aplica la reclamación del recomendador sin tener ninguna GPU delante y lee los cuatro mensajes que te va dando el clúster, cada uno nombrando exactamente la pieza que falta.

Todo lo que has repartido hasta ahora era CPU y memoria: recursos divisibles y homogéneos, donde da igual de qué núcleo salgan esos 500 milicores. Un dispositivo no funciona así. Una GPU tiene un modelo, una memoria propia y una posición en el bus, y se asigna entera o en porciones que decide su driver, no tú. Por eso no se pide con requests: se reclama.

El recomendador es la pieza de la tienda que existe para esto, y es la única que en el libro nunca llega a arrancar. En este playground tampoco va a arrancar — no hay ninguna GPU en estas máquinas. Pero eso no es el final de la lección: es la lección. Vas a aplicar la reclamación y a leer los cuatro mensajes que el clúster te va dando, uno por cada pieza que falta, hasta llegar al único punto donde hace falta hardware de verdad.

Trabaja desde la pestaña dev-machine.

Paso 1: Los tipos ya están ahí

Antes de aplicar nada, comprueba una cosa:

kubectl api-resources --api-group=resource.k8s.io

Cuatro tipos: DeviceClass, ResourceClaim, ResourceClaimTemplate y ResourceSlice. Nadie ha instalado nada en este clúster. Dynamic Resource Allocation es núcleo de la API, no un CRD que traiga el fabricante: el fabricante instala el driver, pero los tipos ya venían puestos. Es el mismo reparto de papeles del capítulo de Almacenamiento —DeviceClass hace de StorageClass, ResourceClaim de PVC, ResourceSlice de PersistentVolume— con un driver detrás, como en CSI.

Ahora crea recomendador.yaml:

cat << 'EOF' > recomendador.yaml
apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: gpu-24gb
spec:
  spec:
    devices:
      requests:
      - name: gpu
        exactly:
          deviceClassName: gpu-inferencia
          allocationMode: ExactCount
          count: 1
          selectors:
          - cel:
              expression: |-
                device.capacity["gpu.example.com"].memory
                  .compareTo(quantity("24Gi")) >= 0
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: recomendador
spec:
  replicas: 1
  selector:
    matchLabels:
      app: recomendador
  template:
    metadata:
      labels:
        app: recomendador
    spec:
      resourceClaims:
      - name: gpu
        resourceClaimTemplateName: gpu-24gb
      containers:
      - name: recomendador
        image: ghcr.io/iximiuz/labs/nginx:alpine
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          claims:
          - name: gpu
EOF

¿Qué dice esa reclamación? No dice «una GPU». Dice «un dispositivo de la clase gpu-inferencia, exactamente uno, y que tenga al menos 24 GiB de memoria». Esa última condición es la expresión CEL, y es la razón de ser de DRA: con el device plugin clásico solo puedes pedir nvidia.com/gpu: 1, que significa «una, la que sea». Aquí puedes elegir.

¿Y por qué una plantilla y no un ResourceClaim a secas? Porque un ResourceClaim es un objeto único: si el Deployment tuviera tres réplicas, se lo repartirían, y eso casi nunca es lo que quieres con un dispositivo. Con la plantilla, Kubernetes genera una reclamación por Pod.

Aplícalo y mira las dos cosas a la vez:

kubectl apply -f recomendador.yaml
kubectl get pods -l app=recomendador
kubectl get resourceclaim

El Pod está en Pending, como esperabas. Pero el ResourceClaim existe, con un nombre derivado del Pod, y también está pending. Comprueba de quién es:

kubectl get resourceclaim -o jsonpath='{.items[0].metadata.ownerReferences[0].kind}'

Pod. Esa reclamación se creó con el Pod y desaparecerá con él. Ahí está la diferencia entre plantilla y reclamación, en un comando en vez de en un párrafo.

Paso 2: El primer mensaje

Un Pod en Pending no dice nada por sí solo. El evento sí:

kubectl describe pod -l app=recomendador | tail -5
0/3 nodes are available: request gpu: device class gpu-inferencia
does not exist.

La clase no existe. Y no existe porque la clase no la escribe la aplicación: la crea quien opera el clúster, igual que la StorageClass. Ponte ese sombrero un momento:

cat << 'EOF' > deviceclass.yaml
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: gpu-inferencia
spec:
  selectors:
  - cel:
      expression: device.driver == "gpu.example.com"
EOF

Fíjate en que la DeviceClass no lleva namespace —es de ámbito de clúster— y en que su selector no dice cuántas GPUs hay ni cuánta memoria tienen: dice qué dispositivos pertenecen a esta clase. El «cuánto quiero» es cosa de la reclamación; el «de qué tipo hay» es cosa de la clase.

kubectl apply -f deviceclass.yaml

Y ahora no toques el Pod. No hace falta borrarlo ni reiniciar nada: el scheduler reintenta solo los Pods pendientes cuando cambia el estado del clúster. Espera unos segundos y vuelve a mirar:

kubectl describe pod -l app=recomendador | tail -5
0/3 nodes are available: 3 cannot allocate all claims.

Mensaje distinto, pieza distinta. Ya no falta la clase: falta el dispositivo. Nadie ha publicado ninguna GPU en ningún nodo, así que no hay nada que asignar. Eso es lo que hace un driver DRA, y este clúster no tiene ninguno.

Paso 3: Publicar una GPU que no existe

Un ResourceSlice es lo que el driver publica en cada nodo con lo que encuentra allí. Nadie lo escribe a mano... salvo hoy, para ver el mecanismo por dentro. Publica dos dispositivos con memorias distintas, que es justo el caso que el selector tiene que resolver:

cat << 'EOF' > resourceslice.yaml
apiVersion: resource.k8s.io/v1
kind: ResourceSlice
metadata:
  name: nodo-simulado-gpu
spec:
  driver: gpu.example.com
  nodeName: node-01
  pool:
    name: node-01
    generation: 1
    resourceSliceCount: 1
  devices:
  - name: gpu-0
    capacity:
      memory:
        value: 24Gi
  - name: gpu-1
    capacity:
      memory:
        value: 8Gi
EOF
kubectl apply -f resourceslice.yaml
kubectl get resourceclaim
kubectl get pods -l app=recomendador -o wide

La reclamación pasa a allocated,reserved y el Pod se programa en node-01. Ahora la pregunta que importa: ¿con cuál de las dos se ha quedado?

kubectl get resourceclaim \
  -o jsonpath='{range .items[*]}{.status.allocation.devices.results[0].device}{end}'

gpu-0. La de 8 GiB ni se ha considerado, porque tu expresión CEL la descarta. El kube-scheduler ha cruzado dos cosas a la vez —qué nodos tienen dispositivos que encajan y cuáles tienen sitio— y ha elegido nodo y dispositivo en la misma decisión. Eso es lo que un device plugin clásico no puede hacer: él solo sabe contar.

Paso 4: Donde se acaba la simulación

El Pod tiene nodo, tiene dispositivo asignado... y sigue sin arrancar. Mira el último evento:

kubectl describe pod -l app=recomendador | tail -3
Failed to prepare dynamic resources: DRA driver gpu.example.com
is not registered

Y aquí se acaba el truco, de la forma más honesta posible. Has descrito un dispositivo, pero describirlo no lo hace existir: falta el plugin del kubelet que prepare esa GPU en el nodo y se la entregue al contenedor. Eso ya no se simula con YAML.

Mira ahora los cuatro eventos seguidos, en orden:

kubectl get events --field-selector involvedObject.kind=Pod --sort-by=.lastTimestamp

Cuatro filas, recortadas aquí a lo que importa —el REASON y el final del MESSAGE—:

Warning  FailedScheduling               ... device class gpu-inferencia does not exist
Warning  FailedScheduling               ... 3 cannot allocate all claims
Normal   Scheduled                      Successfully assigned default/recomendador-... to node-01
Warning  FailedPrepareDynamicResources  ... DRA driver gpu.example.com is not registered

Cuatro líneas y cuatro piezas, cada una nombrando exactamente lo que le faltaba: la clase, el dispositivo, el nodo (ese sí lo consiguió) y el driver. Ningún Pod te va a contar mejor cómo funciona DRA.

Las dos banderas hacen falta las dos. --sort-by=.lastTimestamp porque kubectl get events no garantiza el orden cronológico y aquí el orden es justamente lo que se lee. Y el --field-selector porque, sin él, entre medias aparecen los eventos del Deployment y del ReplicaSet —«scaled up», «created pod»— que no cuentan nada de DRA.

Note

¿Y cuándo NO usar DRA? Si tu carga pide una GPU entera y te da igual cuál, nvidia.com/gpu: 1 con el device plugin clásico sigue siendo perfectamente válido, y es bastante más simple. DRA gana cuando necesitas elegir: «una con al menos 24 GiB», «dos que estén en el mismo bus», «una compartida entre estos tres Pods». La pregunta nunca es cuál es más moderno, sino cuál resuelve tu problema con menos piezas.

Para llegar hasta el final con dispositivos de mentira pero un driver de verdad, existe dra-example-driver; con GPUs reales, el driver DRA de NVIDIA.

Next lesson
kubectl imperativo