Chapitre 18 · 12 min
Fine-tuning avec LoRA
Spécialise ton modèle entraîné à bas coût avec LoRA, la voie pratique pour adapter un comportement sans payer un réentraînement complet.
Tu as un modèle. Il fonctionne, plus ou moins. Tu veux le spécialiser : meilleur en code, dans un dialecte précis, ou pour produire du JSON. L’approche naïve consiste à continuer l’ sur les nouvelles données en mettant à jour tous les .
Ça marche, mais c’est cher. Fine-tuner un modèle d’un milliard de pour une petite tâche coûte beaucoup de GPU et produit un énorme par tâche. La plupart des poids changent à peine.
rend le bon marché. Au lieu de modifier W, on le fige. On ajoute une petite correction basse-rang A · B et on n’entraîne que cette correction :
W est [d × d], A est [d × r], B est [r × d], avec r ≪ d. Les entraînables passent de d² à 2 · d · r — un ratio d’économie de d / (2r). Le gain s’amplifie spectaculairement quand les modèles grossissent :
- Ton modèle du ch.12 (
n_embd = 128,r = 8) : chaquenn.Linearcarré passe de16 384 → 2 048— soit 8× moins. - GPT-2 small (
d = 1024,r = 8) :1 048 576 → 16 384— soit 64× moins. - Échelle GPT-3 (
d = 4 096,r = 8) :16 777 216 → 65 536— soit 256× moins.
prend tout son sens quand les modèles sont gros. Sur ton modèle local de 14M , envelopper chaque nn.Linear ramène quand même le compte entraînable à environ 65k — soit ~200× moins de entraînables que le modèle complet. Le gain relatif ne fait que croître quand tu montes en taille.
1. La passe avant LoRA
Écris la formule. La cellule reçoit une matrice W figée, des matrices basses-rang A et B, et un facteur alpha.
À toi de jouer · JavaScript
La sortie peut imiter un full fine-tune si la mise à jour complète est approximativement basse-rang. Le papier montre que, pour beaucoup de tâches d’adaptation, cette hypothèse tient très bien.
2. Pourquoi ça marche
Ce qu’on adapte est souvent un déplacement étroit : parler dans un autre registre, suivre une instruction, citer une source, respecter un format. Ça ne demande pas forcément des millions de dimensions nouvelles. Quelques dimensions de “delta” suffisent.
Vois W comme la compétence de base du modèle, et α · A · B comme un overlay par tâche. Quand tu changes de tâche, tu changes d’overlay ; le modèle de base reste intact.
3. La version Python
Le pattern PyTorch consiste à envelopper les nn.Linear existants dans une version LoraLinear. Sauvegarde llm/lora.py :
"""llm/lora.py — LoRA adapter wrapper for nn.Linear."""
import math
import torch
import torch.nn as nn
class LoraLinear(nn.Module):
"""Wraps an existing nn.Linear with frozen weights + a trainable low-rank update."""
def __init__(self, base: nn.Linear, r: int = 8, alpha: float = 16.0):
super().__init__()
self.base = base
for p in self.base.parameters():
p.requires_grad = False # freeze
self.r = r
self.alpha = alpha
d_in = base.in_features
d_out = base.out_features
self.A = nn.Parameter(torch.zeros(d_in, r))
nn.init.kaiming_uniform_(self.A, a=math.sqrt(5))
self.B = nn.Parameter(torch.zeros(r, d_out)) # zero init: model starts at base behavior
def forward(self, x):
return self.base(x) + self.alpha * (x @ self.A @ self.B)
def apply_lora(model, r=8, alpha=16):
"""Replace every nn.Linear inside the model with a LoraLinear wrapper."""
for name, module in model.named_children():
if isinstance(module, nn.Linear):
setattr(model, name, LoraLinear(module, r=r, alpha=alpha))
else:
apply_lora(module, r=r, alpha=alpha)
return modelLis ce wrapper comme “comportement de base + petite correction apprise” :
self.base = basegarde la couche originale.requires_grad = Falsefige ses poids.Aprojette vers le rangr.Breprojette vers la sortie.x @ self.A @ self.Best le delta basse-rang.apply_loraparcourt le modèle et remplace lesnn.Linear.
Deux détails :
Binitialisé à zéro : au départ, le modèle produit exactement la sortie du modèle de base.Ainitialisé avec Kaiming : petite init aléatoire prête pour les .
Pour utiliser sur ton modèle du chapitre 13, modifie le setup d’ :
from llm.model import GPT, GPTConfig
from llm.lora import apply_lora
# [1]
model = GPT(GPTConfig())
# [2]
model.load_state_dict(torch.load("checkpoints/model.pt"))
# [3]
model = apply_lora(model, r=8, alpha=16)
# only A and B are trainable now
# [4]
trainable = [p for p in model.parameters() if p.requires_grad]
print(f"trainable params: {sum(p.numel() for p in trainable):,}")
# ... rest of the training loop, using `trainable` for the optimizer- [1] recrée l’architecture.
- [2] charge le de base.
- [3] ajoute les adaptateurs.
- [4] donne à l’ seulement les entraînables.
3b. Qu’est-ce que le rang t’achète vraiment ?
Le rang est le seul bouton que tu vas régler, et son coût se calcule avant de dépenser quoi que ce soit. Calcule le nombre de entraînables et la taille du pour une gamme de rangs sur GPT-2 small.
À toi de jouer · JavaScript
Deux choses doivent sauter aux yeux. D’abord, la relation est linéaire en rang : doubler r double l’adaptateur, et rien d’autre ne change. Ensuite, même le rang 64 reste une erreur d’arrondi à côté du modèle de base gelé. Cette asymétrie est tout l’argument de : tu peux garder une douzaine de spécialisations sur disque pour moins cher qu’une seule copie complète.
4. Régime checkpoint
Un d’un modèle 1B avec r = 8 produit un de quelques dizaines de Mo au lieu de plusieurs Go. Tu peux servir un modèle de base + des dizaines d’adaptateurs.
C’est pourquoi domine le open-source : on partage les deltas, pas tout le modèle.
Recap
- fige
Wet ajouteα · A · B. rest le rang de l’update, souvent 4-32.Bà zéro garantit que le modèle démarre identique à la base.- Les gains disque sont énormes.
- La qualité approche souvent un full fine-tune pour beaucoup de tâches.
- Ton projet local a maintenant
llm/lora.py.
Pour aller plus loin
Prochaine étape : quantification — réduit le coût d’adaptation ; la quantification réduit le coût d’.
Questions fréquentes
Qu'est-ce que le fine-tuning LoRA ?
Low-Rank Adaptation : au lieu de mettre à jour tous les poids, on gèle le modèle de base et on entraîne deux petites matrices A et B dont le produit approxime la mise à jour. La couche calcule (W + A·B)·x, et seules A et B reçoivent des gradients. On obtient l'essentiel du bénéfice d'un fine-tuning pour une fraction des paramètres entraînables.
Combien de paramètres LoRA entraîne-t-il réellement ?
Pour un adaptateur de rang 8 sur GPT-2 small, de l'ordre de quelques centaines de milliers contre 124 millions — soit 0,1 à 0,2 %. L'économie grandit avec la taille du modèle, et c'est ce qui rend réaliste le fine-tuning de gros modèles sur du matériel grand public.
Quel rang choisir ?
Le rang contrôle la capacité de l'adaptateur. Trop bas, le fine-tuning sous-apprend ; trop haut, on rend l'économie de paramètres pour un gain faible. Les valeurs courantes sont 4, 8 et 16, et 8 est un défaut raisonnable pour l'instruction tuning et la plupart des adaptations de domaine.
Sur quelles couches appliquer LoRA ?
Sur les projections d'attention — W_Q et W_V, parfois W_K et W_O. Empiriquement, ça suffit pour l'instruction tuning et la plupart des travaux de domaine. Inclure les couches feed-forward double le nombre de paramètres pour un gain marginal sur les tâches courantes.