69 points par GN⁺ 2026-02-17 | Aucun commentaire pour le moment. | Partager sur WhatsApp
  • Projet artistique publié par karpathy. Il implémente l’algorithme complet de GPT dans un fichier unique de 200 lignes, sans dépendance externe
  • La différence avec les LLM de production tient seulement à l’échelle et à l’efficacité ; le cœur reste identique, et comprendre ce code revient à comprendre l’essence algorithmique de GPT
  • Inclut le dataset, le tokenizer, le moteur d’autograd, une architecture Transformer proche de GPT-2, l’optimiseur Adam, ainsi que les boucles d’entraînement et d’inférence
  • Présenté comme l’aboutissement de 10 ans de travail de simplification des LLM à travers des projets comme micrograd, makemore et nanogpt, il condense l’essence de GPT dans sa forme minimale, impossible à simplifier davantage
  • Entraîné sur 32 000 noms, il génère de nouveaux noms plausibles en effectuant tous les calculs directement via un autograd scalaire
  • Le processus d’entraînement suit calcul de la perte → rétropropagation → mise à jour Adam et peut s’exécuter en environ une minute

Vue d’ensemble de microgpt

  • microgpt est un script Python de 200 lignes qui implémente entièrement le processus d’entraînement et d’inférence d’un modèle GPT
    • Sans bibliothèque externe, il inclut le dataset, le tokenizer, l’autograd, le modèle, l’optimiseur et la boucle d’entraînement
  • Il fusionne des projets existants comme micrograd, makemore et nanogpt dans un fichier unique
  • Une implémentation qui ne conserve que le noyau algorithmique, à un niveau où « on ne peut plus simplifier davantage »
  • Le code complet est disponible sur GitHub Gist, page web et Google Colab

Composition du dataset

  • Le carburant des grands modèles de langage est un flux de données textuelles ; en production on utilise des pages web issues d’Internet, mais microgpt s’appuie ici sur un exemple simple contenant 32 000 noms, un par ligne
  • Chaque nom est traité comme un « document », et l’objectif du modèle est d’apprendre les motifs statistiques présents dans les données afin de générer de nouveaux documents similaires
  • Une fois l’entraînement terminé, le modèle « hallucine » de nouveaux noms plausibles comme "kamon", "karai" ou "vialan"
  • Du point de vue de ChatGPT, une conversation avec l’utilisateur n’est elle aussi qu’un « document à l’allure particulière » ; en initialisant un document avec un prompt, la réponse du modèle correspond à une complétion statistique de document

Tokenizer

  • Les réseaux neuronaux travaillent sur des nombres et non sur des caractères ; il faut donc une méthode pour convertir le texte en séquences d’ID de tokens entiers, puis les reconstruire
  • Les tokenizers de production comme tiktoken (utilisé par GPT-4) opèrent sur des segments de caractères pour plus d’efficacité, mais le tokenizer le plus simple consiste à attribuer un entier à chaque caractère unique du dataset
  • On trie les lettres minuscules de a à z et on attribue à chaque caractère un ID via son index ; la valeur entière elle-même n’a pas de signification, chaque token étant un symbole discret distinct
  • On ajoute le token spécial BOS (Beginning of Sequence) pour signaler qu’« un nouveau document commence/se termine », et "emma" est ainsi encadré comme [BOS, e, m, m, a, BOS]
  • La taille finale du vocabulaire est de 27 (26 lettres minuscules + 1 BOS)

Différenciation automatique (Autograd)

  • L’entraînement d’un réseau neuronal nécessite des gradients : pour chaque paramètre, il faut savoir « si l’on augmente légèrement cette valeur, la perte monte-t-elle, baisse-t-elle, et de combien ? »
  • Le graphe de calcul possède de nombreuses entrées (paramètres du modèle et tokens d’entrée), mais converge vers une sortie scalaire unique : la perte (loss)
  • La rétropropagation (Backpropagation) part de la sortie et remonte le graphe en sens inverse, en s’appuyant sur la règle de chaîne du calcul différentiel pour calculer le gradient de la perte par rapport à toutes les entrées
  • Implémentation avec la classe Value : chaque Value encapsule un scalaire unique (.data) et garde la trace de la manière dont il a été calculé
    • Lors d’opérations comme l’addition ou la multiplication, une nouvelle Value mémorise les entrées (_children) et les dérivées locales de l’opération (_local_grads)
    • Exemple : __mul__ enregistre ∂(a·b)/∂a=b et ∂(a·b)/∂b=a
  • Blocs d’opérations pris en charge : addition, multiplication, puissance, log, exp, ReLU
  • La méthode backward() parcourt le graphe en ordre topologique inverse et applique la règle de chaîne à chaque étape
    • Elle démarre au nœud de perte avec self.grad = 1 (∂L/∂L=1)
    • Puis multiplie les gradients locaux le long des chemins pour les propager jusqu’aux paramètres
  • Accumulation avec += (et non affectation) : quand le graphe se ramifie, des gradients provenant de chaque branche doivent circuler indépendamment puis être additionnés (conséquence de la règle de chaîne en plusieurs variables)
  • C’est algorithmquement identique à .backward() de PyTorch, mais cela fonctionne au niveau scalaire plutôt qu’au niveau tenseur, ce qui rend le tout bien plus simple mais moins efficace

