LLMs in einfachem, reinem C/CUDA, ohne dass 245 MB PyTorch oder 107 MB cPython erforderlich sind. Der aktuelle Schwerpunkt liegt auf dem Vortraining, insbesondere der Reproduktion der GPT-2- und GPT-3-Miniserien, zusammen mit einer parallelen PyTorch-Referenzimplementierung in train_gpt2.py. Sie erkennen diese Datei als leicht optimiertes nanoGPT, ein früheres Projekt von mir. Derzeit ist llm.c etwas schneller als PyTorch Nightly (um etwa 7 %). Zusätzlich zum hochmodernen Mainline-Code in train_gpt2.cu haben wir eine einfache Referenz-CPU-fp32-Implementierung in ca. 1.000 Zeilen sauberem Code in einer Datei train_gpt2.c. Ich möchte, dass dieses Repo nur C- und CUDA-Code verwaltet. Portierungen auf andere Sprachen oder Repos sind sehr willkommen, sollten aber in separaten Repos erfolgen, und ich verlinke sie gerne weiter unten im Abschnitt „Bemerkenswerte Forks“. Die Koordination der Entwickler erfolgt in den Diskussionen und auf Discord, entweder im #llmc
-Kanal auf dem Zero to Hero-Kanal oder auf #llmdotc
auf GPU MODE Discord.
Die beste Einführung in das llm.c-Repo ist heute die Reproduktion des GPT-2 (124M)-Modells. Diskussion Nr. 481 geht ausführlich darauf ein. Wir können andere Modelle der GPT-2- und GPT-3-Serie sowohl in llm.c als auch in der parallelen Implementierung von PyTorch reproduzieren. Schauen Sie sich die README-Datei der Skripte an.
Tipp zum Debuggen: Wenn Sie den Befehl make
zum Erstellen der Binärdatei ausführen, ändern Sie ihn, indem Sie -O3
durch -g
ersetzen, damit Sie den Code in Ihrer bevorzugten IDE (z. B. vscode) schrittweise durchgehen können.
Wenn Sie nicht auf mehreren Knoten trainieren, kein Interesse an gemischter Präzision haben und CUDA lernen möchten, könnten die fp32-Dateien (Legacy) für Sie von Interesse sein. Hierbei handelt es sich um Dateien, die zu Beginn der Geschichte von llm.c mit einem „Checkpoint“ versehen und zeitlich eingefroren wurden. Sie sind einfacher, tragbarer und möglicherweise leichter zu verstehen. Führen Sie den 1-GPU-FP32-Code wie folgt aus:
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2fp32cu
./train_gpt2fp32cu
Das Skript „download_starter_pack.sh“ ist ein schneller und einfacher Einstieg und lädt eine Reihe von .bin-Dateien herunter, die Ihnen den Einstieg erleichtern. Diese enthalten: 1) das GPT-2 124M-Modell, das in fp32 in bfloat16 gespeichert ist, 2) einen „Debug-Status“, der beim Unit-Testen verwendet wird (ein kleiner Datenstapel sowie Zielaktivierungen und -verläufe), 3) den GPT-2-Tokenizer und 3) der tokenisierte TinyShakespeare-Datensatz. Anstatt das .sh-Skript auszuführen, können Sie diese Artefakte auch manuell wie folgt neu erstellen:
pip install -r requirements.txt
python dev/data/tinyshakespeare.py
python train_gpt2.py
Der Abschnitt „Ich bin so arm an der GPU, dass ich nicht einmal eine GPU habe“. Sie können weiterhin viel Spaß beim Zugsehen von llm.c haben! Aber Sie werden nicht zu weit gehen. Genau wie die fp32-Version oben ist die CPU-Version ein noch früherer Prüfpunkt in der Geschichte von llm.c, als es sich lediglich um eine einfache Referenzimplementierung in C handelte. Anstatt beispielsweise von Grund auf zu trainieren, können Sie eine GPT-Feinabstimmung vornehmen. 2 kleine (124 MB) zur Ausgabe von Shakespeare-ähnlichem Text, als Beispiel:
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2
OMP_NUM_THREADS=8 ./train_gpt2
Wenn Sie die Ausführung des Starterpaket-Skripts lieber vermeiden möchten, können Sie, wie im vorherigen Abschnitt erwähnt, genau dieselben .bin-Dateien und Artefakte reproduzieren, indem Sie python dev/data/tinyshakespeare.py
und dann python train_gpt2.py
ausführen.
Die obigen Zeilen (1) laden einen bereits tokenisierten Tinyshakespeare-Datensatz herunter und laden die GPT-2 (124M)-Gewichte herunter, (3) initiieren daraus in C und trainieren 40 Schritte auf Tineshakespeare mit AdamW (mit Batchgröße 4, Kontextlänge nur 64). ), bewerten Sie den Validierungsverlust und probieren Sie Text aus. Ehrlich gesagt, es sei denn, Sie haben eine leistungsstarke CPU (und können die Anzahl der OMP-Threads im Startbefehl erhöhen), werden Sie mit CPU-Trainings-LLMs nicht so weit kommen, aber es könnte eine gute Demo/Referenz sein. Auf meinem MacBook Pro (Apple Silicon M3 Max) sieht die Ausgabe so aus:
[GPT-2]
max_seq_len: 1024
vocab_size: 50257
num_layers: 12
num_heads: 12
channels: 768
num_parameters: 124439808
train dataset num_batches: 1192
val dataset num_batches: 128
num_activations: 73323776
val loss 5.252026
step 0: train loss 5.356189 (took 1452.121000 ms)
step 1: train loss 4.301069 (took 1288.673000 ms)
step 2: train loss 4.623322 (took 1369.394000 ms)
step 3: train loss 4.600470 (took 1290.761000 ms)
... (trunctated) ...
step 39: train loss 3.970751 (took 1323.779000 ms)
val loss 4.107781
generating:
---
Come Running Away,
Greater conquer
With the Imperial blood
the heaviest host of the gods
into this wondrous world beyond.
I will not back thee, for how sweet after birth
Netflix against repounder,
will not
flourish against the earlocks of
Allay
---
Die Datendateien in /dev/data/(dataset).py
sind für das Herunterladen, Tokenisieren und Speichern der Token in .bin-Dateien verantwortlich, die leicht von C aus gelesen werden können. Wenn Sie beispielsweise Folgendes ausführen:
python dev/data/tinyshakespeare.py
Wir laden den Tinyshakespeare-Datensatz herunter und tokenisieren ihn. Die Ausgabe davon sieht so aus:
writing 32,768 tokens to ./dev/data/tinyshakespeare/tiny_shakespeare_val.bin
writing 305,260 tokens to ./dev/data/tinyshakespeare/tiny_shakespeare_train.bin
Die .bin-Dateien enthalten einen kurzen Header (1024 Byte) und dann einen Stream von Tokens in uint16, der die Token-IDs mit dem GPT-2-Tokenizer angibt. Weitere Datensätze sind in /dev/data
verfügbar.
Ich füge auch einen einfachen Komponententest bei, um sicherzustellen, dass unser C-Code mit dem PyTorch-Code übereinstimmt. Auf der CPU als Beispiel kompilieren und ausführen mit:
make test_gpt2
./test_gpt2
Dies lädt nun die Datei gpt2_124M_debug_state.bin
, die von train_gpt2.py geschrieben wird, führt einen Vorwärtsdurchlauf durch, vergleicht die Logits und Verluste mit der PyTorch-Referenzimplementierung, führt dann 10 Trainingsiterationen mit Adam durch und stellt sicher, dass die Verluste mit PyTorch übereinstimmen. Um die GPU-Version zu testen, führen wir Folgendes aus:
# fp32 test (cudnn not supported)
make test_gpt2cu PRECISION=FP32 && ./test_gpt2cu
# mixed precision cudnn test
make test_gpt2cu USE_CUDNN=1 && ./test_gpt2cu
Dadurch werden sowohl der fp32-Pfad als auch der Pfad mit gemischter Präzision getestet. Der Test sollte bestanden werden und overall okay: 1
.
Ich habe hier ein sehr kleines Tutorial angehängt, in doc/layernorm/layernorm.md. Es handelt sich um eine einfache Schritt-für-Schritt-Anleitung zur Implementierung einer einzelnen Schicht des GPT-2-Modells, der Layernorm-Schicht. Dies ist ein guter Ausgangspunkt, um zu verstehen, wie die Ebenen in C implementiert sind.
Blitzaufmerksamkeit . Ab dem 1. Mai 2024 nutzen wir den Flash Attention von cuDNN. Da cuDNN die Kompilierungszeit von einigen Sekunden auf ~Minuten aufbläht und dieser Codepfad derzeit sehr neu ist, ist dies standardmäßig deaktiviert. Sie können es aktivieren, indem Sie wie folgt kompilieren:
make train_gpt2cu USE_CUDNN=1
Dadurch wird versucht, mit cudnn zu kompilieren und auszuführen. Sie müssen cuDNN auf Ihrem System installiert haben. Die cuDNN-Installationsanweisungen mit apt-get greifen auf den Standardsatz von cuDNN-Paketen zu. Für ein minimales Setup reicht das cuDNN-Dev-Paket aus, z. B. auf Ubuntu 22.04 für CUDA 12.x:
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install libcudnn9-dev-cuda-12
Darüber hinaus benötigen Sie das cuDNN-Frontend, bei dem es sich jedoch nur um Header-Dateien handelt. Klonen Sie das Repo einfach auf Ihre Festplatte. Das Makefile sucht derzeit entweder in Ihrem Home-Verzeichnis oder im aktuellen Verzeichnis danach. Wenn Sie es an anderer Stelle abgelegt haben, fügen Sie CUDNN_FRONTEND_PATH=/path/to/your/cudnn-frontend/include
zur make
-Befehlszeile hinzu.
Stellen Sie sicher, dass Sie MPI und NCCL installieren, z. B. unter Linux:
sudo apt install openmpi-bin openmpi-doc libopenmpi-dev
Befolgen Sie für NCCL die Anweisungen auf der offiziellen Website (z. B. Netzwerkinstallationsprogramm).
und dann:
make train_gpt2cu
mpirun -np < number of GPUs > ./train_gpt2cu
oder führen Sie einfach eines unserer Skripte unter ./scripts/
aus.
Stellen Sie sicher, dass Sie NCCL
gemäß den Anweisungen im Abschnitt „Multi-GPU“ installiert haben.
Derzeit unterstützen wir drei Möglichkeiten, mit denen Sie ein Training mit mehreren Knoten durchführen können:
./scripts/multi_node/run_gpt2_124M_mpi.sh
../scripts/multi_node/run_gpt2_124M_fs.sbatch
../scripts/multi_node/run_gpt2_124M_tcp.sbatch
.Notiz:
slurm-wlm
die PMIx-Unterstützung eingestellt hat), müssen Sie den FS (2)- oder TCP (3)-Ansatz verwenden . Um zu testen, ob Ihr Slurm PMIx unterstützt, führen Sie Folgendes aus: srun --mpi=list
und sehen Sie, ob Sie pmix
in der Ausgabe erhalten.mpirun
- MPI (1) einen Lauf mit mehreren Knoten starten.Keine dieser drei Methoden ist überlegen, wir bieten Ihnen lediglich Optionen, damit Sie sie in Ihrer spezifischen Umgebung ausführen können.
Dies ist nur ein Beispielprozess zum Durchsuchen der Lernraten auf einer Maschine mit 4 GPUs in TinyStories. Führen Sie ein Shell-Skript sweep.sh
aus (natürlich nachdem Sie chmod u+x sweep.sh
ausgeführt haben):
#! /bin/bash
learning_rates=(3e-5 1e-4 3e-4 1e-3)
for i in {0..3} ; do
export CUDA_VISIBLE_DEVICES= $i
screen -dmS " tr $i " bash -c " ./train_gpt2cu -i data/TinyStories -v 250 -s 250 -g 144 -l ${learning_rates[$i]} -o stories $i .log "
done
# you can bring these down with
# screen -ls | grep -E "tr[0-3]" | cut -d. -f1 | xargs -I {} screen -X -S {} quit
Dieses Beispiel öffnet vier Bildschirmsitzungen und führt die vier Befehle mit unterschiedlichen LRs aus. Dadurch werden die Protokolldateien stories$i.log
mit allen Verlusten geschrieben, die Sie in Python beliebig darstellen können. Ein kurzes Beispiel für das Parsen und Plotten dieser Protokolldateien finden Sie in dev/vislog.ipynb.
Noch ein paar Worte dazu, wie dieses Repo aussehen soll:
Erstens möchte ich, dass llm.c
ein Ort der Bildung ist. Unser dev/cuda
-Ordner ist beispielsweise ein Ort für eine Bibliothek von Kerneln für alle Ebenen, die manuell handgeschrieben und sehr gut dokumentiert sind, angefangen bei sehr einfachen Kerneln bis hin zu komplexeren/schnelleren Kerneln. Wenn Sie einen neuen Kernel mit verschiedenen Kompromissen haben, können Sie ihn gerne hier beisteuern.
Allerdings möchte ich auch, dass llm.c
sehr schnell ist und sogar praktisch zum Trainieren von Netzwerken nützlich ist. Zu Beginn sollten wir beispielsweise in der Lage sein, den großen GPT-2-Trainingslauf (1,6B) zu reproduzieren. Dies erfordert, dass wir alle schnellsten Kernel integrieren, die es gibt, einschließlich der Verwendung von Bibliotheken wie cuBLAS, cuBLASLt, CUTLASS, cuDNN usw. Ich denke auch, dass dies einem pädagogischen Zweck dient, um eine Obergrenze für Experten und eine Maßeinheit festzulegen. Sie könnten beispielsweise sagen, dass Ihre manuell geschriebenen Kernel 80 % der cuBLAS-Geschwindigkeit haben usw. Dann können Sie sich für einen superschnellen Lauf entscheiden, oder Sie können die manuellen Kernel, die Sie verwenden und mit denen Sie arbeiten möchten, per Drag-and-Drop verschieben diese.
Als Einschränkung möchte ich jedoch die Hauptzeile llm.c
im Stammordner einfach und lesbar halten. Wenn es eine PR gibt, die beispielsweise die Leistung um 2 % verbessert, aber 500 Zeilen komplexen C-Codes „kostet“ und vielleicht eine exotische Abhängigkeit von Drittanbietern, kann ich die PR ablehnen, weil sich die Komplexität nicht lohnt. Als konkretes Beispiel ist es ein Kinderspiel, cuBLAS für Matmuls als Standard in der Root-Trainingsschleife festzulegen: Dadurch wird der Hauptcode viel schneller, es handelt sich um eine einzelne Zeile interpretierbaren Codes und es handelt sich um eine sehr häufige Abhängigkeit. Darüber hinaus können wir manuelle Implementierungen haben, die mit cuBLAS in dev/cuda
konkurrieren können.
Schließlich werde ich viel sensibler auf die Komplexität im Stammordner des Projekts achten, der die Haupt-/Standarddateien des Projekts enthält. Im Vergleich dazu ist der Ordner dev/
eher ein Arbeitsspeicher für uns, um eine Bibliothek von Kerneln oder Klassen zu entwickeln und nützlichen, verwandten oder lehrreichen Code zu teilen, und ein Teil dieses Codes könnte durchaus (lokal) komplex sein.
AMD-Unterstützung
C#
CUDA C++
C++/CUDA
WebGPU C++
C++
Gehen
Java
Metall
Mojo
OpenCL
Rost
Schnell
Zig
Habana Gaudi2
Nim
Möglichkeiten zur Organisation der Entwicklung:
#llmc
-Kanal erstellt. MIT