«Mi CPU no puede alimentar mi GPU»: verdad vs meme

¿Te fue útil?

Compraste la GPU grande. El panel aún muestra 20–40% de utilización. El entrenamiento es lento, la inferencia es irregular, se pierden fotogramas y alguien en el chat suelta la frase: “Tu CPU no puede alimentar tu GPU”.

A veces eso es cierto. A veces es culto al cargo. Y otras veces el culpable real es el almacenamiento, PCIe o una pequeña sincronización que no sabías que habías escrito. La solución depende de qué tipo de “alimentar” estás mencionando: envío de trabajo, entrega de datos o mantener la GPU ocupada con suficiente paralelismo.

Qué significa realmente “alimentar la GPU”

“Alimentar la GPU” es una frase imprecisa que agrupa tres canales distintos bajo una misma sensación:

  1. Envío de trabajo: el host (CPU) lanza kernels, prepara buffers de comandos, programa CUDA Graphs, encola copias y sincroniza streams. Si el host no puede lanzar lo suficientemente rápido, la GPU queda inactiva entre kernels.
  2. Entrega de datos: el host extrae datos del almacenamiento, los decodifica/preprocesa y los transfiere por PCIe/NVLink a la memoria de la GPU. Si esto es lento, la GPU espera el siguiente lote.
  3. Disponibilidad de trabajo paralelo: incluso si el envío y la entrega son perfectos, la GPU necesita suficiente trabajo independiente para llenar los SM. Lotes demasiado pequeños, kernels diminutos o demasiadas dependencias seriales pueden mantener baja la utilización sin que exista un problema de CPU.

Así que cuando alguien dice “la CPU no puede alimentar la GPU”, exige una aclaración: ¿qué alimentación? Si no pueden responder, has encontrado el primer cuello de botella: la calidad del diagnóstico.

Una cita para guardar en el bolsillo (idea parafraseada) de Donald Knuth: La optimización prematura es una forma común de perder tiempo; mide primero y optimiza lo que importa.

Y sí, voy a ser insistente con la medición. Los sistemas de producción no funcionan con confianza.

Verdad vs meme: cuándo la CPU es realmente el cuello de botella

Los casos “verdaderos” (la CPU limita genuinamente el rendimiento de la GPU)

Estos son aburridamente reales:

  • La sobrecarga de lanzamiento de kernels domina. Estás lanzando miles de kernels diminutos por paso. El hilo de CPU vive en llamadas al controlador y la GPU vive esperando el siguiente lanzamiento.
  • Pipeline de entrada en un solo hilo. La decodificación de datos, aumentos, tokenización o ingeniería de características se ejecutan en un núcleo porque alguien puso workers=0 “por determinismo”. La GPU espera el siguiente lote como si estuviera detrás de una caja lenta.
  • Sincronizaciones excesivas. Equivalentes ocultos a cudaDeviceSynchronize() (directa o indirectamente) serializan la canalización. La CPU se bloquea, luego la GPU se bloquea, y culpas a la CPU de ser “demasiado débil”.
  • Preprocesamiento limitado por CPU. Piensa en decodificación JPEG, decodificación de vídeo sin aceleración por hardware, parseo de JSON o descompresión. Las GPUs son rápidas; tu CPU sigue sujeto a las leyes de la física y a la predicción de saltos.
  • NUMA y hambre de ancho de banda de memoria. La CPU tiene “muchos núcleos”, pero todos tiran datos a través de la frontera del zócalo porque tu proceso y la GPU están en nodos NUMA diferentes.
  • Sobrehead del driver/firmware e interrupciones. Especialmente en servidores multi-GPU con I/O intenso. La CPU no es “débil”, está ocupada siendo tu planificador de I/O y receptor de interrupciones.

Los casos “meme” (la CPU no es el factor limitante)

Aquí es donde la gente quema semanas:

  • La baja utilización de GPU se debe a que la GPU hace ráfagas cortas. La utilización promedio oculta micro-paradas; la GPU en realidad espera memoria dentro del kernel, no a la CPU.
  • Estás limitado por PCIe. Las copias saturan PCIe. Cambiar la CPU no ampliará mágicamente un PCIe Gen3 x8.
  • Estás limitado por VRAM. Reduces el tamaño del lote para caber en memoria, lo que reduce la intensidad aritmética y hace que la GPU parezca “mal alimentada”. Eso no es la CPU; es el tamaño del working set.
  • Tus kernels son ineficientes. Baja ocupación, mala coalescencia de memoria, ramas divergentes. La GPU está “ocupada” siendo ineficiente, no esperando a la CPU.
  • Tu trabajo está limitado por latencia (por ejemplo, inferencia con lotes pequeños). La utilización de la GPU puede nunca ser alta porque la carga no tiene suficiente paralelismo. “100% GPU” no es una ley de la naturaleza.

