Ein kleines LLM von Grund auf auf einem Laptop mit 8 GB RAM trainieren
Wie man eine saubere Datenpipeline für das Training eines Transformers auf bescheidener Hardware aufbaut: BPE-Tokenizer, TinyStories, Tokens auf der Festplatte und Batches für die Vorhersage des nächsten Tokens.
Viele glauben, man brauche ein ganzes Rack voller GPUs, um ein Sprachmodell von Grund auf zu trainieren. Braucht man nicht, zumindest nicht zum Lernen. Ein günstiger Gaming-Laptop mit Intel i5-9300H, 8 GB RAM und einer GTX 1650 mit 4 GB VRAM reicht, um einen kleinen Transformer zu bauen, ihn mit einem Spielzeug-Datensatz zu füttern und zuzusehen, wie er besser wird. Es wird langsam und etwas eng. Man lernt dabei aber mehr als mit jeder gehosteten API.
Hier geht es um die erste Stufe eines solchen Projekts: die Datenpipeline. Das Modell selbst kommt später, vorerst geht es also um Tokenizer, Datensätze, Speichergrenzen und Batching.
Warum sich das lohnt
Es gibt drei gute Gründe, das anzugehen:
- Python rostet ein, wenn ein Assistent alles für einen schreibt, und die Gewohnheit von Hand wieder aufzubauen ist ein gutes Gegenmittel.
- Die Systeme hinter dem aktuellen Hype wirken weniger mysteriös, wenn man eines Stück für Stück zusammengesetzt hat.
- Paper selbst zu implementieren ist der kürzeste Weg, Forschung kritisch zu lesen und vielleicht irgendwann selbst welche zu machen.
Das Projekt heißt Axiom, nach dem Wort für eine Regel, die ohne Beweis als wahr gilt. Das passt zu einem ersten Schritt: einen Transformer selbst schreiben und ihn dann trainieren, ausgehend von den einfachsten Grundlagen.
Einer Vorlesung folgen, ohne sie abzuschreiben
Andrej Karpathys Videoreihe über den Bau von GPT ist der naheliegende Leitfaden. Seine Vorlesung beginnt mit einem Tokenizer auf Zeichenebene. Wer Tokenisierung schon versteht, kann vorspringen und gleich Byte-Pair-Encoding (BPE) verwenden. Es geht ums Lernen, also ist es sinnvoll, einen etwas schwierigeren Weg als den auf dem Bildschirm zu wählen und seine Implementierung nicht wortgetreu abzutippen.
Ein Projekt, kein Notebook
Notebooks sind bequem für Experimente, verwischen aber die Struktur. Wer in Neovim lebt, hat noch einen Grund mehr, auf sie zu verzichten. Bauen Sie stattdessen ein gewöhnliches Python-Projekt mit Fehlerbehandlung, einem Datenlader und jedem Schritt als benannter Funktion. Jede Notebook-Zelle wird zu einer Funktion, und ein kleiner Orchestrator ruft sie der Reihe nach auf.
Ein paar Entscheidungen halten das ordentlich:
- Ein Verzeichnis
.localenthält alles Schwere, das nicht committet werden soll: Rohdatensätze, Tokenizer und bereits konvertierte Token-Dateien. Es soll das Beste an einer Notebook-Sitzung nachbilden, bei der nicht bei jedem Lauf alles neu geladen werden muss. - Eine einzige Konfigurationsdatei hält Modelleinstellungen, Pfade und Tokenizer-Namen fest, sodass in der Pipeline nichts fest verdrahtet ist.
- Jeder Download-Schritt prüft, ob die Datei schon existiert, bevor er sie erneut holt.
Daten und Tokenizer
Als Text dient der Datensatz roneneldan/TinyStories von Hugging Face. Er ist klein, aber nicht lächerlich winzig, und sein einfacher Wortschatz passt zu einem Modell, das mit begrenzter Rechenleistung lernen muss. Ein Assistent hat ihn vorgeschlagen, die Wahl wurde also nicht tief recherchiert, ist für einen ersten Lauf aber vernünftig. Eine Download-Funktion lädt ihn einmal herunter und speichert ihn im Cache.
Beim Tokenizer läuft es genauso. Hier ist es lakhera2023/gemma4-nano-tinystories, geladen über einen weiteren kleinen bedingten Downloader. Wieder ein Vorschlag, den man auf Vertrauen übernimmt und dann von Hand programmiert, und das ist eine faire Art zu lernen: Der Assistent schlägt vor, den Code schreiben Sie selbst, damit Sie verstehen, was er tut.
Die Speicherwand
Bei der Tokenisierung wehrt sich eine 8-GB-Maschine. Der naheliegende Ansatz ist, jede Geschichte zu tokenisieren, die IDs an ein einziges wachsendes Array im Speicher anzuhängen und das Array am Ende zu schreiben. Auf einem Laptop mit wenig RAM friert dieser Ansatz das ganze System ein.
Der Grund ist einfach, wenn man genauer hinsieht. Ein Token wird als Ganzzahl gespeichert, und eine Liste von Ganzzahlen belegt weit mehr Platz als der reine Text, aus dem sie stammt. Der Overhead von Python-Listen verschärft das, und das wiederholte Erweitern einer großen Struktur fragmentiert den Speicher immer weiter. Nach Millionen von Geschichten bleibt für das Betriebssystem nichts mehr übrig.
Die Lösung ist, die Ausgabe gar nicht erst im Speicher zu halten. Schreiben Sie die Tokens jeder Geschichte direkt in eine Datei auf der Festplatte, sobald sie erzeugt sind:
with open(output_file, "wb") as f:
for sample in dataset:
text = sample["text"]
tokens = tokenizer.encode(text)
tokens.append(tokenizer.eos_id())
np.asarray(tokens, dtype=np.uint16).tofile(f)
Der Speicherverbrauch bleibt jetzt konstant, egal wie groß der Datensatz wird. Die Tokens landen in einer Binärdatei, die sich später wieder in den Speicher abbilden lässt, und genau so arbeiten auch größere Trainingspipelines. Das ist der einzige echte Engpass in dieser Phase, und ihn durch einen Absturz zu finden ist eine Lektion, die man nicht vergisst.
Batches bauen
Batching ist einfacher, als es klingt, braucht aber etwas gedankliches Aufwärmen. Ein Sprachmodell lernt, das nächste Token vorherzusagen, also ist das Ziel für jede Eingabe dieselbe Sequenz, um eine Position nach vorn verschoben. Nehmen Sie einen Ausschnitt von Tokens als x, den eine Position später beginnenden Ausschnitt als y, und stapeln Sie viele solcher Paare zu einem Batch. Für jede Position in x sieht das Modell in y, was als Nächstes kommt.
Diese Verschiebung um eins ist der ganze Trick hinter dem Trainingssignal. Es wirkt fast zu einfach, deshalb lohnt es sich, es einmal von Hand zu implementieren, statt eine Bibliotheksfunktion aufzurufen.
Wie es weitergeht
Die Datenseite steht: ein gecachter Datensatz, ein gecachter Tokenizer, eine Token-Datei auf der Festplatte und ein Batcher. Als Nächstes kommt das Modell selbst, ein von Grund auf geschriebener Transformer, weiterhin nach Karpathys Vorlesung und mit Abweichungen, wo es einen Grund dafür gibt. Ein so kleiner Laptop begrenzt Modellgröße und Kontextlänge, die eigentliche Arbeit wird also darin bestehen, etwas Nützliches in 4 GB VRAM unterzubringen.