CUDA: Cómo NVIDIA Encerró a Toda una Industria

¿Te fue útil?

El informe postmortem del incidente no dice “CUDA” en el título. Nunca lo hace. Dice cosas como “pipeline de entrenamiento detenido”, “nodos GPU infrautilizados” o mi favorito personal, “regresión del modelo por deriva del entorno”.
Pero investigas y ahí está: un parche de driver, una incompatibilidad de toolkit, una reconstrucción “menor” del contenedor, y tus costosas GPUs ahora son estupendos calefactores.

CUDA no solo hizo a las GPUs útiles para cómputo general. Las hizo operativamente confiables dentro de sus propias reglas, y luego organizó el mundo para que la mayoría de cargas serias siguieran esas reglas. Eso es lock-in. No es maldad. No es magia. Simplemente una estrategia de producto implacablemente buena más un ecosistema que se compone.

Qué es realmente CUDA (y qué no lo es)

CUDA no es “la GPU”. CUDA es la plataforma de computación paralela y el modelo de programación de NVIDIA, además de las librerías en espacio de usuario, la cadena de herramientas del compilador y el runtime que hacen que las GPUs NVIDIA se sientan como un objetivo estable.
Si ejecutas ML, interactúas mayormente con CUDA de forma indirecta: PyTorch llama a cuDNN y cuBLAS; el entrenamiento distribuido utiliza NCCL; tu servidor de inferencia llama a TensorRT; tus kernels quizá se compilan con NVCC; y el driver está debajo de todo fingiendo que esto es normal.

El genio estratégico de CUDA es que es simultáneamente:

  • Un ecosistema de lenguaje (CUDA C/C++, PTX, intrínsecos de dispositivo, flujos de compilación).
  • Un contrato de runtime (API de driver vs API de runtime, gestión de contextos, modelo de memoria, streams/events).
  • Un imperio de librerías (cuBLAS, cuDNN, NCCL, cuFFT, TensorRT, CUTLASS y más).
  • Una historia de rendimiento (kernels afinados por arquitectura, operaciones fusionadas, tensor cores, captura de gráficas).
  • Una historia operativa (nvidia-smi, NVML, MIG, DCGM, empaquetado de drivers, contenedores).

El lock-in no requiere malicia. Requiere una puerta de un solo sentido: entras porque es más rápido y fácil, y más tarde descubres que la salida es una escalera estrecha detrás de una máquina expendedora.

La abstracción clave: “escribe una vez, ejecuta lo bastante rápido”

CUDA dio a los desarrolladores un modelo estable y razonablemente de alto nivel para cómputo en GPU. Escribes kernels. Gestionas transferencias de memoria. Lanzás grids/blocks. Es lo suficientemente bajo nivel como para obtener velocidad y lo suficientemente alto nivel como para entregar.
Luego NVIDIA siguió añadiendo características de arquitectura (tensor cores, mejoras de memoria unificada, nuevas jerarquías de caché) manteniendo una historia de compatibilidad que suele funcionar siempre que no la contradigas.

Qué CUDA no es

  • No es solo un compilador. El compilador es la parte visible; las librerías y el contrato del driver son la gravedad.
  • No es inherentemente “mejor en matemáticas”. Es mejor en entregar una cadena productizada desde la matemática hasta la velocidad y el despliegue.
  • No es un único número de versión. Tienes versiones de driver, versiones de toolkit, versiones de librerías, builds de frameworks e imágenes base de contenedor. Ellos negocian en tiempo de ejecución como un comité con incentivos.

Broma #1: Si crees que tu versión de CUDA es “la que pip instaló”, tengo un puente que venderte—compilado con la capability de cómputo equivocada.

Cómo ocurre el lock-in: la mecánica técnica

“Lock-in” suena a drama de compras. En la práctica es matemática de ingeniería: coste de migración = (reescrituras + pérdida de rendimiento + riesgo operativo) × (número de cargas) ÷ (tu tolerancia al dolor).
CUDA incrementa el coste de migración en varios ejes a la vez.

1) El foso de librerías: cuDNN, cuBLAS, TensorRT, NCCL

La mayoría del código en producción no llama a kernels CUDA crudos. Llama a frameworks, que llaman a librerías del proveedor. Esas librerías están afinadas hasta los detalles de microarquitectura:
tamaños de tiles para tensor cores, patrones de acceso a memoria, epílogos fusionados, selección heurística de kernels, incluso “esta forma de convolución prefiere este kernel en esta GPU”.

Pueden implementarse equivalentes en pilas alternativas, pero la paridad es difícil porque la “API” es la parte fácil; el 30% final del rendimiento son años de perfilado, cirugía de kernels y atención a casos raros.

2) La cadena de compilación: PTX, SASS, fatbins y compatibilidad hacia adelante

CUDA compila código de dispositivo en una mezcla de:

  • PTX (una ISA virtual, una especie de IR tipo ensamblador GPU).
  • SASS (código máquina real para una arquitectura GPU específica).
  • Fat binaries que contienen múltiples variantes.

Esto importa operativamente. Si envías solo SASS para una arquitectura, quedas atado a esas GPUs. Si envías PTX, dependes del JIT del driver para compilar para la GPU instalada, lo que te liga a características del driver y puede añadir latencia al arranque.

