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.jsonPlik 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:
- 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.
- 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.
- 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:
- 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)
- 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.
- 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.