Broma #1: La GPU no está “hambrienta”, es exigente—si le sirves un crutón a la vez, te mirará como si el problema fueras tú.

Hechos interesantes y un poco de historia (porque explican el dolor actual)

  • Hecho 1: Los modelos tempranos de programación de GPU (antes de CUDA) eran esencialmente APIs gráficas disfrazadas; “alimentar la GPU” significaba literalmente mantener la tubería gráfica llena de triángulos. Los kernels de cómputo de hoy heredaron la misma mentalidad de throughput.
  • Hecho 2: El modelo de lanzamiento de CUDA hizo fácil encolar kernels, pero las buenas prácticas iniciales fomentaban muchos kernels pequeños; las guías modernas suelen recomendar fusión y CUDA Graphs para reducir la sobrecarga de lanzamiento.
  • Hecho 3: PCIe ha mejorado de forma constante, pero no al mismo ritmo que los FLOPS de las GPUs. La brecha es la razón por la que las transferencias host→device siguen siendo un cuello de botella frecuente incluso en servidores “monstruo”.
  • Hecho 4: NUMA se convirtió en un punto doloroso cuando los servidores de doble zócalo dominaron los centros de datos; la afinidad de GPU y la colocación en el CPU más cercano importan porque la latencia de memoria entre zócalos no es un error de redondeo.
  • Hecho 5: La memoria pinned (bloqueada en página) es más rápida para transferencias DMA, pero demasiada memoria pinned puede perjudicar al SO y a otros procesos al reducir la flexibilidad de la RAM pageable.
  • Hecho 6: NVLink existe en gran parte porque PCIe no era suficiente para cargas multi-GPU; pero no arregla el preprocesamiento del lado CPU, la sobrecarga de lanzamiento de kernels o la ingestión desde almacenamiento.
  • Hecho 7: Los contadores de “utilización de GPU” se diseñaron originalmente para gráficos y kernels de larga duración. Interpretarlos para entrenamiento ML con mezcla de copias/cómputo puede llevar a conclusiones erróneas si no se usa una vista de la línea de tiempo.
  • Hecho 8: El auge del ML centrado en datos convirtió los pipelines de entrada (decodificar, aumentar, tokenizar) en problemas de rendimiento de primera clase; tu “trabajo de entrenamiento” a menudo se comporta como un job ETL con una GPU adjunta.

Cuatro tipos de cuellos de botella que sigues confundiendo

1) Cuello de botella de envío desde CPU (límite de lanzamiento)

Síntomas: la GPU muestra huecos entre kernels, muchos kernels cortos, el hilo de CPU está al máximo en tiempo de sistema o llamadas al driver, el tiempo por paso escala con el “número de kernels”, no con el tamaño del lote.

Arreglos típicos: fusionar kernels, aumentar el tamaño del lote, usar CUDA Graphs, reducir la sobrecarga de Python, evitar llamadas por muestra al dispositivo, reducir sincronizaciones, usar kernels persistentes donde proceda.

2) Cuello de botella de preprocesamiento por CPU (decodificar/aumentar/tokenizar)

Síntomas: núcleos de CPU saturados en espacio de usuario, lecturas de disco/red parecen bien, la GPU espera la entrada, aumentar workers del dataloader ayuda hasta que se alcanza la contención.

Arreglos típicos: paralelizar el preprocesamiento, vectorizar, cachear datos decodificados, mover transformaciones a la GPU, usar códecs más rápidos, reducir el coste de aumentos, usar lotes más grandes para amortizar la sobrecarga.

3) Cuello de botella de I/O y almacenamiento (tu “servidor GPU” es en realidad cliente de almacenamiento)

Síntomas: alto iowait, latencias de lectura largas, rendimiento inconsistente, utilización de GPU ruidosa, el rendimiento mejora cuando el dataset está en NVMe local o cacheado.

