Sub-byte Quantification — Quantification sous l'octet#

TL;DR — La quantification sub-byte désigne toute méthode qui code les poids en moins de 8 bits (4-bit, 3-bit, 2-bit, voire 1.58-bit). Le défi technique central est le bit-packing : le hardware travaillant par octets, il faut empaqueter plusieurs valeurs sub-byte dans chaque mot mémoire. Les méthodes avancées (AQLM, QuIP#, HQQ) utilisent des codebooks et de la vector quantization non-uniforme pour rester viables en régime extrême.


Le concept : descendre sous l'octet#

Pourquoi le sub-byte ?#

Un octet (byte) = 8 bits. Le hardware (CPU, GPU, RAM) adresse la mémoire par octets. Mais 8-bit par poids, c'est encore beaucoup pour un LLM de 70B (70 GB). Descendre à 4-bit (35 GB), 3-bit (26 GB), ou 2-bit (17.5 GB) permet :

  • Faire tourner de très grands modèles sur du hardware limité
  • Servir plus de modèles en parallèle sur le même GPU
  • Déployer sur edge devices (mobile, embarqué)
Bits/poids 70B (taille) Viabilité & Méthodes
16 (FP16) 140 GB Référence — pleine précision
8 (INT8) 70 GB ✅ Trivial — LLM.int8(), FP8
4 (INT4) 35 GB ✅ Sweet spot — GPTQ, AWQ, GGUF
3 (INT3) 26 GB ⚠️ Dégradé — HQQ, OmniQuant
2 (INT2) 17 GB 🔴 Extrême — QuIP#, AQLM
1.58 (tern.) 14 GB 🔴 Radical — BitNet b1.58
1 (binary) 9 GB 🔴🔴 Expérimental — BitNet b1

L'analogie pour néophyte#

Imagine que tu dois ranger des objets dans des boîtes qui font toutes la même taille (un octet). En 8-bit, chaque objet a sa propre boîte. En 4-bit, tu mets deux objets par boîte. En 2-bit, tu en mets quatre. Mais en 3-bit, ça ne tombe pas juste : tu ne peux pas mettre un nombre entier d'objets 3-bit dans une boîte 8-bit (8÷3 = 2.67). Il faut donc un astucieux système d'empilement (bit-packing) qui répartit les objets sur plusieurs boîtes. C'est ça, le défi sub-byte.


Le défi technique : le bit-packing#

Le problème fondamental#

Le hardware accède à la mémoire par mots de 8, 16, 32 ou 64 bits. Une valeur de 3 bits ne peut pas être adressée directement.

graph TB subgraph INT4["INT4 (4-bit) — 2 valeurs par octet"] B1["🟦 v1 (4b) | 🟩 v2 (4b) → 1 byte"] OK1["✅ Tombe juste : 8 / 4 = 2"] end subgraph INT3["INT3 (3-bit) — ne tombe pas juste !"] B2["8 / 3 = 2.67 → packer sur plusieurs octets"] B3["v1..v8 sur 3 bytes (24 bits)
8 valeurs × 3 bits = 24 bits exactement"] WARN1["⚠️ Accès à une valeur = lire 2 octets"] end subgraph INT2["INT2 (2-bit) — 4 valeurs par octet"] B4["🟦 v1 | 🟩 v2 | 🟧 v3 | 🟥 v4 → 1 byte"] OK2["✅ Tombe juste : 8 / 2 = 4"] end

Le dé-empaquetage (unpacking) à la volée#

Pendant l'inférence, les valeurs packed doivent être dépaquetées avant le GEMM :

graph LR subgraph Packed["Mémoire (packed)"] BYTE1["Byte 1: v1 v2 v3 v4"] BYTE2["Byte 2: v5 v6 v7 v8"] end subgraph Unpacked["Registres GPU (dépaquetés)"] R1["v1 | v2 | v3 | v4"] R2["v5 | v6 | v7 | v8"] end BYTE1 -->|"shift + mask"| R1 BYTE2 -->|"shift + mask"| R2 NOTE["→ Opérations de shift + mask
→ En streaming pendant le GEMM
→ Overhead si mal optimisé"]

Block-wise quantization#

Pour réduire l'overhead des scale factors, on partage les métadonnées par blocs :

graph TB subgraph BLOCK["Groupe de N=128 poids"] SCALE["scale = 0.34 (FP16, partagé par 128 poids)"] ZERO["zero = 0"] WEIGHTS["q0 q1 q2 ... q127 (chacun 4-bit)"] FORMULA["w_réel[i] = q[i] × scale + zero"] end OVERHEAD["Overhead : 2 bytes / 128 poids × 4 bits
= 0.125 bits/poids supplémentaires → négligeable"] BLOCK --> OVERHEAD

L'espace de quantification : uniforme vs non-uniforme#

graph TB subgraph UNIFORM["Quantification Uniforme (INT4 standard)"] U1["Niveaux également espacés"] U2["−0.4 −0.3 −0.2 −0.1 0 +0.1 +0.2 +0.3 +0.4"] U3["✅ Simple, hardware-friendly"] U4["❌ Gaspille des niveaux aux extrémités
(poids concentrés autour de 0)"] U5["Méthodes : GPTQ, AWQ, GGUF Q4_0"] U1 --> U2 --> U3 --> U4 --> U5 end subgraph NONUNIFORM["Quantification Non-Uniforme (codebook / vector quant)"] N1["Niveaux concentrés là où les poids sont denses"] N2["↑ densité autour de 0 ↑"] N3["✅ Plus efficace à bas bitrate"] N4["✅ Codebooks appris → adaptés au modèle"] N5["❌ Plus complexe, pas de support HW natif"] N6["Méthodes : QuIP# (E8 lattice), AQLM, SqueezeLLM"] N1 --> N2 --> N3 --> N4 --> N5 --> N6 end

Méthodes principales de quantification sub-byte#

Vue d'ensemble#

Méthode Bits Approche Calibration SOTA à ce niveau ? Page détaillée
GGUF K-quants 2-4 Block-wise uniforme Non (legacy) / Oui (I-quants) Non (pratique) GGUF
GGUF I-quants 2-4 Importance matrix (non-uniforme) Oui Compétitif GGUF
HQQ 2-4 Half-quadratic optimization Non Bon à 3-bit HQQ
QuIP# 2-3 Hadamard + E8 lattice codebook Oui SOTA à 2-bit QuIP#
AQLM 2-3 Multi-codebook additive vector quant Oui SOTA à 2-bit AQLM
SqueezeLLM 3 Sensitivity-based non-uniform + dense-sparse Oui Bon à 3-bit SqueezeLLM
SpQR 3-4 Sparse outlier isolation Oui Near-lossless SpQR
BitNet b1.58 1.58 QAT from scratch (ternaire) N/A Révolutionnaire BitNet

Détail des approches#

1. Bit-packing uniforme (GPTQ, AWQ, GGUF legacy)#

Le schéma le plus répandu : chaque poids est quantifié indépendamment avec une grille uniforme.

graph TB subgraph GPTQ["GPTQ INT4 avec group_size=128"] G1["scale (FP16) : 1 valeur partagée"] G2["zero (INT4) : 1 valeur partagée"] G3["128 × INT4 : 512 bits = 64 octets"] G4["Total : 2 + 0.5 + 64 = 66.5 bytes pour 128 poids"] G5["→ 4.16 bits/poids effectif (overhead de 0.16 bits)"] G1 --> G2 --> G3 --> G4 --> G5 end

2. Codebook / Vector Quantization (QuIP#, AQLM)#

Au lieu de quantifier chaque poids indépendamment, on quantifie des groupes de poids conjointement via un codebook.

graph TB subgraph SCALAR["Scalaire (indépendant)"] S1["w₁ = 0.037 → q₁ = 1"] S2["w₂ = 0.142 → q₂ = 2"] S3["w₃ = 0.091 → q₃ = 1"] S4["w₄ = 0.218 → q₄ = 3"] S5["Chaque poids → 1 index indépendant
2-bit : 4 niveaux possibles par poids"] S1 --> S2 --> S3 --> S4 --> S5 end subgraph VQ["Vector Quantization (groupé)"] V1["w₁ w₂ w₃ w₄ w₅ w₆ w₇ w₈
(vecteur de 8 poids)"] V2["Codebook de 256 vecteurs (chacun dim 8)"] V3["→ Trouver le vecteur le plus proche"] V4["→ 1 index 8-bit pour 8 poids = 1 bit/poids !"] V5["✅ Exploite les corrélations entre poids"] V6["✅ Beaucoup plus expressif à bas bitrate"] V7["❌ Codebook doit être stocké + recherché"] V1 --> V2 --> V3 --> V4 --> V5 --> V6 --> V7 end

3. QuIP# : E8 lattice codebook#

QuIP# utilise un codebook basé sur le réseau E8 — le packing optimal de la sphère unité en 8 dimensions.

graph TB E1["Le réseau E8 = packing de sphères le plus dense connu en dimension 8"] E2["240 vecteurs les plus proches de l'origine
forment un ensemble symétrique optimal"] E3["QuIP# utilise ces points comme niveaux
de quantification pour 8 poids simultanément"] E4["→ Théoriquement optimal pour poids sub-Gaussiens"] E5["Après Hadamard preprocessing, les poids sont
effectivement sub-Gaussiens → E8 quasi-optimal"] E6["🏆 Résultat : SOTA à 2-bit (PPL ~7.2 sur Llama-2-7B)"] E1 --> E2 --> E3 --> E4 --> E5 --> E6

4. AQLM : multi-codebook additive#

AQLM approxime chaque vecteur de poids par une somme de vecteurs issus de plusieurs codebooks :

graph LR CB1["Codebook 1
(256 entrées)"] CB2["Codebook 2
(256 entrées)"] SUM["w ≈ CB1[i₁] + CB2[i₂]"] W["Poids approximé"] CB1 -->|"index i₁ (8-bit)"| SUM CB2 -->|"index i₂ (8-bit)"| SUM SUM --> W INFO["2 × 8-bit pour 8 poids = 2 bits/poids
256 × 256 = 65 536 combinaisons additives
→ bien plus riche que 4 niveaux scalaires"]

5. HQQ : half-quadratic, sans calibration#

HQQ formule la quantification comme un problème d'optimisation data-free :

graph TB OBJ["Minimiser : ||W − Q(W)||² + λg(z)"] ALT["→ Optimisation alternée (half-quadratic)"] NODATA["→ Pas de données de calibration nécessaires"] FAST["→ Très rapide (< 5 min pour un 70B)"] QUAL["→ Qualité compétitive mais pas SOTA en sub-3-bit"] OBJ --> ALT --> NODATA --> FAST --> QUAL

Diagramme : l'espace qualité/bits des méthodes sub-byte#

quadrantChart title Qualité vs Bits/poids (PPL, plus bas = mieux) x-axis "Faible qualité (PPL élevé)" --> "Haute qualité (PPL bas)" y-axis "1 bit" --> "16 bits" "FP16": [0.95, 0.95] "GPTQ/AWQ INT4": [0.82, 0.72] "HQQ INT3": [0.70, 0.58] "GGUF Q3_K": [0.68, 0.55] "SqueezeLLM 3-bit": [0.65, 0.52] "QuIP# 2-bit (SOTA)": [0.58, 0.40] "AQLM 2-bit": [0.55, 0.40] "HQQ 2-bit": [0.42, 0.38] "GGUF IQ2": [0.48, 0.36] "GPTQ 2-bit (❌)": [0.20, 0.36] "BitNet b1.58": [0.30, 0.30]

K-quants et I-quants de GGUF : le sub-byte grand public#

Le format GGUF de llama.cpp propose les schémas sub-byte les plus accessibles :

K-quants (block-wise, scales variables)#

Type ~bpw Description
Q2_K ~2.6 2-bit poids + 4-bit scales
Q3_K ~3.4 3-bit poids + scales variables
Q4_K ~4.6 4-bit poids + 6-bit scales (sweet)
Q5_K ~5.7 5-bit + scales
Q6_K ~6.6 6-bit + scales (quasi-lossless)

I-quants (importance matrix)#

Type bpw Description
IQ2_XXS 2.0 2-bit extrême, matrice d'importance
IQ2_XS 2.2 2-bit optimisé
IQ2_S 2.4 2-bit amélioré
IQ3_XXS 3.0 3-bit extrême
IQ3_S 3.2 3-bit optimisé
IQ4_XS 4.25 4-bit compact

Les I-quants utilisent une matrice d'importance (issue de calibration) pour guider le placement des niveaux de quantification → meilleure qualité que les K-quants à bitrate égal en régime sub-4-bit.


Le state of the art en sub-byte#

Classement par régime de bitrate#

Régime SOTA PPL (Llama-2-7B) Notes
~4 bit AWQ / GPTQ ~5.5 Quasi-lossless, très supporté
~3 bit SqueezeLLM / HQQ ~5.8-6.0 Bonne qualité
~2.5 bit GGUF IQ2_S ~8.0 Utilisable sur CPU
~2 bit QuIP# ~7.2 SOTA PTQ à 2-bit
~2 bit AQLM ~7.5 Compétitif, kernels matures
1.58 bit BitNet b1.58 ~10.5* *Mais matche FP16 à taille équivalente (from scratch)

⚠️ Les valeurs de PPL sont approximatives et dépendent du modèle, du dataset et des hyperparamètres. Elles sont indicatives pour situer les méthodes relatives les unes aux autres.


Exemple pratique#

Quantification sub-byte avec llama.cpp#

# Cloner llama.cpp
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp && mkdir build && cd build
cmake .. && make -j

# Quantifier en Q3_K (3-bit, ~3.4 bpw)
./bin/llama-quantize \
    models/llama-7b-fp16.gguf \
    models/llama-7b-q3_k.gguf \
    Q3_K

# Quantifier en IQ3_XXS (3-bit importance matrix, ~3.0 bpw)
./bin/llama-quantize \
    models/llama-7b-fp16.gguf \
    models/llama-7b-iq3xxs.gguf \
    IQ3_XXS

# Quantifier en Q2_K (2-bit, ~2.6 bpw)
./bin/llama-quantize \
    models/llama-7b-fp16.gguf \
    models/llama-7b-q2_k.gguf \
    Q2_K

# Inférence avec le modèle quantifié
./bin/llama-cli \
    -m models/llama-7b-q2_k.gguf \
    -p "Explique la quantification sub-byte : " \
    -n 200

Quantification 2-bit avec QuIP#

# Installer QuIP#
git clone https://github.com/Cornell-RelaxML/quip-sharp.git
cd quip-sharp
pip install -e .

# Quantification 2-bit avec codebook E8 lattice
python quantize.py \
    --model meta-llama/Llama-2-7b-hf \
    --output_dir ./llama-7b-quip-sharp-2bit \
    --codebook E8P12 \
    --bits 2 \
    --calibration_dataset redpajama

Quantification 3-bit sans calibration avec HQQ#

from hqq import HQQModel, HQQLinear

# Quantification 3-bit sans aucune donnée de calibration
quant_config = {
    "weight_quant_params": {
        "nbits": 3,
        "group_size": 64,
        "quant_zero": True,
        "quant_scale": True,
    }
}

model = HQQModel.from_pretrained("meta-llama/Llama-2-7b-hf")
model.quantize_model(quant_config=quant_config)

# → 70B en 3-bit en moins de 5 minutes, sans calibration !

Vérifier le bitrate effectif#

import torch

def check_effective_bpw(model_path):
    """Calcule le bits-per-weight effectif d'un modèle quantifié."""
    model = torch.load(model_path, map_location="cpu")
    total_params = 0
    total_bytes = 0

    for name, param in model.named_parameters():
        n_params = param.numel()
        n_bytes = param.storage().nbytes()
        total_params += n_params
        total_bytes += n_bytes

    bpw = (total_bytes * 8) / total_params
    print(f"Paramètres : {total_params / 1e9:.1f}B")
    print(f"Taille : {total_bytes / 1e9:.2f} GB")
    print(f"Bits/poids effectif : {bpw:.2f}")
    return bpw

Avantages et inconvénients du sub-byte#

✅ Avantages ❌ Inconvénients
Compression maximale (4× à 8× vs INT8) Overhead de dépaquetage (bit-packing/unpacking)
Déployer de très grands modèles sur hardware limité Pas de support hardware natif sub-byte (sauf FP4 Blackwell)
Schémas par blocs → qualité adaptative Qualité dégradée en deçà de 3-bit (sauf codebooks)
Codebooks non-uniformes → expressivité à bas bitrate Vector quantization complexe à optimiser
I-quants GGUF : sub-byte accessible au grand public Temps de quantification long (QuIP#, AQLM)
BitNet 1.58-bit : révolution potentielle BitNet : from scratch + hardware non adapté

L'avenir du sub-byte#

mindmap root((Tendances Émergentes)) Hardware Sub-byte NVIDIA Blackwell : FP4 Tensor Cores natifs Chips ternaires dédiés NPU mobiles avec support INT4 natif Vector Quantization à Grande Échelle Codebooks E8, Leech Co-design quantization + kernels GPU CRVQ Channel-Relaxed VQ Modèles Natifs 1-Bit / 1.58-Bit BitNet b1.58 à plus grande échelle Hardware spécialisé ternaire Nouveaux paradigmes de serving Quantification Adaptative Bitrate variable par couche Mixed-precision automatique Quantification dynamique contextuelle

Références#

ia llm quantification sub-byte 2-bit 3-bit codebook vector-quantization bit-packing aqlm quip hqq