LoRA / QLoRA / PEFT — la base technique du FT 2026
TL;DR LoRA (Low-Rank Adaptation) est la technique dominante de FT en 2026 : tu n'entraînes pas les ~7-70B paramètres du modèle, tu apprends des matrices de rang faible (typiquement r=16-64) qui modifient les couches d'attention. Résultat : 0.1-1% des paramètres entraînés, mémoire GPU /10, vitesse × 2-5, et qualité comparable à full FT sur la plupart des tâches. QLoRA = LoRA + quantization 4-bit du base model = tu FT un 70B sur un seul H100. Outils 2026 : Unsloth (2× plus rapide), Axolotl (config YAML production), torchtune (Meta officiel), HF PEFT (lib bas niveau). Multi-adapter serving (S-LoRA, Punica) permet de servir 100+ adapters sur 1 GPU.
🧠 Mental model
Full Fine-Tuning :
W (frozen) → W' = W + ΔW
ΔW est une matrice complète, taille d×k
Pour Mistral 7B : ~7B paramètres à updater. 28 GB VRAM mini.
LoRA :
W (frozen) → W + B·A
où A : d×r, B : r×k, r << d
Pour r=16, d=k=4096 : 4096×16 + 16×4096 = 131K params
vs 4096×4096 = 16.7M params en full FT
→ 0.78% des params, même expressivité pour beaucoup de tâches
┌────────────────────────────────┐
│ Weight W (frozen) │ gros pavé bleu
│ d × k │
└─────────────┬──────────────────┘
│
⊕ ← addition à l'inference
│
┌─────────────▼──────────────────┐
│ B (r×k) ┃ A (d×r) │ petits pavés rouges
│ ┃ │ = adapter LoRA
└────────────────────────────────┘
Quand on serve :
Option 1 — merge : W_new = W + B·A, on déploie W_new (gros, mais zéro overhead)
Option 2 — adapter : on garde W frozen + B·A chargé séparément
permet 100 adapters sur 1 GPU (S-LoRA)Analogie : imagine que le base model est une voiture de série. Full FT = construire une nouvelle voiture. LoRA = poser des autocollants paramétrés sur certaines pièces (volant, levier) qui en modifient le comportement. Les autocollants pèsent 100g, la voiture pèse 1500kg, mais ils suffisent à transformer la conduite si tu choisis bien où les coller (couches d'attention q_proj, v_proj notamment).
🛠️ Code minimal
Hugging Face PEFT (vanilla)
# train_lora.py
from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, Trainer
from datasets import load_dataset
MODEL = "mistralai/Mistral-Small-Instruct-2502"
tokenizer = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype="bfloat16", device_map="auto")
lora_cfg = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=64, # rank — 16, 32, 64 courants
lora_alpha=128, # scaling, souvent 2*r
lora_dropout=0.05,
bias="none",
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
)
model = get_peft_model(model, lora_cfg)
model.print_trainable_parameters()
# trainable params: 167M || all params: 23.7B || trainable%: 0.70
ds = load_dataset("json", data_files="data/train.jsonl")["train"]
def fmt(ex):
msg = [{"role": "user", "content": ex["input"]},
{"role": "assistant", "content": ex["output"]}]
text = tokenizer.apply_chat_template(msg, tokenize=False)
return tokenizer(text, truncation=True, max_length=4096)
ds = ds.map(fmt, remove_columns=ds.column_names)
trainer = Trainer(
model=model,
args=TrainingArguments(
output_dir="out/mistral-small-lora",
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=16,
learning_rate=2e-4,
lr_scheduler_type="cosine",
warmup_ratio=0.03,
bf16=True,
logging_steps=10,
save_strategy="epoch",
),
train_dataset=ds,
)
trainer.train()
model.save_pretrained("out/mistral-small-lora-final")Unsloth (2× plus rapide, 50% moins VRAM)
# train_unsloth.py
from unsloth import FastLanguageModel
import torch
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Mistral-Small-Instruct-2502-bnb-4bit",
max_seq_length=4096,
load_in_4bit=True, # QLoRA
)
model = FastLanguageModel.get_peft_model(
model,
r=64,
lora_alpha=128,
target_modules=["q_proj","k_proj","v_proj","o_proj",
"gate_proj","up_proj","down_proj"],
use_gradient_checkpointing="unsloth", # +30% mémoire libre
)
# … puis Trainer HF classique🎬 Cas d'usage concrets
1. LoRA Mistral 7B sur écritures comptables FR
PME éditrice de logiciel comptable veut un assistant qui propose la bonne écriture (PCG français : 411, 401, 707, 4456...) à partir d'une description en langage naturel ("règlement client par virement 1200€ TTC"). Dataset : 50 000 écritures historiques anonymisées. LoRA r=32 sur Mistral 7B, training 6h sur 1× A100, accuracy 92% (vs 78% pour Mistral 7B base, 88% pour claude-sonnet-4-6 en few-shot). Modèle servi en vLLM sur Scaleway, 0.0005€/requête vs ~0.006€ Sonnet (à 3 USD/15 USD le M tok in/out) : ROI < 2 mois.
2. LoRA Llama 3 sur corpus juridique français
Legaltech qui propose un copilote pour notaires (actes, baux, donations). FT LoRA r=64 sur Llama 3.1 8B + 8 000 actes notariés anonymisés + Code civil annoté. QLoRA pour tenir sur 1× RTX 4090 (40h training). Le modèle apprend la structure formelle des actes (visas, articles cités, formules consacrées) que ni RAG ni few-shot n'arrivaient à imposer rigoureusement. Déployé via TGI on-prem chez le client (souveraineté).
3. LoRA "style maison" pour e-commerce premium
Marque de luxe française veut un chatbot dont le ton respecte la charte éditoriale (tutoiement interdit, vocabulaire spécifique "soulier" et non "chaussure", références culturelles). Fine-tune LoRA r=16 sur 2 000 échanges email du service client historique. Avant : claude-sonnet-4-6 + prompt de 3K tokens, dérives stylistiques fréquentes. Après : LoRA Mistral Small, prompt 500 tokens, ton verrouillé. Bonus : 4 adapters distincts (FR, EN, IT, JP) servis en parallèle via S-LoRA sur 1 GPU.
⚠️ Le bon réflexe senior avant de FT le style : essaie d'abord le prompt caching côté Claude. Mettre la charte éditoriale (3K tokens) dans un préfixe système stable avec
cache_controlramène le coût du prompt à ~0.1× sur les hits — souvent le FT "style" n'est même plus rentable. On ne FT pour le style que quand (a) le volume est massif (le self-host bat l'API même cache chaud) ou (b) le verrouillage stylistique du prompt reste insuffisant. Ici c'est (b) : les dérives persistaient malgré le prompt long.
🛠️ Exemple end-to-end — Axolotl LoRA Mistral Small sur 10K CV + déploiement vLLM
Contexte : cabinet de recrutement IT veut un classifier-extracteur de CV : input = CV brut (PDF→texte), output = JSON structuré {poste, années_xp, stack, séniorité, mobilité, prétentions}. Volume : 8 000 CV/mois.
Étape 1 — Préparation des données
// data/train.jsonl (10 000 exemples curated par 2 recruteurs séniors)
{"input": "Jean DURAND\n5 ans dev backend Python/Django...\n...", "output": "{\"poste\":\"Backend Engineer\",\"annees_xp\":5,\"stack\":[\"Python\",\"Django\",\"PostgreSQL\"],\"seniorite\":\"Confirmé\",\"mobilite\":\"Paris\",\"pretentions\":\"55-65k€\"}"}
{"input": "Marie LEROY\n...", "output": "{...}"}Train/val/test : 8000/1000/1000. Curation prend 15j×2 recruteurs (le job le plus long).
Étape 2 — Config Axolotl
# axolotl_cv.yml
base_model: mistralai/Mistral-Small-Instruct-2502
model_type: AutoModelForCausalLM
tokenizer_type: AutoTokenizer
load_in_4bit: true # QLoRA
strict: false
datasets:
- path: data/train.jsonl
type: chat_template
chat_template: mistral
field_messages: messages
dataset_prepared_path: prepared
val_set_size: 0.1
output_dir: out/mistral-small-cv-lora
adapter: qlora
lora_r: 32
lora_alpha: 64
lora_dropout: 0.05
lora_target_modules:
- q_proj
- k_proj
- v_proj
- o_proj
- gate_proj
- up_proj
- down_proj
sequence_len: 4096
sample_packing: true
pad_to_sequence_len: true
gradient_accumulation_steps: 8
micro_batch_size: 2
num_epochs: 3
optimizer: paged_adamw_8bit
learning_rate: 2e-4
lr_scheduler: cosine
warmup_ratio: 0.05
bf16: true
gradient_checkpointing: true
flash_attention: true
logging_steps: 10
eval_steps: 100
save_strategy: epoch
weight_decay: 0.0Étape 3 — Lancement
# 1× H100 80GB Scaleway, ~6h training
accelerate launch -m axolotl.cli.train axolotl_cv.yml
# Logs : loss 1.45 → 0.32, eval_loss 1.48 → 0.41
# Aucun signe d'overfit (eval suit train)Étape 4 — Merge ou adapter ?
# merge.py — pour déploiement vLLM sans overhead adapter
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
base = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-Small-Instruct-2502", torch_dtype="bfloat16"
)
ft = PeftModel.from_pretrained(base, "out/mistral-small-cv-lora")
merged = ft.merge_and_unload()
merged.save_pretrained("models/mistral-small-cv-merged")
AutoTokenizer.from_pretrained("mistralai/Mistral-Small-Instruct-2502") \
.save_pretrained("models/mistral-small-cv-merged")Étape 5 — Déploiement vLLM
# Dockerfile.vllm
FROM vllm/vllm-openai:v0.7.0
COPY models/mistral-small-cv-merged /models/cv
EXPOSE 8000
CMD ["--model", "/models/cv", \
"--served-model-name", "cv-extractor", \
"--max-model-len", "8192", \
"--gpu-memory-utilization", "0.92", \
"--dtype", "bfloat16", \
"--enable-prefix-caching"]# k8s/deployment.yml — Scaleway Kapsule + GPU H100
apiVersion: apps/v1
kind: Deployment
metadata:
name: cv-extractor
spec:
replicas: 2
selector:
matchLabels:
app: cv-extractor
template:
metadata:
labels:
app: cv-extractor
spec:
nodeSelector:
k8s.scaleway.com/pool-name: gpu-h100
containers:
- name: vllm
image: registry/cv-vllm:1.0
resources:
limits:
nvidia.com/gpu: "1"
readinessProbe:
httpGet: {path: /health, port: 8000}
initialDelaySeconds: 60Étape 6 — Mesure de performances
Métrique | claude-sonnet-4-6 | Mistral-S vanilla | Mistral-S LoRA FT
--------------------------|-------------------|-------------------|-------------------
JSON valide | 99%+ * | 82% | 99.6%
Champs corrects (avg) | 91% | 74% | 95.4%
Stack tech (recall) | 88% | 69% | 96%
Latence p50 | 2.4s | 1.8s | 0.6s
Coût/1000 CV | ~6€ ** | 0.45€ (API) | 0.18€ (self-host)* Avec Claude on utilise les structured outputs (output_config.format + schéma JSON/Pydantic, via client.messages.parse()) — le "JSON valide" est garanti par construction, pas une métrique à mesurer. C'est précisément la capacité que le FT essaie de reproduire côté open-weights (le LoRA apprend la structure ; Claude la contraint au décodage).
** ~6€/1000 CV à 3 USD/15 USD le M tok (in/out) sur claude-sonnet-4-6, CV ~1.5K tok in / ~0.3K tok out. Le prompt système (schéma + instructions, ~2K tok) est mis en cache (cache_control) → coût input réel ≈ 0.1× sur les hits.
ROI annuel : 8K CV/mois × 12 = 96K CV. Économie API ~560€/an + temps recruteur saved (3 min/CV × 96K × 35€/h = 168 000€) = ROI massif. Investissement initial 32k€ payback < 3 mois.
🧠 Le calcul que défend un staff : l'économie d'API (560€/an) est du bruit devant le coût humain évité (168k€). Donc le vrai critère de décision n'est pas "self-host moins cher que l'API" mais "quel setup atteint l'accuracy qui débloque l'automatisation ?". Ici le LoRA gagne sur la latence p50 (0.6s vs 2.4s) et le recall stack (96% vs 88%) — pas sur le prix. Présenter le ROI en € d'API quand le vrai gain est humain, c'est défendre le mauvais chiffre en entretien.
🎯 Patterns courants
- r=16-64 : 16 pour tâches "style", 32 pour classification fine, 64 pour génération complexe. Au-delà, gains marginaux, full FT envisageable.
- alpha = 2×r : règle empirique solide, garde le ratio.
- target_modules complet : viser
q,k,v,ominimum, ajoutergate,up,downpour les tâches générationnelles. - QLoRA quand VRAM serrée : load_in_4bit + paged_adamw_8bit pour FT Llama 70B sur 1× A100 80GB.
- Merge pour prod, adapter pour dev : merge = vLLM throughput max ; adapter = swap rapide multi-tenants.
- Multi-adapter serving (S-LoRA / vLLM 0.7+) : 1 base model + N adapters chargés à la volée selon
X-Adapterheader. Gold pour SaaS multi-clients. - Pack sequences :
sample_packing: truedans Axolotl, +30-40% throughput training. - Flash Attention 3 : obligatoire en 2026 si GPU Hopper/Ada.
🔄 Versions & écosystème 2026
- Hugging Face PEFT 0.13+ : LoRA, DoRA, X-LoRA, AdaLoRA, prompt tuning, prefix tuning.
- Unsloth : 2× plus rapide que HF baseline, support Llama, Mistral, Qwen, Gemma. Free tier OSS, version Pro pour multi-GPU.
- Axolotl 0.6+ : config YAML production, supporte distributed FSDP, gold standard prod.
- torchtune (Meta) : officiel pour FT Llama 3/4, recipes maintenues par Meta.
- DoRA (Weight-Decomposed LoRA) : variante 2024 qui split direction/magnitude, +1-3% qualité.
- LoRA+ : deux LR différents pour A et B, gain mineur mais gratuit.
- vLLM 0.7 : multi-LoRA serving production-ready,
--enable-lora --lora-modules. - S-LoRA / Punica : recherche → prod en 2026, serve 100+ adapters sur 1 GPU.
- Liger Kernel (LinkedIn) : Triton kernels qui économisent 40% mémoire training.
⚠️ Pitfalls
- rank trop bas (r=4-8) : underfitting silencieux, eval loss plafonne. Monter à r=32 si doute.
- alpha mal calé : si alpha << r, mise à jour quasi nulle. Si alpha >> r, instabilité.
- LR trop élevé : 5e-4 sur Mistral Small fait diverger en 100 steps. Start at 2e-4 max pour LoRA, 1e-4 pour QLoRA.
- Pas de gradient checkpointing : OOM garanti sur seq_len 4096 sans gradient_checkpointing=True.
- Tokenizer mismatch après merge : oublier de sauvegarder le tokenizer original avec le modèle mergé = inference cassée.
- Sample packing sans EOS : les exemples se chevauchent et le modèle apprend à ignorer les fins. Toujours vérifier que le tokenizer ajoute EOS.
- Eval pendant training trompeuse : eval_loss flat ne veut pas dire eval métier OK. Toujours faire un eval métier hors loop training.
- QLoRA + flash_attn 2 : interactions buggy historiquement, vérifier compatibilité versions (flash-attn 2.7+ OK).
- Adapter qui boost trop :
lora_alphatrop fort peut "écraser" les capacités du base model (catastrophic forgetting léger). Tester sur tâches générales hors domaine. - Merger sans QLoRA-aware : merger un adapter QLoRA naïvement dégrade le poids quantizé. Utiliser
merge_and_unload()après dequantize ou rester en mode adapter.
💰 Pricing / ROI client
Coûts cloud GPU 2026 (FR / EU) :
| GPU | Scaleway | OVH | Hyperscaler (AWS/GCP) |
|---|---|---|---|
| A100 80GB | 2.40€/h | 2.20€/h | 3.50-4.10€/h |
| H100 80GB | 3.20€/h | 3.50€/h | 5.50-6.50€/h |
| L40S | 1.60€/h | 1.80€/h | 2.20€/h |
Coût d'un training LoRA typique :
- Mistral 7B, r=32, 10K exemples, 3 epochs : 4h × A100 = 10€
- Mistral Small 24B, r=64, 10K exemples, 3 epochs : 6h × H100 = 20€
- Llama 70B QLoRA, r=16, 10K exemples, 2 epochs : 24h × H100 = 80€
Le coût FT est marginal. Le coût réel : ingénierie data (15-30 jours human) + eval (5-10j) + déploiement (5-10j).
Tarification mission LoRA (freelance FR 2026) :
- Audit + design : 6 000€
- Préparation data + training : 15-25k€
- Déploiement + eval prod : 8-15k€
- Total : 30-50k€ pour un projet sérieux
🧪 Testing / Eval
- Loss n'est pas la métrique métier : suivre l'eval métier sur le validation set toutes les N steps.
- Test de régression : 50 prompts "generic" (hors domaine FT) pour détecter catastrophic forgetting.
- Comparer N runs : sweep sur r ∈ {16, 32, 64}, alpha ∈ {r, 2r, 4r}, lr ∈ {1e-4, 2e-4, 5e-5}.
- W&B / TensorBoard : tracker loss, eval, GPU mem, throughput.
- Sanity check post-merge : 5 prompts simples avant/après merge, output doit être identique au mode adapter.
- Multi-adapter A/B en prod : router header pour comparer LoRA-v1 vs LoRA-v2 sur 10% du trafic.
🔁 Quand utiliser / éviter
LoRA quand :
- Tu veux FT mais GPU budget limité.
- Tu sers plusieurs variantes du même base model (multi-tenant SaaS).
- Tu veux swap rapide d'adapters.
- Tu cibles 1-5% des paramètres pour apprendre style/format.
Full FT quand :
- Tu as besoin d'apprendre des capacités fondamentales nouvelles (rare).
- Tu as un cluster multi-GPU (FSDP) et un budget compute large.
- Le LoRA plafonne sur ta métrique et tu as exclu data quality.
Évite LoRA quand :
- Ton dataset < 500 exemples (préfère few-shot).
- Tu veux enseigner des faits (use RAG).
- Tu n'as pas d'eval set construit.
🧩 Deep-dive — multi-adapter serving (S-LoRA / vLLM), la vraie raison économique du LoRA
Le mental model "LoRA = FT pas cher" rate l'argument décisif en prod : un seul base model en VRAM peut servir 100+ adapters. C'est la différence entre un SaaS multi-tenant viable et un cauchemar GPU.
Pourquoi c'est possible — le batching hétérogène
En full FT, chaque client = un modèle 14-140 GB en VRAM. 50 clients = 50 GPUs, impensable.
En LoRA, le base model (frozen) est partagé. Chaque adapter ne pèse que 2 × r × (d_in + d_out) × n_layers × n_modules paramètres — pour Mistral Small r=32, ~50-100 MB en bf16. Tu peux en garder des centaines en VRAM, ou les pager depuis la RAM CPU à la demande (S-LoRA fait du LRU sur un pool d'adapters).
Le coup de génie de S-LoRA, c'est le batched LoRA kernel : dans un même batch d'inference, la requête A (adapter client-1) et la requête B (adapter client-2) passent par le même forward du base model, puis chacune applique son B·A via un kernel groupé (gather des bons adapters par requête). Tu ne sérialises pas par client — tu batches à travers les clients.
Sans S-LoRA (naïf) Avec S-LoRA
───────────────── ───────────
load adapter client-1 base model chargé 1×
→ batch client-1 adapters en pool (VRAM + CPU paging)
swap adapter client-2 batch MIXTE [c1, c2, c3, c1, ...]
→ batch client-2 → 1 forward base
swap adapter client-3 → kernel LoRA groupé par requête
→ ... throughput ≈ base model seul
throughput ÷ N (swaps tuent) +5-10% overhead seulementLe chiffre à défendre
Sur 1× H100, vLLM (--enable-lora --max-loras 32 --max-lora-rank 64) sert un base 7-24B + dizaines d'adapters avec un throughput proche du base model nu (overhead ~5-10% sur le kernel LoRA + la mémoire des B·A). Le routing se fait par requête : header X-Adapter: client-42 → vLLM pioche le bon adapter dans le pool. Coût marginal d'un nouveau client = entraîner un adapter (10-20€) + 50-100 MB de pool. C'est ça, l'unit economics qui rend le LoRA structurellement supérieur au full FT pour le multi-tenant.
Les limites que tu dois citer (sinon tu passes pour naïf)
max_lorasborne le nombre d'adapters actifs simultanément dans un batch (pas le total servable). Au-delà, paging CPU↔GPU = latence sur les adapters froids (premier hit d'un client inactif).- Tous les adapters doivent partager le même base model + le même
target_modules. Un client avecgate/up/downet un autre sans → deux pools distincts. - Le merge tue le multi-adapter.
merge_and_unload()= throughput max mais 1 modèle = 1 client. Merge pour le mono-tenant haut débit, adapter-mode pour le multi-tenant. C'est un arbre de décision, pas une préférence. - Rank mixte coûteux.
--max-lora-rankest dimensionné sur le plus gros r ; un pool mélangeant r=8 et r=64 paie la mémoire du r=64 pour tout le monde.
🔭 Observabilité & coûts en prod (ce qui manque toujours dans les tutos)
Un FT qui marche en notebook ≠ un FT en prod. Ce que tu instrumentes :
| Quoi | Outil | Le piège |
|---|---|---|
| Training : loss, eval_loss, grad_norm, LR, GPU mem, tok/s | W&B / TensorBoard | grad_norm qui explose = LR trop haut bien avant que la loss diverge ; c'est le signal d'alerte précoce |
| Eval métier (pas la loss) | script hors-loop, sur le test set gelé | eval_loss flat ne dit RIEN sur l'accuracy métier — voir Pitfall #7 |
| Inference : latence p50/p95/p99, tok/s, queue depth, KV-cache util | vLLM /metrics (Prometheus) | p99 explose quand un adapter froid est pagé depuis le CPU — surveiller par adapter |
| Coût : €/1000 req, GPU-h, util GPU | dashboards cloud + compteur req | un GPU à 30% d'util = tu paies 3× trop ; batche ou descends de gamme (H100→L40S) |
| Drift : distribution des inputs vs train set | logging + KS-test périodique | le FT vieillit ; un nouveau PCG, un nouveau format de CV → ré-entraîner l'adapter |
Comparaison API vs self-host pour la décision de build : côté Claude, l'observabilité coût est triviale — tu loggues resp.usage (input_tokens, output_tokens, cache_read_input_tokens) et tu multiplies par les prix (claude-sonnet-4-6 : 3 USD / 15 USD le M tok ; claude-haiku-4-5 : 1 USD / 5 USD ; claude-opus-4-8 : 5 USD / 25 USD). Côté self-host LoRA, le coût est un GPU-h amorti sur le throughput réel — beaucoup plus dur à attribuer par requête, et seulement rentable au-dessus d'un seuil de volume. Le seuil de bascule API→self-host, c'est exactement le chiffre qu'on te demandera de défendre (voir Exercices).
🏋️ Exercices
Progression : implémenter → mesurer → casser → durcir → défendre le chiffre. Fais-les dans l'ordre, chacun s'appuie sur le précédent.
Exercice 1 — LoRA baseline + sweep de rank (implémenter)
Objectif : entraîner 3 adapters (r ∈ {8, 32, 64}, alpha = 2r) sur une tâche d'extraction JSON et tracer eval_loss + accuracy métier vs rank. Indice/Solution : pars du train_unsloth.py du fichier. Construis un eval set de 200 exemples gelés. Mesure l'accuracy métier (champs corrects), pas seulement l'eval_loss. Attendu : r=8 plafonne (underfitting), r=32 et r=64 quasi-identiques → conclusion senior = "r=32 est le sweet spot, r=64 gaspille de la VRAM de pool sans gain". Trace la courbe pour le prouver, ne l'affirme pas.
Exercice 2 — Merge vs adapter, prouver l'équivalence (mesurer)
Objectif : démontrer empiriquement que merge_and_unload() produit des outputs identiques au mode adapter, puis mesurer le delta de throughput vLLM entre les deux modes. Indice/Solution : 50 prompts, génère en mode adapter puis en mode mergé (greedy, temperature=0 côté décodage HF), compare token-à-token. Piège QLoRA : si l'adapter a été entraîné en 4-bit, merger naïvement dégrade les poids quantizés (Pitfall #10) — dé-quantise d'abord ou reste en adapter. Mesure ensuite req/s sur vLLM merged vs --enable-lora. Conclusion attendue : merge ~5-10% plus rapide, mais perd le multi-tenant. Quantifie l'arbitrage.
Exercice 3 — Servir 10 adapters sur 1 GPU et casser le serveur (casser)
Objectif : monter vLLM avec --enable-lora --max-loras 4 et 10 adapters, puis générer un pattern de charge qui fait exploser la p99. Indice/Solution : --max-loras 4 mais 10 adapters → forcer un round-robin sur les 10 clients fait pager les adapters froids depuis le CPU à chaque requête. Mesure la p99 par adapter : les adapters hors du pool actif prennent un hit de latence sur leur premier appel. Fix : monter --max-loras, ou router pour grouper les requêtes d'un même client (affinité), ou pré-charger les adapters chauds. C'est la différence entre connaître la flag et comprendre le paging.
Exercice 4 — Catastrophic forgetting : le détecter et le réparer (durcir)
Objectif : entraîner un LoRA agressif (alpha élevé, 5 epochs) qui dégrade les capacités générales du base model, le prouver, puis le corriger. Indice/Solution : construis un set de régression de 50 prompts hors domaine (raisonnement général, autre langue). Entraîne avec alpha=4r sur 5 epochs → l'adapter "écrase" le base (Pitfall #9). Montre la chute sur le set de régression. Fixes à tester et comparer : baisser alpha vers 2r, réduire à 2-3 epochs, mélanger 10-20% de données générales (replay) dans le train set. Documente lequel récupère le mieux sans perdre l'accuracy in-domain — c'est un vrai arbitrage, pas une réponse unique.
Exercice 5 — Défendre le seuil de bascule API → self-host (défendre le chiffre)
Objectif : produire le calcul qui justifie (ou démolit) la décision de self-host un LoRA plutôt que d'appeler claude-sonnet-4-6 (ou claude-haiku-4-5), pour un volume donné. Indice/Solution : modélise les deux courbes. API : volume × (tok_in × prix_in + tok_out × prix_out), avec prompt caching → tok_in cachés à ~0.1× sur les hits (claude-sonnet-4-6 3/15, claude-haiku-4-5 1/5 le M tok). Self-host : coût_FT_amorti + (GPU-h × prix_GPU) / req_par_h_au_throughput_réel, + le coût caché ingénierie data/eval/maintenance (15-50j homme). Trouve le volume de bascule. Le piège senior : à bas volume et sur une tâche que Haiku gère, l'API gagne toujours (zéro coût fixe, structured outputs gratuits, pas de MLOps) — le self-host ne se justifie qu'au-dessus du seuil de volume ET quand la qualité/latence/souveraineté l'exigent. Défendre "self-host moins cher" sans inclure le coût humain et le seuil de volume = red flag en entretien.
Exercice 6 (bonus, expert) — DoRA vs LoRA, mesurer le gain réel
Objectif : entraîner le même setup en LoRA puis en DoRA (use_dora=True dans LoraConfig) et vérifier si le +1-3% annoncé tient sur ta tâche. Indice/Solution : DoRA décompose poids = direction × magnitude, coûte ~10-20% de training en plus. Sur des tâches "format/style" le gain est souvent dans le bruit ; sur des tâches de raisonnement il peut être réel. Mesure avec le même eval set + un test de significativité (plusieurs seeds). Conclusion attendue : "le gain DoRA ne justifie pas le surcoût sur cette tâche" est une réponse parfaitement valable et senior — encore faut-il l'avoir mesuré.
🎤 En entretien
Q : Pourquoi LoRA n'entraîne-t-il que ~0.1-1% des paramètres tout en gardant une qualité proche du full FT ? R : L'hypothèse de rang faible — l'update ΔW nécessaire pour adapter une tâche vit dans un sous-espace de rang faible, donc on l'approxime par B·A (r << d) au lieu de la matrice complète ; pour la plupart des tâches d'adaptation (style, format, domaine) ça suffit, le full FT n'aide que pour des capacités fondamentalement nouvelles.
Q : Quelle est la vraie raison économique de choisir LoRA plutôt que full FT en SaaS multi-tenant ? R : Le multi-adapter serving (S-LoRA / vLLM) — un seul base model frozen en VRAM sert 100+ adapters via un kernel LoRA batché hétérogène, donc le coût marginal d'un client = entraîner un adapter (~50-100 MB) ; en full FT chaque client = un modèle complet en VRAM, ça ne scale pas.
Q : eval_loss est plate pendant le training, le FT est-il réussi ? R : Non — l'eval_loss ne mesure pas la métrique métier. Un modèle peut avoir une loss flat et rater l'accuracy cible (ou avoir une loss qui baisse mais overfit le format) ; il faut un eval métier hors-loop sur un test set gelé, plus un set de régression hors-domaine pour détecter le catastrophic forgetting.
Q : Quand NE faut-il PAS fine-tuner, et que faire à la place ? R : Dataset < 500 exemples → few-shot ; besoin d'injecter des faits qui changent → RAG (le FT mémorise mal et vieillit) ; verrouiller un format/style à faible volume → prompt + structured outputs côté API (output_config.format chez Claude garantit le JSON par construction, prompt caching ramène le coût input à ~0.1×). On FT quand on a du volume, un eval set, et une tâche que le prompting ne verrouille pas.
🔗 Liens
- Paper LoRA original (Hu et al. 2021) — fondateur
- Paper QLoRA (Dettmers et al. 2023) — base mathématique 4-bit
- Unsloth GitHub + blog (recipes par modèle)
- Axolotl docs + repo (configs YAML production)
- HF PEFT docs — référence API
- Predibase blog : "LoRA Land" — 25 LoRA models qui battent GPT-4 sur leur tâche
- Fichier voisin :
01-when-to-fine-tune.md(décider AVANT de coder) - Fichier voisin :
03-distillation.md(alternative compress) - Fichier voisin :
04-vllm-tgi-llamacpp.md(servir les adapters)