Este artÃculo se basa en los hallazgos de una investigaciÃģn de seguimiento a nivel de kernel de GPU realizada en un problema real de PyTorch (#154318) utilizando eBPF uprobes. Las bases de datos de seguimiento se publican en el repositorio de cÃģdigo abierto de Ingero para verificaciÃģn independiente.
TL;DR
El DataLoader de PyTorch puede ser 50-124 veces mÃĄs lento que el Ãndice de tensor directo para cargas de trabajo de GPU en memoria. Reprodujimos un problema real de PyTorch en un RTX 4090 y rastreamos cada llamada a la API de CUDA y cada evento del kernel de Linux para encontrar la causa raÃz. La GPU no era lenta, estaba pasando hambre. Los trabajadores de DataLoader generaron 200,000 conmutaciones de contexto de CPU y 300,000 asignaciones de pÃĄgina en 40 segundos, lo que dejÃģ a la GPU esperando un promedio de 301ms por transferencia de datos que deberÃa tomar microsegundos.
El Problema
Un usuario de PyTorch informÃģ que DataLoader era 7-22 veces mÃĄs lento que el Ãndice de tensor directo para una carga de trabajo de inferencia de MLP simple. Incluso con num_workers=12, pin_memory=True y prefetch_factor=12, la brecha permaneciÃģ masiva. La utilizaciÃģn de la GPU se mantuvo en 10-20%.
Reproducimos el problema. La brecha era aÚn peor en nuestro hardware:
| MÃĐtodo | Tiempo | vs Direct |
|---|---|---|
| Ãndice de tensor directo | 0.39s | 1x |
| DataLoader (shuffle=True) | 48.49s | 124 veces mÃĄs lento |
| DataLoader (optimizado, 4 trabajadores, pin_memory) | 43.29s | 111 veces mÃĄs lento |
La carga de trabajo es trivial: 7M muestras, 100 caracterÃsticas, 2 capas de MLP, tamaÃąo de lote 1M. El modelo procesa un lote en milisegundos. Entonces, ÂŋdÃģnde va el tiempo?
QuÃĐ muestra nvidia-smi
Nada Útil. La utilizaciÃģn de la GPU parpadea entre 0% y 30%. El uso de memoria es estable. La temperatura es buena. La GPU estÃĄ claramente subutilizada, pero nvidia-smi no puede decir por quÃĐ.
QuÃĐ muestra torch.profiler
El informante intentÃģ el perfilador integrado de PyTorch y âobtuvo keine datos de seguimiento significativosâ. Esta es una frustraciÃģn comÚn: los perfiladores de nivel de aplicaciÃģn pueden mostrarte quÃĐ kernels de CUDA se estÃĄn ejecutando, pero no pueden ver la programaciÃģn del lado del host, los eventos de memoria y los eventos de ciclo de vida del proceso que determinan si los datos llegan a la GPU a tiempo.
QuÃĐ muestra el seguimiento a nivel de kernel
Ejecutamos la prueba de rendimiento mientras rastreamos tanto las llamadas a la API de CUDA (a travÃĐs de eBPF uprobes en libcudart.so) como los eventos del kernel de Linux (conmutaciones de contexto del programador, asignaciones de pÃĄgina de memoria, bifurcaciones de proceso) simultÃĄneamente. Los resultados cuentan la historia completa.
Video completo: https://asciinema.org/a/RGwhPeXAPJdhXqxp
En el video, conectamos un LLM de peso abierto (MiniMax-M2.7) a la base de datos de seguimiento a travÃĐs de MCP (Protocolo de Contexto de Modelo):
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.jsonEl archivo de configuraciÃģn JSON indica al cliente de MCP dÃģnde encontrar el servidor de Ingero y quÃĐ base de datos de seguimiento cargar:
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
Esto le da al LLM acceso directo a los datos de seguimiento a travÃĐs de 7 herramientas: get_trace_stats, get_causal_chains, get_per_process_breakdown, y otras. El AI puede consultar la base de datos, correlacionar eventos de CUDA con datos de programaciÃģn del kernel y producir una explicaciÃģn en lenguaje plano sin anÃĄlisis manual.
4 cadenas de causalidad de alta gravedad
El motor de cadena de causalidad detectÃģ 4 patrones de alta gravedad, todos con la misma causa raÃz:
[ALTA] cudaStreamSync p99=42ms (1,638x p50=25us) - CPU 100% + 1,880 conmutaciones de programador LÃnea de tiempo: [SYSTEM] CPU 100% [HOST ] 1,880 conmutaciones de contexto (21s fuera de CPU) [CUDA ] p99=42ms (1,638x p50=25us) Causa raÃz: Trabajadores de DataLoader luchando por CPU, presiÃģn de asignaciÃģn de pÃĄgina masiva
[ALTA] cudaLaunchKernel p99=24.67ms (349x p50) - CPU 100% RaÃz: 34 conmutaciones de programador [ALTA] cuMemAlloc p99=627us (4.0x p50) - CPU 100% [ALTA] cuLaunchKernel p99=106us (4.0x p50) - CPU 100%
La cudaStreamSync p99 es 1,638 veces la p50. Eso no es lentitud de la GPU, eso es la GPU esperando datos que nunca llegan a tiempo.
Figura 1: AnÃĄlisis generado por AI despuÃĐs de ejecutar /investigate. El modelo utilizÃģ las 7 herramientas de MCP de Ingero para consultar la base de datos de seguimiento y produjo una explicaciÃģn en lenguaje plano con recomendaciones accionables derivadas directamente de los datos de seguimiento.

Desglose por proceso
Aquà es donde se vuelve claro. El proceso principal y sus 4 trabajadores de DataLoader son visibles como entidades separadas:
Proceso principal:
- cudaMemcpyAsync (transferencia de host a dispositivo): avg 301ms, max 2.9 segundos - cudaStreamSync: p99 = 42ms (normalmente 25us) - 1,567 conmutaciones de contexto, avg 16ms fuera de CPU, peor parada 5 segundos - 799,018 asignaciones de pÃĄgina
Trabajador de DataLoader 1: 52,863 conmutaciones de contexto, 89,338 asignaciones de pÃĄgina, peor parada 5s Trabajador de DataLoader 2: 50,638 conmutaciones de contexto, 83,509 asignaciones de pÃĄgina, peor parada 5s Trabajador de DataLoader 3: 49,361 conmutaciones de contexto, 70,035 asignaciones de pÃĄgina, peor parada 5s Trabajador de DataLoader 4: 38,862 conmutaciones de contexto, 56,354 asignaciones de pÃĄgina, peor parada 5s
Total en trabajadores: ~191,000 conmutaciones de contexto y ~299,000 asignaciones de pÃĄgina en 40 segundos.
QuÃĐ significa esto
Los trabajadores de DataLoader estÃĄn haciendo tres cosas costosas que el Ãndice directo evita por completo:
- Barajado e Ãndice: DataLoader con shuffle=True genera una permutaciÃģn aleatoria de Ãndices, luego cada trabajador selecciona su parte. Esto requiere acceso aleatorio a memoria a travÃĐs del tensor de 7M muestras completo â terrible para la localidad de cachÃĐ y desencadena fallos de pÃĄgina.
- AgrupaciÃģn y copia: Cada trabajador reÚne muestras dispersas en un tensor de lote contiguo. Esto significa asignar nueva memoria (asignaciones de pÃĄgina), copiar datos de ubicaciones aleatorias (fallos de cachÃĐ) y serializar el resultado de regreso al proceso principal a travÃĐs de memoria compartida o una cola.
- Competencia por CPU: Cuatro trabajadores + el proceso principal en una mÃĄquina de 4 vCPU significa preemption constante. Cada trabajador se desprograma 50,000 veces. La peor parada es de 5 segundos â durante la cual la GPU no tiene nada que procesar.
Con Ãndice directo: X[i:i+batch_size] es una vista de tensor contiguo ya en memoria. .to(device) desencadena una transferencia de DMA desde una regiÃģn contigua Única. No hay trabajadores, no hay barajado, no hay agrupaciÃģn, no hay copias entre procesos, no hay conmutaciones de contexto. La GPU obtiene datos en microsegundos, no en cientos de milisegundos.
La soluciÃģn
Para cargas de trabajo de GPU en memoria donde el conjunto de datos completo cabe en RAM:
- No utilice DataLoader. Ãndice directo con un arreglo de Ãndice prebarajado es mÃĄs simple y 100 veces mÃĄs rÃĄpido:
indices = torch.randperm(num_samples) for i in range(0, num_samples, batch_size): batch = X[indices[i:i+batch_size]].to(device) output = model(batch)
- Si debe utilizar DataLoader, coincida con num_workers con sus nÚcleos de CPU reales menos 1. En una mÃĄquina de 4 nÚcleos, num_workers=2 reduce la contienda. Agregue persistent_workers=True para evitar la sobrecarga de bifurcaciÃģn.
- Para conjuntos de datos mÃĄs grandes que la memoria donde DataLoader es necesario, el verdadero cuello de botella se desplaza a la E/S de disco. Utilice prefetch_factor=2 (no mÃĄs alto â mÃĄs prefetching significa mÃĄs presiÃģn de memoria) y asegÚrese de que su almacenamiento pueda seguir el ritmo.
La imagen mÃĄs grande
Esta investigaciÃģn ilustra un patrÃģn que vemos constantemente en cargas de trabajo de GPU: la GPU es rÃĄpida, el host es el cuello de botella y las mÃĐtricas de la GPU no pueden verlo. nvidia-smi informÃģ una utilizaciÃģn baja pero no pudo explicar por quÃĐ. torch.profiler capturÃģ kernels de CUDA pero perdiÃģ las 200,000 conmutaciones de contexto que ocurrÃan en el espacio de usuario.
La Única forma de ver la imagen completa fue rastrear ambos lados simultÃĄneamente â llamadas a la API de CUDA a nivel de biblioteca y eventos de programaciÃģn del kernel de Linux â y correlacionarlos por tiempo y ID de proceso. La cadena de causalidad âCPU 100% -> 1,880 conmutaciones de programador -> cudaMemcpyAsync 301ms -> cudaStreamSync 42msâ cuenta la historia completa en una lÃnea. Sin seguimiento entre pilas, esto habrÃa seguido siendo un misterio â como lo fue para el informante original que pasÃģ semanas depurÃĄndolo.
Figura 2: Cuando se le preguntÃģ âÂŋCuÃĄl es el problema principal?â, el modelo identifica la sobre-suscripciÃģn de CPU que causa retrasos en la programaciÃģn del lado del host. cudaLaunchKernel pasÃģ de 73us a 25.8ms (356 veces mÃĄs lento) porque la CPU no podÃa programar el lanzamiento a tiempo.

Pruebe usted mismo
Reproduzca la prueba de rendimiento:
import torch, time from torch.utils.data import DataLoaderX = torch.randn(7_000_000, 100) model = torch.nn.Sequential( torch.nn.Linear(100, 512), torch.nn.ReLU(), torch.nn.Linear(512, 512), torch.nn.ReLU(), torch.nn.Linear(512, 10) ).cuda()
# Ruta rÃĄpida start = time.time() with torch.no_grad(): for i in range(0, len(X), 1_048_576): model(X[i:i+1_048_576].cuda()) torch.cuda.synchronize() print(f'Directo: {time.time()-start:.3f}s')
# Ruta lenta loader = DataLoader(X, batch_size=1_048_576, shuffle=True) start = time.time() with torch.no_grad(): for batch in loader: model(batch.cuda()) torch.cuda.synchronize() print(f'DataLoader: {time.time()-start:.3f}s')
Rastree con Ingero para ver quÃĐ estÃĄ sucediendo debajo del capÃģ:
git clone https://github.com/ingero-io/ingero.git cd ingero && make build sudo ./bin/ingero trace --duration 60s # en un terminal python3 benchmark.py # en otro terminal ./bin/ingero explain --since 60s # despuÃĐs de que se complete la prueba de rendimiento
O omita la reproducciÃģn y explore nuestros datos de seguimiento directamente. La base de datos de investigaciÃģn (764KB) estÃĄ en el repositorio:
# Ver cadenas de causalidad de la investigaciÃģn ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m# Desglose por proceso (ver trabajadores de DataLoader vs proceso principal) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m
# Conecte su asistente de AI para investigaciÃģn interactiva ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db
Investigue con AI (recomendado). La forma mÃĄs rÃĄpida de analizar el seguimiento es conectar cualquier AI compatible con MCP directamente a la base de datos. No se requiere anÃĄlisis manual.
Cree un archivo de configuraciÃģn:
cat > /tmp/ingero-mcp-dataloader.json << 'EOF'
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
EOF
Luego conecte su modelo:
# Con Ollama + MiniMax (lo que usamos en el video) ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json# Con Claude Code claude --mcp-config /tmp/ingero-mcp-dataloader.json
# Con cualquier cliente compatible con MCP # Agregue la configuraciÃģn anterior a la configuraciÃģn de MCP de su AI
Escriba /investigate para desencadenar el anÃĄlisis guiado, o haga cualquier pregunta: âÂŋQuÃĐ causÃģ la hambruna de la GPU?â El AI tiene acceso a 7 herramientas que consultan la base de datos de seguimiento directamente.
GitHub: github.com/ingero-io/ingero
Problema original: pytorch/pytorch#154318
Video: https://asciinema.org/a/RGwhPeXAPJdhXqxp
InvestigaciÃģn realizada en TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.













