FP4 / FP8 — Quantification en Point Flottant#

TL;DR — La quantification floating-point (FP4, FP8) est une alternative à la quantification entière (INT4, INT8) qui exploite la structure exponent/mantisse pour mieux représenter les distributions heavy-tailed des poids de LLMs. Le FP8 (E4M3, E5M2) est déjà supporté nativement sur NVIDIA Hopper H100 ; le FP4 (E2M1) arrive avec Blackwell B200. Les formats Microscaling (MX) standardisés par l'OCP ajoutent un shared scale par bloc de 32 valeurs, offrant le meilleur des deux mondes.


Floating-point vs Integer : le débat fondamental#

Niveau néophyte#

Imaginez que vous devez mesurer la taille de tous les humains sur Terre avec une règle. Une règle entière (INT) a des graduations régulières : un trait tous les centimètres, de 0 à 200 cm. Problème : la plupart des humains font entre 150 et 190 cm, là où les graduations sont très serrées et précises — mais les quelques personnes mesurant 2m20+ débordent de la règle.

Une règle flottante (FP) a des graduations non-uniformes : très précise près de la moyenne (1 mm de précision vers 170 cm), mais avec des graduations plus espacées pour les extrêmes (1 cm de précision vers 250 cm). Elle couvre une plage bien plus large avec le même nombre de graduations, en mettant la précision là où elle compte.

Les poids des LLMs sont comme la taille des humains : concentrés autour d'une moyenne, avec quelques outliers (valeurs extrêmes). Le floating-point gère mieux ces distributions.

Le problème des outliers en quantification LLM#

flowchart LR subgraph INT_box["INT8 : grille uniforme"] I1["[-0.5 ═════════════ +0.5]"] I2["❌ Outlier à +5.0 → SATURATION (clipping)"] I3["❌ Résolution gaspillée sur [-0.5, -0.3]"] end subgraph FP_box["FP8 E4M3 : grille non-uniforme"] F1["[████████████ ±448 ████████████]"] F2["✅ Outlier à +5.0 → REPRÉSENTÉ"] F3["✅ Résolution fine près de 0"] end

Les formats floating-point en détail#

Structure générale d'un nombre flottant#

block-beta columns 3 s["Signe (S)"] e["Exponent"] m["Mantissa"] s -->|"signe + ou -"| note_s[" "] e -->|"plage dynamique
(ordre de grandeur)"| note_e[" "] m -->|"précision
(digits significatifs)"| note_m["Valeur = (-1)^S × 2^(E - bias) × (1 + M/2^mant_bits)"]