Initialisation des paramètres

  • Les paramètres constituent la connaissance du modèle : un vaste ensemble de nombres à virgule flottante, initialisés aléatoirement puis optimisés de manière répétée pendant l’entraînement
  • Ils sont initialisés avec de petites valeurs aléatoires issues d’une distribution gaussienne
  • Ils sont organisés en matrices nommées dans le state_dict : table d’embedding, poids d’attention, poids du MLP, projection de sortie finale
  • Réglage des hyperparamètres :
    • n_embd = 16 : dimension d’embedding
    • n_head = 4 : nombre de têtes d’attention
    • n_layer = 1 : nombre de couches
    • block_size = 16 : longueur maximale de séquence
  • Ce petit modèle compte 4 192 paramètres (GPT-2 en compte 1,6 milliard, et les LLM modernes plusieurs centaines de milliards)

Architecture

  • L’architecture du modèle est une fonction sans état : elle reçoit des tokens, des positions, des paramètres et les clés/valeurs mises en cache des positions précédentes, puis renvoie les logits (scores) pour le token suivant
  • Suit GPT-2 avec quelques simplifications : RMSNorm (au lieu de LayerNorm), pas de biais, ReLU (au lieu de GeLU)
  • Fonctions utilitaires

    • linear : calcule un produit scalaire pour chaque ligne de la matrice de poids via une multiplication matrice-vecteur ; c’est la transformation linéaire apprise qui constitue la brique de base d’un réseau de neurones
    • softmax : convertit des scores bruts (logits) en distribution de probabilités ; toutes les valeurs se retrouvent dans l’intervalle [0,1] et leur somme vaut 1 ; pour la stabilité numérique, on soustrait d’abord la valeur maximale
    • rmsnorm : remet à l’échelle un vecteur pour qu’il ait une racine de la moyenne des carrés unitaire, ce qui évite que les activations ne grossissent ou ne diminuent au fil du réseau et stabilise l’apprentissage
  • Structure du modèle

    • Embeddings : l’ID du token et l’ID de position référencent chacun une ligne dans leurs tables d’embedding respectives (wte, wpe) ; on additionne les deux vecteurs pour encoder à la fois ce qu’est le token et il se trouve dans la séquence
      • Les LLM modernes sautent souvent les embeddings de position et utilisent des techniques de positionnement relatif comme RoPE
    • Bloc d’attention : le token courant est projeté en trois vecteurs Q (query), K (key) et V (value)
      • Query : « qu’est-ce que je cherche ? », key : « qu’est-ce que je contiens ? », value : « qu’est-ce que je fournis si je suis sélectionné ? »
      • Exemple : dans « emma », lorsque le second « m » prédit la suite, il peut apprendre une query du type « quelle voyelle est apparue récemment ? » ; le « e » précédent correspond bien à cette query et obtient donc un poids d’attention élevé
      • Les clés et les valeurs sont ajoutées au cache KV, ce qui permet de référencer les positions précédentes
      • Chaque tête d’attention calcule le produit scalaire entre la query et toutes les clés mises en cache (mis à l’échelle par √d_head), obtient les poids d’attention via softmax, puis calcule une somme pondérée des valeurs en cache
      • Les sorties de toutes les têtes sont concaténées puis projetées par attn_wo
      • Le bloc d’attention est le seul endroit où le token en position t peut « voir » les tokens passés 0..t-1 ; l’attention est le mécanisme de communication entre tokens
    • Bloc MLP : un réseau feedforward à 2 couches : expansion à 4 fois la dimension d’embedding → application de ReLU → réduction à nouveau
      • C’est là que s’effectue l’essentiel de la « réflexion » à chaque position
      • Contrairement à l’attention, le calcul y est entièrement local au temps t
      • Le Transformer alterne communication (attention) et calcul (MLP)
    • Connexions résiduelles : les blocs d’attention et de MLP rajoutent tous deux leur sortie à leur entrée
      • Cela permet aux gradients de traverser directement le réseau et rend possible l’apprentissage de modèles profonds
    • Sortie : l’état caché final est projeté via lm_head vers la taille du vocabulaire afin de produire un logit par token (ici 27 valeurs) ; un logit élevé = forte probabilité que ce token vienne ensuite
    • Particularité du cache KV : utiliser un cache KV pendant l’apprentissage est rare, mais comme microgpt ne traite qu’un token à la fois, il est construit explicitement ; les clés et valeurs en cache sont des nœuds Value actifs du graphe de calcul, donc inclus dans la rétropropagation

