INT8 / FP8 Quantization — Support Hardware Native#

TL;DR — La quantification n'a d'intérêt pratique que si le matériel sait calculer directement dans le format compressé. Les GPU modernes (NVIDIA Hopper H100, Blackwell B200), les CPU (Intel AMX) et les accélérateurs (Apple ANE) intègrent désormais des unités de calcul dédiées pour INT8 et FP8. Ce support natif se traduit par un throughput jusqu'à 2× à 4× supérieur au FP16, sans perte d'accuracy notable. Comprendre la correspondance format ↔ hardware est le levier pour déployer des LLMs en production à grande échelle.


Pourquoi le support hardware matters#

Niveau néophyte#

Imaginez que vous deviez traduire un texte du chinois vers le français, mais que votre machine à traduire ne comprend que l'anglais. Vous devez faire une étape intermédiaire (chinois → anglais → français), ce qui prend deux fois plus de temps et introduit des erreurs.

Avec le support hardware natif, la machine traduit directement du chinois au français — pas d'étape intermédiaire, pas de surcoût. Pour les LLMs, le GPU peut multiplier des matrices en INT8 ou FP8 sans jamais repasser par le FP16, ce qui double voire quadruple la vitesse.

Le problème de la déquantification à la volée#

La plupart des méthodes de quantification (GPTQ INT4, AWQ, etc.) stockent les poids compressés mais doivent les déquantifier vers FP16 à chaque calcul :

