๐ง I trained my own French LLM from scratch โ alone, with a 1080 Ti, and the power went out โก๐ซ๐ท
๐ค Context โ Who does this and why
20 years old, solo, GTX 1080 Ti 11GB. No team, no cloud budget, no supervisor.
The real question is: why not grab a HuggingFace model + LoRA like everyone else? Short answer: because I wanted to understand every single step. Not fine-tune a black box. Actually understand it.
So I built the entire pipeline myself:
- ๐ท๏ธ Collection โ my crawler RDTvlokipBot
- ๐งน Cleaning โ RDTextract, my custom HTMLโMarkdown extractor
- ๐ค Tokenization โ ByteLevel BPE tokenizer from scratch (32k vocab)
- ๐๏ธ Architecture โ GPT-2 like with zero HuggingFace dependency
- ๐ Training โ custom multi-phase trainer
4 months building the dataset before touching the model. The complete pipeline, fully mastered โ from the first byte to the generated token.
๐๏ธ Architecture โ LLaMA-style, not vanilla GPT-2
Final choices
After going through 7 different sizes (7.68M โ 13.3M โ 44M โ 109M โ 12.5M โ 4M โ 15M final), here's what stuck:
config = {
"n_embd": 256,
"n_layer": 8,
"n_head": 4,
"head_dim": 64, # Multiple of 64, keeping it clean
"vocab_size": 32000, # Custom BPE 32k
"context_length": 512,
"n_params": "~15M"
}
Why 15M? Chinchilla ratio. The Chinchilla law says an optimal model sees ~20 tokens per parameter. With 271M tokens of dataset: 271M / 15M = 18.1 tokens/param. Right in the zone. Not random.
Modern architecture โ not old-school GPT-2
This is where it really differs from vanilla GPT-2:
| Component | GPT-2 original | This model |
|---|---|---|
| Position embedding | Learned (absolute) | RoPE |
| Normalization | Post-LayerNorm | Pre-norm RMSNorm |
| Activation | GELU | SwiGLU |
| Attention | Standard | Flash Attention (SDPA) + QK-Norm |
| Weight tying | No | Yes |
Bottom line: LLaMA-style architecture, not GPT-2 style. The "GPT-2 like" label refers to the approach (decoder-only autoregressive) โ not the internal components.
RoPE โ why this choice ๐
RoPE encodes positions via rotation in vector space rather than adding a fixed vector. The relationship between two tokens becomes a function of their relative distance, not their absolute position.
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 (no extended context)
Concrete advantages:
- โ No learned positional parameters (fewer params, same performance)
- โ Better generalization on long sequences
- โ Standard used by LLaMA, Mistral, GPT-NeoX, Gemma
Might as well start with the right standard from day one.
SwiGLU โ the detail that changes param count
SwiGLU modifies the internal FFN. The n_inner auto-adjusts to compensate:
# SwiGLU: effective n_inner = 768 instead of 1024
# Maintains parameter parity vs a classic FFN
n_inner = int(n_embd * 4 * 2/3) # ~682 โ rounded to 768
๐ The dataset โ French Wikipedia rewritten by AI
Not the raw dump
Raw French Wikipedia is clean but encyclopedically rigid โ wikitexte syntax, templates, uneven structure. Bad for training an LLM that needs to generate fluid text.
Approach chosen: rewrite every article with an AI into a structured, pedagogical, uniform markdown style. With one hard constraint: "don't add anything you don't know" โ facts are preserved (distances, dates, structures), only the style changes.
Result after correct_markdown.py (apostrophe normalization, indentation, tables):
152,948 .txt files
1.09 billion characters
271M tokens
Custom tokenizer โ not GPT-2's
The original GPT-2 tokenizer was trained on English. On French, one word often becomes 2-3 tokens instead of one. Suboptimal.
Custom ByteLevel BPE tokenizer from scratch:
Vocab size : 32,000 tokens
Coverage : 99% (31,671 / 32,000 unique tokens used)
Chars/token ratio : 3.997 โ optimal for French
UNK rate : 0%
This work on tokenization is also what eventually led to AG-BPE (Attention-Guided BPE) โ but that's another story.
Technical preprocessing
# Chunking with overlap to maximize samples
chunk_size = 512
stride = 400 # 112-token overlap between chunks
# โ 450 useful tokens + 50 shared context
# Train/val split with shuffle
# seed=42, avoids alphabetical bias from Wikipedia filenames
# Multi-processing numpy int32 โ ~85% RAM saved vs Python lists
โ๏ธ Training โ 3 phases, not 1
Multi-phase pipeline (research-level, personal project)
Rather than a classic CLM on all data at once, 18 epochs total across 3 phases:
Phase 1 โ Denoising CLM (3 epochs) 15% token corruption โ the model learns to reconstruct. Forces robust representations before optimizing pure generation.
Phase 2 โ Curriculum CLM (10 epochs) 5 cumulative length buckets. Start on short sequences, progressively expand. The model ramps up in difficulty gradually.
Phase 3 โ CLM + Contrastive SimCSE-style (5 epochs) Dropout as data augmentation โ two views of the same text โ contrastive loss. 2 forward passes per batch = 2x slower, but much richer representations.
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",
"gradient_checkpointing": True,
"flash_attention": True # SDPA
}
The bugs โ there were many
Training from scratch on your own machine means debugging constantly:
- ๐
torch.cuda.ampdeprecated โtorch.amp - ๐
autocastmissingdevice_type='cuda' - ๐ Weight tying applied before weight init โ embeddings overwritten
- ๐ Critical bug: causal mask broken with Flash Attention when
attention_maskprovided โ the model saw the future during training. Artificially low PPL of 1.13. Generation looping"รฏsรฏsรฏs"/"Sen Sen Sen". Hours of debugging. - ๐ Critical bug 2:
enable_padding()permanently enabled โ dataset ballooned to 30 GB instead of 3 GB. 8.5M padding chunks instead of 700k real chunks.ignore_index=0missing from the loss.
Every bug found and fixed. GPU temp: <70ยฐC throughout. 4.5 steps/s average. ~3h per epoch in phase 1.
โก The power outage โ epoch 10, bucket 4/5
Here's the honest moment.
The final run was live: 15M params, 271M tokens, 18 epochs planned. Epoch 10 running, curriculum bucket 4/5. Stats at the time of the outage:
Epoch : 10 / 18
Train loss : 2.8991
Val loss : 2.8829
PPL : 17.87
Bucket : 4/5
Estimated time remaining : ~43h
Power cuts. Brutal.
What saved everything: automatic checkpoint at each epoch. Zero real progress lost โ just the inability to finish the remaining 8 epochs (bucket 5 + 5 contrastive epochs).
Verdict: "The power went out during training, no big deal โ I think the model is already well trained." Direct test with epoch 10 checkpoint.
๐ฎ What's next โ Lambda Labs + expanded dataset
Why Lambda Labs
Vast.ai and runpod.io rejected โ those are individuals renting their GPU. Unreliable, no real guarantees.
Real ML cloud comparison:
| Provider | GPU | Price/h | Setup |
|---|---|---|---|
| Lambda Labs โ | A100 40GB | $1.29 | Simple, direct SSH |
| Lambda Labs | H100 80GB | $2.49 | Simple |
| GCP | V100 | variable | Complex (IAM, VPC) |
| AWS | g5.xlarge | ~$1 | Heavy |
Estimated cost for the full run on A100: ~โฌ4 instead of 35-50h on a 1080 Ti. And no more power outages.
Why expand the dataset before relaunching
271M tokens hits the Chinchilla sweet spot for 15M params. But generation tests are honest:
The model learns form (markdown, grammar, style) but not substance (strong conceptual associations). The only truly coherent prompt: "The solar system" โ massively overrepresented in Wikipedia through thousands of asteroid/planet articles.
One data style = one-style model. Target sources for v2:
- ๐ French tech blogs (developpez.com, linuxfr.org)
- ๐ฌ Forums (forum.ubuntu-fr.org, Stack Overflow FR)
- ๐ Project Gutenberg FR (public domain literature)
- ๐ French technical docs (Python, Django...)
Goal: 1B clean tokens, multi-style, quality 9/10. Not to beat Mistral. To become the best small French model on a specific domain.
โ Advantages
- ๐ฌ 100% mastered pipeline โ collection โ cleaning โ tokenization โ training โ generation
- ๐ Modern architecture โ RoPE, RMSNorm, SwiGLU, Flash Attention. Not 2019 GPT-2.
- ๐ซ๐ท Native French focus โ FR-optimized tokenizer, AI-rewritten FR dataset
- ๐ Chinchilla ratio respected โ 18.1 tokens/param, justified choice
- ๐ 3-phase pipeline โ research-level on a solo personal project
โ Disadvantages
- ๐ธ Limited local compute โ 1080 Ti, not a cluster
- ๐ Contrastive phase = 2x slower โ 2 forward passes per batch
- ๐ Wikipedia only = single style โ model hallucinates outside its comfort zone
- โก Power outage risk โ lived experience
โ ๏ธ Limits
- 15M params is experimental โ GPT-2 small is already 117M
- Without instruction fine-tuning it's a pure completion model
- Dataset diversity will directly dictate the quality of conceptual associations
๐ Quick reference
| Parameter | Value |
|---|---|
| Architecture | Decoder-only LLaMA-style |
| Params | ~15M |
| n_embd / n_layer / n_head | 256 / 8 / 4 |
| Position embedding | RoPE (theta=10000) |
| Normalization | Pre-norm RMSNorm |
| Activation | SwiGLU |
| Attention | Flash Attention SDPA + QK-Norm |
| Tokenizer | Custom ByteLevel BPE 32k |
| Chars/token ratio | 3.997 |
| Dataset | AI-rewritten French Wikipedia |
| Tokens | 271M |
| Tokens seen at stop | 273M (epoch 10/18) |
| PPL at stop | 17.87 |
| GPU | GTX 1080 Ti 11GB |
| Speed | 4.5 steps/s |
| Precision | fp16 mixed |
| Next phase | Lambda Labs A100 |
๐๏ธ Summary
A 15M parameter French LLM, built from zero solo on a 1080 Ti. LLaMA-style architecture (RoPE, RMSNorm, SwiGLU, Flash Attention), custom ByteLevel BPE tokenizer optimized for French, French Wikipedia dataset rewritten by AI for style uniformity. 3-phase training pipeline (denoising โ curriculum โ contrastive). Interrupted at epoch 10/18 by a power outage at 273M tokens. The model learns form but not yet substance โ next run on Lambda Labs with a multi-source dataset.
๐ Conclusion
This project isn't a compute flex โ it's the opposite. It's proving you can build the entire chain from scratch, alone, with consumer hardware. Every bug debugged, every architectural choice justified, every byte of the dataset controlled. The power outage at 273M tokens is just an anecdote โ the real learning happened over the 4 months before. Next stop: Lambda Labs with an expanded dataset. The goal hasn't changed: the best small French model on a specific domain. ๐
โ Q&A
โ Why not use a pre-trained model + LoRA like everyone else?
Because LoRA on an existing model means adapting a black box. The goal here was to understand every component โ why RoPE over absolute embeddings, why SwiGLU over GELU, how curriculum affects loss curves. You don't learn that by fine-tuning Mistral.
โ Is the 3-phase pipeline actually useful at 15M params?
Honestly, hard to measure without a full ablation study. What's certain: the denoising phase forces robust representations early, and SimCSE-style contrastive learning at this scale stays rare in personal projects. Maybe over-engineered for 15M โ but learning was also the objective.
โ How will the model be evaluated on the next run?
PPL on a multi-source val set (not just Wikipedia), generation tests on out-of-domain prompts, and a before/after comparison after dataset diversification. The goal is to see if mixing 5-6 source styles actually changes the coherence of conceptual associations.
๐ก Did you know?
The Chinchilla law (DeepMind, 2022) completely changed LLM training practices. Before it, the trend was to maximize model size with a fixed compute budget. Chinchilla proved that a smaller model trained on more tokens systematically beats a larger undertrained one. GPT-3 (175B params, 300B tokens) was significantly undertrained by these standards. LLaMA 1 was the first major public model to truly apply this logic โ and it's exactly this principle that guided the 15M param choice in this project.
Thรฉo CHARLET
IT Systems & Networks Student - AI/ML Specialization
Creator of AG-BPE (Attention-Guided Byte-Pair Encoding)
๐ LinkedIn: https://www.linkedin.com/in/thรฉo-charlet
๐ Seeking internship opportunities
๐ Website : https://rdtvlokip.fr