3) El contrato del driver: “driver más nuevo ejecuta CUDA más antigua” (mayormente)

El modelo de compatibilidad de NVIDIA es parte importante del lock-in porque reduce el miedo diario. Puedes estandarizar en una rama de driver y luego permitir un rango de toolkits CUDA en contenedores.
La industria aprendió a construir prácticas operativas alrededor de esto: “driver en el host, toolkit en el contenedor.” Ese patrón ahora es memoria muscular.

La trampa es la palabra “mayormente”. Los casos límite se convierten en tus pagers:
GPUs nuevas que requieren drivers nuevos, frameworks que esperan un libcudart más reciente, NCCL que necesita cierto comportamiento del driver, y toolchains antiguas encontrándose con kernels modernos como si fuera una cita a ciegas desastrosa.

4) La gravedad del ecosistema: frameworks, tutoriales, contratación y CI

CUDA se volvió el defecto en ML. Eso significa:

  • La mayoría de ingenieros de ML se forman en flujos de trabajo centrados en CUDA.
  • La mayoría de librerías de terceros publican ruedas CUDA primero.
  • Los consejos de rendimiento asumen contadores y perfiles de NVIDIA.
  • La mayoría de guías para entrenamiento distribuido asumen NCCL.

Este es un lock-in más silencioso: el coste de reentrenar a personas y refactorizar sistemas de build. No aparece en gráficos de benchmark, pero aparece en las fechas de entrega.

5) Características de hardware expuestas vía herramientas CUDA

Cuando NVIDIA lanza una característica de hardware nueva, suele lanzarla con una historia CUDA: APIs, librerías y soporte de perfilador. Si quieres la característica, adoptas la herramienta.
Ejemplos incluyen tensor cores (y las capas de librerías que los rodean), particionado MIG para multi-tenant, y colectivas conscientes de NVLink para escalado multi-GPU.

Hechos e historia que explican el dominio

Aquí están los puntos de contexto concretos que la gente olvida cuando reduce CUDA a “bloqueo del proveedor”. Es más interesante que eso.

  1. CUDA se lanzó en 2007 y hizo que la programación GPU de propósito general se sintiera como C, no como un hack sobre APIs gráficas.
  2. Antes de CUDA, GPGPU a menudo significaba abuso de shaders: empaquetar cálculo en shaders de píxel/vértice mediante OpenGL/DirectX, lo cual era ingenioso y miserable.
  3. cuDNN (2014) fue un punto de inflexión: cargas de trabajo de deep learning obtuvieron una librería de kernels optimizada y soportada por el proveedor en la que los frameworks pudieron confiar.
  4. NCCL normalizó el multi-GPU proporcionando colectivas en anillo/árbol ajustadas para interconexiones y topologías NVIDIA. El entrenamiento distribuido dejó de ser deporte exclusivo de HPC.
  5. Tensor cores (era Volta) cambiaron el juego de “la GPU hace FLOPs” a “la GPU hace matemáticas tipo ML extremadamente bien”, y las librerías CUDA aprendieron a explotarlo.
  6. La pila de perfilado/telemetría de CUDA maduró temprano: NVML, nvidia-smi, Nsight—los operadores tuvieron palancas y visibilidad.
  7. El ímpetu académico y open-source se alineó con NVIDIA: muchos avances tempranos en DL se reproducían en hardware CUDA porque eso era lo que los laboratorios tenían y lo que los frameworks soportaban.
  8. El patrón “driver en host, toolkit en contenedor” se convirtió en el estándar de facto de producción y redujo la fricción de despliegue, reforzando a CUDA como la opción segura.
  9. La historia de compatibilidad hacia atrás de CUDA hizo que la adopción empresarial fuera menos aterradora que “recompilar todo para cada driver”.

La moraleja: NVIDIA no vendió solo chips. Vendieron un camino desde la idea hasta el sistema en ejecución, y siguieron pavimentándolo.

La pila CUDA en producción: donde la realidad aprieta

En producción, CUDA es una tarta por capas. Está deliciosa hasta que la guardas en un coche caliente.

Capa 0: hardware y topología

El rendimiento GPU no es solo “cuántas GPUs”. Es:

  • Generación PCIe y ancho de carril
  • Colocación NUMA (GPU conectada a qué socket CPU)
  • Presencia y cableado NVLink/NVSwitch
  • Tamaño y ancho de banda de memoria GPU
  • Configuraciones ECC y comportamiento ante errores

Capa 1: driver del kernel + driver en espacio de usuario

El driver NVIDIA es la plataforma real. Expone el dispositivo al SO, provee la implementación de la API de driver CUDA y media entre el runtime CUDA de tu contenedor y la GPU real.
Si tu driver está mal, nada más importa. Puedes instalar todos los toolkits del mundo y aún así obtener “no devices found”.

Capa 2: runtime CUDA + librerías

Tu contenedor puede incluir libcudart, cuDNN, cuBLAS, NCCL, etc. Estos deben ser lo suficientemente compatibles con el driver del host.
“Suficientemente compatibles” es la parte que convierte actualizaciones rutinarias en tickets de incidente.

Capa 3: frameworks y artefactos de build

