Objectif du jour : modifier le comportement d'un modèle avec vos propres données — et le faire sur un GPU de 12 Go, pas dans un centre de données. À la fin de cette séance, vous savez construire un jeu de données d'entraînement propre, régler les hyperparamètres qui comptent réellement, reconnaître le sur-apprentissage à temps, et comparer votre modèle affiné à sa version d'origine. Le gain du jour est votre adaptateur LoRA, entraîné sur votre dataset, avec une évaluation avant/après. Today's goal: modify a model's behaviour with your own data — and do it on a 12 GB GPU, not in a data centre. By the end of this session you can build a clean training dataset, set the hyperparameters that actually matter, spot overfitting in time, and compare your fine-tuned model against the original. Today's win is your LoRA adapter, trained on your dataset, with a before/after evaluation.
Rappelez-vous la séance 1 : un modèle a subi trois entraînements, et l'étape 2 (SFT) est celle qui transforme un compléteur en assistant. C'est exactement l'étape sur laquelle vous allez intervenir. Vous ne réapprenez pas le langage : vous ajustez un comportement. Recall session 1: a model goes through three trainings, and stage 2 (SFT) is the one that turns a completer into an assistant. That is exactly the stage you are about to act on. You are not relearning language: you are adjusting a behaviour.
| ApprocheApproach | CoûtCost | Bon pourGood for | Mauvais pourBad for |
|---|---|---|---|
| Prompt engineeringPrompt engineering | MinutesMinutes | Presque tout, en premier essaiAlmost everything, as a first attempt | Format très contraint, volume d'appelsVery constrained format, high call volume |
| RAG | HeuresHours | Connaissance factuelle, citations, corpus évolutifFactual knowledge, citations, evolving corpus | Imposer une forme, apprendre un styleImposing a shape, learning a style |
| LoRA / QLoRA | Quelques heures de GPUA few GPU-hours | Comportement : format de sortie strict, jargon maison, ton, tâche répétitive, appel d'outil systématiqueBehaviour: strict output format, in-house jargon, tone, repetitive task, consistent tool calling | Injecter des faits qui changent ; ajouter de la connaissanceInjecting changing facts; adding knowledge |
| Fine-tuning completFull fine-tuning | Des centaines de GPU-heuresHundreds of GPU-hours | Changer le modèle en profondeur (rarement nécessaire)Deeply changing the model (rarely necessary) |
LoRA réduit les paramètres entraînés, mais le modèle de base doit quand même être en mémoire — et un 8 B en FP16 pèse 16 Go, ce qui ne rentre pas dans une RTX 3060 de 12 Go. QLoRA résout ça : le modèle de base est chargé en 4 bits (format NF4), et seuls les adaptateurs sont entraînés en précision plus élevée. LoRA reduces the trained parameters, but the base model still has to be in memory — and an 8 B in FP16 weighs 16 GB, which does not fit in a 12 GB RTX 3060. QLoRA solves this: the base model is loaded in 4 bits (NF4 format), and only the adapters are trained at higher precision.
| TechniqueTechnique | Ce qu'elle faitWhat it does |
|---|---|
| NF4 4-bit NormalFloat |
Un type de 4 bits adapté à la distribution des poids des réseaux de neurones (approximativement gaussienne), meilleur que l'arrondi uniforme.A 4-bit type fitted to the weight distribution of neural networks (approximately Gaussian), better than uniform rounding. |
| Double quantificationDouble quantization | Les constantes d'échelle sont elles-mêmes quantifiées : ~0,4 bit par paramètre économisé, soit ~3 Go sur un 65 B.The scale constants are themselves quantized: ~0.4 bits per parameter saved, i.e. ~3 GB on a 65 B. |
| Optimiseurs paginésPaged optimizers | L'état de l'optimiseur peut déborder en RAM système au lieu de faire planter l'entraînement : on évite les pics de mémoire.Optimizer state can spill to system RAM instead of crashing the run: memory spikes are avoided. |
Modèle 8 B en QLoRA, entraînement :
poids de base (4 bits) : 8,0e9 x 0,5 octet = ~4,0 Go
adaptateurs LoRA (r=16) : négligeable (~0,05 Go)
états d'optimiseur (Adam) : ~2 x adaptateurs = ~0,1 Go
gradients : ~adaptateurs = ~0,05 Go
activations + cache d'attention : ~2 à 6 Go selon
la LONGUEUR DE SÉQUENCE et la taille de lot
---------------------------------------------------------------
TOTAL approximatif : ~8 à 12 Go
=> Tient sur UNE RTX 3060 (12 Go) si l'on reste sur des séquences
de 1024-2048 tokens et une taille de lot effective modérée.
=> Sur Colab T4 (16 Go) ou Kaggle P100 (16 Go) : confortable.
max_seq_length avant de réduire le rang LoRA, et augmentez l'accumulation de gradient au lieu de la taille de lot. On obtient la même taille de lot effective pour une fraction de la mémoire.
The Lab 2 lesson applies to training: what blows up fine-tuning VRAM is not the parameter count, it is the sequence length (activations grow with it) and the batch size. If you run out of memory: cut max_seq_length before cutting the LoRA rank, and raise gradient accumulation instead of batch size. You get the same effective batch size for a fraction of the memory.
C'est la partie que tout le monde bâcle et qui décide de tout. Un modèle entraîné sur 500 exemples excellents battra systématiquement un modèle entraîné sur 50 000 exemples médiocres. La qualité, la cohérence et la couverture des cas difficiles sont les seuls critères qui comptent. This is the part everyone rushes and that decides everything. A model trained on 500 excellent examples will systematically beat one trained on 50,000 mediocre ones. Quality, consistency and coverage of hard cases are the only criteria that matter.
| FormatFormat | StructureStructure | QuandWhen |
|---|---|---|
| Alpaca | {"instruction": "...", "input": "...", "output": "..."} |
Tâche instruction → réponse, avec entrée optionnelle. Le plus simple pour débuter.Instruction → response task, with optional input. The simplest to start with. |
| ShareGPT | {"conversations": [{"from":"human",...},{"from":"gpt",...}]} |
Dialogues multi-tours. Nécessaire pour apprendre un comportement conversationnel.Multi-turn dialogues. Needed to learn conversational behaviour. |
| Gabarit de chatChat template | <|im_start|>user … <|im_end|> selon le modèleper model |
Ce que la plupart des outils appliquent automatiquement à partir de la conversation. Voir l'avertissement ci-dessous.What most tools apply automatically from the conversation. See the warning below. |
<|start_header_id|>, Mistral [INST]…[/INST], Qwen son propre gabarit). Si vous entraînez avec un gabarit différent de celui du modèle, l'entraînement converge quand même — la perte baisse, tout semble aller bien — mais le modèle produit des réponses dégradées parce que vous lui avez appris une convention qu'il n'attendait pas. Règle : laissez l'outil appliquer le gabarit du modèle (tokenizer.apply_chat_template via TRL/Unsloth), et vérifiez visuellement au moins un exemple formaté avant de lancer l'entraînement.
Every model was trained with a precise conversation format (Llama uses <|start_header_id|>, Mistral [INST]…[/INST], Qwen its own template). If you train with a template different from the model's, training still converges — loss falls, everything looks fine — but the model produces degraded answers because you taught it a convention it did not expect. Rule: let the tool apply the model's template (tokenizer.apply_chat_template via TRL/Unsloth), and visually check at least one formatted example before launching training.
| ParamètreParameter | RôleRole | Valeur de départStarting value |
|---|---|---|
r (rang LoRA)(LoRA rank) |
Capacité de l'adaptateur. Plus grand = plus expressif, plus de mémoire, plus de risque de sur-apprentissage.Adapter capacity. Larger = more expressive, more memory, higher overfitting risk. | 16 (8–64 selon la complexité)(8–64 by complexity) |
lora_alpha |
Pondération de la correction. La convention alpha = 2 × r est un bon point de départ.Weighting of the correction. The alpha = 2 × r convention is a good start. | 2 × r |
target_modules |
Quelles matrices adapter. Toutes les couches linéaires donne plus de qualité ; seulement q_proj/v_proj est plus léger.Which matrices to adapt. All linear layers gives more quality; only q_proj/v_proj is lighter. |
toutes les linéaires (via Unsloth)all linear layers (via Unsloth) |
learning_rate |
Bien plus élevé qu'un fine-tuning complet : LoRA entraîne peu de paramètres.Much higher than full fine-tuning: LoRA trains few parameters. | 2e-4 (1e-4 à 3e-4)(1e-4 to 3e-4) |
num_train_epochs |
Presque toujours 1 à 3. Au-delà, vous mémorisez votre dataset.Almost always 1 to 3. Beyond that, you memorise your dataset. | 2 |
| taille de lot + accumulationbatch size + accumulation | La taille de lot effective = lot × accumulation × GPU. Montez l'accumulation, pas le lot, quand la VRAM manque.The effective batch = batch × accumulation × GPUs. Raise accumulation, not batch, when VRAM is short. | lot 2, accumulation 4 (effectif 8)batch 2, accumulation 4 (effective 8) |
max_seq_length |
Troncature des exemples trop longs. Vérifiez combien de vos exemples sont tronqués — sinon vous entraînez sur des phrases coupées.Truncation of over-long examples. Check how many of your examples get truncated — otherwise you train on severed sentences. | 1024–2048, mesuré sur vos données1024–2048, measured on your data |
warmup_ratio |
Montée progressive du taux d'apprentissage : évite de casser le modèle au premier pas.Gradual learning-rate ramp: avoids breaking the model on the first step. | 0,03 |
| OutilTool | PourquoiWhy |
|---|---|
| Unsloth | Le plus rapide et le plus économe en VRAM ; notebooks Colab gratuits fournis. Le meilleur point de départ.Fastest and most VRAM-frugal; free Colab notebooks provided. The best starting point. |
| TRL + PEFT | La référence Hugging Face : plus de contrôle, mieux documenté, plus lent. Le choix quand vous devez comprendre chaque étape.The Hugging Face reference: more control, better documented, slower. The choice when you need to understand every step. |
| Axolotl | Piloté par fichier de configuration : reproductible, adapté aux pipelines d'équipe.Config-file driven: reproducible, suited to team pipelines. |
| GPU gratuitFree GPU | Google Colab : T4 16 Go, gratuit, sans garantie de disponibilité ni de type de GPU. Kaggle : P100 16 Go, quota de 30 h/semaine réinitialisé chaque semaine (variable selon la demande).Google Colab: T4 16 GB, free, no guarantee of availability or GPU type. Kaggle: P100 16 GB, a 30 h/week quota reset weekly (varies with demand). |
| Votre labYour lab | 2× RTX 3060 (24 Go cumulés). Une seule carte suffit pour QLoRA sur un 8 B avec Unsloth ; les deux avec TRL.2× RTX 3060 (24 GB total). One card suffices for QLoRA on an 8 B with Unsloth; both with TRL. |
# Génère un dataset de démonstration (extraction de tickets vers JSON) # et le VALIDE : format, doublons, longueurs, gabarit, jeu de validation. python3 labs/dataset/make_dataset.py --out dataset_tickets.jsonl --n 300 # Le script refuse de produire un dataset silencieusement défectueux : # il vérifie la longueur en tokens (troncature), les doublons exacts, # et affiche le gabarit formaté pour UN exemple — à relire à l'œil.
# Sur le lab (2x RTX 3060) ou sur Colab T4 — le script choisit Unsloth
# s'il est disponible, sinon TRL+PEFT (voie de référence).
python3 labs/lab7_finetune_qlora.py --dataset dataset_tickets.jsonl \
--base-model unsloth/llama-3.1-8b-bnb-4bit \
--r 16 --alpha 32 --epochs 2 --lr 2e-4 \
--max-seq-len 1024 --batch 2 --grad-accum 4 \
--out ./mon-adaptateur --validate-only # d'abord : vérifier la config
# puis, pour de vrai :
python3 labs/lab7_finetune_qlora.py --dataset dataset_tickets.jsonl \
--base-model unsloth/llama-3.1-8b-bnb-4bit --out ./mon-adaptateur
# Réutilise le harnais de la séance 3 : mêmes cas, même métrique. # C'est le SEUL moyen honnête d'affirmer que le fine-tuning a servi. python3 labs/lab3_eval_harness.py --model llama3.1:8b --prompt hardened --compare python3 labs/lab3_eval_harness.py --model mon-modele-affine --prompt hardened
| Modèle de baseBase model | Qwen/Qwen2.5-1.5B-Instruct |
| DatasetDataset | 255 exemples d'entraînementtraining examples + 45 de validationvalidation |
| RéglagesSettings | r=16, alpha=32, lr=2e-4, 2 époquesepochs, lotbatch 2 × 4, max_length 512 |
| DuréeDuration | 2 min 16 s sur une RTX 3060on one RTX 3060 |
| Perte d'entraînement (moyenne)Training loss (mean) | 0,8227 |
| Perte de validationValidation loss époque 1 → époque 2epoch 1 → epoch 2 |
0,3952 → 0,3081 (elle baisse : pas de sur-apprentissage)(it falls: no overfitting) |
| Précision par token (validation)Per-token accuracy (validation) | 91,1 % → 92,3 % |
| Taille de l'adaptateurAdapter size | 8,75 Mo (adapter_model.safetensors) |
| Base vs affinéBase vs tuned 20 cas de validation tenus à l'écart20 held-out validation cases |
JSON valide 20/20 → 20/20 champs exactsexact fields 0/20 → 4/20 |
impact, une chaîne de texte libre qui doit correspondre mot pour mot. Un seul champ faux sur cinq annule le cas. (2) Le modèle de base produisait déjà du JSON valide (20/20) — parce que le message système décrivait le schéma — mais il inventait toutes les valeurs (0/20). Le fine-tuning ne lui a pas appris à formater : il lui a appris à extraire. (3) 255 exemples sur un modèle de 1,5 B, ce n'est pas censé donner 95 %. C'est censé donner un gain mesurable — et c'est exactement ce que vous avez. Un cours qui vous promet 95 % vous mentira ; un cours qui vous apprend à mesurer 20 % vous rendra autonome.
How to read this "4/20" honestly — and why it is the best lesson in the course. Three points. (1) The metric is deliberately strict: all five fields must be exact, including impact, a free-text string that must match word for word. One wrong field out of five voids the case. (2) The base model already produced valid JSON (20/20) — because the system message described the schema — but it invented every value (0/20). Fine-tuning did not teach it to format: it taught it to extract. (3) 255 examples on a 1.5 B model is not supposed to give 95 %. It is supposed to give a measurable gain — and that is exactly what you have. A course promising 95 % would be lying to you; a course teaching you to measure 20 % makes you self-sufficient.
max_seq_length est devenu max_length, warmup_ratio est devenu warmup_steps, et SFTTrainer(tokenizer=…) est devenu processing_class=…. Pire : dans TRL 1.x, le paramètre loss_type par défaut (chunked_nll) applique un patch au lm_head qui plante dès que le modèle est enveloppé par PEFT. La parade est exactement la discipline du cours : le script interroge la signature réelle de SFTConfig et filtre les paramètres au lieu de les coder en dur — « ne pas supposer une interface, la vérifier ». C'est aussi pour cela que le mode --validate-only existe : il attrape ce genre de problème en 30 secondes au lieu de 2 heures.
A real obstacle hit while running this lab, and its lesson. TRL changed its API between versions: max_seq_length became max_length, warmup_ratio became warmup_steps, and SFTTrainer(tokenizer=…) became processing_class=…. Worse: in TRL 1.x the default loss_type (chunked_nll) applies a patch to the lm_head that crashes as soon as the model is wrapped by PEFT. The fix is exactly the course's discipline: the script inspects SFTConfig's real signature and filters parameters instead of hard-coding them — "do not assume an interface, verify it". That is also why --validate-only exists: it catches this class of problem in 30 seconds instead of 2 hours.
.jsonl validé (format, pas de doublon, longueurs mesurées, gabarit vérifié à l'œil), un adaptateur LoRA entraîné avec sa courbe de perte de validation, et une comparaison chiffrée base contre affiné sur le harnais de la séance 3 ou sur le jeu de validation tenu à l'écart (labs/lab7_compare.py). Sans cette comparaison, vous ne savez pas si vous avez amélioré le modèle ou seulement dépensé du GPU.
What you must produce: a validated .jsonl file (format, no duplicates, measured lengths, visually checked template), a trained LoRA adapter with its validation loss curve, and a quantified base-vs-tuned comparison on the session 3 harness or on the held-out validation set (labs/lab7_compare.py). Without that comparison, you do not know whether you improved the model or just spent GPU time.
max_seq_length libère beaucoup de mémoire pour un coût de qualité souvent nul (si vos exemples tiennent dedans). Et l'accumulation de gradient permet de garder la même taille de lot effective sans payer la mémoire du lot. Réduire le rang LoRA ne fait presque rien gagner : les adaptateurs sont minuscules.Activations dominate consumption at long sequences: cutting max_seq_length frees a lot of memory at often-zero quality cost (if your examples fit). And gradient accumulation keeps the same effective batch size without paying for the batch's memory. Cutting the LoRA rank gains almost nothing: adapters are tiny.