# Concepts de quantification

Un nombre quantifié ne peut prendre qu'une valeur parmi un petit ensemble, 256 pour int8 et 16 pour int4, si bien que toute méthode de quantification introduit une erreur d'arrondi. Les méthodes diffèrent par la manière dont elles empêchent cette erreur de dégrader la sortie du modèle. Cette page explique les choix qui sous-tendent ces différences : la façon dont les nombres sont mappés sur cet ensemble restreint, les tenseurs qui sont quantifiés, les types de données utilisés et le moment où la quantification est effectuée.

Si vous débutez avec la quantification dans Transformers, commencez par le [Présentation](./overview).

## Cartographier les valeurs sur moins de bits

La quantification mappe une plage de valeurs haute précision sur le petit ensemble d'entiers qu'un type à faible nombre de bits peut représenter. int8 peut représenter 256 entiers, `[-128, 127]`, et int4 peut en représenter 16.

*La quantification affine* est la correspondance la plus courante. Elle prend les valeurs minimale et maximale d'un tenseur et étire l'intervalle d'entiers sur celui-ci. Dans l'exemple ci-dessous, un tenseur dont les valeurs vont de -1,8 à 2,4 est mappé sur int8, et 0,0 correspond à -19.

```text
bf16  -1.8             0.0                    2.4
       ├────────────────┼──────────────────────┤
       ▼                ▼                      ▼
int8  -128             -19                    127
```

Deux paramètres définissent la correspondance.

- La *scale* `S` correspond à la taille d'une étape entière dans les unités d'origine.

$$ S = \frac{val_{max} - val_{min}}{q_{max} - q_{min}} $$

- Le *point zéro* `Z` est l'entier qui représente `0.0`.

$$ Z = q_{min} - round\left(\frac{val_{min}}{S}\right) $$

Une valeur `x` est quantifiée en l'entier `q` en la divisant par l'échelle et en la décalant par le point zéro.

$$ q = round\left(\frac{x}{S} + Z\right) $$

La déquantification inverse la correspondance et restitue une approximation de `x`. L'écart entre `x` et son approximation est l'erreur d'arrondi.

$$ x \approx S \cdot (q - Z) $$

Dans l’exemple ci-dessus, `S` est d’environ 0,0165 et `Z` vaut -19. La valeur 0,5 est quantifiée en 11, et 11 est déquantifié en environ 0,494, soit une erreur d’arrondi d’environ 0,006.

```text
┌─────┐  quantize   ┌────┐ dequantize   ┌───────┐
│ 0.5 │ ──────────▶ │ 11 │ ───────────▶ │ 0.494 │
└─────┘             └────┘              └───────┘
```

La quantification affine existe sous deux formes.

- La quantification *asymétrique* utilise les formules ci-dessus. Elle mappe la plage exacte, de sorte qu'aucune valeur représentable ne soit gaspillée lorsque les poids ne sont pas centrés sur zéro.
- La quantification *symétrique* suppose que la plage est `[-a, a]` et fixe `Z` à 0, de sorte que seul l'échelle est stockée. Les calculs sont moins coûteux, mais certaines valeurs représentables restent inutilisées lorsque les poids sont biaisés d'un côté de zéro.

## Choisir la granularité

Une seule échelle pour un tenseur entier est peu coûteuse à stocker, mais une seule valeur aberrante importante étire l’intervalle et écarte davantage les valeurs représentables pour tous les autres poids. Des groupes plus petits permettent à chaque échelle de couvrir un intervalle plus restreint.

- Per-tensor utilise une seule échelle et un seul point zéro pour l’ensemble du tenseur.
- Per-channel utilise une échelle pour chaque canal de sortie, qui correspond à chaque ligne d'une matrice de poids.
- Par groupe, ou par bloc, utilise une échelle pour chaque groupe de valeurs consécutives, par exemple 128 poids. Chaque groupe nécessite un stockage supplémentaire pour son échelle.