Los binarios de PyTorch/TensorFlow/JAX se construyen contra versiones específicas de CUDA y a menudo esperan un rango concreto de ABIs de librería.
Luego están las extensiones CUDA personalizadas (comunes en sistemas de recomendación, aceleración de inferencia y prototipos de investigación que crecieron y recibieron un pager).

Capa 4: orquestación y multi-tenancy

Plugins de dispositivo de Kubernetes, particionado MIG, cuotas GPU, MPS, schedulers de jobs, ajustes de cgroups—aquí es donde “funciona en mi workstation” va a ser auditado.
Cuanto mayor sea tu flota, más tu problema se convierte en cumplimiento de políticas y control de drift.

Capa 5: almacenamiento y pipelines de datos

Como persona de SRE/almacenamiento, diré la parte callada en voz alta: muchos “problemas de rendimiento GPU” son en realidad problemas de datos.
Si la GPU está esperando dataloaders, almacenamiento de objetos lento, IOPS de archivos pequeños o descompresión en el core CPU equivocado, CUDA no es tu cuello de botella; solo es donde el tiempo inactivo es visible.

Una cita que vale la pena tener en una nota adhesiva cerca del panel del clúster:
Paráfrasis: “La latencia es una propiedad de todo el sistema, no de un componente.” — Werner Vogels (paráfrasis)

Guía de diagnóstico rápido: qué comprobar primero/segundo/tercero

Cuando las cargas GPU son lentas o fallan, no empieces reinstalando CUDA. Así creas un segundo incidente. Comienza por las restricciones y la observabilidad.

Primero: “¿Vemos la GPU y está sana?”

  • Comprobar visibilidad GPU: ¿el nodo ve los dispositivos, se cargó el driver correcto, no hay tormenta ECC?
  • Comprobar procesos activos: ¿algo más está ocupando la GPU, está MIG configurado como crees?
  • Comprobar clocks/límites de potencia: ¿estás siendo throttled térmicamente o limitado por potencia?

Segundo: “¿La carga está limitada por GPU o por entrada?”

  • Mirar proxies de utilización y ocupación de SM (solo la utilización puede ser engañosa, pero es una señal rápida).
  • Comprobar PCIe RX/TX: si la transferencia de datos es enorme, puede que estés limitado por el pipeline.
  • Comprobar saturación CPU y iowait: dataloaders y preprocesado a menudo dominan.

Tercero: “¿Es una incompatibilidad de compatibilidad?”

  • Rango driver vs toolkit: ¿el driver del host soporta el runtime CUDA en el contenedor?
  • NCCL + topología: ajustes incorrectos pueden forzar rutas lentas silenciosamente.
  • Expectativas de build del framework: las ruedas de PyTorch para CUDA son exigentes; las extensiones personalizadas aún más.

Cuarto: “¿Es una elección de kernel/regresión de rendimiento?”

  • El autotuning de cuDNN cambia entre versiones.
  • Los modos TF32/FP16/BF16 cambian las vías de cómputo.
  • Los drivers nuevos a veces cambian el comportamiento del JIT o las heurísticas de scheduling.

Este orden evita el modo de fallo clásico: pasar seis horas afinando kernels para un job que está bloqueado por un mount NFS lento.

Tareas prácticas: comandos, qué significa la salida y la decisión que tomas

Estos son los comandos que ejecutas cuando estás de guardia y alguien dice, “las GPUs están lentas”, como si eso fuera un diagnóstico.
Cada tarea incluye: comando, salida típica, qué significa y la decisión que tomas.

Task 1: Verificar driver y visibilidad GPU

cr0x@server:~$ nvidia-smi
Tue Jan 13 10:02:14 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 A100-SXM4-40GB          On   | 00000000:81:00.0 Off |                    0 |
| N/A   47C    P0              165W / 400W|   1024MiB / 40960MiB  |     12%      Default |
+-----------------------------------------+----------------------+----------------------+
+---------------------------------------------------------------------------------------+
| Processes:                                                                            |
|  GPU   GI   CI        PID   Type   Process name                            GPU Memory |
|        ID   ID                                                             Usage      |
|=======================================================================================|
|    0   N/A  N/A     19342      C   python                                      1008MiB|
+---------------------------------------------------------------------------------------+

Significado: El driver está cargado, la GPU es visible y un proceso está usando ~1GB de VRAM.

Decisión: Si esto falla (no hay dispositivos), detente y arregla el driver/plugin de dispositivo/hardware antes de tocar frameworks.

Task 2: Comprobar telemetría detallada de la GPU (utilización, clocks, pistas de throttling)

cr0x@server:~$ nvidia-smi -q -d PERFORMANCE,CLOCK,POWER,TEMPERATURE | sed -n '1,120p'
==============NVSMI LOG==============

Timestamp                                 : Tue Jan 13 10:02:30 2026
Driver Version                            : 550.54.14
CUDA Version                              : 12.4

Attached GPUs                             : 1
GPU 00000000:81:00.0
    Power Readings
        Power Management                  : Supported
        Power Draw                        : 165.32 W
        Power Limit                       : 400.00 W
    Clocks
        Graphics                          : 1410 MHz
        SM                                : 1410 MHz
        Memory                            : 1215 MHz
    Temperature
        GPU Current Temp                  : 47 C
        GPU T.Limit Temp                  : 93 C

