Falsche Dimensionen, Broadcasting-Fallen, Memory-Overflows – und wie man sie vermeidet
SerieTensoren verstehen
Teil 7 von 8
Tensoren sind mächtig – und fehleranfällig. Die ersten Teile der Serie haben erklärt, was Tensoren sind, wie sie durch Netze fließen und welche Operationen sie antreiben. Dieser Teil blickt auf die Praxis: Welche Fehler passieren beim Arbeiten mit Tensoren immer wieder, woran erkennt man sie, und wie lassen sie sich systematisch vermeiden?
Tensor-Fehler sind selten spektakulär. Sie crashen nicht immer sofort – oft laufen Modelle einfach weiter, lernen „irgendetwas” und liefern am Ende unbrauchbare Ergebnisse. Genau das macht sie gefährlich: Sie sind leise. Ein Traceback ist ein Geschenk. Die eigentliche Debugging-Arbeit beginnt, wenn das Training durchläuft, die Loss aber keinen Sinn ergibt.
Die meisten Tensor-Fehler sind keine exotischen Spezialfälle. Sie entstehen aus einer Handvoll wiederkehrender Muster – falsche Shapes, übersehene Broadcasting-Regeln, vergessene Devices, implizite Type-Casts. Wer diese Muster kennt, spart in jedem Projekt Stunden Debugging.
Shape-Mismatches: der häufigste Fehler
Die Mehrheit aller Tensor-Fehler hat eine einzige Ursache: Der Shape passt nicht zu dem, was die nächste Operation erwartet. Typische Fehlermeldung:
RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x784 and 10x128)
Der Grund ist meistens profan: Eine Achse wurde vergessen, ein Reshape nicht gemacht, ein Batch falsch gestapelt. Drei Muster dominieren:
Batch-Achse übersehen. Ein Modell erwartet (batch, features), bekommt aber (features,). Lösung: tensor.unsqueeze(0) fügt die Batch-Dimension hinzu.
Channels in falscher Position. PyTorch erwartet bei Bildern (batch, channels, height, width), viele Bibliotheken liefern (batch, height, width, channels). Lösung: tensor.permute(0, 3, 1, 2) dreht die Achsen.
Flatten vergessen. Zwischen Convolution und Linear Layer fehlt oft der Schritt, den 4D-Tensor in 2D zu bringen. Lösung: tensor.view(tensor.size(0), -1) oder torch.flatten(tensor, start_dim=1).
Für produktiven Code reicht print nicht. Dort lohnen sich Assertions, die Invarianten erzwingen statt nur zu dokumentieren:
assert x.ndim == 2, f"Expected 2D tensor, got {x.shape}"
assert x.shape[1] == 784, f"Expected 784 features, got {x.shape[1]}"
Wer häufig Shapes prüft, schreibt sich das einmal aus:
def check_shape(x, expected):
assert x.shape == expected, f"{x.shape} != {expected}"
Der Unterschied ist klein, aber spürbar: print hilft beim Debuggen, assert verhindert den Bug im nächsten Durchlauf. In Layer-Forwards eingebaut werden aus Shape-Annahmen explizite Verträge.
Broadcasting: wenn es schweigend falsch rechnet
Broadcasting ist ein Segen und ein Fluch zugleich. Die Regel: Wenn zwei Tensoren unterschiedliche Shapes haben, werden fehlende Dimensionen implizit aufgefüllt. Das vereinfacht viele Operationen – und versteckt andere Fehler.
Typisches Beispiel:
x = torch.randn(32, 10) # (batch=32, features=10)
bias = torch.randn(10) # (features=10,)
y = x + bias # Broadcasting: bias wird auf (32, 10) erweitert
Das funktioniert und ist gewünscht. Gefährlich wird es, wenn die Shapes zufällig kompatibel sind, aber semantisch nicht zusammengehören:
a = torch.randn(3, 1)
b = torch.randn(1, 4)
c = a + b # Ergebnis: (3, 4) – das war vielleicht nicht beabsichtigt
Broadcasting wirft keinen Fehler, solange die Shapes regelkonform sind. Das Modell trainiert, die Loss sinkt, das Ergebnis ist Müll.
Die wirkungsvollste Absicherung ist einsum. Statt auf implizite Broadcasting-Regeln zu vertrauen, werden die Achsen explizit benannt:
# explizit statt implizit
y = torch.einsum("bf,f->bf", x, bias)
Der Ausdruck liest sich wie ein Vertrag: x hat die Achsen Batch und Features, bias hat Features, das Ergebnis hat wieder Batch und Features. Wer das nicht sofort formulieren kann, hat auch keine klare Vorstellung davon, was das Broadcasting gerade tut. einsum zwingt zu dieser Klarheit und eliminiert eine ganze Klasse von stillen Broadcasting-Fehlern.
Axis-Verwechslung: welche Dimension war das noch?
Operationen wie sum, mean, softmax oder argmax arbeiten entlang einer Achse. Wird die falsche gewählt, sieht das Ergebnis oft plausibel aus – bis die Metriken nicht mehr stimmen.
logits = torch.randn(32, 10) # (batch, classes)
probs_wrong = torch.softmax(logits, dim=0) # Softmax über den Batch – falsch
probs_right = torch.softmax(logits, dim=1) # Softmax über die Klassen – richtig
Das Netz trainiert trotzdem. Die Loss bewegt sich. Erst beim Blick auf die Confidence-Werte fällt auf, dass etwas nicht stimmt. Der präventive Reflex: Bei jeder Reduktion oder Normalisierung die Achsen-Semantik explizit im Kommentar festhalten – nicht die Zahl, sondern die Bedeutung („über die Klassen”, „über den Batch”).
Noch besser: Die Achsen als benannte Konstanten führen.
BATCH = 0
CLASSES = 1
torch.softmax(logits, dim=CLASSES)
Das macht aus dim=1 einen semantischen Ausdruck und lenkt den Refactoring-Reflex auf die richtige Frage („welche Achse meine ich hier?”). PyTorch bringt mit Named Tensors auch ein experimentelles Feature in diese Richtung mit – konzeptionell der richtige Weg, praktisch aber noch selten produktiv im Einsatz.
Device-Mismatch: CPU und GPU mischen
Sobald GPUs im Spiel sind, tritt ein Fehlertyp auf, den es auf CPU nicht gibt:
RuntimeError: Expected all tensors to be on the same device,
but found at least two devices, cuda:0 and cpu!
Ursachen:
- Das Modell wurde auf die GPU geschoben, die Inputs nicht.
- Ein Zwischenergebnis wurde versehentlich auf die CPU gecastet (oft durch NumPy-Interop).
- Zwei Modelle auf unterschiedlichen GPUs kommunizieren miteinander.
Die saubere Lösung ist eine Device-Variable am Anfang des Scripts und konsequentes .to(device) für jeden Tensor, der in das Modell fließt:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
x = x.to(device)
y = y.to(device)
Wer mit Lightning, Hugging Face Accelerate oder ähnlichen Wrappern arbeitet, bekommt das Device-Handling meist geschenkt. In rohem PyTorch ist Disziplin gefragt.
Ein besonders häufiger Klassiker ist der NumPy-Umweg:
x = x.cpu().numpy()
# ... irgendetwas zwischendurch ...
x = torch.tensor(x) # landet wieder auf CPU
Jeder Sprung über NumPy verliert die Device-Information. Der Tensor kommt auf der CPU zurück, selbst wenn der gesamte Rest des Modells auf der GPU läuft. Wenn NumPy wirklich nötig ist – für Visualisierung oder externe Bibliotheken –, lohnt es sich, die Stelle explizit zu markieren und den Tensor danach wieder auf das richtige Device zu schieben.
Memory-Overflows: wenn der Speicher ausgeht
Die klassische Out-of-Memory-Meldung auf der GPU:
RuntimeError: CUDA out of memory. Tried to allocate 2.50 GiB
Drei Ursachen dominieren:
Batch-Size zu groß. Die einfachste, aber nicht immer die beste Lösung: Batch verkleinern. Alternativ Gradient Accumulation verwenden, um die effektive Batch-Size zu erhalten.
Autograd-Graph zu groß. Jede Operation im Training hält Zwischenwerte für den Backward-Pass vor. Lange Sequenzen oder tiefe Netze sprengen den Speicher. Gegenmittel: Gradient Checkpointing – es spart Speicher auf Kosten zusätzlicher Rechenzeit.
Leaks durch Referenzen. Tensoren, die in Python-Listen oder außerhalb der Train-Loop gehalten werden, werden nicht freigegeben. Typischer Fehler: losses.append(loss) statt losses.append(loss.item()). Das erste hält den gesamten Graphen im Speicher, das zweite nur den skalaren Wert.
torch.no_grad() vergessen. In der Inference baut PyTorch standardmäßig trotzdem den Autograd-Graphen auf – unnötiger Speicherverbrauch inklusive. Der richtige Reflex für jeden Inferenz-Codepfad:
with torch.no_grad():
y = model(x)
Das gilt nicht nur für Produktions-Inferenz, sondern auch für Validation-Loops, Metric-Berechnungen und jede Stelle, an der kein Backward-Pass folgt. Alternativ der Dekorator @torch.inference_mode() für ganze Funktionen – er ist noch etwas strenger als no_grad und eignet sich als Default für reine Inferenz-Code.
Dtype-Fehler: Precision als stille Falle
Tensoren haben einen Datentyp: float32, float16, bfloat16, int64, bool. Wer die Typen mischt, erlebt Überraschungen.
x = torch.tensor([1, 2, 3]) # int64
y = torch.tensor([1.5, 2.5, 3.5]) # float32
z = x + y # Ergebnis: float32
Das funktioniert – aber:
- Indizes müssen
int64sein, nichtfloat. - Loss-Funktionen erwarten oft
float32, nichtfloat16. - Mixed Precision Training funktioniert nur mit korrektem
autocast-Kontext, sonst kommt es zu NaN-Werten. - Vergleichsoperationen liefern
bool-Tensoren, Arithmetik mitboolkastet zuint.
Bei Mixed Precision (AMP) und bei Inference mit quantisierten Modellen lohnt ein expliziter Blick auf .dtype. Unerklärliche NaN-Werte im Training sind oft Precision-Probleme, keine Modellfehler.
NaN und Inf systematisch finden
NaN-Werte verschwinden nicht von allein. Sie propagieren durch das Netz und kontaminieren Gradienten, Gewichte und Metriken. Eine schnelle Prüfung an verdächtigen Stellen:
assert not torch.isnan(x).any(), "NaN im Tensor"
assert not torch.isinf(x).any(), "Inf im Tensor"
Wenn der Ursprung des NaN unklar ist, hilft der Autograd-Anomaly-Detector: Er markiert die Operation, die den NaN verursacht hat, statt erst beim letzten Schritt zu crashen.
torch.autograd.set_detect_anomaly(True)
Das kostet Performance und gehört nicht in produktiven Code, macht aber bei der Fehlersuche oft den Unterschied zwischen „irgendwo wird es NaN” und „Zeile 42 der Forward-Funktion”. Im Alltag einschalten, solange gesucht wird, und danach wieder deaktivieren.
In-place-Operationen und Autograd
PyTorch bietet für viele Operationen eine In-place-Variante mit Unterstrich-Suffix: x.add_(1), x.relu_(). Sie sparen Speicher, können aber Autograd brechen:
RuntimeError: one of the variables needed for gradient computation
has been modified by an inplace operation
Der Grund: Der Backward-Pass braucht die ursprünglichen Werte. Wurde der Tensor überschrieben, kann der Gradient nicht mehr berechnet werden.
Ein einfacher Entscheidungsrahmen hilft, den Reflex zu kalibrieren:
- Training: In-place-Operationen meiden. Das Risiko eines gebrochenen Backward-Pass überwiegt den kleinen Speichergewinn fast immer.
- Inferenz: In-place ist unkritisch, weil kein Gradient berechnet wird.
- Gezielte Memory-Optimierung: Nur bewusst einsetzen, wenn Profiling zeigt, dass genau diese Stelle der Engpass ist.
Bei modernen Architekturen wie Transformers bringen In-place-Optimierungen meist ohnehin kaum messbaren Benefit. Der häufigste Gewinn kommt aus Gradient Checkpointing und Mixed Precision – nicht aus dem Unterstrich-Suffix.
Performance-Fallen
Nicht jeder Fehler wirft einen Traceback. Manche äußern sich nur als schleichende Verlangsamung:
Non-contiguous Tensoren. Nach transpose oder permute liegt der Tensor nicht mehr zusammenhängend im Speicher. Folge-Operationen werden langsamer. Lösung: .contiguous() nach dem Umformen.
Implizite CPU-GPU-Synchronisation. tensor.item(), print(tensor) oder .cpu() mitten in der Train-Loop zwingen die GPU zum Warten. Debug-Ausgaben daher aus den heißen Pfaden verbannen.
Unnötiges NumPy-Interop. Jeder Umweg über NumPy kostet eine Kopie und oft einen Device-Wechsel. In der Train-Loop grundsätzlich in Tensoren bleiben.
Falscher DataLoader-Parallelismus. Zu wenige num_workers lassen die GPU leer laufen, zu viele erzeugen Memory-Pressure. Der Sweet Spot ist systemabhängig und sollte gemessen werden.
Fehlendes .detach() bei Zwischenwerten. Wer Tensoren für Logging, Caching oder Zwischenberechnungen behält, zieht den Computation Graph mit – oft unbewusst.
y = x.detach()
.detach() trennt den Tensor vom Autograd-Graphen. Danach existiert nur noch der reine Zahlenwert, nicht mehr die Historie. Immer dann einsetzen, wenn der Tensor nicht mehr gradientfähig sein muss – für Metriken, für Visualisierung, für Exports.
Ein Debugging-Reflex für Tensor-Fehler
Die meisten Tensor-Fehler lassen sich mit einer kleinen Checkliste schnell eingrenzen:
- Shape, Dtype, Device, Wertebereich prüfen: Eine einzige Zeile deckt fast alle Basisfragen ab.
Shape und Dtype zeigen strukturelle Probleme, Device zeigt Pipeline-Brüche,print(x.shape, x.dtype, x.device, x.min().item(), x.max().item())min/maxzeigen NaN, Inf oder Exploding Values sofort. - NaN und Inf prüfen:
torch.isnan(x).any()undtorch.isinf(x).any()bei unerklärlicher Loss-Divergenz. - Memory prüfen:
torch.cuda.memory_summary()bei Out-of-Memory-Fehlern. - Autograd-Anomalien aufspüren:
torch.autograd.set_detect_anomaly(True)bei NaN-Propagation. - Einzeln ausführen: Komplexe Pipelines Schritt für Schritt durchlaufen, jede Zwischenstufe verifizieren.
Diese fünf Checks fangen den Großteil aller Fehler ab. Der Rest – Numerical Instabilities, Gradient Vanishing, subtile Broadcasting-Effekte – braucht tieferes Verständnis. Genau das war das Ziel der vorherigen Teile dieser Serie.
Fehler isolieren: Minimal Reproduction
Der schnellste Weg zur Lösung führt selten über mehr Logs, sondern über weniger Code. Ein Fehler, der in einer komplexen Train-Loop mit Distributed Training, Mixed Precision und Custom Sampler auftritt, ist fast nie dort tatsächlich lokalisiert. Er zeigt sich nur dort.
Die Minimal-Repro-Technik dreht das Problem um: Statt alles zu behalten, wird alles entfernt, bis nur noch der Kern übrig bleibt.
# statt komplettes Training
x = torch.randn(32, 784)
model = MyModel()
y = model(x) # isoliert testen
Der Prozess:
- Eine Instanz des verdächtigen Modells laden.
- Einen Dummy-Input mit realistischem Shape erzeugen.
- Nur den Forward-Pass ausführen, ohne Daten, ohne Optimizer, ohne Trainer.
- Wenn der Fehler verschwindet, schrittweise wieder zubauen, bis er zurückkommt.
Wenige Entwicklungsschritte sparen hier oft Stunden. Und was als Nebeneffekt entsteht – ein 10-Zeilen-Repro – ist gleichzeitig die beste Form, um einen Bug zu melden, zu archivieren oder in einem Regression-Test festzuhalten.
Tooling für den produktiven Alltag
Für größere Projekte lohnt es sich, über die rohen PyTorch-APIs hinaus zu gehen. Ein kurzer Überblick über Werkzeuge, die wiederkehrende Fehlerklassen wegabstrahieren:
- PyTorch Lightning / Hugging Face Accelerate – reduzieren Device-Handling, AMP-Setup und Distributed Training auf Standard-Patterns. Viele der oben beschriebenen Fehler treten im Lightning-Stack schlicht nicht mehr auf.
- torchviz – visualisiert den Autograd-Graph. Sinnvoll bei komplexen Custom-Layern, um zu sehen, wo Gradienten tatsächlich fließen.
- TensorBoard / Weights & Biases – zeigen den Werteverlauf über das Training hinweg. NaN-Spitzen oder Gradient Explosions werden dort sichtbar, lange bevor die Metriken kippen.
- torch.compile – macht Modelle schneller, bringt aber eigene Fehlerklassen mit (Fallback-Pfade, Graph-Breaks). Vor dem Produktivumstieg auf einer Kopie des Modells testen.
Keines dieser Werkzeuge ersetzt das Verständnis für die zugrundeliegenden Fehler – aber sie reduzieren, wie oft man mit ihnen überhaupt in Kontakt kommt.
Einordnung
Tensor-Fehler sind selten Logikfehler im eigentlichen Modell. Sie entstehen an den Grenzen: zwischen Datenvorverarbeitung und Modell, zwischen CPU und GPU, zwischen NumPy und PyTorch, zwischen Training und Inferenz. Wer an diesen Übergängen diszipliniert Shapes, Dtypes und Devices prüft, erspart sich die Mehrheit der schwer auffindbaren Probleme.
Wer Tensoren debuggen kann, debuggt am Ende seltener noch „Modelle” – sondern versteht, wo im Datenfluss etwas schiefläuft. Das ist der Unterschied zwischen „es läuft irgendwie” und „es ist stabil”. Die fortgeschrittenen Werkzeuge – Profiler, Memory-Tracing, Gradient-Hooks – sind dann nur noch für den Rest, der sich der Checkliste entzieht.