Aller au contenu
The loss curve

Chapitre 19 · 10 min

Quantification simple

Réduis ton checkpoint local et son coût d’inférence avec la quantification INT8, puis vois jusqu’où vont les méthodes de production.

Un entraîné est une collection de matrices de poids remplies de nombres float32. Un modèle d’un milliard de en float32 pèse 4 Go. Le même en int8 pèse 1 Go. Le même en int4, 500 Mo. L’écart de qualité en sortie entre float32 et int8 est faible — souvent imperceptible — et le gain de vitesse à l’ est réel : les CPU et GPU modernes ont des chemins dédiés à l’arithmétique entière.

C’est la : une idée qui tient en une ligne, appliquée systématiquement à chaque matrice de poids, et qui rend les grands modèles assez bon marché pour tourner sur du matériel grand public.

1. Quantification uniforme symétrique

Le schéma le plus simple : choisir une échelle s, stocker chaque poids sous forme de petit entier, puis multiplier par l’échelle pour reconstruire une approximation :

q=round(w/s),w^=qsq = \text{round}(w / s), \qquad \hat{w} = q \cdot s

Pour l’INT8 symétrique (le cas courant), s = max(|w|) / 127. Chaque poids w devient alors un entier dans [-127, 127]. Pour utiliser ces poids à l’, on multiplie chaque entier par s. L’erreur vaut au plus s/2 par poids — bornée par la précision de la , indépendamment de la magnitude.

Écris la fonction d’aller-retour. Le chapitre l’applique à une fausse matrice de poids et te montre les courbes originale et reconstruite à différentes largeurs de bits.

À toi de jouer · JavaScript

Fais varier la largeur de bits. À 8 bits, la courbe reconstruite est visuellement indiscernable de l’originale. À 4 bits, elle reste reconnaissable mais avance en escaliers. À 2 bits, tu as perdu l’essentiel de la forme — tu ne peux exprimer que 4 valeurs sur toute la plage, et c’est en général beaucoup trop grossier pour des poids de réseau de neurones.

2. Pourquoi ça marche, tout simplement

Quantifier une seule matrice de poids te donne de la compression gratuitement, mais le modèle doit ensuite calculer avec ces poids. Si l’erreur de se composait de couche en couche, la qualité s’effondrerait.

En pratique, elle ne se compose pas — pour deux raisons :

  • Les réseaux de neurones sont robustes aux petites perturbations, et particulièrement aux perturbations de poids. L’ produit des modèles dont la surface de est à peu près plate au voisinage immédiat des poids appris ; la déplace les poids à l’intérieur de ce voisinage.
  • L’erreur est de moyenne nulle. Les arrondis aléatoires se compensent au fil des multiplications. La perturbation en sortie d’un produit matrice-vecteur croît comme √n · s/2, pas comme n · s/2, par le théorème central limite.

Pour un typique quantifié en INT8, la sur un jeu de test tenu à l’écart monte d’environ 1 à 2 %. Selon la plupart des mesures, c’est le même modèle.

3. Où la quantification naïve échoue

Le schéma ci-dessus — une seule échelle par matrice de poids — laisse de la performance sur la table. Deux raffinements sont utilisés en pratique :

  • Les échelles par canal (une par ligne de W) capturent le fait que des canaux de sortie différents ont souvent des distributions de magnitude différentes. Une amélioration gratuite.
  • La attentive aux outliers (.int8, GPTQ, AWQ) traite le fait qu’une petite fraction des poids est très grande et domine le max(|w|). Deux stratégies : stocker les outliers en précision supérieure, ou les remettre à l’échelle avant de quantifier.

Les deux ajoutent de la complexité mais récupèrent l’essentiel de l’écart de sur les grands modèles. Pour notre périmètre, le schéma symétrique simple ci-dessus suffit.

4. La version PyTorch

Quantifier un modèle après l’ (sans ) s’appelle la post- (PTQ). Voici la version minimale, avec les outils intégrés de PyTorch. Sauvegarde-la dans scripts/quantize.py :

"""scripts/quantize.py — apply dynamic INT8 quantization to a trained model."""
import torch
 
from llm.model import GPT, GPTConfig
 
# [1]
cfg = GPTConfig()
model = GPT(cfg)
model.load_state_dict(torch.load("checkpoints/model.pt", map_location="cpu"))
model.eval()
 
# Dynamic quantization: weights become INT8 at load time;
# activations stay float and are quantized on-the-fly during the forward pass.
# [2]
qmodel = torch.quantization.quantize_dynamic(
    model,
    {torch.nn.Linear},  # which layer types to quantize
    dtype=torch.qint8,
)
 
# Compare file sizes
# [3]
torch.save(model.state_dict(), "checkpoints/model_fp32.pt")
torch.save(qmodel.state_dict(), "checkpoints/model_int8.pt")
 
