Aller au contenu principal

Module 6 — QLoRA et quantification en 4 bits

Le module 5 a fait tomber les 100 gigaoctets de l'affinage complet à environ 25 gigaoctets grâce à LoRA. C'est trop pour un RTX 4090, un L4 ou un A6000 de 24 gigaoctets. QLoRA, publié en 2023 par Dettmers et coll., ajoute une couche : quantifier le modèle de base en 4 bits pendant qu'on l'affine par LoRA. Le résultat tient alors sur 24 gigaoctets pour un modèle de 7 milliards de paramètres, avec un écart de qualité qui reste inférieur au bruit statistique d'un affinage. C'est ce module.

Ce que quantifier veut dire

Quantifier un poids, c'est le représenter avec moins de bits que les 16 ou 32 habituels. On échange de la précision numérique contre de la place. Un poids en bfloat16 occupe 2 octets ; le même poids en int4 occupe un demi-octet. Pour 7 milliards de paramètres, l'écart tombe de 14 gigaoctets à 3,5 gigaoctets sur les seuls poids.

La quantification classique projette la distribution des poids sur une grille uniforme entre le minimum et le maximum du bloc. Cette approche fonctionne mal sur les poids de modèles de langage, dont la distribution est fortement piquée autour de zéro : les valeurs éloignées consomment de la précision qui manque au centre, là où l'essentiel de l'information se trouve.

Le format NF4, taillé pour les LLM

L'apport central de QLoRA est le format NF4 (Normal Float 4). L'idée est simple, la mise en œuvre subtile : plutôt qu'une grille uniforme, on choisit des niveaux qui suivent une loi normale, plus denses au centre et plus rares aux extrémités. Comme les poids d'un modèle bien entraîné suivent approximativement une gaussienne centrée, chaque niveau NF4 correspond à peu près au même nombre de poids réels — c'est ce que la théorie de l'information appelle un codage à quantiles.

Concrètement, NF4 fixe 16 niveaux (4 bits) qui minimisent l'erreur pour une distribution normale. Le gain sur la perplexité par rapport à int4 uniforme se lit sur les évaluations : NF4 récupère la quasi-totalité du modèle en bfloat16, là où int4 uniforme peut perdre plusieurs points.

La double quantification

Pour quantifier un bloc de poids, il faut mémoriser une échelle — le facteur qui remet les valeurs à la bonne amplitude. Ces échelles, une par bloc de 64 poids, sont d'ordinaire stockées en float32 : 4 octets par bloc, ce qui rajoute 0,5 bit par poids.

La double quantification de QLoRA compresse aussi ces échelles, en 8 bits. Le surcoût descend à environ 0,127 bit par poids. Sur 7 milliards de paramètres, cela représente une économie de 100 mégaoctets — modeste en absolu, mais utile quand on lutte pour tenir sous 24 gigaoctets.

L'optimiseur paginé

Même avec un modèle en 4 bits, une pointe d'activation peut faire déborder la mémoire GPU pendant un pas d'entraînement — surtout sur les longues séquences. QLoRA introduit un optimiseur paginé : les états d'optimiseur sont stockés en mémoire unifiée entre CPU et GPU, et le pilote CUDA fait aller-retour à la demande. Le pas d'entraînement ralentit légèrement, mais l'entraînement ne casse plus sur une allocation qui aurait échoué.

Le budget mémoire final sur 24 Go

Voici le calcul pour Mistral 7B avec QLoRA, LoRA de rang 16 sur les quatre projections d'attention, séquence de 4 096 jetons, lot effectif de 8 via accumulation.

ComposanteTaille
Poids en NF4 (7 milliards ×\times 0,5 octet)3,5 Go
Échelles doublement quantifiées0,1 Go
Adaptateurs LoRA (16,8 M en bfloat16)0,03 Go
Gradients des adaptateurs0,03 Go
États Adam des adaptateurs0,13 Go
Activations (séq. 4 096, lot 2 + recomputation)10 à 14 Go
Totalenviron 18 à 22 Go

