Entregas un contenedor que “definitivamente funciona en mi portátil”, lo despliegas en un host con GPU y tu modelo vuelve a la CPU como si fuera penitencia.
Los registros dicen “no CUDA-capable device detected”, nvidia-smi no aparece, o PyTorch amablemente te informa que CUDA no está disponible.
Mientras tanto, la GPU está ahí, aburrida, cara y facturando por hora.
Las fallas de GPU en contenedores rara vez son místicas. Casi siempre son un pequeño conjunto de desajustes: controlador vs. toolkit, runtime vs. configuración de Docker,
nodos de dispositivo vs. permisos, o un programador y una capa de seguridad haciendo exactamente lo que pediste—simplemente no lo que querías.
Un modelo mental práctico: qué significa realmente “GPU en un contenedor”
Los contenedores no virtualizan hardware. Aíslan procesos mediante namespaces y controlan el acceso a recursos mediante cgroups.
Tu GPU sigue siendo la GPU del host. El controlador del kernel sigue viviendo en el host. Los nodos de dispositivo en /dev siguen originándose en el host.
El contenedor es básicamente un proceso con una vista diferente del sistema de archivos, la red y el espacio de PID.
Para las GPU NVIDIA, la cadena crítica se ve así:
- Controlador del host: los módulos del kernel y las bibliotecas de espacio de usuario en el host. Si esto está roto, ninguna magia de contenedor te salvará.
- Nodos de dispositivo:
/dev/nvidia0,/dev/nvidiactl,/dev/nvidia-uvm, etc. Sin estos, el espacio de usuario no puede hablar con el controlador. - Integración del runtime de contenedores: algo tiene que montar los dispositivos correctos e inyectar las bibliotecas adecuadas en el contenedor. Eso es lo que hace NVIDIA Container Toolkit.
- Stack de CUDA en espacio de usuario: tu imagen de contenedor puede incluir bibliotecas CUDA, cuDNN y frameworks (PyTorch, TensorFlow). Estos deben ser compatibles con el controlador del host.
- Permisos y políticas: Docker sin root, SELinux/AppArmor, la política de dispositivos de cgroup y los contextos de seguridad de Kubernetes pueden bloquear el acceso aun cuando todo lo demás esté correcto.
Si falta algún eslabón, obtendrás los síntomas habituales: CUDA no disponible, libcuda.so no encontrada, nvidia-smi fallando,
o aplicaciones que silenciosamente recurren a la CPU.
Una verdad operacional: la ruta de la GPU no es “configúralo y olvídalo”. Es “configúralo, luego fija versiones, y luego monitoriza la deriva.”
Las actualizaciones de controlador, actualizaciones del kernel, actualizaciones de Docker y cambios en el modo de cgroup son los cuatro jinetes.
Hechos interesantes y breve historia que explican el desorden actual
- Hecho 1: Los primeros enfoques de “GPU en contenedores” usaban frágiles banderas
--devicey bibliotecas montadas a mano, porque el runtime no tenía un hook GPU estandarizado. - Hecho 2: La historia de contenedores de NVIDIA maduró cuando el ecosistema se movió hacia hooks de runtime (OCI) que pueden inyectar mounts/dispositivos al inicio del contenedor.
- Hecho 3: Las bibliotecas de usuario de CUDA pueden vivir dentro del contenedor, pero el controlador del kernel no; el controlador debe coincidir con el kernel del host y es, por naturaleza, gestionado por el host.
- Hecho 4: La “versión de CUDA” que ves en
nvidia-smino es lo mismo que el toolkit CUDA instalado en tu contenedor; refleja la capacidad del controlador, no el contenido de tu imagen. - Hecho 5: El cambio de cgroup v1 a cgroup v2 cambió cómo se comportan el acceso y la delegación de dispositivos, y rompió configuraciones GPU que funcionaban de manera sutil durante las actualizaciones.
- Hecho 6: El scheduling de GPU en Kubernetes se volvió corriente solo después de que los device plugins estandarizaron cómo anunciar y asignar GPUs; antes de eso era el Lejano Oeste de pods privilegiados.
- Hecho 7: MIG (Multi-Instance GPU) introdujo una nueva unidad de asignación—porciones de GPU—lo que convirtió “¿qué GPU obtuvo mi contenedor?” en una pregunta no trivial.
- Hecho 8: Los “contenedores sin root” son excelentes para seguridad, pero las GPU no son naturalmente amigables sin root porque los permisos de los nodos de dispositivo son una barrera rígida, no una sugerencia.
Guion de diagnóstico rápido (comprueba esto primero)
Cuando producción está en llamas, no necesitas un doctorado en empaquetado NVIDIA. Necesitas una secuencia corta que reduzca rápidamente el dominio del fallo.
Empieza por el host, luego el runtime, luego la imagen.
Primero: demuestra que la GPU del host funciona (sin contenedores aún)
- Ejecuta
nvidia-smien el host. Si falla, detente. Arregla primero la situación del controlador/kernel del host. - Confirma que existen los nodos de dispositivo:
ls -l /dev/nvidia*. Si faltan, el controlador no está cargado o udev no creó los nodos. - Revisa el estado de los módulos del kernel:
lsmod | grep nvidiaydmesgen busca de errores de GPU/controlador.
Segundo: demuestra que Docker puede conectar la GPU en un contenedor
- Ejecuta una imagen base CUDA mínima con
--gpus ally ejecutanvidia-smidentro. Si esto falla, es runtime/toolkit, no tu app. - Inspecciona la configuración del runtime de Docker: asegúrate de que el hook de runtime de NVIDIA esté instalado y seleccionado cuando sea necesario.
Tercero: demuestra que tu imagen de app es compatible
- Dentro de tu contenedor de aplicación, comprueba la visibilidad de
libcuda.soy la compilación CUDA del framework. - Valida la compatibilidad driver/toolkit: driver demasiado antiguo para el CUDA del contenedor, o contenedor esperando bibliotecas que no están presentes.
- Revisa permisos/políticas: modo rootless, SELinux/AppArmor y restricciones de dispositivos de cgroup.
El cuello de botella suele descubrirse en el paso 4 o 5. Si sigues adivinando después de eso, probablemente estés depurando tres problemas a la vez.
No lo hagas. Reduce el alcance hasta que falle una cosa a la vez.
Tareas prácticas: comandos, salida esperada y decisiones
Estas son las tareas que realmente ejecuto cuando un contenedor GPU no se comporta. Cada una incluye: el comando, qué significa la salida y qué decisión tomar.
Ejecútalas en orden si quieres una búsqueda binaria limpia a través del stack.
Tarea 1: Comprobación de salud del host con nvidia-smi
cr0x@server:~$ nvidia-smi
Thu Jan 3 10:14:22 2026
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.14 Driver Version: 550.54.14 CUDA Version: 12.4 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 NVIDIA A10 On | 00000000:17:00.0 Off | Off |
| 0% 39C P0 62W / 150W | 512MiB / 23028MiB | 3% Default |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
Significado: El controlador está cargado y habla con la GPU. La “Versión de CUDA” es lo que el controlador soporta, no lo que tu contenedor trae.
Decisión: Si esto falla, arregla primero el host: instalación del controlador, cabeceras del kernel, Secure Boot, reconstrucción DKMS, o problemas de hardware.
Tarea 2: Verificar que existan nodos de dispositivo
cr0x@server:~$ ls -l /dev/nvidia*
crw-rw-rw- 1 root root 195, 0 Jan 3 10:10 /dev/nvidia0
crw-rw-rw- 1 root root 195, 255 Jan 3 10:10 /dev/nvidiactl
crw-rw-rw- 1 root root 235, 0 Jan 3 10:10 /dev/nvidia-uvm
crw-rw-rw- 1 root root 235, 1 Jan 3 10:10 /dev/nvidia-uvm-tools
Significado: Existen dispositivos caracter; el espacio de usuario puede hablar con el controlador del kernel.
Decisión: Si faltan, carga los módulos (modprobe nvidia) e investiga por qué udev no creó nodos (o por qué el controlador falló al cargar).
Tarea 3: Comprueba que los módulos del kernel estén cargados
cr0x@server:~$ lsmod | grep -E 'nvidia|nouveau'
nvidia_uvm 1556480 0
nvidia_drm 94208 2
nvidia_modeset 1564672 1 nvidia_drm
nvidia 62480384 80 nvidia_uvm,nvidia_modeset
Significado: Módulos propietarios de NVIDIA cargados; no se muestra nouveau, lo cual suele ser bueno para nodos de cómputo CUDA.
Decisión: Si nouveau está cargado en un host de cómputo, espera problemas. Añádelo a la lista negra y reconstruye initramfs según la política de tu SO.
Tarea 4: Buscar errores del controlador en dmesg
cr0x@server:~$ dmesg -T | tail -n 12
[Thu Jan 3 10:10:05 2026] nvidia: loading out-of-tree module taints kernel.
[Thu Jan 3 10:10:05 2026] nvidia: module license 'NVIDIA' taints kernel.
[Thu Jan 3 10:10:06 2026] nvidia-nvlink: Nvlink Core is being initialized, major device number 510
[Thu Jan 3 10:10:06 2026] nvidia 0000:17:00.0: enabling device (0000 -> 0003)
[Thu Jan 3 10:10:07 2026] nvidia_uvm: Loaded the UVM driver, major device number 235
Significado: Mensajes normales de carga de módulos. Buscas “failed”; “tainted” no es el problema; “RmInitAdapter failed” sí lo es.
Decisión: Si ves fallos de inicialización, deja de perseguir Docker. Arregla el controlador/kernel/hardware primero.
Tarea 5: Confirmar que NVIDIA Container Toolkit está instalado (host)
cr0x@server:~$ dpkg -l | grep -E 'nvidia-container-toolkit|nvidia-container-runtime'
ii nvidia-container-toolkit 1.15.0-1 amd64 NVIDIA Container toolkit
ii nvidia-container-runtime 3.14.0-1 amd64 NVIDIA container runtime
Significado: Paquetes toolkit/runtime presentes (estilo Debian/Ubuntu). En sistemas RPM usarías rpm -qa.
Decisión: Si faltan, instálalos. Si están presentes pero son antiguos, actualiza—la habilitación GPU no es algo “configurar y olvidar”.
Tarea 6: Comprueba que Docker ve el runtime
cr0x@server:~$ docker info | sed -n '/Runtimes/,+3p'
Runtimes: io.containerd.runc.v2 nvidia runc
Default Runtime: runc
Significado: Docker conoce el runtime nvidia. El valor por defecto sigue siendo runc, lo cual está bien si usas --gpus.
Decisión: Si falta el runtime nvidia, el toolkit no está integrado en Docker (o Docker necesita un reinicio).
Tarea 7: Prueba mínima de contenedor con GPU
cr0x@server:~$ docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
Thu Jan 3 10:16:01 2026
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.14 Driver Version: 550.54.14 CUDA Version: 12.4 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
|===============================+======================+======================|
| 0 NVIDIA A10 On | 00000000:17:00.0 Off | Off |
+-------------------------------+----------------------+----------------------+
Significado: El runtime inyectó con éxito dispositivos y bibliotecas del driver; el contenedor puede consultar la GPU.
Decisión: Si esto falla, la imagen de tu app es irrelevante. Arregla toolkit/runtime, política o permisos de dispositivo.
Tarea 8: Inspeccionar qué dispositivos GPU fueron inyectados
cr0x@server:~$ docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 bash -lc 'ls -l /dev/nvidia*'
crw-rw-rw- 1 root root 195, 0 Jan 3 10:16 /dev/nvidia0
crw-rw-rw- 1 root root 195, 255 Jan 3 10:16 /dev/nvidiactl
crw-rw-rw- 1 root root 235, 0 Jan 3 10:16 /dev/nvidia-uvm
crw-rw-rw- 1 root root 235, 1 Jan 3 10:16 /dev/nvidia-uvm-tools
Significado: El contenedor ve la misma clase de dispositivos caracter que el host.
Decisión: Si los dispositivos no están presentes dentro del contenedor, no estás usando realmente la ruta del runtime GPU (--gpus ausente, runtime mal configurado, o política bloqueando).
Tarea 9: Validar las bibliotecas del driver inyectadas (libcuda)
cr0x@server:~$ docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 bash -lc 'ldconfig -p | grep -E "libcuda\.so|libnvidia-ml\.so" | head'
libcuda.so.1 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libcuda.so.1
libnvidia-ml.so.1 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1
Significado: El contenedor puede resolver bibliotecas clave orientadas al driver.
Decisión: Si libcuda.so falta dentro del contenedor, la inyección del toolkit está rota o estás en una imagen base no estándar que confunde al runtime.
Tarea 10: Comprueba qué pidió Docker para tu contenedor
cr0x@server:~$ docker inspect --format '{{json .HostConfig.DeviceRequests}}' gpu-test
[{"Driver":"","Count":-1,"DeviceIDs":null,"Capabilities":[["gpu"]],"Options":{}}]
Significado: El contenedor solicitó GPUs mediante la interfaz moderna de solicitudes de dispositivo (--gpus all muestra Count:-1).
Decisión: Si esto está vacío para una carga que creías habilitada para GPU, has encontrado el error: tu especificación de run/compose no solicitó una GPU.
Tarea 11: Diagnosticar cgroup v2 y delegación
cr0x@server:~$ stat -fc %T /sys/fs/cgroup/
cgroup2fs
Significado: Estás en cgroup v2. La mayoría de las distribuciones modernas lo usan. Algunas configuraciones GPU antiguas asumen comportamientos v1.
Decisión: Si el acceso a la GPU es inestable después de una actualización del SO, revalida la versión de NVIDIA Container Toolkit y tu stack de runtime de contenedores en cgroup v2.
Tarea 12: Comprobar el estado de AppArmor (bloqueador silencioso común)
cr0x@server:~$ aa-status | sed -n '1,8p'
apparmor module is loaded.
54 profiles are loaded.
51 profiles are in enforce mode.
docker-default
/usr/bin/man
/usr/sbin/sshd
Significado: AppArmor está activo. El perfil por defecto de Docker suele estar bien, pero perfiles personalizados pueden bloquear el acceso a dispositivos.
Decisión: Si hay un perfil endurecido en uso, confirma que permita el acceso requerido a /dev/nvidia* o prueba con un perfil conocido bueno.
Tarea 13: Comprobar el modo SELinux (si aplica)
cr0x@server:~$ getenforce
Enforcing
Significado: SELinux está en modo enforcing. Esto no es un problema por sí mismo; es un problema de política si está mal configurado.
Decisión: Si la GPU funciona en permissive pero no en enforcing, necesitas etiquetas/política SELinux correctas para nodos de dispositivo y runtime de contenedores.
Tarea 14: Realidad de Docker sin root
cr0x@server:~$ docker info | grep -E 'Rootless|Security Options'
Rootless: true
Security Options:
seccomp
rootless
Significado: Estás ejecutando Docker sin root. Esto es estupendo para seguridad, pero el acceso a dispositivos GPU a menudo está bloqueado porque el usuario no puede abrir los nodos de dispositivo.
Decisión: Si se requiere modo rootless, planifica un enfoque soportado (a menudo: no uses GPU vía rootless en hosts compartidos a menos que controles cuidadosamente los permisos de dispositivos).
Tarea 15: Verificar dentro de tu contenedor de app que el framework vea CUDA
cr0x@server:~$ docker run --rm --gpus all myapp:latest bash -lc 'python -c "import torch; print(torch.__version__); print(torch.cuda.is_available()); print(torch.version.cuda)"'
2.2.1
True
12.1
Significado: La compilación del framework soporta CUDA y puede inicializarla.
Decisión: Si is_available() es false pero nvidia-smi funciona, sospecha bibliotecas de usuario CUDA faltantes en la imagen, wheel solo-CPU, o libc/glibc incompatible.
Tarea 16: Atrapar el clásico desajuste “controlador demasiado antiguo”
cr0x@server:~$ docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 bash -lc 'cuda-samples-deviceQuery 2>/dev/null || true'
bash: cuda-samples-deviceQuery: command not found
Significado: Las imágenes base no incluyen samples; eso está bien. El punto es recordar que “imagen CUDA” no implica “toolkit y herramientas instaladas.”
Decisión: Si necesitas herramientas de compilación, usa una imagen devel. No instales toolchains de compilación en imágenes de runtime a menos que disfrutes CI lento y CVEs sorpresa.
Por qué falla: los grandes modos de fallo en producción
1) El controlador del host está roto (y aun así depuras Docker)
Los contenedores son un chivo expiatorio cómodo. Pero si el controlador del host no puede inicializar la GPU, no estás “a una bandera” de la solución.
Causas comunes: actualización del kernel sin reconstruir módulos DKMS, Secure Boot bloqueando módulos sin firmar, o una actualización parcial del controlador que dejó espacio de usuario y espacio kernel desincronizados.
La señal: nvidia-smi falla en el host. Todo lo demás es ruido.
2) NVIDIA Container Toolkit no está instalado o no está integrado en Docker
Docker necesita un hook OCI que añada nodos de dispositivo, monte bibliotecas del controlador e introduzca variables de entorno para capacidades.
Sin el toolkit, --gpus all o bien no hace nada o falla ruidosamente, dependiendo de las versiones.
Si usas containerd directamente, el punto de integración cambia. Si usas Kubernetes, el device plugin de GPU es otra capa donde pueden surgir problemas.
Misma idea, más piezas móviles.
3) Pediste “GPU” en tu cabeza, no en tu especificación
Pensarías que esto no puede pasar. Sucede constantemente. Archivos Compose sin la sección correcta, charts de Helm sin establecer límites de recursos,
o un trabajo CI que corre con docker run pero sin --gpus.
La peor versión es la degradación silenciosa a CPU. Tu servicio sigue “saludable”, la latencia aumenta, costes suben, y nadie lo nota hasta la revisión trimestral de rendimiento.
Broma 1: Una GPU que no está asignada a tu contenedor es solo un calefactor muy caro—excepto que ni siquiera calentará tus manos porque está inactiva.
4) Desajuste entre la capacidad del driver y el toolkit CUDA del contenedor
El controlador define qué características de runtime CUDA se pueden soportar. El contenedor define qué bibliotecas de usuario CUDA enlaza tu app.
Si envías un contenedor construido contra CUDA 12.x y lo despliegas en un host cuyo controlador solo soporta versiones más antiguas, la inicialización puede fallar.
En la práctica, los stacks modernos son más tolerantes que antes, pero “tolerante” no es un plan.
El plan correcto es: fijar versiones de controladores en el host y fijar las versiones de las imágenes base CUDA en tu build.
Trata la compatibilidad como un contrato de interfaz, no como una intuición.
5) Contenedores sin root y permisos de nodos de dispositivo
Los nodos de dispositivo están protegidos por permisos Unix y a veces por capas de seguridad adicionales.
Docker sin root ejecuta contenedores como un usuario no root, lo cual es estupendo hasta que necesitas acceder a /dev/nvidia0 y compañía.
A veces puedes resolverlo ajustando la propiedad de grupo (por ejemplo, video), reglas udev, o usando un ayudante privilegiado.
Pero sé honesto: en sistemas multi-tenant, GPU + rootless suele ser una pelea de políticas disfrazada de problema técnico.
6) La política de seguridad bloquea el acceso a la GPU (SELinux/AppArmor/seccomp)
Los sistemas de seguridad no “rompen” cosas. Hacen cumplir reglas. Si no declaraste los dispositivos GPU como permitidos, el acceso se niega.
Esto aparece como errores de permiso, o frameworks que fallan al inicializar con mensajes crípticos.
En entornos empresariales, la solución a menudo no es “desactivar SELinux”, sino “escribir la política que permita el mínimo acceso requerido al dispositivo.”
Desactivar controles de seguridad porque tu trabajo de ML llega tarde es cómo terminas con un incidente de otro tipo.
7) Kubernetes: incompatibilidad entre asignación de GPU y device plugin
En Kubernetes, el runtime del contenedor es una capa, pero la asignación es otra. Si no está instalado el device plugin de NVIDIA,
el scheduler no puede asignar GPUs y tu pod o no se programará o se ejecutará sin acceso a GPU.
Además: los recursos GPU en Kubernetes típicamente se solicitan vía límites. Si olvidas el límite, no obtienes GPU.
Esto es consistente, predecible, y aún sorprende a alguien cada semana.
8) MIG y “ve una GPU, pero no la que esperabas”
Con MIG, la “GPU” que recibes puede ser una porción. Las librerías y herramientas mostrarán identificadores distintos.
Los operadores confunden esto con “la GPU desapareció”. No: la unidad de asignación cambió.
9) Rarezas del sistema de archivos y rutas de bibliotecas (imágenes scratch, distroless, OS minimal)
Algunas imágenes mínimas no tienen ldconfig, no tienen directorios estándar de bibliotecas, o usan esquemas de enlazador dinámico no estándar.
La inyección de NVIDIA funciona montando las libs del driver en rutas esperadas. Si tu imagen es demasiado “ingeniosa”, se vuelve incompatible con ese enfoque.
Sé pragmático: si quieres minimalismo extremo, paga el impuesto de integración. De lo contrario, elige una imagen base que se comporte como una distribución Linux normal.
10) Fallos de rendimiento: “funciona”, pero es lento
Un contenedor GPU que funciona todavía puede ser un fallo en producción si es lento. Culpables comunes:
CPU sobrecargada o mal afinada, topología PCIe y NUMA desalineadas, límites de memoria del contenedor causando paginación, o cuellos de botella de almacenamiento alimentando la GPU.
Utilización de GPU al 5% con CPU alta y alto iowait no es un problema de GPU. Es un problema de pipeline de datos.
Idea parafraseada — John Ousterhout: la fiabilidad nace de la simplicidad; la complejidad es donde se esconden errores y fallos.
Errores comunes: síntomas → causa raíz → solución
“CUDA no está disponible” en PyTorch/TensorFlow, pero la GPU del host está bien
Síntoma: torch.cuda.is_available() devuelve false; no hay error obvio.
Causa raíz: Build del framework solo para CPU en el contenedor, o bibliotecas de usuario CUDA faltantes.
Solución: Instala el wheel/paquete con soporte CUDA, y asegúrate de que tu imagen base incluya runtimes CUDA compatibles. Valida con la Tarea 15.
nvidia-smi funciona en el host, falla en el contenedor: “command not found”
Síntoma: El contenedor no puede ejecutar nvidia-smi.
Causa raíz: Tu imagen no incluye nvidia-smi (forma parte de las utilidades de usuario de NVIDIA), incluso si el passthrough GPU es correcto.
Solución: Usa imágenes base nvidia/cuda para depuración, o instala nvidia-utils-* dentro de tu imagen si realmente lo necesitas (a menudo no lo necesitas).
nvidia-smi falla en el contenedor: “Failed to initialize NVML”
Síntoma: Errores NVML dentro del contenedor.
Causa raíz: libnvidia-ml.so faltante/montado incorrectamente, librerías de driver desajustadas, o problema de permisos/política.
Solución: Valida la inyección del toolkit (Tareas 5–9). Confirma que los nodos de dispositivo existen en el contenedor (Tarea 8). Revisa SELinux/AppArmor (Tareas 12–13).
El contenedor se ejecuta, pero no hay GPUs visibles
Síntoma: El framework ve cero dispositivos; /dev/nvidia* ausente.
Causa raíz: No solicitaste GPUs (--gpus ausente) o la especificación Compose/K8s no tiene la solicitud de GPU.
Solución: Añade --gpus all (o dispositivos específicos) y verifica con la Tarea 10. En Kubernetes, establece correctamente los límites de recurso de GPU.
Funciona como root, falla como usuario no root
Síntoma: Root puede ejecutar CUDA; usuario no root recibe permiso denegado.
Causa raíz: Permisos/Grupos de nodos de dispositivo no permiten acceso, especialmente con Docker sin root.
Solución: Revisa la propiedad de los nodos de dispositivo (reglas udev), membresía de grupos, o evita el modo rootless para cargas GPU en ese host (Tarea 14).
Después de una actualización del SO, los contenedores GPU dejaron de funcionar
Síntoma: Todo estaba bien, luego “de repente” roto.
Causa raíz: Actualización del kernel rompió la compilación DKMS de NVIDIA, o cambió el modo de cgroup, o Docker/containerd se actualizó sin toolkit compatible.
Solución: Revalida el controlador del host (Tareas 1–4), paquetes del toolkit (Tarea 5), runtimes de Docker (Tarea 6) y modo de cgroup (Tarea 11).
Máquina con múltiples GPUs: el contenedor ve la GPU equivocada
Síntoma: El trabajo se ejecuta en GPU 0, pero querías GPU 2; o múltiples trabajos se solapan.
Causa raíz: No hay selección explícita de dispositivo; dependencia de orden implícito; falta de scheduler.
Solución: Usa --gpus '"device=2"' o un scheduler con asignación adecuada. Para MIG, valida los IDs de slice y la lógica de asignación.
Funciona, pero el rendimiento es terrible
Síntoma: Baja utilización de GPU, tiempo de ejecución alto.
Causa raíz: Cuellos de botella en data loaders, throughput de almacenamiento, afinamiento de CPU, desajuste NUMA, o tamaños de batch pequeños que infrautilizan la GPU.
Solución: Perfila de extremo a extremo: revisa uso de CPU, iowait y throughput de almacenamiento. No “optimices” la GPU hasta probar que la GPU es el cuello de botella.
Tres micro-historias corporativas desde la trinchera
Incidente causado por una suposición equivocada: “La versión CUDA en nvidia-smi es la que está en el contenedor”
Una empresa mediana desplegó un nuevo contenedor de inferencia construido contra un runtime CUDA más reciente. El equipo revisó los nodos GPU y vio
nvidia-smi reportando una versión reciente de CUDA. Asumieron que la historia driver+runtime estaba alineada y pusieron en producción.
El servicio no se cayó. Se degradó. El framework decidió silenciosamente que CUDA no era usable y recurrió a la CPU. La latencia se disparó, el autoscaling entró en acción,
y el cluster creció como si hubiera descubierto un buffet libre.
El ingeniero en on-call hizo lo correcto: dejó de mirar los logs de la app y ejecutó una prueba mínima con un contenedor CUDA. Falló por un desajuste de capacidad del driver.
La “Versión de CUDA” en nvidia-smi era capacidad del driver, no una promesa de que el stack en espacio de usuario del contenedor se inicializaría.
La solución fue aburrida: fijar la versión del driver del host compatible con el runtime CUDA del contenedor, y añadir un job de preflight que ejecute
nvidia-smi dentro de la imagen de producción exacta. También añadieron una alerta cuando la utilización de GPU baja mientras la CPU sube—una señal clarísima de fallback silencioso.
Optimización que salió mal: recortar la imagen hasta romper la inyección GPU
Otra compañía tenía un mandato de seguridad para reducir imágenes de contenedor. Alguien se puso ambicioso: cambió de una base distro estándar
a un enfoque minimal/distroless, eliminó caches del enlazador dinámico y vació directorios “innecesarios”.
La imagen era más pequeña, el escáner de vulnerabilidades estaba más feliz, y la demo funcionó en CI. Luego la producción empezó a fallar solo en ciertos nodos.
La misma imagen corría en un pool GPU y fallaba en otro. Ese tipo de inconsistencia hace que los adultos digan palabras que no deberían estar en tickets.
La causa raíz no fue “NVIDIA es inestable.” Fue una suposición sutil sobre cómo el runtime inyecta las librerías del controlador y espera la disposición del sistema de archivos del contenedor.
La imagen minimal no tenía las rutas estándar de búsqueda de bibliotecas, así que las librerías inyectadas existían pero no eran localizables por el enlazador dinámico.
Revirtieron la imagen base a una convencional para cargas GPU, y luego aplicaron un endurecimiento medido:
builds multi-stage, mantener las imágenes runtime ligeras pero no hostiles, y verificar con una prueba de integración que ejecute una inicialización CUDA real.
Broma 2: Nada dice “seguridad” como una imagen de contenedor tan mínima que no encuentra sus propias bibliotecas.
Práctica aburrida pero correcta que salvó el día: nodo dorado + versiones fijadas + tests canarios
Una empresa regulada ejecutaba cargas GPU en un clúster dedicado. Tenían dos reglas que sonaban burocráticas:
(1) Los nodos GPU se construyen desde una imagen dorada con kernel fijado, driver NVIDIA fijado y toolkit fijado.
(2) Cada cambio pasa por un pool canario que ejecuta tests sintéticos GPU cada hora.
Una semana, una actualización del repositorio del SO introdujo un parche de kernel que era inofensivo para cómputo general pero activó un problema de reconstrucción DKMS en su entorno.
El pool canario se iluminó en una hora: nvidia-smi falló en nodos nuevos; la prueba sintética en contenedores también falló.
Debido a que el cluster usaba versiones fijadas, el radio del impacto quedó confinado. Ningún nodo de producción derivó automáticamente.
El equipo detuvo la actualización, arregló la pipeline de build, reconstruyó la imagen dorada y la promovió después de que los canarios pasaran.
La moraleja no es “nunca actualizar.” Es “actualiza a propósito.” Los stacks GPU son un problema de tres cuerpos: kernel, driver, runtime de contenedor.
Quieres un proceso que impida que la física se convierta en astrología.
Endurecimiento y fiabilidad: hacer que los contenedores GPU sean aburridos
Fija versiones como si realmente importara
El stack GPU tiene múltiples ejes de versión: kernel, driver, toolkit de contenedor, runtime CUDA, framework y a veces NCCL.
Si los dejas flotar independientemente, eventualmente crearás una combinación incompatible.
Haz esto:
- Fija versiones del driver NVIDIA del host por pool de nodos.
- Fija etiquetas de imágenes base CUDA (no uses
latesta menos que odies dormir). - Fija versiones de frameworks y registra qué variante de CUDA instalaste.
- Actualiza en conjunto como un paquete en un despliegue controlado.
Haz que la disponibilidad de GPU sea una señal SLO explícita
“El servicio está arriba” no es lo mismo que “el servicio está usando la GPU.” Para inferencia, la degradación silenciosa a CPU es una bomba de coste y de latencia.
Para entrenamiento, es una bomba de calendario.
Operativamente: emite métricas de utilización de GPU, memoria GPU y una métrica binaria “CUDA inicializada correctamente” al inicio del proceso.
Alerta por una discrepancia (CPU alta, GPU baja) para servicios dependientes de GPU.
Separa “imágenes de depuración” de “imágenes de producción”
Quieres imágenes pequeñas en producción. También quieres herramientas cuando depuras a las 03:00. No confundas los objetivos.
Mantén una variante de depuración que incluya bash, procps, quizá pciutils, y la capacidad de ejecutar nvidia-smi o una pequeña prueba CUDA.
Las imágenes de producción pueden seguir siendo ligeras.
Ten cuidado con la escalada de privilegios
El passthrough GPU no requiere --privileged en la mayoría de las configuraciones. Si lo usas “porque arregla cosas”, probablemente estés tapando
una configuración de runtime faltante o una regla de política. Los contenedores privilegiados amplían la superficie de ataque y complican las revisiones de cumplimiento.
No ignores NUMA y la topología PCIe
Una vez que la GPU es visible, los problemas de rendimiento suelen rastrearse hasta la topología: los hilos de CPU del contenedor se programan en un nodo NUMA lejos de la GPU,
o la NIC y la GPU están en sockets distintos provocando tráfico entre sockets.
Los contenedores no arreglan automáticamente una mala colocación. Tu scheduler y la configuración del nodo lo hacen.
Listas de verificación / planes paso a paso
Paso a paso: habilitar soporte GPU en un host Docker nuevo (NVIDIA)
-
Instala y valida el controlador del host.
Ejecuta la Tarea 1. Sinvidia-smifalla, no procedas. -
Confirma los nodos de dispositivo.
Ejecuta la Tarea 2. La ausencia de nodos significa que el controlador no está cargado correctamente. -
Instala NVIDIA Container Toolkit.
Valida con la Tarea 5. -
Reinicia Docker y confirma runtimes.
Valida con la Tarea 6. -
Ejecuta una prueba mínima de contenedor GPU.
La Tarea 7 debería tener éxito. -
Bloquea versiones.
Registra kernel, driver, toolkit, versión de Docker. Fíjalos por el pool de nodos.
Paso a paso: depurar una carga en producción que falla
- Confirma salud de la GPU del host (Tarea 1). Si está rota, detente.
- Confirma la ruta del runtime con una imagen CUDA conocida buena (Tarea 7).
- Confirma dispositivos y bibliotecas en el contenedor (Tareas 8–9).
- Confirma que la carga realmente solicitó GPU (Tarea 10).
- Revisa las capas de política (Tareas 12–14), especialmente después de cambios de endurecimiento.
- Revisa el stack de la app (Tarea 15) por builds solo-CPU o dependencias faltantes.
- Solo entonces persigue rendimiento: CPU, I/O, tamaño de batch, colocación NUMA.
Checklist operacional: prevenir regresiones
- Mantén un pool de nodos GPU canario. Ejecuta tests sintéticos GPU cada hora.
- Alerta en “GPU esperada pero sin uso”: GPU baja + CPU alta para servicios etiquetados GPU.
- Fija y despliega upgrades de driver/toolkit/kernel como una unidad.
- Registra cambios de modo cgroup como cambios que rompen compatibilidad.
- Mantén un contenedor de depuración que pueda ejecutar validaciones básicas GPU rápidamente.
- Documenta los patrones exactos de Compose/Helm que solicitan GPUs; prohibe configuraciones ad-hoc por copia/pega.
Preguntas frecuentes
1) ¿Por qué funciona nvidia-smi en el host pero no dentro del contenedor?
Por lo general porque el runtime del contenedor no inyectó los dispositivos/bibliotecas (toolkit faltante o --gpus ausente),
o una política de seguridad bloquea el acceso a dispositivos. Valida con las Tareas 6–9 y 12–13.
2) ¿Necesito instalar el driver NVIDIA dentro del contenedor?
No. El controlador del kernel debe estar en el host. El contenedor normalmente lleva bibliotecas de usuario CUDA (runtime/toolkit/framework),
mientras que NVIDIA Container Toolkit inyecta las bibliotecas del driver del host necesarias para hablar con el controlador del kernel.
3) ¿Qué significa realmente la “Versión de CUDA” en nvidia-smi?
Indica la capacidad máxima de CUDA soportada por el controlador instalado. No te dice qué toolkit CUDA está en tu imagen de contenedor.
Trátalo como “el driver habla hasta este dialecto CUDA”, no como “toolkit instalado”.
4) ¿Debería establecer el runtime por defecto de Docker a nvidia?
En la mayoría de setups modernos, no. Usa --gpus y mantén el runtime por defecto como runc.
Establecer nvidia como predeterminado puede sorprender a cargas no GPU y complicar la depuración.
5) ¿Por qué mi framework reporta CUDA no disponible aun cuando nvidia-smi funciona?
Porque nvidia-smi solo demuestra que NVML puede hablar con el controlador. Los frameworks también necesitan las bibliotecas runtime CUDA correctas,
la compilación del framework correcta (no solo-CPU) y a veces una glibc compatible. La Tarea 15 es tu prueba rápida de verdad.
6) ¿Puedo usar GPUs con Docker sin root?
A veces, pero espera fricción. El problema central es el permiso para abrir los nodos de dispositivo /dev/nvidia*.
Si no puedes garantizar permisos de dispositivo y alineación de políticas, rootless + GPU se vuelve un riesgo de fiabilidad.
7) Mi contenedor ve la GPU, pero el rendimiento es horrible. ¿Docker lo está ralentizando?
La sobrecarga de Docker rara vez es el culpable principal para cómputo GPU. Los problemas de rendimiento suelen venir de cuellos de botella CPU/I/O,
colocación NUMA, tamaños de batch pequeños o starvation del pipeline de datos. Prueba que la GPU sea el limitador antes de “optimizar” CUDA.
8) ¿Cuál es la forma más segura de dar acceso GPU a un contenedor?
Solicita solo las GPUs que necesites (no all por defecto), evita --privileged, y confía en la ruta de inyección del runtime NVIDIA.
Combina eso con fijado estricto de imágenes y aislamiento de pools de nodos para cargas GPU.
9) ¿Cómo prevengo la degradación silenciosa a CPU?
Añade checks de inicio que fallen rápido si la inicialización de CUDA falla, expón una métrica que indique uso de GPU, y alerta por discrepancias CPU/GPU.
“Saludable pero equivocado” es una trampa clásica de fiabilidad.
Conclusión: pasos prácticos siguientes
Los problemas de GPU en contenedores parecen caóticos porque hay múltiples capas, y cada capa falla de manera distinta.
La solución es disciplina: valida el host, valida la inyección del runtime, luego valida la imagen de la aplicación.
No te saltes pasos. No depures por intuición.
Pasos siguientes que puedes hacer hoy:
- Ejecuta el guion de diagnóstico rápido en un host roto y en uno conocido bueno; compara las salidas.
- Añade una prueba smoke en CI que ejecute
nvidia-smi(o inicialización CUDA del framework) dentro de tu imagen de producción. - Fija versiones de driver/toolkit/imagen CUDA y desplázalas juntas a través de un pool canario.
- Construye una alerta de “GPU esperada pero sin uso” para capturar degradación silenciosa antes de que lo haga finanzas.