Arreglos típicos: caché local, prefetch, lecturas secuenciales mayores, mejores formatos de archivo, evitar lecturas aleatorias pequeñas, asegurar que el sistema de archivos y la red no estén estrangulando.

4) Cuello de botella del lado GPU (está ocupado, pero no de la forma que quieres)

Síntomas: la utilización de GPU puede ser alta o baja, pero el perfil muestra stalls por memoria, baja ocupación, tensor cores inactivos o mala eficiencia de kernels. La CPU está mayormente inactiva.

Arreglos típicos: optimización de kernels, mejores librerías, precisión mixta, cambios de layout, operaciones fusionadas más grandes, arreglar accesos no coalescentes, asegurarte de usar el backend correcto.

Guion de diagnóstico rápido (primero/segundo/tercero)

Primero: establece si la GPU está esperando o trabajando

  • Revisa utilización de GPU, potencia, relojes, uso de memoria y—críticamente—si la utilización es en ráfagas.
  • Busca motores de copia activos frente a cómputo activo (H2D/D2H vs actividad SM).
  • Si la GPU está realmente inactiva mucho tiempo, está esperando algo aguas arriba (envío desde CPU, preprocesamiento, I/O, sincronización).

Segundo: separa envío desde CPU de preprocesamiento por CPU

  • Si un hilo de CPU está caliente y el tiempo de sistema es alto: sospecha sobrecarga de lanzamientos o sincronización.
  • Si muchos núcleos de CPU están calientes en tiempo de usuario: sospecha preprocesamiento o descompresión/decodificación/tokenización.
  • Si la CPU está mayormente inactiva pero la GPU está subutilizada: sospecha ineficiencia del lado GPU o carga demasiado pequeña.

Tercero: valida la ruta de transporte (PCIe/NUMA) y la ruta de almacenamiento

  • Confirma ancho/velocidad de enlace PCIe. “x16” no es una sensación; es un estado negociado.
  • Revisa la localidad NUMA: CPU, memoria y GPU deberían alinearse cuando sea posible.
  • Revisa latencia de almacenamiento y tamaños de lectura. Lecturas aleatorias de 4KB desde un filesystem en red humillarán a tu H100.

Broma #2: Actualizar la CPU para arreglar un cuello de botella de PCIe es como comprar un cajero más rápido porque el camión de entrega está atascado en el tráfico.

Tareas prácticas: comandos, salidas y decisiones

Estos son los pasos de “deja de discutir, empieza a comprobar”. Cada tarea incluye un comando, una salida de ejemplo, lo que significa y la decisión que tomas.

Task 1: Check live GPU utilization, clocks, and power

cr0x@server:~$ nvidia-smi dmon -s pucvmt
# gpu    pwr gtemp mtemp  sm   mem   enc   dec   mclk   pclk
# Idx      W     C     C   %     %     %     %    MHz    MHz
    0     92    64     -  28     18     0     0   5001   1410
    0     88    63     -  31     20     0     0   5001   1410
    0     55    60     -   4      6     0     0   5001    705

Qué significa: SM% oscilando de ~30% a ~4% con caídas de reloj sugiere trabajo en ráfagas o stalls. Las caídas de potencia/reloj suelen indicar que la GPU está lo suficientemente inactiva como para bajar de frecuencia.

Decisión: Si las ráfagas se correlacionan con los límites de lote, mira aguas arriba (entrada, sincronización). Si SM% es constante pero bajo, revisa la eficiencia de los kernels o el dimensionamiento de batches.

Task 2: See per-process GPU usage (are you even looking at the right job?)

cr0x@server:~$ nvidia-smi pmon -s um
# gpu        pid  type    sm   mem   enc   dec   command
    0      27431     C     9    12     0     0   python
    0      29902     G     0     1     0     0   Xorg

Qué significa: Tu proceso Python está usando apenas SM. La memoria está asignada, el cómputo no lo está.

Decisión: Perfilado del input y sincronización. No asumas que “VRAM asignada” equivale a “GPU ocupada”.

Task 3: Check PCIe link speed and width

cr0x@server:~$ nvidia-smi -q | sed -n '/PCI/,/Replay/p'
    PCI
        Bus                             : 00000000:81:00.0
        Link Width                      : 8x
        Link Speed                      : 8.0 GT/s
        Replay Counter                  : 0

