Raportit

124 kertaa hitaampi: Mitä PyTorch DataLoader todella tekee ytimen tasolla

mm
Lisää Unite.AI suosikkilähteisiisi Google-palvelussa
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.

Tämä artikkeli perustuu havaintoihin, jotka on tehty ytimen tasolla suoritetusta GPU-jäljityksestä, joka suoritettiin oikealla PyTorch-ongelmalla (#154318) käyttäen eBPF uprobesia. Jäljitys-tietokannat on julkaistu Ingero-avoin lähdekoodirepositoriossa itsenäisen vahvistamisen vuoksi.

TL;DR

PyTorchin DataLoader voi olla 50-124 kertaa hitaampi kuin suora tensorin indeksointi muistin sisäisille GPU-työkuormille. Toistimme oikean PyTorch-ongelman RTX 4090:lla ja jäljittäimme jokaisen CUDA API-kutsun ja Linux-ytimen tapahtuman, jotta löydettäisiin ongelman syy. GPU ei ollut hitaampi – se oli nälkäinen. DataLoader-työntekijät loivat 200 000 CPU-kontekstivaihtoa ja 300 000 sivuvarauksen 40 sekunnissa, jättäen GPU:n odottamaan keskimäärin 301 ms kunkin datansiirron kohdalla, joka pitäisi kestää mikrosekunteja.

Ongelma

PyTorch-käyttäjä ilmoitti, että DataLoader oli 7-22 kertaa hitaampi kuin suora tensorin indeksointi yksinkertaiselle MLP-inferenssityökuormalle. Vaikka num_workers=12, pin_memory=True ja prefetch_factor=12, ero oli edelleen valtava. GPU-käyttöaste oli 10-20%.

Toistimme sen. Ero oli vielä pahempi laitteistollamme:

Menetelmä Aika Suhteessa suoraan
Suora tensorin indeksointi 0,39 s 1x
DataLoader (shuffle=True) 48,49 s 124 kertaa hitaampi
DataLoader (optimoituna, 4 työntekijää, pin_memory) 43,29 s 111 kertaa hitaampi

Työkuorma on triviaali: 7M näytettä, 100 piirrettä, 2-kerroksinen MLP, eräkoko 1M. Malli prosessoi erän millisekunteissa. Missä siis menee aika?

Mitä nvidia-smi näyttää

Mitään hyödyllistä. GPU-käyttöaste vaihtelee 0% ja 30% välillä. Muistin käyttö on vakaa. Lämpötila on ok. GPU on selvästi alikäytetty, mutta nvidia-smi ei voi kertoa, miksi.

Mitä torch.profiler näyttää

Raportoija yritti PyTorchin sisäistä profiloijaa ja “hankki ei-merkityksellistä jäljitystietoa.” Tämä on yleinen frustraatio – sovelluksen tasolla olevat profiloijat voivat näyttää, mitkä CUDA-ytimet suoritetaan, mutta ne eivät voi nähdä isäntäpuolen aikataulutusta, muistia ja prosessin elinkaareen liittyviä tapahtumia, jotka määräävät, saapuuko data GPU:lle ajoissa.

Mitä ytimen tasolla oleva jäljitys näyttää

Suoritimme benchmarkin jäljitsemällä sekä CUDA API-kutsuja (eBPF uprobesilla libcudart.so:sta) että Linux-ytimen tapahtumia (aikataulutuskontekstivaihdot, muistisivujen varaus, prosessin forkkaus) samanaikaisesti. Tulokset kertovat koko tarinan.

Täydellinen video-esittely: https://asciinema.org/a/RGwhPeXAPJdhXqxp

Videossa liitimme avoimen painopisteen LLM (MiniMax-M2.7):n jäljitystietokantaan MCP (Model Context Protocol) kautta:

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

JSON-konfiguraatio kertoo MCP-asiakkaalle, missä Ingero-palvelin ja jäljitystietokanta sijaitsevat:

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

Tämä antaa LLM:lle suoran pääsyn jäljitystietoihin 7 työkalun kautta: get_trace_stats, get_causal_chains, get_per_process_breakdown jne. AI voi kysyä tietokannasta, korreloida CUDA-tapahtumia ytimen aikataulutustiedon kanssa ja tuottaa selkeän kielen diagnosin ilman manuaalista analyysiä.

4 korkean tason kausaalisia ketjuja

Kausaaliset ketjutunnistin havaittiin 4 korkean tason mallia, kaikki samalla juurisyyllä:

[KORKEA] cudaStreamSync p99=42ms (1,638x p50=25us) - CPU 100% + 1,880 sched_switch -tapahtumaa
Aikajana:
[SYSTEM] CPU 100%
[HOST ] 1,880 kontekstivaihtoa (21s pois-CPU:sta)
[CUDA ] p99=42ms (1,638x p50=25us)
Juurisyy: DataLoader-työntekijät taistelevat CPU:sta, massiivinen sivuvarauspaine
[KORKEA] cudaLaunchKernel p99=24.67ms (349x p50=70us) - CPU 100%
Juurisyy: 34 sched_switch -tapahtumaa

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

cudaStreamSync p99 on 1,638 kertaa p50. Se ei ole GPU:n hitaus – se on GPU:n odottaminen dataa, joka ei saavu ajoissa.

Kuva 1: AI-generoitu analyysi suorittamisen jälkeen /investigate. Malli käytti Ingeron 7 MCP-työkalua kysymään jäljitystietokannasta ja tuotti selkeän kielen selityksen toimintavinkkejä, jotka perustuvat suoraan jäljitystietoihin.

Prosessikohtainen jakautuminen

Tässä se selviää. Pääprosessi ja sen 4 DataLoader-työntekijää ovat näkyvissä eri entiteetteinä:

Pääprosessi:

- cudaMemcpyAsync (isäntä-laite -siirto): keskimäärin 301ms, maksimi 2,9 sekuntia
- cudaStreamSync: p99 = 42ms (normaalisti 25us)
- 1,567 kontekstivaihtoa, keskimäärin 16ms pois-CPU:sta, pahin tukkeutuminen 5 sekuntia
- 799,018 sivuvarauksia
DataLoader-työntekijä 1: 52,863 kontekstivaihtoa, 89,338 sivuvarauksia, pahin tukkeutuminen 5s
DataLoader-työntekijä 2: 50,638 kontekstivaihtoa, 83,509 sivuvarauksia, pahin tukkeutuminen 5s
DataLoader-työntekijä 3: 49,361 kontekstivaihtoa, 70,035 sivuvarauksia, pahin tukkeutuminen 5s
DataLoader-työntekijä 4: 38,862 kontekstivaihtoa, 56,354 sivuvarauksia, pahin tukkeutuminen 5s

Yhteensä työntekijöiden kesken: noin 191,000 kontekstivaihtoa ja noin 299,000 sivuvarauksia 40 sekunnissa.

Mitä se tarkoittaa

DataLoader-työntekijät tekevät kolme kallista asiaa, joita suora indeksointi välttää kokonaan:

  1. Shuffling ja indeksointi: DataLoader shuffle=True luo satunnaisen indeksien peruuttamisen, ja jokainen työntekijä valitsee oman osuutensa. Tämä vaatii satunnaisen muistin pääsyn koko 7M-näytteisen tensoriin – huonoa välimuistin paikallisuutta ja laukaisee sivuvirheitä.
  2. Collation ja kopioiminen: Jokainen työntekijä kerää hajallaan olevat näytteet yhtenäiseen erätensoriin. Tämä tarkoittaa uuden muistin varaus (sivuvarauksia), datan kopioimista satunnaisista sijainneista (välimuistin hajotus) ja serialisointia takaisin pääprosessiin jaettujen muistojen tai jonon kautta.
  3. Kilpailu CPU:sta: Neljä työntekijää + pääprosessi 4-vCPU-koneessa tarkoittaa jatkuvaan esiajoitukseen. Jokainen työntekijä poistetaan 50,000 kertaa. Pahin tukkeutuminen on 5 sekuntia – jonka aikana GPU:lla ei ole mitään prosessoida.

Suoralla indeksoinnilla: X[i:i+batch_size] on nollakopio yhtenäisestä tensorista, joka on jo muistissa. .to(device) laukaisee yhden DMA-siirron yhdestä yhtenäisestä alueesta. Ei työntekijöitä, ei shufflia, ei collationia, ei ristiinprosessin kopioita, ei kontekstivaihtoja. GPU saa datan mikrosekunteissa, ei satojen millisekuntien aikana.

Korjaus

Muistin sisäisille GPU-työkuormille, joissa koko tietokanta mahtuu muistiin:

  1. Älä käytä DataLoaderia. Suora indeksointi esikääritetyllä indeksitaulukolla on yksinkertaisempaa ja 100 kertaa nopeampaa:
    indeksit = torch.randperm(num_samples)
    for i in range(0, num_samples, batch_size):
    batch = X[indeksit[i:i+batch_size]].to(device)
    output = model(batch)
    
  2. Jos sinun on käytettävä DataLoaderia, vastaa num_workers -arvoa oikeasti olemassa olevien CPU-ytimien määrällä. Neljän ytimen koneessa num_workers=2 vähentää kilpailua. Lisää persistent_workers=True välttääksesi forkkauskustannukset.
  3. Isommille kuin muistiin mahtuville tietokannoille joissa DataLoader on välttämätön, todellinen pullonkaula siirtyy levy-I/O:lle. Käytä prefetch_factor=2 (älä korkeampaa – enemmän esihaku tarkoittaa enemmän muistipainetta) ja varmista, että tallennusvälineesi pystyy pitämään vauhtia.

Laajempi kuva

Tämä tutkimus havainnollistaa mallin, jota näemme jatkuvasti GPU-työkuormissa: GPU on nopea, isäntä on pullonkaula, ja GPU-mittaukset eivät voi nähdä sitä. nvidia-smi raportoi matalan käyttöasteen, mutta ei voinut selittää, miksi. torch.profiler tallensi CUDA-ytimiä, mutta se ei nähnyt 200,000 kontekstivaihtoa, jotka tapahtuivat käyttäjätilassa.

Tapa, jolla nähdä koko kuva, oli jäljitää sekä CUDA API-kutsuja että Linux-ytimen aikataulutustapahtumia samanaikaisesti ja korreloida niitä ajan ja prosessin ID:n perusteella. Kausaalinen ketju “CPU 100% -> 1,880 sched_switch -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” kertoo koko tarinan yhdellä rivillä. Ilman ristirakenteisen jäljitystä tämä olisi jäänyt arvoitukseksi – kuten se oli alun perin ilmoittajalle, joka vietti viikkoja debuggaamassa sitä.

Kuva 2: Kun kysyttiin “mitä on keskeinen ongelma?”, malli tunnistaa CPU-yliedustuksen aiheuttamat isäntäpuolen aikataulutusviiveet. cudaLaunchKernel siirtyi 73us:sta 25,8ms:iin (356 kertaa hitaampi) koska CPU ei voinut ajoittaa laukaisua ajoissa.

Kokeile itse

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

# Nopea polku
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'Suora: {time.time()-start:.3f}s')

