Pourquoi j'ai créé LLM from First Principles

Sur cette page

English · 中文 · Français

Note d’écriture : la motivation et le projet sont les miens. ChatGPT m’a aidé pour la rédaction, la révision, la traduction, ainsi que pour le schéma et le code source de l’animation originale.

Réécrire du code soi-même à l’époque des agents de programmation

Pendant ma recherche d’emploi, j’ai pris conscience d’un changement assez inconfortable dans mon travail quotidien. J’utilisais des LLM en permanence. Avec l’aide des agents de programmation, j’écrivais moi-même de moins en moins de code.

C’était pratique. Mais cela me faisait aussi peur, ou du moins me mettait profondément mal à l’aise. J’avais passé des années à étudier les mathématiques, le machine learning et la construction de systèmes d’IA. Désormais, je pouvais confier une implémentation à un agent et obtenir du code fonctionnel avant même d’avoir réfléchi jusqu’au bout aux raisons de chaque choix. Que deviendrait ma propre compréhension si je continuais ainsi ?

Je ne pense pas que taper du code à la main fasse automatiquement de quelqu’un un meilleur chercheur ou ingénieur. Ce texte n’est pas non plus un réquisitoire contre les agents de programmation. Mon inquiétude était plus personnelle : je ne voulais pas dépendre toujours davantage d’outils dont je perdais l’habitude de questionner les décisions.

C’est de ce malaise qu’est né LLM from First Principles.

Reprendre l’habitude de coder moi-même

Mon idée de départ était simple : reprendre les mécanismes des modèles de langage que j’utilise tous les jours, comprendre pourquoi ils sont conçus de cette façon, puis les implémenter moi-même. C’était aussi une occasion de réviser des notions déjà étudiées, notamment pendant mon doctorat, et devenues si familières que je ne m’y arrêtais plus vraiment.

Je voulais un critère plus exigeant que reconnaître une formule ou trouver une explication convaincante. Suis-je capable de prévoir la forme d’un tenseur avant d’exécuter la cellule suivante ? D’expliquer pourquoi un gradient atteint tel paramètre mais pas tel autre ? De savoir ce qui change si je supprime une opération ?

Dans ce projet, from first principles signifie remonter de l’implémentation vers ses raisons : identités mathématiques, hypothèses, choix de modélisation et contraintes techniques. Cela ne signifie pas que chaque décision dans l’entraînement des LLM peut être déduite de manière unique de quelques axiomes. Souvent, il s’agit plutôt de comprendre pourquoi une option a été retenue et quelles autres options restent possibles.

Peu à peu, j’ai adopté une manière de travailler : poser une question, dériver un mécanisme, écrire le code, observer son comportement, puis essayer de l’expliquer. Il arrive que l’observation me ramène à la question de départ.

Une illustration de ma démarche d'apprentissage, et non l'enregistrement d'un entraînement de modèle. Animation réalisée avec ManimGX.

Des tenseurs au post-training

Le dépôt rassemble aujourd’hui 28 notebooks et un petit ensemble de composants PyTorch réutilisables. Le parcours commence par les tenseurs, le broadcasting et la différentiation automatique. Il construit ensuite un GPT de type decoder-only, puis aborde le préentraînement et le post-training.

Les premiers notebooks abordent autograd, la tokenisation, l’optimisation et la boucle d’entraînement. Les suivants construisent l’attention, les représentations de position, un petit GPT, la génération, le cache KV, l’attention linéaire, le préentraînement et les MoE. Les derniers relient les fondements du RL et PPO au SFT, à l’apprentissage des préférences, à DPO et au RL relatif au groupe.

Les notebooks sont numérotés de 00 à 26, avec deux étapes pour PPO : 21A et 21B. Ils conservent les dérivations, les petites expériences et les observations enregistrées. Les éléments réutilisables, une fois compris, sont regroupés dans src/llmfp/. Un exemple autonome montre comment en assembler certains. Il s’agit avant tout d’un projet pédagogique, pas d’un framework de production pour LLM.

En implémentant ces mécanismes, j’ai redécouvert des détails que le simple nom d’un algorithme ne révèle pas. En SFT, par exemple, le prompt sert de contexte, mais seuls les tokens de la réponse peuvent contribuer à la loss. Il faut décaler les labels et appliquer le masque aux bonnes positions. Si je décale les labels deux fois, le programme peut encore s’exécuter tout en optimisant de mauvaises cibles.

PPO m’a aussi obligé à distinguer deux rôles : l’ancienne politique, dont on a enregistré les probabilités lors de l’échantillonnage, et la politique de référence, utilisée pour la régularisation KL. Dans le code, les deux peuvent apparaître sous forme de log-probabilités fixes. Pourtant, elles ne désignent pas la même chose. Écrire les interfaces et les vérifier m’a obligé à maintenir cette distinction.

