Avant-propos

Cet article est une traduction de celui de Maarten GROOTENDORST : A Visual Guide to Quantization.
Maarten est co-auteur du livre Hands-On Large Language Models.


Introduction

Comme leur nom l’indique, les grands modèles de langage (LLM pour Large Language Models) sont souvent trop volumineux pour être exécutés sur du matériel grand public. Ces modèles peuvent dépasser des milliards de paramètres et ont généralement besoin de GPU avec de grandes quantités de VRAM pour l’inférence.
À ce titre, de plus en plus de recherches ont été axées sur la réduction de la taille de ces modèles grâce à une amélioration de l‘entraînement, les adapteurs, etc.
L’une des principales techniques dans ce domaine s’appelle la quantification.

Dans cet article, nous allons introduire le concept de la quantification dans le contexte de la modélisation du langage et explorer les concepts un par un pour en comprendre l’intuition. Nous explorerons diverses méthodologies, des cas d’utilisation et les principes qui sous-tendent la quantification, via plus de \(50\) visuels.



Partie 1 : Le « problème » des LLM

Les LLM tirent leur nom du nombre de paramètres qu’ils contiennent. De nos jours, ces modèles ont généralement des milliards de paramètres (principalement des poids) qui peuvent être assez coûteux à stocker.
Durant l’inférence, les activations sont créées en tant que produit de l’entrée et des poids, qui peuvent également être assez importants.

Par conséquent, nous aimerions représenter des milliards de valeurs aussi efficacement que possible, en minimisant la quantité d’espace dont nous avons besoin pour stocker une valeur donnée.
Commençons en explorant comment les valeurs numériques sont représentées avant de voir comment les optimiser.

Comment représenter des valeurs numériques

Une valeur donnée est souvent représentée par un nombre à virgule flottante (float en anglais) : un nombre positif ou négatif avec une virgule.
Ces valeurs sont représentées par des « bits », ou chiffres binaires. La norme IEEE-754 décrit comment les bits peuvent représenter la valeur via trois fonctions : le signe, l’exposant et la fraction (ou mantisse).

Ensemble, ces trois aspects peuvent être utilisés pour calculer une valeur à partir d’un certain ensemble de valeurs binaires :

Plus nous utilisons de bits pour représenter une valeur, plus elle est généralement précise :


Contraintes de mémoire

Plus nous avons de bits disponibles, plus la plage de valeurs qui peut être représentée est grande.

Original 75505.0 1.8e-42
64-bits 75505.0 1.8e-42
32-bits 75505.0 1.80066e-42
16-bits inf 0.0


L’intervalle de nombres qu’une représentation donnée peut prendre est appelé la plage dynamique (dynamic range en anglais) alors que la distance entre deux valeurs voisines est appelée la précision.

Une caractéristique astucieuse de ces bits est que nous pouvons calculer la quantité de mémoire dont votre machine a besoin pour stocker une valeur donnée. Comme il y a \(8\) bits dans un octet, nous pouvons créer une formule basique pour la plupart des représentations en virgule flottante :
mémoire = nombre de paramètres × (nombre de bits) / \(8\).
En pratique, c’est un peu plus complexe. La quantité de (V)RAM nécessaire pour l’inférence, dépend aussi de la taille de contexte et de l’architecture.

Appliquons cette formule. Supposons que nous ayons un modèle de \(70\) milliards de paramètres. La plupart des modèles sont représentés nativement avec en FP32 (souvent appelé « pleine précision » ou full-precision), ce qui nécessiterait \(280\) Go de mémoire juste pour charger le modèle. En effet :

  • 64 bits = 70Mds × 64/8 ≈ 560 GB
  • 32 bits = 70Mds × 32/8 ≈ 280 GB
  • 16 bits = 70Mds × 16/8 ≈ 140 GB

