PyTorch: Expected all tensors to be on the same device
Modell auf der GPU, Daten auf der CPU: PyTorch verschiebt nichts von allein. Warum das so ist und wie beide Seiten auf dasselbe Gerät kommen.
Aufwand:
ca. 20 Min.
Voraussetzung:
Python, PyTorch mit CUDA
Gilt für:
PyTorch 2.13 mit CUDA
Das Symptom
Der Trainingslauf startet, verarbeitet den ersten Batch – und stirbt mit
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!. Auf der CPU läuft dasselbe Skript tadellos, nur
eben zwanzigmal langsamer. Manchmal reicht schon eine einzelne zusätzliche
Schicht, damit es umkippt, und manchmal taucht der Fehler erst nach zwei Epochen
auf, wenn zum ersten Mal die Auswertung dran ist.
Die „Layer 8“-Wahrheit
PyTorch ist an dieser Stelle nicht zickig, sondern ehrlich. Ein Tensor liegt
entweder im Hauptspeicher oder im Grafikspeicher, und dazwischen liegt der
PCIe-Bus. Würde die Bibliothek stillschweigend nachkopieren, bekämest du keinen
Fehler, sondern etwas viel Schlimmeres: ein Programm, das läuft und
unerklärlicherweise kriecht, weil in jeder Schleifendurchquerung Daten hin und
her geschaufelt werden. Der Abbruch ist die Alternative dazu, und sie ist die
bessere.
Die eigentliche Falle steckt in einem Detail der API, das man einmal begreifen
muss und dann nie wieder vergisst: to() bedeutet bei einem Modul etwas anderes
als bei einem Tensor. Ein nn.Module wird an Ort und Stelle verändert, seine
Parameter und Puffer wandern mit. Ein Tensor dagegen wird nicht verschoben – es
entsteht eine Kopie, und wenn du sie nicht zuweist, hast du nichts getan außer
kurz Rechenzeit verbrannt. modell.to(geraet) funktioniert also ohne Zuweisung,
x.to(geraet) nicht. Wer diese Asymmetrie kennt, hat vier von fünf Vorkommen
dieses Fehlers bereits erklärt.
Der Rest sind Tensoren, die im Modell selbst neu entstehen: Startzustände,
Masken, Indexlisten. torch.zeros(...) fragt niemanden, wo der Rest der
Rechnung wohnt, sondern legt sein Ergebnis auf dem Standardgerät ab, und das ist
die CPU. Die Gegenmaßnahme ist billig und dauerhaft: neue Tensoren nie ins Leere
erzeugen, sondern immer mit device= und dtype= an einen Tensor hängen, der
schon am richtigen Ort liegt. Dann läuft derselbe Code auf der Karte und auf der
CPU – und du musst nie wieder raten, welche der dreihundert Zeilen die eine
falsche war.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.
Die Meldung zu Ende lesen
Der Text nennt beide Geräte, meist in der Form, dass mindestens zwei Werte gefunden wurden – etwa cuda:0 und cpu. Damit steht schon fest, ob ein CPU-Tensor in die GPU-Rechnung geraten ist oder umgekehrt. Der darunterliegende Aufruf im Traceback nennt die Schicht, in der es passiert; das ist der Ort, an dem gesucht wird, nicht zwangsläufig der Ort, an dem der Fehler gemacht wurde.
2.
Ein einziges Gerät festlegen und überall benutzen
Die Dokumentation empfiehlt geräteunabhängigen Code: einmal ermitteln, ob die GPU benutzt werden soll, das Ergebnis in einem torch.device-Objekt festhalten und ausschließlich damit arbeiten. Verstreute Aufrufe von .cuda() an zehn Stellen sind der sichere Weg in genau diesen Fehler.
import torch
geraet = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
3.
Modell verschieben – und den Rückgabewert der Tensoren beachten
Beim Modul reicht der Aufruf, weil er an Ort und Stelle wirkt. Beim Tensor muss das Ergebnis zugewiesen werden, sonst passiert nichts. Wer das Modell auf der GPU optimieren will, verschiebt es laut Dokumentation vor dem Anlegen des Optimierers, weil die Verschiebung neue Parameterobjekte erzeugt.
modell.to(geraet) # wirkt an Ort und Stelle
optimierer = torch.optim.Adam(modell.parameters(), lr=1e-4)
eingabe = eingabe.to(geraet) # liefert einen neuen Tensor zurück
ziel = ziel.to(geraet)
4.
Neu erzeugte Tensoren am Bezugstensor ausrichten
Alles, was innerhalb von forward entsteht, bekommt Gerät und Datentyp von einem Tensor, der bereits richtig liegt. Dann bleibt der Code auch dann korrekt, wenn dasselbe Modell einmal auf der CPU und einmal auf der GPU läuft.
# bricht auf der GPU ab: landet auf der CPU
h0 = torch.zeros(1, x.size(0), 128)
# richtig: erbt Gerät und Datentyp vom Eingabetensor
h0 = torch.zeros(1, x.size(0), 128, device=x.device, dtype=x.dtype)
5.
Nachsehen, wo Parameter und Puffer wirklich liegen
Puffer sind der häufigste blinde Fleck: Sie werden mit register_buffer angelegt, wandern mit dem Modul mit – aber nur, wenn sie auch wirklich registriert wurden. Ein einfaches Attribut mit einem Tensor darin wird von to() nicht angefasst und bleibt auf der CPU zurück. Die Ausgabe zeigt das in zwei Zeilen.
for name, p in modell.named_parameters():
print('Parameter', name, p.device)
for name, b in modell.named_buffers():
print('Puffer', name, b.device)
6.
Die Daten aus dem DataLoader im Schleifenkopf verschieben
Ein DataLoader liefert CPU-Tensoren. Wer das Verschieben irgendwo im Modell nachholt, verteilt die Verantwortung über die halbe Datei. Sauberer ist genau eine Stelle: der Kopf der Trainingsschleife. Alles danach darf sich darauf verlassen, dass die Daten am richtigen Ort liegen.
for x, y in lader:
x, y = x.to(geraet), y.to(geraet)
verlust = kriterium(modell(x), y)
verlust.backward()
optimierer.step()
optimierer.zero_grad(set_to_none=True)
7.
Bei mehreren Grafikkarten die Grenze respektieren
Liegen zwei Tensoren auf cuda:0 und cuda:1, hilft kein Umstellen der Reihenfolge. Operationen über Kartengrenzen hinweg sind laut Dokumentation standardmäßig nicht erlaubt; erlaubt sind nur kopierende Methoden. Ein Tensor muss also ausdrücklich auf die Karte des anderen kopiert werden, bevor gerechnet werden kann.
a = torch.randn(4, 4, device='cuda:0')
b = torch.randn(4, 4, device='cuda:1')
c = a + b.to('cuda:0') # ausdrückliche Kopie statt stiller Annahme
Häufige Fragen (FAQ)
?Warum schiebt PyTorch die Daten nicht einfach selbst auf die Grafikkarte?
Weil ein stilles Kopieren teuer und unsichtbar wäre. Jeder Wechsel zwischen Hauptspeicher und Grafikspeicher läuft über den PCIe-Bus, der um Größenordnungen langsamer ist als der Speicher auf der Karte selbst. Würde PyTorch das automatisch nachholen, hätte ein Trainingsschritt in einer Schleife plötzlich Millionen versteckter Kopien, und niemand fände heraus, warum die Karte langweilt. PyTorch geht deshalb den anderen Weg und bricht ab. Passend dazu sind laut Dokumentation auch Operationen über mehrere Grafikkarten hinweg standardmäßig verboten – ausgenommen sind ausdrücklich kopierende Methoden wie copy_(), to() und cuda().
?Ich habe doch modell.to(geraet) geschrieben. Warum kommt der Fehler trotzdem?
Weil to() bei Modulen und bei Tensoren zwei verschiedene Dinge tut. Für ein nn.Module sagt die Dokumentation ausdrücklich, dass die Methode das Modul an Ort und Stelle verändert und Parameter sowie Puffer verschiebt – der Rückgabewert ist Beiwerk. Für einen Tensor gilt das Gegenteil: Passen Datentyp und Gerät nicht, ist das Ergebnis laut Dokumentation eine Kopie mit den gewünschten Eigenschaften. Eine Zeile wie x.to(geraet) ohne Zuweisung ändert an x also nichts. Genau diese Asymmetrie erzeugt den Fehler in neun von zehn Fällen.
?Der Fehler kommt aus einer Schicht, in der ich gar nichts verschiebe. Wie kann das sein?
Weil dort vermutlich ein Tensor neu erzeugt wird – ein Startzustand, eine Maske, ein Positionsindex, ein Puffer aus torch.zeros oder torch.arange. Solche Aufrufe legen den Tensor auf dem Standardgerät an, und das ist ohne weiteres Zutun die CPU. Sobald er auf ein Zwischenergebnis der GPU trifft, knallt es. Die PyTorch-Dokumentation behandelt das unter dem Stichwort geräteunabhängiger Code und nennt als Beispiel genau diesen Fall: den Startzustand eines rekurrenten Netzes. Die Lösung ist, neue Tensoren immer an einem vorhandenen Tensor auszurichten, statt sie ins Leere zu erzeugen.
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.
Python meldet ModuleNotFoundError, obwohl das Paket installiert ist? Meist stimmt die Umgebung, der Import-Name oder der Dateiname nicht. So löst du es.
SWAnfängerfehlerTabError: inconsistent use of tabs and spaces in indentation