Raporty

124 razy Wolniej: Co PyTorch DataLoader Robi na Poziomie Jądra

mm
Dodaj Unite.AI do preferowanych ÅšrÃģdeł w 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.

Ten artykuł oparty jest na wynikach śledzenia na poziomie jądra GPU przeprowadzonego na rzeczywistym problemie PyTorch (#154318) z uÅžyciem eBPF uprobes. Bazy danych śledzenia są publikowane w repozytorium open-source Ingero do niezaleÅžnej weryfikacji.

TL;DR

DataLoader PyTorch moÅže być 50-124 razy wolniejszy niÅž bezpośrednie indeksowanie tensorÃģw dla obciÄ…Åžeń pamięci GPU. Odtworzyliśmy rzeczywisty problem PyTorch na RTX 4090 i śledziliśmy kaÅžde wywołanie interfejsu API CUDA i zdarzenia jądra Linux, aby znaleŚć przyczynę. GPU nie był wolny – głodował. Pracownicy DataLoader wygenerowali 200 000 przełączeń kontekstu CPU i 300 000 alokacji stron w 40 sekund, pozostawiając GPU w oczekiwaniu średnio 301 ms na transfer danych, ktÃģry powinien trwać mikrosekundy.

Problem

UÅžytkownik PyTorch zgłosił, Åže DataLoader jest 7-22 razy wolniejszy niÅž bezpośrednie indeksowanie tensorÃģw dla prostego obciÄ…Åženia inferencyjnego MLP. Nawet z num_workers=12, pin_memory=True i prefetch_factor=12, rÃģÅžnica była nadal ogromna. Wykorzystanie GPU wynosiło 10-20%.

Odtworzyliśmy to. RÃģÅžnica była jeszcze gorsza na naszym sprzęcie:

Metoda Czas vs Bezpośrednie
Bezpośrednie indeksowanie tensorÃģw 0,39 s 1x
DataLoader (shuffle=True) 48,49 s 124 razy wolniejszy
DataLoader (zoptymalizowany, 4 pracownicy, pin_memory) 43,29 s 111 razy wolniejszy

ObciÄ…Åženie jest trywialne: 7M prÃģbek, 100 cech, 2-warstwowy MLP, rozmiar partii 1M. Model przetwarza partię w milisekundach. Gdzie więc idzie czas?

Co nvidia-smi Pokazuje

Nic przydatnego. Wykorzystanie GPU migocze między 0% a 30%. UÅžycie pamięci jest stabilne. Temperatura jest w porządku. GPU jest wyraÅšnie niedoÅžywiony, ale nvidia-smi nie moÅže powiedzieć, dlaczego.

Co torch.profiler Pokazuje

Raportujący sprÃģbował wbudowanego profilera PyTorch i “nie uzyskał Åžadnych przydatnych danych śledzenia”. Jest to powszechna frustracja – profilery na poziomie aplikacji mogą pokazać, ktÃģre jądra CUDA są uruchomione, ale nie mogą zobaczyć host-side scheduling, zdarzeń pamięci i procesu cyklu Åžycia, ktÃģre determinują, czy dane docierają do GPU na czas.

Co Śledzenie na Poziomie Jądra Pokazuje

Uruchomiliśmy benchmark, śledząc jednocześnie wywołania interfejsu API CUDA (za pomocą eBPF uprobes na libcudart.so) i zdarzenia jądra Linux (przełączenia kontekstu planisty, alokacje stron pamięci, fork procesÃģw). Wyniki opowiadają całą historię.

Pełne wideo: https://asciinema.org/a/RGwhPeXAPJdhXqxp

W filmie połączyliśmy otwarty model LLM (MiniMax-M2.7) z bazą danych śledzenia za pomocą MCP (Model Context Protocol):

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

Plik konfiguracyjny JSON informuje klienta MCP, gdzie znaleŚć serwer Ingero i jaką bazę danych śledzenia załadować:

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

To daje LLM bezpośredni dostęp do danych śledzenia za pomocą 7 narzędzi: get_trace_stats, get_causal_chains, get_per_process_breakdown i innych. AI moÅže wysyłać zapytania do bazy danych, korelować zdarzenia CUDA z danymi planowania jądra i generować wyjaśnienie w języku naturalnym bez Åžadnej ręcznej analizy.

4 Łańcuchy Przyczynowe o Wysokim Poziomie ZagroÅženia

Silnik łańcuchÃģw przyczynowych wykrył 4 wzorce o wysokim poziomie zagroÅženia, wszystkie z tą samą przyczyną:

[WYSOKI] cudaStreamSync p99=42ms (1,638x p50=25us) - CPU 100% + 1,880 przełączeń kontekstu
Czas:
[SYSTEM] CPU 100%
[HOST] 1,880 przełączeń kontekstu (21s poza CPU)
[CUDA] p99=42ms (1,638x p50=25us)
Przyczyna: Pracownicy DataLoader walczą o CPU, ogromne ciśnienie alokacji stron
[WYSOKI] cudaLaunchKernel p99=24.67ms (349x p50=70us) - CPU 100%
Korzeń: 34 przełączenia kontekstu

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

cudaStreamSync p99 jest 1,638 razy większy niÅž p50. To nie jest powolność GPU – to GPU czekające na dane, ktÃģre nigdy nie przychodzą na czas.

Rysunek 1: Analiza wygenerowana przez AI po uruchomieniu /investigate. Model uÅžył 7 narzędzi MCP Ingero do wysłania zapytań do bazy danych śledzenia i wygenerował wyjaśnienie w języku naturalnym z zaleceniami pochodzącymi bezpośrednio z danych śledzenia.

Rozbicie na Poziomie Procesu

To staje się jasne. GłÃģwny proces i jego 4 pracownicy DataLoader są widoczne jako oddzielne jednostki:
GłÃģwny proces:

- cudaMemcpyAsync (transfer z hosta do urządzenia): średnio 301ms, maksymalnie 2,9 sekundy
- cudaStreamSync: p99 = 42ms (zwykle 25us)
- 1,567 przełączeń kontekstu, średnio 16ms poza CPU, najgorsza zwłoka 5 sekund
- 799,018 alokacji stron
Pracownik DataLoader 1: 52,863 przełączenia kontekstu, 89,338 alokacji stron, najgorsza zwłoka 5s
Pracownik DataLoader 2: 50,638 przełączeń kontekstu, 83,509 alokacji stron, najgorsza zwłoka 5s
Pracownik DataLoader 3: 49,361 przełączeń kontekstu, 70,035 alokacji stron, najgorsza zwłoka 5s
Pracownik DataLoader 4: 38,862 przełączenia kontekstu, 56,354 alokacji stron, najgorsza zwłoka 5s

Łącznie na pracownikÃģw: ~191,000 przełączeń kontekstu i ~299,000 alokacji stron w 40 sekund.

Co to Znaczy

Pracownicy DataLoader robią trzy drogie rzeczy, ktÃģrych unika bezpośrednie indeksowanie:

  1. Mieszanie i indeksowanie: DataLoader z shuffle=True generuje losową permutację indeksÃģw, a następnie kaÅždy pracownik wybiera swÃģj kawałek. To wymaga losowego dostępu do pamięci w całym tensory 7M-prÃģbek – straszne dla lokalności pamięci i wyzwala błędy stron.
  2. Kolekcja i kopiowanie: KaÅždy pracownik gromadzi rozproszone prÃģbki w ciągły tensor partii. To oznacza alokację nowej pamięci (alokacje stron), kopiowanie danych z losowych lokalizacji (błędy cache) i serializację wyniku z powrotem do procesu głÃģwnego za pomocą pamięci wspÃģłdzielonej lub kolejki.
  3. WspÃģłzawodnictwo o CPU: Czterech pracownikÃģw + proces głÃģwny na maszynie 4-vCPU oznacza stałe przerwanie. KaÅždy pracownik jest odłączany 50,000 razy. Najgorsza zwłoka wynosi 5 sekund – podczas ktÃģrej GPU nie ma nic do przetworzenia.

Z bezpośrednim indeksowaniem: X[i:i+rozmiar_partii] jest widokiem zero-kopiowym ciągłego tensora juÅž w pamięci. .to(device) wyzwala jeden transfer DMA z jednej ciągłej części. Åŧadnych pracownikÃģw, Åžadnego mieszania, Åžadnej kolekcji, Åžadnych kopii między procesami, Åžadnych przełączeń kontekstu. GPU otrzymuje dane w mikrosekundach, a nie setkach milisekund.

Naprawa

Dla obciÄ…Åžeń pamięci GPU, gdzie cały zestaw danych mieści się w pamięci RAM:

  1. Nie uÅžywaj DataLoader. Bezpośrednie indeksowanie z pre-mieszaniem tablicy indeksÃģw jest prostsze i 100x szybsze:
    indeksy = torch.randperm(num_samples)
    dla i w zakresie(0, num_samples, batch_size):
    partia = X[indeksy[i:i+batch_size]].to(device)
    wynik = model(partia)
    
  2. Jeśli musisz uÅžywać DataLoader, dopasuj num_workers do Twoich rzeczywistych rdzeni CPU minus 1. Na maszynie 4-rdzeniowej num_workers=2 zmniejsza konflikty. Dodaj persistent_workers=True, aby uniknąć nakładu pracy fork.
  3. Dla zestawÃģw danych większych niÅž pamięć gdzie DataLoader jest konieczny, prawdziwa wąska garść przechodzi do wejścia/wyjścia dysku. UÅžyj prefetch_factor=2 (nie więcej – więcej prefetchingu oznacza więcej ciśnienia pamięci) i upewnij się, Åže Twoje przechowywanie moÅže dotrzymać tempa.

Szerszy Kontekst

To śledzenie ilustruje wzorzec, ktÃģry widzimy ciągle w obciÄ…Åženiach GPU: GPU jest szybkie, host jest wąską garścią, a metryki GPU nie mogą tego zobaczyć. nvidia-smi zgłaszał niskie wykorzystanie, ale nie mÃģgł powiedzieć, dlaczego. torch.profiler przechwycił jądra CUDA, ale przegapił 200,000 przełączeń kontekstu występujących w przestrzeni uÅžytkownika.

Jedynym sposobem, aby zobaczyć pełny obraz, było śledzenie obu stron jednocześnie – wywołań interfejsu API CUDA na poziomie biblioteki i zdarzeń jądra Linux – i skorelowanie ich w czasie i według ID procesu. Łańcuch przyczynowy “CPU 100% -> 1,880 przełączeń kontekstu -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” opowiada całą historię w jednej linii. Bez śledzenia cross-stack tego pozostałoby tajemnicą – tak jak dla oryginalnego raportującego, ktÃģry spędził tygodnie debugując to.

Rysunek 2: Kiedy zapytano “jaka jest podstawowa przyczyna?”, model identyfikuje nadmiar CPU powodujący opÃģÅšnienia w host-side scheduling. cudaLaunchKernel przeszedł od 73us do 25.8ms (356x wolniejszy) dlatego, Åže CPU nie mÃģgł zaplanować uruchomienia na czas.

SprÃģbuj Sam

OdtwÃģrz 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()

# Szybka ścieÅžka
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'Bezpośrednie: {time.time()-start:.3f}s')

