Berichte

124x Langsamer: Was PyTorch DataLoader tatsÃĪchlich auf Kernel-Ebene macht

mm
Unite.AI zu deinen bevorzugten Quellen auf Google hinzufÞgen
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.

Dieser Artikel basiert auf den Ergebnissen einer kernelbasierten GPU-Spurinvestigation, die an einem realen PyTorch-Issue (#154318) mit eBPF-Uprobes durchgefÞhrt wurde. Die Spurdatenbanken werden im Ingero-Open-Source-Repository fÞr unabhÃĪngige ÜberprÞfung verÃķffentlicht.

TL;DR

PyTorchs DataLoader kann bis zu 50-124-mal langsamer sein als direktes Tensor-Indexing fÞr GPU-Arbeitslasten im Speicher. Wir reproduzierten ein reales PyTorch-Issue auf einem RTX 4090 und verfolgten jeden CUDA-API-Aufruf und jedes Linux-Kernel-Ereignis, um die Ursache zu finden. Die GPU war nicht langsam – sie hungerte. DataLoader-Worker erzeugten 200.000 CPU-Kontextwechsel und 300.000 Seitenzuweisungen in 40 Sekunden, was die GPU im Durchschnitt 301 ms pro DatenÞbertragung warten ließ, die normalerweise Mikrosekunden dauern sollte.

Das Problem

Ein PyTorch-Benutzer berichtete, dass DataLoader 7-22-mal langsamer war als direktes Tensor-Indexing fÞr eine einfache MLP-SchÃĪtzung. Selbst mit num_workers=12, pin_memory=True und prefetch_factor=12 blieb die LÞcke enorm. Die GPU-Auslastung lag bei 10-20%.

Wir reproduzierten es. Die LÞcke war auf unserem Hardware sogar noch schlimmer:

Methode Zeit vs Direkt
Direktes Tensor-Indexing 0,39 s 1x
DataLoader (shuffle=True) 48,49 s 124-mal langsamer
DataLoader (optimiert, 4 Worker, pin_memory) 43,29 s 111-mal langsamer

Die Arbeitslast ist trivial: 7 Mio. Proben, 100 Merkmale, 2-Schichten-MLP, Batch-GrÃķße 1 Mio. Das Modell verarbeitet einen Batch in Millisekunden. Wo geht also die Zeit hin?

Was nvidia-smi zeigt

Nichts NÞtzliches. Die GPU-Auslastung fluktuiert zwischen 0% und 30%. Der Speicherbedarf ist stabil. Die Temperatur ist in Ordnung. Die GPU ist offensichtlich unterausgelastet, aber nvidia-smi kann nicht erklÃĪren, warum.

Was torch.profiler zeigt

Der Berichterstatter versuchte PyTorchs integrierten Profiler und “erhielt keine nÞtzlichen Spurdaten”. Dies ist eine hÃĪufige Frustration – Anwendungsprofiler kÃķnnen zeigen, welche CUDA-Kernel ausgefÞhrt werden, aber sie kÃķnnen nicht die Host-Seite-Scheduling-, Speicher- und Prozesslebenszyklus-Ereignisse sehen, die bestimmen, ob Daten rechtzeitig bei der GPU ankommen.

Was kernelbasiertes Tracing zeigt

Wir fÞhrten den Benchmark durch, wÃĪhrend wir sowohl CUDA-API-Aufrufe (Þber eBPF-Uprobes auf libcudart.so) als auch Linux-Kernel-Ereignisse (Scheduler-Kontextwechsel, Speicherseitenzuweisungen, Prozessforks) gleichzeitig verfolgten. Die Ergebnisse erzÃĪhlen die ganze Geschichte.

VollstÃĪndige Video-Anleitung: https://asciinema.org/a/RGwhPeXAPJdhXqxp

In dem Video verbanden wir einen offenen LLM (MiniMax-M2.7) mit der Spurdatenbank Þber MCP (Model Context Protocol):

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

Die JSON-Konfiguration sagt dem MCP-Client, wo er den Ingero-Server und welche Spurdatenbank finden kann:

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

Dies gibt dem LLM direkten Zugriff auf die Spurdaten Þber 7 Tools: get_trace_stats, get_causal_chains, get_per_process_breakdown und andere. Der AI kann die Datenbank abfragen, CUDA-Ereignisse mit Kernel-Scheduling-Daten korrelieren und eine einfache Sprachdiagnose ohne manuelle Analyse erstellen.

4 HIGH-severity-Kausalketten

Der Kausalketten-Motor erkannte 4 High-SeveritÃĪts-Muster, alle mit der gleichen Ursache:

[HIGH] cudaStreamSync p99=42ms (1.638x p50=25us) - CPU 100% + 1.880 sched_switch-Ereignisse
Zeitplan:
[SYSTEM] CPU 100%
[HOST ] 1.880 Kontextwechsel (21s off-CPU)
[CUDA ] p99=42ms (1.638x p50=25us)
Ursache: DataLoader-Worker kÃĪmpfen um CPU, massiver Speicherdruck
[HIGH] cudaLaunchKernel p99=24,67ms (349x p50=70us) - CPU 100%
Wurzel: 34 sched_switch-Ereignisse

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

Die cudaStreamSync p99 ist 1.638-mal die p50. Das ist nicht GPU-Langsamkeit – das ist die GPU, die auf Daten wartet, die nie rechtzeitig ankommen.

Abbildung 1: AI-erzeugte Analyse nach dem AusfÞhren von /investigate. Das Modell verwendete Ingeros 7 MCP-Tools, um die Spurdatenbank abzufragen und eine einfache SpracherklÃĪrung mit handlungsfÃĪhigen Empfehlungen direkt aus den Spurdaten zu erstellen.

Die prozessbasierte AufschlÞsselung

Hier wird es klar. Der Hauptprozess und seine 4 DataLoader-Worker sind als separate EntitÃĪten sichtbar:
Hauptprozess:

- cudaMemcpyAsync (Host-zu-GerÃĪt-Übertragung): Durchschnitt 301ms, Max 2,9 Sekunden
- cudaStreamSync: p99 = 42ms (normalerweise 25us)
- 1.567 Kontextwechsel, Durchschnitt 16ms off-CPU, schlimmster Stau 5 Sekunden
- 799.018 Seitenzuweisungen
DataLoader-Worker 1: 52.863 Kontextwechsel, 89.338 Seitenzuweisungen, schlimmster Stau 5s
DataLoader-Worker 2: 50.638 Kontextwechsel, 83.509 Seitenzuweisungen, schlimmster Stau 5s
DataLoader-Worker 3: 49.361 Kontextwechsel, 70.035 Seitenzuweisungen, schlimmster Stau 5s
DataLoader-Worker 4: 38.862 Kontextwechsel, 56.354 Seitenzuweisungen, schlimmster Stau 5s

Insgesamt Þber die Worker: ~191.000 Kontextwechsel und ~299.000 Seitenzuweisungen in 40 Sekunden.

Was dies bedeutet

Die DataLoader-Worker tun drei teure Dinge, die direktes Indexing vollstÃĪndig vermeidet:

  1. Shuffling und Indexing: DataLoader mit shuffle=True generiert eine zufÃĪllige Permutation von Indizes, dann wÃĪhlt jeder Worker seinen Anteil. Dies erfordert zufÃĪlligen Speicherzugriff Þber den gesamten 7-Mio.-Proben-Tensor – schlecht fÞr Cache-LokalitÃĪt und lÃķst Seitenfehler aus.
  2. Kollation und Kopie: Jeder Worker sammelt verstreute Proben in einem kontinuierlichen Batch-Tensor. Dies bedeutet die Zuweisung neuer Speicher (Seitenzuweisungen), das Kopieren von Daten aus zufÃĪlligen Orten (Cache-Verfehlungen) und die Serialisierung des Ergebnisses zurÞck zum Hauptprozess Þber gemeinsamen Speicher oder eine Warteschlange.
  3. Wettbewerb um CPU: Vier Worker plus der Hauptprozess auf einem 4-vCPU-System bedeuten stÃĪndige Unterbrechung. Jeder Worker wird 50.000-mal unterbrochen. Der schlimmste Stau ist 5 Sekunden – wÃĪhrenddessen hat die GPU nichts zu verarbeiten.

Mit direktem Indexing: X[i:i+batch_size] ist eine Zero-Copy-Ansicht eines kontinuierlichen Tensors, der bereits im Speicher ist. .to(device) lÃķst einen einzigen DMA-Transfer von einer einzigen kontinuierlichen Region aus. Keine Worker, kein Shuffling, keine Kollation, keine Cross-Prozess-Kopien, keine Kontextwechsel. Die GPU erhÃĪlt Daten in Mikrosekunden, nicht in Hunderten von Millisekunden.

Die LÃķsung

FÞr GPU-Arbeitslasten im Speicher, bei denen die gesamte Datenmenge in den RAM passt:

  1. Verwenden Sie keinen DataLoader. Direktes Indexing mit einem vorhergehenden Index-Array ist einfacher und 100-mal schneller:
    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. Wenn Sie DataLoader verwenden mÞssen, passen Sie num_workers an Ihre tatsÃĪchlichen CPU-Kerne minus 1 an. Auf einem 4-Kern-System reduziert num_workers=2 den Wettbewerb. FÞgen Sie persistent_workers=True hinzu, um den Fork-Overhead zu vermeiden.
  3. FÞr grÃķßere Datenmengen als der Speicher, bei denen DataLoader notwendig ist, verschiebt sich die eigentliche Flaschenhals auf die Disk-I/O. Verwenden Sie prefetch_factor=2 (nicht hÃķher – mehr Prefetching bedeutet mehr Speicherdruck) und stellen Sie sicher, dass Ihr Speicher mithalten kann.

Das grÃķßere Bild

Diese Untersuchung veranschaulicht ein Muster, das wir stÃĪndig in GPU-Arbeitslasten sehen: Die GPU ist schnell, der Host ist die Flasche, und GPU-Metriken kÃķnnen es nicht sehen. nvidia-smi berichtete eine geringe Auslastung, aber konnte nicht erklÃĪren, warum. torch.profiler erfasste CUDA-Kernel, aber verpasste die 200.000 Kontextwechsel, die in Benutzerbereichen stattfanden.

Der einzige Weg, das vollstÃĪndige Bild zu sehen, bestand darin, beide Seiten gleichzeitig zu verfolgen – CUDA-API-Aufrufe auf Bibliotheksebene und Linux-Kernel-Scheduling-Ereignisse – und sie nach Zeit und Prozess-ID zu korrelieren. Die Kausalkette “CPU 100% -> 1.880 sched_switch -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” erzÃĪhlt die vollstÃĪndige Geschichte in einer Zeile. Ohne cross-stack-Tracing wÃĪre dies ein RÃĪtsel geblieben – wie es fÞr den ursprÞnglichen Berichterstatter der Fall war, der Wochen damit verbrachte, es zu debuggen.

Abbildung 2: Wenn man den Model fragt “Was ist das Kernproblem?”, identifiziert es die CPU-Überzeichnung als Ursache fÞr Host-Seite-Scheduling-VerzÃķgerungen. cudaLaunchKernel ging von 73us auf 25,8ms (356-mal langsamer) zurÞck, weil die CPU den Start nicht rechtzeitig einplanen konnte.

Probieren Sie es selbst aus

Reproduzieren Sie den 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()

# Schneller Pfad 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'Direkt: {time.time()-start:.3f}s')

# Langsamer Pfad 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')

Verfolgen Sie mit Ingero, um zu sehen, was unter der Haube passiert:

git clone https://github.com/ingero-io/ingero.git
cd ingero && make build
sudo ./bin/ingero trace --duration 60s # in einem Terminal
python3 benchmark.py # in einem anderen Terminal
./bin/ingero explain --since 60s # nach Benchmark-Abschluss

Oder Þberspringen Sie die Reproduktion und erkunden Sie unsere Spurdaten direkt. Die Untersuchungsdatenbank (764KB) befindet sich im Repository:

# Kausalketten aus der Untersuchung anzeigen
./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m

# Prozessbasierte AufschlÞsselung (siehe DataLoader-Worker vs Hauptprozess) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m

# Verbinden Sie Ihren AI-Assistenten fÞr interaktive Untersuchung ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db

Untersuchen Sie mit AI (empfohlen). Der schnellste Weg, die Spur zu analysieren, besteht darin, jeden MCP-kompatiblen AI direkt mit der Datenbank zu verbinden. Keine manuelle Analyse erforderlich.

Erstellen Sie eine Konfigurationsdatei:

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

Dann verbinden Sie Ihr Modell:

# Mit Ollama + MiniMax (was wir im Video verwendet haben)
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json

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

# Mit jedem MCP-kompatiblen Client # FÞgen Sie die Konfiguration oben zu den MCP-Einstellungen Ihres AI hinzu

Geben Sie /investigate ein, um die gefÞhrte Analyse auszulÃķsen, oder stellen Sie eine Frage: “Was verursachte den GPU-Hunger?” Der AI hat Zugriff auf 7 Tools, die die Spurdatenbank direkt abfragen.

GitHub: github.com/ingero-io/ingero
UrsprÞngliches Issue: pytorch/pytorch#154318
Video-Anleitung: https://asciinema.org/a/RGwhPeXAPJdhXqxp

Untersuchung durchgefÞhrt auf TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.

David Mail ist Co-Autor und Maintainer von Ingero, einem Open-Source-eBPF-Agent fÞr CUDA-Level-GPU-Beobachtbarkeit. Er spezialisiert sich auf Kernel-Level-Verfolgung von Produktions-AI-Workloads.