Dispositivos y Dynamic Resource Allocation
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.
¿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.
- Previous lesson
- PodDisruptionBudget y drenado de nodos
- Next lesson
- kubectl imperativo