Le schéma ci-dessous présente les trois options sur une matrice de poids à trois lignes. Chaque `s` est une échelle.

```text
per-tensor        per-channel           per-group
┌────────────┐    ┌────────────┐        ┌──────┬──────┐
│            │    │            │ s0     │  s0  │  s1  │
│            │    ├────────────┤        ├──────┼──────┤
│     s      │    │            │ s1     │  s2  │  s3  │
│            │    ├────────────┤        ├──────┼──────┤
│            │    │            │ s2     │  s4  │  s5  │
└────────────┘    └────────────┘        └──────┴──────┘
```

La plupart des méthodes à faible nombre de bits quantisent par groupe. [GPTQ](./gptq) utilise par défaut des groupes de 128 poids, et [Fine-grained FP8](./finegrained_fp8) utilise par défaut des blocs de 128x128. Les formats FP4 présentés dans [Data types](#data-types) intègrent la taille du bloc directement dans le format.

## Quantifier les poids ou les activations

Les méthodes diffèrent quant aux tenseurs qu'elles quantifient, et c'est ce qui détermine l'endroit où s'exécute la multiplication de matrices.

- Les méthodes *Weight-only* stockent les poids en basse précision et laissent les activations en bf16 ou fp16. Avant chaque matmul, le noyau déquantise les poids vers un dtype de calcul de plus haute précision. bitsandbytes 4-bit, AWQ, GPTQ et HQQ fonctionnent ainsi. Dans bitsandbytes, `bnb_4bit_compute_dtype` définit ce dtype de calcul.
- Les méthodes *poids-activations* quantifient également les activations, et la multiplication matricielle s'exécute directement en basse précision. Cela nécessite un matériel prenant en charge la multiplication matricielle en basse précision, comme FP8 sur les GPU récents. Le schéma est souvent noté W8A8 pour des poids et des activations sur 8 bits.

Les méthodes de quantification « weight-only » économisent de la mémoire, mais n’accélèrent pas toujours l’exécution, car la déquantification ajoute du travail à chaque multiplication matricielle. Elles accélèrent la génération lorsque le modèle est *limité par la mémoire*. En générant un jeton à la fois, une GPU passe la majeure partie de chaque étape à lire les poids depuis la mémoire, plutôt qu’à effectuer des calculs. Lire des poids en 4 bits ne transfère qu’un quart des données par rapport aux poids en bf16, et cette économie peut compenser le coût de la déquantification.

Les méthodes combinant poids et activations sont utiles lorsque le modèle est *limité par le calcul*, comme lors du préremplissage ou avec de grands lots, car la multiplication matricielle en basse précision est elle-même plus rapide. Certaines méthodes basculent entre les deux. NVFP4 exécute une multiplication matricielle avec des poids sur 4 bits et des activations sur 4 bits pour le préremplissage, et une multiplication matricielle avec des poids sur 4 bits et des activations sur 16 bits pour le décodage.

## Types de données

Le type de données définit les valeurs qu'un nombre quantifié peut représenter.

### int8 et int4

int8 et int4 représentent des entiers régulièrement espacés et utilisent la transformation affine décrite ci-dessus. int4 ne peut représenter que 16 valeurs, ce qui entraîne une perte de précision plus importante que int8. Les méthodes utilisant int4 quantifient généralement par groupe ou recourent à une calibration pour maintenir la précision.

La plupart des matériels ne disposent pas de type de stockage sur 4 bits, de sorte que deux valeurs int4 sont *empaquétées* dans un octet. Par exemple, la première valeur peut occuper les 4 bits de poids faible et la seconde les 4 bits de poids fort. Les noyaux dépaquetent les valeurs avant de les utiliser dans les calculs.

```text
┌──────┐  ┌──────┐        ┌──────┬──────┐
│ val1 │  │ val2 │        │ val2 │ val1 │
│ 0101 │  │ 0011 │   →    │ 0011 │ 0101 │  1 byte
└──────┘  └──────┘        └──────┴──────┘
  int4      int4            high   low
```

Certains types 4 bits répartissent leurs valeurs représentables de manière inégale afin de correspondre à la distribution des poids. bitsandbytes NF4 place davantage de ses valeurs près de zéro, là où se trouvent la plupart des poids.

### FP8

FP8 conserve la disposition en virgule flottante composée d'un signe, d'un exposant et d'une mantisse sur 8 bits. Ses valeurs représentables sont plus rapprochées près de zéro et plus espacées pour les grandes amplitudes, ce qui correspond à la distribution des valeurs dans les poids et les activations.

FP8 existe en deux variantes.

- E4M3 comporte 4 bits d'exposant et 3 bits de mantisse. Il est plus précis mais couvre une plage plus restreinte. C'est le choix habituel pour les poids et les activations lors de l'inférence.
- E5M2 comporte 5 bits d'exposant et 2 bits de mantisse. Il couvre une plage plus large avec une précision moindre.

```text
E4M3   ┌─┬───────┬─────┐
       │S│E E E E│M M M│
       └─┴───────┴─────┘
E5M2   ┌─┬─────────┬───┐
       │S│E E E E E│M M│
       └─┴─────────┴───┘
  S sign   E exponent   M mantissa
```

Les produits matriciels FP8 nécessitent un support matériel, tel que des GPU NVIDIA avec une capacité de calcul 8.9 ou supérieure (Ada, Hopper, Blackwell), AMD Instinct MI300 et Intel XPU. Transformers prend en charge le FP8 via [Fine-grained FP8](./finegrained_fp8), [FBGEMM FP8](./fbgemm_fp8) et [compressed-tensors](./compressed_tensors).

### FP4 avec des échelles par bloc

FP4 comporte 1 bit de signe, 2 bits d'exposant et 1 bit de mantisse (E2M1). Il ne représente que les valeurs 0, 0,5, 1, 1,5, 2, 3, 4 et 6, en positif comme en négatif. Avec si peu de valeurs, les formats FP4 ne peuvent pas fonctionner avec une seule échelle par tenseur. Ils associent une échelle à chaque petit bloc de valeurs et stockent ces échelles comme partie intégrante du format.

| Format | Taille du bloc | Mise à l'échelle par bloc | Échelle supplémentaire |
|---|---|---|---|
| [MXFP4](./mxfp4) | 32 valeurs | Puissance de deux sur 8 bits (E8M0) | Aucun |
| [NVFP4](./nvfp4) | 16 valeurs | FP8 | Une échelle FP32 par tenseur |

MXFP4 est un format de micro-échelle. Une échelle de puissance de deux ne décale que l'exposant, ce qui rend son application peu coûteuse, mais elle ne peut prendre que des valeurs qui sont des puissances de deux. NVFP4 utilise des blocs plus petits et une échelle FP8 qui peut prendre des valeurs situées entre des puissances de deux. Cela préserve davantage de précision, au prix d'une échelle par tenseur qui maintient les échelles de bloc FP8 dans la plage admissible. [Four Over Six](./fouroversix) et [FP-Quant](./fp_quant) produisent également des poids NVFP4 ou MXFP4.

La même idée de blocs apparaît à 8 bits. MXFP8 stocke des valeurs FP8 avec des échelles de blocs puissances de deux, et [Fine-grained FP8](./finegrained_fp8) les charge.

### Codebooks

En dessous de 4 bits, certaines méthodes cessent d'arrondir chaque poids à la valeur représentable la plus proche. Elles quantifient des groupes de poids ensemble sous forme de *vecteurs*. Un codebook contient un ensemble de vecteurs appris, et chaque groupe de poids est stocké sous la forme de l'indice de son vecteur le plus proche. [AQLM](./aqlm) utilise par défaut des groupes de 8 poids et un indice sur 16 bits, ce qui équivaut à 2 bits par poids. [VPTQ](./vptq) utilise également des codebooks. La recherche de bons codebooks nécessite une longue optimisation sur des données de calibration, de sorte que ces modèles sont quantifiés à l'avance et chargés sous forme de points de contrôle.

## Lors de la quantification

Un modèle est quantifié soit après l’entraînement, soit pendant celui-ci. La plupart des méthodes de Transformers quantisent après l’entraînement.

### Quantification après l'entraînement

*La quantification après entraînement (PTQ)* quantifie un modèle déjà entraîné, sans modifier ce qu'il a appris. Les méthodes PTQ diffèrent selon qu'elles nécessitent des données ou non, ce qui détermine le groupe auquel une méthode est rattachée dans la table des matières de la documentation.

- En l’absence de données de calibration, les échelles sont dérivées uniquement des poids, et la quantification est suffisamment rapide pour s’exécuter pendant le chargement du modèle. bitsandbytes, HQQ et torchao fonctionnent ainsi. Ces méthodes sont répertoriées sous *Quantize while loading*.
- Avec des données de calibration, la méthode fait passer un petit jeu de données à travers le modèle et utilise les activations pour déterminer comment quantiser chaque couche. La calibration donne généralement une meilleure précision à 4 bits et en dessous, mais elle prend de quelques minutes à quelques heures.

Les méthodes de calibration utilisent les activations de différentes manières. GPTQ quantifie une couche colonne par colonne et ajuste les colonnes restantes pour compenser l'erreur d'arrondi de chaque colonne. AWQ identifie les canaux de poids présentant les activations les plus élevées et les met à l'échelle avant l'arrondi afin de les protéger.

C'est la manière dont les exécutions de calibration sont gérées qui détermine comment vous utilisez la méthode dans Transformers.

- [GPTQ](./gptq) effectue la calibration à l’intérieur de `from_pretrained` lorsque vous passez un `dataset` à [GPTQConfig](/docs/transformers/main/fr/main_classes/quantization#transformers.GPTQConfig), c’est pourquoi il est répertorié sous *Quantize while loading*.
- D'autres méthodes de calibration, comme AWQ, AutoRound, AQLM et VPTQ, effectuent la calibration dans leurs propres bibliothèques. Transformers charge le résultat, c'est pourquoi elles sont répertoriées sous *Charger des points de contrôle pré-quantifiés*.

### Entraînement tenant compte de la quantification

*l’entraînement tenant compte de la quantification (QAT)* simule la quantification pendant l’entraînement. Le passage avant arrondit les valeurs comme le ferait la quantification, tandis que les gradients mettent à jour les poids en pleine précision, ce qui permet au modèle d’apprendre à tolérer l’erreur d’arrondi. Le QAT offre généralement la meilleure précision à très faible nombre de bits, mais il nécessite d’entraîner le modèle.

Les modèles [BitNet](./bitnet) sont entraînés de cette manière, avec leurs poids de 1,58 bits dans la boucle, et Transformers charge les points de contrôle entraînés.

## Ressources

- [Fondamentaux de la quantification avec Hugging Face](https://www.deeplearning.ai/short-courses/quantization-fundamentals-with-hugging-face/)
- [La quantification en profondeur](https://www.deeplearning.ai/short-courses/quantization-in-depth)
- [Introduction à la quantification, concoctée avec 🤗 et 💗🧑‍🍳](https://huggingface.co/blog/merve/quantization)
- [EfficientML.ai Cours 5 - Quantification, partie I](https://www.youtube.com/watch?v=RP23-dRVDWM)
- [Faire avancer l'apprentissage profond à partir des principes fondamentaux](https://horace.io/brrr_intro.html)
- [Accélérer l'IA générative avec PyTorch, partie 2 : Optimisations des LLM](https://pytorch.org/blog/accelerating-generative-ai-2/)

