🔧 L'architecture est un seuil, pas un levier — ce que j'ai appris en optimisant un LLM français de 15M de paramĂštres đŸ‡«đŸ‡·

Community Article
Published June 29, 2026

đŸ‘€ Contexte — oĂč on en Ă©tait

Si tu as lu le premier article : LLM français de 15M de paramÚtres, construit from scratch, en solo, sur une GTX 1080 Ti. Architecture style LLaMA, BPE 32k custom, Wikipedia français réécrit par IA. Coupure de courant à l'epoch 10/18.

Le modĂšle avait appris la forme, pas le fond. Il Ă©crivait un français parfait, du markdown structurĂ©, une grammaire fluide — mais hallucine les faits et dĂ©rive hors sujet aprĂšs ~200 tokens.

Cet article est le suivi honnĂȘte : une semaine entiĂšre de mesure → correction → mesure, sans nouvelle architecture, sans GPU plus puissant. Juste extraire systĂ©matiquement tout ce qu'il reste dans le modĂšle 15M. La conclusion dĂšs maintenant : l'architecture n'a jamais Ă©tĂ© le goulot d'Ă©tranglement. C'Ă©tait les donnĂ©es et le dĂ©codage.


đŸȘœ Le modĂšle mental — l'architecture est un seuil, pas un levier

Le plus grand changement de perspective de la semaine.

Tout le monde s'obsĂšde sur l'architecture parce qu'elle est visible et copiable — RoPE, SwiGLU, GQA, papers, courbes. Les donnĂ©es et l'objectif d'entraĂźnement sont sales, ennuyeux, invisibles. Alors on optimise ce qu'on voit — comme chercher ses clĂ©s sous le lampadaire parce que c'est lĂ  qu'il y a de la lumiĂšre.

Mais voilà la vérité à 15M de paramÚtres :

L'architecture est un seuil, pas un levier. En dessous d'un certain niveau elle te bloque. Au-dessus — et un transformer pre-norm propre avec weight tying est au-dessus — elle ne bouge presque plus le curseur. Le goulot migre vers les donnĂ©es et l'objectif.

J'ai donc construit une échelle de priorité, du ROI le plus élevé au plus faible :

Niveau Levier Verdict
1 Code / mesure le multiplicateur de tout le reste
2 Efficacité des données le plus sous-estimé
3 Réglage de l'entraßnement incrémental mais sûr
4 Vitesse / VRAM itérer plus vite
5 Algo / innovation le fun — mais en dernier

L'insight clé : l'innovation (niveau 5) est inutile sans mesure (niveau 1). Une innovation qu'on ne peut pas quantifier, c'est de la foi, pas de la recherche. Donc on construit l'instrument d'abord.


đŸ—ïž Niveau 0 — corriger les bugs d'architecture d'abord

Avant d'optimiser, j'ai auditĂ© gpt2.py. Plusieurs vrais bugs cachĂ©s en pleine vue — et un seul avait vraiment de l'importance.

Celui qui changeait rĂ©ellement les sorties : repetition_penalty Ă©tait Ă  l'envers. Il divisait tous les logits par la pĂ©nalitĂ©. Pour un logit nĂ©gatif, diviser le rend plus probable — exactement l'inverse de l'intention. Donc la feature censĂ©e rĂ©duire la rĂ©pĂ©tition l'encourageait en partie. SĂ©mantique HF : logit > 0 ? /penalty : *penalty. C'est le seul qui a visiblement amĂ©liorĂ© la gĂ©nĂ©ration ; les autres sont des corrections de correction/stabilitĂ©.

Les suivants :

  • 🐛 Pas de scaled init GPT-2 — les projections rĂ©siduelles n'Ă©taient pas scalĂ©es par 1/√(2·n_layer), ce qui maintient la variance du residual stream bornĂ©e avec la profondeur. Affecte la stabilitĂ© de l'entraĂźnement, seulement sur un run from scratch.
  • 🐛 KV-cache n'utilisait jamais SDPA — la gĂ©nĂ©ration (use_cache=True) tombait sur l'attention manuelle lente. Correction de vitesse, pas de qualitĂ©.
  • 🐛 IncompatibilitĂ© torch 2.3.1 — add_safe_globals n'existe pas avant la 2.4 ; from_pretrained crashait au chargement. Correction de crash.

