Dans Finetuning vs RAG, j'expliquais pourquoi LoRA entraîne quelques millions de paramètres au lieu de plusieurs milliards. Ici, on passe au code : finetuner un modèle 7B en QLoRA (LoRA + quantification 4 bits) sur un seul GPU grand public, de l'installation jusqu'au test d'inférence.
Prérequis
Un GPU avec au moins 8 Go de VRAM pour un modèle 7B (un RTX 3060 12 Go ou mieux suffit largement grâce à la quantification 4 bits). Côté logiciel :
pip install transformers peft bitsandbytes trl accelerate datasets
1. Charger le modèle en 4 bits
BitsAndBytesConfig quantifie les poids du modèle de base au chargement — c'est ce qui fait tenir un 7B sur 8 Go au lieu de 28 Go en float32.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
model_id = "mistralai/Mistral-7B-v0.1"
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
model_id,
quantization_config=bnb_config,
device_map="auto",
)
2. Préparer le dataset
Le format compte plus que le volume : quelques centaines d'exemples bien construits dans un format cohérent battent souvent un dataset énorme et bruité. Ici un format instruction/contexte/réponse simple :
from datasets import Dataset
exemples = [
{
"instruction": "Résume ce ticket support en une phrase.",
"input": "Le client ne peut plus se connecter depuis la mise à jour de mardi, message 'session expirée' en boucle.",
"output": "Connexion impossible après la mise à jour de mardi (boucle de session expirée).",
},
# ... quelques centaines d'exemples dans le même format
]
def format_prompt(ex):
return {
"text": f"### Instruction:\n{ex['instruction']}\n\n"
f"### Contexte:\n{ex['input']}\n\n"
f"### Réponse:\n{ex['output']}"
}
dataset = Dataset.from_list(exemples).map(format_prompt)
3. Configurer l'adapter LoRA
prepare_model_for_kbit_training est facile à oublier — sans lui, les gradients ne remontent pas correctement à travers les poids quantifiés. target_modules liste les couches où injecter les matrices A/B ; se limiter à q_proj/v_proj marche mais couvrir aussi k_proj/o_proj donne de meilleurs résultats pour un coût mémoire marginal.
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
model = prepare_model_for_kbit_training(model)
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="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: ~8M || all params: ~7.25B || trainable%: ~0.1%
4. Lancer l'entraînement
from trl import SFTTrainer, SFTConfig
config = SFTConfig(
output_dir="./mistral-support-lora",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
num_train_epochs=3,
learning_rate=2e-4,
logging_steps=10,
bf16=True,
dataset_text_field="text",
)
trainer = SFTTrainer(model=model, train_dataset=dataset, args=config)
trainer.train()
Le learning_rate de 2e-4 est volontairement plus élevé qu'en finetuning complet (souvent 1e-5 à 5e-5) : avec si peu de paramètres entraînables, un taux trop bas n'apprend simplement rien en 3 epochs.
5. Fusionner et tester
Une fois l'entraînement fini, deux options : garder l'adapter séparé (léger, quelques dizaines de Mo, facile à swapper) ou le fusionner dans les poids du modèle pour un déploiement sans dépendance à peft à l'inférence.
merged = model.merge_and_unload()
merged.save_pretrained("./mistral-support-merged")
tokenizer.save_pretrained("./mistral-support-merged")
inputs = tokenizer(
"### Instruction:\nRésume ce ticket...\n\n### Réponse:\n",
return_tensors="pt",
).to(merged.device)
output = merged.generate(**inputs, max_new_tokens=80)
print(tokenizer.decode(output[0], skip_special_tokens=True))
Pièges fréquents
- Dataset trop petit. En dessous de quelques centaines d'exemples, le modèle overfit dès la 2ᵉ epoch — surveiller la loss d'évaluation, pas seulement la loss d'entraînement.
- target_modules incomplet. Se limiter à
q_proj/v_projfonctionne mais sous-performe légèrement par rapport à couvrir aussik_proj/o_proj, pour un coût mémoire négligeable. - Utiliser LoRA pour injecter des faits. Comme vu dans Finetuning vs RAG, ce n'est pas ce que ça fait bien — LoRA excelle sur le format et le ton, pas sur la mémorisation de connaissances.
- Se lancer alors qu'un prompt suffirait. Si le comportement voulu tient dans quelques exemples en few-shot prompting, tout ce pipeline n'apporte souvent rien de plus qu'un prompt bien construit.
FAQ
Faut-il un GPU cloud pour ce tutoriel ?
Non pour un 7B : un GPU grand public avec 8-12 Go de VRAM suffit grâce à la quantification 4 bits. Au-delà de 13B, un GPU cloud devient plus confortable.
Peut-on entraîner sans CUDA (Mac, CPU) ?
bitsandbytes dépend de CUDA — sur Mac, il faut passer par un backend MPS avec des contraintes différentes, ou louer un GPU cloud pour suivre ce tutoriel tel quel.
Combien de temps prend l'entraînement ?
Pour quelques centaines d'exemples et 3 epochs sur un 7B, compter de 20 à 60 minutes selon le GPU — très loin des dizaines d'heures d'un finetuning complet.