import os
# [4]
fp32_size = os.path.getsize("checkpoints/model_fp32.pt")
int8_size = os.path.getsize("checkpoints/model_int8.pt")
print(f"fp32: {fp32_size / 1024:.1f} KB")
print(f"int8: {int8_size / 1024:.1f} KB")
print(f"ratio: {fp32_size / int8_size:.2f}x")

Lis ce script comme une mesure avant/après :

  • [1] charge le normal sur CPU. Ici, la est surtout un gain CPU au moment de l’.
  • [2] quantize_dynamic repère les couches nn.Linear et stocke leurs poids sous forme INT8. Les activations restent en flottant, donc tu n’as besoin d’aucun jeu de calibration.
  • [3] sauvegarde les deux state_dict pour que tu puisses comparer les fichiers directement.
  • [4] mesure le rapport de taille avant/après. Ça ne réentraîne rien : c’est de la compression post-.

Lance-le :

python -m scripts.quantize
python -m scripts.quantize
python -m scripts.quantize

Pour notre modèle de 14M de , tu devrais voir une réduction de taille d’environ 3× (un peu moins que 4×, parce que certains buffers ne se quantifient pas). L’ sur CPU est à peu près 2× plus rapide. La monte de moins de 1 %.

Et INT4, et en dessous ?

Tu peux continuer à descendre. La INT4 (4 bits par poids) donne 8× de compression et c’est le défaut des installations d’ locale façon llama.cpp. L’INT2 ne marche généralement pas — trop peu de niveaux.

L’état de l’art (GPTQ, AWQ) fait de l’INT4 avec une perte de qualité très faible. L’astuce : utiliser un petit jeu de calibration et choisir le schéma de qui minimise la après sur ce jeu, et pas seulement l’erreur de reconstruction poids par poids. On ne le couvre pas ici ; le principe est le même et l’implémentation existe dans toute bibliothèque de de production.

Recap

  • La échange un peu de qualité contre 4 à 8× moins de taille et 2 à 4× plus de vitesse à l’.
  • La uniforme symétrique est le schéma le plus simple : q = round(w / s), avec s = max(|w|) / (2^(bits-1) - 1).
  • Pourquoi ça marche : la sortie d’un réseau de neurones est robuste aux petites perturbations de poids, et l’erreur de se moyenne sur un grand nombre de produits.
  • L’INT8 est essentiellement gratuit ; l’INT4 avec de bonnes méthodes coûte 2 à 5 % de ; l’INT2 ne fonctionne généralement pas.
  • quantize_dynamic de PyTorch tient en une ligne, gère les nn.Linear à ta place, et c’est le bon défaut pour l’ d’un .
  • Ton projet local a maintenant scripts/quantize.py et un plus petit à comparer à l’original.

Pour aller plus loin

Prochaine étape : parler à ton modèle — le est entraîné et moins cher à faire tourner. Il reste à l’emballer dans la plus petite interface de chat honnête.

Questions fréquentes

Qu'est-ce que la quantification dans un modèle de langage ?

Stocker les poids en entiers de basse précision plutôt qu'en flottants. La quantification INT8 symétrique fait correspondre chaque poids à un entier de [-127, 127] avec un seul facteur d'échelle par matrice, ce qui divise la taille du modèle par environ quatre et accélère l'inférence sur du matériel doté de chemins entiers — en général au prix d'un ou deux pour cent de perplexité.

Pourquoi la quantification abîme-t-elle si peu la qualité ?

Deux raisons. Un réseau entraîné se trouve sur une surface de loss à peu près plate au voisinage de ses poids, donc les déplacer un peu à l'intérieur de ce voisinage change peu de chose. Et l'erreur d'arrondi est de moyenne nulle, donc elle se compense au fil d'un produit matriciel : la perturbation croît comme √n·s/2 et non comme n·s/2.

Jusqu'où peut-on descendre ?

L'INT8 est essentiellement gratuit. L'INT4 donne 8× de compression et c'est le défaut des installations d'inférence locale façon llama.cpp, au prix de quelques pour cent de perplexité avec de bonnes méthodes comme GPTQ ou AWQ. L'INT2 ne fonctionne généralement pas : quatre valeurs sur toute la plage, c'est trop grossier pour des poids de réseau de neurones.

Quelle différence entre quantification dynamique et statique ?

La quantification dynamique stocke les poids en INT8 et quantifie les activations à la volée à chaque passe avant, sans aucune donnée de calibration — le bon défaut pour les transformers, où le débit des multiplications matricielles domine. La statique pré-quantifie aussi les activations : plus rapide, mais elle exige une passe de calibration sur des entrées représentatives.