Ce ne sont pas des découvertes spectaculaires. Ce sont précisément les détails que je ne voulais plus considérer comme allant de soi.

Une loss plus faible, sans meilleures réponses

Une petite expérience du notebook 25, consacré à DPO, résume assez bien l’esprit du dépôt.

Dans une exécution enregistrée, un petit GPT a reçu des mises à jour sur trois paires synthétiques de réponses préférées et rejetées. Sa loss DPO sur l’entraînement est passée d’environ 0,693 à 0,015. Pourtant, avec un décodage greedy sur six consignes jouets, il produisait la réponse attendue 2 fois sur 6 avant l’entraînement, et toujours 2 fois sur 6 après.

Pour la consigne Say no, le modèle répondait d’abord Yes, puis Thanks après DPO. Or la réponse attendue était No.

La marge de préférence de cette paire d’entraînement s’était bien améliorée. Mais augmenter un écart de scores relatif à une politique de référence, sur deux réponses complètes, n’est pas la même chose que générer effectivement la bonne réponse token par token. La loss indiquait que l’objectif de préférence était mieux ajusté sur les exemples fournis. Le comportement à la génération devait être évalué séparément.

Ce sont les résultats enregistrés d’une expérience pédagogique minuscule, ni un benchmark ni une conclusion sur DPO à grande échelle. Je tenais à les conserver : un test de comportement peu concluant peut parfois apprendre davantage qu’une jolie courbe de loss.

Apprendre avec ChatGPT, sans lui faire écrire l’exercice

Il y a une contradiction apparente : je voulais moins dépendre aveuglément du code généré par les LLM, et j’ai pourtant énormément utilisé ChatGPT pendant ce projet.

Je m’en suis servi comme d’un tuteur. Nous avons discuté des dérivations ; j’ai contesté certaines explications, comparé des choix d’implémentation et utilisé des revues de code pour repérer des erreurs de raisonnement. Mais dans les exercices des notebooks, j’ai désactivé la complétion de code par IA. J’ai tapé moi-même les implémentations, après avoir réfléchi à ce qu’elles devaient faire.

Cette règle a une portée précise. Elle ne signifie pas que la maintenance du dépôt, l’extraction de composants réutilisables ou les exemples d’intégration n’ont jamais bénéficié d’une assistance par IA. Elle ne m’oblige pas non plus à réimplémenter éternellement les opérations déjà comprises au lieu d’utiliser les primitives éprouvées de PyTorch. Une fois un mécanisme assimilé, je peux passer au suivant.

J’essayais de respecter une règle assez simple :

Si je ne peux pas expliquer une ligne de code, elle n’a pas encore sa place dans mon exercice d’apprentissage.

L’IA permet d’obtenir une réponse rapidement. Je voulais continuer à faire moi-même l’autre partie du travail : déterminer si je l’avais réellement comprise.

Pourquoi partager ce projet

Le code est disponible sur github.com/neuromaancer/llm-from-first-principles. Pour voir les composants fonctionner ensemble avant de parcourir les notebooks, le dépôt contient un petit exemple CPU fondé sur des données synthétiques, sans téléchargement de checkpoint ni clé API :

git clone https://github.com/neuromaancer/llm-from-first-principles.git
cd llm-from-first-principles
uv sync
uv run python examples/tiny_sft_dpo.py

L’exemple part d’un GPT aux poids aléatoires, entraîne les réponses préférées de façon supervisée, fige une référence, applique DPO et affiche les générations. Il montre comment assembler des composants ; l’ajustement sur ces quelques exemples ne prouve aucune capacité de généralisation.

Je partage ce dépôt parce que je suppose que d’autres reconnaîtront peut-être l’inquiétude qui l’a fait naître. Nous utilisons des modèles de langage pour écrire, faire de la recherche et programmer, souvent sans avoir besoin d’en examiner les mécanismes. C’est un vrai avantage. Mais certains ont aussi envie de comprendre l’outil qu’ils utilisent désormais chaque jour. J’espère que ces notebooks proposeront une manière concrète de s’y remettre.

C’est également pour cela que j’ai commencé la série de blog First Principles of LLMs & RL. Le dépôt me permet de vérifier que je sais construire un mécanisme ; les articles, que je peux expliquer pourquoi il fonctionne. Faire tourner le code ne suffit pas à accomplir ces deux tâches.

Je continuerai d’utiliser des agents de programmation. Mais je veux pouvoir dire ce que je leur délègue, comprendre et discuter leurs choix, et reconnaître les moments où je dois reprendre moi-même le problème.