Este artigo ÃĐ baseado em descobertas de uma investigaçÃĢo de rastreamento de nÃvel de kernel da GPU realizada em um problema real do PyTorch (#154318) usando eBPF uprobes. Os bancos de dados de rastreamento sÃĢo publicados no repositÃģrio de cÃģdigo aberto do Ingero para verificaçÃĢo independente.
TL;DR
O DataLoader do PyTorch pode ser 50-124x mais lento do que o Ãndice de tensor direto para cargas de trabalho de GPU em memÃģria. Reproduzimos um problema real do PyTorch em um RTX 4090 e rastreamos cada chamada de API do CUDA e evento do kernel do Linux para encontrar a causa raiz. A GPU nÃĢo estava lenta â estava com fome. Os trabalhadores do DataLoader geraram 200.000 mudanças de contexto de CPU e 300.000 alocaçÃĩes de pÃĄgina em 40 segundos, deixando a GPU esperando em mÃĐdia 301ms por transferÊncia de dados que deveria levar microssegundos.
O Problema
Um usuÃĄrio do PyTorch relatou que o DataLoader era 7-22x mais lento do que o Ãndice de tensor direto para uma carga de trabalho de inferÊncia de MLP simples. Mesmo com num_workers=12, pin_memory=True e prefetch_factor=12, a lacuna permaneceu massiva. A utilizaçÃĢo da GPU estava em 10-20%.
Reproduzimos isso. A lacuna era ainda pior em nosso hardware:
| MÃĐtodo | Tempo | vs Direto |
|---|---|---|
| Ãndice de tensor direto | 0,39s | 1x |
| DataLoader (shuffle=True) | 48,49s | 124x mais lento |
| DataLoader (otimizado, 4 trabalhadores, pin_memory) | 43,29s | 111x mais lento |
A carga de trabalho ÃĐ trivial: 7M amostras, 100 recursos, 2 camadas de MLP, tamanho de lote 1M. O modelo processa um lote em milissegundos. EntÃĢo, onde vai o tempo?
O que o nvidia-smi Mostra
Nada Útil. A utilizaçÃĢo da GPU oscila entre 0% e 30%. O uso de memÃģria ÃĐ estÃĄvel. A temperatura estÃĄ boa. A GPU estÃĄ claramente subutilizada, mas o nvidia-smi nÃĢo pode dizer por quÊ.
O que o torch.profiler Mostra
O relator tentou o profiler integrado do PyTorch e âobteve nenhum dado de rastreamento significativoâ. Isso ÃĐ uma frustraçÃĢo comum â os perfiladores de nÃvel de aplicativo podem mostrar quais kernels do CUDA estÃĢo executando, mas nÃĢo podem ver a programaçÃĢo de lado do host, eventos de memÃģria e ciclo de vida de processo que determinam se os dados chegam à GPU no momento certo.
O que o Rastreamento de NÃvel de Kernel Mostra
Executamos o benchmark enquanto rastreamos chamadas de API do CUDA (via eBPF uprobes no libcudart.so) e eventos do kernel do Linux (mudanças de contexto de programaçÃĢo, alocaçÃĩes de pÃĄgina de memÃģria, bifurcaçÃĩes de processo) simultaneamente. Os resultados contam a histÃģria completa.
Full video walkthrough: https://asciinema.org/a/RGwhPeXAPJdhXqxp
No vÃdeo, conectamos um LLM de peso aberto (MiniMax-M2.7) ao banco de dados de rastreamento via MCP (Model Context Protocol):
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.jsonO arquivo de configuraçÃĢo JSON diz ao cliente MCP onde encontrar o servidor Ingero e qual banco de dados de rastreamento carregar:
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
Isso dÃĄ ao LLM acesso direto aos dados de rastreamento por meio de 7 ferramentas: get_trace_stats, get_causal_chains, get_per_process_breakdown e outras. O AI pode consultar o banco de dados, correlacionar eventos do CUDA com dados de programaçÃĢo do kernel e produzir uma explicaçÃĢo em linguagem simples sem anÃĄlise manual.
4 Cadeias Causais de Alta Severidade
O mecanismo de cadeia causal detectou 4 padrÃĩes de alta severidade, todos com a mesma causa raiz:
[ALTA] cudaStreamSync p99=42ms (1.638x p50=25us) - CPU 100% + 1.880 mudanças de contexto Linha do tempo: [SYSTEM] CPU 100% [HOST ] 1.880 mudanças de contexto (21s fora da CPU) [CUDA ] p99=42ms (1.638x p50=25us) Causa raiz: Trabalhadores do DataLoader lutando por CPU, pressÃĢo de alocaçÃĢo de pÃĄgina maciça
[ALTA] cudaLaunchKernel p99=24,67ms (349x p50=70us) - CPU 100% Raiz: 34 mudanças de contexto [ALTA] cuMemAlloc p99=627us (4,0x p50) - CPU 100% [ALTA] cuLaunchKernel p99=106us (4,0x p50) - CPU 100%
O cudaStreamSync p99 ÃĐ 1.638 vezes o p50. Isso nÃĢo ÃĐ lentidÃĢo da GPU â ÃĐ a GPU esperando por dados que nunca chegam no momento certo.
Figura 1: AnÃĄlise gerada pelo AI apÃģs executar /investigate. O modelo usou as 7 ferramentas MCP do Ingero para consultar o banco de dados de rastreamento e produzir uma explicaçÃĢo em linguagem simples com recomendaçÃĩes derivadas diretamente dos dados de rastreamento.

A Quebra por Processo
Aqui ÃĐ onde fica claro. O processo principal e seus 4 trabalhadores do DataLoader sÃĢo visÃveis como entidades separadas:
Processo principal:
- cudaMemcpyAsync (transferÊncia host-para-dispositivo): mÃĐdia 301ms, mÃĄximo 2,9 segundos - cudaStreamSync: p99 = 42ms (normalmente 25us) - 1.567 mudanças de contexto, mÃĐdia 16ms fora da CPU, pior parada 5 segundos - 799.018 alocaçÃĩes de pÃĄgina
Trabalhador do DataLoader 1: 52.863 mudanças de contexto, 89.338 alocaçÃĩes de pÃĄgina, pior parada 5s Trabalhador do DataLoader 2: 50.638 mudanças de contexto, 83.509 alocaçÃĩes de pÃĄgina, pior parada 5s Trabalhador do DataLoader 3: 49.361 mudanças de contexto, 70.035 alocaçÃĩes de pÃĄgina, pior parada 5s Trabalhador do DataLoader 4: 38.862 mudanças de contexto, 56.354 alocaçÃĩes de pÃĄgina, pior parada 5s
Total nos trabalhadores: ~191.000 mudanças de contexto e ~299.000 alocaçÃĩes de pÃĄgina em 40 segundos.
O que Isso Significa
Os trabalhadores do DataLoader estÃĢo fazendo trÊs coisas caras que o Ãndice direto evita completamente:
- Embaralhamento e Ãndice: O DataLoader com shuffle=True gera uma permutaçÃĢo aleatÃģria de Ãndices, entÃĢo cada trabalhador seleciona seu pedaço. Isso requer acesso aleatÃģrio à memÃģria ao longo do tensor de 7M amostras â terrÃvel para a localidade do cache e dispara falhas de pÃĄgina.
- Coleta e cÃģpia: Cada trabalhador reÚne amostras espalhadas em um tensor de lote contÃguo. Isso significa alocar nova memÃģria (alocaçÃĩes de pÃĄgina), copiar dados de locais aleatÃģrios (erros de cache) e serializar o resultado de volta ao processo principal via memÃģria compartilhada ou fila.
- ConcorrÊncia por CPU: Quatro trabalhadores + o processo principal em uma mÃĄquina de 4 vCPUs significa preempçÃĢo constante. Cada trabalhador ÃĐ desprogramado 50.000 vezes. A pior parada ÃĐ de 5 segundos â durante o qual a GPU nÃĢo tem nada para processar.
Com Ãndice direto: X[i:i+batch_size] ÃĐ uma visÃĢo de zero cÃģpia de um tensor contÃguo jÃĄ na memÃģria. .to(device) dispara uma transferÊncia de DMA de uma regiÃĢo contÃgua Única. Nenhum trabalhador, nenhum embaralhamento, nenhuma coleta, nenhuma cÃģpia entre processos, nenhuma mudança de contexto. A GPU obtÃĐm dados em microssegundos, nÃĢo em centenas de milissegundos.
A SoluçÃĢo
Para cargas de trabalho de GPU em memÃģria onde o conjunto de dados inteiro cabe na RAM:
- NÃĢo use o DataLoader. Ãndice direto com um array de Ãndice prÃĐ-embaralhado ÃĐ mais simples e 100x mais 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)
- Se vocÊ deve usar o DataLoader, faça com que num_workers corresponda aos nÚcleos de CPU reais menos 1. Em uma mÃĄquina de 4 nÚcleos, num_workers=2 reduz a concorrÊncia. Adicione persistent_workers=True para evitar a sobrecarga de bifurcaçÃĢo.
- Para conjuntos de dados maiores que a memÃģria onde o DataLoader ÃĐ necessÃĄrio, o verdadeiro gargalo muda para E/S de disco. Use prefetch_factor=2 (nÃĢo mais alto â mais prÃĐ-busca significa mais pressÃĢo de memÃģria) e certifique-se de que seu armazenamento possa acompanhar.
O Quadro Maior
Esta investigaçÃĢo ilustra um padrÃĢo que vemos constantemente em cargas de trabalho de GPU: a GPU ÃĐ rÃĄpida, o host ÃĐ o gargalo e as mÃĐtricas da GPU nÃĢo podem vÊ-lo. O nvidia-smi relatou baixa utilizaçÃĢo, mas nÃĢo pÃīde explicar por quÊ. O torch.profiler capturou kernels do CUDA, mas perdeu as 200.000 mudanças de contexto acontecendo no espaço do usuÃĄrio.
A Única maneira de ver a imagem completa foi rastrear ambos os lados simultaneamente â chamadas de API do CUDA no nÃvel da biblioteca e eventos de programaçÃĢo do kernel do Linux â e correlacionÃĄ-los por tempo e ID de processo. A cadeia causal âCPU 100% -> 1.880 mudanças de contexto -> cudaMemcpyAsync 301ms -> cudaStreamSync 42msâ conta a histÃģria completa em uma linha. Sem rastreamento entre pilhas, isso teria permanecido um mistÃĐrio â como foi para o relator original que passou semanas depurando.
Figura 2: Quando perguntado âqual ÃĐ o problema principal?â, o modelo identifica a superinscriçÃĢo da CPU causando atrasos de programaçÃĢo do lado do host. O cudaLaunchKernel passou de 73us para 25,8ms (356x mais lento) porque a CPU nÃĢo pÃīde programar o lançamento no momento certo.

