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.jsonJSON-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:
- 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ä.
- 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.
- 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:
- Ä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)
- 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.
- 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.













