La quantization consiste à représenter les poids (et parfois les activations) d'un réseau de neurones avec moins de bits : passer de 32 bits par paramètre à 8, 4, voire 2 bits. Pour un grand modèle de langage, l'enjeu n'est pas anecdotique — un modèle 7B en FP32 occupe **28 Go** rien que pour ses poids, ce qui le rend impossible à charger sur la plupart des GPU grand public. Cet article explique les formats numériques en jeu, pourquoi l'inférence des LLM est **limitée par la bande passante mémoire**, les grandes familles de méthodes (GPTQ, AWQ, SmoothQuant, GGUF, NF4), et les techniques de service (batching continu, PagedAttention, décodage spéculatif) qui transforment un modèle quantifié en service à haut débit.

## Pourquoi quantifier : mémoire et bande passante

Un poids stocké en virgule flottante 32 bits (FP32) coûte 4 octets. Un modèle de **N** paramètres pèse donc `4 × N` octets en FP32 ; en demi-précision (16 bits) la moitié, en INT8 le quart, en INT4 le huitième. La règle mentale utile est simple : **mémoire des poids ≈ N × (bits / 8)**.

| Format | Octets/poids | Mémoire d'un 7B |
| --- | --- | --- |
| FP32 | 4 | 28 Go |
| FP16 / BF16 | 2 | 14 Go |
| FP8 / INT8 | 1 | 7 Go |
| INT4 | 0,5 | 3,5 Go |
| INT2 | 0,25 | 1,75 Go |

