bitsandbytes & NF4 (QLoRA)#

TL;DR — bitsandbytes est la bibliothèque qui a démocratisé la quantification des LLMs. Son type de données signature, NF4 (NormalFloat 4-bit), est théoriquement optimal pour les poids distribués selon une gaussienne. Combiné à QLoRA, il permet de fine-tuner un modèle de 65 milliards de paramètres sur un seul GPU 48 GB.


Pourquoi NF4 ? (expliqué pour un néophyte)#

Imagine que tu doives représenter une grande ville sur une toute petite carte. Si tu places tes repères (points) de façon uniforme (un point tous les 100 m), tu gaspilles de la précision dans les zones vides et tu n'en as pas assez dans le centre-ville dense.

Les poids des réseaux de neurones sont comme une ville : la plupart sont concentrés autour de zéro (le « centre-ville »), avec quelques valeurs extrêmes en périphérie. Une distribution dite normale (en cloche).

NF4 place ses 16 niveaux de quantification exactement là où les poids sont le plus probables — selon les quantiles d'une distribution normale standardisée. C'est l'équivalent de mettre plus de repères dans le centre-ville et moins dans la campagne.

xychart-beta title "Distribution des poids (gaussienne)" x-axis ["-3σ", "-2σ", "-1σ", "0", "1σ", "2σ", "3σ"] y-axis "Densité" 0 --> 1 line [0.01, 0.05, 0.2, 0.5, 0.2, 0.05, 0.01]

NF4 place ses 16 niveaux de quantification densément au centre (zone de forte densité) et de façon éparse aux extrémités.

Résultat : avec seulement 4 bits par poids (16 valeurs possibles), l'erreur de quantification est minimisée par construction.


Les trois piliers de l'écosystème bitsandbytes#

1. NF4 (NormalFloat 4-bit)#

Type de données information-theoretically optimal pour les poids distribués normalement.

Type Niveaux Allocation Optimal pour
INT4 16 Uniformes (-8 à +7) Valeurs uniformément distribuées
FP4 16 Exponent + mantissa Large plage dynamique
NF4 16 Quantiles de N(0,1) Poids de réseaux de neurones

Les 16 valeurs NF4 sont calculées comme les quantiles d'une distribution normale standardisée, de sorte que chaque niveau a la même probabilité d'être utilisé — minimisant l'entropie de l'erreur de quantification.

