Rapporter

124x Långsammare: Vad PyTorch DataLoader Verkligen Gör på Kärnnivå

mm
Lägg till Unite.AI bland dina föredragna källor på 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.

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.json

JSON-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:

  1. 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.
  2. 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ö.
  3. 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:

  1. 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)
    
  2. 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.
  3. 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å biblioteks­nivå 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 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()

# 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.

David Mail är medförfattare och underhållare av Ingero, en öppen källkods-eBPF-agent för CUDA-nivå GPU-övervakning. Han specialiserar sig på kernel-nivå spårning av produktions AI-arbetsbelastningar.