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.













