SpQR (Sparse-Quantized Representation)#
SpQR est une méthode de quantification PTQ qui identifie et isole les poids outliers — ceux qui causent les plus grandes erreurs de quantification — pour les stocker en haute précision (16-bit) dans un format sparse, tandis que la grande majorité des poids sont compressés à 3-4 bits. Le résultat : une compression 4×+ avec une perte de perplexité < 1%, qualifiée de near-lossless.
Le problème fondamental (expliqué pour un néophyte)#
Quand on quantifie un modèle de 16 bits vers 3-4 bits, la plupart des poids sont bien approximés. Mais une poignée de poids rebelles — les outliers — ont un impact disproportionné sur la qualité du modèle. Les compresser comme les autres dégrade gravement les performances.
Imagine un tableau de maître numérisé en 4 couleurs (quantification). La plupart des zones sont bien représentées en 4 couleurs. Mais certains détails fins (les « outliers ») sont détruits.
La solution SpQR : garder ces détails en haute résolution (16-bit), compresser tout le reste en 3-4 bit. Résultat : image quasi identique, fichier 4× plus petit.
La sensibilité par poids : pourquoi certains détruisent tout#
Qu'est-ce qu'un poids outlier ?#
Dans une matrice de poids de LLM, la distribution est généralement concentrée autour de zéro (distribution gaussienne). Mais certains poids ont des magnitudes anormalement élevées ou des positions structurelles critiques :
Distribution typique des poids : concentration gaussienne autour de zéro, avec 0.01% des poids à magnitude très élevée (outliers) — impact énorme sur la sortie malgré leur nombre négligeable.
L'erreur de quantification disproportionnée#
| Poids | Quantifié | Erreur | Impact |
|---|---|---|---|
| 0.03 | 0.00 | 0.03 | négligeable ✓ |
| 0.12 | 0.10 | 0.02 | négligeable ✓ |
| 0.08 | 0.10 | 0.02 | négligeable ✓ |
| ... | ... | ... | ... |
| 2.50 | 2.00 | 0.50 | ÉNORME ! ✗ |
| -1.80 | -2.00 | 0.20 | ÉNORME ! ✗ |
L'erreur absolue est similaire (0.02 vs 0.50), mais l'impact sur la sortie est amplifié par la magnitude du poids ET par l'activation qui le traverse.
La solution SpQR : décomposition sparse + dense#
SpQR sépare la matrice de poids en deux composantes :
Pour chaque poids wᵢⱼ : errorᵢⱼ = |wᵢⱼ - Quant(wᵢⱼ)|
Si errorᵢⱼ > seuil → OUTLIER"] --> SPLIT SPLIT{"Outlier ?"} SPLIT -- "Oui (~1%)" --> SPARSE SPLIT -- "Non (~99%)" --> DENSE SPARSE["SPARSE
Outliers en 16-bit
Stockage CSR/COO"] DENSE["DENSE
Poids normaux en 3-4 bit
Quantification standard"]
Diagramme de la matrice sparse vs dense#
Matrice de poids originale (toutes les valeurs en FP16) — les outliers (⚡) sont isolés dans des positions spécifiques (ex: (0,3)=2.50, (2,1)=-1.80, (3,6)=0.90).
Après décomposition SpQR :
- Partie dense (3-4 bit) : poids quantifiés (Q), positions « trous » (·) aux emplacements des outliers
- Partie sparse (16-bit) : format CSR compact — (index, value) — ~1% des entrées, précision FP16 préservée
La sensibilité double sens de SpQR#
SpQR ne se contente pas d'identifier les outliers par magnitude. Il analyse les corrélations d'erreur dans deux directions :
Si toute la colonne a des erreurs élevées
→ le CANAL entier est sensible → extraire comme outlier
→ Capture les 'emergent outlier features'
(comme LLM.int8(), mais au niveau des poids)"] end subgraph HORIZ["Sensibilité horizontale (par ligne / neurone)"] HD["Ligne i = [wᵢ₁, wᵢ₂, wᵢ₃, ...]
Si une ligne a une concentration d'erreurs
→ le NEURONE de sortie est sensible → extraire les poids concernés
→ Capture les corrélations au sein d'un même neurone"] end BOTH["Combinaison: un poids est outlier si
SA propre erreur est élevée ET/OU
son canal OU sa ligne ont des erreurs systématiques"] VERT --> BOTH HORIZ --> BOTH
Pourquoi cette double analyse est nécessaire#
Q Q Q Q Q Q Q
Q Q Q Q Q Q Q"] --> C1B["Détecté par l'erreur
du poids lui-même"] end subgraph C2["Cas 2: Canal entier sensible"] C2A["Q Q Q ⚡ Q Q Q
Q Q Q ⚡ Q Q Q
Q Q Q ⚡ Q Q Q"] --> C2B["Détecté par
corrélation verticale"] end subgraph C3["Cas 3: Neurone sensible"] C3A["⚡ ⚡ ⚡ ⚡ ⚡ ⚡ ⚡
Q Q Q Q Q Q Q
Q Q Q Q Q Q Q"] --> C3B["Détecté par
corrélation horizontale"] end
Niveau technique : le pipeline complet#
Poids quantifiés 3-4 bit"] end subgraph S2["2. Analyse d'erreur individuelle"] S2A["Pour chaque poids wᵢⱼ :
eᵢⱼ = |wᵢⱼ - Q(wᵢⱼ)| × |xⱼ|
(erreur pondérée par l'activation)
Identifier les poids où eᵢⱼ > τ (seuil)"] end subgraph S3["3. Analyse de corrélation double sens"] S3A["Vertical: variance(eᵢⱼ) pour chaque colonne j
Horizontal: variance(eᵢⱼ) pour chaque ligne i
Marquer canaux/lignes sensibles comme outliers"] end subgraph S4["4. Extraction sparse"] S4A["W_sparse = {wᵢⱼ | outlier}
Stockage: format CSR compressé
Précision: 16-bit (FP16)"] end subgraph S5["5. Re-quantification dense"] S5A["Poids restants (99%) re-quantifiés
en 3-4 bit avec group-size optimisé
(tenant compte des trous des outliers)"] end subgraph S6["6. Inférence"] S6A["Activation X (FP16)"] --> S6B["GEMM dense (3-4 bit)"] S6A --> S6C["GEMM sparse (16-bit)"] S6B --> S6D["Y = Y_dense + Y_sparse"] S6C --> S6D end S1 --> S2 --> S3 --> S4 --> S5 --> S6
Le format de stockage sparse#
Format CSR (Compressed Sparse Row) pour les outliers :
-values = [2.50, -1.80, 0.90, ...]— valeurs FP16
-col_idx = [3, 1, 6, ...]— index colonne
-row_ptr = [0, 1, 1, 2, 3, ...]— pointeurs ligneAvantage : ~1% des entrées × 16-bit + overhead index ≈ 0.5-1 bit effectif supplémentaire par poids → Total : ~4-5 bpw pour near-lossless !
Pourquoi certains poids restent en haute précision#
certains poids en 16-bit)) Erreur disproportionnée Outlier en 3-bit: erreur 0.5+ En 16-bit: erreur ≈ 0 Corrélations structurelles Certains canaux systématiquement sensibles Émergence de features dominantes (cf. LLM.int8()) Toute la colonne doit être préservée Coût marginal faible ~1% des poids en 16-bit N'ajoute que ~0.5 bpw Bénéfice qualité énorme Format sparse efficace Stockage CSR compresse les zéros implicites Pas de gaspillage de mémoire
Caractéristiques#
| Propriété | Valeur |
|---|---|
| Type | Post-Training Quantization (PTQ) |
| Précisions | 3-4 bit (dense) + 16-bit (sparse outliers) |
| Outliers | ~1% des poids en FP16 (format sparse) |
| Décomposition | Sparse + Dense |
| Sensibilité | Double sens (vertical + horizontal) |
| Loss de perplexité | < 1% (near-lossless) |
| Compression | 4×+ vs FP16 |
| Speedup | +15% vs FP16 (inférence plus rapide) |
| Mixed-precision runtime | ✅ Oui (GEMM dense + GEMM sparse) |
Résultats de perplexité#
LLaMA — comparaison SpQR vs autres méthodes#
SpQR 3-bit : perte < 1% en perplexité — near-lossless sur tous les modèles testés.
Tableau récapitulatif#
| Modèle | Config SpQR | PPL WikiText2 | PPL FP16 (ref) | Perte |
|---|---|---|---|---|
| LLaMA-7B | 3-bit | 5.78 | 5.68 | +1.8% |
| LLaMA-7B | 4-bit | 5.69 | 5.68 | +0.2% |
| LLaMA-13B | 3-bit | 4.98 | 4.89 | +1.8% |
| LLaMA-33B | 3-bit | 4.14 | 4.10 | +1.0% |
| LLaMA-65B | 3-bit | 3.57 | 3.53 | +1.1% |
| Falcon-7B | 3-bit | ~1.02× ref | — | < 1% |
Highlight : LLaMA-33B en SpQR 3-bit tient sur un seul GPU 24GB consumer (RTX 3090/4090) avec near-lossless quality.
Comparaison avec les méthodes alternatives#
| Critère | SpQR | GPTQ | AWQ | SqueezeLLM |
|---|---|---|---|---|
| Approche | Sparse outliers + dense | Compensation Hessienne | Scaling saillant | Non-uniform + sparse |
| Near-lossless | ⭐ Oui (< 1%) | Non | Non | Proche |
| 3-bit viable | ⭐ Excellent | Dégradé | Dégradé | ⭐ Excellent |
| Complexité format | Sparse + dense | Simple | Simple | Sparse + dense |
| Kernels custom | Requis | ExLlama/Marlin | Marlin | Requis |
| Support écosystème | Limité | ⭐ Très large | ⭐ Très large | Limité |
| Inférence > FP16 | ⭐ Oui (+15%) | Variable | 3× | 2.3× |
SpQR vs SqueezeLLM : deux approches sparse#
(vert + horiz)"] SP2["• Outliers isolés"] SP3["• Re-quantif GPTQ"] SP4["• 3-4 bit standard"] SP5["• Inférence > FP16"] end subgraph SQLM["SqueezeLLM"] SQ1["• Quantif non-uniform
basée sur Hessian"] SQ2["• Outliers = sensibilité"] SQ3["• Dense-sparse
decomposition"] SQ4["• 3-bit lossless"] end
Les deux isolent les outliers, mais la détection et la quantification dense diffèrent.
Niveau néophyte : l'analogie complète#
Imagine une bibliothèque de 10 000 livres. SpQR découvre que 100 livres (1%) sont des éditions rares et précieuses qui ne supportent aucune dégradation. Au lieu de tout numériser en basse résolution (3-4 bit), SpQR :
1. Photographie ces 100 livres en ultra-haute définition (16-bit, format sparse)
2. Numérise les 9 900 autres en basse résolution (3-4 bit, format dense)Le résultat : une bibliothèque numérique 4× plus petite qu'une version pleine résolution, mais indiscernable de l'original. Et paradoxalement, l'accès est plus rapide car moins de données à charger en mémoire.
Exemple pratique#
Installation#
git clone https://github.com/Vahe1994/SpQR.git
cd SpQR
pip install -e .
Quantification d'un modèle#
from transformers import AutoTokenizer
from spqr_quant import SpQRQuantizer
# 1. Configuration
model_path = "meta-llama/Llama-2-7B-hf"
output_path = "./llama-7b-spqr-3bit"
quant_config = {
"wbits": 3, # 3-bit pour la partie dense
"groupsize": 16, # group-size pour la quantification
"outlier_threshold": 0.85, # seuil de sensibilité pour outliers
"sparse_format": "csr", # format de stockage sparse
}
# 2. Charger le modèle et tokenizer
tokenizer = AutoTokenizer.from_pretrained(model_path)
# 3. Quantifier avec SpQR
quantizer = SpQRQuantizer(
model_path=model_path,
quant_config=quant_config,
calibration_data="pile", # dataset de calibration
num_samples=128,
)
quantizer.quantize()
quantizer.save(output_path)
print(f"Modèle quantifié sauvegardé dans {output_path}")
Inférence#
from spqr_quant import SpQRModelForCausalLM
from transformers import AutoTokenizer
# Charger le modèle SpQR quantifié
model = SpQRModelForCausalLM.from_quantized("./llama-7b-spqr-3bit")
tokenizer = AutoTokenizer.from_pretrained("./llama-7b-spqr-3bit")
# Génération
inputs = tokenizer("Explique la théorie de la relativité :", return_tensors="pt")
output = model.generate(**inputs, max_new_tokens=200)
print(tokenizer.decode(output[0]))
Comparaison mémoire#
# Taille des modèles (LLaMA-33B)
ls -lh ./models/
# FP16 original : ~66 GB (impossible sur un GPU consumer)
# SpQR 3-bit : ~15 GB ← tient sur RTX 3090/4090 (24 GB) !
# SpQR 4-bit : ~18 GB ← aussi jouable
# GPTQ 3-bit : ~13 GB (mais perte de qualité plus élevée)
Avantages et inconvénients#
✅ Avantages#
- Near-lossless : perte de perplexité < 1%, quasi indiscernable du FP16
- Compression 4×+ vs FP16 → modèles 33B sur GPU consumer 24GB
- Inférence plus rapide que FP16 (+15% speedup)
- Sensibilité double sens capture plus d'outliers que les approches naïves
- Format sparse efficace → coût marginal des outliers minimal
⚠️ Inconvénients#
- Format complexe : nécessite de gérer dense + sparse simultanément
- Kernels GPU custom requis pour l'inférence (pas de support natif vLLM/TGI)
- Overhead de gestion du format sparse à l'inférence
- Support limité dans les frameworks mainstream (vs GPTQ/AWQ très intégrés)
- Temps de quantification modéré (analyse de sensibilité + re-quantification)
Références#
- Papier arXiv : SpQR: A Sparse-Quantized Representation for Near-Lossless LLM Weight Compression — Dettmers et al., ICLR 2024
- Code officiel : github.com/Vahe1994/SpQR
- Voir aussi : GPTQ · AWQ · LLM.int8() · SmoothQuant · Index quantification