De ce fait, c’est très intéressant de pouvoir minimiser le nombre de bits pour représenter les paramètres de votre modèle (ainsi que pendant l’entraînement !). Cependant, à mesure que la précision diminue, l’accuracy des modèles décroit généralement aussi.
Nous voulons réduire le nombre de bits représentant des valeurs tout en conservant l’accuracy… C’est là qu’intervient la quantification !


Partie 2 : Introduction à la quantification

La quantification vise à réduire la précision des paramètres d’un modèle en passant de largeurs de bits supérieures (comme la virgule flottante \(32\) bits) à des largeurs de bits inférieures (comme les entiers \(8\) bits).

Il y a souvent une certaine perte de précision (granularité) lors de la réduction du nombre de bits pour représenter les paramètres d’origine.
Pour illustrer cet effet, nous pouvons prendre n’importe quelle image et n’utiliser que \(8\) couleurs pour la représenter :

Image adaptée de celle de Slava Sidorov

Remarquez que la partie zoomée semble plus « granuleuse » que l’originale puisque nous utilisons moins de couleurs pour la représenter.
L’objectif principal de la quantification est de réduire le nombre de bits (couleurs) nécessaires pour représenter les paramètres d’origine tout en préservant au mieux la précision des paramètres d’origine.

Types de données courants

Tout d’abord, examinons les types de données courants et l’impact de leur utilisation plutôt que des représentations en \(32\) bits (appelées FP32 pour full-precision soit la pleine précision).

FP16

Prenons l’exemple d’un passage de \(32\) à \(16\) bits (appelé FP16 ou half precision soit la demi-précision) :

Remarquez que la plage de valeurs que la FP16 peut prendre est un peu plus petite que celle de la FP32.

BF16

Pour obtenir une plage de valeurs similaire à celle de FP32, la précision bfloat 16 a été introduite par Google (le « B » se réfaire au Brain de Google Brain qui a introduit la méthode) comme un type de « FP32 tronqué » :

BF16 utilise la même quantité de bits que FP16, mais peut prendre une gamme plus large de valeurs et est souvent utilisé dans les applications d’apprentissage profond.

INT8

Lorsque nous réduisons encore davantage le nombre de bits, nous nous rapprochons du domaine de représentations basées sur des entiers plutôt que des représentations en virgule flottante. À titre d’exemple, le passage de FP32 à INT8, qui n’a que \(8\) bits, donne un quart du nombre de bits d’origine :

Selon le matériel, les calculs basés sur des nombres entiers peuvent être plus rapides que les calculs en virgule flottante, mais ce n’est pas toujours le cas. Cependant, les calculs sont généralement plus rapides lorsque l’on utilise moins de bits.
Pour chaque réduction de bits, une correspondance est effectuée pour « comprimer » les représentations initiales de FP32 dans les bits inférieurs.
En pratique, nous n’avons pas besoin de faire correspondance toute la plage FP32 [\(-3.4e38,3.4e38\)] dans INT8. Nous avons simplement besoin de trouver un moyen de faire correspondre la plage de nos données (les paramètres du modèle) dans INT8.
Les méthodes courantes de compression/correspondance sont la quantification symétrique et asymétrique qui sont des formes de correspondance linéaire.
Explorons ces méthodes pour quantifier de FP32 à INT8.

Quantification symétrique

Dans la quantification symétrique, la plage de valeurs d’origine est mise en correspondance avec une plage symétrique autour de \(0\) dans l’espace quantifié. Dans les exemples précédents, remarquez que les plages avant et après la quantification restent centrées autour de \(0\).
Cela signifie que la valeur \(0\) dans l’espace à virgule flottante est aussi \(0\) dans l’espace quantifié.

Un bon exemple d’une forme de quantification symétrique est la quantification via maximum absolu (absmax).
Étant donné une liste de valeurs, nous prenons la valeur absolue la plus élevée (α) comme plage pour effectuer les correspondances linéaires.

Notez que la plage de valeurs [-127,127] représente la plage restreinte. La plage non restreinte est [-128,127] et dépend de la méthode de quantification.