Significado: Potencia y clocks parecen normales; no está térmicamente limitado.

Decisión: Si los clocks son bajos o el consumo está fijado en un límite bajo, arregla la política de potencia/térmica (o las restricciones de la instancia en la nube) antes de culpar a CUDA.

Task 3: Identificar quién está ocupando la GPU

cr0x@server:~$ nvidia-smi pmon -c 1
# gpu        pid  type    sm   mem   enc   dec   command
    0      19342     C     12     4     0     0   python

Significado: Existe un único proceso de cómputo; la utilización de SM es baja.

Decisión: SM bajo sugiere cuello de botella en input, bloqueos de sincronización o tamaños de batch pequeños; ve a inspeccionar CPU/I/O a continuación.

Task 4: Confirmar que el módulo del kernel está cargado y no falla

cr0x@server:~$ lsmod | egrep 'nvidia|nouveau' | head
nvidia_uvm           1769472  0
nvidia_drm            110592  2
nvidia_modeset       1622016  1 nvidia_drm
nvidia              77168640  92 nvidia_uvm,nvidia_modeset

Significado: Los módulos NVIDIA están cargados; no se muestra conflicto con nouveau.

Decisión: Si nouveau está presente, ponlo en la lista negra y reconstruye initramfs; drivers mezclados causan rarezas que parecen “CUDA inestable”.

Task 5: Revisar logs recientes del kernel por errores GPU

cr0x@server:~$ sudo dmesg -T | egrep -i 'nvrm|xid|gpu has fallen|ecc' | tail -n 10
[Tue Jan 13 09:58:07 2026] NVRM: GPU 0000:81:00.0: RmInitAdapter succeeded
[Tue Jan 13 10:01:02 2026] NVRM: Xid (PCI:0000:81:00): 31, pid=19342, Ch 00000028, MMU Fault: ENGINE GRAPHICS

Significado: Errores Xid indican fallos a nivel de GPU/driver (MMU fault aquí). Esto no es “tu código PyTorch” hasta que se demuestre lo contrario.

Decisión: Poner el nodo en cuarentena, drenar cargas y considerar actualización/retroceso del driver o RMA de hardware si es recurrente.

Task 6: Validar acceso GPU desde el contenedor (runtime de contenedores NVIDIA)

cr0x@server:~$ docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
Tue Jan 13 10:03:10 2026
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 550.54.14              Driver Version: 550.54.14      CUDA Version: 12.4   |
+---------------------------------------------------------------------------------------+

Significado: El contenedor puede ver la GPU y el passthrough del driver funciona.

Decisión: Si esto falla, tu problema es la integración del runtime (device plugin, permisos, configuración del runtime de contenedores), no el framework ML.

Task 7: Comprobar librerías runtime CUDA dentro del contenedor

cr0x@server:~$ docker run --rm --gpus all myimage:latest bash -lc "ldconfig -p | egrep 'libcudart|libcublas|libcudnn' | head -n 20"
	libcudart.so.12 (libc6,x86-64) => /usr/local/cuda/lib64/libcudart.so.12
	libcublas.so.12 (libc6,x86-64) => /usr/local/cuda/lib64/libcublas.so.12
	libcudnn.so.9 (libc6,x86-64) => /usr/lib/x86_64-linux-gnu/libcudnn.so.9

Significado: El contenedor está entregando librerías CUDA específicas.

Decisión: Si el framework espera versiones mayores diferentes (común con cuDNN), fija las imágenes base y los builds del framework juntos. No hagas “apt upgrade” hasta meterte en la ruleta ABI.

Task 8: Confirmar que PyTorch ve CUDA y contra qué versión fue compilado

cr0x@server:~$ python3 - <<'PY'
import torch
print("torch", torch.__version__)
print("cuda available", torch.cuda.is_available())
print("torch built cuda", torch.version.cuda)
print("gpu", torch.cuda.get_device_name(0) if torch.cuda.is_available() else None)
PY
torch 2.4.0
cuda available True
torch built cuda 12.1
gpu NVIDIA A100-SXM4-40GB

Significado: PyTorch tiene soporte CUDA y fue compilado para CUDA 12.1, ejecutándose en un driver del host que anuncia capacidad CUDA 12.4.

Decisión: Si cuda available es false, para: es acceso a dispositivo ausente, rueda solo-CPU equivocada o librerías incompatibles.

Task 9: Identificar presión de memoria GPU y señales de fragmentación

cr0x@server:~$ nvidia-smi --query-gpu=memory.total,memory.used,memory.free --format=csv
memory.total [MiB], memory.used [MiB], memory.free [MiB]
40960 MiB, 38912 MiB, 2048 MiB

Significado: Estás cerca de saturación de VRAM.

Decisión: Si los jobs fallan con OOM, reduce batch size, habilita activation checkpointing o pasa a GPUs más grandes; no “añadas swap” porque la VRAM no funciona así.

Task 10: Comprobar ancho de banda PCIe y saneamiento de colocación (NUMA)

cr0x@server:~$ nvidia-smi topo -m
        GPU0    CPU Affinity    NUMA Affinity
GPU0     X     0-31            0