Validé avec la suite de tests d'architecture (tests/test_architecture.py) : 10/10, dont "Flash vs Manual equivalence" et "KV-cache vs no-cache match". Le refactoring a changé comment les choses se calculent, pas ce qu'elles produisent.


đŸ§č Niveaux 1+2 — les donnĂ©es me mentaient

La réécriture du document packing

Le prepare_data.py original découpait chaque document en tranches de 450 tokens à des offsets arbitraires, paddait à 512 (~12% de padding gaspillé), sans marqueurs <bos>/<eos>.

La conséquence c'était la dérive thématique. Le modÚle avait vu des millions de fragments coupés en pleine phrase, jamais un document entier, propre, délimité. Il n'avait donc jamais appris qu'un texte a un début, un développement et une fin.

Correction : document packing (style GPT-3 / LLaMA). Envelopper chaque doc dans <bos> 
 <eos>, tout concaténer en un flux continu de tokens, découper en blocs pleins de 768. Zéro padding, zéro coupure en pleine phrase, frontiÚres de documents explicites.

Avant (chunking) :  354 148 blocs de 512, ~12% padding, pas de frontiĂšres
AprĂšs (packing)  :  349 490 blocs de 768, 0% padding, <bos>/<eos> partout
Tokens perdus    :  527 (0,000%)

Le corpus était plein d'emojis

En lisant les fichiers rĂ©els, c'est apparu : ~30% du corpus Ă©tait polluĂ© par des emojis dĂ©coratifs (un article de thĂ©ologie avait un 🕌 ou ✹ aprĂšs presque chaque proposition). Ajoutez des espaces insĂ©cables Ă©troites (U+202F) partout et des guillemets droits avec un espacement bizarre.

scripts/clean_corpus.py : supprimer les emojis, normaliser les espaces spĂ©ciaux, convertir les guillemets droits " en guillemets français « » en les appairisant — tout en protĂ©geant les blocs de code (tout ce qui est dans des fences ``` ou des backticks inline reste intact, donc le HTML class="x" survit).

Ça a rĂ©cupĂ©rĂ© ~3,5M tokens de bruit pur : 271,9M → 268,4M tokens de français rĂ©el.

La vérité dure sur les données

Puis j'ai lu assez de fichiers pour voir le vrai problĂšme, celui qu'aucun nettoyage ne peut corriger :

Le corpus est la longue traĂźne de Wikipedia. Un fichier = un sujet, vu exactement une fois. Petits villages (BaranĂłw, Oderaue), plantes obscures (Aruncus dioicus), inconnus. La qualitĂ© du texte est excellente — mais chaque fait est un hapax.

C'est pourquoi le modĂšle hallucine. Il voit Synlycostrobus une fois → il ne peut pas ancrer le fait → il invente. Structure cohĂ©rente (152k exemples de "comment Ă©crire un article") mais faits ratĂ©s (chaque fait vu une seule fois). Pas un bug. La nature du corpus + le plafond Ă  15M.

J'ai 100k articles nettoyĂ©s supplĂ©mentaires prĂȘts. Je ne les utilise pas Ă  15M — ils sont toujours en format un-sujet-par-fichier. On l'a dĂ©jĂ  prouvĂ© : passer de 20% → 60% des donnĂ©es a amĂ©liorĂ© le style/PPL (30 → 17) mais pas la factualitĂ©. Plus d'hapax = plus de style, mĂȘme hallucination. Mesurer avant d'agir.


📊 Niveau 1 — le harnais de mesure