# Wolna ścieÅžka
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')

ŚledÅš z Ingero, aby zobaczyć, co dzieje się pod spodem:

git clone https://github.com/ingero-io/ingero.git
cd ingero && make build
sudo ./bin/ingero trace --duration 60s # w jednej konsoli
python3 benchmark.py # w innej konsoli
./bin/ingero explain --since 60s # po zakończeniu benchmarku

Lub pomiń odtworzenie i przejdÅš bezpośrednio do naszych danych śledzenia:

# Wyświetl łańcuchy przyczynowe z śledzenia
./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m

# Rozbicie na poziomie procesu (zobacz pracownikÃģw DataLoader vs proces głÃģwny)
./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m

# Połącz swÃģj asystent AI dla interaktywnego śledzenia
./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db

ŚledÅš z AI (zalecane). Najszybszym sposobem analizy śledzenia jest połączenie dowolnego kompatybilnego AI z bazą danych bezpośrednio. Nie jest wymagana ręczna analiza.

UtwÃģrz plik konfiguracyjny:

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

Następnie połącz swÃģj model:

# Z Ollama + MiniMax (co uÅžyliśmy w filmie)
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json

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

# Z dowolnym kompatybilnym klientem
# Dodaj powyÅžszą konfigurację do ustawień MCP Twojego AI

Wpisz /investigate, aby uruchomić przewodzoną analizę lub zapytaj o coś: “Co spowodowało głodowanie GPU?” AI ma dostęp do 7 narzędzi, ktÃģre wysyłają zapytania do bazy danych śledzenia bezpośrednio.

GitHub: github.com/ingero-io/ingero
Oryginalny problem: pytorch/pytorch#154318
Przewodnik wideo: https://asciinema.org/a/RGwhPeXAPJdhXqxp

Śledzenie wykonane na TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.

David Mail jest wspÃģłautorem i utrzymuje Ingero, agenta open-source eBPF dla obserwowalności GPU na poziomie CUDA. Specjalizuje się w śledzeniu jądra produkcji AI.