L'insight clé : l'exponent gère la plage et la mantissa gère la précision. Avec peu de bits, il faut choisir son compromis :
- Plus d'exponent → plus de plage dynamique (gère les outliers)
- Plus de mantissa → plus de précision relative (moins d'erreur)

FP8 E4M3 (4 exp, 3 mant) — le format forward#

block-beta columns 1 e4m3_t["FP8 E4M3 — format forward (8 bits)"] e4m3_s["S | EEEE | MMM"] e4m3_d["Bias = 7 · Valeur = (-1)^S × 2^(E-7) × (1 + M/8)\nPlage dynamique : ~2^9 = 512× · Précision relative : ~12.5%\nNiveaux finis : 240 · Plus grande valeur : ±448\nUsage : forward pass (poids + activations)"]

FP8 E5M2 (5 exp, 2 mant) — le format backward#

block-beta columns 1 e5m2_t["FP8 E5M2 — format backward (8 bits)"] e5m2_s["S | EEEEE | MM"] e5m2_d["Bias = 15 · Valeur = (-1)^S × 2^(E-15) × (1 + M/4)\nPlage dynamique : ~2^15 = 32 768× (très large !)\nPrécision relative : ~25% (moins précis que E4M3)\nPlus grande valeur finie : ±57 344\nUsage : backward pass (gradients) — dynamique beaucoup plus large"]

FP4 E2M1 (2 exp, 1 mant) — le format ultra-low-bit#

block-beta columns 1 e2m1_t["FP4 E2M1 — format ultra-low-bit (4 bits)"] e2m1_s["S | EE | M"] e2m1_d["Bias = 1 · Valeur = (-1)^S × 2^(E-1) × (1 + M/2)\nValeurs : +0, +0.5, +1.0, +1.5, +2.0, +3.0\n→ Seulement 6 à 8 valeurs finies !\n→ Nécessite obligatoirement un scale partagé (Microscaling)"]

Comparaison visuelle des plages de valeurs#

flowchart LR subgraph E4M3["FP8 E4M3 (240 valeurs)"] E1["Plage : -448 à +448
Précision fine au centre
Sparse aux extrêmes"] end subgraph E5M2["FP8 E5M2 (120 valeurs)"] E2["Plage : -57344 à +57344
Très étalé, moins de niveaux
Très large amplitude"] end subgraph E2M1["FP4 E2M1 (6-8 valeurs)"] E3["Valeurs : -3, -2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2, 3
Trop peu → nécessite un SCALE partagé"] end subgraph INT8["INT8 (256 valeurs, uniformes)"] I1["Plage : -128 à +127
Espacement CONSTANT
Gaspille la résolution
Ne peut pas représenter les outliers"] end

Précision relative : FP vs INT#

La précision relative (erreur maximale relative) est ce qui compte pour les LLMs — pas l'erreur absolue.

Format Bits Erreur rel. max Notes
FP32 32 ~0.00001% Référence
FP16 16 ~0.05% Standard
BF16 16 ~0.8% Plus large dynamique
INT8 8 ~0.2%* *constant
FP8 E4M3 8 ~6.25% Près de zéro : mieux
FP8 E5M2 8 ~25% Très large dynamique
INT4 4 ~3%* *constant
FP4 E2M1 4 ~25% Mais meilleure dynamique

※ L'erreur relative de INT est CONSTANTE quelle que soit la valeur. L'erreur relative de FP est CONSTANTE en % (proportionnelle à la magnitude).
→ Pour les poids près de 0 (fréquents) : FP est meilleur. Pour les outliers (rares) : FP est nettement meilleur.

flowchart LR subgraph INT8_box["INT8 (scale=1/127, 256 niveaux uniformes)"] I1["w=0.001 → niveau le plus proche : 0.0"] I2["Erreur ABSOLUE : 0.001"] I3["❌ Erreur RELATIVE : 100% (!!!)"] end subgraph FP8_box["FP8 E4M3"] F1["w=0.001 → niveau le plus proche : 0.0009765625"] F2["Erreur ABSOLUE : 0.0000234375"] F3["✅ Erreur RELATIVE : 2.3%"] end

FP8 est ordres de grandeur plus précis pour les petites valeurs, qui constituent la majorité des poids d'un LLM.


Formats Microscaling (MX) — le standard OCP#

Le problème du FP4 seul#

Le FP4 E2M1 n'a que 6 à 8 valeurs. Ce n'est pas assez pour représenter la diversité des poids d'un LLM sans scale adapté. La solution : Microscaling.

Principe du Microscaling#

block-beta columns 2 mx_t["FORMAT MICROSCALING (MX) — OCP STANDARD"]:2 mx_d["Shared Scale (E8M0) | 32 × FP4 (E2M1) values"]:2 mx_b["8 bits scale | 32 × 4 = 128 bits values"]:2 mx_r["Bitrate effectif = (8 + 128) / 32 = 4.25 bits/valeur"]:2 mx_v["Valeur décodée = scale_shared × fp4_value"]:2

Variantes de formats MX#

Format Scale Values Block size Bits/value
MXFP8 E8M0 E4M3 32 8.25
MXFP6 E8M0 E2M3 32 6.25
MXFP6 alt E8M0 E3M2 32 6.25
MXFP4 E8M0 E2M1 32 4.25
MXINT8 E8M0 INT8 32 8.25
MXINT4 E8M0 INT4 32 4.25

Le scale partagé (E8M0 = exponent seule, 8 bits) agit comme un « zoom » global pour le bloc de 32 valeurs, permettant une dynamique de 2^256 entre blocs différents.

Pourquoi MX est important#

flowchart LR subgraph FP4ALONE["❌ FP4 seul (sans Microscaling)"] F1["Bloc de poids : [0.001, 0.002, 0.0015, ...]"] F2["→ Tous arrondis à 0 ou 0.5
→ perte totale ❌"] end subgraph MXFP4["✅ MXFP4 (scale partagé)"] M1["scale = 2^(-10) = 0.0009765625"] M2["Normalisés : [1.024, 2.048, 1.536, ...]"] M3["Quantifiés FP4 : [1.0, 2.0, 1.5, ...] ✅"] M4["Décodés : [0.00098, 0.00195, ...] ✅"] end

Le scale partagé adapte dynamiquement la grille FP4 à la magnitude locale des poids → précision préservée


Support hardware FP4 / FP8#

Plateforme FP8 FP4 MX Disponibilité
NVIDIA Hopper Disponible
NVIDIA Blackwell ✅ ✅ ✅ ✅ 2024+ (B200)
AMD MI300x Disponible
AMD MI350 2025+
Intel Gaudi 3 Disponible
Google TPU v5p Cloud only

NVIDIA Blackwell B200 : FP4 Tensor Cores avec throughput de ~9 PFLOPS (dense) / ~18 PFLOPS (sparse) — le hardware le plus puissant pour la quantification FP4 à ce jour.


FP8 Training — entraîner directement en FP8#

Le FP8 n'est pas seulement pour l'inférence — il permet aussi d'entraîner des modèles plus rapidement.

Configuration FP8 training#

flowchart LR W["Weights
(master FP32)"] -->|"E4M3"| FWD["Forward
(FP8 TC)"] FWD -->|"E4M3"| ACT["Activations
(FP8)"] ACT -->|"E5M2"| FWD FWD -->|"update"| W
Phase Poids Activations Gradients Master weights Optimizer
Forward FP8 E4M3 FP8 E4M3 FP8 E5M2 FP32 FP32
Backward FP8 E5M2 FP32 FP32

Avantages : 2× throughput vs BF16 · 2× réduction mémoire poids · Batch size plus grand · Pas de perte d'accuracy si bien calibré


Comparaison FP vs INT : quand choisir quoi#

Critère FP8/FP4 INT8/INT4
Outliers ✅ Excellente gestion ⚠️ Clipping nécessaire
Précision près de zéro ✅ Très bonne ⚠️ Médiocre
Simplicité ⚠️ Plus complexe ✅ Simple
Hardware support Hopper+ (FP8), Blackwell (FP4) Volta+ (INT8), large
Écosystème En développement Mature
Training ✅ FP8 training natif ⚠️ QAT nécessaire
Standardisation OCP MX (en cours) Très standardisé
Edge computing ⚠️ Limité ✅ Très répandu

Exemple pratique#

FP8 inference avec NVIDIA Transformer Engine#

pip install transformer_engine torch
import torch
import transformer_engine.pytorch as te
from transformer_engine.common.recipe import Format, DelayedScaling

# Définir la recette FP8
fp8_recipe = DelayedScaling(
    fp8_format=Format.HYBRID,  # E4M3 forward, E5M2 backward
    amax_history_len=16,
    amax_compute_algo="max",
)

# Remplacer nn.Linear par te.Linear (FP8 natif)
model = te.TransformerLayer(
    hidden_size=4096,
    ffn_hidden_size=11008,
    num_attention_heads=32,
    layernorm_eps=1e-5,
)

model = model.cuda()

# Forward pass avec FP8
input_ids = torch.randint(0, 32000, (4, 512, 4096)).cuda()

with te.fp8_autocast(enabled=True, fp8_recipe=fp8_recipe):
    output = model(input_ids)
    # Le Transformer Engine gère automatiquement :
    # - La sélection E4M3/E5M2
    # - Le scaling par tensor
    # - L'amax tracking

print(f"Output shape: {output.shape}")

FP8 serving avec vLLM (modèles Neural Magic)#

# Utiliser un modèle pré-quantifié FP8
python -m vllm.entrypoints.openai.api_server \
    --model neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8 \
    --quantization fp8 \
    --port 8000

# Tester
curl http://localhost:8000/v1/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8",
        "prompt": "Explique le FP8 :",
        "max_tokens": 200
    }'

Quantification FP8 statique avec TensorRT Model Optimizer#

pip install --upgrade tensorrt-model-optimizer

# Quantification FP8 avec calibration
python -m tensorrt_model_optimizer.quantize \
    --model meta-llama/Meta-Llama-3-8B \
    --format fp8 \
    --calib-size 512 \
    --calib-dataset c4 \
    --output ./llama3-8b-fp8

# Résultat :
# - Taille : 16 GB → 8 GB (−50%)
# - Throughput H100 : ~1.7× vs FP16
# - PPL WikiText2 : ~6.14 (vs 6.13 FP16) → quasi-lossless

MXFP4 expérimental (Blackwell)#

# NVIDIA Blackwell B200 + PyTorch 2.5+
# Le MXFP4 est supporté via le Transformer Engine v2+

import transformer_engine.pytorch as te

# Configuration MXFP4
model = te.TransformerLayer(
    hidden_size=8192,
    ffn_hidden_size=28672,
    num_attention_heads=32,
)

# Quantification MXFP4 pour les poids
# Le scale E8M0 partagé est calculé par bloc de 32
# lors de la conversion du modèle

Références#

ia llm quantification fp4 fp8 floating-point e4m3 e5m2 e2m1 microscaling mxfp ocp blackwell