L'écosystème des modèles de langage (LLM) a connu une accélération fulgurante avec l'émergence de modèles performants tels que Llama 3 de Meta, Mistral/Mixtral, Falcon et la série Qwen. Pour adapter ces modèles à des besoins métier spécifiques, le "fine-tuning" (ajustement fin) reste la méthode de référence.
Cet article détaille le processus de fine-tuning d'un LLM ouvert en utilisant la bibliothèque TRL (Transformer Reinforcement Learning) de Hugging Face, ainsi que transformers et datasets. Nous aborderons la préparation des données, l'entraînement avec QLoRA et le déploieemnt en production.
1. Définition de l'objectif métier
Le fine-tuning n'est pas toujousr nécessaire. Avant de l'envisager, il est recommandé de tester des techniques de Prompt Engineering ou de RAG (Retrieval Augmented Generation). Cependant, pour des tâches complexes nécessitant un format de sortie rigide ou une connaissance structurelle profonde, le fine-tuning est idéal.
Cas d'usage : Création d'un assistant "Text-to-SQL" capable de transformer une instruction en langage naturel en requête SQL valide, en se basant sur un schéma de base de données spécifique.
2. Configuration de l'environnement
Pour optimiser l'entraînement sur des GPU récents (NVIDIA A10G, RTX 4090/3090), nous utilisons les dernières versions des bibliothèques Hugging Face et le support de Flash Attention.
# Installation des dépendances principales
%pip install torch==2.4.0 tensorboard
%pip install --upgrade transformers==4.44.2 datasets==2.21.0 accelerate==0.33.0 evaluate==0.4.2 bitsandbytes==0.43.3 trl==0.9.6 peft==0.12.0
# Installation de Flash Attention pour accélérer le calcul (GPU Ampere+)
!pip install ninja packaging
!MAX_JOBS=4 pip install flash-attn --no-build-isolation
3. Préparation du jeu de données
Nous utilisons le dataset sql-create-context. Pour que trl traite efficacement les données, nous devons les convertir au format "conversationnel" (ChatML).
from datasets import load_dataset
# Configuration du message système
template_sql = """Vous êtes un expert en SQL. Générez une requête SQL basée sur le schéma suivant :
SCHEMA:
{schema}"""
def formater_conversation(ligne):
return {
"messages": [
{"role": "system", "content": template_sql.format(schema=ligne["context"])},
{"role": "user", "content": ligne["question"]},
{"role": "assistant", "content": ligne["answer"]}
]
}
# Chargement et transformation
raw_data = load_dataset("b-mc2/sql-create-context", split="train")
raw_data = raw_data.shuffle(seed=42).select(range(12500))
formatted_dataset = raw_data.map(formater_conversation, remove_columns=raw_data.features)
# Split entraînement / test
final_data = formatted_dataset.train_test_split(test_size=0.2)
final_data["train"].to_json("train_set.json", orient="records")
final_data["test"].to_json("test_set.json", orient="records")
4. Fine-tuning avec SFTTrainer et QLoRA
Le SFTTrainer de TRL simplifie l'entraînement supervisé. Nous utilisons QLoRA pour réduire l'empreinte mémoire en quantifiant le modèle de base en 4 bits.
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig, TrainingArguments
from peft import LoraConfig
from trl import SFTTrainer
checkpoint_path = "Qwen/Qwen2.5-3B-Instruct"
# Configuration de la quantification 4-bit
config_quantisation = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
# Chargement du modèle et du tokenizer
llm_base = AutoModelForCausalLM.from_pretrained(
checkpoint_path,
device_map="auto",
attn_implementation="flash_attention_2",
torch_dtype=torch.bfloat16,
quantization_config=config_quantisation
)
tokenizer_llm = AutoTokenizer.from_pretrained(checkpoint_path)
tokenizer_llm.padding_side = 'right'
# Configuration LoRA
config_peft = LoraConfig(
lora_alpha=128,
lora_dropout=0.05,
r=256,
bias="none",
target_modules="all-linear",
task_type="CAUSAL_LM"
)
# Paramètres d'entraînement
config_entrainement = TrainingArguments(
output_dir="qwen-sql-expert",
num_train_epochs=3,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
gradient_checkpointing=True,
optim="adamw_torch_fused",
learning_rate=2e-4,
bf16=True,
save_strategy="epoch",
logging_steps=10,
report_to="tensorboard"
)
# Initialisation du Trainer
instructeur_sft = SFTTrainer(
model=llm_base,
args=config_entrainement,
train_dataset=final_data["train"],
peft_config=config_peft,
max_seq_length=2048,
tokenizer=tokenizer_llm,
packing=True
)
instructeur_sft.train()
instructeur_sft.save_model()
5. Fusion des poids et évaluation
Après l'entraînement, nous obtenons des adaptateurs LoRA. Pour faciliter le déploiement, il est préférable de fusionner ces poids avec le modèle de base.
from peft import AutoPeftModelForCausalLM
# Rechargement et fusion
modele_final = AutoPeftModelForCausalLM.from_pretrained(
"qwen-sql-expert",
torch_dtype=torch.float16,
low_cpu_mem_usage=True,
)
modele_fusionne = modele_final.merge_and_unload()
modele_fusionne.save_pretrained("model-sql-prod", safe_serialization=True)
Pour évaluer le modèle, nous comparons la sortie générée avec la réponse attendue sur le jeu de test.
from transformers import pipeline
generateur = pipeline("text-generation", model="model-sql-prod", tokenizer=tokenizer_llm)
def tester_sql(prompt_test):
messages = [{"role": "user", "content": prompt_test}]
prompt = tokenizer_llm.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
resultat = generateur(prompt, max_new_tokens=128, do_sample=False)
return resultat[0]['generated_text']
print(tester_sql("Combien d'employés ont un salaire supérieur à 50000 dans la table 'staff'?"))
6. Déploiement avec TGI
Pour une mise en production robuste, Text Generation Inference (TGI) est recommandé. Il permet de servir le modèle via une API compatible OpenAI avec des optimisations comme le continuous batching.
# Lancement du conteneur TGI
docker run -d --name sql-service --gpus all -p 8080:80 \
-v $(pwd)/model-sql-prod:/data \
ghcr.io/huggingface/text-generation-inference:2.2.0 \
--model-id /data \
--max-total-tokens 2048
Une fois le service démarré, le modèle peut être interrogé par n'importe quel client HTTP ou via le SDK OpenAI en pointant vers l'adresse locale.