« LoRA entraîne environ 1 % du modèle » est l'explication habituelle en une ligne. C'est vrai, mais elle cache ce qui décide si un entraînement tient sur votre machine et combien de temps il prend : quelles couches reçoivent des adaptateurs, ce que le rank change au nombre de paramètres, dans quelle précision sont les poids gelés, et combien de pas d'optimiseur un petit dataset donne vraiment.
Voici ces chiffres calculés pour un entraînement concret : Qwen2.5-1.5B-Instruct, rank 16, sur un Mac Apple Silicon de 16 Go. Les réglages sont les valeurs par défaut de l'entraîneur local de l'application de bureau osFoundry, qui est un simple script transformers + PEFT + TRL ; l'arithmétique s'applique donc à n'importe quelle configuration similaire. (La page fine-tuning LoRA de l'appli montre l'interface correspondante.)
Un adaptateur, ce sont deux matrices minces
Pour une matrice de poids W de forme d_out × d_in, LoRA gèle W et apprend deux matrices, A (r × d_in) et B (d_out × r). La sortie de la couche devient W·x + (α/r)·B·A·x. B démarre à zéro : l'entraînement part donc exactement du modèle de base.
Le nombre de paramètres entraînables de cette couche est r × (d_in + d_out). Il croît linéairement avec le rank et avec la largeur de la couche, pas avec sa surface.
Où vont les adaptateurs
L'entraîneur place des adaptateurs sur les sept projections linéaires de chaque bloc transformer :
target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"] Qwen2.5-1.5B a 28 blocs, une taille cachée de 1536, une taille de MLP de 8960 et du grouped-query attention avec 2 têtes KV de 128 dimensions, si bien que k et v projettent vers 256. Par bloc, à r = 16 :
| Couche | d_in → d_out | Entraînables |
|---|---|---|
| q_proj | 1536 → 1536 | 49,152 |
| k_proj | 1536 → 256 | 28,672 |
| v_proj | 1536 → 256 | 28,672 |
| o_proj | 1536 → 1536 | 49,152 |
| gate_proj | 1536 → 8960 | 167,936 |
| up_proj | 1536 → 8960 | 167,936 |
| down_proj | 8960 → 1536 | 167,936 |
Cela fait 659,456 par bloc, et 18,464,768 sur 28 blocs : 18.5M entraînables sur 1.54B au total, soit 1.2 %.
Deux choses ressortent. Les projections du MLP portent les trois quarts des poids de l'adaptateur ; les omettre (comme le font certaines anciennes recettes, qui n'adaptent que q et v) ramène le total à environ 2.2M, un huitième de la capacité. Et comme le GQA rend k et v étroites, leurs adaptateurs coûtent peu.
L'essentiel de la mémoire, ce sont les poids gelés
Sur le GPU d'Apple, l'entraîneur charge le modèle en float32 :
torch_dtype = torch.float16 if device == "cuda" else torch.float32 La demi-précision sur MPS a tendance à produire des pertes NaN, le 32 bits est donc le choix sûr. La base gelée occupe alors 1.54B × 4 octets ≈ 6.2 Go.
La partie entraînable est petite en comparaison. 18.5M de poids en fp32, c'est 74 Mo, les gradients en prennent 74 de plus, et AdamW garde deux estimations de moment par poids, soit encore 148 Mo. Environ 0.3 Go en tout. Le reste du budget part dans les activations, qui dépendent de la taille de batch et de la longueur de séquence (l'entraîneur plafonne les séquences à 2048 tokens ; les réponses courtes de chat en sont très loin).
Avec environ 12 Go sur 16 Go utilisables par le GPU d'un Mac, un modèle de 1.5B en fp32 laisse de la marge. Une base de 7B en fp32 ferait environ 30 Go avant même de commencer l'entraînement. C'est pourquoi, sur un Mac, le modèle de base reste petit, et pourquoi QLoRA (poids gelés en 4 bits) existe ; mais le chemin 4 bits de bitsandbytes exige un GPU NVIDIA, donc sur un Mac c'est du LoRA classique.
Un petit dataset donne moins de mises à jour qu'on ne croit
L'entraîneur utilise un batch size de 4 avec 4 pas d'accumulation de gradient : chaque mise à jour de l'optimiseur voit 16 exemples. Avec 200 paires :
- 200 ÷ 4 = 50 batches forward/backward par epoch
- 50 ÷ 4 ≈ 13 mises à jour de l'optimiseur par epoch
- 3 epochs ≈ 39 mises à jour au total, les 10 premières servant de warmup du learning rate
Trente-neuf mises à jour avec un learning rate de 2e-4 et une décroissance linéaire suffisent à déplacer le ton, pas à enseigner de nouvelles connaissances. C'est ce qu'on constate en pratique : quelques centaines de réponses changent la façon dont un modèle écrit, pas ce qu'il sait.
Ce qui en sort
Après l'entraînement, seul l'adaptateur est enregistré : A et B pour chacune des 196 couches adaptées, quelques dizaines de mégaoctets. Pour l'inférence, l'appli le convertit en fichier LoRA GGUF et llama.cpp l'applique sur une copie quantifiée de la base, ici qwen2.5-1.5b-instruct en Q4_K_M. La base reste partagée : plusieurs adaptateurs (un par ton ou par tâche) coûtent chacun quelques dizaines de Mo.
À noter, l'écart de précision : l'adaptateur a été entraîné sur des poids de base en fp32 et tourne sur des poids en 4 bits. Pour un petit adaptateur de style, la différence passe généralement inaperçue, mais c'est l'une des raisons pour lesquelles un modèle fusionné puis re-quantifié peut se comporter un peu différemment du même adaptateur appliqué à l'exécution.




