LLM.int8()#

TL;DR#

LLM.int8() est une méthode de quantification INT8 qui résout le problème des emergent outliers dans les grands transformers. Elle utilise une décomposition mixed-precision : 99.9% des calculs en INT8 et ~0.1% en FP16 pour les outliers. Résultat : zéro dégradation de performance pour des modèles jusqu'à 175B paramètres, avec 2× moins de mémoire.


Pour un néophyte#

Dans un grand modèle de langage, la plupart des valeurs sont "normales" (petites), mais quelques-unes deviennent énormes — comme quelques élèves brillants dans une classe. Ces valeurs extrêmes sont essentielles : si on les arrondit mal (quantification classique), le modèle perd toute sa capacité de raisonnement.

LLM.int8() fait simple mais efficace :
1. Les valeurs normales → INT8 (rapide et léger)
2. Les quelques valeurs extrêmes → FP16 (précision conservée)

On ne perd aucune précision tout en économisant ~2× de mémoire.


La découverte clé : Emergent Outlier Features#

Le problème des outliers#

Tim Dettmers et son équipe ont découvert que les transformers à grande échelle (≥ 6.7B paramètres) développent des emergent outlier features : des dimensions spécifiques où certaines activations atteignent des magnitudes extrêmes — jusqu'à 20× la moyenne.

xychart-beta title "Distribution des activations (magnitude vs dimensions)" x-axis ["128", "", "256", "", "384", "", "512", "", "..."] y-axis "Magnitude" 0 --> 25 bar [5, 4, 5, 4, 5, 4, 5, 4, 5]

◆ Les outliers (~0.1% des dimensions) atteignent des magnitudes 20× supérieures à la moyenne.

Pourquoi les méthodes PTQ classiques échouent#

flowchart LR A["Avant : [0.3, 0.5, 0.2,
67.0, 0.4, 0.1, 0.3]
← 67.0 = outlier"] A --> B["Scale = max/127
= 67.0/127 = 0.528"] B --> C["Après : [1, 1, 0,
127, 1, 0, 1]
← TOUT devient 0 ou 1 !"] C --> D["❌ 99.9% de l'information
est PERDUE"]

Caractéristiques des emergent outliers#

Propriété Valeur
Modèles concernés ≥ 6.7B paramètres
Proportion des dimensions ~0.1% des features
Magnitude Jusqu'à 20× la moyenne
Concentration Dans des dimensions fixes (systématiques)
Impact sur la performance Dominant — les retirer détruit le modèle
Taille du modèle Outliers ? PTQ classique viable ?
< 1B Non ✅ Oui
1B – 6.7B Rarement ⚠️ Marginalement
≥ 6.7B OUI, systématiquement ❌ Non → nécessite LLM.int8()

La solution : Décomposition Mixed-Precision#

LLM.int8() sépare chaque multiplication matricielle en deux parties :

Y = X × W          (X = activations, W = poids)

Décomposition :
         <div class="mermaid">
         flowchart TD
             Y["Y = X × W"]
             Y --> S1["1. Extraire les outlier features<br/>X_outlier (|x| > α), W_outlier"]
             Y --> S2["2. Partie normale (99.9%)<br/>Y_normal = INT8(X_normal × W_normal)<br/>Vector-wise INT8 → MatMul accélérée"]
             Y --> S3["3. Partie outlier (0.1%)<br/>Y_outlier = FP16(X_outlier × W_outlier)<br/>Pleine précision"]
             S2 --> R["4. Résultat final<br/>Y = Y_normal + Y_outlier"]
             S3 --> R
         </div>

### Schéma de la décomposition

<div class="mermaid">
flowchart TD
    MATMUL["MATMUL X × W<br/>(FP16 → INT8 souhaité)"]
    MATMUL --> SPLITX["Matrice X (activations)<br/>[seq, dim]"]
    SPLITX --> NORM["Partie normale<br/>99.9% des valeurs"]
    SPLITX --> OUT["Colonnes outlier<br/>(|x| > threshold α)"]

    NORM --> NM["Vector-wise INT8<br/>X_normal × W_normal<br/>→ INT8 MatMul<br/>(très rapide)<br/>~99.9% du calcul"]
    OUT --> OM["FP16 précision complète<br/>X_out × W_out<br/>→ FP16 MatMul<br/>(lent mais 0.1%)<br/>~0.1% du calcul"]

    NM --> RESULT["Y = Y_int8 + Y_fp16<br/>Résultat final = EXACTEMENT comme FP16"]
    OM --> RESULT
</div>

### Seuil de détection α

Le seuil α (typiquement **6.0**) détermine quelles colonnes sont considérées comme outliers :

<div class="mermaid">
flowchart TD
    A["max(|X[:, j]|) > α ?"] -->|OUI| B["Colonne j = outlier<br/>→ traitée en FP16"]
    A -->|NON| C["Colonne normale<br/>→ traitée en INT8"]
</div>

---

## Vector-wise INT8 Quantization

Pour la partie "normale" (99.9%), LLM.int8() utilise une **vector-wise quantization** :

<div class="mermaid">
flowchart TD
    A["X_row : [x1, x2, ..., xn]<br/>scale_x = max(|X_row|)"]
    B["W_col : [w1, w2, ..., wn]<br/>scale_w = max(|W_col|)"]
    A --> C["X_int8 = round(X_row / scale_x)"]
    B --> D["W_int8 = round(W_col / scale_w)"]
    C --> E["MatMul INT8 :<br/>dot_product_int8(X_int8, W_int8)"]
    D --> E
    E --> F["Déquantification :<br/>result = dot_product × scale_x × scale_w"]
