DEV-AI

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 ?

TL;DR

· ~40 min de lecture · peft 0.14 · transformers 4.45 · Python 3.10+
65B sur 1×48 Go
QLoRA NeurIPS 2023
~12-16 Go
VRAM Llama 8B (selon config)
~780×
Moins de params (r=16)
4-bit
NF4 quantization

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 :

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

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 :

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 :

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

Quelle est la différence entre LoRA et QLoRA ?

LoRA entraîne de petites matrices A, B à côté des poids gelés (~1 % des paramètres entraînables). QLoRA ajoute une quantization 4-bit NF4 du modèle base, ce qui réduit fortement l'empreinte mémoire du modèle chargé et rend le fine-tuning possible sur des GPU bien plus modestes. La VRAM réelle dépend ensuite du batch size, de la longueur de séquence, du gradient checkpointing et des couches ciblées. QLoRA conserve en pratique des performances proches de la pleine précision sur les benchmarks évalués dans le papier original (Dettmers et al., NeurIPS 2023).

Quel rank LoRA choisir ?

Empiriquement, r=16 est le sweet spot. r=8 suffit pour style transfer, r=32-64 aide sur raisonnement complexe. Au-delà de r=64, rendement décroissant. Hu et al. (ICLR 2022) montrent qu'un rank très bas (r=1, r=4) peut suffire pour certaines tâches GLUE.

Quels target_modules cibler ?

Trois stratégies : attention uniquement (q/k/v/o_proj), all-linear incluant MLP (gate/up/down_proj), ou attention + output. All-linear donne +2-5 % sur benchmarks mais double le coût mémoire. Recommandation QLoRA paper : all-linear avec rank faible.

QLoRA dégrade-t-il vraiment la qualité ?

Non, ou très peu. Le papier QLoRA (Dettmers et al., NeurIPS 2023) démontre empiriquement que 4-bit NF4 atteint des performances proches de la pleine précision sur les benchmarks évalués. La 4-bit NormalFloat est optimalement distribuée pour les poids de LLMs (loi normale), minimisant l'erreur de quantization. À noter : le fameux "99,3 %" souvent cité concerne Guanaco (QLoRA sur Llama 65B) vs ChatGPT sur le benchmark Vicuna, pas un ratio général QLoRA vs FP16.

Comment éviter le catastrophic forgetting ?

Quatre techniques : (1) limiter à 1-3 epochs max, (2) LR bas (1e-4 à 3e-4), (3) mélanger 10-20 % de données généralistes (replay buffer), (4) évaluer régulièrement sur MMLU/ARC. LoRA limite naturellement le forgetting car les poids base restent gelés.

Quelle VRAM pour Llama 8B / 70B avec QLoRA ?

Llama 3.1 8B + QLoRA r=16 : ~12-16 Go VRAM (RTX 4090). Llama 3.1 70B + QLoRA r=16 : ~48 Go VRAM (1× A100 80 Go ou H100). Gradient checkpointing réduit de 30-40 % au prix de +20 % compute. Unsloth optimise davantage (jusqu'à 50 % gain VRAM).

Articles liés

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 →