flowchart LR subgraph NOHW["⚠️ Sans support hardware"] W4A["W_int4"] --> DQA["[DÉQUANT 4→16 bit]"] --> WFA["W_fp16"] XFA["X_fp16"] WFA --> GEA["GEMM FP16 → Y"] XFA --> GEA GEA -. "Toujours en FP16" .-> NOTE1["Pas de speedup du calcul
juste moins de mémoire"] end
flowchart LR subgraph HW["✅ Avec support hardware natif"] W8B["W_int8"] --> GEB["GEMM INT8 → Y_int32
Tensor Cores INT8 natifs"] X8B["X_int8"] --> GEB GEB -. "Calcul DIRECTEMENT en INT8" .-> NOTE2["2× à 4× de speedup réel
Accumulation en INT32/FP32"] end

La différence est fondamentale : sans support hardware natif, la quantification ne fait que réduire la mémoire (utile pour l'edge) mais n'accélère pas le calcul. Avec le support natif, elle accélère tout — mémoire ET compute.


Les formats : FP8 E4M3 vs FP8 E5M2 vs INT8#

Diagramme bit-level#

block-beta columns 3 h1["Format"] h2["Structure"] h3["Caractéristiques"] block:i1["INT8"]:1 block:i1b["S | 7 bits valeur"]:2 block:i1c["Plage : -128 à +127\n256 niveaux, espacement UNIFORME\nRésolution : constante (Δ = scale)"]:3 block:f1["FP8 E4M3"]:1 block:f1b["S | exp(4) | mantissa(3)"]:2 block:f1c["Plage : ±448\nRésolution variable (plus précise près de zéro)\nOptimisé pour : forward pass"]:3 block:f2["FP8 E5M2"]:1 block:f2b["S | exp(5) | mant(2)"]:2 block:f2c["Plage : ±57344\nTrès large dynamique, peu de mantisse\nOptimisé pour : backward pass"]:3

Comparaison détaillée#

Caractéristique INT8 FP8 E4M3 FP8 E5M2
Bits 8 8 8
Plage dynamique 256 niveaux (fixe) ±448 ±57 344
Précision relative Constante ~1.6% ~6.25%
Niveaux représentables -128…+127 240 valeurs finies 120 valeurs finies
Gestion des outliers ❌ Mauvaise (saturation) ✅ Bonne ✅ Excellente
Usage optimal Activations, poids réguliers Poids + activations forward Gradients backward
Hardware Volta+, AMX, ANE Hopper H100+ Hopper H100+
Maturité ★★★★★ (très mature) ★★★☆☆ (2023+) ★★★☆☆ (2023+)

Pourquoi le FP8 gère mieux les outliers#

Les poids de LLMs suivent une distribution heavy-tailed (queue épaisse) avec quelques valeurs extrêmes :

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

Le FP8, grâce à son exponent, alloue dynamiquement la précision là où c'est nécessaire — plus de niveaux près de zéro, moins pour les grandes valeurs. L'INT8, lui, distribue ses niveaux uniformément, ce qui gaspille de la résolution sur les zones peu peuplées et sature sur les outliers.


Support hardware par plateforme#

Vue d'ensemble#

Plateforme INT8 FP8 Architecture
NVIDIA Volta ✅ (V100) Tensor Cores
NVIDIA Ampere ✅ (A100) Tensor Cores
NVIDIA Hopper ✅ (H100) ✅ ✅ FP8 TC + TMA
NVIDIA Blackwell ✅ (B200) ✅ ✅ FP8 + FP4 TC
AMD MI300x Matrix Cores
Intel Xeon (AMX) ✅ ✅ AMX tiles
Apple Silicon ✅ (ANE) Neural Engine
Google TPU v5 ✅ (v5e+) MXU

NVIDIA Hopper (H100) — le game-changer FP8#

L'architecture Hopper a introduit le FP8 natif dans les Tensor Cores :

Format Dense Sparse 2:4
FP64 67 TFLOPS
FP32 67 TFLOPS
TF32 495 TFLOPS 990 TFLOPS
BF16 989 TFLOPS 1979 TFLOPS
FP8 1979 TFLOPS 3958 TFLOPS 🚀
INT8 1979 TOPS 3958 TOPS

FP8 = 2× la vitesse du BF16 en dense · 4× en sparse
Transformer Engine : gestion automatique du FP8 (E4M3 forward / E5M2 backward)

Le Transformer Engine (bibliothèque logicielle NVIDIA) gère automatiquement :
- La sélection du format FP8 (E4M3 pour forward, E5M2 pour backward)
- Le scaling dynamique par tile/tensor
- L'amax tracking (suivi de la valeur max d'activation)
- Le fallback vers BF16 si nécessaire

Intel AMX — INT8 sur CPU#

L'Advanced Matrix Extensions (AMX) d'Intel apporte des instructions matricielles INT8/BF16 directement sur CPU Xeon (Sapphire Rapids et ultérieurs) :

block-beta columns 1 amx_title["INTEL AMX — INT8 SUR CPU XEON"] tile_a["Tile A (INT8) — max 1024 bytes"] tile_b["Tile B (INT8) — max 1024 bytes"] tile_c["Tile C (INT32 acc) — accumulation"] op["C += A × B (INT8 × INT8 → INT32)"] advantage["Avantage : inférence INT8 sur CPU sans GPU\nIdéal pour edge / on-prem / cost-sensitive\nOutil : Intel OpenVINO"] amx_title --> tile_a --> tile_b --> tile_c --> op --> advantage

Apple Neural Engine (ANE)#

L'ANE des puces Apple Silicon (M1, M2, M3, M4) supporte nativement l'INT8 via le Apple Neural Engine :

Puce ANE TOPS INT8 natif
M1 11 TOPS
M2 15.8 TOPS
M3 18 TOPS
M4 38 TOPS ✅ ✅ (amélioré)

Framework : CoreML, MLX → inférence locale INT8 sur Mac/iPad
Pas de FP8 natif sur ANE (mais GPU Apple supporte FP16/BF16)


Niveau tech avancé : le pipeline FP8 complet#

Quantification FP8 statique (PTQ)#

flowchart TD M["Modèle FP16"] -->|"128 échantillons"| CAL["**1. Calibration**
amax = max(|activation|)
scale = max_fp8 / amax
FP8_max = 448 (E4M3)"] CAL --> WQ["**2. Quantification poids**
W_fp8 = round(W_fp16 × scale_w) → E4M3"] CAL --> XQ["**3. Quantification activations**
X_fp8 = round(X_fp16 × scale_x) → E4M3"] WQ --> GEMM["**4. Inférence native**
FP8 GEMM (Tensor Cores)
→ acc_fp32 → Y"] XQ --> GEMM GEMM -. "Scaling par-tensor ou par-token" .-> NOTE[""]

Granularités de quantification#

Granularité Description Précision Overhead mémoire Usage
Per-tensor 1 scale pour toute la matrice Bas Minimal (1 float) Hopper Tensor Cores
Per-channel 1 scale par colonne de poids Bon Modéré (N floats) Standard INT8
Per-token 1 scale par token d'activation Très bon Modéré Activations dynamiques
Per-block (MX) 1 scale par bloc de 32 Excellent Non négligeable Format Microscaling
Per-group 1 scale par groupe de N Variable Selon N INT4/INT8 custom

Benchmarks throughput vs FP16#

Throughput théorique (Tensor Cores)#

xychart-beta title "Throughput relatif (FP16/BF16 = 1.0×) — NVIDIA H100 SXM" x-axis ["FP32", "BF16", "INT8 dense", "FP8 dense", "INT8 sparse", "FP8 sparse"] y-axis "Throughput relatif" 0 --> 4.5 bar [0.07, 1.0, 2.0, 2.0, 4.0, 4.0]

FP8 dense = 2× le throughput du BF16 dense · INT8 dense = 2× · FP8/INT8 sparse = 4×

Benchmarks réels (vLLM, Llama-2-7B)#

Configuration GPU Throughput (tok/s) Speedup vs FP16
FP16 (ref) H100 ~2 500 1.0×
INT8 W8A8 H100 ~3 800 1.5×
FP8 W8A8 H100 ~4 200 1.7×
INT8 W8A8 A100 ~1 800 1.2×
FP16 (ref) A100 ~1 500 1.0×

Note : le speedup réel est inférieur au speedup théorique car le GEMM n'est qu'une partie du pipeline complet (attention, sampling, KV cache management).

Accuracy : INT8 / FP8 vs FP16#

Modèle Format WikiText2 PPL Δ vs FP16 Évaluation
Llama-2-7B FP16 5.47 Référence
Llama-2-7B INT8 (LLM.int8) 5.49 +0.02 Quasi-lossless ✅
Llama-2-7B FP8 E4M3 5.48 +0.01 Quasi-lossless ✅
Llama-2-70B FP16 3.32 Référence
Llama-2-70B INT8 3.35 +0.03 Quasi-lossless ✅
Llama-2-70B FP8 E4M3 3.33 +0.01 Quasi-lossless ✅

À 8 bits, INT8 et FP8 sont essentiellement lossless pour la plupart des modèles ≥ 7B. C'est le régime « safe » de la quantification.


Outils principaux#

NVIDIA TensorRT-LLM — le standard production#

# Installation
pip install tensorrt-llm

# Quantification FP8 d'un modèle Hugging Face
python tensorrt_llm/quantization/quantize.py \
    --model_dir meta-llama/Llama-2-7b-hf \
    --dtype float16 \
    --qformat fp8 \
    --output_dir ./llama-7b-fp8 \
    --calib_size 512

# Build du moteur TensorRT optimisé
trtllm-build \
    --checkpoint_dir ./llama-7b-fp8 \
    --output_dir ./llama-7b-fp8-engine \
    --gemm_plugin fp8

# Inférence via l'API Python ou le serveur Triton

vLLM — le plus simple pour FP8#

# Servir un modèle en FP8 sur H100
python -m vllm.entrypoints.openai.api_server \
    --model neuralmagic/Meta-Llama-3.1-8B-Instruct-FP8 \
    --quantization fp8 \
    --dtype auto \
    --port 8000

# vLLM détecte le FP8 et utilise les Tensor Cores nativement

Intel OpenVINO — INT8 sur CPU#

from optimum.intel import OVQuantizer, OVModelForCausalLM

# Quantification INT8 avec calibration
model = OVModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
quantizer = OVQuantizer.from_pretrained(model)

# Calibration + quantification INT8
quantizer.quantize(
    calibration_dataset=dataset,
    save_directory="./llama-7b-int8-ov",
)

# Inférence sur CPU Xeon avec AMX
model_int8 = OVModelForCausalLM.from_pretrained("./llama-7b-int8-ov")

torch.compile — PyTorch natif#

import torch

# Quantification INT8 dynamique avec PyTorch 2+
model_fp16 = ...  # votre modèle

# Quantization dynamique INT8 (activations)
model_int8 = torch.ao.quantization.quantize_dynamic(
    model_fp16,
    {torch.nn.Linear},
    dtype=torch.qint8
)

# torch.compile avec backend optimisé
model_int8 = torch.compile(model_int8, mode="max-autotune")

# Pour FP8 (nécessite PyTorch 2.2+ + H100)
# Utiliser le Transformer Engine de NVIDIA :
import transformer_engine.pytorch as te

# Remplacer les Linear par des te.Linear (FP8 natif)

NVIDIA Transformer Engine — FP8 natif#

import transformer_engine.pytorch as te
import torch

# Modèle avec FP8 natif
model = te.TransformerLayer(
    hidden_size=4096,
    ffn_hidden_size=11008,
    num_attention_heads=32,
)

# Le FP8 est géré automatiquement par le Transformer Engine
# Sélection E4M3 (forward) / E5M2 (backward) transparente
with te.fp8_autocast():
    output = model(input_ids)
    loss = loss_fn(output, labels)
    loss.backward()  # gradients en E5M2

Quand utiliser INT8 vs FP8#

flowchart TD Q["Quel GPU avez-vous ?"] Q --> H100["H100 / B200
(Hopper/Blackwell)"] Q --> A100["A100 / V100
(Ampere/Volta)"] Q --> AMD["AMD MI300x"] Q --> Intel["Intel Xeon (AMX)"] Q --> Apple["Apple Silicon"] Q --> CPU["CPU générique"] H100 --> FP8["**FP8 E4M3**
(meilleur accuracy + throughput)"] A100 --> INT8a["**INT8**
(FP8 non supporté nativement)"] AMD --> BOTH["**FP8 ou INT8**
(les deux supportés)"] Intel --> INT8b["**INT8 via OpenVINO**"] Apple --> INT8c["**INT8 via CoreML/MLX**"] CPU --> INT8d["**INT8 dynamique (PyTorch)**
ou FP16"]

Références#

ia llm quantification int8 fp8 hardware gpu tensor-cores h100 blackwell intel-amx apple-ane tensorrt openvino