Den här artikeln baseras på resultat från en kernel-nivå GPU-spårningsutredning som utfördes på ett riktigt PyTorch-problem (#154318) med hjälp av eBPF uprobes. Spårningsdatabaser publiceras i Ingero öppen källkodsrepository för oberoende verifiering.
TL;DR
PyTorch’s DataLoader kan vara 50-124x långsammare än direkt tensorindexering för in-memory GPU-arbetsbelastningar. Vi återskapade ett riktigt PyTorch-problem på en RTX 4090 och spårade varje CUDA API-anrop och Linux-kernelhändelse för att hitta roten till problemet. GPU:n var inte långsam – den svalt. DataLoader-arbetare genererade 200 000 CPU-kontextbyten och 300 000 sidallokeringar på 40 sekunder, vilket gjorde att GPU:n i genomsnitt väntade 301 ms per dataöverföring som borde ta mikrosekunder.
Problemet
En PyTorch-användare rapporterade att DataLoader var 7-22x långsammare än direkt tensorindexering för en enkel MLP-inferensarbetsbelastning. Även med num_workers=12, pin_memory=True och prefetch_factor=12 kvarstod gapet. GPU-användningen låg på 10-20%.
Vi återskapade det. Gapet var ännu värre på vår maskinvara:
| Metod | Tid | vs Direkt |
|---|---|---|
| Direkt tensorindexering | 0,39 s | 1x |
| DataLoader (shuffle=True) | 48,49 s | 124x långsammare |
| DataLoader (optimerad, 4 arbetare, pin_memory) | 43,29 s | 111x långsammare |
Arbetsbelastningen är trivial: 7M prover, 100 funktioner, 2-lagers MLP, batchstorlek 1M. Modellen bearbetar en batch på millisekunder. Så var tar tiden vägen?
Vad nvidia-smi Visar
Ingenting användbart. GPU-användningen flimrar mellan 0% och 30%. Minnesanvändningen är stabil. Temperaturen är bra. GPU:n är tydligt underutnyttjad, men nvidia-smi kan inte berätta varför.
Vad torch.profiler Visar
Rapportören försökte PyTorch’s inbyggda profiler och “fick ingen meningsfull spårdata.” Detta är en vanlig frustration – applikationsnivåprofiler kan visa vilka CUDA-kärnor som körs, men de kan inte se värdenivåschemaläggning, minnes- och processlivscykelhändelser som bestämmer om data anländer till GPU:n i tid.
Vad Kernel-nivå Spårning Visar
Vi körde benchmarken medan vi spårade både CUDA API-anrop (via eBPF uprobes på libcudart.so) och Linux-kernelhändelser (schemaläggarkontextbyten, minnessidallokeringar, processfork) samtidigt. Resultaten berättar hela historien.
Fullständig videogenomgång: https://asciinema.org/a/RGwhPeXAPJdhXqxp
I videon anslöt vi en öppen viktad LLM (MiniMax-M2.7) till spårningsdatabasen via MCP (Model Context Protocol):
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.jsonJSON-konfigen berättar för MCP-klienten var den ska hitta Ingero-servern och vilken spårningsdatabas som ska laddas:
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
Detta ger LLM direkt tillgång till spårningsdata genom 7 verktyg: get_trace_stats, get_causal_chains, get_per_process_breakdown och andra. AI:n kan fråga databasen, korrelera CUDA-händelser med kernel-schemaläggningsdata och producera en vanlig språkdiagnos utan någon manuell analys.
4 HÖG-severity orsakskedjor
Orsakskedje-motorn upptäckte 4 hög-severity-mönster, alla med samma roten:
[HÖG] cudaStreamSync p99=42ms (1 638x p50=25us) - CPU 100% + 1 880 schemaläggningsbyten Tidslinje: [SYSTEM] CPU 100% [HOST ] 1 880 kontextbyten (21s off-CPU) [CUDA ] p99=42ms (1 638x p50=25us) Rotorsak: DataLoader-arbetare kämpar om CPU, massiv sidallokeringspress
[HÖG] cudaLaunchKernel p99=24,67ms (349x p50=70us) - CPU 100% Rot: 34 schemaläggningsbyten [HÖG] cuMemAlloc p99=627us (4,0x p50) - CPU 100% [HÖG] cuLaunchKernel p99=106us (4,0x p50) - CPU 100%
cudaStreamSync p99 är 1 638 gånger p50. Det är inte GPU-långsamhet – det är GPU:n som väntar på data som aldrig anländer i tid.
Figur 1: AI-genererad analys efter att ha kört /investigate. Modellen använde Ingeros 7 MCP-verktyg för att fråga spårningsdatabasen och producerade en vanlig språkförklaring med åtgärdsrekommendationer som härrör direkt från spårningsdata.

Per-process-genombrott
Detta är där det blir tydligt. Huvudprocessen och dess 4 DataLoader-arbetare är synliga som separata enheter:
Huvudprocess:
- cudaMemcpyAsync (host-to-device-överföring): genomsnitt 301ms, max 2,9 sekunder - cudaStreamSync: p99 = 42ms (vanligtvis 25us) - 1 567 kontextbyten, genomsnitt 16ms off-CPU, värsta stillestånd 5 sekunder - 799 018 sidallokeringar
DataLoader-arbetare 1: 52 863 kontextbyten, 89 338 sidallokeringar, värsta stillestånd 5s DataLoader-arbetare 2: 50 638 kontextbyten, 83 509 sidallokeringar, värsta stillestånd 5s DataLoader-arbetare 3: 49 361 kontextbyten, 70 035 sidallokeringar, värsta stillestånd 5s DataLoader-arbetare 4: 38 862 kontextbyten, 56 354 sidallokeringar, värsta stillestånd 5s
Totalt över arbetare: ~191 000 kontextbyten och ~299 000 sidallokeringar på 40 sekunder.
Vad Detta Betyder
DataLoader-arbetarna gör tre dyra saker som direktindexering undviker helt:
- Blandning och indexering: DataLoader med shuffle=True genererar en slumpmässig permutation av index, sedan väljer varje arbetare sin chunk. Detta kräver slumpmässig minnesåtkomst över hela 7M-provtensorn – dåligt för cache-lokalitet och utlöser sidfel.
- Sammanfogning och kopiering: Varje arbetare samlar spridda prover i en sammanhängande batch-tensor. Detta innebär att allokera nytt minne (sidallokeringar), kopiera data från slumpmässiga platser (cache-missar), och serialisera resultatet tillbaka till huvudprocessen via delad minnesrymd eller en kö.
- Konkurrerar om CPU: Fyra arbetare + huvudprocessen på en 4-vCPU-maskin betyder konstant fördröjning. Varje arbetare blir descheduled 50 000 gånger. Värsta stilleståndet är 5 sekunder – under vilket GPU:n inte har något att bearbeta.
Med direktindexering: X[i:i+batch_size] är en noll-kopiering av en sammanhängande tensor som redan finns i minnet. .to(device) utlöser en enda DMA-överföring från en enda sammanhängande region. Inga arbetare, ingen blandning, ingen sammanfogning, inga cross-process-kopior, inga kontextbyten. GPU:n får data på mikrosekunder, inte hundratals millisekunder.
Lösningen
För in-memory GPU-arbetsbelastningar där hela datasetet passar i RAM:
- Använd inte DataLoader. Direktindexering med en för-blandad indexarray är enklare och 100x snabbare:
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)
- Om du måste använda DataLoader, matcha num_workers till dina faktiska CPU-kärnor minus 1. På en 4-kärnig maskin, num_workers=2 minskar konkurrensen. Lägg till persistent_workers=True för att undvika fork-överhuvud.
- För större-än-minne-datasets där DataLoader är nödvändig, skiftar den verkliga flaskhalsen till disk-I/O. Använd prefetch_factor=2 (inte högre – mer prefetching betyder mer minnespress). Se till att din lagring kan hålla jämna steg.
Den Större Bilden
Den här utredningen illustrerar ett mönster vi ser konstant i GPU-arbetsbelastningar: GPU:n är snabb, värden är flaskhalsen, och GPU-mätningar kan inte se det. nvidia-smi rapporterade låg användning men kunde inte förklara varför. torch.profiler fångade CUDA-kärnor men missade de 200 000 kontextbyten som skedde i användarutrymme.
Den enda vägen att se hela bilden var att spåra båda sidor samtidigt – CUDA API-anrop på biblioteksnivå och Linux-kernel-schemaläggningshändelser – och korrelera dem med tid och process-ID. Orsakskedjan “CPU 100% -> 1 880 schemaläggningsbyten -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” berättar hela historien på en rad. Utan cross-stack-spårning skulle detta ha förblivit ett mysterium – som det var för den ursprungliga rapportören som tillbringade veckor med felsökning.
Figur 2: När modellen tillfrågades “vad är det grundläggande problemet?”, identifierade den CPU-överprenumeration som orsakar värdenivå-schemaläggningsförseningar. cudaLaunchKernel gick från 73us till 25,8ms (356x långsammare) eftersom CPU:n inte kunde schemalägga lanseringen i tid.