Qué significa: Eso es PCIe Gen3 x8. Si esperabas Gen4 x16, ya encontraste una clase de cuello de botella.

Decisión: Arregla ajustes de BIOS, colocación de slots, risers o incompatibilidad de plataforma antes de reescribir código.

Task 4: Validate negotiated PCIe state via lspci

cr0x@server:~$ sudo lspci -s 81:00.0 -vv | egrep -i 'LnkCap|LnkSta'
LnkCap: Port #0, Speed 16GT/s, Width x16
LnkSta: Speed 8GT/s (downgraded), Width x8 (downgraded)

Qué significa: La tarjeta puede funcionar a Gen4 x16 pero está degradada. Esto puede ocurrir por cableado del slot, BIOS forzando estado o un riser defectuoso.

Decisión: Trátalo como un problema de hardware/plataforma. Ninguna cantidad de “dataloader workers” arregla un enlace degradado.

Task 5: Check GPU topology and NUMA affinity

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

Qué significa: GPU0 está más cercana a los núcleos CPU 0–15 en el nodo NUMA 0.

Decisión: Fija tu proceso y las asignaciones de memoria a esos núcleos/nodo si haces preprocesamiento intensivo por CPU o transferencias H2D.

Task 6: Check CPU saturation and run queue pressure

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

12:40:20 PM  CPU   %usr %nice %sys %iowait %irq %soft %idle
12:40:21 PM  all   210.0 0.00 35.0  0.50    0.0  1.2   753.3
12:40:21 PM    7    98.0 0.00  2.0  0.00    0.0  0.0     0.0
12:40:21 PM    8     4.0 0.00 60.0  0.00    0.0  0.0    36.0

Qué significa: Un núcleo (CPU 7) está al máximo en tiempo de usuario mientras el sistema en general está mayormente ocioso. CPU 8 muestra alto tiempo de sistema (trabajo del driver/kernel). Huele a cuello de botella de un solo hilo (GIL de Python, hilo de lanzamiento o sincronización).

Decisión: Optimiza el camino del hilo host: reduce la sobrecarga por paso en Python, usa batching, CUDA Graphs, evita llamadas de sincronización frecuentes.

Task 7: Catch I/O wait and storage latency hints

cr0x@server:~$ iostat -xz 1 3
avg-cpu:  %user %nice %system %iowait  %steal %idle
          18.2   0.0    6.1     22.9     0.0  52.8

Device            r/s   rkB/s  rrqm/s  %util  await
nvme0n1          85.0  4200.0    0.0   78.0   9.8

Qué significa: iowait es alto y el await de NVMe es ~10ms bajo carga. Para un pipeline de entrada que hace muchas lecturas pequeñas, eso es doloroso.

Decisión: Aumenta el tamaño de lectura, prefetch, empaqueta datos en shards mayores, cachea localmente o mueve el dataset fuera del almacenamiento contendiente.

Task 8: Confirm the dataset access pattern (small random reads vs streaming)

cr0x@server:~$ sudo strace -f -e trace=openat,read -p 27431 -s 80 -tt 2>&1 | head -n 8
12:41:10.102334 openat(AT_FDCWD, "/data/ds/img_000812.jpg", O_RDONLY) = 57
12:41:10.102801 read(57, "\377\330\377\340\0\20JFIF\0\1\1\0\0\1\0\1\0\0", 4096) = 4096
12:41:10.103122 read(57, "...", 4096) = 4096
12:41:10.103444 openat(AT_FDCWD, "/data/ds/img_000813.jpg", O_RDONLY) = 58

Qué significa: Muchas lecturas pequeñas de 4KB a través de muchos archivos. Ese es el clásico patrón “funciona en mi portátil” que colapsa a escala.

Decisión: Consolida archivos (tar/shards), usa lecturas secuenciales, habilita patrones amigables con page cache y prefetch de lotes.

Task 9: Check CPU frequency scaling (the quiet throughput killer)

cr0x@server:~$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
powersave

Qué significa: La CPU tiene permitido bajar la frecuencia agresivamente. Para preprocesos con ráfagas, esto puede aumentar la latencia cola y dejar a la GPU sin datos entre lotes.

Decisión: En nodos dedicados de entrenamiento, usa el governor performance o ajustes de plataforma apropiados, y vuelve a medir.

Task 10: Spot NUMA misplacement (process running “far” from the GPU)

