Ο Πρακτικός Οδηγός για Προχωρημένο PyTorch
Εισαγωγή
Η εμπειρία στο PyTorch δεν σημαίνει πλέον μόνο οικειότητα με τις δυνατότητες· αφορά στη λειτουργία μιας επαναλήψιμης ροής μηχανικής όπου ο κώδικας εκπαίδευσης παραμένει γρήγορος, κλιμακούμενος και ανακτήσιμος υπό ρεαλιστικά φορτία παραγωγής. Δυνατότητες όπως torch.compile, torch.profiler, DDP/FSDP και Distributed Checkpointing είναι ισχυρά εργαλεία στο οπλοστάσιο των ομάδων ML και υποδομών για τη διατήρηση της ταχύτητας της εκπαίδευσης. Ωστόσο, είναι χωρίς νόημα αν δεν εφαρμοστούν με τη σωστή σειρά και δεν επικυρωθούν αυστηρά.
Σε αυτό το άρθρο, μοιραζόμαστε μια προτεινόμενη ροή εργασίας: βασική γραμμή → συναρμολόγηση → προφίλ → κλιμάκωση → σημείο ελέγχου. Θα καλύψουμε τι πρέπει να μετράμε πριν από την βελτιστοποίηση, τυπικές παγίδες που πρέπει να αποφύγουμε με τον compiler και τον profiler, κανόνες αποφάσεων για τη χρήση DDP έναντι FSDP, και πώς να εφαρμόζουμε ανθεκτικά σε σφάλματα checkpoints για εκτελέσεις εκπαίδευσης πολλαπλών κόμβων.
Κύρια Συμπεράσματα
- Προσεγγίστε τη βελτιστοποίηση επιδόσεων του PyTorch ως μια επαναλαμβανόμενη διαδικασία μηχανικής και όχι ως λίστα ελέγχου χαρακτηριστικών: Η επαναλαμβανόμενη εφαρμογή της διαδικασίας baseline → compile → profile → scale → checkpoint οδηγεί σε πιο βιώσιμες επιδόσεις από το να ενεργοποιείτε τις βελτιστοποιήσεις απομονωμένα.
- Εκτός αν έχετε μια σταθερή βασική έκδοση με ένα GPU, με γνωστή απόδοση και επαληθευμένη ορθότητα, δεν μπορείτε να συγκρίνετε αποτελεσματικά την απόδοση ή να εντοπίσετε προβλήματα.
- Μην βελτιστοποιείτε ποτέ πριν καθορίσετε μια σωστή βασική γραμμή: Μια σταθερή βασική γραμμή με μία GPU, με μετρημένο ρυθμό απόδοσης και επαληθευμένη ορθότητα, είναι απαραίτητη για ουσιαστικές συγκρίσεις επιδόσεων και εντοπισμό σφαλμάτων.
- Χρησιμοποιήστε
torch.compileσκόπιμα, όχι τυφλά: Παρακολουθήστε τις διακοπές γραφήματος, διαχειριστείτε τα σχήματα, ζεστάνετε πριν μετρήσετε και επιβεβαιώστε ότι η απόδοση σε σταθερή κατάσταση βελτιώνεται πραγματικά σε σχέση με την εκτέλεση σε eager mode. - Προφίλ για καθοδήγηση αποφάσεων, όχι για επιβεβαίωση υποθέσεων:
Χρησιμοποιήστε το torch.profiler για να πάρετε σαφείς αποφάσεις, να επιλύσετε στασιμότητες CPU, σημεία συμφόρησης του πυρήνα, επανακαθορισμό σχήματος και να επαληθεύσετε το κόστος επικοινωνίας σε κατανεμημένες εκτελέσεις. - Σχεδιάστε τον έλεγχο σημείων για αποτυχία από την πρώτη μέρα: Ο κατανεμημένος έλεγχος σημείων (με προαιρετική ασύγχρονη αποθήκευση) πρέπει να καταγράφει πλήρως την κατάσταση της εκπαίδευσης, να υποστηρίζει την ανακατανομή σε διαφορετικό αριθμό GPU και να δοκιμάζεται τακτικά μέσω ασκήσεων επαναφοράς για να εξασφαλιστεί πραγματική αντοχή σε σφάλματα.
Βασικό Επίπεδο: Καθιέρωση Σημείου Αναφοράς
Ξεκινήστε έχοντας ένα λειτουργικό παράδειγμα εκπαίδευσης με μία μόνο GPU. Αυτό θα είναι το σημείο αναφοράς σας τόσο για τη λειτουργικότητα όσο και για την απόδοση. Ορίστε το μοντέλο, τον φορτωτή δεδομένων και τον βρόχο εκπαίδευσης. Βεβαιωθείτε ότι όλα τρέχουν από άκρη σε άκρη σε λειτουργία eager (χωρίς μεταγλώττιση, μία διαδικασία). Ακολουθεί ένα παράδειγμα του πώς μπορεί να φαίνεται ο βρόχος εκπαίδευσης:
import torch
import torch.nn as nn
# Dummy model and data for illustration
model = nn.Sequential(nn.Linear(100, 50), nn.ReLU(), nn.Linear(50, 10))
data = torch.randn(32, 100)
targets = torch.randint(0, 10, (32,))
# Baseline forward + backward
optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
outputs = model(data)
loss = nn.CrossEntropyLoss()(outputs, targets)
loss.backward()
optimizer.step()Εκτελέστε μερικές επαναλήψεις εκπαίδευσης και μετρήστε την απόδοση (για παράδειγμα, δείγματα/δευτερόλεπτο). Η σωστή βάση αναφοράς εξασφαλίζει ότι το μοντέλο σας συγκλίνει και παρέχει ένα σημείο σύγκρισης για μελλοντικές βελτιστοποιήσεις. Σε αυτό το στάδιο, η ορθότητα και η απόδοση της βάσης αναφοράς είναι προτεραιότητες: βεβαιωθείτε ότι η GPU χρησιμοποιείται (ελέγξτε με nvidia-smi ή τα αρχεία καταγραφής του torch), και ότι δεν υπάρχουν εμφανή σημαντικά στενά σημεία (όπως η καθυστέρηση του loadgen στα δεδομένα, υπερβολικές διαδικασίες CPU κ.ά.). Μην προχωρήσετε στη μεταγλώττιση και άλλες βελτιστοποιήσεις μέχρι να έχετε μια σταθερή βάση αναφοράς.
Βασικός κατάλογος ελέγχου
- Λειτουργική ορθότητα: Το μοντέλο εκπαιδεύεται και παράγει τα αναμενόμενα αποτελέσματα σε μια μόνο GPU.
- Καταγεγραμμένη βασική απόδοση: π.χ., χρόνος ανά παρτίδα ή ποσοστό χρησιμοποίησης GPU.
- Καμία προφανής στενωπός: Η ροή δεδομένων κρατά την GPU απασχολημένη (χωρίς μεγάλα διαστήματα αδράνειας στη βασική εκδοχή).
Μόλις εφαρμόσετε μια καλή βασική έκδοση, μπορείτε να αρχίσετε να προσθέτετε προχωρημένα χαρακτηριστικά σταδιακά, ξεκινώντας με τον κορυφαίο μεταγλωττιστή του PyTorch 2.x για να επιταχύνετε την εκπαίδευση.
Συγκεκριμενοποίηση: Επιτάχυνση με torch.compile στο PyTorch
Το PyTorch 2 επέτρεψε στους χρήστες να κάνουν JIT compile μοντέλα για καλύτερη απόδοση χρησιμοποιώντας το torch.compile. Τυλίξτε το μοντέλο ή τη λειτουργία σας με το torch.compile και το PyTorch δημιουργεί βελτιστοποιημένο κώδικα στο παρασκήνιο.
Δες πόσο εύκολο είναι παρακάτω:
# Switch to compiled mode
model = torch.compile(model) # uses default backend 'inductor'Καλώντας model(data) θα χρησιμοποιηθεί αυτόματα μια βελτιστοποιημένη διαδρομή κώδικα. Οι πρώτες λίγες επαναλήψεις θα μεταγλωττίσουν το μοντέλο σας σε χρόνο εκτέλεσης· οι επόμενες επαναλήψεις θα χρησιμοποιούν απευθείας τους βελτιστοποιημένους πυρήνες. Το PyTorch 2.9 αποθηκεύει αυτόματα τα αποτελέσματα της μεταγλώττισης στη μνήμη για τη βελτίωση των μελλοντικών εκτελέσεων (ακόμη και μεταξύ διαφορετικών διεργασιών).
Ωστόσο, αν θέλετε να αξιοποιήσετε στο έπακρο την απόδοση των συνθέσεων, υπάρχουν μερικά πράγματα που θα πρέπει να γνωρίζετε για τα διαλείμματα γραφήματος και τα δυναμικά σχήματα.
Διακοπές γραφήματος
Οι διακοπές γραφήματος συμβαίνουν όποτε ο μεταγλωττιστής δεν μπορεί να συλλάβει ένα τμήμα του κώδικά σας σε ένα ενιαίο γράφημα (όπως η ροή ελέγχου Python που εξαρτάται από δεδομένα). Από προεπιλογή, το torch.compile θα επιστρέφει αυτόματα στην εκτέλεση με όρεξη για τα μη υποστηριζόμενα μέρη (σπάζοντας το γράφημα) και θα συντάσσει τα υπόλοιπα. Αυτό διασφαλίζει ότι ο κώδικάς σας δεν θα καταρρεύσει. Ωστόσο, κάθε διακοπή γραφήματος εμποδίζει ένα μέρος του κώδικα να βελτιστοποιηθεί (εκείνο το τμήμα εκτελείται καθαρά σε Python, όχι στο συνενωμένο γράφημα). Το torch.compile(fullgraph=True) είναι χρήσιμο για την αποσφαλμάτωση διακοπών γραφήματος κατά την ανάπτυξη, καθώς εμφανίζει σφάλμα εάν οποιοδήποτε μέρος του μοντέλου δεν μπορεί να συνταχθεί. Για παράδειγμα:
model = torch.compile(model, fullgraph=True)
try:
model(data)
except Exception as e:
print("Graph break:", e)
model = torch.compile(model, fullgraph=True)
try:
model(data)
except Exception as e:
print("Graph break:", e)Αυτό θα προκαλέσει σφάλμα στην πρώτη μη υποστηριζόμενη λειτουργία και θα σας κατευθύνει προς τα τμήματα που χρειάζονται αναδιάρθρωση. Το μήνυμα σφάλματος συνήθως θα περιλαμβάνει κάποιες ενδείξεις (ή ένα URL) για το τι προκάλεσε τη διακοπή και πώς να την αποφύγετε. Τα αρχεία καταγραφής του PyTorch μπορεί να είναι χρήσιμα: αν εκτελέσετε το script σας με τη μεταβλητή περιβάλλοντος TORCH_LOGS=“graph_breaks ”, θα εμφανιστούν οι λόγοι και οι τοποθεσίες όπου συμβαίνουν οι διακοπές του γραφήματος. Χρησιμοποιήστε τις παρεχόμενες πληροφορίες για να ξαναγράψετε ή να αφαιρέσετε λειτουργίες από την πλευρά της Python που εμποδίζουν τη μεταγλώττιση (όπως η αντικατάσταση λειτουργιών Python list() ή κώδικα if/else που εξαρτάται από δεδομένα με λειτουργίες tensor, ή η πλήρης αφαίρεσή τους).
Δυναμικά σχήματα
Εξ ορισμού, το torch.compile θα εξειδικεύσει το συνταγμένο γραφικό διάγραμμα στις μορφές που βλέπει. Αν το μοντέλο σας λειτουργεί με εισόδους διαφορετικών μεγεθών, θα ξανασυγκροτήσει το διάγραμμα κάθε φορά που βλέπει μια νέα μορφή. Αυτή η επανεκχώρηση έχει κάποιο επιπλέον κόστος. Στο PyTorch 2.9, προσθέσαμε μια δυναμική σημαία. Ο καθορισμός dynamic=True εκ των προτέρων προσπαθεί να δημιουργήσει έναν ενιαίο πυρήνα που μπορεί να χειριστεί πολλές μορφές (συμβολικές μορφές). Για παράδειγμα:
model = torch.compile(model, dynamic=True)Αυτό καθοδηγεί τον μεταγλωττιστή να παρακολουθεί ένα γενικευμένο γράφο, το οποίο συνεπάγεται ανταλλαγή επαναμεταγλωττισμών καθώς οι διαστάσεις αλλάζουν (όπως οι μήκη ακολουθίας μεταβλητών). Από την άλλη πλευρά, η παράδοση του dynamic=False δεν θα δημιουργήσει ποτέ δυναμικούς πυρήνες και θα επιβάλει εξειδίκευση σε ακριβείς διαστάσεις (κάτι που μπορεί να οδηγήσει σε ταχύτερο κώδικα αν ισχύει). Η ρύθμιση dynamic=None είναι η προεπιλογή ξεκινώντας από την έκδοση 2.9 και ενημερώνει τον μεταγλωττιστή να ανιχνεύσει αυτόματα αν υπήρξαν πολλαπλοί επαναμεταγλωττισμοί και στη συνέχεια να μεταβεί στη χρήση δυναμικού πυρήνα κατά τη διάρκεια της εκτέλεσης.
- Αφήστε το dynamic στις προεπιλεγμένες ρυθμίσεις ή ορίστε το σε False αν τα σχήματα εισόδου είναι σταθερά ή αλλάζουν σπάνια για μέγιστη εξειδίκευση.
- Ορίστε dynamic=True αν λαμβάνετε συχνά επανασυμπιλήσεις λόγω παραλλαγών στο σχήμα (μοντέλα NLP που εκτελούν προτάσεις διαφορετικού μήκους κ.λπ.) και θέλετε να αποφύγετε να φτάσετε το όριο επανασυμπίλησης (που είναι 8 από προεπιλογή πριν εγκαταλείψει και επανέλθει στην άμεση εκτέλεση).
Συγκεντρωμένο Παράδειγμα
Ας συντάξουμε ένα απλό μοντέλο και ας δείξουμε πώς να χειριζόμαστε τις διακοπές γραφήματος και τα δυναμικά σχήματα:
import torch
# Sample model with a potential graph break (data-dependent control flow)
class ToyModel(torch.nn.Module):
def forward(self, x):
# Example: data-dependent branching (not traceable by Dynamo)
if x.sum() > 0:
return x * 2
else:
return x
model = ToyModel()
# Attempt full-graph compilation to catch breaks
try:
model = torch.compile(model, fullgraph=True)
output = model(torch.randn(4, 4))
except Exception as e:
print("Graph break detected:", e)
# Rewrite model or accept partial graph compilation
model = torch.compile(model, fullgraph=False) # fallback to allow breaks
Στον παραπάνω κώδικα, το if που εξαρτάται από τα δεδομένα θα προκαλέσει σφάλμα διακοπής γραφήματος όταν χρησιμοποιείται fullgraph=True. Αιχμαλωτίζουμε αυτό το σφάλμα, το εκτυπώνουμε και στη συνέχεια επανασυντάσσουμε με την προεπιλεγμένη συμπεριφορά, επιτρέποντας τις διακοπές γραφήματος. Στην πράξη, θα διορθώνατε το μοντέλο σας (αφαιρώντας την διακλάδωση που εξαρτάται από τα δεδομένα σε αυτή την περίπτωση) μέχρι να μπορείτε να συντάξετε με fullgraph=True χωρίς εξαιρέσεις – σημαίνει ότι ολόκληρο το μοντέλο μπορεί να συνταχθεί στατικά ως ένα γράφημα για μέγιστη ταχύτητα.
Συγκεντρώστε τη Λίστα Ελέγχου
- Τυλίξτε το μοντέλο με torch.compile(): Μικρή αλλαγή κώδικα που συχνά οδηγεί σε σημαντική επιτάχυνση. Επιλέξτε το backend «inductor» από προεπιλογή, εκτός αν έχετε κάποιο λόγο να μην το κάνετε.
- Ζέσταμα: Τρέξτε μερικές επαναλήψεις πριν μετρήσετε τον χρόνο – οι πρώτοι γύροι περιλαμβάνουν τον χρόνο μεταγλώττισης.
- Έλεγχος διακοπών γραφήματος: Κατά την ανάπτυξη, δοκιμάστε να εκτελέσετε με fullgraph=True και ορίστε τη μεταβλητή περιβάλλοντος TORCH_LOGS=“graph_breaks” για να εντοπίσετε τμήματα κώδικα που δεν υποστηρίζονται. Αφαιρέστε ή αναδιαμορφώστε αυτές τις χρήσεις.
- Ρύθμιση δυναμικών σχημάτων: Εάν διαπιστώσετε ότι γίνονται πολλές επανασυνθέσεις (αναζητήστε το «recompiling» στο stdout/stderr/logs), μπορείτε να εξετάσετε το dynamic=True για να δημιουργήσετε ένα γραφικό με ευέλικτα σχήματα.
- Επιτάχυνση παρακολούθησης: Μετρήστε τη ροή δεδομένων σε σύγκριση με τη βασική σας απόδοση. Η πρώτη επανάληψη πιθανότατα θα είναι πιο αργή, αλλά η σταθερή ταχύτητα θα πρέπει να υπερβαίνει τη βασική σας απόδοση. Αν όχι, εκτελέστε με ενεργοποιημένες τις ενδείξεις απόδοσης (π.χ., ορίστε TORCH_LOGS=“perf_hints ”).
Αφού συντάξουμε το μοντέλο μας και επιτύχουμε ταχύτερη απόδοση, θα πρέπει να αναλύσουμε την εκτέλεσή του για να εντοπίσουμε τυχόν εναπομείναντα σημεία συμφόρησης και να καθορίσουμε περαιτέρω περιοχές για βελτιστοποίηση.
Προφίλ: Διάγνωση Συσκότισης με το torch.profiler
Τα bottlenecks απόδοσης μπορούν ακόμα να υπάρχουν ακόμα και μετά τη μεταγλώττιση - είτε πρόκειται για υποχρησιμοποιημένη GPU, εμπλοκές I/O ή μη βελτιστοποιημένους πυρήνες. Το PyTorch διαθέτει έναν ενσωματωμένο Profiler για τη συλλογή ιχνών εκτέλεσης και την αναγνώριση αυτών των bottlenecks. Στο PyTorch 2.9, το torch.profiler μπορεί να παρακολουθήσει δραστηριότητες CPU και GPU, να καταγράψει σχήματα και να ενσωματωθεί με το Chrome Trace Viewer ή το TensorBoard για οπτικοποίηση. Το torch.profiler μπορεί επίσης να καταγράφει ίχνη ασύγχρονα. Με αυτόν τον τρόπο, μπορείτε να κάνετε profiling τμημάτων της εκπαίδευσης χωρίς να διακόπτετε τη ροή του προγράμματος.
import torch
import torch.nn as nn
import torch.optim as optim
import torch.profiler
# -----------------------------
# Device setup
# -----------------------------
device = "cuda" if torch.cuda.is_available() else "cpu"
# -----------------------------
# Model definition
# -----------------------------
model = nn.Sequential(
nn.Linear(100, 256),
nn.ReLU(),
nn.Linear(256, 10)
).to(device)
# -----------------------------
# Optimizer and loss
# -----------------------------
optimizer = optim.Adam(model.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
# -----------------------------
# Data iterator (synthetic)
# -----------------------------
def data_generator(batch_size=32):
while True:
inputs = torch.randn(batch_size, 100, device=device)
targets = torch.randint(0, 10, (batch_size,), device=device)
yield inputs, targets
data_iter = data_generator()
# -----------------------------
# Training step
# -----------------------------
def train_step(batch):
model.train()
inputs, targets = batch
optimizer.zero_grad(set_to_none=True)
outputs = model(inputs)
loss = loss_fn(outputs, targets)
loss.backward()
optimizer.step()
return loss
# -----------------------------
# Warm-up phase
# -----------------------------
for _ in range(5):
train_step(next(data_iter))
# -----------------------------
# Profiling phase
# -----------------------------
with torch.profiler.profile(
activities=[
torch.profiler.ProfilerActivity.CPU,
torch.profiler.ProfilerActivity.CUDA,
],
record_shapes=True,
profile_memory=True,
) as prof:
for _ in range(3):
train_step(next(data_iter))
prof.step()
# -----------------------------
# Export trace
# -----------------------------
prof.export_chrome_trace("trace.json")
print("Profiler trace saved to trace.json")Αυτό το παράδειγμα script δείχνει πώς να ορίσετε έναν ελάχιστο (αλλά πλήρη) βρόχο εκπαίδευσης PyTorch και να χρησιμοποιήσετε το torch.profiler για να καταγράψετε την σταθερή απόδοση CPU και GPU. Δημιουργεί ένα απλό νευρωνικό δίκτυο, φτιάχνει συνθετικά δεδομένα με έναν iterator και ενσωματώνει μια επανάληψη εκπαίδευσης στη συνάρτηση train_step().
Τρέχουμε πρώτα μερικές επαναλήψεις προθέρμανσης (ώστε να μην προφιλάρουμε το κόστος μιας μόνος φοράς αρχικοποίησης/κρυφής μνήμης). Στη συνέχεια προφιλάρουμε μερικά βήματα εκπαίδευσης, καταγράφοντας τα σχήματα των τελεστών και τη χρήση μνήμης. Επίσης, εξάγουμε το ίχνος Chrome (trace.json), το οποίο μπορεί να φορτωθεί στον περιηγητή Chrome για την ανάλυση της χρήσης GPU, τις εκκινήσεις πυρήνων, την επικάλυψη CPU–GPU και πιθανά σημεία συμφόρησης απόδοσης. Έχουμε ενεργοποιήσει record_shapes=True και profile_memory=True για να παρακολουθούμε τα σχήματα εισόδου και τις κατανομές μνήμης για τη διάγνωση σφαλμάτων OOM ή αναποτελεσματικοτήτων.
Συμβουλή: Ανοίξτε το chrome://tracing στο Google Chrome και φορτώστε το trace.json για να δείτε την προβολή της χρονογραμμής.
Για την αυτόματη ανίχνευση στενωμάτων, μπορείτε να χρησιμοποιήσετε το torch.utils.bottleneck ή το torch.profiler.schedule για να καταγράψετε στιγμιότυπα. Για παράδειγμα, την παρακολούθηση λίγων βημάτων σε κάθε εποχή χρησιμοποιώντας ένα χρονοδιάγραμμα:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import torch.profiler
# -----------------------------
# Device
# -----------------------------
device = "cuda" if torch.cuda.is_available() else "cpu"
# -----------------------------
# Model
# -----------------------------
model = nn.Sequential(
nn.Linear(100, 256),
nn.ReLU(),
nn.Linear(256, 10),
).to(device)
# -----------------------------
# Optimizer and loss
# -----------------------------
optimizer = optim.Adam(model.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
# -----------------------------
# Dataset and DataLoader
# -----------------------------
num_samples = 10_000
batch_size = 32
inputs = torch.randn(num_samples, 100)
targets = torch.randint(0, 10, (num_samples,))
dataset = TensorDataset(inputs, targets)
data_loader = DataLoader(
dataset,
batch_size=batch_size,
shuffle=True,
num_workers=2,
)
# -----------------------------
# Training step
# -----------------------------
def train_step(batch):
model.train()
inputs, targets = batch
inputs = inputs.to(device, non_blocking=True)
targets = targets.to(device, non_blocking=True)
optimizer.zero_grad(set_to_none=True)
outputs = model(inputs)
loss = loss_fn(outputs, targets)
loss.backward()
optimizer.step()
return loss
# -----------------------------
# Warm-up (outside profiler)
# -----------------------------
for i, batch in enumerate(data_loader):
train_step(batch)
if i >= 5:
break
# -----------------------------
# Profiling with schedule
# -----------------------------
with torch.profiler.profile(
activities=[
torch.profiler.ProfilerActivity.CPU,
torch.profiler.ProfilerActivity.CUDA,
],
schedule=torch.profiler.schedule(
wait=1,
warmup=1,
active=3,
repeat=2,
),
on_trace_ready=torch.profiler.tensorboard_trace_handler("./prof_log"),
record_shapes=True,
profile_memory=True,
with_stack=True,
) as prof:
for step, batch in enumerate(data_loader):
loss = train_step(batch)
prof.step()
if step >= 50:
breakΑυτό θα ξεκινήσει ασύγχρονα δύο περιόδους καταγραφής (καθεμία διάρκειας 3 βημάτων) αφού παραλειφθεί μια περίοδος αναμονής/προθέρμανσης. Η συνάρτηση ανακλησης on_trace_ready στη συνέχεια εξάγει τις καταγραφές που μπορείτε να δείτε στην καρτέλα προφίλ του TensorBoard χωρίς να σταματήσετε την εκπαίδευση. Η ασύγχρονη καταγραφή επιτρέπει την περιοδική ανάλυση (π.χ., κάθε 3 βήματα σε ένα παράθυρο 50 βημάτων) για μείωση του φόρτου ενώ εξακολουθεί να συλλέγει δεδομένα απόδοσης.
Μετά την ανάλυση προφίλ, ερμηνεύστε τα αποτελέσματα για να πάρετε μέτρα:
- Σε δέσμευση CPU (πολλά κενά όταν η GPU είναι σε αναμονή): Σκεφτείτε εάν μπορείτε να προωθήσετε περισσότερη εργασία στη GPU ή να επικαλύψετε τη φόρτωση δεδομένων με υπολογισμό χρησιμοποιώντας ασύγχρονη φόρτωση δεδομένων (π.χ. μεταβίβαση "εργαζομένων" στο DataLoader ή προεπεξεργασία στη GPU). Επίσης, ελέγξτε ότι δεν υπάρχουν βρόχοι Python που λειτουργούν κατά τη διάρκεια της εκπαίδευσης. Εάν συμβαίνει αυτό, προσπαθήστε να τα αφαιρέσετε ή να τα «μεταγλωττίσετε».
- Οι πυρήνες GPU είναι το σημεία συμφόρησης: Μπορείτε να βελτιστοποιήσετε αυτές τις λειτουργίες (συνδυασμένες λειτουργίες, χαμηλότερη ακρίβεια, κ.λπ.). Ο οδηγός απόδοσης του PyTorch συνιστά να χρησιμοποιείτε υψηλού επιπέδου λειτουργίες (όπως κλήσεις torch.nn.functional) για να επιτρέψετε στις βιβλιοθήκες να τις βελτιστοποιήσουν. Ωστόσο, αν παρατηρήσετε μια συγκεκριμένη λειτουργία που είναι κατά τάξεις μεγέθους πιο αργή, προσδιορίστε αν αυτό είναι αναμενόμενο (η φόρτωση μεγάλων FFT μπορεί να είναι αργή) ή αν υπάρχουν εναλλακτικοί τρόποι για να την υπολογίσετε.
- Πολλαπλά σχήματα που προκαλούν επαναδρομολόγηση: σκεφτείτε να ομαδοποιήσετε τις εισόδους κατά μέγεθος, ή να χρησιμοποιήσετε dynamic=True όπως αναφέρθηκε παραπάνω.
- Βεβαιωθείτε ότι δεν υπάρχουν εξωτερικά εμπόδια: Για παράδειγμα, αν χρησιμοποιείτε κατανεμημένη εκπαίδευση, επαληθεύστε ότι η επικοινωνία (all-reduce) δεν αποτελεί το εμπόδιο - αυτό θα υποδεικνυόταν από τους πυρήνες NCCL ή την CPU που περιμένει στο δίκτυο.
Λίστα Ελέγχου Προφίλ
- Ζεσταθείτε πριν από την ανάλυση προφίλ: Αποφύγετε τη μέτρηση του κόστους μεταγλώττισης ή της τεμπέλικης αρχικοποίησης.
- Καταγράψτε τόσο την CPU όσο και την GPU: Καταγράψτε τόσο την CPU όσο και την GPU (activities=[CPU, CUDA]) για να κατανοήσετε την αλληλεπίδρασή τους.
- Καταγραφή σχημάτων & μνήμης: Βοηθά στον εντοπισμό προβλημάτων που σχετίζονται με σχήματα και αιχμές στη χρήση μνήμης.
- Χρησιμοποιήστε προγραμματισμό για μεγάλες εκτελέσεις: Χρησιμοποιήστε το profiler.schedule για να καταγράψετε σύντομα χρονικά διαστήματα περιοδικά. Αυτό μειώνει το φόρτο του profiler και εξακολουθεί να παρέχει ορατότητα στη συμπεριφορά του προγράμματος.
- Ανάλυση αξιοποίησης: Βεβαιωθείτε ότι οι GPU σας χρησιμοποιούνται πλήρως. Αν όχι, διαγνώστε αν ο περιοριστικός παράγοντας είναι η CPU ή οι λειτουργίες εισόδου/εξόδου.
- Ταυτοποιήστε τις κορυφαίες λειτουργίες: Ο αναλυτής μπορεί να παρέχει μια λίστα με λειτουργίες ταξινομημένες βάσει σημαντικών μετρικών όπως ο χρόνος εκτέλεσης κάθε λειτουργίας. Προσδιορίστε ποιες λειτουργίες καταναλώνουν τον περισσότερο χρόνο και βελτιστοποιήστε τες (καλύτεροι αλγόριθμοι, αύξηση του μεγέθους του batch αν οι GPU δεν χρησιμοποιούνται πλήρως).
Με την εφαρμογή να έχει βελτιστοποιηθεί σε έναν μόνο υπολογιστή, η επόμενη πρόκληση είναι η κλιμάκωση για την αξιοποίηση πολλαπλών GPUs ή κόμβων, κάτι που εισάγει τα παραδείγματα κατανεμημένης εκπαίδευσης του PyTorch.
Κλίμακα: Κατανεμημένη Εκπαίδευση μέσω DDP ή FSDP
Όταν ο φόρτος εργασίας ή το μοντέλο σας μεγαλώσει πέρα από μια μεμονωμένη GPU, θα χρειαστεί να κλιμακώσετε την εκπαίδευση σε πολλές GPUs. Το PyTorch παρέχει δύο βασικές μεθόδους για την εκπαίδευση μοντέλων σε πολλαπλές GPUs: Distributed Data Parallel και Fully Sharded Data Parallel. Ενώ και οι δύο μέθοδοι αξιοποιούν την παραλληλία δεδομένων στο εσωτερικό τους (κάθε διαδικασία εκπαιδεύεται σε ένα διαφορετικό υποσύνολο των δεδομένων σας), έχουν διαφορετικές προσεγγίσεις στη διαχείριση των παραμέτρων του μοντέλου στη μνήμη. Θα εξερευνήσουμε πότε να επιλέξετε κάθε μέθοδο και πώς να τις διαμορφώσετε για εκπαίδευση σε έναν ή πολλούς κόμβους με το torchrun.
Κατανεμημένα Δεδομένα Παράλληλα
Το DDP διατηρεί ένα αντίγραφο ολόκληρου του μοντέλου σε κάθε GPU και συγχρονίζει τα αντίγραφα μειώνοντας τα gradients μετά από κάθε βήμα. Ιδανικό για μοντέλα που μπορούν εύκολα να χωρέσουν στη μνήμη μιας μόνο GPU, το DDP είναι εννοιολογικά απλό: Αρχικοποιήστε μια ομάδα διεργασιών και στη συνέχεια τυλίξτε το μοντέλο με torch.nn.parallel.DistributedDataParallel.
import os
import torch
import torch.nn as nn
import torch.optim as optim
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, TensorDataset
from torch.utils.data.distributed import DistributedSampler
# -----------------------------
# Setup distributed (safe)
# -----------------------------
def setup_distributed():
world_size = int(os.environ.get("WORLD_SIZE", "1"))
if world_size > 1:
dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
device = torch.device(f"cuda:{local_rank}")
distributed = True
else:
local_rank = 0
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
distributed = False
return distributed, local_rank, device
def cleanup_distributed(distributed):
if distributed:
dist.destroy_process_group()
# -----------------------------
# Main training
# -----------------------------
def main():
distributed, local_rank, device = setup_distributed()
# -----------------------------
# Model
# -----------------------------
model = nn.Sequential(
nn.Linear(100, 256),
nn.ReLU(),
nn.Linear(256, 10),
).to(device)
if distributed:
model = DDP(model, device_ids=[local_rank])
# -----------------------------
# Optimizer and loss
# -----------------------------
optimizer = optim.Adam(model.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
# -----------------------------
# Dataset
# -----------------------------
num_samples = 20_000
batch_size = 32
inputs = torch.randn(num_samples, 100)
targets = torch.randint(0, 10, (num_samples,))
dataset = TensorDataset(inputs, targets)
if distributed:
sampler = DistributedSampler(dataset, shuffle=True)
else:
sampler = None
train_loader = DataLoader(
dataset,
batch_size=batch_size,
sampler=sampler,
shuffle=(sampler is None),
num_workers=2,
pin_memory=True,
)
# -----------------------------
# Training loop
# -----------------------------
epochs = 3
for epoch in range(epochs):
if distributed:
sampler.set_epoch(epoch)
for step, (x, y) in enumerate(train_loader):
x = x.to(device, non_blocking=True)
y = y.to(device, non_blocking=True)
optimizer.zero_grad(set_to_none=True)
outputs = model(x)
loss = loss_fn(outputs, y)
loss.backward()
optimizer.step()
if step % 50 == 0 and local_rank == 0:
print(
f"[Epoch {epoch}] Step {step} | "
f"Loss {loss.item():.4f}"
)
cleanup_distributed(distributed)
# -----------------------------
# Entry point
# -----------------------------
if __name__ == "__main__":
main()Η εκκίνηση μιας εργασίας DDP γίνεται με το torchrun. Για παράδειγμα, αν θέλετε να ξεκινήσετε μια εργασία σε έναν μόνο κόμβο με 4 GPU:
torchrun --nproc_per_node=4 train.pyΗ εντολή θα δημιουργήσει 4 διαδικασίες (μία για κάθε GPU) και θα αρχικοποιήσει τις κατάλληλες μεταβλητές περιβάλλοντος (τάξη, μέγεθος κόσμου κ.λπ.). Η καθορισμός πολλαπλών κόμβων απαιτεί να περάσετε --nnodes και λεπτομέρειες δικτύου:
# On node 0:
torchrun --nnodes=2 --nproc_per_node=4 --node_rank=0 --master_addr="<IP of node0>" --master_port=12345 train.py
# On node 1:
torchrun --nnodes=2 --nproc_per_node=4 --node_rank=1 --master_addr="<IP of node0>" --master_port=12345 train.pyΠλήρως Κατανεμημένα Παράλληλα Δεδομένα (FSDP)
Το FSDP πηγαίνει ένα βήμα παραπέρα διασπώντας τις παραμέτρους του μοντέλου και τις καταστάσεις του optimizer σε διάφορες GPUs, αντί να τις αντιγράφει πλήρως σε κάθε GPU. Η χρήση του FSDP στο PyTorch 2.9 είναι απλή με τον FullyShardedDataParallel wrapper.
Βασικά σημεία
- Τυλίξτε το μοντέλο (ή τα υπομοντέλα) με FSDP πριν από τη δημιουργία του βελτιστοποιητή.
- Χρησιμοποιήστε torch.cuda.set_device(rank) και μεταφέρετε το μοντέλο στην GPU όπως με το DDP.
- Προαιρετικό: παρέχετε μια πολιτική αυτόματης κατανομής αν θέλετε να κατανεμηθούν τα τμήματα σε επίπεδο μονάδας (π.χ., να τυλίγεται ξεχωριστά κάθε μπλοκ μετασχηματιστή). Από προεπιλογή, FSDP(model) θα κατανεμηθεί ολόκληρο το μοντέλο ως μία ενιαία μονάδα κατανομής (τεχνικά μία μονάδα FSDP), κάτι που το καθιστά μη ιδανικό για πολύ βαθιά μοντέλα.
Αυτό θα κατακερματίσει αναδρομικά οποιαδήποτε υπομονάδα με περισσότερες από 100.000 παραμέτρους σε δικά της FSDP κομμάτια. Αν έχετε πολύ λίγα κομμάτια (π.χ., ολόκληρο το μοντέλο είναι ένα μόνο κομμάτι), δεν θα έχετε εξοικονόμηση μνήμης μεταξύ των επιπέδων. Ωστόσο, πάρα πολλά κομμάτια αυξάνουν το κόστος επικοινωνίας. Ας συνοψίσουμε τις διαφορές μεταξύ DDP και FSDP:
| Πτυχή | DDP (Κατανεμημένα Δεδομένα Παράλληλα) | FSDP (Πλήρως Κατακερματισμένη Παραλληλία Δεδομένων) |
|---|---|---|
| Μέθοδος εκκίνησης | Ξεκίνησε με torchrun, μία διαδικασία ανά GPU. | Ξεκίνησε επίσης με torchrun, μια διεργασία ανά GPU. |
| Αναπαραγωγή μοντέλου | Κάθε βαθμίδα κατέχει ένα πλήρες αντίγραφο του μοντέλου. | Το μοντέλο είναι κατανεμημένο σε βαθμίδες· κάθε βαθμίδα κρατά μόνο ένα υποσύνολο παραμέτρων. |
| Χρήση μνήμης ανά GPU | Υψηλό, ανάλογο με το πλήρες μέγεθος του μοντέλου. | Πολύ χαμηλότερο, περίπου μέγεθος μοντέλου διαιρεμένο με τον αριθμό των GPU. |
| Μοτίβο επικοινωνίας | Όλα τα βαθμίδια μειώνονται μετά την αντίστροφη διέλευση. | Όλες οι παράμετροι συγκεντρώνονται πριν από την πρόοδο και οι βαθμίδες μειώνονται-διασκορπίζονται μετά την οπισθοδρόμηση. |
| Απαίτηση περιτύλιξης μοντέλου | Όλοι οι βαθμοί τυλίγουν ολόκληρο το μοντέλο σε DistributedDataParallel. | Όλοι οι βαθμοί πρέπει να εφαρμόζουν ακριβώς την ίδια λογική περιτυλίγματος FSDP, ώστε κάθε βαθμός να γνωρίζει ποιο shard κατέχει. |
| Ευκολία χρήσης | Απλό στη ρύθμιση· ελάχιστες αλλαγές στον κώδικα. | Πιο πολύπλοκο· απαιτεί προσεκτικές πολιτικές περιτύλιξης και ρύθμιση του βελτιστοποιητή. |
| Κλιμακωσιμότητα | Περιορισμένο από τη μνήμη ανά GPU· δεν είναι κατάλληλο για πολύ μεγάλα μοντέλα. | Σχεδιασμένο για πολύ μεγάλα μοντέλα που δεν χωράνε σε μια μόνο GPU. |
| Τυπική περίπτωση χρήσης | Μοντέλα που χωράνε άνετα στη μνήμη GPU και χρειάζονται ταχύτερη εκπαίδευση. | Εξαιρετικά μεγάλα μοντέλα ή σενάρια όπου η αποδοτικότητα της μνήμης είναι κρίσιμη. |
Για σκοπούς παραδείγματος, μπορείτε να ξεκινήσετε με DDP. Αν αντιμετωπίσετε πρόβλημα με έλλειψη μνήμης ή χρειάζεστε μεγαλύτερα μοντέλα, σκεφτείτε να χρησιμοποιήσετε FSDP. Σημειώστε ότι το FSDP στο PyTorch 2.9 έχει εξελιχθεί· από προεπιλογή, «διασπά τα πάντα» (ZeRO Stage 3) και εκμεταλλεύεται άλλες προεπιλογές, όπως limit_all_gathers=True, για να αποτρέψει απροσδόκητες αιχμές στη μνήμη.
Λίστα Ελέγχου Κλιμάκωσης
- Εξασφαλίστε ντετερμινισμό: Βεβαιωθείτε ότι ορίζετε τον ίδιο τυχαίο σπόρο σε όλα τα επίπεδα (torch.manual_seed(seed για τυχαία μετατόπιση) εάν χρειάζεστε αναπαραγωγιμότητα).
- Χρησιμοποιήστε το DistributedSampler: Διαχωρίστε τα δεδομένα σας σε πολλαπλά ranks και καλέστε το set_epoch() σε κάθε εποχή για να τα ανακατεύει διαφορετικά κάθε εποχή.
- DDP: Οι κλίσεις προστίθενται αυτόματα σε όλες τις GPUs μετά την κλήση της backward() στα μοντέλα σας που έχουν τυλιχτεί σε DDP. Μην ξεχάσετε να αυξήσετε το learning rate ή το μέγεθος batch όταν αυξάνετε τον αριθμό των GPUs!
- FSDP: Περιτύλιξε τα modules σου πριν δημιουργήσεις τον optimizer. Πειραματίσου με το auto_wrap_policy για να αποφύγεις ένα τεράστιο shard (πιθανό αν έχεις ένα πολύ βαθύ μοντέλο). Παρακολούθησε την GPU – βεβαιώσου ότι το μέγεθος του shard ταιριάζει πραγματικά.
- Υποδομή επικοινωνίας: Για πολλαπλούς κόμβους, βεβαιωθείτε ότι έχετε ρυθμίσει δίκτυο (συνήθως NCCL). Ορίστε το περιβάλλον NCCL_P2P_LEVEL=NVL αν έχετε NVLink, κ.λπ., για βελτιστοποίηση του NCCL.
- Συσσώρευση βαθμίδων: Αν τη χρησιμοποιείτε, βεβαιωθείτε ότι με το FSDP καλείτε model.no_sync() αν παραλείπετε τον συγχρονισμό των βαθμίδων σε ορισμένες επαναλήψεις (παρόμοιο με τη χρήση του DDP).
Σημείο Ελέγχου: Ανάκτηση Εκπαίδευσης Αξιόπιστα με Κατανεμημένα Σημεία Ελέγχου
Σε μεγάλες ή εκτεταμένες περιόδους εκπαίδευσης, είναι σημαντικό να αποθηκεύετε σημεία ελέγχου (checkpoints). Μπορεί να θέλετε να συνεχίσετε την εκπαίδευση μετά από μια αποτυχία, να κάνετε fine-tune από το μέσο της εκπαίδευσης ή να ελέγξετε ενδιάμεσα αποτελέσματα. Το PyTorch 2.9 περιλαμβάνει βολικές λειτουργίες για κατανεμημένη αποθήκευση σημείων ελέγχου (DCP). Αυτό επιτρέπει στους χρήστες να αποθηκεύουν/φορτώνουν πιο αποτελεσματικά και αξιόπιστα σε σχέση με τις παραδοσιακές μεθόδους χρησιμοποιώντας torch.save. Ας συγκρίνουμε και τις δύο προσεγγίσεις πριν εξετάσουμε την προτεινόμενη μέθοδο.
Παραδοσιακό torch.save/torch.load
Η παραδοσιακή μέθοδος σε ένα σενάριο εκπαίδευσης με μία GPU ή ακόμα και DDP θα αποθηκεύει το state_dict() του μοντέλου σε ένα αρχείο (συνήθως μόνο στο rank 0, αφού όλες οι διαδικασίες θα έχουν πρόσβαση σε αυτό το αρχείο). Για παράδειγμα:
# On rank 0 only:
torch.save(model.state_dict(), "checkpoint.pt")
and to load:
model.load_state_dict(torch.load("checkpoint.pt"))Ενώ αυτό είναι αποδεκτό για μικρά μοντέλα/ρυθμίσεις όπου μια διαδικασία μπορεί να κρατήσει ολόκληρη την κατάσταση του μοντέλου στη μνήμη (συνήθως η διαδικασία με βαθμίδα 0), υπάρχουν προβλήματα με τη χρήση αυτής της προσέγγισης για κατανεμημένη εκπαίδευση όπου το μοντέλο μπορεί να διαμοιραστεί μεταξύ των διαδικασιών.
Κατανεμημένο Σημείο Ελέγχου (DCP): Το torch.distributed.checkpoint module στο PyTorch (προστέθηκε από την έκδοση v2.1+ και ωρίμασε μέχρι την 2.9) αντιμετωπίζει αυτά τα ζητήματα. Παράλληλοποιεί την αποθήκευση: κάθε βαθμίδα γράφει το κομμάτι της κατάστασης του μοντέλου της, παράγοντας πολλαπλά αρχεία (τουλάχιστον ένα ανά βαθμίδα) που αποτελούν το σημείο ελέγχου. Επιπλέον, πραγματοποιεί επανακατανομή κατά τη φόρτωση: μπορείτε να αποθηκεύσετε σε N βαθμίδες και να φορτώσετε σε M βαθμίδες, και το DCP θα συγκεντρώσει/ανακατανείμει αυτόματα όπως είναι κατάλληλο.
Το τυπικό μοτίβο χρήσης περιλαμβάνει την τοποθέτηση του μοντέλου σας (και του βελτιστοποιητή, αν χρειάζεται) σε ένα container με κατάσταση που παρέχει ένα state_dict. Αυτό μπορεί να γίνει χρησιμοποιώντας τις παρεχόμενες βοηθητικές συναρτήσεις get_state_dict και set_state_dict, οι οποίες είναι συμβατές με FSDP:
import torch.distributed.checkpoint as dcp
from torch.distributed.checkpoint.state_dict import get_state_dict, set_state_dict
from torch.distributed.checkpoint.stateful import Stateful
# Define a stateful container for model & optimizer
class AppState(Stateful):
def __init__(self, model, optimizer=None):
self.model = model
self.optimizer = optimizer
def state_dict(self):
model_sd, opt_sd = get_state_dict(self.model, self.optimizer)
return {"model": model_sd, "optim": opt_sd}
def load_state_dict(self, state):
set_state_dict(self.model, self.optimizer, state["model"], state.get("optim"))AppState εδώ υλοποιεί το interface Stateful, οπότε το DCP ξέρει πώς να παίρνει και να θέτει κατάσταση. Σημειώστε ότι το get_state_dict χρησιμοποιεί αυτόματα το κατακερματισμένο state dict του FSDP αν το μοντέλο είναι κατακερματισμένο (κάθε βαθμίδα παίρνει μόνο το κομμάτι της από το μοντέλο).
Εξοικονόμηση με DCP
Όλες οι βαθμίδες εκτελούν:
app_state = AppState(model, optimizer)
dcp.save(app_state, checkpoint_id="mycheckpoint")Αυτό θα δημιουργήσει φακέλους που περιέχουν αρχεία με πρόθεμα τη συμβολοσειρά «mycheckpoint» (mycheckpoint/mpirank_0_0.pdparams). Κάθε rank γράφει το κομμάτι του παράλληλα. Αυτό μπορεί να μειώσει δραστικά τον χρόνο σε σύγκριση με το να το κάνει ένα μόνο rank.
Φόρτωση με DCP
Για να συνεχίσετε από ένα σημείο ελέγχου, θα χρειαστείτε ένα μοντέλο με την ίδια τοπολογία (ενδεχομένως με διαφορετικό αριθμό βαθμίδων). Δημιουργήστε το μοντέλο σας (και τον βελτιστοποιητή), τοποθετήστε το σε AppState, και στη συνέχεια:
app_state = AppState(model, optimizer)
dcp.load(app_state, checkpoint_id="mycheckpoint");Δεδομένου ότι ο αριθμός των βαθμίδων διαφέρει από αυτόν που αποθηκεύσατε, το DCP θα φροντίσει για την επανακατανομή των τμημάτων εσωτερικά (για παράδειγμα, η μετάβαση από 8 σε 4 GPU σημαίνει ότι κάθε βαθμίδα θα φορτώσει δύο αποθηκευμένα τμήματα, ή αν αυξηθούν οι βαθμίδες, θα τα διανείμει ανάλογα). Αυτό το βήμα επανακατανομής κατά το χρόνο φόρτωσης αφαιρεί την ανάγκη για οποιαδήποτε χειροκίνητη μετατροπή σημείου αποθήκευσης.
Ασύγχρονη σημείωση ελέγχου
Μία από τις πιο εντυπωσιακές δυνατότητες στο PyTorch 2.9 είναι η αποθήκευση checkpoint ασύγχρονα, η οποία μπορεί να επικαλύψει τη διαδικασία εγγραφής του checkpoint με την εκπαίδευση. Χρησιμοποιώντας το dcp.async_save, το API επιστρέφει ένα μέλλον (future) και εκτελεί τις βαριές εργασίες I/O σε παρασκηνιακά νήματα. Η βασική ιδέα:
save_future = dcp.async_save(app_state, checkpoint_id="chkpt_epoch10")
# ... training can continue immediately ...
save_future.wait() # later, wait for completion (or periodically check)async_save πρώτα προετοιμάζει τα δεδομένα τοπικά στην CPU (αντιγράφει την κατάσταση του μοντέλου/του optimizer σε μνήμες pinned), και στη συνέχεια γράφει αυτές τις μνήμες στο δίσκο/δίκτυο ασύγχρονα. Οι GPU μπορούν να συνεχίσουν τη διαδικασία υπολογισμού (δηλαδή, την εκπαίδευση) ενώ το checkpoint γράφεται.
Η ασύγχρονη αποθήκευση προσωρινά απαιτεί διπλάσιες απαιτήσεις μνήμης για την αποθήκευση των καταστάσεων του μοντέλου σας. Κάθε βαθμίδα δεσμεύει έναν buffer στη CPU περίπου ίσο σε μέγεθος με το τμήμα της από το checkpoint. Έτσι, η αποθήκευση πολύ μεγάλων μοντέλων ασύγχρονα σε πολλές βαθμίδες μπορεί να αυξήσει σημαντικά τη χρήση μνήμης (RAM CPU + χρήση pinned μνήμης ~ checkpoint_size_per_rank * num_ranks, στην χειρότερη περίπτωση).
Τώρα, ας συντάξουμε μια σύγκριση των μεθόδων checkpointing με οδηγίες για το πότε να χρησιμοποιείται η κάθε μία:
| Μέθοδος Σημείου Ελέγχου | Περιγραφή | Πότε να χρησιμοποιείται |
|---|---|---|
torch.save/torch.load (κατάταξη 0) | Έλεγχος σημείου σε ένα αρχείο για μία διαδικασία. Το state_dict του μοντέλου συγκεντρώνεται (αν είναι κατακερματισμένο) και αποθηκεύεται ως ένα αρχείο, συνήθως από το rank 0. Απλό και λειτουργεί άμεσα για μοντέλα που δεν είναι κατακερματισμένα. | Χρησιμοποιείται για μικρά έως μεσαία μοντέλα ή εκπαίδευση με μία GPU. Είναι επίσης αποδεκτό για DDP εάν το μέγεθος του μοντέλου είναι μέτριο (κάθε βαθμίδα έχει ολόκληρο το μοντέλο, οπότε η βαθμίδα 0 μπορεί να το αποθηκεύσει). Δεν είναι κατάλληλο όταν το μοντέλο είναι εξαιρετικά μεγάλο ή κατανεμημένο (μπορεί να προκληθεί έλλειψη μνήμης ή να είναι πολύ αργό). |
| DCP (συγχρονισμένο) | Κατανεμημένο Σημείο Επαναφοράς (παράλληλο, συγχρονισμένο). Κάθε βαθμίδα αποθηκεύει το τμήμα της από το state_dict. Η εκπαίδευση σταματά μέχρι να ολοκληρωθεί (όλες οι βαθμίδες τελειώνουν την εγγραφή). Διαχειρίζεται FSDP, ShardedTensor, κλπ., και επιτρέπει τη φόρτωση σε διαφορετικά μεγέθη κόσμου (αυτόματη επαναδιανομή). Παράγει πολλά αρχεία (τουλάχιστον ένα ανά βαθμίδα). | Χρησιμοποιήστε για μεγάλα μοντέλα σε multi-GPU (DDP ή FSDP). Συνιστάται όταν το checkpoint ενός μόνο rank θα ήταν πολύ μεγάλο ή αργό. Εξασφαλίζει ταχύτερες αποθηκεύσεις (παράλληλο I/O) και αποφεύγει ένα μόνο σημείο αποτυχίας. Χρησιμοποιήστε συγχρονισμένη αποθήκευση αν η εκπαίδευση μπορεί να διακοπεί για ένα checkpoint (π.χ., μεταξύ εποχών) ή αν το I/O του συστήματος είναι αρκετά γρήγορο ώστε η διακοπή να είναι σύντομη. |
DCP + Async (async_save) | Κατανεμημένο ασύγχρονο checkpoint. Ίδιο με το συγχρονισμένο DCP, αλλά επιστρέφει άμεσα και εκτελεί την αποθήκευση σε νήματα στο παρασκήνιο. Η εκπαίδευση μπορεί να συνεχιστεί ενώ το checkpoint γράφεται, με κόστος επιπλέον μνήμης για τους buffers. Απαιτεί προσεκτικό χειρισμό των επικαλυπτόμενων αποθηκεύσεων (μην ξεκινάτε πολλαπλές χωρίς αναμονές, για να αποφευχθεί η υπερβολική χρήση μνήμης). | Χρησιμοποιήστε για πολύ μεγάλα μοντέλα ή όταν ο χρόνος του σημείου ελέγχου είναι σημαντικός σε σχέση με τον χρόνο εκπαίδευσης. Ιδανικό για εκπαίδευση σε παραγωγή όπου ακόμα και ένα λεπτό παύσης είναι πολύ μεγάλο—η ασύγχρονη λειτουργία κρύβει την καθυστέρηση του σημείου ελέγχου. Διασφαλίστε επαρκή RAM CPU/αγκιστρωμένη μνήμη. Δοκιμάστε στο περιβάλλον σας, καθώς η απόδοση ασύγχρονης I/O μπορεί να διαφέρει (π.χ. αργά συστήματα αρχείων μπορεί να επιβραδύνουν έμμεσα την εκπαίδευση αν ο δίσκος είναι κορεσμένος). |
Λίστα Ελέγχου Σημείων Ελέγχου
- Συμπεριλάβετε όλη την απαραίτητη κατάσταση: Βεβαιωθείτε ότι αποθηκεύετε τα βάρη του μοντέλου και την κατάσταση του optimizer (και του scheduler, της κατάστασης RNG, κ.λπ., εάν είναι απαραίτητο) για να μπορείτε να συνεχίσετε πλήρως την εκπαίδευση αργότερα. Εάν χρησιμοποιείτε τη μεθοδολογία Stateful του DCP, μπορείτε εύκολα να περάσετε τον optimizer μέσω get_state_dict(…, model, optimizer).
- Δοκιμή επαναφοράς: Μόλις αποθηκεύσετε το σημείο ελέγχου, φορτώστε το ξανά (κατά προτίμηση με τον ίδιο και διαφορετικό αριθμό GPU εάν χρησιμοποιείται κατανεμημένη εκπαίδευση) σε ένα καθαρό περιβάλλον για να βεβαιωθείτε ότι όλα επαναφέρονται όπως αναμένεται. Αυτό θα σας βοηθήσει να εντοπίσετε σφάλματα, όπως η παράλειψη αποθήκευσης κάποιου μέρους της κατάστασης εκπαίδευσης νωρίς.
- Διαχείριση αποθηκευτικού χώρου: Τα checkpoints μπορούν να γίνουν πολύ μεγάλα πολύ γρήγορα (ιδιαίτερα με διανεμημένα checkpoints με καταμερισμένα τμήματα). Εφαρμόστε κάποια στρατηγική καθαρισμού (κρατήστε τα τελευταία k checkpoints, κλπ.) για να αποφύγετε την εξάντληση του χώρου στο δίσκο.
- Χρήση ασύγχρονου: το async_save είναι μια χρήσιμη λειτουργία, αλλά πρέπει να είστε προσεκτικοί να μην υπάρχουν πολλαπλά ασύγχρονα saves σε εξέλιξη ταυτόχρονα (εκτός αν είστε πολύ προσεκτικοί με το πώς χρησιμοποιεί τη μνήμη και επιτρέπει επικαλύψεις). Τελικά, πρέπει να καλέσετε .wait() στο προκύπτον future για να εγείρετε οποιεσδήποτε εξαιρέσεις ή να επιβεβαιώσετε την επιτυχία.
- Συνέπεια: Όταν συνεχίζετε από ένα checkpoint, βεβαιωθείτε ότι όλοι οι βαθμοί καλούν την ίδια dcp.load. Αυτό είναι ιδιαίτερα σημαντικό για το FSDP, καθώς χρειάζεται να συγχρονίσει το εσωτερικό φορτίο. Επιπλέον, βεβαιωθείτε ότι το μοντέλο βρίσκεται στις αναμενόμενες συσκευές πριν από τη φόρτωση εάν χρησιμοποιείτε μεταφορά φόρτου CPU ή άλλους μηχανισμούς.
Συμπέρασμα
Ακολουθώντας αυτή τη ροή εργασίας, θα αναπτύξετε κώδικα εκπαίδευσης PyTorch που αποδίδει καλά και είναι ανθεκτικός σε αποτυχίες:
- Ξεκίνα από μια λειτουργική βάση.
- Συμπιέστε για μέγιστη ταχύτητα.
- Προφίλ για τον εντοπισμό ευκαιριών βελτιστοποίησης.
- Επεκτείνετε χρησιμοποιώντας μια κατάλληλη στρατηγική παραλληλισμού δεδομένων ή μοντέλου.
- Το σημείο ελέγχου λειτουργεί αποτελεσματικά για την αποκατάσταση μετά από αποτυχία.
Υπάρχουν διαθέσιμα εγχειρίδια παιχνιδιού για κάθε βήμα. Χρησιμοποιήστε τα επαναληπτικά (π.χ. το προφίλ ξανά αφού το βήμα 4 αλλάξει τα χαρακτηριστικά απόδοσης ή επισκεφτείτε ξανά τις ρυθμίσεις μεταγλώττισης στο βήμα 2 για να συντονιστείτε στο νέο σας περιβάλλον). Καθώς το PyTorch έχει εξελιχθεί γρήγορα, υπάρχουν δυνατότητες που βοηθούν σε κάθε βήμα. Μάθετε πότε και πώς να χρησιμοποιείτε το καθένα και θα είστε έτοιμοι να εκπαιδεύσετε μεγάλα μοντέλα σε ταχύτητα και κλίμακα, με καλές στρατηγικές ανάκτησης.
Συχνές Ερωτήσεις
Γιατί είναι τόσο σημαντικό να καθοριστεί μια βασική γραμμή πριν από τη βελτιστοποίηση;
Πρέπει να έχετε μια σωστή βασική έκδοση με μία GPU προτού ξεκινήσετε τις βελτιστοποιήσεις, ώστε να έχετε κάτι στο οποίο εμπιστεύεστε για ορθότητα και απόδοση. Διαφορετικά, οι βελτιστοποιήσεις μπορεί να κρύψουν σφάλματα που προκαλούν λανθασμένα αποτελέσματα. Μόνο με μια σωστή βασική έκδοση μπορείτε να αποδώσετε τις επιταχύνσεις ή τις υστερήσεις σε συγκεκριμένες αλλαγές κώδικα και όχι σε αινιγματικά σφάλματα ή θόρυβο μέτρησης.
Πότε πρέπει να χρησιμοποιώ το torch.compile και σε τι πρέπει να προσέχω;
Το torch.compile θα πρέπει να χρησιμοποιείται αφού έχετε ένα σταθερό βασικό επίπεδο για να επιταχύνετε την εκτέλεση σε σταθερή κατάσταση. Παρακολουθήστε τυχόν διακοπές του γράφου, κάντε προθέρμανση πριν τη μέτρηση και βεβαιωθείτε ότι τα δυναμικά σχήματα χειρίζονται σκόπιμα για να αποφύγετε υπερβολική επανασυγγραφή.
Πώς βοηθά το torch.profiler πέρα από την επιβεβαίωση υποθέσεων;
torch.profiler αποκαλύπτει τα πραγματικά σας εμπόδια - είτε πρόκειται για καθυστερήσεις της CPU, αναποτελεσματικούς πυρήνες GPU, επανακαθορισμό σχημάτων ή υπερβολικό φόρτο επικοινωνίας. Αυτό σας επιτρέπει να εστιάσετε τις προσπάθειες βελτιστοποίησης εκεί που έχουν σημασία, βάσει αποδείξεων και όχι διαίσθησης.
Πώς να επιλέξω ανάμεσα σε DDP και FSDP για κατανεμημένη εκπαίδευση;
Αν το μοντέλο σας χωράει άνετα σε κάθε GPU που θέλετε να χρησιμοποιήσετε, προτιμήστε το DDP για την απλότητά του και τη σχετική ταχύτητα. Αν περιορίζεστε από τη μνήμη της GPU ή δουλεύετε με πολύ μεγάλα μοντέλα, το FSDP θα προσφέρει αυξημένη κλιμακωσιμότητα με το κόστος της πρόσθετης πολυπλοκότητας.
Γιατί προτιμάται το Διανεμημένο Σημείο Ελέγχου σε σχέση με το torch.save σε μεγάλες εκτελέσεις;
Η κατανεμημένη αποθήκευση σημείων ελέγχου παραλληλοποιεί τις αποθηκεύσεις μεταξύ των βαθμίδων, υποστηρίζει επανακατανομή κατά τη φόρτωση και επιτρέπει ασύγχρονη αποθήκευση σημείων ελέγχου, καθιστώντας την ανάκτηση αξιόπιστη και αποτελεσματική για μεγάλα έργα εκπαίδευσης με πολλαπλές GPU ή πολλαπλούς κόμβους.
Αναφορές
- Ο απόλυτος οδηγός για τη βιβλιοθήκη PyTorch στην Python
- Κλιμάκωση Εκπαίδευσης Μοντέλων σε Πολλαπλές GPU: Αποδοτικές Στρατηγικές με PyTorch DDP και FSDP
- Πλήρως Κατανεμημένη Παράλληλη Επεξεργασία Δεδομένων
- Διανεμημένο Σημείο Ελέγχου Torch
- Οδηγός χρήσης του torch.compile
Ευχαριστούμε που μάθατε με την Κοινότητα Linux-Console.net.
