torch.OutOfMemoryError beim Laden oder Trainieren? Der Grafikspeicher muss mehr fassen als die Gewichte allein. Was wirklich hilft und was nicht.
Aufwand:
ca. 40 Min.
Voraussetzung:
Python, PyTorch mit CUDA, nvidia-smi
Gilt für:
PyTorch 2.13 mit CUDA, Nvidia-Grafikkarte
Das Symptom
Das Modell lädt, der erste Trainingsschritt läuft an – und dann bricht alles mit
torch.OutOfMemoryError: CUDA out of memory ab. Die Meldung ist ungewöhnlich
gesprächig: Sie nennt, wie viel angefordert wurde, wie viel die Karte insgesamt
hat, wie viel bereits belegt ist und wie viel PyTorch reserviert, aber nicht
benutzt hat. Beim zweiten Versuch bricht es an einer anderen Stelle ab, beim
dritten läuft es plötzlich durch. Und nvidia-smi behauptet die ganze Zeit,
die Karte sei voll, obwohl das Skript längst beendet ist.
Die „Layer 8“-Wahrheit
Der Fehler ist keine Schätzung und keine Warnung, sondern eine Tatsache: Die
Karte hat den angeforderten Block nicht mehr. Falsch ist fast immer die Rechnung
davor. Wer nur die Gewichte kalkuliert, hat den kleineren Posten kalkuliert. Die
Zwischenergebnisse jeder Schicht müssen für den Rückwärtsdurchlauf aufgehoben
werden und wachsen mit der Batchgröße; beim Training legt der Optimierer noch
seine eigenen Zustände dazu, bei Adam pro Parameter zwei weitere Werte in
Parametergröße. Der Rest ist Arithmetik statt Meinung: Ein Parameter kostet in
32 Bit vier Byte, in 16 Bit zwei, in 8 Bit eines. Sieben Milliarden Parameter in
16 Bit sind damit vierzehn Milliarden Byte, rund 13 GiB – und das ist die
Untergrenze, bevor überhaupt gerechnet wird.
Der zweite Teil der Wahrheit ist unangenehmer: Oft fehlt gar kein Speicher,
sondern ein zusammenhängender Block. PyTorch reserviert in Blöcken und gibt
sie nicht sofort zurück, deshalb zeigt nvidia-smi belegt, was intern längst
frei ist. Genau darum steht in der Fehlermeldung so oft eine große Zahl hinter
reserved but unallocated. Das ist Fragmentierung, und dagegen hilft kein
Speicherriegel: Der Arbeitsspeicher im Rechner und der Speicher auf der Karte
sind zwei getrennte Dinge, und nur eines davon lässt sich nachkaufen. Die
ehrliche Reihenfolge lautet deshalb: erst messen, dann Batchgröße senken, dann
das Zahlenformat verkleinern, dann den Zuteiler konfigurieren – und erst ganz
zum Schluss über neue Hardware nachdenken.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.
Messen, statt raten: belegt gegen reserviert
Bevor irgendetwas geändert wird, muss klar sein, wo der Speicher hingeht. memory_allocated() nennt den von Tensoren belegten Speicher, memory_reserved() den gesamten Bestand des zwischenspeichernden Zuteilers. Klaffen die beiden Werte weit auseinander, ist Fragmentierung im Spiel und nicht schlichter Platzmangel. memory_summary() gibt die Aufschlüsselung als Text aus.
import torch
print(torch.cuda.memory_allocated() / 1024**3, 'GiB in Tensoren')
print(torch.cuda.memory_reserved() / 1024**3, 'GiB vom Zuteiler gehalten')
print(torch.cuda.memory_summary())
2.
Den Bedarf der Gewichte ausrechnen, nicht schätzen
Der Speicherbedarf der Gewichte folgt direkt aus dem Zahlenformat: 32 Bit sind 4 Byte pro Parameter, 16 Bit sind 2 Byte, 8 Bit ist 1 Byte, 4 Bit sind 0,5 Byte. Für ein Modell mit 7 Milliarden Parametern in 16 Bit sind das 7e9 mal 2 Byte gleich 14 Milliarden Byte, also rund 13,0 GiB – in 32 Bit das Doppelte, in 8 Bit die Hälfte. Diese Zahl ist die Untergrenze für reines Ableiten, ohne Zwischenergebnisse.
python3 -c "p = 7e9; print([p*b/1024**3 for b in (4, 2, 1, 0.5)])"
3.
Batchgröße senken und stattdessen Gradienten aufsummieren
Die Zwischenergebnisse wachsen mit der Batchgröße, die Gewichte nicht. Halbierte Batchgröße heißt also grob halbierter Bedarf an Zwischenergebnissen – das ist der wirksamste Hebel und meist der erste, der ausprobiert werden sollte. Damit die effektive Batchgröße gleich bleibt, werden mehrere kleine Durchläufe aufsummiert, bevor der Optimierer einen Schritt macht. Die PyTorch-Dokumentation beschreibt das als effektive Batchgröße aus Batch pro Durchlauf mal Anzahl der aufsummierten Durchläufe.
akkumuliere = 4 # effektive Batchgröße = Batchgröße * 4
optimierer.zero_grad(set_to_none=True)
for i, (x, y) in enumerate(lader):
verlust = kriterium(modell(x.to(geraet)), y.to(geraet)) / akkumuliere
verlust.backward()
if (i + 1) % akkumuliere == 0:
optimierer.step()
optimierer.zero_grad(set_to_none=True)
4.
Das Zahlenformat verkleinern (gemischte Genauigkeit)
Statt alles in 32 Bit zu rechnen, wählt autocast pro Rechenoperation das passende Format. Die Dokumentation nennt als Grund, dass manche Operationen in niedrigerer Genauigkeit deutlich schneller sind, während andere den Wertebereich von 32 Bit brauchen. Bei float16 gehören kleine Gradientenwerte zu den gefährdeten: Sie fallen laut Dokumentation auf null zurück – deshalb der Skalierer, der den Verlust vor dem Rückwärtsdurchlauf hochskaliert.
Fragmentierung angehen, bevor Hardware gekauft wird
Meldet die Fehlermeldung, dass reichlich reserviert, aber wenig belegt ist, fehlt kein Speicher, sondern ein zusammenhängender Block. Das Verhalten des Zuteilers steuert laut Dokumentation die Umgebungsvariable PYTORCH_ALLOC_CONF. expandable_segments lässt Zuteilungen mitwachsen und hilft besonders bei schwankenden Batchgrößen; max_split_size_mb verhindert, dass große Blöcke zerschnitten werden, was laut Dokumentation die Fragmentierung verringern und Grenzfälle noch durchbringen kann.
Den Cache leeren – und wissen, was das nicht bringt
empty_cache() gibt laut Dokumentation ausschließlich den ungenutzten Cache frei, damit andere Anwendungen ihn bekommen. Von Tensoren belegter Speicher wird dabei nicht frei. Als Dauerlösung im Trainingsschritt ist der Aufruf deshalb nutzlos bis schädlich, weil der Zuteiler danach wieder beim Treiber anfragen muss. Sinnvoll ist er genau einmal zwischen zwei Phasen, etwa nach dem Training vor der Auswertung.
import gc
del zwischenergebnisse
gc.collect()
torch.cuda.empty_cache()
7.
Bei hartnäckigen Fällen eine Momentaufnahme aufzeichnen
PyTorch kann laut Dokumentation den Zustand des belegten CUDA-Speichers zu jedem Zeitpunkt aufzeichnen. Die entstehende Datei lässt sich in den Betrachter unter pytorch.org/memory_viz ziehen und zeigt als Zeitstrahl, welche Tensoren wann leben. Damit findet man den einen aufgehobenen Tensor, der die ganze Rechnung am Leben hält, statt weiter an der Batchgröße zu drehen.
import torch
torch.cuda.memory._record_memory_history(max_entries=100000)
# hier den Durchlauf ausführen, der scheitert
torch.cuda.memory._dump_snapshot('speicher.pickle')
torch.cuda.memory._record_memory_history(enabled=None)
Häufige Fragen (FAQ)
?Warum reicht der Grafikspeicher nicht, obwohl die Gewichte rechnerisch hineinpassen?
Weil die Gewichte nur ein Posten von mehreren sind. Dazu kommen die Zwischenergebnisse jeder Schicht, die für den Rückwärtsdurchlauf aufgehoben werden müssen und mit der Batchgröße wachsen. Beim Training kommen die Zustände des Optimierers obendrauf: Adam führt laut PyTorch-Dokumentation pro Parameter ein erstes und ein zweites Moment mit, also zwei weitere Werte in Parametergröße. Und schließlich reserviert PyTorch den Speicher nicht Byte für Byte, sondern in Blöcken über einen zwischenspeichernden Zuteiler. Wer nur die Gewichte rechnet, rechnet den kleineren Teil.
?nvidia-smi zeigt belegten Speicher, obwohl mein Skript längst nichts mehr rechnet. Ist das ein Leck?
In aller Regel nein. PyTorch gibt freigewordene Blöcke nicht sofort an den Treiber zurück, sondern behält sie für die nächste Anforderung. Die Dokumentation sagt es wörtlich: nicht genutzter, vom Zuteiler verwalteter Speicher erscheint in nvidia-smi trotzdem als belegt. Deshalb ist nvidia-smi das falsche Messgerät für PyTorch-Prozesse. Aussagekräftig sind memory_allocated() für den von Tensoren wirklich belegten Speicher und memory_reserved() für das, was der Zuteiler insgesamt hält. Die Lücke zwischen beiden Zahlen ist der Cache und oft der Grund, warum eine Anforderung scheitert, obwohl rechnerisch noch Platz wäre.
?Ist Grafikspeicher dasselbe wie Arbeitsspeicher, nur woanders?
Nein, und diese Verwechslung kostet die meisten Nachmittage. Der Arbeitsspeicher hängt an der CPU, der Grafikspeicher sitzt fest auf der Grafikkarte und lässt sich nicht aufrüsten, nicht auslagern und nicht durch mehr RAM ergänzen. 64 GB im Rechner helfen einer Karte mit 8 GB kein Stück. Ein Modell, das nicht in den Grafikspeicher passt, passt nicht – es sei denn, du verkleinerst es über das Zahlenformat oder lässt Teile auf der CPU rechnen, was dann entsprechend langsam wird.
Belege
Diese Seite ist gegen die folgenden Primärquellen geprüft – Hersteller, Standard oder Gesetzestext. Wer es nicht glaubt, muss es nicht: Da steht, wo es nachzulesen ist.