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 :

flowchart TD W["Matrice de poids W (FP16)"] --> Q1["Quantification initiale (ex: GPTQ)"] Q1 --> ANALYSE ANALYSE["Analyse d'erreur (sensibilité)
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 :

flowchart TD subgraph VERT["Sensibilité verticale (par canal / colonne)"] VD["Colonne j = [w₁ⱼ, w₂ⱼ, w₃ⱼ, ...]
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#

flowchart LR subgraph C1["Cas 1: Outlier individuel"] C1A["Q Q Q ⚡ Q Q Q
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#

flowchart TD subgraph S1["1. Quantification initiale"] S1A["Modèle FP16 + données calib."] --> S1B["GPTQ (ou autre PTQ)
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 ligne

Avantage : ~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#

mindmap root((Raisons pour garder
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#

xychart-beta title "Perplexité WikiText2 — LLaMA-7B (plus bas = mieux)" x-axis ["FP16", "GPTQ 4b", "AWQ 4b", "SpQR 4b", "SpQR 3b"] y-axis "Perplexité" 5.5 --> 6 bar [5.68, 5.71, 5.70, 5.69, 5.78]
xychart-beta title "Perplexité WikiText2 — LLaMA-33B & 65B (plus bas = mieux)" x-axis ["33B FP16", "33B SpQR 3b", "65B FP16", "65B SpQR 3b"] y-axis "Perplexité" 3 --> 5 bar [4.10, 4.14, 3.53, 3.57]

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 2.3×

SpQR vs SqueezeLLM : deux approches sparse#

flowchart LR subgraph SPQR["SpQR"] SP1["• Sensibilité double sens
(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#

ia llm quantification spqr sparse dense outlier ptq int3 int4 near-lossless