Significado: GPU0 está conectada al nodo NUMA 0; la afinidad de CPU sugiere qué cores son locales.

Decisión: Fija dataloaders y hilos CPU a los cores locales NUMA cuando sea posible; el acceso a memoria remota puede sabotear el rendimiento silenciosamente.

Task 11: Capturar el clásico “GPU idle porque el dataloader es lento”

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (server) 	01/13/2026 	_x86_64_	(64 CPU)

10:03:58 AM  CPU   %usr  %nice   %sys %iowait  %irq  %soft  %steal  %idle
10:03:59 AM  all   8.12   0.00   2.01   18.44  0.00   0.22    0.00  71.21
10:04:00 AM  all   7.55   0.00   1.88   21.10  0.00   0.25    0.00  69.22

Significado: Alto iowait sugiere que la CPU está bloqueada en almacenamiento.

Decisión: Antes de tocar CUDA, arregla la ruta de datos: cache local NVMe, read-ahead mayor, menos archivos pequeños, prefetch en paralelo o un cliente de object-store más rápido.

Task 12: Validar throughput del sistema de archivos en el nodo (revisión de la realidad del pipeline de datos)

cr0x@server:~$ dd if=/mnt/dataset/bigfile.bin of=/dev/null bs=64M count=16 iflag=direct,status=progress
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 1.244 s, 863 MB/s
16+0 records in
16+0 records out
1073741824 bytes (1.1 GB, 1.0 GiB) copied, 1.24567 s, 862 MB/s

Significado: La lectura secuencial es ~860MB/s; puede estar bien, pero las cargas ML suelen necesitar lecturas aleatorias y operaciones de metadatos también.

Decisión: Si esto es bajo o errático, tu “cuello de botella GPU” probablemente sea almacenamiento o red. Arregla eso y verás la utilización subir mágicamente.

Task 13: Comprobar salud de red para entrenamiento multinodo (sensibilidad NCCL)

cr0x@server:~$ ip -s link show dev eth0 | sed -n '1,12p'
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 9000 qdisc mq state UP mode DEFAULT group default qlen 1000
    link/ether 52:54:00:aa:bb:cc brd ff:ff:ff:ff:ff:ff
    RX:  bytes packets errors dropped  missed   mcast
    9876543210  1234567      0       12       0       0
    TX:  bytes packets errors dropped carrier collsns
    8765432109  2345678      0        0       0       0

Significado: Existen paquetes RX descartados. En un clúster ocupado, “12” puede ser ruido o el inicio de un desastre.

Decisión: Si las caídas aumentan durante el entrenamiento, investiga buffers NIC, desajustes MTU, congestión y puertos de switch. NCCL castigará redes inestables con ralentizaciones que parecen “mala escalabilidad GPU”.

Task 14: Depuración rápida de NCCL por errores de topología/transporte

cr0x@server:~$ NCCL_DEBUG=INFO NCCL_DEBUG_SUBSYS=INIT,NET python3 - <<'PY'
import os
print("NCCL_DEBUG", os.environ.get("NCCL_DEBUG"))
print("NCCL_DEBUG_SUBSYS", os.environ.get("NCCL_DEBUG_SUBSYS"))
PY
NCCL_DEBUG INFO
NCCL_DEBUG_SUBSYS INIT,NET

Significado: Has activado logs de init/red de NCCL para una ejecución.

Decisión: Usa esto para confirmar el transporte (InfiniBand vs TCP), selección de interfaz y detección de topología. Si cae a TCP inesperadamente, arregla la configuración de la red antes de afinar otra cosa.

Task 15: Comprobar configuración MIG (sorpresas de multi-tenancy)

cr0x@server:~$ nvidia-smi -L
GPU 0: NVIDIA A100-SXM4-40GB (UUID: GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee)
  MIG 1g.5gb Device 0: (UUID: MIG-GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/1/0)
  MIG 1g.5gb Device 1: (UUID: MIG-GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/2/0)

Significado: La GPU está particionada en instancias MIG; tu trabajo puede ver solo una porción.

Decisión: Si el rendimiento es “misteriosamente bajo”, confirma que la planificación solicitó el perfil MIG correcto. No midas una porción MIG y culpes a CUDA por no alcanzar números de GPU completa.

Tres mini-historias corporativas (anonimizadas, plausibles y dolorosamente familiares)

Mini-historia 1: El incidente causado por una suposición equivocada

Una empresa de tamaño medio ejecutaba fine-tunes nocturnos en una piscina de GPUs. Containerizaron todo y se sentían maduros: dependencias de Python fijadas, rueda PyTorch fijada, imagen base fijada.
Alguien propuso un mantenimiento “seguro” del host: actualizar el driver NVIDIA en toda la flota para que coincida con el nuevo kernel. La solicitud de cambio incluía la frase reconfortante “los drivers son retrocompatibles”.

El despliegue empezó bien. Luego una clase de trabajos empezó a fallar con errores de acceso ilegal a memoria. No inmediatamente. A los diez a treinta minutos de entrenamiento, que es el peor momento posible: lo bastante largo para desperdiciar dinero y lo bastante corto para arruinar el rendimiento.
Los ingenieros persiguieron fantasmas en el código del modelo, luego en extensiones CUDA, luego en corrupción del dataloader. El incidente duró todo un día porque las fallas eran no deterministas y estaban correlacionadas con ciertos nodos.

