Aller au contenu principal

Chapitre 3 : Implémentation de LoRA avec PEFT

Formation pratique LoRA​

Ce chapitre fournit un code complet, prêt pour la production, pour implémenter le fine-tuning LoRA en utilisant la bibliothèque Hugging Face PEFT.


Script de formation complet​

import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
)
from peft import (
LoraConfig,
get_peft_model,
TaskType,
prepare_model_for_kbit_training,
)
from trl import SFTTrainer
from datasets import load_dataset

# ============================================
# Configuration
# ============================================

MODEL_NAME = "meta-llama/Llama-3.1-8B"
OUTPUT_DIR = "./lora-output"
DATASET = "databricks/dolly-15k"

# Hyperparamètres LoRA
LORA_R = 16
LORA_ALPHA = 32
LORA_DROPOUT = 0.05
TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj"]

# Hyperparamètres d'entraînement
LEARNING_RATE = 2e-4
BATCH_SIZE = 4
GRADIENT_ACCUMULATION = 4
MAX_SEQ_LENGTH = 512
NUM_EPOCHS = 3

# ============================================
# Charger le modèle et le tokenizer
# ============================================

print("Chargement du modèle et du tokenizer...")

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
torch_dtype=torch.bfloat16,
device_map="auto",
)

# ============================================
# Configurer LoRA
# ============================================

print("Configuration de LoRA...")

lora_config = LoraConfig(
r=LORA_R,
lora_alpha=LORA_ALPHA,
lora_dropout=LORA_DROPOUT,
target_modules=TARGET_MODULES,
bias="none",
task_type=TaskType.CAUSAL_LM,
)

# Appliquer LoRA
model = get_peft_model(model, lora_config)

# Imprimer les paramètres entraînables
model.print_trainable_parameters()

# ============================================
# Préparer le jeu de données
# ============================================

print("Chargement du jeu de données...")

dataset = load_dataset(DATASET, split="train")

def format_instruction(example):
"""Formater les données pour l'ajustement des instructions."""
if example.get("context"):
text = f"""### Instruction:
{example['instruction']}

### Contexte:
{example['context']}

### Réponse:
{example['response']}"""
else:
text = f"""### Instruction:
{example['instruction']}

### Réponse:
{example['response']}"""
return {"text": text}

dataset = dataset.map(format_instruction)

# ============================================
# Arguments d'entraînement
# ============================================

training_args = TrainingArguments(
output_dir=OUTPUT_DIR,
num_train_epochs=NUM_EPOCHS,
per_device_train_batch_size=BATCH_SIZE,
gradient_accumulation_steps=GRADIENT_ACCUMULATION,
learning_rate=LEARNING_RATE,
weight_decay=0.01,
warmup_ratio=0.03,
lr_scheduler_type="cosine",
logging_steps=10,
save_strategy="epoch",
bf16=True,
gradient_checkpointing=True,
optim="adamw_torch_fused",
report_to="none", # ou "wandb"
seed=42,
)

# ============================================
# Créer un formateur
# ============================================

trainer = SFTTrainer(
model=model,
train_dataset=dataset,
args=training_args,
dataset_text_field="text",
max_seq_length=MAX_SEQ_LENGTH,
tokenizer=tokenizer,
packing=False,
)

# ============================================
# Entraîner
# ============================================

print("Début de l'entraînement...")
trainer.train()

# ============================================
# Sauvegarder
# ============================================

print("Sauvegarde du modèle...")
model.save_pretrained(OUTPUT_DIR)
tokenizer.save_pretrained(OUTPUT_DIR)

print(f"Entraînement terminé ! Modèle sauvegardé dans {OUTPUT_DIR}")

Paramètres de LoraConfig expliqués​

Paramètres principaux​

LoraConfig(
# Rang - capacité d'adaptation
r=16, # Typique : 8, 16, 32, 64

# Facteur de mise à l'échelle
lora_alpha=32, # Typique : 1x à 2x de r

# Régularisation
lora_dropout=0.05, # Typique : 0.0 à 0.1

# Quelles couches adapter
target_modules=["q_proj", "v_proj"],

# Entraînement du biais
bias="none", # "none", "all", ou "lora_only"

# Type de tâche
task_type=TaskType.CAUSAL_LM,
)

Modules cibles par modèle​

# Llama / Mistral
TARGET_MODULES_LLAMA = [
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
]

# GPT-2 / GPT-J
TARGET_MODULES_GPT = [
"c_attn", "c_proj", "c_fc",
]

# BERT / RoBERTa
TARGET_MODULES_BERT = [
"query", "key", "value",
"dense",
]

Chargement et utilisation d'un adaptateur entraîné​

Chargement de base​

from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

# Charger le modèle de base
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B",
torch_dtype=torch.bfloat16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B")

# Charger l'adaptateur
model = PeftModel.from_pretrained(base_model, "./lora-output")

# Générer
inputs = tokenizer("Écrivez un haïku sur le codage :", return_tensors="pt").to("cuda")
outputs = model.generate(**inputs, max_new_tokens=50)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))

