ParoQuant (Pairwise Rotation Quantization)#
ParoQuant est une méthode PTQ weight-only (INT4, groupes de 128 canaux) qui applique avant quantification une transformation dite scaled pairwise rotation : un empilement de petites rotations par paires de canaux, apprises couche par couche. Publiée par Z Lab / NVIDIA / MIT (ICLR 2026), elle atteint la meilleure précision des méthodes de quantification linéaires — devançant AWQ, EfficientQAT, SpinQuant et OmniQuant, et talonnant les méthodes vectorielles (QTIP, QuIP#) — tout en restant quasi aussi rapide qu'AWQ (< 10 % d'overhead runtime). Son terrain de jeu privilégié : les LLM de raisonnement, où l'erreur de quantification s'accumule le long des longues chaînes de pensée.
Fiche papier#
| Titre | ParoQuant: Pairwise Rotation Quantization for Efficient Reasoning LLM Inference |
| Auteurs | Yesheng Liang (algorithme), Haisheng Chen (système), Zihan Zhang, Song Han, Zhijian Liu |
| Affiliations | NVIDIA, MIT, UC San Diego (Z Lab) |
| arXiv | 2511.10645 (v1 : 13 nov. 2025) |
| Venue | ICLR 2026 (accepté) |
| GitHub | z-lab/paroquant — PyPI : pip install paroquant |
| Blog / projet | z-lab.ai/projects/paroquant — vidéo : youtu.be/fISG4CkizLM |
| Modèles HF | Collection z-lab/paroquant (checkpoints suffixe -PARO) |
| Claim central | +2,4 % de précision moyenne vs AWQ sur tâches de raisonnement, < 10 % d'overhead runtime |
Relation avec QuaRot#
ParoQuant appartient à la même famille que QuaRot — la quantification par rotation : les deux partent du constat que quelques canaux « outliers » cassent la quantification INT4, et appliquent une rotation orthogonale avant quantification pour uniformiser les magnitudes. La différence clé :
| QuaRot (2024) | ParoQuant (2025) | |
|---|---|---|
| Rotation | Hadamard fixe (non apprise) | Rotations de Givens par paires apprises |
| Cible | W4A4K4 (poids + activations + KV cache) | W4 weight-only, focus LLM de raisonnement |
| Coût | Quasi gratuit, sans calibration | Optimisation 2 stages (~9 h / 8B sur H200) |
| Vitesse | — | Proche d'AWQ, kernel CUDA fusionné unique |
ParoQuant remplace la matrice de Hadamard générique par des rotations adaptées à chaque couche, tout en restant aussi rapide qu'une méthode linéaire classique.
Le problème : les outliers et la redondance des rotations#
Pour un néophyte#
Un LLM contient quelques valeurs anormalement grandes (« outliers ») qui gâchent la quantification : comme quelques cris dans une conversation calme, elles forcent tout le monde à « parler fort » et on perd la précision. Les méthodes par rotation font pivoter le modèle dans un espace mathématique équivalent où ces valeurs extrêmes sont réparties uniformément — le modèle dit exactement la même chose, mais devient facile à compresser en 4 bits.
L'intuition propre à ParoQuant : une rotation complète qui mélange tous les canaux entre eux est massivement redondante. Optimiser seulement le top 10 % des paires de canaux (celles dont les magnitudes diffèrent le plus) suffit à obtenir la même réduction d'erreur qu'une rotation complète — à une fraction du coût.
Le mécanisme en trois étapes#
-
Rotations de Givens : au lieu d'une matrice orthogonale n×n complète, on utilise un petit ensemble de rotations par paires (2 canaux) G(i, j, θ) — chacune n'est qu'un simple multiply-add vectorisable.
-
Rotations indépendantes : chaque canal apparaît dans au plus une paire par ensemble de rotations → aucune dépendance, parallélisme GPU total (token / groupe de canaux / paire), le tout fusionné dans un seul kernel CUDA.
-
Empilement + scaling : on chaîne K = 8 ensembles de rotations indépendantes (jusqu'à 64 paires chacun par groupe de 128 canaux) + un scaling par canal diag(α). Le scaling uniformise les magnitudes entre canaux ; les rotations alignent les valeurs au sein des paires (elles se regroupent autour de la diagonale x = y).
La transformation finale, appliquée par groupe de 128 canaux :
T(W) = (∏ R(P_t, Θ_t)) · diag(α) · W, t = 1..8
Optimisation en deux stages#
Minimisation couche par couche de ‖Q(D)(X′) − D(X)‖ :
| Stage | Ce qui est optimisé | Réglages |
|---|---|---|
| 1 | Angles de rotation θ + scaling α | AdamW, lr 0,05 |
| 2 | Fine-tune QAT-like des poids + paramètres s, z (style EfficientQAT) | lr 1e-5 / 1e-6 |
- Calibration : 2048 échantillons (WikiText2 + C4 + RedPajama), 10 époques par stage.
- Coût : ~9 h sur H200 pour Llama-3-8B (vs AWQ : minutes ; EfficientQAT : ~3 h ; QTIP : ~20 h).
- Robuste au sous-échantillonnage : 128 échantillons de calibration suffisent presque (PPL 7,30 vs 7,27).
À l'inférence#
Les rotations inverses sont appliquées aux activations en ligne dans un unique kernel CUDA fusionné : groupe de 128 canaux en shared memory, paramètres de rotation dans les registres → < 10 % d'overhead vs AWQ. Sur Apple Silicon, un backend MLX avec kernels Metal remplit le même rôle.
Bits et formats#
- W4A16 (principal) : INT4 weight-only, quantification linéaire block-wise, groupes de 128 canaux (taille optimale selon les scaling laws de Dettmers).
- W4A4 (extension) : avec compensation d'erreur de type GPTQ sur les poids — matche FlatQuant et bat SpinQuant (INT4) ; la variante MXFP4 bat MR-GPTQ.
- Choix d'une quantification linéaire (et non vectorielle) délibéré, pour la compatibilité frameworks (vLLM, Transformers) — la méthode reste extensible à la vector quantization.
- Paramètres de rotation : 8 rotations indépendantes × jusqu'à 64 paires par groupe de 128 canaux.
- Pas de résultats publiés sous 4 bits.
Avantages / inconvénients#
Avantages ✅#
- Précision SOTA de la quantification linéaire : +2,4 % en moyenne vs AWQ sur tâches de raisonnement, +6,3 % vs EfficientQAT, +0,9 % vs QTIP.
- Vitesse proche d'AWQ : < 10 % d'overhead ; 15–30 % plus rapide que QTIP (~25 % plus rapide à précision égale).
- Spécialisé raisonnement : seulement ~10 % de dégradation moyenne vs FP16 ; l'erreur résiste à l'accumulation sur les longues chaînes de pensée (AIME-24/25 préservés).
- Robuste en calibration : 128 échantillons presque aussi bons que 2048.
- Kernel unique, hardware-friendly : fusion totale, parallélisme à 3 niveaux (token, groupe, paire).
Inconvénients ❌#
- Temps de quantification : ~9 h sur H200 pour un 8B (AWQ : minutes) — le coût de l'optimisation en 2 stages.
- ~10 % plus lent qu'AWQ pur en décodage (throughput 2,1× vs 2,3× sur Qwen3-4B).
- Pas de compensation de second ordre à la GPTQ dans le chemin W4A16 (réservée au W4A4).
- Écosystème jeune : pas de support llama.cpp / GGUF — kernels custom requis (vLLM ≥ 0.10.1, Transformers, MLX).
- Pas de résultats < 4 bits publiés.
- Weight-only : pas d'accélération compute des activations (contrairement au W4A4 de QuaRot).
Benchmarks#
Perplexité WikiText-2 / C4 (4-bit, groupes de 128) — Table 5.1 du papier#
| Méthode | Type | L3-8B W2 | L3-70B W2 | L2-7B W2 | Q3-1.7B W2 | Q3-4B W2 | Q3-8B W2 | Q3-14B W2 | L3-8B C4 |
|---|---|---|---|---|---|---|---|---|---|
| FP16 | – | 5,54 | 2,56 | 5,12 | 8,32 | 7,01 | 6,24 | 5,70 | 7,10 |
| QuIP# | vector | 5,81 | 2,99 | 5,19 | – | – | – | – | 7,32 |
| QTIP | vector | 5,69 | 2,75 | 5,17 | 8,46 | 7,09 | 6,28 | 5,75 | 7,22 |
| AWQ | linear | 5,92 | 2,96 | 5,23 | 8,80 | 7,36 | 6,45 | 5,85 | 7,42 |
| SpinQuant | linear | 5,83 | – | 5,21 | – | – | – | – | 7,41 |
| EfficientQAT | linear | 5,87 | 3,33 | 5,22 | 8,60 | 7,19 | 6,37 | 5,82 | 7,36 |
| ParoQuant | linear | 5,73 | 2,82 | 5,17 | 8,44 | 7,10 | 6,29 | 5,75 | 7,27 |
(C4 ParoQuant : L3-70B 5,86 · L2-7B 6,73 · Q3-1.7B 8,74 · Q3-4B 7,70 · Q3-8B 7,04 · Q3-14B 6,59)
→ Meilleure PPL de toutes les méthodes linéaires, proche de QTIP (vector), à ~92 % de la vitesse AWQ. Bat AWQ de 0,15–0,36 PPL W2 sur tous les modèles.
⚠️ Pas de comparaison directe vs GPTQ Q4, GGUF Q4_K_M ou Q6_K dans le papier ni dans les évaluations tierces trouvées. Les baselines sont AWQ, EfficientQAT, QTIP, QuIP#, OmniQuant, SpinQuant. Contexte indirect : AWQ surpasse généralement GPTQ en 4-bit ; les k-quants GGUF sont une famille distincte sans face-à-face publié.
Raisonnement — Qwen3-4B 4-bit (accuracy %)#
| Méthode | MMLU-Pro | GPQA | AIME24 | AIME25 | Moy. | Speedup |
|---|---|---|---|---|---|---|
| FP16 | 71,0 | 50,0 | 75,6 | 62,2 | 64,7 | 1,0× |
| QTIP | 69,7 | 55,2 | 67,8 | 58,9 | 62,9 | 1,5× |
| AWQ | 68,2 | 52,2 | 62,2 | 53,3 | 59,0 | 2,3× |
| EfficientQAT | 67,5 | 49,8 | 45,6 | 44,4 | 51,8 | 2,3× |
| ParoQuant | 70,1 | 53,7 | 73,3 | 63,3 | 65,1 | 2,1× |
À noter : AIME25 ParoQuant (63,3) > FP16 (62,2). Sur les tâches non-raisonnement (BoolQ/ARC/HSwag), ParoQuant ≈ FP16 (69,9 vs 70,1).
Throughput (tokens/s, batch 1, RTX A6000)#
| Q3-1.7B | Q3-4B | L3-8B | Q3-14B | L3-70B | |
|---|---|---|---|---|---|
| FP16 | 170 | 78 | 45 | 25 | OOM |
| AWQ | 320 | 176 | 120 | 70 | 17 |
| QTIP | 209 | 117 | 95 | 55 | 15 |
| ParoQuant | 278 | 160 | 112 | 65 | 16 |
Ablations (Llama-3-8B, PPL C4)#
RTN 7,56 → scaling seul 7,40 → 8 rotations seules 7,50 → rotations + scaling 7,35 → + fine-tune stage 2 7,27. Optimum à 8 rotations ; 128 échantillons de calibration → 7,30. Scaling et rotations sont complémentaires.
Outils et implémentations#
| Élément | Détail |
|---|---|
| GitHub | z-lab/paroquant — reproductible sur la branche legacy ; branche main = dev actif |
| PyPI | pip install "paroquant[vllm]" (CUDA 12.9/13.0) ou "paroquant[mlx]" (Apple Silicon) |
| Serving | vllm serve $MODEL, python -m paroquant.cli.serve (API OpenAI-compatible), .cli.chat, .cli.agent (tool calling) |
| Apple Silicon | Runtime oMLX / backend MLX (kernels Metal) |
| Docker | ghcr.io/z-lab/paroquant:{chat,serve,latest,eval,chat-cu129} |
| Modèles pré-quantifiés HF | Qwen3 (0.6B–14B), Qwen3.5 (0.8B–35B-A3B), Qwen3.6 (27B, 35B-A3B), Gemma 4 (E2B–31B), Llama-2-7B, Llama-3-8B, Llama-3.1-8B-Instruct — 43 modèles citent le papier |
| llama.cpp / GGUF | ❌ Non supporté (pas d'équivalent k-quants ; demande ouverte : mlx-lm issue #977) |
Exemple pratique#
# Utiliser un modèle déjà quantifié (NVIDIA, CUDA 12.9/13.0)
pip install "paroquant[vllm]"
vllm serve z-lab/Qwen3-4B-PARO --port 8000
# ou chat direct :
python -m paroquant.cli.chat --model z-lab/Qwen3-4B-PARO
# ou API OpenAI-compatible :
python -m paroquant.cli.serve --model z-lab/Qwen3-4B-PARO --port 8000
# Quantifier son propre modèle
git clone https://github.com/z-lab/paroquant && cd paroquant
pip install -e ".[optim,eval]"
experiments/optimize/4bit.sh Qwen/Qwen3-8B # ~9h sur H200
python -m paroquant.cli.convert \
--model Qwen/Qwen3-8B \
--result-dir output/Qwen3-8B \
--output-path models/Qwen3-8B-PARO # --mode real (INT4) / pseudo (FP16)
Quand choisir ParoQuant ?#
| Situation | Verdict |
|---|---|
| LLM de raisonnement (AIME, GPQA, longues CoT) en 4-bit sur GPU | ✅ Premier choix — la dégradation y est la plus faible |
| Serving vLLM / Apple Silicon avec checkpoints prêts | ✅ Bon fit (modèles -PARO sur HF) |
| Inférence CPU / llama.cpp / GGUF | ❌ Non supporté → GGUF Q4_K_M |
| Quantification rapide d'un modèle one-shot | ❌ ~9 h d'optimisation → AWQ ou GPTQ |
| Compression extrême ≤ 3 bits | ❌ Pas de résultats publiés → QuIP# ou AQLM |
Conversion MLX (Apple Silicon)#
Principe : pas de commande d'export séparée#
Il n'existe aucune commande convert --mlx : le checkpoint exporté par paroquant.cli.convert --mode real (format HuggingFace, packing AWQ-compatible) est directement consommable par le backend MLX. La conversion vers le format MLX est effectuée automatiquement au chargement par paroquant/inference/backends/mlx/load.py, qui :
- détecte le format d'entrée — PARO natif (clés
.qlinear.qweight) ou AutoAWQ (clés.qweight) ; - décompresse les poids int32 AWQ → uint32 compact MLX (
_convert_autoawq()), ou int16 nibbles → uint32 (_convert_paro_native()) ; - remappe les paramètres de rotation (
theta,pairs,channel_scales) vers un espace de noms plat par couche ; - remplace les
nn.Linearpar desRotateQuantizedLinear(rotation Givens via kernel Metal +mx.quantized_matmul4-bit).
Installation (macOS)#
pip install "paroquant[mlx]" # tire mlx, mlx-lm, mlx-vlm
xcode-select --install # requis : le kernel Metal est compilé JIT à l'exécution
Utiliser un checkpoint sur Apple Silicon#
# Backend auto-détecté (→ mlx sur Mac) :
python -m paroquant.cli.chat --model z-lab/Qwen3.5-4B-PARO
python -m paroquant.cli.serve --model z-lab/Qwen3.5-4B-PARO --port 8000
# Ou forcer explicitement le backend MLX (chat / benchmark / evaluate) :
python -m paroquant.cli.chat --model z-lab/Qwen3.5-4B-PARO --backend mlx
# Serve : --vlm pour charger les composants vision, --llm-only pour texte seul (défaut)
Quantifier son propre modèle puis l'exécuter en MLX#
⚠️ L'optimisation et le convert exigent CUDA (RuntimeError: CUDA required.) — le pipeline complet est donc bi-machine :
# 1. Sur GPU NVIDIA : optimisation (~9 h / 8B sur H200) + export HF
experiments/optimize/4bit.sh Qwen/Qwen3-8B
python -m paroquant.cli.convert \
--model Qwen/Qwen3-8B --result-dir output/Qwen3-8B \
--output-path models/Qwen3-8B-PARO --mode real
# 2. Sur Mac : charger directement le checkpoint exporté — la conversion
# MLX (dépacking AWQ → uint32, rotations) se fait au vol au chargement
python -m paroquant.cli.chat --model models/Qwen3-8B-PARO
Notes techniques#
- Le kernel Metal (rotation par paires) est JIT-compilé via
mx.fast.metal_kernel()avec deux tailles de tuiles : 1 (prefill) et 4 (décodage batch). - MoE : les poids par expert sont empilés en un tenseur unique (
RotateSwitchGLU), les rotations étant partagées entre experts. embed_tokensetlm_headsont quantifiés nativement parmlx_nn.quantize(); les couches ParoQuant restent intactes.
Sources#
- Papier (HTML complet) : arxiv.org/html/2511.10645v2
- Z Lab blog : z-lab.ai/projects/paroquant
- GitHub : z-lab/paroquant
- Hugging Face : huggingface.co/papers/2511.10645 · collection paroquant
- OpenReview : openreview.net/forum?id=1USeVjsKau
- Voir aussi : QuIP / QuIP# (autre famille par rotation), OmniQuant, AWQ, comparatif complet