1. Pipeline (vue d'ensemble)
88 faits réels (FAQ Mon espace santé)
│ ① Génération synthétique (Qwen3-32B-FP8 + NeMo Curator + EntiGraph) → corpus ~7,3M tokens
│ ② Décontamination vs jeu de test gelé → sous-ensembles 0,5M / 2M / 6M
▼
Qwen3-8B-Base ──③ CPT full FT (next-token)──► Qwen3-8B-CPT ──④ SFT (format Q/R)──► CE MODÈLE
│
└─ ⑤ Évaluation closed-book (juge Qwen3-32B) : positives + négatives
Fondements scientifiques. Le texte brut ne suffit pas : un fait doit être augmenté sous de
nombreuses formes pour devenir extractible (Allen-Zhu & Li, Physics of LM 3.1, arXiv:2309.14316).
La précision closed-book croît avec le volume de tokens synthétiques (EntiGraph, arXiv:2409.07431 ;
WRAP, arXiv:2401.16380). La connaissance s'injecte au CPT, pas au SFT — qui ne fait qu'apprendre à
utiliser (Gekhman et al., arXiv:2405.05904).
2. ① Génération synthétique
Générateur : Qwen3-32B-FP8 servi par vLLM (mode thinking désactivé). Pilotage par les
prompts Nemotron-CC de NVIDIA NeMo Curator (conteneur
nvcr.io/nvidia/nemo-curator:26.04), traduits en français. Toute génération est 1-hop depuis les
88 faits réels (jamais de synthétique-sur-synthétique → anti model collapse).
Deux familles de documents :
- Reformulations multi-styles (par fait) — opérateurs Nemotron-CC : wikipedia-rephrase, distill,
extract-knowledge, knowledge-list, diverse-QA. 15 répétitions stochastiques (temp 0,95).
- EntiGraph (implémentation d'après arXiv:2409.07431) — passages de synthèse reliant des
paires/triplets de faits : 5 chunks × 2 400 groupes, tailles de groupe {2,3,3}. Produit aussi
2 251 questions compositionnelles (multi-faits) filtrées par un juge de cohérence factuelle.
Échantillonnage : temp 0,9–0,95, top_p 0,95. Pool brut ≈ 7,3 M tokens (18 700 documents).
Décontamination & paliers. Suppression de tout document trop proche (embeddings
paraphrase-multilingual-MiniLM-L12-v2, cosine ≥ 0,85) d'une question de test held-out (357 retirés).
Tokenisation au tokenizer Qwen3-8B, puis sous-ensembles emboîtés : cpt_500k ⊂ cpt_2M ⊂ cpt_6M.
3. ③ CPT — continued pre-training (full fine-tuning)
Next-token prediction sur cpt_6M.jsonl depuis Qwen3-8B-Base.
Table with columns: Paramètre, Valeur| Paramètre | Valeur |
|---|
| Type | full fine-tuning (tous les poids) |
| Précision | bf16 |
| Longueur de séquence | 2048 (packing) |
| Époques | 3 |
| Learning rate | 2e-5, scheduler cosine, warmup 5 % |
| Optimiseur | paged_adamw_8bit (bitsandbytes) |
| Batch | per-device 1 × grad-accum 16 (= 16 séquences/step) |
| Gradient checkpointing |
Le full FT d'un 8 B tient sur 48 Go grâce à paged_adamw_8bit + gradient checkpointing
(pic mesuré ≈ 45,5 Go ; PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True).
Sortie : fenyo/L40S-Qwen3-8B-MonEspaceSante-CPT.
SFT par-dessus le checkpoint CPT, sur MonEspaceSante-SFT-dataset
(2 771 paires). Template chat Qwen3 (ChatML), loss masquée sur le prompt (calculée uniquement sur
la réponse).
Table with columns: Paramètre, Valeur| Paramètre | Valeur |
|---|
| Type | full fine-tuning · bf16 · grad-checkpointing |
| Époques | 3 (voir balayage du nombre d'époques) |
| Learning rate | 1e-5, cosine, warmup 3 % |
| Optimiseur | paged_adamw_8bit · batch 1 × grad-accum 16 · max_len 2048 |
Note de robustesse : 1 époque de SFT ne suffisait pas à réapprendre la terminaison (<|im_end|>)
après le full-FT CPT → le modèle dégénérait. 3 époques corrigent ce point. À l'inférence, ajouter
<|im_end|> aux eos_token_id.
5. ⑤ Protocole d'évaluation
Closed-book (aucun contexte). Jeux gelés avant toute génération d'entraînement et décontaminés :
- Positives :
test_heldout (696 = 8 paraphrases inédites × 88 faits) et eval_official (30, split
validation d'origine).
- Négatives : 47 questions hors-périmètre (refus attendu).
Juge : Qwen3-32B-FP8 (LLM-as-judge). Positives → binaire CORRECT/INCORRECT. Négatives → REFUS
(bon) vs INVENTION (mauvais). Tous les fichiers + détails : eval-suite.
Étude d'échelle (accuracy held-out, n=696)
Table with columns: Tokens CPT synthétiques, Accuracy closed-book| Tokens CPT synthétiques | Accuracy closed-book |
|---|
| 0 (SFT direct, sans CPT) | 55,5 % |
| 500 000 | 66,7 % |
| 2 000 000 | 78,0 % |
| 6 000 000 (ce modèle) | 86,9 % |
La précision croît de façon quasi log-linéaire avec le volume de tokens synthétiques — confirmant
EntiGraph/WRAP. Le SFT direct (sans CPT) plafonne à 55,5 % : c'est bien le CPT qui injecte la connaissance.
6. Comparaison vs gpt-oss-20b-FAQ-MES
Même protocole, même juge, mêmes questions. Référence : fenyo/gpt-oss-20b-FAQ-MES
(openai/gpt-oss-20b, MoE ~20 B, SFT sur la même FAQ).
Table with columns: Modèle, Taille, Positives held-out (696), Positives officiel (30), Négatives — refus correct (47)| Modèle | Taille | Positives held-out (696) | Positives officiel (30) | Négatives — refus correct (47) |
|---|
| gpt-oss-20b-FAQ-MES | MoE 20 B | 70,8 % | 70,0 % | 12,8 % |
| Qwen3-8B — SFT direct (0 CPT) | 8 B | 55,5 % | 50,0 % | 6,4 % |
| Qwen3-8B — CPT 2M + SFT | 8 B | 78,0 % | 76,7 % | 19,1 % |
Conclusion. Sur la couverture factuelle (positives), ce modèle de 8 B dépasse nettement le
gpt-oss-20b (86,9 % vs 70,8 %), avec un modèle 2,5× plus petit. Sur les négatives
(anti-hallucination), ce modèle factuel-max et gpt-oss sont faibles (≈ 13 % de refus) — mais la variante
anti-hallucination
(dernière ligne), entraînée avec des exemples de refus, atteint 78,7 % tout en restant plus précise
que gpt-oss : elle domine le 20 B sur les deux axes.
Caveats : (i) le jeu de test porte sur les 88 faits de MonEspaceSante-FAQ-QA ;
gpt-oss a été entraîné sur fenyo/FAQ-MES (même domaine, couverture possiblement différente) → léger
avantage de terrain. (ii) Juge différent de celui du rapport d'origine de gpt-oss → seuls les classements
au sein de CE protocole sont comparables.
7. Balayage du nombre d'époques SFT
Pour vérifier que 3 époques est le plafond (held-out, n=696) :
Table with columns: Tokens CPT, SFT 3 ép., SFT 6 ép., Δ(6−3)| Tokens CPT | SFT 3 ép. | SFT 6 ép. | Δ(6−3) |
|---|
| 0 (SFT seul) | 55,5 % | 62,9 % | +7,5 |
| 500k | 66,7 % | 67,1 % | +0,4 |
| 2M | 78,0 % | 76,4 % | −1,6 |
| 6M | 86,9 % | 84,2 % | −2,7 |
Pour les modèles post-CPT, passer à 6 époques n'aide pas, voire dégrade (sur-apprentissage) →
3 époques suffisent. Seul le SFT-seul gagne à sur-entraîner (il n'a que ce canal pour apprendre),
mais reste loin derrière le CPT.
8. Reproduction
Pipeline complet (génération → CPT → SFT → éval). Hyperparamètres = ceux des sections 2–5.
# ① Génération synthétique (conteneur NeMo Curator → vLLM Qwen3-32B-FP8 sur l'hôte)
# opérateurs Nemotron-CC reps=15 + EntiGraph 5×2400 groupes → pool ~7,3M tokens
# puis décontamination + sous-ensembles emboîtés → cpt_6M.jsonl
# (corpus déjà fourni : dataset MonEspaceSante-CPT-corpus)
# ③ CPT full fine-tuning de Qwen3-8B-Base sur cpt_6M.jsonl
python cpt_train.py --model Qwen/Qwen3-8B-Base --corpus cpt_6M.jsonl --tag 6M \
--seq-len 2048 --epochs 3 --lr 2e-5 --warmup-ratio 0.05 \
--bs 1 --grad-accum 16 --optim paged_adamw_8bit
# → models/CPT_6M (= fenyo/L40S-Qwen3-8B-MonEspaceSante-CPT)
# ④ SFT sur les paires Q/R (loss masquée sur le prompt, template ChatML)
python sft_train.py --base models/CPT_6M --data sft.jsonl --tag 6M \
--epochs 3 --lr 1e-5 --bs 1 --grad-accum 16 --max-len 2048 --optim paged_adamw_8bit
# → models/SFT_6M (= CE MODÈLE)
# ⑤ Évaluation closed-book + juge Qwen3-32B (positives + négatives)
Stack : Python 3.x · torch (CUDA) · transformers 5.9 · TRL/Trainer · bitsandbytes 0.49 · vLLM ·
NeMo Curator (conteneur NGC). 1× L40S 48 Go. Anti-fuite : test gelé avant génération + décontamination
par embeddings (cosine ≥ 0,85).
9. Utilisation
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
mid = "fenyo/L40S-Qwen3-8B-MonEspaceSante-CPT-SFT"
tok = AutoTokenizer.from_pretrained(mid)
model = AutoModelForCausalLM.from_pretrained(mid, dtype=torch.bfloat16).cuda().eval()
eos = [tok.eos_token_id, tok.convert_tokens_to_ids("<|im_end|>")]
msgs = [{"role": "user", "content": "Où peut-on télécharger l'application Mon espace santé ?"}]
prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False)
enc = tok(prompt, return_tensors="pt", add_special_tokens=False).to("cuda")
out = model.generate(**enc, max_new_tokens=320, do_sample=False, eos_token_id=eos)
print(tok.decode(out[0][enc.input_ids.shape[1]:], skip_special_tokens=True))
Le modèle a été entraîné sans system prompt (questions utilisateur seules).
10. Limites
- Anti-hallucination faible : sur les questions hors-périmètre, le modèle invente souvent (refus
~13 %). Aucun exemple de refus dans le SFT. Piste : ajouter des négatives au SFT.
- Pas de replay au CPT : un mélange de 10–30 % de texte FR généraliste réduirait l'oubli
catastrophique et lisserait peut-être la courbe (itération future).
- Domaine étroit : 88 faits d'un service public français précis ; hors de ce périmètre, le modèle
conserve surtout le comportement de Qwen3-8B-Base.
- Éval :
eval_official (n=30) est bruité ; la référence est test_heldout (n=696).
Références
EntiGraph — arXiv:2409.07431 · WRAP — arXiv:2401.16380 · Physics of LM 3.1 — arXiv:2309.14316 ·
Gekhman et al. (hallucinations) — arXiv:2405.05904 · LoRA Learns Less — arXiv:2405.09673.