La suposición equivocada no fue que existe retrocompatibilidad. Fue asumir que es una garantía universal para todas las combinaciones de: rama de driver, microcódigo GPU, framework y extensiones personalizadas.
Una extensión usaba compilación JIT vía PTX y dependía de un comportamiento que cambió sutilmente con la actualización del driver. El JIT generó código distinto y bajo una forma de entrada particular desencadenó un caso límite.

La solución fue aburrida: fijar la rama del driver para ese clúster, reconstruir la extensión con objetivos de arch explícitos (fatbin incluyendo SASS para las GPUs desplegadas) y añadir un job canario que ejecutara formas representativas durante una hora antes de promover el driver.
La lección fue más aguda: “compatible” no es lo mismo que “idéntico”. En el mundo GPU, idéntico es la única palabra que respeta tu cola de incidentes.

Mini-historia 2: La optimización que salió mal

Otra organización tenía un pipeline de entrenamiento distribuido que escalaba mal más allá de cuatro GPUs. Alguien detectó un villano familiar: transferencias host-a-dispositivo. Activaron prefetch agresivo y aumentaron los workers del dataloader.
El primer benchmark mejoró. Celebración. Luego la producción empezó a fallar en SLAs, y no solo el entrenamiento—otros servicios en los mismos nodos también se volvieron más lentos.

La optimización salió mal porque movió el cuello de botella a recursos compartidos. Más workers significaron más presión CPU, más churn del page cache y más tormentas de metadatos contra el almacenamiento compartido.
Las GPUs mostraron mayor utilización por un tiempo, pero la latencia tail del clúster se volvió horrible. Algunos jobs eran rápidos; otros se quedaban estancados cuando el backend de almacenamiento estaba caliente. Los reintentos amplificaron la carga, claro.

El problema raíz no fue “prefetch es malo”. Fue prefetch sin aislamiento de recursos y sin medir el sistema completo.
Afinaron para el throughput de un único job y accidentalmente hicieron DoS a su propio almacenamiento, lo cual es una forma clásica de aprender que IOPS es algo real y no una sugerencia.

La corrección incluyó limitar el número de workers, añadir throttles I/O por trabajo, usar caché NVMe local para shards calientes e introducir un pipeline que preprocesara y empaquetara archivos pequeños en blobs contiguos más grandes.
La utilización GPU bajó ligeramente, pero el throughput general y la previsibilidad mejoraron—que en producción es la intención.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una empresa que ejecutaba inferencia tenía una regla: cada servicio GPU debía exponer un endpoint de diagnóstico que devolviera versión de driver, dispositivos visibles y las versiones de librerías CUDA cargadas en proceso.
A nadie le gustaba la regla. Parecía papeleo en forma JSON. Se aplicó de todos modos.

Entonces llegó una reconstrucción de imagen con una nueva versión mayor de cuDNN. Funcionó en staging (nodo único, carga ligera) y falló en producción (SKU GPU diferente, mayor concurrencia). Los errores parecían “CUDNN_STATUS_INTERNAL_ERROR” genéricos.
El ingeniero de guardia no tuvo que adivinar. Accedió al endpoint, vio la discrepancia de librerías y la correlacionó con la ola de despliegue en minutos.

El rollback fue rápido porque las imágenes eran inmutables y versionadas, y porque tenían una etiqueta baseline conocida que siempre permanecía disponible.
El postmortem no fue glamoroso: mejor testing de integración contra múltiples SKUs GPU y una política que requiere una puerta de compatibilidad para bumps mayores de cuDNN.

Esa regla—el endpoint de diagnóstico aburrido—convirtió un potencial incidente de varias horas en un rollback controlado. La fiabilidad a menudo es el arte de no ser romántico.

Broma #2: “Arreglamos la imagen CUDA en caliente en producción” es el equivalente GPU de “voy a malabarear con motosierras para ahorrar tiempo.”

Errores comunes: síntomas → causa raíz → solución

1) Síntoma: torch.cuda.is_available() es false en contenedores

Causa raíz: Runtime de contenedores no configurado para GPUs, plugin de dispositivo faltante o ejecución de una build del framework solo-CPU.

Solución: Validar con una imagen base CUDA conocida + nvidia-smi dentro del contenedor; asegurar runtime correcto; instalar ruedas habilitadas para CUDA.

2) Síntoma: “La versión del driver CUDA es insuficiente para la versión del runtime CUDA”

Causa raíz: El contenedor trae un runtime CUDA más nuevo que el driver del host soporta.

Solución: Actualizar el driver del host o degradar el toolkit/framework del contenedor; estandarizar una matriz de compatibilidad por clúster.

3) Síntoma: Acceso ilegal a memoria aleatorio / errores Xid bajo carga

Causa raíz: Bug del driver, hardware inestable, sobrecalentamiento/potencia, o un caso límite de JIT/PTX provocado por kernels específicos.

Solución: Revisar dmesg por Xids; poner nodos en cuarentena; probar con fijación de driver; reconstruir extensiones personalizadas con targets de arch explícitos; considerar diagnósticos de hardware/RMA.