Le multiplicateur. scripts/evaluate.py : donne-lui un checkpoint, il retourne des chiffres comparables :

  • val_ppl — perplexitĂ© sur l'ensemble de validation
  • repetition_3gram — % de trigrammes rĂ©pĂ©tĂ©s (plus bas = mieux)
  • distinct_2 — bigrammes uniques / total (diversitĂ©, plus haut = mieux)
  • coherence_len — tokens gĂ©nĂ©rĂ©s avant la dĂ©rive (un heading # ou une boucle de rĂ©pĂ©tition). Ça quantifie enfin la dĂ©rive que j'estimais Ă  l'Ɠil.

Un piĂšge mĂ©thodologique dans lequel je suis tombĂ© : le sampling sans seed fixe donnait des mĂ©triques bruitĂ©es (le mĂȘme modĂšle oscillait entre cohĂ©rence 30 et 37). Inutilisable pour comparer deux modĂšles. Seed fixe → reproductible et rĂ©aliste.

L'avant/aprĂšs sur les runs :

Run config val PPL coherence_len
A 20% × 1 epoch (corpus sale) 77 —
B 20% × 3 (nettoyĂ©) 30,6 38
C 60% × 6 (nettoyĂ© + packed) 17,8 48

🐛 Le bug de dĂ©rive — deux tokens spĂ©ciaux, des heures de confusion

La meilleure histoire de débogage de la semaine.

Le modĂšle abandonnait le sujet du prompt en dĂ©marrant un nouveau document : prompt "
dans le dĂ©partement du Lot" → "# La commune d'Évreux, nichĂ©e dans les Alpes françaises
". Prose fluide et cohĂ©rente — sur un endroit complĂštement diffĂ©rent.

J'ai d'abord incriminé le token #. Faux. AprÚs beaucoup de tests, deux causes cumulatives, toutes deux liées au document packing :

  1. encode(add_special_tokens=True) ajoutait un <eos> Ă  la FIN du prompt. Le modĂšle, entraĂźnĂ© sur 
<eos><bos># Titre, lisait ça comme "ce document est terminĂ©" → en dĂ©marrait un nouveau. C'Ă©tait le dĂ©clencheur principal.
  2. Le modÚle pouvait émettre <bos> en cours de génération.

Le <bos> Ă©tait invisible dans la sortie (dĂ©codĂ© avec skip_special_tokens=True) mais dĂ©clenchait quand mĂȘme le comportement "nouveau document" — ce qui m'a fait chercher le # pendant des heures.

Correction : supprimer le <eos> final du prompt, et interdire <bos> pendant le décodage (no_new_doc, activé par défaut). Résultat : le modÚle reste sur Saint-Céré et conserve ses sections ## internes.

La leçon, maintenant bug #6 dans mon README : les tokens spĂ©ciaux de l'entraĂźnement (<bos> texte <eos>) ne sont PAS ce qu'on veut Ă  l'infĂ©rence (<bos> texte
). Un classique, mais brutal Ă  trouver dans un codebase from scratch sans framework pour prĂ©venir.


🎰 Niveau 3 — rĂ©gler le sampling (le plus gros gain gratuit)

scripts/tune_sampling.py balaie (temperature, top_k, repetition_penalty) et tout mesure. Le résultat était spectaculaire :

config rep3↓ distinct2↑ cohĂ©rence↑
baseline (t0.8 k50 r1.2) ~0,07 ~0,86 ~38
optimal (t0.8 k40 r1.3) ~0,00 ~0,99 ~76