# Hidas polku
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')

Jäljitä Ingerolla, jotta näet, mitä tapahtuu alla:

git clone https://github.com/ingero-io/ingero.git
cd ingero && make build
sudo ./bin/ingero trace --duration 60s # toisessa terminaalissa
python3 benchmark.py # toisessa terminaalissa
./bin/ingero explain --since 60s # benchmarkin jälkeen

Tai ohita toisto ja tutki suoraan meidän jäljitystietokantamme:

# Näytä kausaaliset ketjut tutkimuksesta
./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m

# Prosessikohtainen jakautuminen (näe DataLoader-työntekijät vs pääprosessi)
./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m

# Liitä AI-avustajasi interaktiiviseen tutkimukseen
./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db

Tutki AI:n avulla (suositellaan). Nopein tapa analysoida jäljitys on liittää MCP-yhteensopiva AI suoraan tietokantaan. Ei manuaalista analyysiä tarvita.

Luo konfiguraatiotiedosto:

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

Sitten liitä mallisi:

# Ollamalla + MiniMax (mitä käytimme videossa)
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json

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

# Millä tahansa MCP-yhteensopivalla asiakkaalla
# Lisää konfiguraatio yllä AI:n MCP-asetuksiin

Kirjoita /investigate laukaiseksesi ohjatun analyysin, tai kysy mitä tahansa: “Mikä aiheutti GPU:n nälän?” AI:lla on pääsy 7 työkaluun, jotka kysyvät jäljitystietokannasta suoraan.

GitHub: github.com/ingero-io/ingero
Alkuperäinen ongelma: pytorch/pytorch#154318
Video-esittely: https://asciinema.org/a/RGwhPeXAPJdhXqxp

Tutkimus suoritettiin TensorDock RTX 4090:lla (24GB), Ubuntu 22.04:llä, PyTorch 2.10.0+cu128:lla.

David Mail on Ingeron ja avoimen lähdekoodin eBPF-agentin ylläpitäjä, joka tarjoaa CUDA-tasoa olevan GPU-havainnollistamisen. Hän on erikoistunut tuotannon AI-työkuormien ydinlevyn jäljitykseen.