Rapporten

124x Langzamer: Wat PyTorch DataLoader Echt Doet op Kernelniveau

mm
Voeg Unite.AI toe aan je voorkeursbronnen op Google
A conceptual widescreen illustration of an hourglass containing glowing digital data streams and circuit patterns, with the bottom half featuring a large data block and a GPU hardware component sitting idle to the side, representing data processing delays and GPU starvation.

Dit artikel is gebaseerd op de resultaten van een kernel-niveau GPU-trace-onderzoek dat is uitgevoerd op een echt PyTorch-probleem (#154318) met behulp van eBPF-uprobes. Trace-databases worden gepubliceerd in de Ingero-open-source-repository voor onafhankelijke verificatie.

TL;DR

PyTorch’s DataLoader kan 50-124x langzamer zijn dan directe tensor-indexering voor in-memory GPU-werkbelasting. We hebben een echt PyTorch-probleem gereproduceerd op een RTX 4090 en elke CUDA-API-aanroep en Linux-kernel-gebeurtenis getraceerd om de oorzaak te vinden. De GPU was niet langzaam – het was hongerig. DataLoader-werkers genereerden 200.000 CPU-context-switches en 300.000 pagina-toewijzingen in 40 seconden, waardoor de GPU gemiddeld 301ms per gegevensoverdracht moest wachten die normaal gesproken microseconden zou duren.

Het Probleem

Een PyTorch-gebruiker rapporteerde dat DataLoader 7-22x langzamer was dan directe tensor-indexering voor een eenvoudige MLP-inferentie-werkbelasting. Zelfs met num_workers=12, pin_memory=True en prefetch_factor=12 bleef de kloof enorm. GPU-gebruik zat op 10-20%.

We hebben het gereproduceerd. De kloof was nog erger op onze hardware:

Methode Tijd vs Direct
Directe tensor-indexering 0,39s 1x
DataLoader (shuffle=True) 48,49s 124x langzamer
DataLoader (geoptimaliseerd, 4 werknemers, pin_memory) 43,29s 111x langzamer

De werkbelasting is triviaal: 7M samples, 100 kenmerken, 2-laags MLP, batchgrootte 1M. Het model verwerkt een batch in milliseconden. Dus waar gaat de tijd heen?

Wat nvidia-smi Laat Zien

Niets nuttigs. GPU-gebruik schiet heen en weer tussen 0% en 30%. Geheugengebruik is stabiel. Temperatuur is in orde. De GPU is duidelijk onderbenut, maar nvidia-smi kan niet vertellen waarom.

Wat torch.profiler Laat Zien

De rapporteur probeerde PyTorch’s ingebouwde profiler en “kreeg geen zinvolle trace-gegevens.” Dit is een veelvoorkomende frustratie – applicatie-niveau-profilers kunnen laten zien welke CUDA-kernels worden uitgevoerd, maar ze kunnen de host-side-scheduling, geheugen- en proces-levensevents die bepalen of gegevens op tijd bij de GPU aankomen, niet zien.

Wat Kernel-Niveau Tracing Laat Zien

We hebben de benchmark uitgevoerd terwijl we zowel CUDA-API-aanroepen (via eBPF-uprobes op libcudart.so) als Linux-kernel-gebeurtenissen (scheduler-context-switches, geheugen-pagina-toewijzingen, proces-forks) tegelijkertijd traceerden. De resultaten vertellen het complete verhaal.

Volledige video-walkthrough: https://asciinema.org/a/RGwhPeXAPJdhXqxp

In de video hebben we een open-weight LLM (MiniMax-M2.7) verbonden met de trace-database via MCP (Model Context Protocol):

ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json

De JSON-config vertelt de MCP-client waar de Ingero-server en welke trace-database te vinden zijn:

{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}

Dit geeft de LLM directe toegang tot de trace-gegevens via 7 tools: get_trace_stats, get_causal_chains, get_per_process_breakdown en anderen. De AI kan de database ondervragen, CUDA-gebeurtenissen correleren met kernel-scheduling-gegevens en een eenvoudige taal-diagnose produceren zonder enige handmatige analyse.

4 HOGE-risico-causale ketens

De causale keten-engine detecteerde 4 hoge-risico-patronen, allemaal met dezelfde oorzaak:

[HOGE] cudaStreamSync p99=42ms (1.638x p50=25us) - CPU 100% + 1.880 sched_switch-gebeurtenissen
Tijdlijn:
[SYSTEM] CPU 100%
[HOST ] 1.880 context-switches (21s off-CPU)
[CUDA ] p99=42ms (1.638x p50=25us)
Oorzaak: DataLoader-werkers vechten om CPU, massive pagina-toewijzingsdruk
[HOGE] cudaLaunchKernel p99=24,67ms (349x p50=70us) - CPU 100%
Root: 34 sched_switch-gebeurtenissen

[HOGE] cuMemAlloc p99=627us (4,0x p50) - CPU 100%
[HOGE] cuLaunchKernel p99=106us (4,0x p50) - CPU 100%

De cudaStreamSync p99 is 1.638 keer de p50. Dat is geen GPU-trageheid – dat is de GPU die wacht op gegevens die nooit op tijd aankomen.

Figuur 1: AI-gegenereerde analyse na het uitvoeren van /investigate. Het model gebruikte Ingero’s 7 MCP-tools om de trace-database te ondervragen en produceerde een eenvoudige taal-verklaring met actieaanbevelingen die rechtstreeks uit de trace-gegevens zijn afgeleid.

De Per-Process Breakdown

Dit is waar het duidelijk wordt. Het hoofdproces en zijn 4 DataLoader-werkers zijn zichtbaar als afzonderlijke entiteiten:
Hoofdproces:

- cudaMemcpyAsync (host-to-device-overdracht): gem 301ms, max 2,9 seconden
- cudaStreamSync: p99 = 42ms (normaal 25us)
- 1.567 context-switches, gem 16ms off-CPU, worst-case-stall 5 seconden
- 799.018 pagina-toewijzingen
DataLoader-werker 1: 52.863 context-switches, 89.338 pagina-toewijzingen, worst-case-stall 5s
DataLoader-werker 2: 50.638 context-switches, 83.509 pagina-toewijzingen, worst-case-stall 5s
DataLoader-werker 3: 49.361 context-switches, 70.035 pagina-toewijzingen, worst-case-stall 5s
DataLoader-werker 4: 38.862 context-switches, 56.354 pagina-toewijzingen, worst-case-stall 5s

Totaal over werknemers: ~191.000 context-switches en ~299.000 pagina-toewijzingen in 40 seconden.

Wat Dit Betekent

De DataLoader-werkers doen drie dure dingen die directe indexering volledig vermijdt:

  1. Shuffling en indexering: DataLoader met shuffle=True genereert een willekeurige permutatie van indices, waarna elke werker zijn chunk selecteert. Dit vereist willekeurige geheugen-toegang over de volledige 7M-sample-tensor – verschrikkelijk voor cache-localiteit en triggerpagina-fouten.
  2. Collatie en kopiëren: Elke werker verzamelt verspreide samples in een aaneengesloten batch-tensor. Dit betekent het toewijzen van nieuw geheugen (pagina-toewijzingen), het kopiëren van gegevens van willekeurige locaties (cache-misses) en het serialiseren van het resultaat terug naar het hoofdproces via gedeelde geheugen of een wachtrij.
  3. Concurrentie voor CPU: Vier werknemers + het hoofdproces op een 4-vCPU-machine betekent constante pre-emptie. Elke werker wordt 50.000 keer uitgeschakeld. De worst-case-stall is 5 seconden – tijdens welke de GPU niets te verwerken heeft.

Met directe indexering: X[i:i+batch_size] is een zero-copy-weergave van een aaneengesloten tensor die al in het geheugen zit. .to(device) trigger een enkele DMA-overdracht van een enkele aaneengesloten regio. Geen werknemers, geen shuffling, geen collatie, geen cross-proces-kopiëren, geen context-switches. De GPU krijgt gegevens in microseconden, niet in honderden milliseconden.

De Oplossing

Voor in-memory GPU-werkbelastingen waar de hele dataset in het RAM-past:

  1. Gebruik geen DataLoader. Directe indexering met een vooraf-geshuffelde index-array is eenvoudiger en 100x sneller:
    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)
    
  2. Als u DataLoader moet gebruiken, moet u num_workers overeenkomen met uw werkelijke CPU-kernen minus 1. Op een 4-core-machine, num_workers=2 vermindert concurrentie. Voeg persistent_workers=True toe om fork-overhead te vermijden.
  3. Voor grotere-dan-geheugen-datasets waar DataLoader noodzakelijk is, verschuift de echte bottleneck naar schijf-I/O. Gebruik prefetch_factor=2 (niet hoger – meer prefetching betekent meer geheugendruk) en zorg ervoor dat uw opslag kan bijhouden.