Comme il s’agit d’une correspondance linéaire centrée autour de \(0\), la formule est simple.
Nous calculons d’abord un facteur d’échelle \(s\) en utilisant :
\(b\), le nombre d’octets que l’on veut quantifier à (\(8\)),
\(α\), la valeur absolue la plus élevée,
Ensuite, nous utilisons le \(s\) pour quantifier l’entrée \(x\) :

En renseignant les valeurs, on obtient alors ce qui suit :

Pour retrouver les valeurs FP32 originales, nous pouvons utiliser le facteur d’échelle (\(s\)) calculé précédemment pour déquantifier les valeurs quantifiées.

L’application du processus de quantification puis de déquantification pour récupérer l’original se présente comme suit :

Vous pouvez voir que certaines valeurs, telles que \(3.08\) et \(3.02\), sont assignées à l’INT8, c’est-à-dire \(36\) dans le graphique. Lorsque vous déquantifiez les valeurs pour revenir à FP32, elles perdent de la précision et ne sont plus distinguables.
C’est ce que l’on appelle souvent l’erreur de quantification, que l’on peut calculer en déterminant la différence entre la valeur originale et la valeur déquantifiée.

En général, plus le nombre de bits est faible, plus nous avons tendance à avoir d’erreurs de quantification.

Quantification asymétrique

La quantification asymétrique, en revanche, n’est pas symétrique autour de \(0\). Au lieu de cela, elle fait correspondre les valeurs minimales (\(β\)) et maximales (\(α\)) de la plage flottante aux valeurs minimales et maximales de la plage quantifiée.
La méthode que nous allons explorer s’appelle la quantification du point \(0\).

Vous avez remarqué que le \(0\) a changé de position ? C’est pourquoi on parle de quantification asymétrique. Les valeurs min/max ont des distances différentes par rapport à \(0\) dans la plage [\(-7.59,10.8\)].
En raison de sa position décalée, nous devons calculer le \(0\) pour la plage INT8 afin d’effectuer la correspondance linéaire. Comme précédemment, nous devons également calculer un facteur d’échelle (\(s\)), mais en utilisant la différence de la plage INT8 à la place [\(-128,127\)].

Remarquez que c’est un peu plus compliqué en raison de la nécessité de calculer le point du \(0\) (\(z\)) pour la plage en INT8 afin de modifier les poids.
Comme précédemment, remplissons la formule :

Pour déquantifier de INT8 à FP32, nous devrons utiliser le facteur d’échelle (\(s\)) et point \(0\) (\(z\)). En dehors de cela, la déquantification est simple :

Lorsque nous mettons la quantification symétrique et asymétrique côte à côte, nous pouvons rapidement voir la différence entre les méthodes :



Elagage de la plage

Dans nos exemples précédents, nous avons exploré comment la plage de valeurs d’un vecteur donné pouvait être associée à une représentation avec moins de bits. Bien que cela permette de faire correspondre toute la gamme des valeurs vectorielles, cela présente un inconvénient majeur, à savoir les valeurs aberrantes.
Imaginez que vous ayez un vecteur avec les valeurs suivantes :

On observe qu’une valeur est beaucoup plus grande que toutes les autres et pourrait être considérée comme une valeur aberrante. Si nous devions représenter l’entièreté de la plage de ce vecteur, toutes les petites valeurs seraient représentées sur le même bit et perdraient leur facteur de différenciation :

Il s'agit de la méthode absmax que nous avons utilisée précédemment. Notez que le même comportement se produit avec la quantification asymétrique si nous n'appliquons pas d’élagage.

Au lieu de cela, nous pouvons choisir d’élaguer certaines valeurs. L’élagage implique la définition d’une plage dynamique différente des valeurs d’origine, de sorte que toutes les valeurs aberrantes obtiennent la même valeur.
Dans l’exemple ci-dessous, si nous devions définir manuellement la plage dynamique à [\(- 5,5\)], toutes les valeurs en dehors seront soit associées à \(-127\), soit à \(127\), quelle que soit leur valeur :