flowchart LR subgraph I4["INT4 (uniforme)"] I1["espacement constant
|—|—|—|—|—|—|—|—|"] end subgraph NF4_["NF4 (quantiles normaux)"] N1["dense au centre, sparse aux extrémités
|--|--|---|----|--------|----|---|--|--|"] end subgraph FP4_["FP4 (flottant)"] F1["symétrique, plage dynamique étendue
|—|—|--|------|------|--|—|—|"] end

2. Double Quantization#

Chaque groupe de 64 poids partage un scale factor (en FP32). Ces constantes prennent de la place ! La Double Quantization les quantifie à leur tour en FP8, économisant ~0,37 bits par paramètre.

Sans double quant :   64 poids × 4 bits + 1 scale × 32 bits = 288 bits / 64 = 4.50 bpw
Avec double quant :   64 poids × 4 bits + 1 scale ×  8 bits = 264 bits / 64 = 4.13 bpw
                                                    ─────
                                              économie ~8% de mémoire

3. Paged Optimizers#

Utilise la pagination NVIDIA unified memory pour gérer les pics de mémoire des optimiseurs (moments Adam stockés pour chaque paramètre). Évite les OOM (Out of Memory) pendant le fine-tuning.


QLoRA : NF4 + LoRA = Fine-tuning accessible#

QLoRA (Quantized Low-Rank Adaptation) est la méthode qui combine NF4 et LoRA pour rendre le fine-tuning de très grands modèles accessible sur du matériel consumer.

Principe#

flowchart TD M["Modèle pré-entraîné"] M --> F["Poids gelé en NF4 (4-bit)
65B params × 4 bits ≈ 33 GB
← frozen"] F --> FW["Forward pass en FP16
(déquant à la volée)"] FW --> L["LoRA Adapters (trainables)
Rang faible, FP16
~0.1-1% des params
← trained"]

Le flow :

  1. Le modèle base (gelé) est quantifié en NF4
  2. Des LoRA adapters (matrices de rang faible A et B) sont ajoutés sur chaque couche linéaire
  3. Le forward pass déquantifie les poids NF4 → FP16 à la volée
  4. Les gradients sont backpropagés à travers le modèle quantifié vers les adapters en FP16
  5. Seuls les adapters sont mis à jour — mémoire d'entraînement minime

Pourquoi ça marche si bien ?#

  • Les LoRA adapters captent les changements liés au fine-tuning en pleine précision
  • L'erreur de quantification NF4 est compensée par les adapters
  • Résultat : performance équivalente à un fine-tuning 16-bit pour une fraction du coût mémoire

Performance et résultats#

Métrique Valeur
Guanaco-65B vs ChatGPT (Vicuna bench) 99,3%
GPU requis pour fine-tuner 65B 1× 48 GB
Dégradation vs fine-tuning FP16 Aucune
Réduction mémoire (vs FP16) ~4×
xychart-beta title "Consommation mémoire (modèle 65B)" x-axis ["FP16 baseline", "NF4 (QLoRA)"] y-axis "Mémoire (GB)" 0 --> 150 bar [130, 33]

✅ NF4 (QLoRA) tient sur 1 GPU 48GB (~33 GB) contre ~130 GB en FP16.


Exemple pratique#

Installation#

pip install bitsandbytes transformers peft accelerate torch

Chargement d'un modèle en NF4 (4-bit)#

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

# Configuration de la quantification NF4
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,                    # Active la quantification 4-bit
    bnb_4bit_quant_type="nf4",            # Type NF4 (optimal pour poids)
    bnb_4bit_compute_dtype="float16",     # Calculs en FP16
    bnb_4bit_use_double_quant=True,       # Double quantization activée
)

# Chargement du modèle
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-2-70B-hf",
    quantization_config=bnb_config,
    device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-70B-hf")

print(f"Modèle chargé en NF4 — mémoire : {model.get_memory_footprint() / 1e9:.1f} GB")
# → ~38 GB au lieu de ~140 GB en FP16

Fine-tuning QLoRA complet#

from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
from transformers import TrainingArguments, Trainer

# 1. Préparer le modèle pour l'entraînement k-bit
model = prepare_model_for_kbit_training(model)

# 2. Configuration LoRA
lora_config = LoraConfig(
    r=16,                          # Rang des adapters
    lora_alpha=32,                 # Scaling factor
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

# 3. Ajouter les adapters
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# → trainable params: 13,631,488 || all params: 6,738,485,248 || trainable%: 0.20%

# 4. Fine-tuning
training_args = TrainingArguments(
    output_dir="./qlora-output",
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,
    learning_rate=2e-4,
    num_train_epochs=3,
    fp16=True,
    optim="paged_adamw_8bit",      # Paged optimizer !
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
)
trainer.train()

# 5. Sauvegarder uniquement les adapters (~50-200 MB)
model.save_pretrained("./qlora-output")

Chargement en INT8 (alternative conservatrice)#

from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "bigscience/bloom-176B",
    load_in_8bit=True,             # Quantification INT8 via LLM.int8()
    device_map="auto",
)
# BLOOM-176B tient sur un seul serveur avec 8× A100 40GB

Avantages et inconvénients#

✅ Avantages#

  • Fine-tuning de modèles géants sur un seul GPU (65B sur 48 GB)
  • Performance équivalente au 16-bit — aucune dégradation observable
  • NF4 théoriquement optimal pour les poids normalement distribués
  • Double quantization pour un surcroît de mémoire
  • Écosystème Hugging Face parfaitement intégré (load_in_4bit=True)
  • Paged optimizers évitent les OOM

⚠️ Inconvénients#

  • Léger overhead à l'inférence (déquantification NF4 → FP16 à chaque forward)
  • Conçu principalement pour le fine-tuning, pas l'inférence pure (préférer GPTQ/AWQ/GGUF pour ça)
  • Les paged optimizers nécessitent un support CUDA spécifique
  • NF4 suppose une distribution normale (pas toujours exact pour tous les modèles)
  • Pas de support natif sur CPU (CUDA obligatoire)

Cas d'usage typiques#

Cas d'usage Recommandation
Fine-tuner un 7B-70B sur 1 GPU ✅ QLoRA + NF4
Fine-tuner avec budget mémoire minimal ✅ NF4 + double quant
Inférence pure sur GPU ⚠️ Voir plutôt GPTQ/AWQ
Inférence sur CPU/edge ⚠️ Voir plutôt GGUF/llama.cpp
Quantification lossless INT8 load_in_8bit=True (LLM.int8)

Références#

ia llm quantification bitsandbytes nf4 qlora fine-tuning peft