Skip to content
Learni
Voir tous les tutoriels
Machine Learning

Comment fine-tuner un LLM avec LoRA en 2026

Read in English

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

setup.sh
#!/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

prepare_dataset.py
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

model_setup.py
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% trainable

Charge 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

train_lora.py
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

merge_inference.py
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)

deploy_vllm.py
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