Boucle d’apprentissage

  • La boucle d’apprentissage répète : (1) sélection d’un document → (2) exécution du modèle en passe avant sur les tokens → (3) calcul de la perte → (4) obtention des gradients par rétropropagation → (5) mise à jour des paramètres
  • Tokenisation

    • À chaque étape d’apprentissage, un document est sélectionné puis encapsulé par BOS des deux côtés : « emma » → [BOS, e, m, m, a, BOS]
    • L’objectif du modèle est de prédire chaque token suivant à partir des tokens précédents
  • Passe avant et perte

    • Les tokens sont fournis au modèle un par un, ce qui construit le cache KV
    • À chaque position, le modèle produit 27 logits, convertis en probabilités par softmax
    • La perte à chaque position est la log-probabilité négative du bon token suivant : −log p(target), ce qu’on appelle la perte d’entropie croisée
    • La perte mesure à quel point le modèle est surpris par ce qui arrive réellement : si la probabilité assignée est 1,0, la perte est 0 ; si elle est proche de 0, la perte tend vers +∞
    • On fait la moyenne des pertes sur toutes les positions du document pour obtenir une perte scalaire unique
  • Passe arrière

    • Un seul appel à loss.backward() exécute la rétropropagation sur l’ensemble du graphe de calcul
    • Ensuite, .grad de chaque paramètre indique comment il doit changer pour réduire la perte
  • Optimiseur Adam

    • Au lieu d’une simple descente de gradient (p.data -= lr * p.grad), on utilise Adam
    • Deux moyennes mobiles sont conservées par paramètre :
      • m : moyenne des gradients récents (momentum)
      • v : moyenne des carrés récents des gradients (adaptation du taux d’apprentissage par paramètre)
    • m_hat et v_hat sont les versions corrigées du biais de m et v initialisés à 0
    • Le taux d’apprentissage décroît linéairement pendant l’entraînement
    • Après la mise à jour, on réinitialise avec .grad = 0
  • Résultats de l’apprentissage

    • Sur 1 000 étapes, la perte passe d’environ 3,3 (devinette aléatoire parmi 27 tokens : −log(1/27)≈3,3) à environ 2,37
    • Plus c’est bas, mieux c’est, et le minimum est 0 (prédiction parfaite) ; il reste donc une marge de progression, mais il est clair que le modèle apprend les motifs statistiques des prénoms

Inférence

  • Une fois l’apprentissage terminé, on peut échantillonner de nouveaux prénoms à partir du modèle ; les paramètres sont figés puis la passe avant est exécutée en boucle, chaque token généré étant réinjecté comme entrée suivante
  • Processus d’échantillonnage

    • Chaque échantillon commence par le token BOS (« début d’un nouveau prénom »)
    • Le modèle génère 27 logits → conversion en probabilités → échantillonnage aléatoire d’un token selon ces probabilités
    • Ce token est réinjecté comme entrée suivante, et on répète jusqu’à ce que le modèle génère de nouveau BOS (« terminé ») ou que la longueur maximale de séquence soit atteinte
  • Température (Temperature)

    • Avant le softmax, les logits sont divisés par la température
    • Température 1.0 : échantillonnage direct depuis la distribution apprise par le modèle
    • Température basse (ex. 0.5) : rend la distribution plus piquée, ce qui pousse le modèle à faire des choix de tête plus conservateurs
    • Température proche de 0 : sélectionne toujours l’unique token le plus probable (greedy decoding)
    • Température élevée : aplatit la distribution, produisant des sorties plus variées mais moins cohérentes