</div>

> 💡 **Vector-wise** = un scale factor par vecteur (ligne/colonne), pas un seul pour tout le tenseur. C'est entre per-tensor et per-channel.

---

## Bits cibles et précision

| Aspect | Valeur |
|---|---|
| **Bits (normal)** | INT8 (poids + activations) |
| **Bits (outlier)** | FP16 (pleine précision) |
| **Proportion INT8** | ~99.9% des calculs |
| **Proportion FP16** | ~0.1% des calculs |
| **Dégradation** | **0.0** (zéro perte mesurable) |
| **Réduction mémoire** | ~2× vs FP16 |

---

## Avantages

- ✅ **Zéro dégradation de performance** — la décomposition préserve exactement les calculs importants
- ✅ Réduction de mémoire par **~2×** (FP16 → INT8)
- ✅ Permet de faire tourner **OPT-175B / BLOOM-176B** sur des GPU consumer
- ✅ **Plug-and-play** : `load_in_8bit=True` et c'est tout
- ✅ Implémentation de référence dans **bitsandbytes** (CUDA kernels optimisés)
- ✅ Validé sur des modèles jusqu'à 175B paramètres

## Inconvénients

- ⚠️ **Léger overhead computationnel** (~30% plus lent que FP16 pur) à cause de la décomposition mixed-precision
- ❌ **Limité à INT8** — pas de compression plus agressive (INT4, INT2)
- ❌ Les outliers peuvent ralentir l'inférence pour les très grands modèles
- ❌ Ne résout pas le problème de quantification à très bas bitrate

---

## Performance et overhead

| Configuration | Mémoire | Vitesse (relative) |
|---|---|---|
| FP16 (baseline) | 100% | 1.0× |
| LLM.int8() (INT8) | ~50% | ~0.7-0.8× (overhead ~20-30%) |
| INT8 pur (PTQ classique) | ~50% | ~1.5-2.0× (mais dégradation sur grands modèles) |

> Le compromis : LLM.int8() est **légèrement plus lent** que l'INT8 pur, mais **zéro perte de précision**. Pour les modèles ≥ 6.7B, c'est le seul INT8 viable.

---

## Exemple pratique

### Avec Hugging Face Transformers (le plus simple)

```python
from transformers import AutoModelForCausalLM, AutoTokenizer

# ═══════════════════════════════════════════════
#  Chargement d'un modèle en INT8 avec LLM.int8()
#  → UNE seule ligne : load_in_8bit=True
# ═══════════════════════════════════════════════

model = AutoModelForCausalLM.from_pretrained(
    "facebook/opt-6.7b",           # ou tout autre modèle ≥ 6.7B
    load_in_8bit=True,             # ← Active LLM.int8()
    device_map="auto",             # Répartit sur les GPU disponibles
)

tokenizer = AutoTokenizer.from_pretrained("facebook/opt-6.7b")

# Inférence — identique à un modèle FP16 normal
inputs = tokenizer("Bonjour, comment ça va ?", return_tensors="pt")
outputs = model.generate(**inputs, max_new_tokens=50)
print(tokenizer.decode(outputs[0]))

# → Zéro dégradation vs FP16, mais ~2× moins de VRAM

Vérifier la quantification#

# Inspecter les modules quantifiés
for name, module in model.named_modules():
    if hasattr(module, 'weight') and hasattr(module.weight, 'quant_state'):
        print(f"{name}: INT8 quantifié")
        print(f"  dtype original: {module.weight.quant_state.dtype}")
        print(f"  shape: {module.weight.shape}")

# Vérifier la mémoire utilisée
import torch
print(f"VRAM allouée: {torch.cuda.memory_allocated() / 1e9:.2f} GB")
# OPT-6.7B : ~13 GB en FP16 → ~7 GB en INT8

Avec bitsandbytes directement#

import bitsandbytes as bnb
import torch.nn as nn

# Remplacer une couche Linear standard par une couche INT8
linear_fp16 = nn.Linear(4096, 4096)

# Conversion en Int8Params
from bitsandbytes.nn import Linear8bitLt
linear_int8 = Linear8bitLt(
    input_features=4096,
    output_features=4096,
    bias=True,
    has_fp16_weights=False,   # poids quantifiés en INT8
    threshold=6.0,            # seuil outlier α
)

# Le forward pass gère automatiquement la décomposition mixed-precision
output = linear_int8(input_tensor)

Inférence sur GPU consumer#

# ═══════════════════════════════════════════════
#  OPT-175B / BLOOM-176B sur un seul serveur
#  (impossible en FP16, possible en INT8)
# ═══════════════════════════════════════════════

model = AutoModelForCausalLM.from_pretrained(
    "bigscience/bloom",
    load_in_8bit=True,
    device_map="auto",
    # Répartit automatiquement sur plusieurs GPU
    # BLOOM-176B : ~350 GB FP16 → ~180 GB INT8
    # → 4× RTX 3090 (24GB) ou 2× A100 (80GB)
)

Papier arXiv source#

LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale
- 📄 arXiv : 2208.07339
- 👤 Auteurs : Tim Dettmers, Mike Lewis, Younes Belkada, Luke Zettlemoyer
- 🏢 University of Washington / Meta AI
- 📅 15 août 2022
- 📰 Publication : NeurIPS 2022


Références#

ia llm quantification int8 mixed-precision outliers bitsandbytes inference