Fusionner l'adaptateur​

# Fusionner pour le déploiement (pas de surcharge d'adaptateur)
model = model.merge_and_unload()

# Sauvegarder le modèle fusionné
model.save_pretrained("./merged-model")
tokenizer.save_pretrained("./merged-model")

Entraînement multi-adaptateurs​

Entraîner plusieurs adaptateurs​

from peft import LoraConfig, get_peft_model

# Créer un modèle PEFT de base
model = get_peft_model(base_model, lora_config)

# Entraîner le premier adaptateur
trainer.train()
model.save_pretrained("./adapter-task-a")

# Ajouter un autre adaptateur pour une tâche différente
model.add_adapter("task_b", lora_config)
model.set_adapter("task_b")

# Entraîner le deuxième adaptateur
trainer.train() # Avec un jeu de données différent
model.save_pretrained("./adapter-task-b", save_adapter="task_b")

Changer d'adaptateurs​

# Charger la base avec le premier adaptateur
model = PeftModel.from_pretrained(base_model, "./adapter-task-a")

# Charger un adaptateur supplémentaire
model.load_adapter("./adapter-task-b", adapter_name="task_b")

# Changer entre les adaptateurs
model.set_adapter("default") # Utiliser la tâche A
output_a = model.generate(...)

model.set_adapter("task_b") # Utiliser la tâche B
output_b = model.generate(...)

Guide de réglage des hyperparamètres​

Sélection du rang​

# Commencez ici
rank_experiments = [8, 16, 32]

for r in rank_experiments:
config = LoraConfig(r=r, lora_alpha=r*2, ...)
# Entraîner et évaluer
# Comparer la qualité par rapport au temps d'entraînement

Taux d'apprentissage​

# LoRA utilise généralement un taux d'apprentissage plus élevé que le FT complet
lr_options = [1e-4, 2e-4, 5e-4]

# Commencez avec 2e-4, ajustez en fonction de :
# - La perte ne diminue pas → augmentez le taux d'apprentissage
# - Entraînement instable → diminuez le taux d'apprentissage

Modules cibles​

# Minimal (le plus rapide, qualité la plus basse)
minimal = ["q_proj", "v_proj"]

# Standard (bon équilibre)
standard = ["q_proj", "k_proj", "v_proj", "o_proj"]

# Complet (meilleure qualité, plus de paramètres)
full = ["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"]

Dépannage des problèmes courants​

Problème : La perte ne diminue pas​

# Vérification 1 : L'adaptateur est-il entraînable ?
model.print_trainable_parameters()
# Devrait montrer des paramètres entraînables non nuls

# Vérification 2 : Taux d'apprentissage trop bas ?
# Essayez d'augmenter : 1e-4 → 2e-4 → 5e-4

# Vérification 3 : Formatage des données correct ?
print(dataset[0]["text"]) # Vérifiez le format

Problème : Perte NaN​

# Solution 1 : Réduire le taux d'apprentissage
learning_rate = 1e-5

# Solution 2 : Utiliser le clipping de gradient
training_args = TrainingArguments(
max_grad_norm=1.0,
...
)

# Solution 3 : Vérifiez les inf/nan dans les données

Problème : Mémoire insuffisante​

# Solution 1 : Réduire la taille du lot
per_device_train_batch_size = 1

# Solution 2 : Augmenter l'accumulation de gradient
gradient_accumulation_steps = 16

# Solution 3 : Activer le checkpointing de gradient
gradient_checkpointing = True

# Solution 4 : Utiliser QLoRA à la place (module suivant)

Liste de vérification pour la production​

□ Choisir un rang approprié (commencez avec 16)
□ Définir alpha à 2x le rang
□ Cibler au minimum toutes les projections d'attention
□ Utiliser un taux d'apprentissage approprié (1e-4 à 5e-4)
□ Activer le checkpointing de gradient pour la mémoire
□ Configurer les métriques d'évaluation
□ Sauvegarder régulièrement les checkpoints
□ Fusionner l'adaptateur pour le déploiement
□ Tester le modèle fusionné de manière approfondie

Points clés à retenir​

✅ La bibliothèque PEFT simplifie l'implémentation de LoRA

✅ Configuration clé : r (rang), alpha (mise à l'échelle), target_modules

✅ SFTTrainer simplifie la boucle d'entraînement

✅ Les adaptateurs peuvent être sauvegardés/chargés/fusionnés indépendamment

✅ Plusieurs adaptateurs permettent le changement de tâche

✅ Commencez avec les paramètres standards, ajustez au besoin


Prochaines étapes​

Testez vos connaissances avec le Quiz du Module 4, puis continuez avec Module 5 : QLoRA et Quantification.


Vérification rapide​

Avant de continuer, assurez-vous de pouvoir répondre :

  1. Quelle fonction crée un modèle PEFT à partir d'un modèle de base ?
  2. Comment fusionnez-vous un adaptateur LoRA dans le modèle de base ?
  3. Quel est un taux d'apprentissage typique pour l'entraînement LoRA ?