Rapporter

124 gange langsommere: Hvad PyTorch DataLoader faktisk gør på kernelniveau

mm
Føj Unite.AI til dine foretrukne kilder 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.
NøglepunkterPyTorch DataLoader can be 124x slower than direct tensor indexing due to massive CPU…
Vis mere
  • PyTorch DataLoader can be 124x slower than direct tensor indexing due to massive CPU overhead.
  • Kernel-level tracing revealed 200,000 context switches starved the GPU for hundreds of milliseconds.
  • The root cause was DataLoader workers generating excessive page allocations and scheduling contention.

Denne artikel er baseret på fund fra en kernel-niveau GPU-sporingsundersøgelse udført på et rigtigt PyTorch-problem (#154318) ved hjælp af eBPF uprobes. Spordatabaser er offentliggjort i Ingero open-source-repositoriet til uafhængig verificering.

TL;DR

PyTorch’s DataLoader kan være 50-124 gange langsommere end direkte tensor-indexering for in-memory GPU-arbejdslaster. Vi genskabte et rigtigt PyTorch-problem på en RTX 4090 og spored hver CUDA-API-opkald og Linux-kernel-hændelse for at finde rodårsagen. GPU’en var ikke langsom – den sultede. DataLoader-arbejdere genererede 200.000 CPU-kontekstskift og 300.000 sideallokeringer i 40 sekunder, hvilket fik GPU’en til at vente i gennemsnit 301 ms pr. dataoverførsel, der burde tage mikrosekunder.

Problemet

En PyTorch-bruger rapporterede, at DataLoader var 7-22 gange langsommere end direkte tensor-indexering for en simpel MLP-inferensarbejdslast. Selv med num_workers=12, pin_memory=True og prefetch_factor=12, forblev gapet massivt. GPU-udnyttelse lå på 10-20%.

Vi genskabte det. Gapet var endda værre på vores hardware:

Metode Tid vs Direkte
Direkte tensor-indexering 0,39 s 1x
DataLoader (shuffle=True) 48,49 s 124 gange langsommere
DataLoader (optimeret, 4 arbejdere, pin_memory) 43,29 s 111 gange langsommere

Arbejdslasten er trivial: 7M eksempler, 100 funktioner, 2-lagdelt MLP, batch-størrelse 1M. Modellen behandler en batch på få millisekunder. Så hvor går tiden hen?

Hvad nvidia-smi viser

Ingenting nyttigt. GPU-udnyttelse flimrer mellem 0% og 30%. Hukommelsesbrug er stabil. Temperatur er fin. GPU’en er tydelig underudnyttet, men nvidia-smi kan ikke fortælle, hvorfor.

Hvad torch.profiler viser

Rapporteren prøvede PyTorch’s indbyggede profiler og “fik ingen meningsfulde sporfiler.” Dette er en almindelig frustration – applikationsniveau-profilere kan vise, hvilke CUDA-kerner der kører, men de kan ikke se host-siden scheduling, hukommelse og proces-livscyklus-hændelser, der bestemmer, om data ankommer til GPU’en på tid.

Hvad kernel-niveau-sporing viser

Vi kørte benchmarket, mens vi spored både CUDA-API-opkald (via eBPF uprobes på libcudart.so) og Linux-kernel-hændelser (scheduler-kontekstskift, hukommelsesside-allokeringer, proces-fork) samtidigt. Resultaterne fortæller den komplette historie.

Full video-gennemgang: https://asciinema.org/a/RGwhPeXAPJdhXqxp

I videoen forbinder vi en åben-vægt LLM (MiniMax-M2.7) til spordatabasen via MCP (Model Context Protocol):

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

JSON-konfigurationen fortæller MCP-klienten, hvor den kan finde Ingero-serveren og hvilken spordatabase, der skal indlæses:

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

Dette giver LLM direkte adgang til sporfilerne gennem 7 værktøjer: get_trace_stats, get_causal_chains, get_per_process_breakdown og andre. AI’en kan forespørge databasen, korrelerer CUDA-hændelser med kernel-scheduling-data og producerer en plain-language-diagnose uden nogen manuel analyse.

4 HIGH-severity årsagskæder

Årsagskæde-motoren detekterede 4 high-severity-mønstre, alle med samme rodårsag:

[HIGH] cudaStreamSync p99=42ms (1,638x p50=25us) - CPU 100% + 1,880 sched_switch-hændelser
Tidslinje:
[SYSTEM] CPU 100%
[HOST ] 1,880 kontekstskift (21s off-CPU)
[CUDA ] p99=42ms (1,638x p50=25us)
Rodårsag: DataLoader-arbejdere kæmper om CPU, massiv sideallokeringstryk
[HIGH] cudaLaunchKernel p99=24.67ms (349x p50=70us) - CPU 100%
Rod: 34 sched_switch-hændelser

[HIGH] cuMemAlloc p99=627us (4.0x p50) - CPU 100%
[HIGH] cuLaunchKernel p99=106us (4.0x p50) - CPU 100%

cudaStreamSync p99 er 1.638 gange p50. Det er ikke GPU-langsomhed – det er GPU’en, der venter på data, der aldrig ankommer på tid.

Figur 1: AI-genereret analyse efter kørsel af /investigate. Modellen brugte Ingero’s 7 MCP-værktøjer til at forespørge spordatabasen og producerede en plain-language-forklaring med handlelige anbefalinger, der er direkte afledt fra sporfilerne.

Den pr. proces-breakdown

Dette er, hvor det bliver klart. Hovedprocessen og dens 4 DataLoader-arbejdere er synlige som separate enheder:
Hovedprocessen:

- cudaMemcpyAsync (host-to-device-overførsel): gennemsnit 301ms, maks 2,9 sekunder
- cudaStreamSync: p99 = 42ms (normalt 25us)
- 1,567 kontekstskift, gennemsnit 16ms off-CPU, værst stall 5 sekunder
- 799,018 sideallokeringer
DataLoader-arbejder 1: 52,863 kontekstskift, 89,338 sideallokeringer, værst stall 5s
DataLoader-arbejder 2: 50,638 kontekstskift, 83,509 sideallokeringer, værst stall 5s
DataLoader-arbejder 3: 49,361 kontekstskift, 70,035 sideallokeringer, værst stall 5s
DataLoader-arbejder 4: 38,862 kontekstskift, 56,354 sideallokeringer, værst stall 5s

I alt over arbejdere: ~191.000 kontekstskift og ~299.000 sideallokeringer i 40 sekunder.

Hvad dette betyder

DataLoader-arbejderne gør tre dyre ting, som direkte indexering undgår helt:

  1. Shuffling og indexering: DataLoader med shuffle=True genererer en tilfældig permutation af indeks, derefter vælger hver arbejder sin del. Dette kræver tilfældig hukommelsesadgang på tværs af den fulde 7M-eksemplar-tensor – frygteligt for cache-lokalitet og udløser sidefejl.
  2. Collation og kopiering: Hver arbejder samler spredte eksempler i en kontinuert batch-tensor. Dette betyder at allokerer ny hukommelse (sideallokeringer), kopiere data fra tilfældige placeringer (cache-miss), og serialisere resultatet tilbage til hovedprocessen via delt hukommelse eller en kø.
  3. Konkurrerende om CPU: Fire arbejdere + hovedprocessen på en 4-vCPU-maskine betyder konstant preempt. Hver arbejder bliver afbrudt 50.000 gange. Den værste stall er 5 sekunder – under hvilken GPU’en intet har at proces.

Med direkte indexering: X[i:i+batch_size] er en zero-copy-visning af en kontinuert tensor, der allerede er i hukommelse. .to(device) udløser en enkelt DMA-overførsel fra en enkelt kontinuert region. Ingen arbejdere, ingen shuffling, ingen collation, ingen cross-process-kopier, ingen kontekstskift. GPU’en får data på mikrosekunder, ikke hundredvis af millisekunder.

Løsningen

Til in-memory GPU-arbejdslaster, hvor hele datasettet passer i RAM:

  1. Brug ikke DataLoader. Direkte indexering med en forhåndsshufflet indeksarray er simpelt og 100 gange hurtigere:
    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. Hvis du må bruge DataLoader, match num_workers til dine faktiske CPU-kerner minus 1. På en 4-kernet maskine, num_workers=2 reducerer konkurrence. Tilføj persistent_workers=True for at undgå fork-overhead.
  3. Til større-end-hukommelse-datasæt hvor DataLoader er nødvendig, skifter den virkelige flaskehals til disk-I/O. Brug prefetch_factor=2 (ikke højere – mere prefetching betyder mere hukommelsespres) og sikr, at din lagring kan følge med.

Det større billede

Denne undersøgelse illustrerer en mønster, vi ser konstant i GPU-arbejdslaster: GPU’en er hurtig, hosten er flaskehalsen, og GPU-målinger kan ikke se det. nvidia-smi rapporterede lav udnyttelse, men kunne ikke forklare, hvorfor. torch.profiler fanget CUDA-kerner, men missede de 200.000 kontekstskift, der skete i brugerens rum.

Den eneste måde at se det fulde billede var at spore både sider samtidigt – CUDA-API-opkald på biblioteks-niveau og Linux-kernel-scheduling-hændelser – og korrelerer dem efter tid og proces-ID. Årsagskæden “CPU 100% -> 1,880 sched_switch -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” fortæller den komplette historie på en linje. Uden cross-stack-sporing ville dette være blevet en mysterium – som det var for den oprindelige rapport, der tilbragte uger med at fejlfinde det.

Figur 2: Når man beder om “hvad er det centrale problem?”, identificerer modellen CPU-over-subscription, der forårsager host-side-scheduling-forsinkelser. cudaLaunchKernel gik fra 73us til 25,8ms (356 gange langsommere), fordi CPU’en ikke kunne planlægge lanceringen på tid.

Prøv det selv

Reproducer benchmarket:

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

# Hurtig sti 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'Direkte: {time.time()-start:.3f}s')

# Langsom sti 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')

Spor med Ingero for at se, hvad der sker under kørsel:

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 anden terminal
./bin/ingero explain --since 60s # efter benchmark er færdig

Eller spring reproduction over og udforsk vores spordata direkte. Undersøgelsesdatabasen (764KB) er i repositoriet:

# Vis årsagskæder fra undersøgelsen
./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m

# Pr. proces-breakdown (se DataLoader-arbejdere vs hovedprocessen) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m

# Forbind din AI-assistent til interaktiv undersøgelse ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db

Undersøg med AI (anbefales). Den hurtigste måde at analysere sporfilerne er at forbinde enhver MCP-kompatibel AI direkte til databasen. Ingen manuel analyse nødvendig.

Opret en konfigurationsfil:

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

Så forbind din model:

# Med Ollama + MiniMax (hvad vi brugte i videoen)
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json

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

# Med enhver MCP-kompatibel klient # Tilføj konfigurationen ovenfor til din AI's MCP-indstillinger

Skriv /investigate for at udløse den guidede analyse eller still et spørgsmål: “Hvad forårsagede GPU-sulten?” AI’en har adgang til 7 værktøjer, der forespørger spordatabasen direkte.

GitHub: github.com/ingero-io/ingero
Original issue: pytorch/pytorch#154318
Video-gennemgang: https://asciinema.org/a/RGwhPeXAPJdhXqxp

Undersøgelse udført på TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.

David Mail er medforfatter og vedligeholder af Ingero, en open-source eBPF-agent til CUDA-niveau GPU-overvågning. Han specialiserer sig i sporing på kerneniveau af AI-arbejdsbelastninger i produktion.