🧠 J'ai entraîné mon propre LLM français from scratch — seul, avec une 1080 Ti, et le courant a coupé ⚡🇫🇷
👤 Contexte — Qui fait ça et pourquoi
20 ans, solo, GTX 1080 Ti 11GB. Pas d'équipe, pas de budget cloud, pas de superviseur.
La vraie question c'est : pourquoi ne pas prendre un modèle HuggingFace + LoRA comme tout le monde ? Réponse courte : parce que je voulais comprendre chaque étape. Pas fine-tuner une boîte noire. Comprendre. Vraiment.
Alors j'ai construit toute la chaîne moi-même :
- 🕷️ Collecte — mon crawler RDTvlokipBot
- 🧹 Cleaning — RDTextract, mon extracteur HTML→Markdown custom
- 🔤 Tokenisation — tokenizer ByteLevel BPE from scratch (32k vocab)
- 🏗️ Architecture — GPT-2 like sans dépendance HuggingFace
- 🎓 Training — trainer multi-phase custom
4 mois passés à construire le dataset avant de toucher au modèle. Le pipeline complet maîtrisé, du premier octet au token généré.
🏗️ L'architecture — LLaMA-style, pas GPT-2 vanilla
Les choix finaux
Après être passé par 7 tailles différentes (7.68M → 13.3M → 44M → 109M → 12.5M → 4M → 15M final), voilà ce qui a été retenu :
config = {
"n_embd": 256,
"n_layer": 8,
"n_head": 4,
"head_dim": 64, # Multiple de 64, "rester propre"
"vocab_size": 32000, # BPE custom 32k
"context_length": 512,
"n_params": "~15M"
}
Pourquoi 15M ? Ratio Chinchilla. La loi Chinchilla dit qu'un modèle optimal voit ~20 tokens par paramètre. Avec 271M tokens de dataset : 271M / 15M = 18.1 tokens/param. Dans la zone. Pas au hasard.
Architecture moderne — pas du GPT-2 old-school
C'est là que ça diffère vraiment d'un GPT-2 vanilla :
| Composant | GPT-2 original | Ce modèle |
|---|---|---|
| Position embedding | Appris (absolu) | RoPE |
| Normalisation | LayerNorm post | RMSNorm pre-norm |
| Activation | GELU | SwiGLU |
| Attention | Standard | Flash Attention (SDPA) + QK-Norm |
| Weight tying | Non | Oui |
En gros : architecture LLaMA-style, pas GPT-2 style. Le nom "GPT-2 like" c'est pour l'approche (decoder-only autorégressif) — pas pour les composants internes.
RoPE — pourquoi ce choix 🌀
RoPE encode la position par rotation dans l'espace vectoriel plutôt que par un vecteur fixe ajouté. La relation entre deux tokens devient fonction de leur distance relative, pas de leur position absolue.
def rotary_embedding(x, cos, sin):
x1 = x[..., :x.shape[-1] // 2]
x2 = x[..., x.shape[-1] // 2:]
rotated = torch.cat([-x2, x1], dim=-1)
return x * cos + rotated * sin
# rope_theta = 10000.0
# rope_scaling = null (pas de contexte étendu)
Avantages concrets :
- ✅ Pas de paramètres positionnels appris (moins de params, même perf)
- ✅ Généralisation sur séquences longues
- ✅ Standard utilisé par LLaMA, Mistral, GPT-NeoX, Gemma
Autant partir sur le bon standard dès le début.
SwiGLU — le détail qui change les params
SwiGLU modifie le FFN interne. Le n_inner s'auto-ajuste pour compenser :
# SwiGLU : n_inner effectif = 768 au lieu de 1024
# Garde la parité de paramètres vs un FFN classique
n_inner = int(n_embd * 4 * 2/3) # ~682 → arrondi à 768
📚 Le dataset — French Wikipedia reformulé par IA
Pas le dump brut
Wikipedia FR brut c'est propre mais encyclopédique dur — syntaxe wikitexte, templates, structure inégale. Mauvais pour l'entraînement d'un LLM qui doit générer du texte fluide.
Approche choisie : reformuler chaque article avec une IA dans un style markdown structuré, pédagogique, uniforme. Avec une contrainte stricte : "ne rajoute rien si tu ne sais pas" — les faits sont préservés (distances, dates, structures), seul le style change.
Résultat après correct_markdown.py (normalisation apostrophes, indentation, tableaux) :
152 948 fichiers .txt
1.09 milliard de caractères
271M tokens
Tokenizer custom — pas GPT-2
Le tokenizer GPT-2 original est entraîné sur de l'anglais. Sur du français, un mot = souvent 2-3 tokens au lieu d'un. Sous-optimal.
Tokenizer custom ByteLevel BPE from scratch :
Vocab size : 32 000 tokens
Couverture : 99% (31 671 / 32 000 tokens uniques utilisés)
Ratio chars/token : 3.997 ← optimal pour le français
UNK rate : 0%
C'est d'ailleurs ce travail sur la tokenisation qui a mené vers AG-BPE (Attention-Guided BPE) par la suite — mais c'est une autre histoire.
Preprocessing technique
# Chunking avec overlap pour maximiser les samples
chunk_size = 512
stride = 400 # overlap de 112 tokens entre chunks
# → 450 tokens utiles + 50 de contexte partagé
# Split train/val avec shuffle
# seed=42, évite le biais alphabétique des fichiers Wikipedia
# Multi-processing numpy int32 → ~85% RAM économisée vs Python lists
⚙️ L'entraînement — 3 phases, pas 1
Pipeline multi-phase (niveau recherche, projet perso)
Plutôt qu'un CLM classique sur toutes les données d'un coup, 18 epochs au total en 3 phases :
Phase 1 — Denoising CLM (3 epochs) Corruption de 15% des tokens → le modèle apprend à reconstruire. Force une représentation robuste avant d'optimiser la génération pure.
Phase 2 — Curriculum CLM (10 epochs) 5 buckets de longueur cumulatifs. On commence sur les séquences courtes, on élargit progressivement. Le modèle monte en difficulté graduellement.
Phase 3 — CLM + Contrastive SimCSE-style (5 epochs) Dropout comme augmentation de données → deux vues du même texte → loss contrastive. 2 forward passes par batch = 2x plus lent, mais représentations bien plus riches.
training_config = {
"optimizer": "AdamW",
"learning_rate": 3e-4,
"lr_scheduler": "cosine_with_warmup",
"warmup_steps": 1000,
"weight_decay": 0.1,
"grad_clip": 1.0,
"precision": "fp16", # Mixed precision
"gradient_checkpointing": True,
"flash_attention": True # SDPA
}
Les bugs — il y en a eu
Entraîner from scratch sur sa propre machine, c'est déboguer en permanence :
- 🐛
torch.cuda.ampdéprécié →torch.amp - 🐛
autocastsansdevice_type='cuda' - 🐛 Weight tying appliqué avant l'init des poids → embeddings écrasés
- 🐛 Bug critique : masquage causal cassé avec Flash Attention quand
attention_maskfourni → le modèle voyait le futur pendant le training. PPL artificielle de 1.13. Génération qui bouclait"ïsïsïs"/"Sen Sen Sen". Des heures de debug. - 🐛 Bug critique 2 :
enable_padding()activé en permanence → dataset à 30 GB au lieu de 3 GB. 8.5M chunks de padding au lieu de 700k chunks réels.ignore_index=0manquant dans la loss.
Chaque bug trouvé et corrigé. Température GPU : <70°C tout au long. 4.5 steps/s en moyenne. ~3h par epoch en phase 1.
⚡ La coupure de courant — epoch 10, bucket 4/5
Voilà le moment concret.
Le run final était lancé : 15M params, 271M tokens, 18 epochs prévus. Epoch 10 en cours, bucket 4/5 du curriculum. Stats au moment de la coupure :
Epoch : 10 / 18
Train loss: 2.8991
Val loss : 2.8829
PPL : 17.87
Bucket : 4/5
Temps restant estimé : ~43h
Le courant coupe. Brutal.
Ce qui a sauvé la mise : checkpoint automatique à chaque epoch. Zéro perte de progression réelle — juste l'impossibilité de finir les 8 epochs restants (bucket 5 + 5 epochs contrastive).
Verdict : "Bon j'ai eu une coupure de courant pendant l'entraînement, pas grave, je pense que le modèle est déjà bien entraîné." Test direct avec le checkpoint epoch 10.
🔮 La suite — Lambda Labs + dataset élargi
Pourquoi Lambda Labs
Vast.ai et runpod.io rejetés — ce sont des particuliers qui louent leur GPU. Peu fiable, peu de garanties.
Comparatif vrais clouds ML :
| Provider | GPU | Prix/h | Setup |
|---|---|---|---|
| Lambda Labs ✅ | A100 40GB | 1.29$ | Simple, SSH direct |
| Lambda Labs | H100 80GB | 2.49$ | Simple |
| GCP | V100 | variable | Complexe (IAM, VPC) |
| AWS | g5.xlarge | ~1$ | Lourd |
Coût estimé du run complet sur A100 : ~4€ au lieu de 35-50h sur 1080 Ti. Et plus de coupure de courant.
Pourquoi élargir le dataset avant de relancer
271M tokens c'est le sweet spot Chinchilla pour 15M params. Mais les tests de génération sont honnêtes :
Le modèle apprend la forme (markdown, grammaire, style) mais pas le fond (associations conceptuelles fortes). Le seul prompt vraiment cohérent : "Le système solaire" — sur-représenté dans Wikipedia via les milliers d'articles d'astéroïdes.
Un seul style de données = un modèle mono-style. Sources ciblées pour la v2 :
- 📝 Blogs tech FR (developpez.com, linuxfr.org)
- 💬 Forums (forum.ubuntu-fr.org, Stack Overflow FR)
- 📚 Project Gutenberg FR (littérature domaine public)
- 📖 Docs techniques FR (Python, Django...)
Objectif : 1B tokens propres, multi-styles, qualité 9/10. Pas pour battre Mistral. Pour devenir le meilleur petit modèle français sur un domaine précis.
✅ Avantages
- 🔬 Pipeline 100% maîtrisé — collecte → cleaning → tokenisation → training → génération
- 🌀 Architecture moderne — RoPE, RMSNorm, SwiGLU, Flash Attention. Pas du GPT-2 2019.
- 🇫🇷 Focus français natif — tokenizer optimisé FR, dataset FR reformulé
- 📐 Ratio Chinchilla respecté — 18.1 tokens/param, choix justifié
- 🎓 Pipeline 3 phases — niveau recherche sur un projet perso solo
❌ Inconvénients
- 💸 Compute local limité — 1080 Ti, pas un cluster
- 🐌 Phase contrastive = 2x plus lente — 2 forward passes par batch
- 📉 Wikipedia seul = style unique — le modèle hallucine hors de son domaine de confort
- ⚡ Risque coupure de courant — expérience vécue
⚠️ Limites
- 15M params c'est expérimental — GPT-2 small fait déjà 117M
- Sans fine-tuning instruction c'est un modèle de complétion pur
- La diversité du dataset va directement dicter la qualité des associations conceptuelles
📋 Fiche mémo
| Paramètre | Valeur |
|---|---|
| Architecture | Decoder-only LLaMA-style |
| Params | ~15M |
| n_embd / n_layer / n_head | 256 / 8 / 4 |
| Position embedding | RoPE (theta=10000) |
| Normalisation | RMSNorm pre-norm |
| Activation | SwiGLU |
| Attention | Flash Attention SDPA + QK-Norm |
| Tokenizer | ByteLevel BPE custom 32k |
| Ratio chars/token | 3.997 |
| Dataset | French Wikipedia reformulé IA |
| Tokens | 271M |
| Tokens vus à l'arrêt | 273M (epoch 10/18) |
| PPL à l'arrêt | 17.87 |
| GPU | GTX 1080 Ti 11GB |
| Vitesse | 4.5 steps/s |
| Précision | fp16 mixed |
| Phase suivante | Lambda Labs A100 |
🗂️ Résumé
Un LLM français 15M params, construit de zéro en solo avec une 1080 Ti. Architecture LLaMA-style (RoPE, RMSNorm, SwiGLU, Flash Attention), tokenizer ByteLevel BPE custom optimisé pour le français, dataset French Wikipedia reformulé par IA pour uniformiser le style. Pipeline d'entraînement 3 phases (denoising → curriculum → contrastive). Interrompu à epoch 10/18 par une coupure de courant à 273M tokens. Le modèle apprend la forme mais pas encore le fond — la suite sur Lambda Labs avec un dataset multi-sources.
🏁 Conclusion
Ce projet c'est pas un flex de compute — c'est l'opposé. C'est prouver qu'on peut construire toute la chaîne from scratch, seul, avec les ressources d'un particulier. Chaque bug débogué, chaque choix architectural justifié, chaque octet du dataset contrôlé. La coupure de courant à 273M tokens c'est anecdotique — le vrai apprentissage c'est les 4 mois avant. La suite sur Lambda Labs avec un dataset élargi. L'objectif n'a pas changé : le meilleur petit modèle français sur un domaine précis. 🚀
❓ Q&A
— Pourquoi ne pas avoir utilisé un modèle pré-entraîné + LoRA comme tout le monde ?
Parce que LoRA sur un modèle existant c'est adapter une boîte noire. L'objectif ici c'était de comprendre chaque composant — pourquoi RoPE plutôt que des embeddings absolus, pourquoi SwiGLU plutôt que GELU, comment le curriculum affecte la loss. On apprend pas ça en fine-tunant Mistral.
— Le pipeline 3 phases c'est vraiment utile à 15M params ?
Honnêtement, difficile à mesurer sans ablation complète. Ce qui est certain : la phase denoising force des représentations robustes très tôt, et la phase contrastive SimCSE-style à petite échelle reste rare dans les projets personnels. C'est peut-être over-engineered pour 15M — mais c'était aussi un objectif d'apprentissage.
— Comment tu comptes évaluer le modèle sur le prochain run ?
PPL sur un val set multi-sources (pas uniquement Wikipedia), tests de génération sur des prompts variés hors domaine encyclopédique, et comparatif avant/après diversification du dataset. L'objectif c'est de voir si mélanger 5-6 styles de sources change vraiment la cohérence des associations conceptuelles.
💡 Le saviez-vous ?
La loi de Chinchilla (DeepMind, 2022) a bouleversé les pratiques d'entraînement de LLMs. Avant elle, la tendance était de maximiser la taille du modèle avec un budget compute fixe. Chinchilla a démontré qu'un modèle plus petit entraîné sur plus de tokens bat systématiquement un modèle plus grand sous-entraîné. GPT-3 (175B params, 300B tokens) était ainsi significativement sous-entraîné selon ces critères. LLaMA 1 a été le premier grand modèle public à vraiment appliquer cette logique — et c'est exactement ce principe qui a guidé le choix des 15M params dans ce projet.
Théo CHARLET
Étudiant TSSR - Spécialisation IA/ML
Créateur d'AG-BPE (Attention-Guided Byte-Pair Encoding)
🔗 LinkedIn: https://www.linkedin.com/in/théo-charlet
🚀 En recherche de stage
🔗 Site Web: https://rdtvlokip.fr