repetition_penalty est LE levier. Passer de 1,2 → 1,3 a Ă  peu prĂšs doublĂ© la cohĂ©rence (38 → 76) et tuĂ© la rĂ©pĂ©tition — et surtout, le texte est restĂ© naturel (vĂ©rifiĂ© Ă  l'Ɠil, pas que par les mĂ©triques). ZĂ©ro rĂ©entraĂźnement. Le plus grand saut de qualitĂ© de toute la semaine est venu d'une seule valeur de config.

⚠ Note : en dĂ©codage greedy le mĂȘme modĂšle rĂ©pĂšte massivement (rep 0,50), mais en sampling c'est propre (rep 0,07). Les petits modĂšles ont besoin du sampling — greedy les piĂšge en boucles.


đŸ§Ș Niveau 5 — la campagne innovation (tout contraster)

C'est là que je suis passé en mode recherche. L'idée : le contrastive decoding pour pousser la qualité encore plus loin. J'ai mené une campagne complÚte.

Contrastive Decoding (Li et al. 2022)

logits = (1+λ)·expert − λ·amateur, avec la contrainte de plausibilitĂ© (α-masking). Trois variantes de contraste testĂ©es :

Contraste résultat pourquoi
Entre checkpoints (15M vs 15M) ❌ −4 coh distributions trop proches → on soustrait du bruit
DoLa (couche finale vs prĂ©coce) ❌ −3 coh 8 couches trop proches
Classique (15M vs amateur tiny 4.5M) ✅ +1,9 coh vrai Ă©cart de capacitĂ©

Le pattern : Ă  15M, le contraste n'aide que s'il y a un vrai Ă©cart de capacitĂ©. Trop proche (checkpoints, couches) = rien Ă  soustraire. Trop loin (un amateur qui bafouille) = on soustrait du bruit. Il y a une fenĂȘtre Ă©troite.

Le bug qui a failli me donner un faux négatif

Le moment honnĂȘte. Mon premier run CD disait "Ă©chec total, tout dĂ©grade." Un rĂ©flexe de relecteur m'a fait vĂ©rifier le code plutĂŽt que de croire le rĂ©sultat.

Le script CD n'appliquait pas le sampling optimal (rep_pen 1.3) — il comparait un CD non rĂ©glĂ© contre une baseline non rĂ©glĂ©e, les deux en boucle. L'"Ă©chec" Ă©tait un bug mĂ©thodologique, pas un rĂ©sultat. Avec le mĂȘme sampling des deux cĂŽtĂ©s, le CD classique aide lĂ©gĂšrement.

Leçon : un résultat négatif d'un harnais buggé ne vaut rien. Toujours vérifier le code avant de publier la conclusion.

Éval qualitative — ce que les mĂ©triques ne voyaient pas

Les chiffres disaient "+1,9 cohérence". J'ai lu les vraies générations, baseline vs CD, sur des prompts factuels :

  • FactualitĂ© : ❌ aucune amĂ©lioration — les deux inventent autant (capitale de la France ≠ Paris dans les deux cas).
  • PropretĂ© : ✅ le CD supprime les artefacts. Sur "La Seconde Guerre mondiale a durĂ© de", la baseline produisait des emojis-chiffres parasites ("3⃣3⃣7⃣ ans") ; le CD non.
  • FluiditĂ© : ✅ le CD se lit lĂ©gĂšrement mieux, des templates d'articles plus nets.

Le CD nettoie les tics de gĂ©nĂ©ration (l'amateur en a aussi → ils sont soustraits) mais ne peut pas crĂ©er de savoir que l'expert n'a pas. Exactement ce que la thĂ©orie prĂ©dit.


✅ Ce qui a marchĂ©

  • đŸ§č Document packing — a tuĂ© la dĂ©rive structurelle, la plus grande correction de donnĂ©es
  • 🎰 repetition_penalty 1.3 — cohĂ©rence doublĂ©e, gratuit, une valeur de config
  • 🐛 Correction des tokens spĂ©ciaux — le modĂšle reste sur son sujet
  • 📊 Un harnais de mesure reproductible — chaque affirmation est falsifiable
  • đŸ§Ș CD classique — gain qualitatif petit mais rĂ©el

❌ Ce qui n'a pas marchĂ© (et c'est normal — c'est de la recherche)

  • 📉 CD entre checkpoints — distributions trop corrĂ©lĂ©es
  • 📉 DoLa — 8 couches c'est trop peu pour le contraste par couche
  • 🎯 FactualitĂ© — irrĂ©ductible Ă  15M sur un corpus longue traĂźne. Aucune astuce de dĂ©codage n'invente des faits.
  • 📚 +100k articles — mĂȘme format un-sujet-par-fichier, ne changera pas la factualitĂ© Ă  cette taille

⚠ Limites

  • 15M paramĂštres reste un modĂšle de style, pas une base de connaissance
  • Le corpus longue traĂźne plafonne la factualitĂ© quel que soit le dĂ©codage
  • Les vrais gains factuels nĂ©cessitent soit du RAG soit un scale-up Ă  25-30M (avec les donnĂ©es supplĂ©mentaires) — un prochain chapitre

📋 RĂ©fĂ©rence rapide — Ă©tat final

ParamĂštre Valeur
Params ~15M (n_embd 256 / n_layer 8 / n_head 4)
Données nettoyées + document-packed, 268M tokens, blocs de 768
Meilleur run 60% × 6 epochs, val PPL 17,8
Sampling temp 0.8 / top_k 40 / repetition_penalty 1.3
coherence_len 38 → 76 aprĂšs rĂ©glage du sampling
Corrections décodage supprimer le <eos> final, interdire <bos>
Meilleur gain algo CD classique (15M vs amateur 4.5M), +1,9 coh
Harnais d'éval rep3 / distinct2 / coherence_len, seed fixe

🏁 Conclusion

Une semaine de travail et pas une ligne de nouvelle architecture. Les gains sont venus d'endroits ennuyeux : comment les donnĂ©es sont dĂ©coupĂ©es, deux tokens spĂ©ciaux, un hyperparamĂštre de dĂ©codage. La recherche flashy (contrastive decoding, DoLa) a donnĂ© les plus petits gains — et l'un d'eux Ă©tait presque un faux nĂ©gatif causĂ© par mon propre harnais buggĂ©.

La vraie leçon ne concerne pas les LLM français. C'est que une fois que l'architecture passe le seuil, le goulot d'étranglement est partout sauf dans l'architecture. Mesurer d'abord. Lire ses propres données. Vérifier son code avant de croire ses conclusions. Le modÚle n'a jamais été le problÚme.

Prochaine Ă©tape : le seul levier restant au niveau des donnĂ©es — scale-up Ă  25-30M avec le corpus Ă©largi, sur du vrai cloud compute. Mais ça, c'est le prochain article. 🚀


❓ Q&R

— Tu as passĂ© une semaine et la PPL est passĂ©e de 17,8 à
 17,8 ?

Exact — la PPL a plafonnĂ©, parce que le run 60% × 6 avait dĂ©jĂ  atteint le plafond donnĂ©es du modĂšle. Les gains de la semaine sont dans la qualitĂ© de gĂ©nĂ©ration (cohĂ©rence doublĂ©e, dĂ©rive corrigĂ©e, artefacts supprimĂ©s), que la PPL ne capture pas. C'est exactement pourquoi j'ai construit un harnais multi-mĂ©triques plutĂŽt que de faire confiance Ă  la perplexitĂ© seule.

— Le contrastive decoding valait l'effort pour +1,9 de cohĂ©rence ?

En tant que feature produit, Ă  peine. En tant que recherche, oui : ça produit un rĂ©sultat propre et falsifiable sur quand le contraste aide Ă  petite Ă©chelle (un Ă©cart de capacitĂ© est nĂ©cessaire), et l'histoire du dĂ©bogage du faux nĂ©gatif est plus instructive que le gain lui-mĂȘme.

— Pourquoi ne pas simplement accepter le modùle et passer à autre chose ?

Parce que le but n'a jamais Ă©tĂ© le modĂšle — c'Ă©tait comprendre chaque levier. Maintenant je sais, chiffres Ă  l'appui, quels leviers bougent quoi. C'est transfĂ©rable au run scale-up d'une façon que "ça a l'air mieux" ne permettrait jamais.


💡 Le saviez-vous ?

Un rĂ©sultat nĂ©gatif n'est fiable qu'Ă  hauteur du harnais qui l'a produit. Mon premier run de contrastive decoding rapportait "Ă©chec total" — et c'Ă©tait faux, parce que le code d'Ă©valuation n'appliquait pas le mĂȘme sampling des deux cĂŽtĂ©s. Un bug de mesure ne te donne pas une petite erreur ; il peut inverser entiĂšrement ta conclusion.


Théo CHARLET

DiplĂŽmĂ© du TSSR (Technicien en systĂšmes et rĂ©seaux informatiques) – SpĂ©cialisation IA/ML

CrĂ©ateur d’AG-BPE (Attention-Guided Byte-Pair Encoding)

🔗 LinkedIn : https://www.linkedin.com/in/thĂ©o-charlet

🔎 RDTvlokip Search (mon moteur de recherche) : https://search.rdtvlokip.fr

🚀 À la recherche d'opportunitĂ©s de stage

🔗 Site web : https://rdtvlokip.fr

🔗 GitHub du projet : https://github.com/RDTvlokip/GPT-2-French-From-Scratch

Community

It's impressive that you're running an LLM from scratch, all by yourself, on a 1080 Ti that crashes at night. Just you, all on your own—that's true dedication. The instinct to check your code before jumping to the conclusion that a result is wrong—that's the true mindset of a researcher, not the pursuit of headlines. I'm following your work closely. Keep it up—you're showing that it's possible to do serious AI research without lab resources.

Sign up or log in to comment