L’avantage majeur est que l’erreur de quantification des valeurs non aberrantes est considérablement réduite. Cependant, celle des non aberrantes augmente.

Étalonnage

Dans l’exemple précédant, nous avons montré une méthode naïve consistant à choisir une plage arbitraire de [\(- 5,5\)]. Le processus de sélection de cette plage est connu sous le nom d’étalonnage, où le but est de trouver une plage qui comprend les plus de valeurs possibles tout en minimisant l’erreur de quantification.
La réalisation de cette étape n’est pas la même pour tous les types de paramètres.

Poids (et biais)

Nous pouvons considérer les poids et les biais d’un LLM comme des valeurs statiques puisqu’ils sont connus avant l’exécution du modèle. Par exemple, le fichier de ~20GB de Llama 3 de Meta (2024) est composé principalement de ces derniers.

Comme il y a beaucoup moins de biais (millions) que de poids (milliards), les biais sont souvent maintenus avec une plus grande précision (en INT16 par exemple), et l’effort principal de quantification est consacré aux poids.
Pour les poids, qui sont statiques et connus, les techniques d’étalonnage permettant de choisir l’intervalle sont les suivantes :

  • Choix manuel d’un centile de la plage d’entrée
  • Optimiser l’erreur quadratique moyenne (MSE pour mean squared error) entre les poids originaux et quantifiés
  • Minimiser l’entropie (divergence de Kullback-Leibler) entre les valeurs originales et quantifiées

Choisir un centile, par exemple, conduirait à un comportement d’élagage similaire à celui que nous avons observé précédemment.


Activations

L’entrée qui est continuellement mise à jour tout au long du LLM est généralement appelée « activations ».

Notez que ces valeurs sont appelées activations car elles passent souvent par une fonction d’activation, comme une sigmoïde ou une ReLU.
Contrairement aux poids, les activations varient avec chaque donnée d’entrée introduite dans le modèle pendant l’inférence, ce qui rend difficile leur quantification précise.
Étant donné que ces valeurs sont mises à jour après chaque couche cachée, nous ne pouvons connaître leur valeur exacte qu’au moment de l’inférence, lorsque les données d’entrée passent par le modèle.

D’une manière générale, il existe deux méthodes pour calibrer la méthode de quantification des poids et des activations :
• Quantification post-entraînement (PTQ pour Post-Training Quantization)
• Quantification pendant l’entraînement (QAT pour Quantization Aware Training)


Partie 3 : Quantification post-entraînement

L’une des techniques de quantification les plus populaires est la quantification post-entraînement (PTQ). Il s’agit de quantifier les paramètres d’un modèle (poids et activations) après l’entraînement du modèle.
La quantification des poids est effectuée à l’aide d’une quantification symétrique ou asymétrique.
La quantification des activations, quant à elle, nécessite l’inférence du modèle pour obtenir leur distribution potentielle puisque nous ne connaissons pas leur plage.
Il existe deux formes de quantification des activations :
• Quantification dynamique
• Quantification statique

Quantification dynamique

Une fois que les données passent par une couche cachée, ses activations sont collectées :

Cette distribution des activations est ensuite utilisée pour calculer le point \(0\) (\(z\)) et le facteur d’échelle (\(s\)) valeurs nécessaires pour quantifier la sortie :

Le processus est répété chaque fois que les données passent par une nouvelle couche. Par conséquent, chaque couche a ses propres valeurs \(s\) et \(z\) et donc des schémas de quantification différents.

Quantification statique

Contrairement à la quantification dynamique, la quantification statique ne calcule pas le point \(0\) (\(z\)) et le facteur d’échelle (\(s\)) pendant l’inférence, mais avant.
Pour trouver ces valeurs, un jeu de données d’étalonnage est utilisé et donné au modèle pour recueillir ces distributions potentielles.

