Call-Informatique
Call-Informatique
Le média tech
Fine-tuning LoRA/QLoRA : configuration avancée et optimisation
Intelligence ArtificielleGuides11 min de lecture

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-smi fonctionnel)
  • Familiarité avec PyTorch et les Transformers Hugging Face

1. Environnement et dépendances

bash
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

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

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

Estimation 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).

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

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

bash
## 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=True

NaN dans la loss

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

Overfitting (loss train baisse, eval stagne ou monte)

bash
## 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égularisation

Erreur "No module named bitsandbytes"

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

bash
## —- 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.py

Pour charger dans Ollama après conversion :

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

bash
#!/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

—-

Références : [PEFT docs](https://huggingface.co/docs/peft), [TRL docs](https://huggingface.co/docs/trl), [QLoRA paper](https://arxiv.org/abs/2305.14314), [Unsloth](https://unsloth.ai/docs)
Sur le même sujet

À lire aussi

#tutoriel#guide-technique#lora#qlora#fine-tuning