Wenn 3 Bit passen und 4 Bit nicht — warum quantisiert man sich das Modell dann nicht einfach selbst?
Am Ende von Teil 2 stand ein unbefriedigendes Ergebnis: Das 27B-Modell läuft auf der Karte nur, weil jemand anderes eine passende 3-Bit-Fassung gebaut hat. Die 4-Bit-Variante wäre besser, passt aber nicht in 16 GB.
Der naheliegende Gedanke: Dann bau ich mir eben genau die Quantisierung, die ich brauche. Das Werkzeug dafür ist frei verfügbar, der Prozess etabliert, und die Kiste steht nachts ohnehin herum.
Ich habe es durchgerechnet und dann gelassen. Die Begründung ist lehrreicher als das Ergebnis — sie erklärt nämlich, warum es fast keine 3-Bit-Modelle gibt, warum 2 Bit technisch einfacher wäre als 3, und warum es trotzdem niemand naiv macht.
Kurz: Was Quantisierung tut
Ein Modell besteht aus Milliarden Zahlen. Ursprünglich stehen die mit 16 Bit Genauigkeit da. Quantisierung rundet sie auf ein gröberes Raster — 4 Bit bedeuten 16 mögliche Werte pro Gewicht, 3 Bit acht, 2 Bit noch vier.
Weniger Bits heißen weniger Speicher. Und weil bei einer einzelnen Anfrage jedes aktive Gewicht einmal durch den Speicherbus muss, heißen weniger Bits auch: schneller. Der Preis ist Genauigkeit.
Das eigentliche Problem: 3 teilt keine Zweierpotenz
Hier wird es unerwartet handfest. Rechner arbeiten in Bytes und 32-Bit-Wörtern, und diese Größen sind Zweierpotenzen.
Bei 4 Bit geht die Rechnung auf: Zwei Gewichte passen exakt in ein Byte, acht in ein 32-Bit-Register. Sie wieder auszupacken ist eine triviale Operation — Bits verschieben, Maske drüber, fertig.
Bei 3 Bit geht sie nicht auf. Die Gewichte liegen quer über die Byte-Grenzen: Eines beginnt im ersten Byte und endet im zweiten. Es gibt keine saubere Aufteilung, und um genau dieses Detail dreht sich der gesamte Aufwand.
Man kann es lösen. llama.cpp tut es: 32 Gewichte à 3 Bit ergeben 96 Bit, die in drei 32-Bit-Wörter gepackt werden, organisiert in Blöcken mit eigenen Skalierungsfaktoren. Aber jede dieser Verrenkungen kostet Rechenzeit an einer Stelle, an der es auf jeden Takt ankommt — und sie muss so gebaut sein, dass 32 parallel arbeitende GPU-Threads trotzdem nebeneinanderliegende Speicherbereiche lesen. Sonst bricht die Leistung ein, bevor überhaupt gerechnet wird.
Ein solcher Kernel in der Qualitätsklasse der etablierten 4-Bit-Kernel ist in der Größenordnung von tausenden Zeilen handoptimiertem CUDA-Code — Wochen bis Monate Arbeit für jemanden, der GPU-Programmierung auf Thread-Ebene wirklich beherrscht. Das ist keine Abendbeschäftigung.
Der überraschende Teil: 2 Bit wäre einfacher
Jetzt kommt die Wendung, die ich nicht erwartet hatte. Kernel-technisch ist 2 Bit deutlich angenehmer als 3.
Denn 2 teilt jede Zweierpotenz. Sechzehn Gewichte passen exakt in ein 32-Bit-Wort, kein einziges liegt über einer Wortgrenze, das Auspacken ist wieder das billige Verschieben-und-Maskieren wie bei 4 Bit. Alles wird einfacher.
Nur kollidiert das mit der anderen Seite der Rechnung: Bei 2 Bit bricht naives Quantisieren qualitativ zusammen. Vier mögliche Werte pro Gewicht sind zu wenig. Rundet man einfach auf dieses Raster, wird aus einem klugen Modell Kauderwelsch — die Grenze, an der Modelle kippen, liegt erfahrungsgemäß zwischen 3 und 2,5 Bit.
Die 2-Bit-Verfahren, die tatsächlich funktionieren, arbeiten deshalb völlig anders. Sie runden nicht einzelne Gewichte, sondern bilden ganze Gruppen auf Einträge aus einem Codebuch ab — einer festen Sammlung von Mustern. Kalibrierdaten entscheiden dabei, welche Gewichte genau sein müssen und welche grob dürfen.
Und damit ist der Vorteil dahin. Der Kernel packt jetzt nicht mehr mit zwei billigen Operationen aus, sondern schlägt für jede Gruppe in einer Tabelle nach. Das ist teurer und passt schlechter zu den Recheneinheiten moderner Grafikkarten als jedes 3-Bit-Gefummel.
Die Ironie: Genau die Verfahren, die 2 Bit erträglich machen, machen den 2-Bit-Kernel wieder kompliziert.
Die Lösung existiert längst — und ich benutzte sie schon
Während ich noch überlegte, wie man das Bit-Budget klüger verteilen könnte, benutzte ich diese Lösung bereits, ohne es zu merken.
Das Modell auf meiner Kiste heißt UD-Q3_K_XL. Das „UD" steht für „Unsloth Dynamic" — und das ist nichts anderes als gemischte Genauigkeit. „Q3" ist nur der Durchschnitt, nicht die Wahrheit pro Schicht.
Der Gedanke dahinter: Nicht alle Gewichte sind gleich empfindlich. Ein festes Bit-Budget bringt am meisten, wenn man es ungleich verteilt.
→ Empfindliche Teile bekommen mehr Bits: die Einbettungen, die Ausgabeschicht, die Attention-Projektionen, die ersten und letzten Blöcke. Fehler dort schlagen direkt auf die Wortwahl durch.
→ Robuste Teile bekommen wenige: die großen Matrizen in der Mitte des Netzes. Sie stellen den Löwenanteil der Gewichte und verzeihen Rundungsfehler am besten.
Deshalb liegt „Q3" real bei etwa 3,4 Bit im Schnitt. Und deshalb hat das Modell meinen Qualitätsparcours überstanden: Die Stellen, an denen es auf exakte Ausgabe ankommt — sauberes JSON, eine korrekt abgeschriebene IBAN — laufen dort gar nicht mit 3 Bit.
Welche Schicht wie viel bekommt, entscheidet kein Bauchgefühl, sondern Messung: Man schickt Beispieltexte durchs Modell und beobachtet, welche Teile bei grober Rundung den Fehler am stärksten hochtreiben.
Zwei Philosophien, ein Speicherplatz
Das erklärt nebenbei, warum diese Modelle in llama.cpp leben und nicht in vLLM. Ein gemischtes Modell braucht mehrere Kernel-Varianten im selben Modell — eine Schicht mit 3 Bit, die nächste mit 6. llama.cpp ist genau dafür gebaut: Jede Schicht ruft ihren passenden Kernel.
vLLM will das Gegenteil: ein einheitliches Format über alles, dafür maximaler Durchsatz beim Batching. Zwei legitime Ziele. llama.cpp optimiert „bestes Modell pro Gigabyte", vLLM „beste Token pro Sekunde". Wer 16 GB hat, braucht meistens Ersteres.
Warum ich es dann gelassen habe
Der Reiz der Selbstquantisierung wäre gewesen, das 27B in vLLM zu bekommen — mit dessen exzellentem Batching. Diese Tür ist verschlossen, und zwar sauber verriegelt:
→ vLLMs schnelle Kernel beherrschen nur 4 und 8 Bit. Ein selbstgebautes 3-Bit-Modell würde geladen, fiele aber auf einen alten, langsamen Pfad zurück — absehbar langsamer als das, was llama.cpp heute schon liefert.
→ Bei 4 Bit wären wir wieder bei 16,5 GB, die nicht hineinpassen. Genau deshalb waren wir ja bei 3 Bit gelandet.
→ Das fertige Produkt existiert bereits, ist sorgfältiger gemacht als eine Nachtschicht es hinbekäme, und ein Gegentest auf dem Mac hat gezeigt, dass 3 Bit die Substanz hält.
Der Aufwand hätte 55 GB Download, einen Kalibrierdatensatz und eine durchlaufende Nacht bedeutet — für ein Ergebnis, das bestenfalls gleichzieht.
Einen dritten Weg habe ich trotzdem noch ausprobiert: ein Verfahren mit Codebuch-Quantisierung, das als Erweiterung in vLLM läuft und mit 13 GB passt. Ergebnis auf meiner Karte: 16 Token pro Sekunde einzeln, 29 bis 38 im Batch — etwa halb so schnell wie llama.cpp, weil das Nachschlagen im Codebuch auf dieser Kartengeneration teuer ist. Getestet, gemessen, gelöscht.
Was ich mitgenommen habe
Quantisierung sieht von außen aus wie ein Schieberegler: mehr Bits, mehr Qualität; weniger Bits, mehr Tempo. Tatsächlich ist sie ein Zusammenspiel aus Zahlenformat und Hardware, bei dem die krummen Werte am aufwendigsten sind — nicht weil sie ungenau wären, sondern weil sie nicht in die Register passen.
Und die praktische Lehre: Bevor man etwas selbst baut, lohnt die Frage, ob jemand das Problem bereits besser gelöst hat. In diesem Fall hatte ich die Antwort längst installiert.
Im nächsten Teil geht es um die Zahl, die alle angeben und die fast nie die richtige ist: Token pro Sekunde. Beim Batching schlägt diese Karte meinen Mac um ein Vielfaches — und zwar aus einem Grund, der mit dem Modell nichts zu tun hat.