cr0x@server:~$ numactl --show
policy: default
preferred node: current
physcpubind: 16 17 18 19 20 21 22 23
membind: 1

Qué significa: Tu proceso está fijado al nodo NUMA 1, pero la topología previa mostró que GPU0 prefiere el nodo NUMA 0. Eso es tráfico cross-socket para cada buffer DMA y salida de preprocesamiento.

Decisión: Re-fija al nodo NUMA local a la GPU (o mueve el job a una GPU conectada al nodo 1). Esto suele ser una mejora de varios puntos porcentuales.

Task 11: Check for throttling and thermal constraints

cr0x@server:~$ nvidia-smi -q | sed -n '/Clocks/,/Applications Clocks/p'
    Clocks
        Graphics                        : 705 MHz
        SM                              : 705 MHz
        Memory                          : 5001 MHz
    Applications Clocks
        Graphics                        : 1410 MHz
        Memory                          : 5001 MHz

Qué significa: El reloj SM actual es mucho más bajo que el reloj de la aplicación. Si esto persiste bajo carga, puede que estés siendo limitado por potencia/temperatura o simplemente inactivo.

Decisión: Si la utilización es alta pero los relojes son bajos, investiga límites de potencia, refrigeración y flujo de aire del chasis. Si la utilización es baja, probablemente está inactiva y bajando de frecuencia normalmente.

Task 12: Identify a kernel-launch-heavy pattern (lots of tiny GPU work)

cr0x@server:~$ sudo perf top -p 27431 -g --stdio
Samples: 3K of event 'cycles'
  18.40%  libcuda.so.1        [.] cuLaunchKernel
  11.22%  libc.so.6           [.] memcpy
   9.87%  libpthread.so.0     [.] pthread_mutex_lock
   6.31%  python3.10          [.] _PyEval_EvalFrameDefault

Qué significa: El proceso gasta muchas ciclos en lanzar kernels y en marcos del intérprete Python. Esto es “overhead de envío”.

Decisión: Fusiona operaciones, usa grafos compilados, reduce llamadas por operación en el lado Python y considera kernels/lotes más grandes.

Task 13: Check network filesystem impact (if your dataset is remote)

cr0x@server:~$ nfsstat -c
Client rpc stats:
calls      retrans    authrefrsh
248391     1203       0

Client nfs v4:
ops         count
read        182744
open        50322
getattr     411802

Qué significa: Existen retransmisiones y el volumen de getattr/open es enorme. El parloteo de metadatos más las lecturas remotas pueden absolutamente dejar sin datos a una GPU.

Decisión: Stagea en NVMe local, reduce el número de archivos, aumenta el cache del cliente donde sea seguro, o cambia el formato del dataset.

Task 14: Verify huge pages / pinned memory pressure signals (host memory path)

cr0x@server:~$ grep -E 'MemAvailable|Dirty|Writeback' /proc/meminfo
MemAvailable:   1842332 kB
Dirty:           482912 kB
Writeback:        12984 kB

Qué significa: MemAvailable baja y Dirty alta sugiere presión de memoria y writeback. Esto puede ralentizar el preprocesamiento y causar latencias ruidosas, incluso si la GPU “está bien”.

Decisión: Reduce uso de pinned memory, evita cachés excesivos en la aplicación o provisiona más RAM / aísla vecinos ruidosos.

Tres mini-historias corporativas (cómo los equipos fallan en esto)

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

Tenían un nuevo clúster de GPU. Todos estaban emocionados, y la lista de verificación de lanzamiento tenía los ítems habituales: versiones de driver, runtime CUDA, checks de salud y una corrida rápida de entrenamiento. La utilización de GPU era baja, así que la conclusión llegó rápido: “Estas CPUs son demasiado pequeñas. Abarataron.”

El equipo escaló. Procurement fue arrastrado a una llamada de ingeniería. Alguien propuso cambiar todo el SKU del nodo. Una semana después, un SRE hizo la pregunta molesta: “¿Qué enlace PCIe negociamos realmente?”

Resultó que los nodos estaban cableados mediante una configuración de riser que silenciosamente negoció PCIe a un ancho y generación menores de lo esperado. Las GPUs estaban bien. Las CPUs estaban bien. El camino entre ellas no lo estaba. Las transferencias H2D estaban capadas y el loop de entrenamiento se detenía en cada copia de lote.