Het Grotere Plaatje

Dit onderzoek illustreert een patroon dat we constant zien in GPU-werkbelastingen: de GPU is snel, de host is de bottleneck en GPU-metrieken kunnen het niet zien. nvidia-smi rapporteerde lage utilisatie, maar kon niet vertellen waarom. torch.profiler ving CUDA-kernels, maar miste de 200.000 context-switches die in userspace gebeurden.

De enige manier om het complete plaatje te zien was om zowel CUDA-API-aanroepen als Linux-kernel-scheduling-gebeurtenissen tegelijkertijd te traceren en te correleren door tijd en proces-ID. De causale keten “CPU 100% -> 1.880 sched_switch -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” vertelt het complete verhaal in één regel. Zonder cross-stack-tracing zou dit een mysterie zijn gebleven – zoals het voor de oorspronkelijke rapporteur was die wekenlang aan het debuggen was.

Figuur 2: Toen de model werd gevraagd “wat is het kernprobleem?”, identificeerde het CPU-overschrijving als oorzaak van host-side-scheduling-vertragingen. cudaLaunchKernel ging van 73us naar 25,8ms (356x langzamer) omdat de CPU de lancering niet op tijd kon plannen.

Probeer Het Zelf

Reproduceer de benchmark:

import torch, time
from torch.utils.data import DataLoader

