Le protocole expérimental, de A à Z
Le déroulé technique réel du run, avec tous les paramètres, pour reproduire à l'identique.
Tout a été piloté en SSH depuis un Mac : générer le dataset, louer une A100, fine-tuner avec Unsloth, vérifier, rapatrier les poids, benchmarker, analyser.
1. Environnement (versions exactes)
Image Docker vastai/unsloth-studio sur l'A100. On n'utilise pas l'interface Studio, seulement son Python, en lançant nos propres scripts.
- Python 3.12, torch 2.10.0+cu128, CUDA 12.8
- transformers 5.5.0, unsloth 2026.5.5, trl 0.23.1, peft 0.18.1, bitsandbytes 0.49.2
- Pas de token HuggingFace nécessaire : Qwen3 est Apache 2.0 (non gated). C'est aussi pourquoi on a quitté Gemma, dont le canal de raisonnement n'est pas tokenisé.
2. Chargement du modèle
from unsloth import FastLanguageModel
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="unsloth/Qwen3-32B-unsloth-bnb-4bit",
max_seq_length=2048, load_in_4bit=True, full_finetuning=False)
3. LoRA (QLoRA 4-bit)
model = FastLanguageModel.get_peft_model(
model, r=32, lora_alpha=32, lora_dropout=0, bias="none",
target_modules=["q_proj","k_proj","v_proj","o_proj","gate_proj","up_proj","down_proj"],
use_gradient_checkpointing="unsloth", random_state=3407)
4. Hyperparamètres d'entraînement
from trl import SFTConfig
SFTConfig(
dataset_text_field="text", max_seq_length=2048, packing=False,
per_device_train_batch_size=2, gradient_accumulation_steps=4, # batch effectif = 8
num_train_epochs=2, learning_rate=2e-4, lr_scheduler_type="linear",
warmup_ratio=0.05, optim="adamw_8bit", weight_decay=0.01,
bf16=True, fp16=False, seed=3407,
save_steps=116, save_total_limit=3, eval_steps=116, logging_steps=5)
5. Le point décisif : train_on_responses_only
La loss ne porte que sur le tour assistant, donc sur le <think> et la réponse, pas sur la question.
from unsloth.chat_templates import train_on_responses_only
trainer = train_on_responses_only(
trainer,
instruction_part="<|im_start|>user\n",
response_part="<|im_start|>assistant\n")
Avant de dépenser l'heure de calcul, le script décode la zone supervisée de l'exemple 0 et vérifie que le <think> y est bien présent (sinon il abandonne). Au run : 1158 tokens supervisés sur 1230, <think> et </think> présents. C'est exactement le test que le run précédent sur Gemma ratait (0 trace <think> apprise sur 12), parce que son canal de raisonnement n'est pas tokenisé.
6. Lancement détaché et résultats
Tout run long en nohup ... < /dev/null & plus une sonde de polling sur le log, pour résister aux coupures SSH.
- train_loss 1,566, eval_loss 1,594 (pas de surapprentissage)
- VRAM utilisée ~43 Go sur 80 (large)
- Adaptateur sauvé :
adapter_model.safetensors(1,07 Go) + config + tokenizer + chat template