Αναφορές

124x Αργότερα: Τι Κάνει Πραγματικά το PyTorch DataLoader στο Επίπεδο Πυρήνα

mm
Προσθέστε το Unite.AI στις προτιμώμενες πηγές σας στο 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.

Αυτό το άρθρο βασίζεται σε ευρήματα από μια έρευνα ιχνηλάτη στο επίπεδο πυρήνα GPU που πραγματοποιήθηκε σε ένα πραγματικό πρόβλημα PyTorch (#154318) χρησιμοποιώντας eBPF uprobes. Οι βάσεις δεδομένων ιχνηλάτη δημοσιεύονται στο ανοικτό αποθετήριο Ingero για ανεξάρτητη επαλήθευση.

TL;DR

Το DataLoader του PyTorch μπορεί να είναι 50-124 φορές πιο αργό από την άμεση ευθυγράμμιση πινάκων για φορτία GPU στην μνήμη. Αναπαράγαμε ένα πραγματικό πρόβλημα PyTorch σε ένα RTX 4090 και ιχνηλάτησα κάθε κλήση API CUDA και συμβάν πυρήνα Linux για να βρούμε την ρίζα του προβλήματος. Η GPU δεν ήταν αργή – λιμοκτονούσε. Οι εργαζόμενοι του DataLoader παρήγαγαν 200.000 ανταλλαγές контекστα και 300.000 αναθέσεις σελίδων σε 40 δευτερόλεπτα, αφήνοντας την GPU να περιμένει κατά μέσο όρο 301ms ανά μεταφορά δεδομένων που θα πρέπει να διαρκέσει μικρότερα από 1 χιλιοστό του δευτερολέπτου.

Το Πρόβλημα

Ένας χρήστης PyTorch ανέφερε ότι το DataLoader ήταν 7-22 φορές πιο αργό από την άμεση ευθυγράμμιση πινάκων για ένα απλό φορτίο εύρεσης MLP. Ακόμη και με num_workers=12, pin_memory=True και prefetch_factor=12, η διαφορά παρέμεινε τεράστια. Η χρήση GPU ήταν στο 10-20%.

Αναπαράγαμε το πρόβλημα. Η διαφορά ήταν ακόμη χειρότερη στο υλικό μας:

Μέθοδος Χρόνος Σε σύγκριση με την άμεση ευθυγράμμιση
Άμεση ευθυγράμμιση πινάκων 0,39 δευτερόλεπτα 1 φορά
DataLoader (shuffle=True) 48,49 δευτερόλεπτα 124 φορές πιο αργό
DataLoader (βελτιωμένο, 4 εργαζόμενοι, pin_memory) 43,29 δευτερόλεπτα 111 φορές πιο αργό

Το φορτίο είναι ελαφρύ: 7 εκατομμύρια δείγματα, 100 χαρακτηριστικά, 2-στρωματικό MLP, μέγεθος batch 1 εκατομμύριο. Το μοντέλο επεξεργάζεται ένα batch σε χιλιοστά του δευτερολέπτου. Έτσι, πού πηγαίνει ο χρόνος;

Τι Δείχνει το nvidia-smi

Τίποτα χρήσιμο. Η χρήση GPU αναβοσβήνει μεταξύ 0% και 30%. Η χρήση μνήμης είναι σταθερή. Η θερμοκρασία είναι καλή. Η GPU είναι σαφώς υποχρησιμοποιημένη, αλλά το nvidia-smi δεν μπορεί να σας πει γιατί.

Τι Δείχνει το torch.profiler

Ο αναφέροντας προσπάθησε το ενσωματωμένο profiler του PyTorch και “παρέλαβε keine σημαντικά δεδομένα ιχνηλάτη”. Αυτό είναι ένα συνηθισμένο εύρημα – οι προγραμματιστές εφαρμογών μπορούν να σας δείξουν ποια πυρήματα CUDA εκτελούνται, αλλά δεν μπορούν να δουν την προγραμματισμό host, τη μνήμη και τα γεγονότα ζωής διαδικασίας που καθορίζουν εάν τα δεδομένα φτάνουν στην GPU εγκαίρως.

Τι Δείχνει το Ιχνηλάτης στο Επίπεδο Πυρήνα

Εκτελέσαμε το benchmark ενώ ιχνηλάτησα και τις κλήσεις API CUDA (μέσω eBPF uprobes στο libcudart.so) και τα γεγονότα πυρήνα Linux (αλλαγές контекστα, αναθέσεις σελίδων, διαδικασίες fork) ταυτόχρονα. Τα αποτελέσματα λένε την πλήρη ιστορία.

Πλήρης βίντεο αναπαράστασης: https://asciinema.org/a/RGwhPeXAPJdhXqxp

Στο βίντεο, συνδέσαμε ένα ανοικτό LLM (MiniMax-M2.7) με τη βάση δεδομένων ιχνηλάτη μέσω MCP (Model Context Protocol):

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

Το αρχείο JSON ρυθμίζει τον πελάτη MCP να βρει τον сервер Ingero και ποια βάση δεδομένων ιχνηλάτη να φορτώσει:

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

Αυτό δίνει στο LLM άμεση πρόσβαση στα δεδομένα ιχνηλάτη μέσω 7 εργαλείων: get_trace_stats, get_causal_chains, get_per_process_breakdown, και άλλα. Το AI μπορεί να ερωτήσει τη βάση δεδομένων, να συσχετίσει τα γεγονότα CUDA με τα δεδομένα προγραμματισμού πυρήνα και να παράγει μια απλή γλώσσα διάγνωσης χωρίς καμία χειροκίνητη ανάλυση.

4 Υψηλής Σεβασμότητας Αιτιώδεις Αλυσίδες

Ο μηχανισμός αιτιώδους αλυσίδας ανίχνευσε 4 υψηλής σεβασμότητας μοτίβα, όλα με την ίδια ρίζα αιτία:

[Υψηλή] cudaStreamSync p99=42ms (1,638x p50=25us) - CPU 100% + 1,880 αλλαγές контекστα
Χρονοδιάγραμμα:
[ΣΥΣΤΗΜΑ] CPU 100%
[HOST ] 1,880 αλλαγές контекστα (21s εκτός-CPU)
[CUDA ] p99=42ms (1,638x p50=25us)
Ρίζα αιτία: Εργαζόμενοι του DataLoader αγωνίζονται για CPU, τεράστια πίεση αναθέσεων σελίδων
[Υψηλή] cudaLaunchKernel p99=24.67ms (349x p50=70us) - CPU 100%
Ρίζα: 34 αλλαγές контекστα

[Υψηλή] cuMemAlloc p99=627us (4.0x p50) - CPU 100%
[Υψηλή] cuLaunchKernel p99=106us (4.0x p50) - CPU 100%

Το cudaStreamSync p99 είναι 1.638 φορές το p50. Αυτό δεν είναι αργότητα GPU – αυτό είναι η GPU που περιμένει δεδομένα που δεν φτάνουν εγκαίρως.

Εικόνα 1: AI-γεννημένη ανάλυση μετά την εκτέλεση /investigate. Το μοντέλο χρησιμοποίησε τα 7 εργαλεία MCP της Ingero για να ερωτήσει τη βάση δεδομένων ιχνηλάτη και παρήγαγε μια απλή γλώσσα εξήγηση με ενεργές συστάσεις που προέρχονται trực tiếp από τα δεδομένα ιχνηλάτη.

Η Αναλυτική Ανάλυση ανά Διεργασία

Αυτή είναι όπου γίνεται σαφές. Η κύρια διεργασία και οι 4 εργαζόμενοι του DataLoader είναι ορατοί ως ξεχωριστά αντικείμενα:
Κύρια διεργασία:

- cudaMemcpyAsync (μεταφορά host-to-device): μέσος όρος 301ms, μέγιστος 2.9 δευτερόλεπτα
- cudaStreamSync: p99 = 42ms (κανονικά 25us)
- 1,567 αλλαγές контекστα, μέσος όρος 16ms εκτός-CPU, χειρότερη στάση 5 δευτερόλεπτα
- 799,018 αναθέσεις σελίδων
Εργαζόμενος του DataLoader 1: 52,863 αλλαγές контекστα, 89,338 αναθέσεις σελίδων, χειρότερη στάση 5s
Εργαζόμενος του DataLoader 2: 50,638 αλλαγές контекστα, 83,509 αναθέσεις σελίδων, χειρότερη στάση 5s
Εργαζόμενος του DataLoader 3: 49,361 αλλαγές контекστα, 70,035 αναθέσεις σελίδων, χειρότερη στάση 5s
Εργαζόμενος του DataLoader 4: 38,862 αλλαγές контекστα, 56,354 αναθέσεις σελίδων, χειρότερη στάση 5s

Σύνολο σε εργαζόμενους: ~191,000 αλλαγές контекστα και ~299,000 αναθέσεις σελίδων σε 40 δευτερόλεπτα.

Τι Σημαίνει Αυτό

Οι εργαζόμενοι του DataLoader κάνουν τρία δαπανηρά πράγματα που η άμεση ευθυγράμμιση αποφεύγει εντελώς:

  1. Συμμετοχή και ευθυγράμμιση: DataLoader με shuffle=True γεννάει μια τυχαία ανακατανομή δεικτών, στη συνέχεια κάθε εργαζόμενος επιλέγει το τμήμα του. Αυτό απαιτεί τυχαία πρόσβαση μνήμης σε ολόκληρο τον πίνακα 7 εκατομμυρίων δειγμάτων – κακό για την τοπικότητα cache και προκαλεί page faults.
  2. Συλλογή και αντιγραφή: Κάθε εργαζόμενος συλλέγει σπασμένα δείγματα σε einen συνεχόμενο πίνακα batch. Αυτό σημαίνει αναθέσεις νέας μνήμης (αναθέσεις σελίδων), αντιγραφή δεδομένων από τυχαίες τοποθεσίες (cache misses), και σειριοποίηση του αποτελέσματος πίσω στην κύρια διεργασία μέσω κοινής μνήμης ή μιας ουράς.
  3. Ανταγωνισμός για CPU: Τέσσερις εργαζόμενοι + η κύρια διεργασία σε μια μηχανή 4-vCPU σημαίνει συνεχείς προγραμματισμούς. Κάθε εργαζόμενος αποσυνδεθεί 50,000 φορές. Η χειρότερη στάση είναι 5 δευτερόλεπτα – κατά τη διάρκεια της οποίας η GPU δεν έχει τίποτα να επεξεργαστεί.

Με άμεση ευθυγράμμιση: X[i:i+batch_size] είναι μια άποψη χωρίς αντίγραφο ενός συνεχόμενου πίνακα που ήδη βρίσκεται στη μνήμη. .to(device) προκαλεί μια μεταφορά DMA από μια seule συνεχόμενη περιοχή. Δεν υπάρχουν εργαζόμενοι, δεν υπάρχει συμμετοχή, δεν υπάρχει συλλογή, δεν υπάρχει ανταγωνισμός για CPU. Η GPU λαμβάνει δεδομένα σε χιλιοστά του δευτερολέπτου, όχι εκατοντάδες χιλιοστά του δευτερολέπτου.

Η Επίλυση

Για φορτία GPU στην μνήμη όπου το σύνολο του συνόλου δεδομένων χωράει στη RAM:

  1. Μη χρήση του DataLoader. Άμεση ευθυγράμμιση με ένα προ-συμμεμιγμένο πίνακα δεικτών είναι απλούστερο και 100 φορές ταχύτερο:
    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. Εάν πρέπει να χρησιμοποιήσετε DataLoader, αντιστοιχίστε το num_workers με τους πραγματικούς πυρήνες CPU minus 1. Σε μια μηχανή 4-πυρήνα, num_workers=2 μειώνει την αντιπαλότητα. Προσθέστε persistent_workers=True για να αποφευχθεί ο προγραμματισμός fork.
  3. Για μεγαλύτερα φορτία από τη μνήμη όπου το DataLoader είναι απαραίτητο, το πραγματικό εμπόδιο μεταφέρεται στην είσοδο/έξοδο δίσκου. Χρησιμοποιήστε prefetch_factor=2 (όχι υψηλότερα – περισσότερη προ-ανάκτηση σημαίνει περισσότερη πίεση μνήμης) και βεβαιωθείτε ότι η αποθήκευση σας μπορεί να跟ει.

Το Μεγαλύτερο Πλαίσιο

Αυτή η έρευνα εικονογραφεί ένα μοτίβο που βλέπουμε συνεχώς σε φορτία GPU: η GPU είναι γρήγορη, ο host είναι το εμπόδιο, και τα μετρικά GPU δεν possono δουν. Το nvidia-smi ανέφερε χαμηλή χρήση αλλά δεν μπορούσε να εξηγήσει γιατί. Το torch.profiler κατέγραψε πυρήνα CUDA αλλά missed τις 200,000 αλλαγές контекστα που συμβαίνουν στο userspace.

Ο μόνος τρόπος για να δείτε την πλήρη εικόνα ήταν να ιχνηλατήσετε και τις δύο πλευρές ταυτόχρονα – κλήσεις API CUDA στο επίπεδο βιβλιοθήκης και γεγονότα πυρήνα Linux – και να τις συσχετίσετε με χρόνο και αναγνωριστικό διεργασίας. Η αιτιώδης αλυσίδα “CPU 100% -> 1,880 αλλαγές контекστα -> cudaMemcpyAsync 301ms -> cudaStreamSync 42ms” λέει την πλήρη ιστορία σε μια γραμμή. Χωρίς ιχνηλάτηση cross-stack, αυτό θα είχε παραμείνει ένα μυστήριο – όπως ήταν για τον αρχικό αναφέροντα που πέρασε εβδομάδες να το διορθώσει.

Εικόνα 2: Όταν ζητήθηκε “τι είναι το βασικό ζήτημα;”, το μοντέλο αναγνωρίζει την υπερ-χρησιμοποίηση CPU που προκαλεί καθυστερήσεις προγραμματισμού host. Το cudaLaunchKernel πήγε από 73us σε 25.8ms (356 φορές πιο αργό) γιατί ο CPU δεν μπορούσε να προγραμματίσει την εκκίνηση εγκαίρως.

Δοκιμάστε το Εαυτό σας

Αναπαράγετε το 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()

# Γρήγορη διαδρομή 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'Άμεση: {time.time()-start:.3f}s')

# Αργή διαδρομή 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')

Ιχνηλατήστε με Ingero για να δείτε τι συμβαίνει κάτω από το καπό:

git clone https://github.com/ingero-io/ingero.git
cd ingero && make build
sudo ./bin/ingero trace --duration 60s # σε ένα τερματικό
python3 benchmark.py # σε ένα άλλο τερματικό
./bin/ingero explain --since 60s # μετά την ολοκλήρωση του benchmark

Ή απλώς εξερευνήστε τα δεδομένα ιχνηλάτη μας απευθείας. Η βάση δεδομένων έρευνας (764KB) βρίσκεται στο αποθετήριο:

# Δείτε τις αιτιώδεις αλυσίδες από την έρευνα
./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --since 5m

# Αναλυτική ανάλυση ανά διεργασία (δείτε τους εργαζόμενους του DataLoader έναντι της κύριας διεργασίας) ./bin/ingero explain --db investigations/pytorch-dataloader-starvation.db --per-process --since 5m

# Συνδέστε τον βοηθό AI σας για διαδραστική έρευνα ./bin/ingero mcp --db investigations/pytorch-dataloader-starvation.db

Ερευνήστε με AI (συνιστάται). Ο ταχύτερος τρόπος για να αναλύσετε τα δεδομένα ιχνηλάτη είναι να συνδέσετε οποιοδήποτε AI συμβατό με MCP απευθείας στη βάση δεδομένων. Δεν χρειάζεται χειροκίνητη ανάλυση.

Δημιουργήστε ένα αρχείο ρυθμίσεων:

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

Στη συνέχεια, συνδέστε το μοντέλο σας:

# Με Ollama + MiniMax (αυτό που χρησιμοποιήσαμε στο βίντεο)
ollmcp -m minimax-m2.7:cloud -j /tmp/ingero-mcp-dataloader.json

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

# Με οποιοδήποτε πελάτη συμβατό με MCP # Προσθέστε τη ρύθμιση παραπάνω στις ρυθμίσεις MCP του AI σας

Γράψτε /investigate για να ενεργοποιήσετε την οδηγούμενη ανάλυση, ή ζητήστε οποιαδήποτε ερώτηση: “Τι προκάλεσε τη λιμοκτονία της GPU;” Το AI έχει πρόσβαση σε 7 εργαλεία που ερωτούν τη βάση δεδομένων ιχνηλάτη απευθείας.

GitHub: github.com/ingero-io/ingero
Αρχικό ζήτημα: pytorch/pytorch#154318
Βίντεο αναπαράστασης: https://asciinema.org/a/RGwhPeXAPJdhXqxp

Η έρευνα πραγματοποιήθηκε σε TensorDock RTX 4090 (24GB), Ubuntu 22.04, PyTorch 2.10.0+cu128.

Ο David Mail είναι συν-συγγραφέας και συντηρητής του Ingero, ενός ανοικτού κώδικα eBPF agent για CUDA-level GPU παρατηρησιμότητα. Ειδικεύεται στην ανίχνευση σε επίπεδο πυρήνα παραγωγικών AI workloads.