Une fois ces valeurs collectées, nous pouvons calculer les valeurs \(s\) et \(z\) nécessaires pour effectuer la quantification pendant l’inférence.
Lors de l’inférence, les valeurs \(s\) et \(z\) ne sont pas recalculées, mais sont utilisées globalement pour toutes les activations afin de les quantifier.
En général, la quantification dynamique est un peu plus précise car elle ne tente de calculer les valeurs \(s\) et \(z\) que pour chaque couche cachée. Nénamoins, elle peut entraîner une augmentation du temps de calcul car ces valeurs doivent être calculées pour chaque couche cachée.
Au contraire, la quantification statique est moins précise mais plus rapide car elle connaît déjà les valeurs \(s\) et \(z\) utilisées pour la quantification.

Le royaume de la quantification 4 bits

Descendre en dessous de la quantification à \(8\) bits s’est avéré être une tâche difficile car l’erreur de quantification augmente à chaque perte de bit. Heureusement, il existe plusieurs façons intelligentes de réduire les bits à \(6\), \(4\) et même \(2\) bits (bien qu’il ne soit généralement pas conseillé de descendre en dessous de \(4\) bits en utilisant ces méthodes).
Nous allons explorer deux méthodes qui sont couramment partagées sur Hugging Face :
GPTQ de FRANTAR et al. (2022) (modèle entier sur GPU)
GGUF de GERGANOV (2023) (possibilité de décharger les couches sur le CPU)

GPTQ

GPTQ est sans doute l’une des méthodes les plus connues utilisées dans la pratique pour la quantification à \(4\) bits.
Elle utilise la quantification asymétrique et le fait couche par couche de sorte que chacune est traitée indépendamment avant de passer à la suivante :

Au cours de ce processus de quantification par couche, on convertit d’abord les poids de la couche de la hessienne inverse. Il s’agit d’une dérivée seconde de la fonction de perte du modèle, qui nous indique à quel point la sortie du modèle est sensible aux variations de chaque poids.
En simplifiant, cela démontre l’importance (inverse) de chaque poids dans une couche.
Les poids associés à des valeurs plus petites dans la matrice hessienne sont plus cruciaux car de petites modifications de ces poids peuvent entraîner des changements significatifs dans les performances du modèle.

Dans la hessienne inverse, les valeurs les plus faibles indiquent des poids plus « importants ».

Ensuite, nous quantifions et déquantifions le poids de la première ligne de notre matrice de poids :

Ce processus nous permet de calculer l’erreur de quantification (\(q\)) que nous pouvons pondérer à l’aide de la hessienne inverse (\(h_1\)) que nous avons calculée au préalable.
En somme, nous créons une erreur de quantification pondérée en fonction de l’importance des poids :

Puis, nous redistribuons cette erreur de quantification pondérée sur les autres poids de la ligne. Cela permet de maintenir la fonction globale et la sortie du réseau.
Par exemple, si nous procédons ainsi pour le deuxième poids, à savoir \(0,3\) (\(x_2\)), nous ajoutons l’erreur de quantification (\(q\)) multipliée par la hessienne inverse du deuxième poids (\(h_2\)).

Nous pouvons faire le même processus sur le troisième poids de la ligne :

Nous itérons sur ce processus de redistribution de l’erreur de quantification pondérée jusqu’à ce que toutes les valeurs soient quantifiées.
Cela fonctionne très bien car les poids sont généralement liés les uns aux autres. Ainsi, lorsqu’un poids présente une erreur de quantification, les poids connexes sont mis à jour en conséquence (par l’intermédiaire de la hessienne inverse).

Remarque : Les auteurs ont utilisé plusieurs astuces pour accélérer les calculs et améliorer les performances, comme l’ajout d’un facteur d’amortissement à la hessienne, le « lazy batching » et le pré-calcul d’informations à l’aide de la méthode de Cholesky. Nous conseillons au lecteur de visionner cette vidéo YouTube sur le sujet.

Notez que vous pouvez utiliser la librairie EXLlama2 si vous souhaitez une méthode de quantification visant à optimiser les performances et à améliorer la vitesse d’inférence.

