Zahlen im Rechner
float32, float16, bfloat16 und int8: was die Formate unterscheidet, wann eine Rechnung kippt und warum Quantisierung überhaupt funktioniert.
Die Idee
Ein Rechner speichert keine Zahlen, sondern Näherungen. 0,1 + 0,2 ergibt in
fast jeder Programmiersprache nicht 0,3, sondern 0,30000000000000004. Das
ist kein Fehler, sondern die Folge davon, dass 0,1 im Binärsystem eine
unendliche Nachkommastelle hat.
Bei einer Rechnung fällt das nicht auf. Bei Milliarden von Rechnungen hintereinander schon.
Wozu es gut ist
Wer weiß, wie ungenau ein Format ist, kann entscheiden, wie klein ein Modell werden darf, bevor es schlechter wird. Genau diese Entscheidung steht am Anfang jeder Frage nach der Hardware.
Die Formate im Vergleich
| Format | Bits | Exponent | Mantisse | Größter Wert | Kleinste relative Auflösung |
|---|---|---|---|---|---|
| float32 | 32 | 8 | 23 | 3,4e38 | ~1,2e-7 |
| float16 | 16 | 5 | 10 | 65.504 | ~9,8e-4 |
| bfloat16 | 16 | 8 | 7 | 3,4e38 | ~7,8e-3 |
| int8 | 8 | entfällt | entfällt | 127 | 1 Schritt je Skalenteil |
| int4 | 4 | entfällt | entfällt | 7 | 16 Stufen insgesamt |
bfloat16 ist float32 mit abgeschnittener Mantisse. Deshalb ist die
Umwandlung trivial und der Wertebereich identisch: der Grund, warum es sich
für Training durchgesetzt hat.
import numpy as np
a = np.float16(60000)
print(a * 2) # inf -> float16 laeuft bei 65504 ueber
x = np.float32([1e8, 1.0, -1e8])
print(x.sum()) # 0.0 -> die 1 ist ausgeloescht
print(np.float64(x).sum()) # 1.0Typische Fehler
- Verlustwerte in float16 akkumulieren statt in float32.
- Softmax ohne Abzug des Maximums rechnen:
exp(800)ist sofortinf. - Varianzen über die Formel
E[x²] − E[x]²bilden; bei großen Mittelwerten löscht sich das aus. - Modelle in int4 quantisieren und anschließend Benchmarks aus dem float16-Lauf zitieren.
Stabiler Softmax
Ohne den Abzug ist exp(zᵢ) bei zᵢ = 800 in float32 bereits inf, und die
Division inf/inf ergibt NaN. Mit dem Abzug ist der größte Exponent exakt
exp(0) = 1, und die Rechnung ist unabhängig von der Skala der Logits.
Quantisierung, durchgerechnet
Beispiel: ein Kanal mit max|w| = 0,42. Dann ist s = 0,42/127 = 0,003307.
Ein Gewicht w = 0,1234 wird zu q = round(37,32) = 37, zurück also
ŵ = 0,12235. Der Fehler beträgt 0,00105, das sind 0,85 Prozent des Wertes und
0,25 Prozent des Kanalmaximums. Über tausende Gewichte mitteln sich diese
Fehler weitgehend heraus, solange sie unkorreliert sind: das ist der
eigentliche Grund, warum Quantisierung funktioniert.
Ausreißer brechen diese Annahme. Ein einzelnes Gewicht mit |w| = 5 in einem
Kanal, dessen übrige Werte unter 0,5 liegen, zwingt s um den Faktor zehn nach
oben und macht alle anderen Gewichte zehnmal ungenauer. Deshalb behandeln
Verfahren wie LLM.int8() und AWQ Ausreißerkanäle getrennt, siehe
Quantisierung.
Determinismus
Gleitkommaaddition ist nicht assoziativ. Zwei Läufe derselben Matrixmultiplikation mit unterschiedlicher Thread-Aufteilung liefern nicht bitgleiche Ergebnisse. Wer reproduzierbare Ausgaben braucht, etwa zur Nachweisführung nach Protokollierung, muss deterministische Kernel erzwingen und den Durchsatz-Nachteil in Kauf nehmen, oder er protokolliert Ergebnis und Modellversion statt der Reproduzierbarkeit.
Passende Kurse und Quellen
NumPy
Die Grundlage aller Rechnung mit Feldern in Python. Wer lineare Algebra nachrechnen will, braucht sonst nichts weiter.
Für alle, die Formeln aus den Technikartikeln selbst nachrechnen wollen.
PyTorch-Dokumentation
Die Referenz zu Autograd, Datentypen, Speicherverhalten und Determinismus. Die Stelle, an der sich Fragen zur Reproduzierbarkeit tatsächlich klären lassen.
Die Stelle, an der sich Fragen zu Determinismus und Speicherverhalten tatsächlich klären lassen.