Post-Training Quantization (PTQ)#
TL;DR#
La PTQ (Post-Training Quantization) est une famille de méthodes qui quantifient un modèle après son entraînement, sans ré-entraînement. On utilise un petit dataset de calibration (128-1024 échantillons) pour observer les distributions des activations, puis on quantifie en une seule passe. C'est rapide, simple, et suffisant pour INT8.
Pour un néophyte#
Imagine que tu as fait cuire un gâteau parfait (ton modèle entraîné). Tu dois maintenant le couper en portions individuelles (le quantifier) pour le transporter plus facilement.
- PTQ : Tu regardes la forme du gâteau, tu mesures, puis tu coupes. Rapide, mais quelques miettes perdues.
- QAT : Tu ré-entraînes ta recette pour que le gâteau soit déjà à la bonne forme. Plus long, mais zéro perte.
La PTQ, c'est la méthode rapide : on observe le modèle existant, on détermine les meilleurs paramètres de quantification, on applique. Point.
Le pipeline PTQ étape par étape#
128-1024 échantillons
Observer les distributions"] B --> C["2. DÉTERMINATION DES PARAMÈTRES
Scale factors, Zero points,
Clipping thresholds"] C --> D["3. QUANTIFICATION DES POIDS
Statique, Per-channel ou per-group"] D --> E["4. QUANTIFICATION DES ACTIVATIONS
Dynamique (runtime) ou statique"] E --> F["MODÈLE QUANTIFIÉ (INT8 / INT4)
→ Prêt pour l'inférence accélérée"]
Étape 1 : La calibration#
La calibration est le cœur de la PTQ. Son but : observer les distributions des activations pour déterminer les paramètres de quantification optimaux.
(128-1024 échantillons)"] --> B["Modèle FP16
Forward pass"] B --> C["Enregistrer les activations
de CHAQUE couche intermédiaire"] C --> D["Pour chaque couche :
• min, max observés
• Histogramme des valeurs
• Percentiles (99.9%, 99.99%)
• Détection des outliers"] D --> E["Décision des range
de quantification [α, β]"]
Combien d'échantillons de calibration ?#
| Nombre d'échantillons | Qualité | Temps de calibration |
|---|---|---|
| 32-64 | ⚠️ Basique | < 1 minute |
| 128-512 | ✅ Standard (recommandé) | Quelques minutes |
| 1024+ | ✅ Optimal | 10-30 minutes |
💡 Au-delà de ~1024 échantillons, l'amélioration est marginale. Les distributions convergent rapidement.
Étape 2 : Scale factors et quantification#
La formule de quantification#
La quantification d'une valeur réelle x (FP16/FP32) vers un entier q (INT8) se fait ainsi :
q = round(x / scale) + zero_point
où :
scale = (max_real - min_real) / (max_int - min_int)
zero_point = round(min_real / scale) [pour asymétrique]
Méthodes de détermination des range [α, β]#
| Méthode | Principe | Quand l'utiliser |
|---|---|---|
| Min-Max | α = min(activations), β = max(activations) |
Défaut, rapide, mais sensible aux outliers |
| Percentile | Coupe les X% extrêmes (ex: 99.9%) | Quand il y a des outliers modérés |
| MSE | Minimise l'erreur quadratique entre original et quantifié | Bon compromis précision/qualité |
| Entropy (KL) | Minimise la divergence KL entre distributions | Meilleur pour préserver la sémantique (TensorRT) |
Comparaison des méthodes de clipping :
- Min-Max :
[min ──────── max]→ scale trop large, précision perdue - Percentile :
[···trimmed···]→ ignore l'outlier, meilleur scale - MSE :
[···optimisé···]→ meilleur compromis erreur/scale
Clipping : gérer les bornes#
Le clipping consiste à tronquer les valeurs qui tombent en dehors du range [α, β]. Toute valeur x > β devient β, toute valeur x < α devient α.
clamped à β=3.0"] A2["x=2.5 → 2.5
inchangé"] A3["x=0.0 → 0.0
inchangé"] A4["x=-3.0 → -2.0
clamped à α=-2.0"] end
Toute valeur
x > βdevientβ, toute valeurx < αdevientα.
Variantes de quantification#
1. Symétrique vs Asymétrique#
Quantification symétrique : le range est centré autour de 0.
zero_point = round(α / scale)"] end
Quantification asymétrique : le range s'adapte à la distribution.
zero_point = round(α / scale)"] A4["✅ Utilise tous les niveaux
⚠️ Légèrement plus coûteux"] end
2. Granularité : Per-tensor / Per-channel / Per-group#
| Granularité | Description | Précision | Overhead |
|---|---|---|---|
| Per-tensor | Un seul scale pour tout le tenseur | ❌ Basse | ✅ Minimal |
| Per-channel | Un scale par canal de sortie | ✅ Bonne | ⚠️ Modéré |
| Per-group | Un scale par groupe de N poids (ex: 128) | ✅ Excellente | ❌ Élevé |
1 scale pour tout le tenseur"] end subgraph C["Per-channel"] C1["━━━━
canal 0"] C2["━━━━
canal 1"] C3["━━━━
canal 2"] C4["━━━━
canal 3"] end subgraph G["Per-group"] G1["━ g0"] G2["━ g1"] G3["━ g2"] G4["..."] G5["━ gn"] end
3. Quantification statique vs dynamique#
| Type | Quand sont calculés les scales des activations ? | Usage |
|---|---|---|
| Statique | Pendant la calibration (avant l'inférence) | INT8 complet (W8A8) |
| Dynamique | À chaque forward pass (au runtime) | Weight-only quant (ex: INT4 poids + FP16 activations) |
Quantification uniforme vs non-uniforme#
- Uniforme : les niveaux de quantification sont également espacés (INT8 standard). Simple, accéléré par le hardware.
- Non-uniforme : les niveaux sont concentrés là où il y a le plus de valeurs (ex: NF4 de bitsandbytes). Meilleure précision à bas bitrate, mais pas toujours accéléré.
également espacés"] end subgraph N["Non-uniforme (NF4)"] N1["| || | | | |
concentrés au centre
(densité normale)"] end
Bits cibles#
- INT8 (W8A8) — configuration standard, quasi sans perte
- INT4 (W4A16) — configuration agressive, perte légère mais acceptable
- FP16 → INT8 ou FP8 — conversion pour accélération hardware
Avantages#
- ✅ Rapide : minutes à heures, pas de ré-entraînement
- ✅ Simple à mettre en œuvre — quelques lignes de code
- ✅ Pas besoin de données d'entraînement (juste calibration)
- ✅ Compatible avec la plupart des frameworks (PyTorch, TF Lite, ONNX)
- ✅ INT8 quasi sans perte de précision
- ✅ Écosystème mature (bitsandbytes, AutoGPTQ, AutoAWQ, TensorRT)
Inconvénients#
- ❌ Dégradation de précision plus importante qu'avec QAT, surtout à ≤ 4 bits
- ❌ Sensibilité aux outliers dans les activations (problème principal des LLMs)
- ❌ Pas de récupération des erreurs de quantification (pas de backprop)
- ❌ Le choix de la méthode de calibration et de la granularité influence fortement la qualité
Exemple pratique#
Avec bitsandbytes (INT8 — le plus simple)#
from transformers import AutoModelForCausalLM
# PTQ INT8 en une seule ligne
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b",
load_in_8bit=True, # ← PTQ immédiate
device_map="auto",
)
# Le modèle est quantifié au chargement, prêt à l'emploi
Avec AutoGPTQ (INT4 — weight-only quantization)#
from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
# 1. Charger le modèle original
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b")
model = AutoGPTQForCausalLM.from_pretrained("meta-llama/Llama-2-7b")
# 2. Préparer les données de calibration
calibration_texts = [
"Exemple de texte pour la calibration...",
# ... 128-512 échantillons
]
# 3. Configuration de quantification
quantize_config = BaseQuantizeConfig(
bits=4, # INT4
group_size=128, # per-group
desc_act=False, # activation order
)
# 4. Quantification PTQ
model.quantize(calibration_texts, quantize_config)
# 5. Sauvegarder le modèle quantifié
model.save_quantized("./llama-2-7b-gptq-4bit")
Avec PyTorch natif (PTQ statique INT8)#
import torch
import torch.ao.quantization as quant
# 1. Préparer le modèle
model_fp32 = MyModel().eval()
model_fp32.qconfig = quant.get_default_qconfig('fbgemm')
# 2. Insertion des observateurs (pour la calibration)
model_prepared = quant.prepare(model_fp32)
# 3. Calibration : passer le dataset de calibration
with torch.no_grad():
for batch in calibration_dataloader:
model_prepared(batch)
# → Les observateurs ont enregistré les min/max/percentiles
# 4. Convertir : les observateurs deviennent des nœuds de quantification
model_int8 = quant.convert(model_prepared)
# 5. Inférence avec le modèle quantifié
output = model_int8(test_input)
Avec TensorRT (calibration entropique)#
import tensorrt as trt
# Le calibrator TensorRT utilise la méthode KL-divergence
class MyCalibrator(trt.IInt8EntropyCalibrator2):
def __init__(self, calibration_data):
super().__init__()
self.data = calibration_data
def get_batch(self, names):
# Retourner le prochain batch de calibration
batch = next(self.data_iterator, None)
if batch is None:
return None
return [batch.ptr]
def read_calibration_cache(self):
# Optionnel : cache pour éviter de recalibrer
...
def write_calibration_cache(self, cache):
...
Papier arXiv source#
La PTQ est une famille de méthodes plutôt qu'un seul papier. Les références fondamentales :
- 📄 A Survey of Quantization Methods for Efficient Neural Network Inference (arXiv:2103.13630) — Survey exhaustif
- 📄 GPTQ (arXiv:2210.17323) — PTQ avancée avec Hessienne
- 📄 SmoothQuant (arXiv:2211.10438) — PTQ avec migration d'outliers
Références#
- 📄 Survey on Quantization (arXiv:2103.13630) — Vue d'ensemble académique
- 📦 bitsandbytes — PTQ INT8/INT4 plug-and-play
- 📦 AutoGPTQ — PTQ INT4 avec Hessienne
- 📦 AutoAWQ — PTQ activation-aware
- 📦 PyTorch Quantization — PTQ natif
- 📦 TensorRT Developer Guide — INT8 calibration
- 🔗 Voir aussi : [[qat]] — Quantization Aware Training (alternative avec entraînement)
- 🔗 Voir aussi : [[llm-int8]] — LLM.int8() (PTQ INT8 avec gestion des outliers)
- 🔗 Voir aussi : [[index]] — Vue d'ensemble des méthodes de quantification