Försök Själv
Reproduera benchmarken:
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()
# Snabb väg 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'Direkt: {time.time()-start:.3f}s')
# Långsam väg 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')
Spåra med Ingero för att se vad som händer under huven:
git clone https://github.com/ingero-io/ingero.git cd ingero && make build sudo ./bin/ingero trace --duration 60s # i en terminal python3 benchmark.py # i en annan terminal ./bin/ingero explain --since 60s # efter benchmarken är klar
Eller hoppa över reproduktionen och utforska vår spårningsdata direkt. Utredningsdatabasen (764KB) finns i repot:
# Visa orsakskedjor från utredningen ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m# Per-process-genombrott (se DataLoader-arbetare vs huvudprocess) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m
# Anslut din AI-assistent för interaktiv utredning ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db
Utred med AI (rekommenderas). Den snabbaste vägen att analysera spårningen är att ansluta någon MCP-kompatibel AI direkt till databasen. Inga manuella analyser behövs.
Skapa en konfigurationsfil:
cat > /tmp/ingero-mcp-dataloader.json << 'EOF'
{
"mcpServers": {
"ingero": {
"command": "./bin/ingero",
"args": ["mcp", "--db", "investigations/pytorch-dataloader-starvation.db"]
}
}
}
EOF
Sedan anslut din modell:
# Med Ollama + MiniMax (vad vi använde i videon) ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json# Med Claude Code claude --mcp-config /tmp/ingero-mcp-dataloader.json
# Med någon MCP-kompatibel klient # Lägg till konfigen ovan till din AI:s MCP-inställningar
Skriv /investigate för att utlösa den guidade analysen, eller ställ någon fråga: “Vad orsakade GPU-svälten?” AI:n har tillgång till 7 verktyg som frågar spårningsdatabasen direkt.
GitHub: github.com/ingero-io/ingero
Original issue: pytorch/pytorch#154318
Video walkthrough: https://asciinema.org/a/RGwhPeXAPJdhXqxp
Utredning utförd på TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.