GGUF

Bien que GPTQ soit une excellente méthode de quantification pour exécuter votre LLM en entier sur un GPU, il se peut que vous ne disposiez pas toujours de la capacité nécessaire. Au lieu de cela, nous pouvons utiliser GGUF pour décharger n’importe quelle couche du LLM sur le CPU.
Cela permet d’utiliser à la fois le CPU et le GPU lorsque la VRAM est insuffisante.
La méthode de quantification GGUF est fréquemment mise à jour et peut dépendre du niveau de quantification souhaité. Toutefois, le principe général est le suivant.
Tout d’abord, les poids d’une couche donnée sont divisés en super-blocs contenant chacun un ensemble de sous-blocs. De ces blocs, nous extrayons le facteur d’échelle (\(s\)) et l’alpha (\(α\)) :

Pour quantifier un sous-bloc donné, nous pouvons utiliser la quantification absmax que nous avons utilisée précédemment. Rappelons qu’elle multiplie un poids donné par le facteur d’échelle (\(s\)) :

Le facteur d’échelle est calculé à l’aide des informations du sous-bloc, mais la quantification est réalisée en utilisant les informations du super-bloc qui possède son propre facteur d’échelle :

Cette quantification par bloc utilise le facteur d’échelle (\(s_{super}\)) du super-bloc pour quantifier le facteur d’échelle (\(s_{sous}\)) du sous-bloc.
Le niveau de quantification de chaque facteur d’échelle peut être différent, le super-bloc ayant généralement une précision plus élevée que le facteur d’échelle du sous-bloc.
Pour illustrer notre propos, examinons quelques quantifications de niveaux différents (\(2\) bits, \(4\) bits et \(6\) bits) :

Nom Quantification du poids Echelle de quantification ($$s_{super}$$) Echelle de quantification ($$s_{sous}$$) Bits par poids (w) # Sous blocs Poids par bloc
Q2_K 2 bits 4 bits 2 bits 2,5625 16 16
Q4_K 4 bits 6 bits 4 bits 4,5 8 32
Q6_K 6 bits 8 bits 6 bits 6,5625 16 16


Note : Selon le type de quantification, une valeur minimale supplémentaire (\(m\)) est nécessaire pour ajuster le point \(0\).

Consultez la pull request pour obtenir une vue d’ensemble de tous les niveaux de quantification. Consultez également celle-ci pour plus d’informations sur la quantification à l’aide de matrices d’importance.


Partie 4 : Quantification pendant l’entraînement

Dans la troisième partie, nous avons vu comment quantifier un modèle après l’entraînement. L’inconvénient de cette approche est que la quantification ne prend pas en compte le processus d’entraînement.
C’est là qu’intervient la quantification pendant l’entraînement (QAT pour Quantization Aware Training).

La QAT a tendance à être plus précis que la PTQ puisque la quantification a déjà été prise en compte pendant l’entraînement. Cela fonctionne de la façon suivante.
Au cours de l’entraînement, des « fausses » quantifications sont introduites. Il s’agit par exemple de quantifier les poids en INT4 puis à les déquantifier en FP32 :

Cette approche permet au modèle de prendre en compte le processus de quantification durant l’entraînement, le calcul de la perte et les mises à jour des poids.
QAT explore la perte pour trouver des minima « larges » afin de minimiser les erreurs de quantification, car les minima « étroits » ont tendance à en entraîner de plus importantes.

Par exemple, imaginons que nous ne tenions pas compte de la quantification lors de la passe arrière. Nous choisissons le poids avec la plus petite perte selon la méthode de la descente du gradient. Cependant, cela introduirait une erreur de quantification plus importante s’il se trouve dans un minimum « étroit ».
En revanche, si nous tenons compte de la quantification, un poids actualisé différent sera sélectionné dans un minimum « large » avec une erreur de quantification beaucoup plus faible.

Ainsi, bien que PTQ ait une perte plus faible en haute précision (par exemple, FP32), QAT entraîne une perte plus faible en basse précision (par exemple, INT4), ce qui est notre objectif.