![Disposition des bits (signe, exposant, mantisse) et empreinte mémoire d'un modèle 7B, de FP32 à INT4.](/articles/quantization-et-inference-efficace/formats-numeriques.svg)
*Figure : formats numériques et mémoire. Chaque ligne montre la répartition des bits et la mémoire correspondante pour un modèle de 7 milliards de paramètres.*

Mais la mémoire n'est que la moitié de l'histoire. Le vrai gain à l'inférence vient de la **bande passante**. Pour produire **un seul** token de sortie, le GPU doit relire l'intégralité des poids du modèle depuis la VRAM vers les unités de calcul. À chaque pas de décodage, on transfère donc `taille du modèle` octets — diviser cette taille par 2 ou par 4 divise d'autant le trafic mémoire, qui est le goulot d'étranglement dominant. Quantifier, c'est avant tout **lire moins d'octets par token**.

### Intensité arithmétique et roofline

Pour formaliser ce goulot, on raisonne en **intensité arithmétique** : le nombre d'opérations flottantes (FLOPs) effectuées par octet lu en mémoire. Le modèle **roofline** dit qu'une charge est limitée par le calcul si son intensité dépasse le ratio `FLOPs crête / bande passante crête` du GPU, et limitée par la mémoire sinon. Sur un H100, ce ratio « pivot » se situe autour de **300 FLOPs/octet**.

Or, le décodage d'un LLM avec un lot de taille 1 a une intensité arithmétique proche de **2 FLOPs/octet** (chaque poids lu sert à une multiplication-addition, soit 2 FLOPs, pour ~2 octets en FP16). On est donc très loin à gauche du pivot : le GPU est massivement sous-utilisé côté calcul et passe son temps à attendre la VRAM. Deux leviers déplacent le curseur vers la droite : **quantifier** (moins d'octets par poids) et **batcher** (réutiliser chaque poids lu pour plusieurs requêtes, ce qui augmente directement l'intensité).

## Les formats numériques en détail

Un nombre flottant se décompose en trois champs : **signe** (1 bit), **exposant** (la plage) et **mantisse** (la précision). Les arbitrages diffèrent selon le format :

- **FP32** — 1 signe + 8 exposant + 23 mantisse. La référence « pleine précision », utilisée à l'entraînement historique.
- **FP16** — 1 + 5 + 10. Demi-précision « IEEE ». Bonne précision relative mais **plage d'exposant étroite** (5 bits) : risque d'overflow/underflow sur de grandes activations.
- **BF16** (bfloat16) — 1 + 8 + 7. Même plage d'exposant que FP32 (8 bits) mais mantisse réduite. C'est devenu le format d'entraînement et d'inférence par défaut sur les LLM : il « plante » beaucoup moins que FP16 sur les valeurs extrêmes.
- **FP8** (E4M3 ou E5M2) — 8 bits flottants, supportés nativement par les GPU récents (Hopper/Blackwell). Deux variantes coexistent : **E4M3** (4 bits d'exposant, 3 de mantisse) privilégie la précision et sert aux poids et activations ; **E5M2** (5 exposant, 2 mantisse) privilégie la plage dynamique et sert souvent aux gradients. Bon compromis quand le matériel a des unités dédiées : on garde un comportement « flottant » (pas de zero-point) tout en doublant le débit des tensor cores.
- **INT8 / INT4** — entiers. Pas d'exposant : on encode un réel `x` par `x ≈ scale × q` où `q` est un entier et `scale` un facteur d'échelle flottant. Le choix du `scale` (et d'un éventuel `zero-point`) est tout l'art de la quantization entière.
- **INT2 / ternaire** — l'extrême de la compression. À 2 bits (voire 1,58 bit pour les schémas ternaires {−1, 0, +1}), la mémoire s'effondre mais la qualité aussi, sauf entraînement dédié (QAT). Réservé à la recherche et aux contraintes matérielles extrêmes.

L'équation de base de la quantization entière affine est :

```text
q = round(x / scale) + zero_point      (quantize)
x ≈ scale × (q − zero_point)           (dequantize)
```

Deux variantes coexistent :

- **Symétrique** : `zero_point = 0`, la plage est centrée sur 0 (`[−s·2^{b−1}, s·(2^{b−1}−1)]`). Plus simple et un peu plus rapide.
- **Asymétrique (affine)** : un `zero_point` non nul aligne le zéro réel sur un entier. Indispensable pour les distributions décentrées (sorties de ReLU, GELU…).

La **granularité** du `scale` est cruciale :

- **Par tenseur** : un seul scale pour toute la matrice. Compact, mais grossier.
- **Par canal** : un scale par ligne (par sortie). Bien meilleur, peu de surcoût.
- **Par groupe** : un scale par bloc de 32, 64 ou 128 poids contigus. C'est le réglage des méthodes 4 bits sérieuses ; le `group_size = 128` est le standard de fait.

Plus la granularité est fine, mieux on capture les variations locales d'amplitude — au prix de quelques métadonnées supplémentaires (les scales et zero-points). Un petit calcul : pour des poids 4 bits avec `group_size = 128` et un scale FP16 par groupe, le surcoût des scales est de `16 bits / 128 poids ≈ 0,125 bit/poids`, soit ~3 % de plus que les 4 bits nominaux. C'est pourquoi on parle parfois de **« 4,5 bits effectifs »** pour ces formats : la taille réelle inclut toujours les métadonnées.

### Erreur de quantization et clipping

Deux sources d'erreur cohabitent. L'**erreur d'arrondi** (chaque réel est arrondi au niveau le plus proche) vaut au pire `scale / 2` ; elle diminue quand on raffine le scale. L'**erreur de clipping** (saturation) apparaît quand on borne la plage pour mieux résoudre le centre de la distribution, au prix de l'écrasement des extrêmes. Trouver le scale optimal, c'est arbitrer entre les deux : un scale trop grand augmente l'arrondi, un scale trop petit augmente le clipping. Les méthodes sérieuses cherchent ce point en minimisant une erreur de reconstruction (souvent l'erreur quadratique sur la sortie de la couche, pas sur les poids eux-mêmes).

## Weight-only vs weight + activation

Deux grandes familles s'opposent selon **ce** qu'on quantifie :

- **Weight-only** (poids seuls) : on compresse les poids en INT4/INT8, mais les activations restent en FP16/BF16, et les calculs se font en flottant après déquantization à la volée. C'est la voie reine pour le **décodage** (limité par la bande passante) : on lit moins d'octets de poids, on garde la précision des activations. GPTQ et AWQ relèvent de cette famille (W4A16 : poids 4 bits, activations 16 bits).
- **Weight + activation** : on quantifie **aussi** les activations (par ex. W8A8 : poids et activations en INT8). On exploite alors les unités de calcul entières (tensor cores INT8), ce qui accélère les phases **compute-bound** comme le prefill et le service à fort débit. Mais c'est beaucoup plus dur, à cause des **outliers d'activation**.

La notation **WxAy** résume tout cela : `x` bits pour les poids, `y` pour les activations. W4A16 (poids 4 bits, activations 16 bits) est weight-only ; W8A8 et W4A8 sont des schémas weight + activation. Le choix dépend de la phase qu'on optimise et du régime de batch.

### Les kernels mixtes : le coût caché de la déquantization

Le weight-only n'est gagnant que si la déquantization à la volée ne mange pas le gain de bande passante. C'est le rôle des **kernels GEMM mixtes** (Marlin pour W4A16/W8A16, Machete sur Hopper) : ils multiplient des activations 16 bits par des poids 4 ou 8 bits en **recouvrant** la latence de chargement et de déquantization avec le calcul flottant, de sorte que la lecture compressée se traduit réellement en accélération. À faible batch, un GPTQ packé pour Marlin peut dépasser AWQ de ~25 %, et de près de 50 % à plus gros batch ; mais à très grand batch, on redevient compute-bound et le FP16 finit par rattraper. Morale : un format n'a de valeur que par le kernel qui le sert.

## Le problème des outliers d'activation

C'est l'obstacle central de la quantization d'activations.

Dans les grands transformeurs, **une poignée de canaux** (de l'ordre de 0,1 à 1 % des dimensions cachées) portent des activations d'amplitude 100 à 1000× supérieure au reste. Le dilemme est sans issue avec un scale naïf :

- Si l'on choisit un scale **assez grand** pour couvrir ces valeurs extrêmes, tous les canaux « normaux » s'effondrent sur quelques niveaux de quantization, et la précision s'écroule.
- Si l'on choisit un scale **fin**, les outliers saturent (clipping) et l'information est perdue.

Les poids, eux, sont beaucoup plus « sages » : leur distribution est proche d'une gaussienne, sans valeurs aberrantes massives. C'est pourquoi la quantization **weight-only** est nettement plus facile que la quantization d'activations. C'est aussi ce dilemme des outliers que les méthodes modernes (AWQ, SmoothQuant) résolvent, chacune à sa façon.

## PTQ vs QAT

Deux régimes pour produire un modèle quantifié :

- **PTQ** (Post-Training Quantization) : on quantifie **après** l'entraînement, sans rétro-propagation. Rapide (minutes à heures), c'est l'option par défaut. La plupart des méthodes utilisent un petit **jeu de calibration** (quelques centaines de phrases) pour estimer les plages d'activation et minimiser l'erreur de reconstruction.
- **QAT** (Quantization-Aware Training) : on **simule** la quantization pendant l'entraînement (ou un fine-tuning), pour que le modèle apprenne à être robuste à la perte de précision. Meilleure qualité en très basse précision (2-3 bits), mais coûteux : il faut des données, du calcul et du temps.

En pratique, à 8 et 4 bits, **la PTQ suffit** dans l'écrasante majorité des cas ; la QAT se réserve aux régimes très agressifs ou aux contraintes matérielles strictes.

## Les méthodes PTQ qui comptent

**GPTQ** (2022) — weight-only, INT3/INT4. Méthode **one-shot** par couche : elle quantifie les poids colonne par colonne en utilisant une approximation de la **Hessienne inverse** pour minimiser l'erreur de reconstruction, en ajustant les poids restants à chaque étape. Concrètement, après avoir figé une colonne à sa valeur quantifiée, l'erreur introduite est **propagée** sur les colonnes encore en flottant, qui « compensent » — d'où une dégradation bien moindre qu'un arrondi naïf. Résultat : on peut faire tenir un modèle 175B sur un seul A100 et obtenir ~3,25× d'accélération vs FP16, avec une perplexité quasi inchangée.

**AWQ** (Activation-aware Weight Quantization, 2023) — weight-only, INT4. Constat clé : tous les poids ne se valent pas. Une faible fraction de poids « **saillants** » (repérés via la magnitude des **activations**, pas des poids) domine la qualité. Au lieu de garder ces poids en pleine précision (ce qui casse l'efficacité matérielle des formats mixtes), AWQ **met à l'échelle par canal** : il multiplie les canaux importants par un facteur `s > 1` avant quantization et divise l'activation correspondante par `s`, transformation mathématiquement neutre qui réduit l'erreur relative sur les poids saillants. Pas de rétro-propagation ni de Hessienne, juste une recherche du `s` optimal par grille sur le jeu de calibration. C'est devenu un standard de fait pour le déploiement INT4, notamment côté périphérie/edge.

**SmoothQuant** (2022) — weight + activation, W8A8. Plutôt que de subir les outliers d'activation, on les **« lisse »** : par une transformation mathématiquement équivalente, on déplace une partie de la difficulté des activations vers les poids (plus faciles à quantifier). Résultat annoncé : jusqu'à 2× de réduction mémoire et ~1,56× d'accélération, sans entraînement.

**GGUF / k-quants** (llama.cpp) — l'écosystème CPU/Apple Silicon. Les **k-quants** (Q2_K à Q6_K) organisent les poids en **super-blocs de 256 valeurs** subdivisés, avec une **double quantization** des échelles (on quantifie les scales eux-mêmes) pour réduire le surcoût de métadonnées. Repères pratiques sur Llama-3-8B : **Q4_K_M** (~4,5 Go, +0,18 de perplexité) est le choix « par défaut » qualité/taille, **Q5_K_M** (~5,3 Go, +0,06) quand la qualité prime.

**bitsandbytes NF4** — le format de QLoRA. **NF4** (4-bit NormalFloat) est un type de données **information-théoriquement optimal pour des poids distribués normalement** : ses 16 niveaux sont placés sur les quantiles d'une loi normale (chaque « casier » reçoit la même masse de probabilité) plutôt qu'uniformément. Couplé à la **double quantization** — on quantifie en 8 bits les constantes de quantization (un scale FP32 par bloc de 64), ce qui économise ~0,4 bit/poids — NF4 égale les performances de BF16 tout en permettant le **QLoRA** : fine-tuner un modèle gelé en 4 bits via des adaptateurs LoRA, avec une empreinte mémoire 20× à 60× plus faible que le fine-tuning complet. Le gradient ne traverse les poids 4 bits (déquantifiés à la volée) que pour atteindre les petits adaptateurs entraînables.

### Un exemple chiffré de gain mémoire au fine-tuning

Sur un 7B, le fine-tuning complet en BF16 demande non seulement les **poids** (~14 Go) mais aussi les **gradients** (~14 Go) et les **états de l'optimiseur** Adam (deux moments, ~28 Go en FP32), soit ~56 Go avant même les activations — hors de portée d'un seul GPU 24 Go. QLoRA renverse l'équation : poids gelés en NF4 (~3,5 Go), aucun gradient ni état d'optimiseur sur les poids du modèle, et seuls les adaptateurs LoRA (souvent < 1 % des paramètres) portent gradients et moments. Le total tient confortablement sur un GPU 24 Go, voire moins.

En résumé, on peut classer les méthodes ainsi :

- **Weight-only INT4 sur GPU** : GPTQ, AWQ.
- **Weight + activation sur GPU** : SmoothQuant (W8A8), FP8.
- **CPU / Apple Silicon / edge** : GGUF (k-quants).
- **Fine-tuning économe** : NF4 (QLoRA).

## Le rôle du jeu de calibration

La plupart des méthodes PTQ ont besoin de quelques centaines d'exemples pour estimer les plages d'activation. Ce **jeu de calibration** influence le résultat :

- Il doit être **représentatif** du domaine d'usage (du code si vous servez du code, du texte multilingue si vous servez plusieurs langues).
- Sa taille raisonnable est de l'ordre de **128 à 512 séquences** ; au-delà, le gain est marginal.
- Un jeu inadapté peut **dégrader** silencieusement la qualité sur votre tâche réelle.

À noter : des travaux récents montrent que, sur les LLM modernes, l'influence des outliers et du choix du jeu de calibration **tend à diminuer** — mais la prudence reste de mise.

## Quantifier aussi le KV cache

Sur des contextes longs, le **KV cache** (les clés/valeurs d'attention de tous les tokens) peut peser autant que le modèle lui-même, voire davantage. Sa taille suit une formule simple :

```text
octets_KV = 2 × n_couches × n_têtes_kv × dim_tête × longueur × octets_par_élément
```

Le facteur `2` couvre les clés **et** les valeurs. Sur un modèle type 7B (32 couches, dimension cachée 4096) en FP16, cela représente environ **0,5 Mo par token**, soit ~2 Go pour un contexte de 4096 tokens — et cela par requête. C'est pourquoi l'attention multi-requêtes (MQA) et l'attention par groupes (GQA), qui réduisent `n_têtes_kv`, sont devenues standards : elles divisent directement le poids du KV cache.

Quantifier le cache (KV cache en FP8 ou INT8) :

- libère de la mémoire pour **des lots plus gros** ou **des contextes plus longs** ;
- réduit la bande passante à relire à chaque pas de décodage.

C'est un levier distinct de la quantization des poids, souvent combiné avec elle dans les moteurs de service récents.

## De la compression au débit : l'inférence en service

Compresser le modèle ne suffit pas : il faut un **moteur de service** qui sature le GPU. Première clé : comprendre que l'inférence a **deux phases** aux profils opposés.

- **Prefill** : on traite **tout** le prompt d'un coup. Beaucoup de FLOPs en parallèle → **limité par le calcul** (compute-bound).
- **Decode** : on génère les tokens **un par un**, en relisant tous les poids (et le KV cache) à chaque pas pour produire un seul token → **limité par la bande passante mémoire** (memory-bandwidth-bound).

Cette dissymétrie explique tout le reste. En decode, le GPU passe son temps à **attendre la mémoire**, pas à calculer. La parade : **mettre plusieurs requêtes en lot** pour amortir chaque lecture de poids sur de nombreux tokens à la fois.

## Batching continu et PagedAttention

Le **batching continu** (continuous / in-flight batching) entrelace dynamiquement les requêtes : dès qu'une génération se termine, une nouvelle requête entre dans le lot, sans attendre que tout le lot finisse. Le planificateur mêle dans un même pas du prefill (requêtes en attente) et du decode (requêtes en cours). Gain typique : **3 à 10×** de débit sur le même matériel.

Pour batcher efficacement, il faut gérer le **KV cache** (les clés/valeurs d'attention de tous les tokens déjà vus) sans le gaspiller. **PagedAttention** s'inspire de la **mémoire virtuelle** des OS : le KV cache est découpé en **blocs** de taille fixe (par défaut 16 tokens), alloués à la demande et non contigus. On élimine ainsi la fragmentation (gain de ~19-27 % de mémoire), on autorise des lots plus gros, et on partage facilement des préfixes communs entre requêtes.

Le **prefill segmenté** (chunked prefill) complète l'ensemble : un très long prompt est découpé en morceaux sur plusieurs pas du moteur, pour qu'il ne monopolise pas le GPU au détriment des requêtes en cours de décodage. Sans lui, un prompt de 100 000 tokens bloquerait toutes les autres générations le temps de son prefill ; avec lui, le moteur entrelace des morceaux de prefill et des pas de decode, lissant la latence inter-tokens.

### Caching de préfixes

Quand plusieurs requêtes partagent un même début — un long *system prompt*, un few-shot commun, un document en RAG — recalculer leur KV cache à chaque fois est du gaspillage pur. Le **caching de préfixes** (prefix caching) conserve et **réutilise** le KV cache du préfixe commun : seule la partie variable est recalculée. PagedAttention rend ce partage trivial (les blocs du préfixe sont simplement référencés par plusieurs séquences), et SGLang en fait un art avec **RadixAttention**, qui organise les préfixes en arbre radix pour maximiser les recouvrements. Sur des charges à fort partage, le gain de latence du *time-to-first-token* est spectaculaire.

## Décodage spéculatif : plusieurs tokens par passage

Le decode est séquentiel **par nature** : token `t+1` dépend de `t`. Le **décodage spéculatif** brise cette barrière sans changer la sortie. L'idée : un **petit modèle brouillon** (rapide) propose `k` tokens d'avance ; le **grand modèle cible** les **vérifie tous en une seule passe** parallèle, accepte le plus long préfixe correct et corrige le premier token rejeté. Comme la vérification est exacte, **la sortie est strictement identique** à un décodage classique — on ne paie aucune perte de qualité, on gagne juste plusieurs tokens par passage du grand modèle.

![Décodage spéculatif : le modèle brouillon propose k tokens, le grand modèle les vérifie en parallèle, accepte le préfixe correct et corrige le premier rejet.](/articles/quantization-et-inference-efficace/speculative-decoding.svg)
*Figure : flux draft + verify du décodage spéculatif. Le débit dépend du taux d'acceptation des tokens proposés.*

Plusieurs variantes évitent même d'avoir un modèle brouillon séparé :

- **n-gram / prompt lookup** : on cherche dans le texte déjà généré une occurrence antérieure du dernier n-gramme et on propose les tokens qui suivaient. Quasi gratuit, très efficace sur du texte répétitif (code, JSON, RAG).
- **Medusa** : on greffe sur le grand modèle plusieurs **têtes** légères qui prédisent les tokens `+1, +2, +3…` en parallèle, vérifiés via un arbre de candidats.
- **EAGLE** : on prédit au niveau des **features** (états cachés) plutôt que des tokens, pour un taux d'acceptation plus élevé. **EAGLE-2** y ajoute un **arbre de brouillon dynamique** : plutôt qu'une structure fixe, il étend l'arbre en fonction des scores de confiance (le taux d'acceptation dépend du contexte, pas seulement de la position), gagnant 20-40 % sur EAGLE-1. **EAGLE-3** exploite plusieurs couches cachées intermédiaires et relâche la contrainte de prédiction de features, repoussant encore les taux d'acceptation (accélérations annoncées de l'ordre de 3-5×).

### Pourquoi le taux d'acceptation est tout

Le gain dépend entièrement de la fraction de tokens proposés que le modèle cible accepte. Si le brouillon propose `k` tokens et que le taux d'acceptation moyen est `α`, le nombre espéré de tokens validés par passage du grand modèle vaut environ `(1 − α^{k+1}) / (1 − α)`. À `α = 0,8` et `k = 4`, on valide ~3,4 tokens par passage au lieu d'un seul — d'où l'accélération. Mais chaque passage du grand modèle reste plus coûteux (vérification de `k+1` positions) et le brouillon a son propre coût : si `α` est faible, le décodage spéculatif peut **ralentir** le service. D'où l'importance d'un brouillon bien aligné sur la cible.

Le décodage spéculatif brille surtout à **faible concurrence** (quand la latence par requête prime) ; à fort débit, le batching continu remplit déjà le GPU et le gain marginal diminue.

## Passer à l'échelle : tensor parallelism

Quand un modèle ne tient pas sur un GPU (ou pour réduire la latence), on le **découpe** sur plusieurs GPU. Le **tensor parallelism** (TP) répartit chaque matrice de poids entre les rangs : avec TP=8, chaque GPU détient 1/8 des poids et calcule sa part, puis les rangs se synchronisent (all-reduce) à chaque couche. C'est complémentaire de la quantization : on réduit d'abord la taille par GPU, puis on parallélise ce qui reste.

## Les moteurs de service

Ces techniques sont packagées dans des moteurs prêts à l'emploi :

- **vLLM** — référence open-source, PagedAttention « maison », batching continu, large support de quantization (GPTQ, AWQ, FP8…), décodage spéculatif.
- **TensorRT-LLM** (NVIDIA) — performances de pointe sur GPU NVIDIA, kernels optimisés, FP8 natif.
- **TGI** (Text Generation Inference, Hugging Face) — service prêt à l'emploi, bonne intégration de l'écosystème.
- **SGLang** — orienté programmes LLM structurés, caching de préfixes (RadixAttention) très efficace.
- **llama.cpp** — l'incontournable côté CPU / Apple Silicon / edge, moteur des GGUF.

## Mesurer le compromis précision / taille

On ne quantifie jamais à l'aveugle. Deux familles de mesures :

1. **Perplexité (PPL)** — mesure intrinsèque sur un corpus de référence (WikiText, C4) : à quel point le modèle est « surpris » par le texte réel. Rapide à calculer, utile pour repérer une dégradation grossière, mais une faible hausse de PPL ne garantit pas que les **capacités** sont préservées.
- **Évaluations de tâches** — benchmarks réels (MMLU, GSM8K, HumanEval, etc.) qui mesurent ce qui compte vraiment : raisonnement, code, connaissances. Plus lents, mais c'est la vérité terrain.

Quelques repères empiriques robustes : INT8 et W4A16 (GPTQ/AWQ, group 128) dégradent en général très peu (souvent < 1 point sur les évals) ; sous 4 bits la qualité chute plus vite ; et **toujours mesurer sur sa propre tâche et sa propre langue** — un modèle quantifié peut bien tenir en anglais et se dégrader davantage sur une langue moins représentée.

## Un calcul de budget mémoire

Avant de choisir un format, un calcul de coin de table évite les mauvaises surprises. Voici une estimation grossière de la mémoire des **poids** :

```py
def weight_memory_gb(params_billions: float, bits: int) -> float:
    bytes_per_param = bits / 8
    total_bytes = params_billions * 1e9 * bytes_per_param
    return total_bytes / (1024 ** 3)

for bits in (16, 8, 4):
    gb = weight_memory_gb(7, bits)
    print(f"7B @ {bits} bits : {gb:.1f} Go")
# 7B @ 16 bits : 13.0 Go
# 7B @ 8 bits  :  6.5 Go
# 7B @ 4 bits  :  3.3 Go
```

À cela s'ajoutent le **KV cache** (proportionnel à `batch × contexte`) et un peu d'overhead. Un GPU de 24 Go accueille confortablement un 7B en 4 bits avec un long contexte, ou un 13B serré.

## Pièges fréquents

- **Quantifier puis ne pas mesurer** : la perplexité seule ne suffit pas, vérifiez les évals de tâches.
- **Jeu de calibration non représentatif** : il fausse l'estimation des plages d'activation.
- **Mauvaise langue de test** : valider uniquement en anglais masque une dégradation sur d'autres langues.
- **Granularité trop grossière** : un scale par tenseur en 4 bits écrase souvent la qualité ; préférez `group_size = 128`.
- **Oublier le KV cache** : sur contexte long, il peut saturer la VRAM même avec des poids compressés.
- **Croire le décodage spéculatif gratuit à fort débit** : son gain s'estompe quand le batching continu remplit déjà le GPU.

## Recette pratique

- **Démarrer** en BF16 pour valider le comportement, puis quantifier.
- **GPU, latence faible** : weight-only INT4 (AWQ ou GPTQ, `group_size=128`) + vLLM/TensorRT-LLM ; activer le décodage spéculatif si la concurrence est faible.
- **GPU, fort débit** : envisager W8A8 (SmoothQuant) ou FP8 pour exploiter les tensor cores entiers, avec batching continu.
- **CPU / Mac / edge** : GGUF, viser **Q4_K_M** par défaut, **Q5_K_M** si la qualité prime.
- **Fine-tuning à petit budget** : QLoRA (NF4 + double quantization + adaptateurs LoRA).
- **Toujours** : valider avec perplexité **et** évals de tâches, sur vos données et vos langues, avant de figer le choix.

En somme, la quantization s'attaque au goulot mémoire (moins d'octets par token), tandis que les techniques de service (batching continu, PagedAttention, décodage spéculatif, parallélisme) saturent le GPU et amortissent chaque lecture de poids. C'est la combinaison des deux — modèle compressé **et** moteur efficace — qui fait passer un LLM d'une démo coûteuse à un service viable à grande échelle.