Exécution

  • Python seulement nécessaire (pas de pip install, aucune dépendance) : python train.py
  • Environ 1 minute sur un MacBook
  • Affiche la perte à chaque étape : d’environ 3,3 (aléatoire) à environ 2,37
  • Une fois l’apprentissage terminé, génère de nouveaux prénoms hallucinés : « kamon », « ann », « karai », etc.
  • Peut aussi s’exécuter dans un notebook Google Colab, et il est possible de poser des questions à Gemini
  • On peut essayer d’autres jeux de données, augmenter num_steps pour entraîner plus longtemps, ou augmenter la taille du modèle pour de meilleurs résultats

Étapes d’évolution du code

fichier ajout
train0.py table de comptage de bigrammes — pas de réseau de neurones, pas de gradients
train1.py MLP + gradients manuels (numériques et analytiques) + SGD
train2.py Autograd (classe Value) — remplace les gradients manuels
train3.py embeddings de position + attention à tête unique + rmsnorm + résidus
train4.py attention multi-têtes + boucle sur les couches — architecture GPT complète
train5.py optimiseur Adam — c’est train.py
  • Toutes les versions et les diff entre chaque étape sont disponibles dans les Revisions du Gist build_microgpt.py

Différences avec les LLM de production

  • microgpt contient l’essence algorithmique complète de l’entraînement et de l’exécution d’un GPT ; la différence avec un LLM de production comme ChatGPT ne tient pas à l’algorithme de base, mais aux éléments qui lui permettent de fonctionner à grande échelle
  • Données

    • au lieu de 32K noms courts, entraînement sur des milliers de milliards de tokens de texte issus d’Internet (pages web, livres, code, etc.)
    • déduplication des données, filtrage de qualité et mélange soigneux entre différents domaines
  • Tokenizer

    • au lieu de caractères isolés, utilisation d’un tokenizer en sous-mots comme BPE (Byte Pair Encoding)
    • fusionne les séquences de caractères apparaissant fréquemment ensemble en un seul token ; un mot courant comme "the" devient un token unique, tandis que les mots rares sont découpés en fragments
    • vocabulaire d’environ 100K tokens, bien plus efficace car davantage de contenu est vu à chaque position
  • Autograd

    • au lieu d’objets Value scalaires en Python pur, utilisation de tenseurs (grands tableaux multidimensionnels de nombres), exécutés sur des GPU/TPU capables d’effectuer des milliards d’opérations en virgule flottante par seconde
    • PyTorch gère l’autograd sur les tenseurs, et des noyaux CUDA comme FlashAttention fusionnent plusieurs opérations
    • les mathématiques restent les mêmes, mais beaucoup de scalaires sont traités en parallèle
  • Architecture

    • microgpt : 4 192 paramètres ; un modèle de niveau GPT-4 : des centaines de milliards
    • globalement, le réseau neuronal Transformer est très similaire, mais bien plus large (dimension d’embedding 10 000+) et bien plus profond (100+ couches)
    • types de briques Lego supplémentaires et ordre modifié :
      • RoPE (rotary positional embeddings) — à la place des embeddings positionnels appris
      • GQA (grouped query attention) — pour réduire la taille du cache KV
      • activations linéaires gated — à la place de ReLU
      • couches MoE (mixture of experts)
    • la structure de base reste largement préservée : l’attention (communication) et le MLP (calcul) alternent sur le flux résiduel
  • Entraînement

    • au lieu d’un document par étape, utilisation de grands batches (des millions de tokens par étape), accumulation de gradients, précision mixte (float16/bfloat16), et réglage soigneux des hyperparamètres
    • l’entraînement des modèles de pointe mobilise des milliers de GPU pendant plusieurs mois
  • Optimisation

    • microgpt : Adam + simple décroissance linéaire du taux d’apprentissage
    • à grande échelle, l’optimisation est un domaine à part entière : précision réduite (bfloat16, fp8), entraînement sur de grands clusters de GPU
    • les réglages de l’optimiseur (taux d’apprentissage, weight decay, paramètres bêta, planning de warmup/décroissance) doivent être ajustés très finement ; les bonnes valeurs dépendent de la taille du modèle, de la taille du batch et de la composition du dataset
    • les lois de scaling (par ex. Chinchilla) guident la répartition d’un budget de calcul fixe entre taille du modèle et nombre de tokens d’entraînement
    • à grande échelle, se tromper sur ces détails peut gaspiller des millions de dollars de calcul, d’où de nombreuses expériences à petite échelle avant un entraînement complet
  • Post-entraînement

    • le modèle de base issu de l’entraînement (modèle « préentraîné ») est un compléteur de documents, pas un chatbot
    • la transformation en ChatGPT se fait en deux étapes :
      • SFT (supervised fine-tuning) : on remplace les documents par des conversations curées et on poursuit l’entraînement, sans changement algorithmique
      • RL (reinforcement learning) : le modèle génère une réponse → une note est attribuée (par des humains, un modèle « juge » ou un algorithme) → il apprend à partir de ce retour
    • fondamentalement, il s’entraîne toujours sur des documents, sauf que ces documents sont désormais composés de tokens produits par le modèle lui-même
  • Inférence

    • servir le modèle à des millions d’utilisateurs nécessite sa propre stack d’ingénierie : batching des requêtes, gestion et pagination du cache KV (vLLM, etc.), décodage spéculatif pour la vitesse, quantification pour réduire la mémoire (exécution en int8/int4), répartition du modèle sur plusieurs GPU
    • au fond, il s’agit toujours de prédire le token suivant dans une séquence, mais beaucoup d’efforts d’ingénierie servent à le faire plus vite