L’ère des LLM 1 bit : BitNet

Passer à \(4\) bits, comme nous l’avons vu précédemment, est déjà grosse réduction, mais que se passerait-il si nous réduisions encore plus ?
C’est là qu’intervient BitNet de WANG, MA et al. (2023), qui représente les poids d’un modèle avec \(1\) bit, en utilisant soit \(-1\), soit \(1\) pour un poids donné.
Pour ce faire, le processus de quantification directement injecté dans l’architecture du Transformer.
Pour rappel, l’architecture Transformer est utilisée comme base de la plupart des LLM et est composée de calculs qui impliquent des couches linéaires :

Ces couches linéaires sont généralement représentées avec une grande précision, par exemple FP16, et c’est là que résident la plupart des poids.
BitNet remplace ces couches linéaires par quelque chose que les auteurs appellent « BitLlinear » :

Une couche BitLinear fonctionne de la même manière qu’une couche linéaire standard et calcule la sortie sur la base des poids multipliés par l’activation.
En revanche, une couche bit-linéaire représente les poids d’un modèle sur 1 bit et les activations sur INT8 :

Une couche BitLineary, comme l’approche QAT, effectue une forme de « fausse » quantification pendant l’entraînement pour analyser l’effet de la quantification des poids et des activations :

Dans le papier les auteurs utilisent γ au lieu de α mais puisque nous avons utilisé α tout au long de nos exemples, nous poursuivons avec cette notation. De même, β n'est pas identique à ce que nous avons utilisée pour la quantification du point 0 mais la valeur absolue moyenne.


Passons en revue le BitLinear étape par étape.

Quantification des poids

Pendant l’entraînement, les poids sont stockés en INT8, puis quantifiés à \(1\) bit à l’aide d’une stratégie simple, appelée fonction signe.
En substance, on déplace la distribution des poids pour qu’elle soit centrée autour de \(0\), puis on attribue tout ce qui est à gauche de \(0\) à \(-1\) et tout ce qui est à droite à \(+1\) :

De plus, on traque une valeur \(β\) (valeur absolue moyenne) que nous utiliserons plus tard pour la déquantification.

Quantification des activations

Pour quantifier les activations, BitLinear utilise la quantification absmax pour convertir les activations FP16 en INT8 car elles doivent être plus précises pour la multiplication matricielle.

De plus, on traque \(α\) (valeur absolue maximale) que nous utiliserons plus tard pour la déquantification.

Déquantification

Nous avons traqué \(α\) (valeur absolue la plus élevée des activations) et \(β\) (valeur absolue moyenne des poids), car ces valeurs nous aideront à déquantifier en FP16 les activations.
Les activations de sortie sont redimensionnées avec {\(α\), \(γ\)} pour les déquantifier à la précision d’origine :

Et c’est tout ! Cette procédure est relativement simple et permet de représenter les modèles avec seulement deux valeurs : \(-1\) ou \(1\).
En utilisant cette procédure, les auteurs ont observé que plus la taille du modèle augmente, plus l’écart de performance entre un modèle \(1\)-bit et un modèle entraîné en FP16 se réduit.
Cependant, cela ne concerne que les modèles de grande taille (>\(30\)B paramètres) et l’écart avec les modèles plus petits reste assez important.

Tous les LLM sont en 1,58 bits

BitNet 1.58b de MA, WANG et al. (2024) a été introduit pour améliorer le problème de passage à l’échelle mentionné précédemment.
Dans cette nouvelle méthode, chaque poids du modèle n’est pas seulement \(-1\) ou \(1\), mais peut désormais également prendre \(0\) comme valeur, ce qui le rend ternaire. Il est intéressant de noter que l’ajout du \(0\) améliore considérablement le BitNet et permet un calcul beaucoup plus rapide.

Le pouvoir du 0

