Introduction
Le fine-tuning LoRA (Low-Rank Adaptation) révolutionne l'adaptation de grands modèles de langage (LLM) en 2026. Contrairement au full fine-tuning qui exige des téraoctets de VRAM, LoRA injecte des matrices de bas rang dans les couches d'attention, ne fine-tunant que 0,1-1% des paramètres. Résultat : un entraînement 3x plus rapide et 80% moins gourmand en mémoire sur un seul GPU A100.
Ce tutoriel expert vous guide pas à pas pour fine-tuner Llama-3-8B sur le dataset databricks-dolly-15k (instruction-following). Nous utilisons Hugging Face PEFT, Transformers et TRL pour un Supervised Fine-Tuning (SFT) production-ready. À la fin, vous mergez les adaptateurs LoRA dans le modèle base pour inférence optimale. Parfait pour personnaliser un assistant IA sur vos données métier sans cluster massif. (128 mots)
Prérequis
- Python 3.10+ et Git installés
- GPU NVIDIA avec ≥16GB VRAM (A100/H100 recommandé ; testé sur RTX 4090)
- Connaissances avancées : PyTorch, Transformers, tokenizers
- Espace Hugging Face (token pour modèles gated comme Llama-3)
- ≥50GB stockage SSD pour datasets/caches
Installation des dépendances
#!/bin/bash
set -e
# Mises à jour pip et torch CUDA
pip install --upgrade pip
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
# Core libs HF + PEFT/TRL
pip install transformers==4.45.1
pip install peft==0.12.0
pip install trl==0.9.6
pip install datasets==2.21.0
pip install accelerate==0.33.0
pip install bitsandbytes==0.43.1
pip install wandb
# Login HF (remplacez par votre token)
huggingface-cli login
# Vérification CUDA
python -c "import torch; print(f'CUDA: {torch.cuda.is_available()}, Devices: {torch.cuda.device_count()}')"Ce script installe toutes les bibliothèques essentielles en versions pinned pour compatibilité 2026. Torch CUDA 12.1 optimise pour Ampere/Ada GPUs ; bitsandbytes active 4/8-bit quantization. Exécutez-le une fois, il gère le login HF pour accéder à Llama-3. Piège : oubliez pas accelerate config post-install pour multi-GPU si besoin.
Préparation du dataset
Nous utilisons databricks-dolly-15k, un dataset d'instructions naturelles (15k exemples : question/réponse). Chargez-le via datasets, appliquez un formatage template pour Llama-3 (<|begin_of_text|><|start_header_id|>user<|end_header_id|>
{instruction}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
{response}<|eot_id|>). Cela aligne le modèle sur du chat instructif. Tokenisez en batch pour efficacité.
Script de préparation dataset
from datasets import load_dataset, DatasetDict
# Chargement Dolly-15k (train split seulement pour simplicité)
dataset = load_dataset("databricks/databricks-dolly-15k", split="train")
dataset = dataset.train_test_split(test_size=0.1) # 90/10 split
# Template Llama-3 pour SFTTrainer
llama_prompt = "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\n{instruction}<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n{output}<|eot_id|>"
def formatting_prompts_func(example):
return {'text': llama_prompt.format(instruction=example['instruction'], output=example['output'])}
train_dataset = dataset['train'].map(formatting_prompts_func)
eval_dataset = dataset['test'].map(formatting_prompts_func)
# Sauvegarde local pour réutilisation
train_dataset.save_to_disk('dolly_train')
eval_dataset.save_to_disk('dolly_eval')
print(f'Train: {len(train_dataset)}, Eval: {len(eval_dataset)}')Ce script charge, splitte et formate le dataset en prompts Llama-3 prêts pour SFT. Le template EOS <|eot_id|> est critique pour éviter overflow. Sauvegarde en Arrow pour chargement rapide ultérieur. Piège : sans split, pas de validation ; map() est lazy mais save_to_disk matérialise.
Configuration du modèle et LoRA
Chargez meta-llama/Meta-Llama-3-8B-Instruct en 4-bit (QLoRA) pour cabler sur 16GB VRAM. Appliquez PEFT LoRA sur q_proj, v_proj (r=16, alpha=32). Cela gèle le base model, n'entraînant que ~7M params. Utilisez SFTTrainer de TRL pour packing et loss masking automatique.
Chargement modèle + config LoRA
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, TaskType
model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
# Quantization 4-bit pour QLoRA
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
attn_implementation="flash_attention_2"
)
# Tokenizer avec padding EOS
tokenizer = AutoTokenizer.from_pretrained(model_id)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# Config LoRA experte : cible attention, r=16 pour balance perf/mémoire
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # ~0.3% trainableCharge le modèle en QLoRA 4-bit NF4 pour max compression (réduit VRAM de 16GB→5GB). LoRA cible les 4 projections d'attention ; r=16 est sweet spot (plus=overfit, moins=underfit). print_trainable_parameters() confirme efficacité. Piège : sans flash_attention_2, perf chute 2x ; pad_token doit matcher EOS.
Setup du SFTTrainer
Configurez SFTTrainer avec packing (groupe prompts pour max seq_len=2048), gradient checkpointing et DPO-ready logging. Max_steps=100 pour test rapide (1h sur A100) ; ajustez pour full run. Logging WandB pour monitorer loss/perplexité.
Configuration et lancement entraînement
from datasets import load_from_disk
from trl import SFTTrainer, SFTConfig
from peft import LoraConfig
# Datasets préparés
train_dataset = load_from_disk('dolly_train')
eval_dataset = load_from_disk('dolly_eval')
# Hyperparams expertes 2026
sft_config = SFTConfig(
output_dir="./lora-llama3-dolly",
num_train_epochs=1,
per_device_train_batch_size=2,
per_device_eval_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
logging_steps=10,
save_steps=50,
eval_steps=50,
max_seq_length=2048,
packing=True, # Pack prompts pour efficacité
dataset_num_proc=4,
report_to="wandb",
push_to_hub=True,
gradient_checkpointing=True,
remove_unused_columns=False,
warmup_steps=100
)
trainer = SFTTrainer(
model=model,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
tokenizer=tokenizer,
args=sft_config,
max_seq_length=2048,
packing=True,
dataset_text_field="text"
)
trainer.train()
trainer.save_model()
trainer.push_to_hub("votre-username/lora-llama3-dolly")SFTTrainer gère tokenization, masking et packing automatiquement. Batch_size=2/accum=4 équivaut effective batch=16 ; LR=2e-4 optimal pour LoRA. Packing booste throughput 2x en remplissant séquences. Piège : sans remove_unused_columns=False, erreur sur text_field ; push_to_hub nécessite repo HF pré-créé.
Merge et inférence
Post-entraînement, mergez LoRA dans le base model (full precision) pour inférence rapide sans PEFT overhead. Testez avec pipeline pour générer réponses instructives.
Merge LoRA et test inférence
from peft import PeftModel
from transformers import AutoTokenizer, AutoModelForCausalLM
# Chemin LoRA entraîné
peft_model_id = "./lora-llama3-dolly"
# Load base + adapter
model = AutoModelForCausalLM.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct", torch_dtype=torch.bfloat16, device_map="auto")
model = PeftModel.from_pretrained(model, peft_model_id)
# Merge et unload
merged_model = model.merge_and_unload()
merged_model.save_pretrained("merged-lora-llama3")
# Tokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B-Instruct")
tokenizer.pad_token = tokenizer.eos_token
# Test inférence
prompt = "<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\nExpliquez LoRA en 3 phrases.<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"
inputs = tokenizer(prompt, return_tensors="pt").to(merged_model.device)
outputs = merged_model.generate(**inputs, max_new_tokens=128, temperature=0.7, do_sample=True)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))Merge compresse LoRA dans base model (taille +0.1%) pour inférence native. merge_and_unload() libère mémoire. Prompt template exact match entraînement évite hallucinations. Piège : dtype bfloat16 pour précision ; sans do_sample, output déterministe et plat.
Script de déploiement (vLLM)
from vllm import LLM, SamplingParams
llm = LLM(model="merged-lora-llama3", tensor_parallel_size=1, dtype="bfloat16")
prompts = [
"<|begin_of_text|><|start_header_id|>user<|end_header_id|>\n\nQue fait LoRA ?<|eot_id|><|start_header_id|>assistant<|end_header_id|>\n\n"
]
sampling_params = SamplingParams(temperature=0.7, top_p=0.95, max_tokens=128)
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
print(output.outputs[0].text)vLLM (install via pip install vllm) sert le merged model à 100+ req/s. Tensor_parallel pour multi-GPU. Params optimisés : top_p=0.95 évite répétitions. Piège : repo HF merged doit être public ou logged ; dtype match entraînement.
Bonnes pratiques
- Choisissez r dynamiquement : r=8 pour datasets petits, r=64 pour >1M exemples (testez via validation loss).
- Quantizez agressivement : Toujours QLoRA 4-bit + double_quant pour <10GB VRAM sur 7B models.
- Packez et checkpoint : Activez packing + gradient_accum pour scaler batch sans OOM.
- Monitorer overfitting : Eval perplexity <1.2 cible ; early-stop si plateau >3 epochs.
- Versionnez adaptateurs : Push LoRA séparé (20MB) sur HF, merge à l'inférence.
Erreurs courantes à éviter
- Template mismatch : Prompt train ≠ inférence → générations incohérentes. Vérifiez
<|eot_id|>partout. - Oubli pad_token : Causait pad_id=0 invalid → tokenizer fix obligatoire.
- LR trop haute : >5e-4 catapulte loss ; decay cosine + warmup=100 steps.
- Pas de merge : Inférence PEFT 2x plus lente ; toujours merge pour prod.
Pour aller plus loin
- Docs officielles : PEFT LoRA, TRL SFT
- Datasets avancés : UltraChat, OpenHermes
- Suivant : QLoRA + DPO pour alignment RLHF
- Formations Learni ML avancé : Deep dive PEFT + scaling.