FAQ

  • Le modèle « comprend-il » quelque chose ?

    • question philosophique, mais mécaniquement : il ne se passe rien de magique
    • le modèle est une grande fonction mathématique qui mappe des tokens d’entrée vers une distribution de probabilité sur le token suivant
    • pendant l’entraînement, les paramètres sont ajustés pour rendre le bon token suivant plus probable
    • savoir si cela constitue une « compréhension » dépend de chacun, mais le mécanisme tient entièrement dans ces 200 lignes
  • Pourquoi cela fonctionne-t-il ?

    • le modèle dispose de milliers de paramètres ajustables, et l’optimiseur les déplace légèrement à chaque étape pour réduire la perte
    • au fil de nombreuses étapes, les paramètres se stabilisent vers des valeurs qui capturent les régularités statistiques des données
    • dans le cas des noms : ils commencent souvent par une consonne, "qu" tend à apparaître ensemble, trois consonnes de suite sont rares, etc.
    • le modèle n’apprend pas des règles explicites, mais une distribution de probabilité qui reflète ces régularités
  • Quel rapport avec ChatGPT ?

    • ChatGPT reprend cette même boucle centrale (prédiction du token suivant, sampling, répétition), la pousse à une échelle immense et y ajoute un post-entraînement pour la rendre conversationnelle
    • en mode chat, le prompt système, les messages utilisateur et les réponses ne sont tous que des tokens dans une séquence
    • le modèle complète un document un token à la fois, exactement comme microgpt complète un nom
  • Qu’est-ce qu’une « hallucination » ?

    • le modèle génère des tokens en échantillonnant dans une distribution de probabilité
    • il n’a aucune notion de vérité et ne connaît que les séquences statistiquement plausibles au regard de ses données d’entraînement
    • quand microgpt « hallucine » un nom comme "karia", c’est le même phénomène que lorsque ChatGPT énonce avec assurance un fait faux
    • dans les deux cas, il s’agit d’une complétion plausible en apparence, mais non réelle
  • Pourquoi est-ce si lent ?

    • microgpt traite un scalaire à la fois en Python pur, et une seule étape d’entraînement prend plusieurs secondes
    • sur GPU, les mêmes mathématiques traitent des millions de scalaires en parallèle, avec un gain de vitesse de plusieurs ordres de grandeur
  • Peut-on lui faire générer de meilleurs noms ?

    • oui : en l’entraînant plus longtemps (augmenter num_steps), en augmentant la taille du modèle (n_embd, n_layer, n_head) ou en utilisant un dataset plus grand
    • ce sont les mêmes leviers de réglage importants à grande échelle
  • Et si on change le dataset ?

    • le modèle apprendra n’importe quel motif présent dans les données
    • si on le remplace par des noms de villes, de Pokémon, des mots anglais ou un fichier de courts poèmes, il apprendra à générer cela à la place
    • aucun autre changement du code n’est nécessaire

Aucun commentaire pour le moment.

Aucun commentaire pour le moment.