Alors, pourquoi l’ajout de \(0\) est-il une amélioration si majeure ?
Tout est lié à la multiplication matricielle !
Tout d’abord, voyons comment fonctionne la multiplication matricielle en général. Lors du calcul de la sortie, nous multiplions une matrice de poids par un vecteur d’entrée. Ci-dessous, la première multiplication de la première couche d’une matrice de poids est visualisée :

Notons que cette multiplication implique deux actions : multiplier les poids individuels avec l’entrée, puis les additionner.
BitNet 1.58b, en revanche, parvient à se passer de la multiplication puisque les poids ternaires vous indiquent essentiellement ce qui suit :
• \(1\) : Je veux ajouter cette valeur
• \(0\) : Je ne veux pas cette valeur
• \(-1\) : Je veux soustraire cette valeur
Par conséquent, vous n’avez besoin d’effectuer une addition que si vos poids sont quantifiés à 1,58 bit :

Cela permet non seulement d’accélérer considérablement les calculs, mais aussi de filtrer les caractéristiques.
En mettant un poids donné à \(0\), il est possible de l’ignorer au lieu d’ajouter ou de soustraire les poids comme c’est le cas avec les représentations sur \(1\) bit.

Quantification

Pour effectuer la quantification des poids, BitNet 1.58b utilise la quantification absmean qui est une variante de la quantification absmax que nous avons vue précédemment.
Il compresse simplement la distribution des poids et utilise la moyenne absolue (\(α\)) pour quantifier les valeurs. Ils sont ensuite arrondis à \(-1\), \(0\) ou \(1\) :

Par rapport à BitNet, la quantification de l’activation est la même à l’exception d’une chose. Au lieu d’étalonner les activations sur une plage [\(0,2b⁻¹\)], elles le sont sur [\(-2b⁻¹,2b⁻¹\)] du fait de la quantification absmax.
Et c’est tout ! La quantification 1,58 bit nécessitait (principalement) deux astuces :
• Ajouter l’option \(0\) pour créer des représentations ternaires [\(-1,0,1\)]
• la quantification absmean pour les poids

« Le BitNet 13B 1.58b est plus efficace, en termes de latence, d’utilisation de la mémoire et de consommation d’énergie qu’un LLM 3B FP16 »

En conséquence, nous obtenons des modèles légers car nous n’avons que 1,58 bits ce qui est efficace en termes de calcul !


Conclusion

Ainsi se termine notre voyage dans la quantification ! J’espère que cet article vous donnera une meilleure compréhension du potentiel de la quantification, de GPTQ, GGUF et du BitNet. Qui sait à quel point les modèles seront petits à l’avenir ?

Si vous voulez aller plus loin, je vous suggère les ressources suivantes :
• Les articles de blog d’Hugging Face et notamment :
   • Cet article sur la méthode de quantification LLM.int8().
   • Cet article sur la quantification des embeddings.
   • Cet article consacré au BitNet.
• Hugging Face a également un cours sur les fondamentaux de la quantification sur DeepLearning.AI.
• Un article de blog d’Eleuther.ai décrivant les mathématiques de base liées au calcul et à l’utilisation de la mémoire pour les Transformers.
• Cette application et celle-ci sont deux bonnes ressources pour calculer la (V)RAM dont vous avez besoin pour un modèle donné.
• Le papier sur le QLoRA pour de la quantification pour les méthodes PEFT.
• Une vidéo YouTube sur GPTQ expliquée de manière incroyablement intuitive.

Nous vous invitions également à jeter un œil à des librairies pour dédiées au sujet comme :
bitsandbytes (vous pouvez consulter cet article et cet article)
AutoGPTQ (vous pouvez consulter également cet article)
transfomers intègre également sous le capot ces deux librairies (vous pouvez lire cet article de blog sur le sujet, et surtout le documentation officielle)




Références




Citation

@inproceedings{quantification_blog_post,
author = {Loïck BOURDOIS},
title = {Un guide visuel sur la quantification},
year = {2025},
url = {https://lbourdois.github.io/blog/Quantification/}
}