LLM en C/CUDA simple et pur sans besoin de 245 Mo de PyTorch ou 107 Mo de cPython. L'accent est actuellement mis sur la pré-formation, en particulier la reproduction des mini-séries GPT-2 et GPT-3, ainsi qu'une implémentation parallèle de référence PyTorch dans train_gpt2.py. Vous reconnaîtrez ce fichier comme un nanoGPT légèrement modifié, un de mes projets antérieurs. Actuellement, llm.c est un peu plus rapide que PyTorch Nightly (d'environ 7 %). En plus du code principal de pointe dans train_gpt2.cu, nous avons une simple implémentation de référence CPU fp32 dans environ 1 000 lignes de code propre dans un seul fichier train_gpt2.c. J'aimerais que ce dépôt ne conserve que le code C et CUDA. Les ports vers d'autres langues ou dépôts sont les bienvenus, mais doivent être effectués dans des dépôts séparés, et je suis heureux de créer un lien vers eux ci-dessous dans la section « forks notables ». La coordination des développeurs a lieu dans les discussions et sur Discord, soit sur le canal #llmc
sur le canal Zero to Hero, soit sur #llmdotc
sur GPU MODE Discord.
La meilleure introduction au dépôt llm.c aujourd'hui consiste à reproduire le modèle GPT-2 (124M). La discussion n° 481 explique cela en détail. Nous pouvons reproduire d'autres modèles des séries GPT-2 et GPT-3 à la fois dans llm.c et dans l'implémentation parallèle de PyTorch. Jetez un œil aux scripts README.
conseil de débogage : lorsque vous exécutez la commande make
pour construire le binaire, modifiez-le en remplaçant -O3
par -g
afin de pouvoir parcourir le code dans votre IDE préféré (par exemple vscode).
Si vous ne vous entraînez pas sur plusieurs nœuds, n'êtes pas intéressé par une précision mixte et souhaitez apprendre CUDA, les fichiers fp32 (hérités) pourraient vous intéresser. Ce sont des fichiers qui ont été « vérifiés » au début de l'histoire de llm.c et figés dans le temps. Ils sont plus simples, plus portables et peut-être plus faciles à comprendre. Exécutez le code 1 GPU, fp32 comme ceci :
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2fp32cu
./train_gpt2fp32cu
Le script download_starter_pack.sh est un moyen rapide et facile de démarrer et il télécharge un tas de fichiers .bin qui vous aident à décoller. Ceux-ci contiennent : 1) le modèle GPT-2 124M enregistré dans fp32, dans bfloat16, 2) un "état de débogage" utilisé dans les tests unitaires (un petit lot de données, et des activations et gradients de cibles), 3) le tokenizer GPT-2 , et 3) l'ensemble de données tokenisé tinyshakespeare. Alternativement, au lieu d'exécuter le script .sh, vous pouvez recréer ces artefacts manuellement comme suit :
pip install -r requirements.txt
python dev/data/tinyshakespeare.py
python train_gpt2.py
La section "Je suis tellement pauvre en GPU que je n'ai même pas un seul GPU". Vous pouvez toujours profiter du train llm.c ! Mais vous n'irez pas trop loin. Tout comme la version fp32 ci-dessus, la version CPU est un point de contrôle encore plus ancien dans l'histoire de llm.c, à l'époque où il ne s'agissait que d'une simple implémentation de référence en C. Par exemple, au lieu de vous entraîner à partir de zéro, vous pouvez affiner un GPT- 2 petits (124 M) pour afficher un texte de type Shakespeare, à titre d'exemple :
chmod u+x ./dev/download_starter_pack.sh
./dev/download_starter_pack.sh
make train_gpt2
OMP_NUM_THREADS=8 ./train_gpt2
Si vous préférez éviter d'exécuter le script du pack de démarrage, comme mentionné dans la section précédente, vous pouvez reproduire exactement les mêmes fichiers et artefacts .bin en exécutant python dev/data/tinyshakespeare.py
puis python train_gpt2.py
.
Les lignes ci-dessus (1) téléchargent un ensemble de données tinyshakespeare déjà tokenisé et téléchargent les poids GPT-2 (124M), (3) initialisent à partir d'eux en C et s'entraînent pendant 40 étapes sur tineshakespeare avec AdamW (en utilisant une taille de lot de 4, une longueur de contexte uniquement de 64 ), évaluez la perte de validation et échantillonnez du texte. Honnêtement, à moins que vous n'ayez un processeur puissant (et que vous puissiez augmenter le nombre de threads OMP dans la commande de lancement), vous n'irez pas aussi loin dans les LLM de formation du processeur, mais cela pourrait être une bonne démo/référence. La sortie ressemble à ceci sur mon MacBook Pro (Apple Silicon M3 Max) :
[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
---
Les fichiers de données à l'intérieur de /dev/data/(dataset).py
sont responsables du téléchargement, de la tokenisation et de l'enregistrement des jetons dans des fichiers .bin, facilement lisibles à partir de C. Ainsi, par exemple, lorsque vous exécutez :
python dev/data/tinyshakespeare.py
Nous téléchargeons et tokenisons l'ensemble de données tinyshakespeare. Le résultat de ceci ressemble à ceci :
writing 32,768 tokens to ./dev/data/tinyshakespeare/tiny_shakespeare_val.bin
writing 305,260 tokens to ./dev/data/tinyshakespeare/tiny_shakespeare_train.bin
Les fichiers .bin contiennent un en-tête court (1024 octets) puis un flux de jetons dans uint16, indiquant les identifiants des jetons avec le tokenizer GPT-2. D'autres ensembles de données sont disponibles dans /dev/data
.
Je joins également un test unitaire simple pour m'assurer que notre code C est conforme au code PyTorch. Sur le CPU à titre d'exemple, compilez et exécutez avec :
make test_gpt2
./test_gpt2
Cela charge maintenant le fichier gpt2_124M_debug_state.bin
qui est écrit par train_gpt2.py, exécute une passe avant, compare les logits et les pertes avec l'implémentation de référence de PyTorch, puis effectue 10 itérations de formation avec Adam et s'assure que les pertes correspondent à PyTorch. Pour tester la version du GPU que nous exécutons :
# fp32 test (cudnn not supported)
make test_gpt2cu PRECISION=FP32 && ./test_gpt2cu
# mixed precision cudnn test
make test_gpt2cu USE_CUDNN=1 && ./test_gpt2cu
Cela teste à la fois le chemin fp32 et le chemin de précision mixte. Le test devrait réussir et s'imprimer overall okay: 1
.
J'ai joint un très petit tutoriel ici, dans doc/layernorm/layernorm.md. Il s'agit d'un guide simple, étape par étape, pour implémenter une seule couche du modèle GPT-2, la couche layernorm. C'est un bon point de départ pour comprendre comment les couches sont implémentées en C.
attention éclair . Depuis le 1er mai 2024, nous utilisons le Flash Attention de cuDNN. Étant donné que cuDNN fait passer le temps de compilation de quelques secondes à ~ minutes et que ce chemin de code est actuellement très nouveau, il est désactivé par défaut. Vous pouvez l'activer en compilant comme ceci :
make train_gpt2cu USE_CUDNN=1
Cela va essayer de compiler avec cudnn et de l'exécuter. Vous devez avoir cuDNN installé sur votre système. Les instructions d'installation de cuDNN avec apt-get récupéreront l'ensemble par défaut de packages cuDNN. Pour une configuration minimale, le package de développement cuDNN est suffisant, par exemple sur Ubuntu 22.04 pour 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
En plus de cela, vous avez besoin de l'interface cuDNN, mais ce ne sont que des fichiers d'en-tête. Clonez simplement le dépôt sur votre disque. Le Makefile le recherche actuellement soit dans votre répertoire personnel, soit dans le répertoire courant. Si vous l'avez placé ailleurs, ajoutez CUDNN_FRONTEND_PATH=/path/to/your/cudnn-frontend/include
à la ligne de commande make
.
Assurez-vous d'installer MPI et NCCL, par exemple sous Linux :
sudo apt install openmpi-bin openmpi-doc libopenmpi-dev
Pour NCCL, suivez les instructions du site officiel (par exemple, installateur réseau)
et puis:
make train_gpt2cu
mpirun -np < number of GPUs > ./train_gpt2cu
ou exécutez simplement l'un de nos scripts sous ./scripts/
.
Assurez-vous d'avoir installé NCCL
en suivant les instructions de la section multi-GPU.
Nous prenons actuellement en charge trois méthodes qui vous permettent d'exécuter une formation multi-nœuds :
./scripts/multi_node/run_gpt2_124M_mpi.sh
pour plus de détails../scripts/multi_node/run_gpt2_124M_fs.sbatch
pour plus de détails../scripts/multi_node/run_gpt2_124M_tcp.sbatch
pour plus de détails.Note:
slurm-wlm
a abandonné le support PMIx), vous devrez utiliser l'approche FS (2) ou TCP (3). . Pour tester si votre slurm prend en charge PMIx, exécutez : srun --mpi=list
et voyez si vous obtenez pmix
dans la sortie.mpirun
- MPI (1).Aucune de ces 3 méthodes n'est supérieure, nous vous proposons simplement des options afin que vous puissiez exécuter dans votre environnement spécifique.
Juste à titre d'exemple, processus pour balayer les taux d'apprentissage sur une machine avec 4 GPU sur TinyStories. Exécutez un script shell sweep.sh
(après bien sûr chmod u+x sweep.sh
) :
#! /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
Cet exemple ouvre 4 sessions d'écran et exécute les quatre commandes avec différents LR. Celui-ci écrit les fichiers journaux stories$i.log
avec toutes les pertes, que vous pouvez tracer à votre guise en Python. Un exemple rapide de la façon d'analyser et de tracer ces fichiers journaux se trouve dans dev/vislog.ipynb.
Quelques mots supplémentaires sur ce que je veux que ce dépôt soit :
Premièrement, je veux llm.c
soit un lieu d'éducation. Par exemple, notre dossier dev/cuda
est un emplacement pour une bibliothèque de noyaux pour toutes les couches qui sont écrites manuellement et très bien documentées, depuis les noyaux très simples jusqu'aux noyaux plus complexes/plus rapides. Si vous disposez d'un nouveau noyau avec différents compromis, n'hésitez pas à le contribuer ici.
Cela dit, je veux aussi que llm.c
soit très rapide, voire pratiquement utile pour former des réseaux. Par exemple, pour commencer, nous devrions être capables de reproduire le grand cycle d'entraînement GPT-2 (1,6B). Cela nécessite que nous incorporions tous les noyaux les plus rapides qui soient, y compris l'utilisation de bibliothèques telles que cuBLAS, cuBLASLt, CUTLASS, cuDNN, etc. Je pense également que cela sert un objectif pédagogique pour établir une limite supérieure experte et une unité de mesure, par exemple, vous pourriez dire que vos noyaux écrits manuellement représentent 80 % de la vitesse de cuBLAS, etc. Ensuite, vous pouvez choisir de faire une exécution ultra rapide, ou vous pouvez choisir de "glisser-déposer" les noyaux manuels que vous souhaitez utiliser, et de les exécuter avec ceux.
Cependant, comme contrainte, je souhaite garder la ligne principale llm.c
dans le dossier racine simple et lisible. S'il existe un PR qui, par exemple, améliore les performances de 2 % mais qui "coûte" 500 lignes de code C complexe, et peut-être une dépendance tierce exotique, je peux rejeter le PR car la complexité n'en vaut pas la peine. À titre d'exemple concret, faire de cuBLAS pour matmuls la valeur par défaut dans la boucle d'entraînement racine est une évidence : cela rend le code principal beaucoup plus rapide, il s'agit d'une seule ligne de code interprétable et c'est une dépendance très courante. À côté de cela, nous pouvons avoir des implémentations manuelles qui peuvent rivaliser avec cuBLAS dans dev/cuda
.
Enfin, je serai beaucoup plus sensible à la complexité du dossier racine du projet, qui contient les fichiers principaux/par défaut du projet. En comparaison, le dossier dev/
est un peu plus un espace de travail pour nous permettre de développer une bibliothèque de noyaux ou de classes et de partager du code utile, connexe ou éducatif, et une partie de ce code pourrait être acceptable (localement) complexe.
Prise en charge d'AMD
C#
CUDAC++
C++/CUDA
WebGPU C++
C++
Aller
Java
Métal
Mojo
OpenCL
Rouiller
Rapide
Zig
La Havane Gaudi2
Nim
Modalités d'organisation du développement :
#llmc
sur ma chaîne Discord Zero to Hero. MIT