Fine-tuning LoRA/QLoRA : configuration avancée et optimisation
Guide technique complet pour fine-tuner un LLM avec LoRA : quantification QLoRA, hyperparamètres, multi-GPU, troubleshooting et déploiement GGUF.
Fine-tuning LoRA/QLoRA : configuration avancée et optimisation
Ce guide part du principe que vous connaissez déjà les bases du machine learning et que vous avez un environnement GPU fonctionnel. On va couvrir le fine-tuning LoRA en profondeur : choix des hyperparamètres, gestion mémoire, debugging, et déploiement du modèle final.
Prérequis
- GPU NVIDIA avec CUDA 12.1+ (minimum 8 Go VRAM, recommandé 16+ Go)
- Python 3.10-3.12
- Drivers NVIDIA récents (
nvidia-smifonctionnel) - Familiarité avec PyTorch et les Transformers Hugging Face
1. Environnement et dépendances
mkdir ~/lora-advanced && cd ~/lora-advanced
python3 -m venv venv && source venv/bin/activate
pip install —upgrade pip setuptools wheel
## PyTorch avec CUDA 12.1
pip install torch torchvision torchaudio —index-url https://download.pytorch.org/whl/cu121
## Stack Hugging Face
pip install transformers peft trl datasets accelerate
## Quantification
pip install bitsandbytes
## Monitoring (optionnel mais recommandé)
pip install wandb tensorboard
## Vérification rapide
python3 -c "
import torch, transformers, peft, trl
print(f'PyTorch: {torch.__version__}')
print(f'CUDA: {torch.version.cuda}')
print(f'Transformers: {transformers.__version__}')
print(f'PEFT: {peft.__version__}')
print(f'TRL: {trl.__version__}')
print(f'GPU: {torch.cuda.get_device_name(0)}')
print(f'VRAM: {torch.cuda.get_device_properties(0).total_mem / 1e9:.1f} Go')
"2. LoRA : théorie rapide et paramètres clés
LoRA décompose les mises à jour de poids en deux matrices de rang faible : W = W₀ + BA, où B ∈ ℝᵈˣʳ et A ∈ ℝʳˣᵈ. Au lieu de mettre à jour d×d paramètres, on en met à jour 2×d×r.
Paramètres critiques :
| Paramètre | Rôle | Valeurs typiques | Impact VRAM |
|—————-|———|————————-|——————-|
| r (rang) | Capacité d'adaptation | 8, 16, 32, 64 | Linéaire |
| lora_alpha | Facteur d'échelle (alpha/r = scaling effectif) | 2×r habituel | Aucun |
| lora_dropout | Régularisation | 0.0 à 0.1 | Aucun |
| target_modules | Couches modifiées | Voir table ci-dessous | Linéaire |
Target modules par architecture
## Llama / Llama 2 / Llama 3
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
## Mistral / Mixtral
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
## Qwen 2 / Qwen 2.5
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]
## Phi-3 / Phi-4
target_modules = ["qkv_proj", "o_proj", "gate_up_proj", "down_proj"]
## Gemma 2
target_modules = ["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"]Règle empirique : cibler toutes les couches linéaires de l'attention ET du MLP donne de meilleurs résultats que l'attention seule. Le coût en VRAM reste raisonnable.
3. Configuration QLoRA complète
QLoRA combine quantification NF4 + double quantification + LoRA. C'est le meilleur rapport qualité/mémoire pour des GPU consumer.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, TaskType, prepare_model_for_kbit_training
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
## —- Quantification 4 bits NF4 —-
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # Normal Float 4 (meilleur que FP4)
bnb_4bit_use_double_quant=True, # Double quantification : ~0.4 Go économisés sur 7B
bnb_4bit_compute_dtype=torch.bfloat16, # Calculs en bf16 (plus stable que fp16)
)
model_id = "meta-llama/Llama-3.1-8B-Instruct" # ou tout modèle compatible
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=quant_config,
device_map="auto",
attn_implementation="sdpa", # Scaled Dot Product Attention (PyTorch 2.x natif)
)
## Préparer le modèle pour l'entraînement en précision réduite
model = prepare_model_for_kbit_training(model)
## —- Configuration LoRA —-
lora_config = LoraConfig(
r=32, # Rang plus élevé pour un 8B
lora_alpha=64, # alpha = 2*r, scaling effectif = 2.0
lora_dropout=0.05,
target_modules=[ # Toutes les couches linéaires
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
task_type=TaskType.CAUSAL_LM,
bias="none", # Pas de biais LoRA (standard)
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
## Sortie attendue : ~1-2% des paramètres totauxEstimation VRAM par taille de modèle (QLoRA r=32)
| Modèle | VRAM base | VRAM entraînement (batch=2) |
|————|—————-|——————————————-|
| 3B | ~2.5 Go | ~6 Go |
| 7-8B | ~5 Go | ~12 Go |
| 13B | ~8 Go | ~18 Go |
| 70B | ~38 Go | ~48 Go (multi-GPU requis) |
4. Préparation du dataset
Le format le plus fiable pour le SFT est le format conversationnel (messages).
## Méthode 1 : Dataset Hugging Face existant
dataset = load_dataset("mlabonne/guanaco-llama2-1k", split="train")
## Méthode 2 : Dataset personnalisé depuis un JSON
## Format attendu dans data.json :
## [
## {"messages": [
## {"role": "system", "content": "Tu es un assistant spécialisé en cybersécurité."},
## {"role": "user", "content": "Comment fonctionne une attaque MITM ?"},
## {"role": "assistant", "content": "Une attaque Man-in-the-Middle..."}
## ]},
## ...
## ]
dataset = load_dataset("json", data_files="data.json", split="train")
## Méthode 3 : Génération automatique via un LLM plus gros
## Utiliser Claude, GPT-4 ou Llama 405B pour générer des paires Q/R
## dans votre domaine cible, puis filtrer manuellement.
## Mélanger et diviser
dataset = dataset.shuffle(seed=42)
if len(dataset) > 100:
split = dataset.train_test_split(test_size=0.1, seed=42)
train_dataset = split["train"]
eval_dataset = split["test"]
else:
train_dataset = dataset
eval_dataset = None
print(f"Train: {len(train_dataset)} exemples")
if eval_dataset:
print(f"Eval: {len(eval_dataset)} exemples")5. Configuration d'entraînement optimisée
training_args = SFTConfig(
output_dir="./checkpoints",
# —- Epochs et batch —-
num_train_epochs=3, # 2-5 selon la taille du dataset
per_device_train_batch_size=2, # Ajuster selon VRAM
gradient_accumulation_steps=8, # Batch effectif = 2*8 = 16
# —- Learning rate —-
learning_rate=2e-4, # Standard pour LoRA
lr_scheduler_type="cosine", # Cosine decay > linear
warmup_ratio=0.03, # 3% warmup
weight_decay=0.01,
# —- Précision —-
bf16=True, # BFloat16 (Ampere+ recommandé)
tf32=True, # TF32 pour les matmuls (gratuit en perf)
# —- Séquence —-
max_seq_length=2048, # Adapter à vos données
packing=True, # Empiler les exemples courts (gros gain de vitesse)
# —- Checkpoints —-
save_strategy="steps",
save_steps=100,
save_total_limit=3,
# —- Évaluation —-
eval_strategy="steps" if eval_dataset else "no",
eval_steps=100,
# —- Logging —-
logging_steps=10,
report_to="tensorboard", # ou "wandb" si configuré
# —- Stabilité —-
gradient_checkpointing=True, # Économise ~30% VRAM
gradient_checkpointing_kwargs={"use_reentrant": False},
max_grad_norm=0.3, # Gradient clipping
# —- Performance —-
optim="paged_adamw_8bit", # Optimiseur 8 bits (économie VRAM)
dataloader_num_workers=2,
dataloader_pin_memory=True,
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
)
## Lancer l'entraînement
result = trainer.train()
## Sauvegarder
trainer.save_model("./final-adapter")
print(f"Train loss final: {result.training_loss:.4f}")6. Troubleshooting
CUDA Out of Memory (OOM)
## Symptôme : RuntimeError: CUDA out of memory
## Solutions par ordre de priorité :
## 1. Réduire le batch size
per_device_train_batch_size=1
gradient_accumulation_steps=16 # compenser pour garder le même batch effectif
## 2. Réduire max_seq_length
max_seq_length=1024 # ou 512
## 3. Activer gradient checkpointing (si pas déjà fait)
gradient_checkpointing=True
## 4. Réduire le rang LoRA
r=8 # au lieu de 32
## 5. Cibler moins de modules
target_modules=["q_proj", "v_proj"] # attention seule
## 6. Passer en 4 bits si vous étiez en 8 bits
load_in_4bit=TrueNaN dans la loss
## Symptôme : loss = nan après quelques steps
## Causes fréquentes :
## 1. Learning rate trop élevé
learning_rate=1e-4 # réduire de moitié
## 2. Problème fp16 sur GPU ancienne (Turing)
bf16=False
fp16=True # utiliser fp16 à la place sur les RTX 20xx
## 3. Données corrompues (tokens spéciaux mal gérés)
## Vérifier avec :
python3 -c "
from datasets import load_from_disk
ds = load_from_disk('./data_prepared')
for i, ex in enumerate(ds):
if not ex.get('text') and not ex.get('messages'):
print(f'Exemple vide à l\'index {i}')
"
## 4. Gradient clipping trop agressif ou absent
max_grad_norm=1.0 # valeur par défaut PyTorchOverfitting (loss train baisse, eval stagne ou monte)
## Symptôme : train loss < 0.5 mais eval loss remonte
## Solutions :
## 1. Réduire le nombre d'epochs
num_train_epochs=1 # souvent suffisant avec > 5000 exemples
## 2. Augmenter le dropout
lora_dropout=0.1
## 3. Réduire le rang
r=8 # moins de capacité = moins d'overfitting
## 4. Augmenter le dataset (le vrai fix)
## Plus de données > meilleure régularisationErreur "No module named bitsandbytes"
## Sur certaines distributions, bitsandbytes nécessite une compilation
pip uninstall bitsandbytes -y
pip install bitsandbytes —no-cache-dir
## Vérification
python3 -c "import bitsandbytes; print(bitsandbytes.__version__)"7. Fusion et export GGUF
Pour déployer le modèle fine-tuné dans Ollama, llama.cpp ou d'autres runtimes d'inférence :
## —- Script de fusion + conversion GGUF —-
cat > export_gguf.py << 'EXPORTEOF'
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import subprocess
import os
model_id = "meta-llama/Llama-3.1-8B-Instruct"
adapter_path = "./final-adapter"
merged_path = "./merged-model"
print("[1/4] Chargement du modèle de base en bf16...")
tokenizer = AutoTokenizer.from_pretrained(model_id)
base_model = AutoModelForCausalLM.from_pretrained(
model_id,
torch_dtype=torch.bfloat16,
device_map="cpu",
low_cpu_mem_usage=True,
)
print("[2/4] Application de l'adaptateur LoRA...")
model = PeftModel.from_pretrained(base_model, adapter_path)
print("[3/4] Fusion des poids (merge_and_unload)...")
merged = model.merge_and_unload()
merged.save_pretrained(merged_path, safe_serialization=True)
tokenizer.save_pretrained(merged_path)
print(f"Modèle fusionné sauvegardé : {merged_path}")
print("[4/4] Conversion GGUF...")
## Nécessite llama.cpp cloné localement
llama_cpp = os.path.expanduser("~/llama.cpp")
if os.path.exists(f"{llama_cpp}/convert_hf_to_gguf.py"):
subprocess.run([
"python3", f"{llama_cpp}/convert_hf_to_gguf.py",
merged_path,
"—outfile", "./model-q8_0.gguf",
"—outtype", "q8_0"
], check=True)
print("GGUF Q8_0 exporté : ./model-q8_0.gguf")
else:
print(f"llama.cpp non trouvé dans {llama_cpp}")
print("Clonez-le : git clone https://github.com/ggerganov/llama.cpp ~/llama.cpp")
print("Puis relancez ce script.")
EXPORTEOF
python3 export_gguf.pyPour charger dans Ollama après conversion :
## Créer un Modelfile
cat > Modelfile << 'MFEOF'
FROM ./model-q8_0.gguf
SYSTEM "Tu es un assistant spécialisé."
PARAMETER temperature 0.7
PARAMETER top_p 0.9
MFEOF
## Importer dans Ollama
ollama create mon-modele -f Modelfile
## Tester
ollama run mon-modele "Bonjour, présente-toi."8. Pipeline automatisé
Script bash complet pour lancer le pipeline de bout en bout :
#!/usr/bin/env bash
set -euo pipefail
## —- Configuration —-
MODEL_ID="Qwen/Qwen2.5-3B-Instruct"
DATASET="mlabonne/guanaco-llama2-1k"
OUTPUT_DIR="./run-$(date +%Y%m%d-%H%M%S)"
RANK=16
EPOCHS=3
BATCH=2
GRAD_ACCUM=8
LR="2e-4"
MAX_SEQ=1024
mkdir -p "$OUTPUT_DIR"
echo "=== Fine-tuning LoRA ==="
echo "Modèle : $MODEL_ID"
echo "Dataset : $DATASET"
echo "Output : $OUTPUT_DIR"
echo "Rang LoRA : $RANK"
echo ""
## Vérification GPU
nvidia-smi —query-gpu=name,memory.total,driver_version —format=csv,noheader
echo ""
## Lancer l'entraînement via TRL CLI
python3 -m trl sft \
—model_name_or_path "$MODEL_ID" \
—dataset_name "$DATASET" \
—output_dir "$OUTPUT_DIR" \
—num_train_epochs "$EPOCHS" \
—per_device_train_batch_size "$BATCH" \
—gradient_accumulation_steps "$GRAD_ACCUM" \
—learning_rate "$LR" \
—max_seq_length "$MAX_SEQ" \
—bf16 \
—gradient_checkpointing \
—logging_steps 10 \
—save_steps 100 \
—packing \
—use_peft \
—lora_r "$RANK" \
—lora_alpha "$((RANK * 2))" \
—lora_dropout 0.05 \
—load_in_4bit \
—report_to tensorboard
echo ""
echo "=== Entraînement terminé ==="
echo "Adaptateur sauvegardé dans : $OUTPUT_DIR"
echo "Vérifiez les logs : tensorboard —logdir $OUTPUT_DIR/runs"9. Bonnes pratiques
Données : la qualité prime sur la quantité. 500 exemples propres battent 10 000 exemples bruyants. Filtrez, déduplicez, et vérifiez manuellement un échantillon.
Évaluation : ne vous fiez pas uniquement à la loss. Testez le modèle sur des prompts réels, variés, et hors-distribution. Une loss basse ne garantit pas un bon modèle.
Reproductibilité : fixez les seeds (seed=42 dans SFTConfig), logguez les hyperparamètres, versionnez vos datasets.
Sécurité : vérifiez l'intégrité des modèles téléchargés depuis Hugging Face. Utilisez trust_remote_code=False sauf nécessité. Les fichiers pickle dans certains vieux modèles peuvent contenir du code arbitraire ; préférez les modèles au format safetensors.
Alternative Unsloth : pour les entraînements en production, Unsloth offre des kernels optimisés qui réduisent la VRAM et accélèrent l'entraînement. Compatible avec les mêmes datasets et configurations. Site : https://unsloth.ai/docs
—-
À lire aussi
Fine-tuner un modèle IA avec LoRA : le guide pas à pas
Apprenez à personnaliser un modèle d'IA en le fine-tunant avec LoRA, même avec une carte graphique modeste. Guide complet pour débutants.
Bot Discord + Claude API : architecture complète et production
Architecture robuste d'un bot Discord alimenté par Claude. Gestion d'erreurs, rate limiting, Docker, déploiement VPS et bonnes pratiques de production.
Créer un chatbot Discord avec Claude : le guide pas à pas
Apprenez à créer votre propre chatbot Discord propulsé par l'IA Claude d'Anthropic. Aucune expérience en programmation requise, on part de zéro.