On tient sur 24 gigaoctets avec la marge nécessaire aux pointes. C'est le résultat qui a changé le paysage : l'affinage d'un modèle de 7 milliards devient accessible à toute personne disposant d'un RTX 4090 grand public, sans centre de calcul.

La configuration en pratique

Une configuration QLoRA typique tient en une trentaine de lignes, autour de la bibliothèque bitsandbytes pour la quantification et de peft pour LoRA.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

nom_modele = "mistralai/Mistral-7B-Instruct-v0.3"

# Configuration de la quantification : NF4, double quantification.
config_quant = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_use_double_quant=True,
bnb_4bit_compute_dtype=torch.bfloat16, # calcul en bfloat16, stockage en 4 bits
)

# Chargement du modele quantifie sur GPU.
modele = AutoModelForCausalLM.from_pretrained(
nom_modele,
quantization_config=config_quant,
device_map="auto",
)
tokeniseur = AutoTokenizer.from_pretrained(nom_modele)

# Prepare le modele quantifie pour l'entrainement (cast, ajout d'entrees en gradient).
modele = prepare_model_for_kbit_training(modele)

# LoRA par-dessus.
config_lora = 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",
)
modele = get_peft_model(modele, config_lora)
modele.print_trainable_parameters()

Deux détails méritent d'être soulignés. Le paramètre bnb_4bit_compute_dtype=torch.bfloat16 sépare le stockage en 4 bits du calcul en 16 bits : quand un poids est utilisé dans un produit matriciel, il est déquantifié à la volée vers bfloat16, ce qui préserve la stabilité numérique du calcul sans coûter de mémoire durablement. Et prepare_model_for_kbit_training gère quelques réglages internes — mise en évaluation partielle, activation des entrées en gradient — qui sont fastidieux à faire à la main.

L'écart de qualité, mesuré

La question qui inquiète à raison est : combien QLoRA coûte-t-il en qualité par rapport à LoRA en pleine précision ? Le papier original mesure sur des jeux d'évaluation classiques une différence inférieure à 0,3 % sur des scores comme MMLU ou HumanEval — soit du même ordre que la variance entre deux exécutions du même entraînement avec des germes aléatoires différents. En pratique, on ne mesure pas d'écart perceptible sur nos comptes rendus de réunion.

C'est une chance : la contrainte matérielle qui obligeait à choisir un plus petit modèle a été levée. Un affinage QLoRA d'un modèle de 7 milliards sur un RTX 4090 bat presque toujours un affinage LoRA complet d'un modèle de 3 milliards sur le même matériel — la taille du modèle de base compte plus que sa précision numérique.

Les limites à connaître

QLoRA ne gagne pas partout. La vitesse d'entraînement est en général 1,3 à 1,8 fois plus lente que LoRA en bfloat16 à cause des déquantifications à la volée. Certaines architectures très récentes ne sont pas encore parfaitement prises en charge par bitsandbytes. Et surtout, QLoRA ne convient pas quand on veut ensuite fusionner l'adaptateur dans les poids : la fusion dans un modèle 4 bits abîme la qualité, il faut alors recharger le modèle en bfloat16 avant fusion (module 9).

En résumé

  • La quantification échange précision numérique contre place ; QLoRA utilise 4 bits par poids.
  • Le format NF4 épouse la distribution normale des poids et récupère la quasi-totalité de la qualité de bfloat16.
  • La double quantification compresse aussi les échelles, l'optimiseur paginé évite les débordements de pointe.
  • Un modèle de 7 milliards de paramètres s'affine sur un GPU de 24 Go avec un écart de qualité inférieur à la variance des germes aléatoires.

Module suivant : les hyperparamètres — taux d'apprentissage, époques, lot effectif, longueur de séquence.