X = 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()

# Snelle weg 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'Direct: {time.time()-start:.3f}s')

# Trage weg 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')

Trace met Ingero om te zien wat er gebeurt onder de motorkap:

git clone https://github.com/ingero-io/ingero.git
cd ingero && make build
sudo ./bin/ingero trace --duration 60s # in één terminal
python3 benchmark.py # in een andere terminal
./bin/ingero explain --since 60s # na benchmark-completie

Of sla de reproduktie over en onderzoek onze trace-gegevens rechtstreeks. De onderzoeksdatabase (764KB) is in de repo:

# Bekijk causale ketens van het onderzoek
./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m

# Per-proces-breakdown (zie DataLoader-werkers vs hoofdproces) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m

# Verbind uw AI-assistent voor interactief onderzoek ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db

Onderzoek met AI (aanbevolen). De snelste manier om de trace te analyseren is om elke MCP-compatibele AI rechtstreeks te verbinden met de database. Geen handmatige analyse nodig.

Maak een configuratiebestand:

cat > /tmp/ingero-mcp-dataloader.json << 'EOF'
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
EOF

Verbind dan uw model:

# Met Ollama + MiniMax (wat we in de video gebruikten)
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json

# Met Claude Code claude --mcp-config /tmp/ingero-mcp-dataloader.json

# Met elke MCP-compatibele client # Voeg de configuratie hierboven toe aan de MCP-instellingen van uw AI

Typ /investigate om de geleide analyse te activeren, of stel een vraag: “Wat veroorzaakte de GPU-sterfte?” De AI heeft toegang tot 7 tools die de trace-database rechtstreeks ondervragen.

GitHub: github.com/ingero-io/ingero
Oorspronkelijk probleem: pytorch/pytorch#154318
Video-walkthrough: https://asciinema.org/a/RGwhPeXAPJdhXqxp

Onderzoek uitgevoerd op TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.

David Mail is mede-auteur en onderhouder van Ingero, een open-source eBPF-agent voor CUDA-niveau GPU-observatie. Hij specialiseert zich in kernel-niveau tracing van productie-AI-werklasten.