Technik
Der Loader, der abstürzt, statt zu scheitern
MLX-Gewichte auf dem GPU-Stream zu laden erzeugt keinen Fehler. Es liefert einen Rückgabewert ungleich null, den Sie vermutlich nicht geprüft haben, und danach bringt das Lesen der Daten den Prozess um. Hier steht, warum, und was man dagegen tut.
Aktualisiert 2026-09-12Gemessen 2026-09-06
Wenn Sie MLX aus C heraus aufrufen — über mlx-c oder über irgendeine darauf gebaute Anbindung —
und Ihr Programm beim ersten Lesen eines Gewichts mit einem Speicherzugriffsfehler stirbt, dann
ist das vermutlich der Grund.
Safetensors laden nur auf dem CPU-Stream. mlx_load auf dem voreingestellten GPU-Stream
erzeugt nichts, was Ihnen auffallen würde. mlx_array_eval liefert 1 zurück, und wenn Sie diesen
Rückgabewert nicht geprüft haben, bleiben die Arrays unausgewertet. Sie danach zu lesen liest
Speicher, der nie gefüllt wurde, und der Prozess stirbt irgendwo, das mit dem Laden nichts zu tun
hat.
Die Behebung ist eine Zeile: Übergeben Sie mlx_default_cpu_stream_new() an das Laden, sonst
nichts.
Warum das verwirrend ist
Weil das naheliegende Denkmodell auf vernünftige Weise falsch ist. MLX läuft auf der GPU, Sie wollen die Gewichte auf der GPU, also laden Sie auf dem GPU-Stream. Diese Begründung ist in Ordnung, und die Schlussfolgerung ist es nicht.
Laden ist Datei-Ein-/Ausgabe und Speicherplatzierung, und auf Apple Silicon ist der Speicher ohnehin geteilt — es gibt keine Übertragung zu organisieren. Auf dem CPU-Stream zu laden lässt Ihre Gewichte nicht am falschen Ort liegen. Die Berechnung findet weiterhin auf der GPU statt. Nur das Laden ändert sich.
Wenn man es weiß, ist es offensichtlich. Bevor man es weiß, sucht man einen Speicherfehler im eigenen Code, denn so sieht der eigene kaputte Zeiger nun einmal aus.
Der Teil, der aus einem Fehler ein Rätsel macht
mlx_array_eval liefert einen Statuscode. Ihn zu ignorieren ist das Naheliegende, weil er meistens
null ist und jede Prüfung nach Rauschen aussieht.
Ignorieren Sie ihn nicht. Ein unausgewertetes Array kündigt sich nicht an. Es liest sich als Nullen, lautlos, ohne Fehlermeldung irgendwo. Und sobald Nullen durch einen Transformer fließen, lautet das Symptom «das Modell erzeugt Nullen» oder «das Modell gibt Müll aus» — was Sie zum Sampling-Code schickt, oder zum Tokenizer, oder zur Architektur. Überallhin außer zum Loader.
Das ist die allgemeine Form dieser ganzen Fehlerklasse, und sie gehört für sich gesagt:
Ein Fehlschlag, der einen plausiblen Wert zurückgibt, ist teurer als einer, der abstürzt, weil der Absturz Ihnen sagt, wo Sie hinsehen müssen.
Also: Prüfen Sie bei jedem eval den Rückgabewert. Das kostet je eine Zeile und verwandelt eine
Klasse von Rätseln in eine Klasse von Fehlermeldungen.
Wie man es reproduziert
Laden Sie irgendeine MLX-Safetensors-Datei mit dem voreingestellten Stream, werten Sie aus, und lesen Sie das erste Element.
// Die Fassung, die abstürzt
mlx_array w;
mlx_load(&w, "model.safetensors", /* stream */ mlx_default_gpu_stream_new());
mlx_array_eval(&w); // liefert 1 — und niemand sieht hin
const float* p;
mlx_array_data_float32(&p, w);
printf("%f\n", p[0]); // Speicherzugriffsfehler, hier
// Die Fassung, die funktioniert
mlx_stream s = mlx_default_cpu_stream_new();
mlx_load(&w, "model.safetensors", s);
if (mlx_array_eval(&w) != 0) { /* jetzt erfahren Sie es */ }
Der Absturzort ist das Lesen und nicht das Laden, und genau das ist das ganze Problem: Ihr Debugger zeigt auf eine Zeile, die richtig ist.
Wie es aus Rust heraus aussieht
Dasselbe, eine Schicht höher, und die Ergonomie macht es leichter, es falsch zu machen: Ein
Wrapper, der aus dem Laden ein Result zurückgibt und das eval verschluckt, sieht aus, als
behandle er Fehler ordentlich.
Das Muster, das hält, besteht darin, die Auswertung überall als fehlbare Operation zu behandeln und nicht nur dort, wo ein Fehlschlag wahrscheinlich wirkt. In der Praxis heißt das: ein kleiner Helfer, der auswertet und den Status in Ihren Fehlertyp umwandelt, und die rohe Funktion niemals direkt aufrufen. Das ist kein interessanter Code. Es ist der Code, der aus einem Segfault einen Satz macht.
Warum der Ladepfad überhaupt anders ist
Etwas informierte Spekulation, denn dokumentiert ist das so nicht: Laden muss Bytes aus einer Datei in den Speicher bringen, und der Metal-Pfad erwartet, fertige Puffer gereicht zu bekommen, statt sie aus einem Dateideskriptor zu füllen. Was auch immer der interne Grund ist — das beobachtbare Verhalten ist stabil und der Umweg trivial.
Praktisch zählt vor allem, dass dies keine Leistungsentscheidung ist, die Sie treffen. Sie entscheiden sich nicht dafür, langsam auf der CPU zu laden. Sie entscheiden sich für den einzigen Weg, der funktioniert, und die anschließende Berechnung bleibt davon unberührt.
Wie lange das gedauert hat, und wie die Suche aussah
Zwei Tage, und fast alles davon an den falschen Stellen. Der Weg gehört festgehalten, weil die falschen Stellen keine dummen waren.
Das Symptom war Müll in der Ausgabe. Der erste Verdächtige war also das Sampling — ein Abweichung um eins in der Top-k-Auswahl, eine doppelt angewandte Temperatur, ein Startwert, der etwas Unerwartetes tut. Im Sampling wohnt der Zufall, und Zufall ist, wonach Müll aussieht. Zwei Stunden, um das auszuschließen, indem der Startwert festgesetzt wurde und derselbe Müll identisch zurückkam — was das nützliche negative Ergebnis ist: deterministischer Müll ist kein Sampling-Problem.
Dann der Tokenizer, weil Müll, der sich aus gültigen Token-Kennungen dekodieren lässt, anders aussieht als Müll, der das nicht tut, und dies sah nach dem Zweiten aus. Ein paar weitere Stunden, um zu bestätigen, dass der Tokenizer für sich genommen sauber hin und zurück arbeitet.
Dann die Architektur, wo eine Portierung tatsächlich am ehesten falsch ist: eine transponierte Matrix, eine fehlende Normalisierung, eine rotierende Einbettung an der falschen Stelle. Das ist der teure Ausschluss, denn ihn zu prüfen heißt, Zwischentensoren Schicht für Schicht gegen eine Referenz zu vergleichen.
Und während genau dieses schichtweisen Vergleichs tauchte die eigentliche Antwort seitlich auf: Der erste verglichene Tensor war bereits falsch, bevor irgendeine Berechnung stattgefunden hatte. Die Gewichte selbst waren Nullen.
Die allgemeine Lehre ist die, die sich immer wieder auszahlt:
Vergleichen Sie an der frühestmöglichen Stelle gegen eine Referenz, nicht an der Stelle, an der das Symptom auftaucht.
Das Symptom trat an der Ausgabe auf, also begann die Suche dort. Der Defekt saß an der Eingabe, und eine halbe Stunde Vergleich der geladenen Gewichte gegen die Referenz hätte ihn am ersten Tag gefunden.
Die verwandte Falle: Shards werden entdeckt, nicht benannt
Wo Sie schon im Loader sind, lohnt sich auch der Nachbarfehler zu vermeiden.
Die Gewichte eines Modells können in einer Datei oder in mehreren liegen, und die Zahl richtet
sich nach dem Modell und nicht nach einer Konvention: Llama-3.2-1B veröffentlicht eine Datei,
phi-4 zwei. Code, der model.safetensors benennt, funktioniert bis zum ersten aufgeteilten
Modell, und Code, der model-00001-of-* voraussetzt, scheitert am ersten einteiligen.
Lesen Sie das Verzeichnis, oder besser den Gewichtsindex — model.safetensors.index.json und
seine weight_map —, der jeden Shard nennt, den das Modell braucht. Das ist zugleich der einzige
verlässliche Weg, einen fertigen Download von einem abgebrochenen zu unterscheiden, denn eine
Dateizahl sagt nichts darüber, ob die richtigen Dateien da sind.
Die Fassung davon, die zweimal zugebissen hat
Noch eine Sache aus derselben Familie, denn sie hat einen zweiten Nachmittag gekostet und hat dieselbe Form.
Rufen Sie eval nicht innerhalb der Schichtenschleife auf. Dort stand eines, übrig aus einem
Machbarkeitsnachweis: eval(&h) nach jeder Schicht, was richtig und ungeheuer teuer ist. Bei
einem Modell mit sechzehn Schichten sind das sechzehn GPU-Synchronisationen pro Token, bei einem
mit vierzig eben vierzig.
Die Arrays müssen erst am Ende des Schritts ausgewertet werden. Das Entfernen brachte die Erzeugung von 94 auf 244 Token pro Sekunde — mehr als die halbe Geschwindigkeit ging grundlos für das Synchronisieren mit der GPU drauf.
Es ist dieselbe Lehre von der anderen Seite. Der erste Defekt war, ein Ergebnis nicht zu prüfen, das man hätte prüfen sollen; dieser war, ein Ergebnis zu erzwingen, das man nicht brauchte. Beide handeln davon, wann ausgewertet wird, und in einem trägen Graphen ist das keine Nebensache.
Die Kurzfassung
- Laden Sie Safetensors auf dem CPU-Stream. Die Berechnung läuft weiterhin auf der GPU.
- Prüfen Sie den Rückgabewert jedes
eval. Ein unausgewertetes Array liest sich lautlos als Nullen. - Entdecken Sie Shards über den Index, statt Dateien zu benennen.
Gemessen gegen mlx-c am 6. September 2026. Sollte MLX das ändern, ist das Datum Ihre Warnung,
dass diese Seite seither nicht nachgeprüft wurde.