4) Síntoma: Utilización GPU baja pero CPU e iowait altos

Causa raíz: Cuello de botella en el pipeline de entrada: latencia de almacenamiento, demasiados archivos pequeños, sobrecarga de descompresión, CPU insuficiente, mal colocamiento NUMA.

Solución: Perfilado del dataloader; empaquetar datasets; usar cache local; fijar hilos a cores locales NUMA; reducir overhead por muestra.

5) Síntoma: Escalado multi-GPU colapsa más allá de 2–4 GPUs

Causa raíz: NCCL cae a TCP, mala conciencia topológica, NIC saturada o variables de entorno incorrectas seleccionando la interfaz equivocada.

Solución: Activar debug de NCCL; verificar transporte; corregir selección de NIC; validar MTU; asegurar algoritmos de colectivas según topología; evitar oversubscription.

6) Síntoma: Regresión de rendimiento tras actualizar cuDNN/cuBLAS

Causa raíz: Diferentes heurísticas de selección de kernel, cambios en el comportamiento de autotune o deshabilitación de fast paths por diferencias de forma/layout.

Solución: Re-ejecutar benchmarks con formas representativas; fijar ajustes de autotune; pinnear versiones de librerías conocidas para cargas críticas; considerar selección explícita de kernels cuando esté disponible.

7) Síntoma: “Funciona en A100, lento en L4 (o viceversa)”

Causa raíz: Diferencias de arquitectura: generación de tensor cores, tamaño/ancho de banda de memoria, comportamiento de clocks, precisiones soportadas.

Solución: Compilar con targets de arch correctos; afinar batch sizes y modos de precisión por SKU; mantener baselines de rendimiento separados por clase de GPU.

8) Síntoma: Picos de latencia en inferencia periódicamente

Causa raíz: Fragmentación de memoria GPU, compilación JIT en la primera petición, compactaciones de fondo o trabajos concurrentes robando tiempo GPU.

Solución: Calentar kernels; preconstruir engines (TensorRT) donde aplique; reservar pools de memoria; aplicar aislamiento GPU (MIG/MPS/cuotas); evitar co-tenant para rutas sensibles a latencia.

Listas de verificación / plan paso a paso

Checklist: ejecutar CUDA en producción sin odiar tu vida

  1. Estandarizar ramas de driver por clúster (no por nodo) y validar cambios con canarios.
  2. Elegir una baseline de contenedor (SO + familia de toolkit CUDA) y tratarla como una plataforma, no como sugerencia.
  3. Fijar builds de frameworks (PyTorch/TF/JAX) a la plataforma; evitar “latest” a menos que te gusten las sorpresas.
  4. Versionar y congelar extensiones CUDA con builds de arch explícitos (fatbins) cuando sea posible.
  5. Exponer diagnósticos runtime: versión de driver, dispositivos visibles, librerías CUDA cargadas e información de build.
  6. Separar entornos de rendimiento: nodos de benchmarking no deberían compartirse con vecinos ruidosos.
  7. Hacer de los pipelines de datos un SLO de primera clase: registrar iowait, throughput de lectura, operaciones de metadatos, tasas de acierto de cache.
  8. Mantener baselines por SKU de GPU para throughput y latencia; no compares manzanas con porciones MIG.
  9. Decidir tu modelo de multi-tenancy: MIG, MPS, modo exclusivo o “ninguno”. Luego aplicarlo.
  10. Practicar rollback: imágenes inmutables, tags conocidos buenos y una ruta de downgrade probada para drivers.

Paso a paso: migrar desde CUDA (o al menos reducir riesgo)

  1. Inventariar lo que es realmente específico de CUDA: kernels personalizados, engines TensorRT, suposiciones NCCL, ruedas solo-CUDA.
  2. Medir el “rendimiento imprescindible”: definir deltas aceptables; 5% puede ser ok para entrenamiento, catastrófico para márgenes de inferencia.
  3. Empezar por los bordes: preprocesado, inferencia en CPU o flujos de exportación ONNX son más fáciles de portar que ops CUDA personalizados.
  4. Separar corrección de velocidad: lograr paridad funcional primero, luego afinar.
  5. Construir un arnés de doble ejecución: mismas entradas, comparar salidas/estadísticas, detectar deriva; no confiar en “se ve bien”.
  6. Planear brechas operativas: telemetría, herramientas de depuración, reporte de errores de kernel, madurez del empaquetado.
  7. Mantener CUDA como fallback hasta que la alternativa haya pasado múltiples ciclos de release bajo tráfico real.

Paso a paso: actualizar drivers/toolkits de forma segura

  1. Elegir el objetivo de compatibilidad: qué frameworks y contenedores deben funcionar con el nuevo driver.
  2. Construir una suite canaria: jobs representativos, incluyendo extensiones personalizadas y ejecuciones distribuidas.
  3. Actualizar una pequeña piscina de nodos y ejecutar canarios por horas, no minutos.
  4. Vigilar errores Xid y regresiones sutiles: throughput, latencia tail, patrones de uso de memoria.
  5. Promover gradualmente con triggers automáticos de rollback por tasas de fallo y caídas de rendimiento.
  6. Documentar la nueva matriz “bendecida” y aplicarla en CI.

