Fine-tuner un modèle frontier propriétaire coûte cher en données, essais et itérations. À l'inverse, QLoRA permet d'adapter un 7B–8B localement sur un GPU grand public, avec un coût marginal très faible. Et sur une tâche métier étroite et correctement évaluée, un 7B/8B fine-tuné peut rivaliser avec, voire dépasser, un modèle généraliste beaucoup plus gros. La catch : il faut comprendre ce qui se passe sous le capot. Ce guide couvre la théorie (papiers ICLR/NeurIPS sourcés) et le code exécutable.
Fine-tuning LoRA (Low-Rank Adaptation) & QLoRA en Python 2026 : théorie, code et benchmarks
À qui s'adresse cet article ?
- Vous voulez fine-tuner un LLM (Llama, Mistral, Qwen) sur vos données métier
- Vous avez un GPU 16+ Go (RTX 4090) ou un accès A100/H100
- Vous voulez comprendre le pourquoi mathématique, pas juste copier du code
- Vous avez lu la théorie mais il vous manque le bridge vers la pratique
TL;DR
- LoRA = entraîner 2 petites matrices A, B telles que ΔW ≈ BA avec rank(BA) << min(d_in, d_out) → 99 % moins de paramètres
- QLoRA = LoRA + quantization 4-bit NF4 du modèle base → fine-tuning 70B sur 1× A100 80 Go
- r=16, α=32 = sweet spot empirique pour 90 % des cas
- target_modules='all-linear' = +2-5 % sur benchmarks vs attention-only, coût 2× en mémoire
- 1-3 epochs max pour éviter le catastrophic forgetting
- Export GGUF → inférence avec Ollama en local
1. Quand fine-tuner — RAG vs fine-tuning vs prompt
Avant toute chose : fine-tuner n'est souvent pas la bonne réponse. Pour intégrer des connaissances factuelles ou des documents privés, le RAG est 10× moins cher et plus flexible (mise à jour en temps réel). Le fine-tuning brille dans 4 cas précis :
| Besoin | Prompt | RAG | Fine-tuning |
|---|---|---|---|
| Injecter des faits / docs récents | ⚠ Contexte limité | ✓ Idéal | ✗ Obsolète vite |
| Enseigner un style / ton d'écriture | ⚠ Few-shot fragile | ✗ Non adapté | ✓ Parfait |
| Format de sortie structuré fiable (JSON, DSL) | ⚠ ~90 % de succès | — | ✓ 99 %+ |
| Terminologie métier (juridique, médical) | ✗ Hallucinations | ⚠ Partiel | ✓ Excellent |
| Raisonnement nouveau / skill spécifique | ✗ Impossible | ✗ Impossible | ✓ Seule option |
Règle d'or : fine-tunez quand vous devez modifier le comportement du modèle, pas pour lui donner des connaissances. Un agent support client fine-tuné sur 5 000 conversations réelles apprend le style, la structure et les patterns de résolution — qu'aucun prompt ne peut reproduire de façon fiable.
1.1 Qu'est-ce que LoRA résout exactement ?
Le fine-tuning classique (full fine-tuning) entraîne tous les paramètres du modèle — 8 milliards pour Llama 8B, 70 milliards pour Llama 70B. Coût matériel et stockage prohibitifs : fine-tuner Llama 70B en FP16 nécessite ~780 Go de VRAM (modèle + optimiseur + gradients + activations) et produit un nouveau checkpoint de 140 Go par variante.
LoRA (Hu et al., ICLR 2022) propose une réponse élégante : on gèle les poids originaux et on entraîne seulement deux petites matrices de rang faible qui représentent la différence à apprendre. Résultat empirique : ~0.1 à 1 % des paramètres d'origine suffisent pour matcher la qualité du full fine-tuning sur la plupart des tâches.
2. Théorie LoRA — low-rank decomposition et intrinsic rank
2.1 La formulation mathématique
Soit W₀ ∈ ℝ^(d×k) une matrice de poids figée d'un Transformer (par exemple la matrice de projection d'une Attention Q, K, V). Un fine-tuning classique apprend une mise à jour ΔW telle que :
W = W₀ + ΔW (où ΔW ∈ ℝ^(d×k), tous paramètres entraînables)
LoRA fait l'hypothèse (étayée théoriquement, cf. section 2.2) que ΔW est intrinsèquement de rang faible. Autrement dit, il existe deux matrices A ∈ ℝ^(r×k) et B ∈ ℝ^(d×r) avec r << min(d, k), telles que :
ΔW = BA (rank(BA) ≤ r << min(d, k))
W = W₀ + BA (W₀ gelé, A et B entraînables)
h = Wx = W₀x + BAx (A initialisé aléatoirement, B = 0 au départ)
Comptage des paramètres. Pour une matrice Q de Llama 3.1 8B, d = k = 4096. Full fine-tuning = 4096² ≈ 16.8M paramètres pour cette seule matrice. Avec LoRA r=16 : (16 × 4096) + (4096 × 16) = 131 072 paramètres, soit 128× moins. Et il y a ~32 matrices d'attention par couche × 32 couches = environ 1024 matrices dans le modèle.
Scaling factor α. En pratique, LoRA applique un scaling : ΔW_effective = (α / r) × BA. Le paramètre α permet d'ajuster l'amplitude de la mise à jour indépendamment de r. Convention standard : α = 2r (donc α/r = 2). C'est pourquoi on voit souvent r=16, α=32 dans le code.
2.2 Pourquoi ça marche — l'intrinsic rank des LLMs
La question théorique clé : pourquoi peut-on se contenter d'un rank si bas ? La réponse vient d'Aghajanyan et al. (ACL 2021, "Intrinsic Dimensionality Explains the Effectiveness of Language Model Fine-Tuning"). Leur résultat central : le fine-tuning d'un LLM peut être paramétré par une variable latente de dimension intrinsèque très faible — pour RoBERTa-large, d_int ≈ 200 suffisait pour atteindre 90 % de la perf full fine-tuning.
Intuition : les LLMs pré-entraînés opèrent déjà dans un espace de représentations riche. Une nouvelle tâche n'a pas besoin de recréer tout cet espace — elle a juste besoin de réorienter certaines directions. Mathématiquement, ces réorientations sont capturées par des mises à jour de rang faible.
Référence : Hu, Shen, Wallis, Allen-Zhu, Li, Wang, Wang, Chen, "LoRA: Low-Rank Adaptation of Large Language Models", ICLR 2022 (arXiv:2106.09685). Le papier démontre expérimentalement que même r=1 suffit pour certaines tâches GLUE, et que r=8 matche full fine-tuning sur GPT-3 175B.
2.3 Pas d'overhead à l'inférence
Une propriété cruciale : après entraînement, on peut fusionner les poids LoRA dans le modèle base en calculant W = W₀ + (α/r)BA une seule fois. L'inférence devient alors identique à celle du modèle original : 0 % de surcoût latence vs un modèle fine-tuné classiquement. C'est ce qui rend LoRA praticable en production.
3. QLoRA — NF4, double quantization, paged optimizers
Même avec LoRA, on a toujours besoin de charger les poids W₀ en mémoire. Pour Llama 70B en FP16 : 140 Go. C'est le problème que QLoRA (Dettmers, Pagnoni, Holtzman, Zettlemoyer, NeurIPS 2023) résout en quantisant les poids base en 4-bit tout en entraînant les adaptateurs LoRA en précision plus haute.
3.1 4-bit NormalFloat (NF4)
Les poids pré-entraînés d'un LLM suivent une distribution approximativement gaussienne centrée (empirique, vérifié sur Llama). Quantiser uniformément sur 16 niveaux gaspille de la précision dans les queues (valeurs rares) et en manque au centre.
NF4 (NormalFloat 4-bit) définit 16 niveaux de quantization optimalement distribués pour une gaussienne : les 16 valeurs sont les quantiles equi-probables d'une N(0,1), rescalées pour couvrir [-1, 1]. Chaque bloc de 64 poids est divisé par son maximum absolu avant quantization.
# Les 16 niveaux NF4 (approximatifs) :
[-1.0, -0.696, -0.525, -0.395, -0.285, -0.184, -0.091, 0.0,
0.080, 0.161, 0.246, 0.338, 0.441, 0.563, 0.723, 1.0]
# Quantization bloc par bloc (64 poids) :
# 1. absmax = max(|W_bloc|)
# 2. W_normalisé = W_bloc / absmax → [-1, 1]
# 3. Pour chaque w : choisir le niveau NF4 le plus proche
# 4. Stocker l'index 4-bit + l'absmax (FP32) du bloc
Dettmers et al. prouvent que NF4 est information-theoretically optimal pour des poids gaussiens, avec une erreur de quantization 0.03 inférieure à la quantization int4 uniforme.
3.2 Double quantization
Stocker un absmax FP32 par bloc de 64 poids coûte 32 bits / 64 poids = 0.5 bit/poids d'overhead. Pour un modèle 70B, cela représente 4.4 Go rien que pour les constantes de quantization. QLoRA ajoute une seconde quantization : ces absmax sont eux-mêmes quantisés en int8 (par blocs de 256), réduisant l'overhead à ~0.127 bit/poids — économie de ~3 Go sur un 70B.
3.3 Paged optimizers (NVIDIA Unified Memory)
Les optimiseurs (AdamW) maintiennent deux états par paramètre (moment 1 et 2), soit 2× la taille du modèle en FP32. Pour LoRA, ces états sont petits (puisque peu de paramètres entraînables). Mais les pics de mémoire pendant la backprop peuvent faire sauter le GPU OOM.
Les paged optimizers utilisent la Unified Memory de NVIDIA pour que les états optimiseurs soient transparentément déplacés entre RAM CPU et VRAM GPU en cas de pression mémoire. Dans la pratique, ils limitent fortement les OOM sporadiques avec un impact souvent modéré sur les performances.
3.4 Résultat : Llama 65B sur 1× A100 48 Go
Résultats empiriques clés du papier QLoRA :
- Llama 65B full fine-tuning FP16 : besoins VRAM impraticables sur un seul GPU grand public
- Llama 65B fine-tunable sur 1× GPU 48 Go grâce à NF4 + double quantization + paged optimizers
- QLoRA atteint des performances proches de la pleine précision (near full-precision task performance) sur les benchmarks évalués
- Le modèle Guanaco (issu de QLoRA sur Llama 65B) atteint 99,3 % des performances de ChatGPT sur le benchmark Vicuna — c'est cette comparaison précise qui est souvent citée, pas un ratio général "QLoRA vs FP16"
Référence : Dettmers, Pagnoni, Holtzman, Zettlemoyer, "QLoRA: Efficient Finetuning of Quantized LLMs", NeurIPS 2023 (arXiv:2305.14314).
4. Variantes 2024-2025 — DoRA, VeRA, PiSSA, LoRA+
LoRA a engendré une famille de variantes qui cherchent à améliorer la qualité, réduire les paramètres, ou accélérer la convergence. Les 4 plus importantes :
| Méthode | Principe | Gain typique | Quand l'utiliser |
|---|---|---|---|
| DoRA ICML 2024 |
Décompose W = m·(W/‖W‖), entraîne magnitude m et direction séparément | +1-3 % vs LoRA même rank | Tâches où rank=r LoRA sous-performe |
| VeRA ICLR 2024 |
A, B partagées et figées (random), entraîne 2 vecteurs d'échelle | 10× moins de params, -1 % qualité | Déploiement multi-tenant (1000+ variantes) |
| PiSSA NeurIPS 2024 |
Initialise A, B avec les r premiers vecteurs singuliers de W₀ (SVD) | Convergence 2× plus rapide | Budget GPU limité, itérations courtes |
| LoRA+ ICML 2024 |
Learning rate B = 16× LR de A (hypothèse : B tend vers 0 au départ) | +1-2 % qualité, même coût | Toujours — gain gratuit |
Recommandation pratique : commencez par QLoRA standard (couvert dans la suite de l'article). Si la qualité n'est pas suffisante, essayez DoRA (supporté par peft). Utilisez VeRA uniquement si vous avez besoin de déployer des centaines de variantes du même modèle.
Références primaires
- DoRA — Liu et al., "DoRA: Weight-Decomposed Low-Rank Adaptation", ICML 2024 (arXiv:2402.09353)
- VeRA — Kopiczko, Blankevoort, Asano, "VeRA: Vector-based Random Matrix Adaptation", ICLR 2024 (arXiv:2310.11454)
- PiSSA — Meng, Wang, Zhang, "PiSSA: Principal Singular Values and Singular Vectors Adaptation", NeurIPS 2024 (arXiv:2404.02948)
- LoRA+ — Hayou, Ghosh, Yu, "LoRA+: Efficient Low Rank Adaptation of Large Models", ICML 2024 (arXiv:2402.12354)
Les gains listés dans le tableau ci-dessus sont des ordres de grandeur tirés de ces papiers — les résultats réels dépendent du modèle base, du dataset et de la tâche d'évaluation.
5. Setup — peft, bitsandbytes, trl, accelerate
L'écosystème Hugging Face fournit tous les blocs. Installation :
# CUDA 12.1+ requis pour bitsandbytes récent
# Versions testées au moment de rédiger cet article — ajustez aux releases courantes
pip install "torch>=2.5" --index-url https://download.pytorch.org/whl/cu121
pip install "transformers>=4.45" "peft>=0.14" "bitsandbytes>=0.44" \
"trl>=0.12" "accelerate>=1.0" "datasets>=3.0"
# Sanity check : CUDA visible + import bitsandbytes OK
python -c "import torch; print(torch.cuda.is_available(), torch.version.cuda)"
python -m bitsandbytes # premier sanity check — non suffisant
# Test runtime réel : charger un petit modèle en 4-bit
python -c "
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
import torch
m = AutoModelForCausalLM.from_pretrained(
'TinyLlama/TinyLlama-1.1B-Chat-v1.0',
quantization_config=BitsAndBytesConfig(load_in_4bit=True),
device_map='auto'
)
print('✓ 4-bit quantization fonctionne — VRAM:', torch.cuda.memory_allocated() // 1024**2, 'Mo')
"
Rôle de chaque bibliothèque :
- transformers : chargement des modèles Llama/Mistral/Qwen, tokenizers
- bitsandbytes : quantization NF4 et linear layers 4-bit
- peft : LoraConfig, get_peft_model, merge_and_unload
- trl : SFTTrainer (boucle d'entraînement SFT optimisée)
- accelerate : gestion GPU multi/mono, mixed precision, DeepSpeed
- datasets : chargement et tokenization du corpus d'entraînement
6. Préparation dataset — ChatML, masking, packing
6.1 Format des données
Trois formats standards pour le fine-tuning instruction :
# Format Alpaca (simple, ancien)
{
"instruction": "Résume cet email en 2 phrases.",
"input": "Bonjour, nous avons bien reçu...",
"output": "L'expéditeur confirme la réception..."
}
# Format ChatML (recommandé 2025+, natif Llama 3 / Qwen2.5)
{
"messages": [
{"role": "system", "content": "Tu es un assistant spécialisé en..."},
{"role": "user", "content": "Bonjour, nous avons reçu..."},
{"role": "assistant", "content": "L'expéditeur confirme..."}
]
}
# Format ShareGPT (multi-turn, hérité de conversations ChatGPT)
{
"conversations": [
{"from": "human", "value": "..."},
{"from": "gpt", "value": "..."}
]
}
6.2 Tokenization avec apply_chat_template
Chaque famille de modèles a son format de tokens spéciaux (<|im_start|>, <|eot_id|>, etc.). Utiliser tokenizer.apply_chat_template() applique automatiquement le format correct :
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
messages = [
{"role": "user", "content": "Traduis 'Hello' en français."},
{"role": "assistant", "content": "Bonjour"}
]
# Applique le template Llama 3 (avec <|begin_of_text|>, <|start_header_id|>, etc.)
text = tokenizer.apply_chat_template(messages, tokenize=False)
6.3 Masking du prompt — calculer la loss uniquement sur la réponse
Erreur courante : calculer la loss sur tout le texte (prompt + réponse). Cela force le modèle à apprendre à générer des prompts utilisateur, ce qui n'est pas l'objectif.
Bonne pratique : masquer les tokens du prompt (label = -100, ignoré dans CrossEntropyLoss) et ne calculer la loss que sur les tokens de la réponse. SFTTrainer de trl le fait automatiquement si on utilise le paramètre DataCollatorForCompletionOnlyLM ou la nouvelle API SFTConfig(completion_only_loss=True).
6.4 Packing — efficacité du batch
Les séquences ont des longueurs variables. Le padding classique gaspille jusqu'à 50 % du compute GPU. Le packing concatène plusieurs séquences courtes en une seule de longueur fixe (séparées par eos_token), avec une attention mask bloc-diagonale pour empêcher les séquences de se voir entre elles. Gain : 30-50 % de vitesse d'entraînement à qualité identique.
7. Code complet — fine-tuning Llama 3.1 8B avec QLoRA
Script complet, exécutable tel quel sur une RTX 4090 (24 Go) ou A100 40 Go. Il fine-tune Llama 3.1 8B sur le dataset HuggingFaceH4/ultrachat_200k (échantillon).
# finetune_qlora.py — Fine-tuning Llama 3.1 8B avec QLoRA
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
MODEL_ID = "meta-llama/Llama-3.1-8B-Instruct"
OUTPUT_DIR = "./llama3-lora-finetuned"
# 1. Config quantization 4-bit NF4 avec double quantization
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16,
)
# 2. Charger le modèle quantisé
model = AutoModelForCausalLM.from_pretrained(
MODEL_ID,
quantization_config=bnb_config,
device_map="auto",
attn_implementation="flash_attention_2", # si disponible
)
model.config.use_cache = False # incompatible avec gradient_checkpointing
model = prepare_model_for_kbit_training(model)
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
tokenizer.pad_token = tokenizer.eos_token
# 3. Config LoRA
lora_config = LoraConfig(
r=16, # rank
lora_alpha=32, # alpha (scaling = alpha/r = 2)
target_modules=[ # attention + MLP
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: trainable params: 41,943,040 || all params: 8,072,204,288 || trainable%: 0.52
# 4. Charger et formater le dataset
dataset = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft[:5000]")
def format_chat(example):
return {"text": tokenizer.apply_chat_template(example["messages"], tokenize=False)}
dataset = dataset.map(format_chat, remove_columns=dataset.column_names)
# 5. Training config
training_args = SFTConfig(
output_dir=OUTPUT_DIR,
num_train_epochs=2,
per_device_train_batch_size=2,
gradient_accumulation_steps=4, # effective batch = 8
gradient_checkpointing=True, # économise VRAM
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
optim="paged_adamw_8bit", # paged optimizer QLoRA
bf16=True,
max_seq_length=2048,
packing=True, # packing pour efficacité
logging_steps=10,
save_strategy="epoch",
report_to="tensorboard",
completion_only_loss=True, # masque le prompt
)
# 6. Trainer + lancement
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
args=training_args,
peft_config=lora_config,
dataset_text_field="text",
)
trainer.train()
trainer.save_model(OUTPUT_DIR)
Durée typique : sur RTX 4090 (24 Go), 5000 exemples × 2 epochs ≈ 45-60 min. Sur A100 80 Go, ~25-30 min. Monitoring via TensorBoard — surveiller la train/loss (doit décroître) et grad_norm (doit rester stable < 2.0).
8. Configuration LoRA — rank, alpha, target_modules
8.1 Choix du rank — r=8, 16, 32, 64 ?
| Rank r | Params entraînables (Llama 8B) | Cas d'usage |
|---|---|---|
| r=4 | ~10 M (0.13 %) | Style transfer léger, tone adjustment |
| r=8 | ~21 M (0.26 %) | Instruction following basique, Q&A simple |
| r=16 ⭐ | ~42 M (0.52 %) | Sweet spot — 90 % des cas |
| r=32 | ~83 M (1.03 %) | Raisonnement, code, math |
| r=64 | ~167 M (2.07 %) | Domaines très spécialisés (medical, légal dense) |
8.2 target_modules — attention-only vs all-linear
Les matrices visées par LoRA dans un Transformer Llama :
# Attention-only (minimum viable, -30% de params entraînables)
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj"]
# All-linear (recommandé QLoRA paper)
target_modules = [
"q_proj", "k_proj", "v_proj", "o_proj", # attention
"gate_proj", "up_proj", "down_proj", # MLP (SwiGLU)
]
# Shortcut peft — découverte auto
target_modules = "all-linear"
Règle empirique (QLoRA paper) : targeter toutes les couches linéaires + rank faible (r=8 à 16) donne de meilleurs résultats que attention-only + rank élevé. Le signal est distribué dans les MLP, pas seulement dans l'attention.
9. Training loop — paramètres clés
Quelques choix techniques qui font vraiment la différence :
- learning_rate = 2e-4 à 3e-4 : plus haut qu'un full fine-tuning (car peu de params). LR trop bas = pas de progression ; trop haut = divergence
- lr_scheduler_type = "cosine" : décroissance lisse, évite les plateaux
- warmup_ratio = 0.03 : les premiers 3 % des steps montent progressivement le LR (évite l'explosion de gradient au début)
- num_train_epochs = 1-3 : au-delà de 3, risque élevé d'overfitting — LoRA converge vite
- optim = "paged_adamw_8bit" : AdamW en 8-bit + paged memory pour QLoRA
- bf16 = True : bfloat16 pour les activations (support Ampere+, A100/H100/RTX 30+)
- gradient_checkpointing = True : économie VRAM 30-40 % au prix de +20 % temps compute
10. Benchmarks VRAM réels — 8B / 13B / 70B
| Modèle | LoRA FP16 | QLoRA NF4 | + grad checkpoint | GPU minimal |
|---|---|---|---|---|
| Llama 3.1 8B (r=16) | ~22 Go | ~13 Go | ~10 Go | RTX 3090 (24 Go) |
| Mistral 7B (r=16) | ~20 Go | ~11 Go | ~9 Go | RTX 3090 (24 Go) |
| Llama 13B (r=16) | ~36 Go | ~20 Go | ~16 Go | RTX 4090 (24 Go) |
| Llama 70B (r=16) | ~180 Go | ~48 Go | ~38 Go | A100 48 Go |
Mesures indicatives : batch_size=2, seq_len=2048, target_modules=all-linear. Varie ±15 % selon drivers et taille batch. Unsloth permet d'économiser encore ~30 % via optimisations custom (kernels CUDA dédiés).
11. Évaluation — perplexity, benchmarks, catastrophic forgetting
Trois niveaux d'évaluation :
11.1 Perplexity sur eval set
import math
eval_dataset = load_dataset("HuggingFaceH4/ultrachat_200k", split="test_sft[:500]")
trainer.eval_dataset = eval_dataset.map(format_chat)
metrics = trainer.evaluate()
ppl = math.exp(metrics["eval_loss"])
print(f"Perplexity: {ppl:.2f}")
# La perplexité dépend fortement du tokenizer, du dataset et du format.
# À comparer surtout au modèle de base et à vos runs précédents,
# plus qu'à une valeur absolue universelle.
11.2 Benchmarks standards (lm-evaluation-harness)
Pour détecter le catastrophic forgetting, évaluez sur des benchmarks généralistes avant et après fine-tuning :
pip install lm-eval
lm_eval --model hf --model_args pretrained=./llama3-lora-finetuned \
--tasks mmlu,arc_challenge,hellaswag,gsm8k \
--batch_size 4
Signaux à surveiller : une chute de >3 points sur MMLU indique du forgetting significatif. Un modèle fine-tuné sur support client ne devrait pas oublier comment résoudre un problème de math.
11.3 Évaluation métier custom
Les métriques génériques ne disent pas si le modèle est bon sur votre tâche. Construisez un eval set de 100-500 exemples représentatifs, avec références annotées par un expert métier. Utilisez un LLM juge (GPT-4o, Claude) pour scorer automatiquement — ou mieux, des métriques déterministes si votre domaine le permet (exact match, F1, BLEU selon cas).
12. Merger les poids + export GGUF pour Ollama
12.1 Merger les adaptateurs LoRA
from peft import PeftModel
from transformers import AutoModelForCausalLM
# Recharger le modèle base en FP16 (pas en 4-bit) pour le merge
base_model = AutoModelForCausalLM.from_pretrained(
MODEL_ID, torch_dtype=torch.bfloat16, device_map="auto"
)
model = PeftModel.from_pretrained(base_model, "./llama3-lora-finetuned")
merged = model.merge_and_unload()
merged.save_pretrained("./llama3-merged", safe_serialization=True)
tokenizer.save_pretrained("./llama3-merged")
12.2 Conversion GGUF + quantization
# Installer llama.cpp
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp && make
# Convertir HF → GGUF FP16
python convert_hf_to_gguf.py ../llama3-merged \
--outfile llama3-ft.f16.gguf --outtype f16
# Quantiser en Q4_K_M (équilibre qualité/taille)
./llama-quantize llama3-ft.f16.gguf llama3-ft.Q4_K_M.gguf Q4_K_M
12.3 Créer un modèle Ollama
# Modelfile
FROM ./llama3-ft.Q4_K_M.gguf
TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>
{{ .System }}<|eot_id|>{{ end }}<|start_header_id|>user<|end_header_id|>
{{ .Prompt }}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
"""
PARAMETER stop "<|eot_id|>"
PARAMETER temperature 0.7
# Puis :
ollama create mon-modele -f Modelfile
ollama run mon-modele
13. 7 pièges courants à éviter
1. Oublier de masquer le prompt dans la loss
Le modèle apprend alors à générer des instructions utilisateur. Solution : completion_only_loss=True.
2. LR trop bas (< 1e-5)
La loss stagne. LoRA demande un LR 10× plus haut qu'un full fine-tuning. Commencez à 2e-4.
3. Trop d'epochs sur petit dataset
500 exemples × 10 epochs = memorization. Règle : taille dataset < 1000 → max 2 epochs.
4. target_modules = ["q_proj"] uniquement
Cible trop restrictive. Minimum : attention complète (q, k, v, o). Préférable : all-linear.
5. use_cache=True avec gradient_checkpointing
Incompatibles. Mettre model.config.use_cache = False avant le training.
6. Mauvais chat template
Entraîner Llama 3 avec le template ChatML d'OpenAI casse le modèle. Toujours utiliser apply_chat_template().
7. Déploiement sans évaluer le catastrophic forgetting
Un modèle qui répond parfaitement sur votre domaine mais hallucine sur du simple math est inutilisable. Toujours évaluer MMLU+ARC avant mise en prod.
FAQ
Articles liés
Formation
Formation LLM & IA Générative 2026
Prompt engineering, RAG, fine-tuning LoRA, agents LangChain — gratuit.
Voir la formation →Article
Ollama 2026 : héberger un LLM en local
Servir votre modèle fine-tuné via Ollama (GGUF Q4_K_M inclus).
Lire →Article
RAG avec Ollama en Python
Alternative au fine-tuning pour les connaissances factuelles — RAG complet avec ChromaDB.
Lire →Article
FastAPI streaming LLM 2026
Exposer votre modèle fine-tuné en production avec streaming SSE.
Lire →Consulting DEV-AI
Vous voulez fine-tuner un LLM sur vos données métier ?
Sélection du modèle base, préparation dataset, entraînement, évaluation, déploiement production. Accompagnement complet, confidentialité garantie.
Contacter l'équipe →