Una vez que arreglaron la colocación de slots y ajustes de BIOS, la utilización subió sin tocar una línea de código. El postmortem no fue sobre PCIe. Fue sobre suposiciones: trataron “la CPU no puede alimentar la GPU” como una explicación en vez de una hipótesis.

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

Un grupo de ingeniería de datos quiso “alimentar mejor la GPU”, así que aumentaron agresivamente workers del dataloader y habilitaron pinned memory en todas partes. El throughput mejoró en un nodo de prueba tranquilo. Lo desplegaron en la flota compartida de entrenamiento.

En un día, los jobs de entrenamiento empezaron a fallar de forma impredecible. Algunos corrían rápido. Otros colgaban. Algunos eran matados por el OOM killer aunque la memoria GPU estaba estable. La rotación on-call la pasó mal, principalmente porque las gráficas hacían parecer que era “inestabilidad aleatoria de infraestructura”.

La causa raíz fue mundana: pinned memory más muchos workers creó presión significativa sobre la RAM del host y el asignador de páginas. En nodos con otros servicios co-ubicados y necesidades de cache de filesystem, la “optimización” se convirtió en contención de memoria y picos de latencia. Los workers también saturaron el filesystem de red con más lecturas concurrentes pequeñas, aumentando la latencia cola para todos.

La solución no fue deshacer la paralelización; fue dimensionarla. Pusieron un tope de workers por nodo, staged datasets localmente para jobs de alto throughput y usaron pinned memory sólo donde la transferencia H2D estaba realmente en el camino crítico. Alimentar la GPU no es permiso para dejar al sistema operativo sin recursos.

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

Un equipo distinto operaba una plataforma de inferencia multi-tenant. Tenían una regla estricta: cada incidente de rendimiento comienza con una captura reproducible de métricas de host, métricas de GPU y una traza de perfil corta. Sin excepciones, sin “creo que”.

Un viernes, sonaron alarmas de latencia. La utilización de GPU era baja y la historia fácil era “la CPU está saturada”. Pero su runbook obligó a una comprobación rápida de colas de ejecución CPU, relojes GPU, estado PCIe y latencia de almacenamiento para la caché del modelo. La CPU no estaba saturada. La GPU no esperaba lanzamientos. PCIe estaba sano.

La traza mostró que el proceso de inferencia estaba bloqueado en lecturas de archivos para shards del modelo tras un despliegue. Un cambio aparentemente inocuo había movido el directorio de caché del modelo de NVMe local a un montaje en red. Nadie pensó que importara porque “el modelo cabe en RAM”, excepto que no siempre cabe en page cache bajo churn.

Revirtieron la ruta de caché, calentaron la caché deliberadamente y el incidente terminó sin depuraciones heroicas. La práctica que los salvó no fue un profiler sofisticado. Fue una lista de verificación que les impidió perseguir la historia de cuello de botella equivocada.

Errores comunes: síntoma → causa raíz → arreglo

1) Síntoma: Utilización de GPU baja, VRAM alta

Causa raíz: Asignaste tensores en GPU, pero el cómputo es ínfimo o está bloqueado por sincronización/espera de entrada.

Arreglo: Perfilado para detectar huecos; aumenta tamaño de lote; elimina sincronización por paso; verifica throughput del dataloader; revisa hotspots de preprocesamiento en CPU.

2) Síntoma: Utilización en picos, tiempo por paso inconsistente

Causa raíz: Jitter del pipeline de entrada (latencia de almacenamiento, filesystem remoto, pausas de GC, escalado de frecuencia de CPU).

Arreglo: Stagea datos localmente; shardea archivos; prefetch; fija governor de CPU; limita workers para evitar thrash.

3) Síntoma: Un core de CPU al 100%, otros ociosos, GPU no ocupada

Causa raíz: Lanzamiento single-threaded o sobrecarga de Python; contención por GIL; demasiados kernels pequeños; llamadas frecuentes al driver.

Arreglo: Fusiona ops; usa CUDA Graphs; agrupa trabajo; mueve lógica fuera del bucle caliente en Python; reduce operaciones por muestra en dispositivo.

4) Síntoma: Motores de copia ocupados, SM bajo

Causa raíz: Cuello de botella de transferencia (PCIe saturado, copias desde memoria pageable, transferencias pequeñas, estado de enlace incorrecto).