Preguntas frecuentes

1) ¿Es CUDA “la razón” por la que NVIDIA domina la IA?

Es una razón importante. CUDA hizo a las GPUs programables a escala, luego el ecosistema de librerías (cuDNN, cuBLAS, NCCL, TensorRT) hizo que rendimiento y despliegue fueran repetibles.
El hardware importa, pero el software es lo que convierte el hardware en un estándar de industria.

2) ¿Qué exactamente es el lock-in: código, herramientas u operaciones?

Los tres. El lock-in de código ocurre vía kernels CUDA y extensiones. El lock-in de herramientas ocurre vía perfiles y depuradores afinados al modelo de NVIDIA.
El lock-in operativo ocurre vía patrones driver/toolkit y una dependencia profunda de librerías NVIDIA para rendimiento y estabilidad.

3) ¿Puedo evitar el lock-in escribiendo en PyTorch y sin tocar CUDA directamente?

Reduces el lock-in de código fuente, pero no el lock-in de plataforma. Las ruedas de tu framework, el backend distribuido (a menudo NCCL) y los kernels críticos para rendimiento aún te atan al ecosistema CUDA.

4) ¿Por qué es tan importante “driver en host, toolkit en contenedor”?

Porque te permite estandarizar drivers a nivel de flota mientras permites toolkits por aplicación.
Es pragmático: los drivers son más difíciles de actualizar de forma segura que los contenedores. Este patrón hizo que los despliegues GPU se comportaran más como infraestructura normal.

5) ¿Cuál es el modo de fallo más común en producción con CUDA?

Deriva de versiones: una reconstrucción de contenedor cambia librerías CUDA o builds de framework y de repente la combinación driver/toolkit del host deja de ser compatible.
El segundo más común es “GPU lenta” que en realidad es almacenamiento, CPU o red.

6) ¿Cómo sé si estoy limitado por cómputo o por entrada?

Empieza con utilización GPU y consumo de potencia. Si la utilización es baja y iowait CPU es alto, eres input-bound.
Si la potencia GPU es alta y la utilización es estable, probablemente estés compute-bound. Entonces optimizas kernels, modos de precisión y tamaños de batch.

7) ¿MIG reduce el lock-in?

MIG reduce la contención de recursos y mejora la predictibilidad de multi-tenancy en hardware NVIDIA. No reduce el lock-in de CUDA; aumenta tu dependencia en el tooling operativo y la integración de scheduling de NVIDIA.
Sigue valiendo la pena usarlo cuando la mezcla de cargas demanda aislamiento.

8) ¿Por qué las actualizaciones de CUDA a veces cambian el rendimiento aun cuando nada se cae?

Las actualizaciones de librería cambian heurísticas de selección de kernel y pueden habilitar/deshabilitar rutas rápidas.
Las actualizaciones de driver pueden cambiar compilación JIT y comportamiento de scheduling. Tu modelo no cambió, pero el código que ejecuta sí puede hacerlo.

9) ¿Es realista migrar cargas CUDA a otra pila GPU?

A veces. Si dependes mayormente de ops comunes de framework, la migración es más sobre validar corrección y rendimiento.
Si tienes extensiones CUDA personalizadas, engines TensorRT y comportamiento NCCL fuertemente afinado, la migración se vuelve un programa de varios trimestres con riesgo real.

10) ¿Qué debo estandarizar primero: drivers, toolkits o frameworks?

Primero drivers a nivel de clúster, luego baselines de contenedor (toolkits + SO), luego frameworks.
Estandarizar frameworks sin controlar el driver/plataforma es la forma de acabar con “funciona en algunos nodos”.

Conclusión: pasos prácticos siguientes

CUDA encerró a la industria de la misma manera que la buena infraestructura encierra a una compañía: siendo lo que funciona de forma fiable a escala, y luego acumulando herramientas, hábitos y suposiciones de rendimiento alrededor.
Si ejecutas sistemas en producción, no luchas contra eso con ideología. Lo afrontas con claridad.

  1. Escribe tu matriz de compatibilidad bendecida: rama de driver, imagen base, versiones de framework, versiones mayores de NCCL/cuDNN.
  2. Implementa la guía de diagnóstico rápido como runbook y automatiza las primeras cinco comprobaciones (visibilidad GPU, errores, utilización, iowait, drops de red).
  3. Añade cargas canarias que corran lo bastante para atrapar “solo falla bajo carga”, especialmente para JIT/PTX y extensiones personalizadas.
  4. Deja de llamar problemas de datos “problemas GPU”: monitorea almacenamiento y métricas de preprocesado junto a métricas GPU en el mismo dashboard.
  5. Si quieres opcionalidad, empieza aislando código específico de CUDA y construyendo tests de doble ejecución. La opcionalidad se diseña, no se desea.

CUDA es una plataforma. Trátala como tal: versiona, prueba, gatea y obsérvala. El lock-in se vuelve manejable cuando dejas de fingir que es solo una librería que puedes actualizar casualmente un viernes.

← Anterior
P-cores vs E-cores: CPUs híbridos explicados sin marketing
Siguiente →
Correo: registros DNS mixtos — la errata que mata la entrega (y la solución)

Deja un comentario