Tente VocÊ Mesmo
Reproduza o benchmark:
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()
# Caminho rÃĄpido 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'Direto: {time.time()-start:.3f}s')
# Caminho lento 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')
Rastreie com o Ingero para ver o que estÃĄ acontecendo por baixo dos panos:
git clone https://github.com/ingero-io/ingero.git cd ingero && make build sudo ./bin/ingero trace --duration 60s # em um terminal python3 benchmark.py # em outro terminal ./bin/ingero explain --since 60s # apÃģs o benchmark concluir
Ou pule a reproduçÃĢo e explore os dados de rastreamento diretamente. O banco de dados de investigaçÃĢo (764KB) estÃĄ no repositÃģrio:
# Exiba cadeias causais da investigaçÃĢo ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m# Quebra por processo (veja trabalhadores do DataLoader vs processo principal) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m
# Conecte seu assistente de IA para investigaçÃĢo interativa ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db
Investigue com a IA (recomendado). A maneira mais rÃĄpida de analisar o rastreamento ÃĐ conectar qualquer IA compatÃvel com o MCP diretamente ao banco de dados. Nenhuma anÃĄlise manual ÃĐ necessÃĄria.
Crie um arquivo de configuraçÃĢo:
cat > /tmp/ingero-mcp-dataloader.json << 'EOF'
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
EOF
Em seguida, conecte seu modelo:
# Com Ollama + MiniMax (o que usamos no vÃdeo) ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json# Com Claude Code claude --mcp-config /tmp/ingero-mcp-dataloader.json
# Com qualquer cliente compatÃvel com o MCP # Adicione a configuraçÃĢo acima à s configuraçÃĩes do MCP do seu AI
Digite /investigate para disparar a anÃĄlise guiada ou faça qualquer pergunta: âO que causou a fome da GPU?â A IA tem acesso a 7 ferramentas que consultam o banco de dados de rastreamento diretamente.
GitHub: github.com/ingero-io/ingero
Problema original: pytorch/pytorch#154318
Video walkthrough: https://asciinema.org/a/RGwhPeXAPJdhXqxp
InvestigaçÃĢo realizada no TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.