Arreglo: Asegura Gen/ancho; usa pinned memory con juicio; aumenta tamaño de lote; solapa copias con cómputo; reduce el chat entre host y dispositivo.

5) Síntoma: Relojes GPU bajos bajo carga

Causa raíz: Límite de potencia, throttling térmico, o la GPU está suficientemente inactiva como para bajar reloj.

Arreglo: Revisa límites de potencia, refrigeración y flujo de aire; si está inactiva, ve aguas arriba y halla la espera.

6) Síntoma: El rendimiento empeoró tras “más workers”

Causa raíz: Contención (tormenta de metadatos en filesystem, presión de RAM, overhead de cambio de contexto, thrash de caché).

Arreglo: Dimensiona bien el conteo de workers; usa shards más grandes; cachea; reduce transformaciones; mide throughput end-to-end, no solo la velocidad del loader.

7) Síntoma: Dos servidores idénticos difieren mucho

Causa raíz: Diferente negociación PCIe, colocación NUMA, ajustes de BIOS de potencia, daemons en segundo plano o diferencias en la ruta de almacenamiento.

Arreglo: Compara LnkSta de PCIe, governor de CPU, bindings NUMA y opciones de montaje de almacenamiento. Estandariza la imagen del nodo y el perfil de BIOS.

8) Síntoma: GPU subutilizada sólo con lotes pequeños

Causa raíz: La carga carece de paralelismo; overhead de lanzamiento y latencia de memoria dominan con lotes pequeños.

Arreglo: Aumenta tamaño de lote, usa batching/queueing o acepta baja utilización como compensación por latencia. No persigas 100% de utilización para un SLA p99.

Listas de verificación / plan paso a paso

Checklist A: Prueba o descarta “la CPU no puede alimentar la GPU” en 20 minutos

  1. Observa el comportamiento de la GPU: utilización, relojes, potencia, memoria, ráfagas.
  2. Comprueba el estado PCIe: gen/ancho negociado, errores, topología.
  3. Revisa la forma de la CPU: núcleo único al 100% vs muchos núcleos vs inactividad; presión de run queue.
  4. Revisa iowait y la ruta del dataset: local vs red; lecturas aleatorias vs secuenciales.
  5. Confirma localidad NUMA: afinidad CPU del proceso y binding de memoria vs attachment de la GPU.
  6. Toma un perfil corto: identifica si el tiempo está en lanzamientos de kernel, memcpy, decodificación o espera.

Checklist B: Si es envío desde CPU (límite de lanzamiento)

  1. Reduce el número de kernels: fusiona operaciones, reduce bucles en Python.
  2. Aumenta trabajo por lanzamiento: batches más grandes, tiles mayores, menos micro-kernels.
  3. Elimina puntos de sincronización: evita sincronizaciones forzadas en el camino caliente.
  4. Considera CUDA Graphs o una ruta de ejecución compilada si tu framework lo soporta.
  5. Reprueba con el mismo dataset y una semilla aleatoria fija para evitar perseguir ruido.

Checklist C: Si es preprocesamiento por CPU (decodificar/aumentar/tokenizar)

  1. Mide tiempos por etapa (lectura, decodificación, transformación, batching, copia).
  2. Paraleliza con cuidado: más workers hasta que aparezca contención, luego para.
  3. Prefiere operaciones vectorizadas y librerías que usen SIMD eficazmente.
  4. Cachea transformaciones costosas cuando sean reproducibles.
  5. Mueve transformaciones a la GPU cuando reduzca el coste de CPU más de lo que aumente el tiempo GPU.

Checklist D: Si es I/O

  1. Stagea datasets localmente para runs de alto throughput.
  2. Empaqueta muchos archivos pequeños en shards; evita opens por muestra.
  3. Prefetch y lee secuencialmente; aumenta tamaños de solicitud.
  4. Vigila operaciones de metadatos en filesystems de red.
  5. Verifica que el almacenamiento no esté compartido y saturado por otros jobs.

Checklist E: Si es ineficiencia en GPU

  1. Usa un profiler de línea de tiempo para ver stalls (memoria vs cómputo vs sync).
  2. Verifica que estás llegando a los kernels correctos (tensor cores, librerías optimizadas).
  3. Ajusta tamaño de lote y precisión para aumentar la intensidad aritmética.
  4. Arregla layout y patrones de acceso a memoria; evita kernels diminutos.
  5. Deja de culpar a la CPU cuando la GPU es la que está haciendo un mal trabajo.

