Skip to content

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)

python
# 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)

python
# 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_control ramè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

jsonl
// 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

yaml
# 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

bash
# 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 ?

python
# 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

bash
# 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"]
yaml
# 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,o minimum, ajouter gate,up,down pour 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-Adapter header. Gold pour SaaS multi-clients.
  • Pack sequences : sample_packing: true dans 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

  1. rank trop bas (r=4-8) : underfitting silencieux, eval loss plafonne. Monter à r=32 si doute.
  2. alpha mal calé : si alpha << r, mise à jour quasi nulle. Si alpha >> r, instabilité.
  3. LR trop élevé : 5e-4 sur Mistral Small fait diverger en 100 steps. Start at 2e-4 max pour LoRA, 1e-4 pour QLoRA.
  4. Pas de gradient checkpointing : OOM garanti sur seq_len 4096 sans gradient_checkpointing=True.
  5. Tokenizer mismatch après merge : oublier de sauvegarder le tokenizer original avec le modèle mergé = inference cassée.
  6. Sample packing sans EOS : les exemples se chevauchent et le modèle apprend à ignorer les fins. Toujours vérifier que le tokenizer ajoute EOS.
  7. Eval pendant training trompeuse : eval_loss flat ne veut pas dire eval métier OK. Toujours faire un eval métier hors loop training.
  8. QLoRA + flash_attn 2 : interactions buggy historiquement, vérifier compatibilité versions (flash-attn 2.7+ OK).
  9. Adapter qui boost trop : lora_alpha trop fort peut "écraser" les capacités du base model (catastrophic forgetting léger). Tester sur tâches générales hors domaine.
  10. 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) :

GPUScalewayOVHHyperscaler (AWS/GCP)
A100 80GB2.40€/h2.20€/h3.50-4.10€/h
H100 80GB3.20€/h3.50€/h5.50-6.50€/h
L40S1.60€/h1.80€/h2.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 seulement

Le 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_loras borne 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 avec gate/up/down et 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-rank est 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 :

QuoiOutilLe piège
Training : loss, eval_loss, grad_norm, LR, GPU mem, tok/sW&B / TensorBoardgrad_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 utilvLLM /metrics (Prometheus)p99 explose quand un adapter froid est pagé depuis le CPU — surveiller par adapter
Coût : €/1000 req, GPU-h, util GPUdashboards cloud + compteur requn GPU à 30% d'util = tu paies 3× trop ; batche ou descends de gamme (H100→L40S)
Drift : distribution des inputs vs train setlogging + KS-test périodiquele 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)

Bibliothèque tech perso — Achref