Preguntas frecuentes

1) ¿La baja utilización de GPU es siempre mala?

No. Para inferencia sensible a latencia con lotes pequeños, baja utilización puede ser esperable. Optimiza para p95/p99 de latencia, no para que la gráfica de utilización luzca imponente.

2) ¿Cuál es una señal rápida de que el cuello de botella es el dataloader?

La utilización de GPU cae en los límites de lote, los núcleos CPU pican en tiempo de usuario y el throughput mejora cuando cacheas datos localmente o aumentas workers (hasta que aparece contención). También: tiempos de paso espasmódicos.

3) ¿Cómo diferencio un cuello de botella PCIe de uno de CPU?

Si los motores de copia están ocupados y SM bajo, sospecha PCIe/transferencia. Valida ancho/velocidad del enlace negociado. Si la CPU está caliente en llamadas al driver y ves muchos kernels pequeños, sospecha overhead de envío.

4) ¿Por qué a veces “más workers del dataloader” ralentiza?

Porque la concurrencia crea contención: tormentas de metadatos en el filesystem, thrash de caché, presión de memoria (especialmente con pinned memory) y overhead de cambio de contexto. El throughput tiene un máximo; encuéntralo.

5) ¿La memoria pinned siempre ayuda?

Ayuda en transferencias DMA, pero no es gratis. Demasiada memoria pinned reduce la flexibilidad del SO y puede aumentar la inestabilidad del sistema en cargas multi-tenant. Úsala donde H2D esté realmente en el camino crítico.

6) ¿Puede la CPU “alimentar” a la GPU con un solo hilo?

A veces. Para kernels grandes y cómputo intensivo, un único hilo host puede ser suficiente. Para cargas con muchos kernels pequeños, muchos lanzamientos o llamadas por muestra al dispositivo, un hilo se vuelve cuello de botella.

7) ¿Por qué dos nodos “idénticos” rinden distinto?

Porque no son idénticos en lo que importa: estado de enlace PCIe, localidad NUMA, ajustes de potencia del BIOS, I/O en segundo plano o rutas de almacenamiento pueden diferir. Mide eso primero.

8) ¿Cuál es el malentendido más común detrás de “la CPU no puede alimentar la GPU”?

La gente trata la “utilización de GPU” como una métrica única y verdadera. Es un promedio de una línea de tiempo compleja. Necesitas saber si la GPU está inactiva, copiando, estancada por memoria o simplemente ejecutando ráfagas cortas.

9) ¿Debería actualizar CPU o GPU primero si el entrenamiento es lento?

Si no has medido, ninguna. Si el perfil muestra preprocesamiento host o overhead de lanzamiento dominando, la CPU (o cambios en software) ayuda. Si los kernels GPU dominan y estás limitado por cómputo, la GPU ayuda. Si estás limitado por I/O, compra ancho de banda de almacenamiento y mejores formatos de datos.

Siguientes pasos (haz esto, no vibes)

Si sacas una lección operativa del meme “la CPU no puede alimentar la GPU”, que sea esta: la frase no es un diagnóstico. Es un recordatorio para instrumentar la canalización.

  1. Ejecuta el guion de diagnóstico rápido y clasifica el cuello de botella: envío, preprocesamiento, I/O o ineficiencia GPU.
  2. Valida la verdad física: estado de enlace PCIe, afinidad NUMA, relojes y throttling. Arregla la plataforma antes de tocar código.
  3. Elige una métrica que represente valor al usuario (muestras/segundo, latencia p99, coste por batch) y optimiza hacia ella. No hacia una bonita línea de utilización.
  4. Haz cambios reversibles y medibles: una variable a la vez, capturada con la misma porción de dataset y una corrida reproducible.
  5. Escribe el runbook que desearías tener. Tú en el futuro estará cansado y menos hábil.

Alimentar la GPU es un problema de sistemas. La CPU es solo uno de los camareros.

← Anterior
Limitación de tasas en Postfix: prevenir abusos sin bloquear a usuarios reales
Siguiente →
Ubuntu 24.04: Cuando GRO/LRO/TSO rompen cosas — cómo probar y desactivar con seguridad

Deja un comentario