Polydesk-logotype
Polydesk.ai — Header

FP16 (Half Precision)

FP16 (Float16, ou half precision) est un format de nombre flottant sur 16 bits (1 bit de signe, 5 bits d’exposant, 10 bits de mantisse) utilisé en deep learning pour accélérer l’entraînement et l’inférence tout en réduisant la consommation mémoire de moitié par rapport au FP32.

FP16 a été le premier format de précision réduite largement adopté dans l’entraînement de réseaux de neurones. Popularisé par NVIDIA avec l’architecture Volta (V100, 2017) et le paper fondateur de Micikevicius et al. (ICLR 2018), il a démontré qu’on pouvait entraîner des modèles de deep learning en 16 bits sans perte de qualité significative, à condition d’utiliser les bonnes techniques (poids maîtres FP32, loss scaling). C’est le format qui a lancé l’ère du mixed precision training.

Aujourd’hui, FP16 a été largement supplanté par BF16 pour l’entraînement des LLM modernes, mais il reste pertinent pour l’inférence, les GPU plus anciens, et certains cas d’usage spécifiques en vision par ordinateur.

FP16 en bref
Nom complet
IEEE 754 Half-Precision Floating Point
Taille
16 bits (2 octets)
Structure
1 bit signe | 5 bits exposant | 10 bits mantisse
Plage
~6,10 × 10⁻⁵ à ~6,55 × 10⁴
Précision
~3-4 chiffres significatifs
GPU minimum
Volta (V100, 2017) pour Tensor Cores
PyTorch
torch.float16 ou torch.half
Utilisé avec
torch.amp.autocast + GradScaler

Structure du format FP16

Le format FP16 suit le standard IEEE 754 pour les nombres flottants en demi-précision. Ses 16 bits sont répartis ainsi :

1 bit de signe (S). Détermine si le nombre est positif (0) ou négatif (1).

5 bits d’exposant (E). Encode la puissance de 2. Avec un biais de 15, l’exposant réel va de -14 à +15. C’est ici que réside la principale limitation du FP16 : seulement 5 bits d’exposant, contre 8 pour FP32 et BF16. Cela donne une plage dynamique beaucoup plus restreinte.

10 bits de mantisse (M). Encode la fraction, avec un « 1 » implicite en tête (format normalisé). Les 10 bits de mantisse donnent environ 3 à 4 chiffres significatifs de précision, ce qui est suffisant pour la plupart des opérations en deep learning.

La valeur d’un nombre FP16 normalisé se calcule comme : (-1)^S × 2^(E-15) × (1 + M/1024)

Propriété FP16 FP32 BF16
Taille totale 16 bits 32 bits 16 bits
Bits d’exposant 5 8 8
Bits de mantisse 10 23 7
Valeur max 65 504 ~3,4 × 10³⁸ ~3,4 × 10³⁸
Plus petit normalisé ~6,10 × 10⁻⁵ ~1,18 × 10⁻³⁸ ~1,18 × 10⁻³⁸
Précision relative ~0,1% ~0,00001% ~0,8%

Le point crucial : la plage maximale de FP16 est seulement de 65 504. Toute valeur supérieure déborde en inf. Or, les activations, les gradients ou la loss d’un réseau de neurones peuvent facilement dépasser cette valeur. C’est la principale raison pour laquelle FP16 nécessite des précautions supplémentaires (loss scaling) que BF16 n’exige pas.

Les trois problèmes du FP16

Underflow des gradients

Les gradients dans les couches profondes d’un réseau sont souvent très petits (ordre de 10⁻⁶ à 10⁻⁸). En FP16, le plus petit nombre normalisé est ~6,10 × 10⁻⁵. Tout gradient plus petit est arrondi à zéro (underflow). L’étude de NVIDIA sur les distributions de gradients a montré qu’une fraction non négligeable des gradients tombe en dessous de cette limite, particulièrement dans les réseaux profonds.

Résultat : les couches profondes cessent d’apprendre car elles ne reçoivent plus de signal de gradient. L’entraînement stagne sans raison apparente.

Overflow des activations et de la loss

Les activations intermédiaires ou la loss brute du modèle peuvent facilement dépasser 65 504, surtout en début d’entraînement ou avec certaines fonctions de loss. En FP16, ces valeurs deviennent inf, puis propagent des NaN dans tout le réseau. L’entraînement diverge instantanément.

Mises à jour imprécises

Quand un poids vaut 10.0 et que la mise à jour est de -0.0001, le résultat en FP16 (10.0 – 0.0001) est arrondi à 10.0 car la différence est inférieure à la résolution de FP16 à cette échelle. Les petites mises à jour disparaissent, empêchant la convergence fine du modèle. C’est pourquoi le mixed precision maintient toujours une copie maître des poids en FP32.

Loss Scaling : la solution aux problèmes FP16

Le loss scaling est la technique clé qui rend l’entraînement FP16 viable. Le principe est simple : multiplier la loss par un facteur d’échelle avant le backward, ce qui « remonte » tous les gradients dans la plage représentable de FP16, puis diviser les gradients par le même facteur avant la mise à jour des poids.

Scaling statique

Le facteur est fixé à l’avance (par exemple 128 ou 1024). Simple mais fragile : un facteur trop grand provoque des overflow, trop petit ne résout pas l’underflow. Il faut parfois l’ajuster manuellement selon le modèle et le dataset.

Scaling dynamique (recommandé)

PyTorch AMP utilise le GradScaler qui ajuste automatiquement le facteur d’échelle. L’algorithme commence avec un facteur élevé (typiquement 2¹⁶ = 65 536), et :

Si aucun overflow n’est détecté pendant N steps (par défaut 2000), le facteur est multiplié par 2 (plus de précision).

Si un overflow est détecté (gradients inf ou NaN), le step est sauté, et le facteur est divisé par 2 (plus de stabilité).

from torch.amp import autocast, GradScaler

scaler = GradScaler()

for batch in dataloader:
    optimizer.zero_grad()

    with autocast(device_type="cuda", dtype=torch.float16):
        loss = model(batch)

    # Scale loss → backward → unscale → clip → step → update scale
    scaler.scale(loss).backward()
    scaler.unscale_(optimizer)
    torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
    scaler.step(optimizer)     # Skip si overflow détecté
    scaler.update()            # Ajuste le facteur
Le GradScaler qui s’effondre Si vous observez que le scale factor du GradScaler chute continuellement (visible en loggant scaler.get_scale()), c’est le signe que votre modèle produit constamment des overflow en FP16. Solutions : réduire le learning rate, ajouter du gradient clipping, vérifier les couches de normalisation, ou passer à BF16.

FP16 vs BF16 : pourquoi BF16 domine

La comparaison est directe. BF16 a les mêmes 8 bits d’exposant que FP32, ce qui lui donne une plage dynamique identique (~3,4 × 10³⁸ vs 65 504 pour FP16). Cela élimine les problèmes d’overflow et d’underflow qui nécessitent le loss scaling en FP16.

FP16 a un avantage : sa mantisse de 10 bits offre une meilleure précision que les 7 bits de BF16. En pratique, cette précision supérieure est rarement utile pour l’entraînement de réseaux de neurones, qui sont intrinsèquement tolérants au bruit numérique.

Le paper de Kalamkar et al. (Intel, 2019) a démontré empiriquement que BF16 atteint une parité numérique avec FP32 sur une grande variété de tâches (classification d’images, reconnaissance vocale, modèles de langage, systèmes de recommandation) sans aucun ajustement d’hyperparamètres, alors que FP16 nécessite un tuning du loss scaling pour chaque modèle.

Verdict : utilisez BF16 sauf contrainte matérielle Sur un GPU Ampere ou plus récent (A100, H100, RTX 3090+), BF16 est strictement préférable à FP16 pour l’entraînement. FP16 ne devrait être utilisé que sur les GPU Volta (V100) ou les anciennes cartes consommateur (RTX 2080, GTX 1080) qui ne supportent pas BF16. Pour l’inférence, FP16 reste viable sur tout matériel compatible.

Support matériel

Le support de FP16 varie selon les architectures GPU :

Architecture GPU FP16 Tensor Core Débit FP16 TC BF16 supporté
Pascal (2016) P100, GTX 1080 Non (FP16 CUDA uniquement) ~21 TFLOPS Non
Volta (2017) V100 Oui (1ère génération) ~125 TFLOPS Non
Turing (2018) RTX 2080, T4 Oui ~65 TFLOPS (RTX 2080) Non
Ampere (2020) A100, RTX 3090 Oui ~312 TFLOPS (A100) Oui
Hopper (2022) H100 Oui ~990 TFLOPS Oui + FP8

Les Tensor Cores sont essentiels pour tirer parti du FP16. Sur un GPU Pascal sans Tensor Cores, FP16 est environ 2x plus rapide que FP32 (grâce à la réduction de bande passante mémoire). Sur un V100 avec Tensor Cores, le gain peut atteindre 8x en débit théorique pour les opérations matricielles. Pour maximiser l’utilisation des Tensor Cores, les dimensions des matrices doivent être des multiples de 8.

Cas d’usage actuels du FP16

Inférence

FP16 reste très populaire pour l’inférence de modèles. Les modèles pré-entraînés sont souvent distribués en FP16 (les fichiers de poids sont 2x plus petits que FP32), et l’inférence en FP16 est simple car il n’y a pas de mise à jour de poids (donc pas besoin de copie maître FP32 ni de loss scaling). La plupart des moteurs d’inférence (vLLM, TensorRT-LLM, ONNX Runtime) supportent nativement FP16.

Vision par ordinateur

Les modèles de vision (ResNet, EfficientNet, YOLO) ont été historiquement entraînés en FP16 mixed precision avant l’arrivée de BF16. Ces architectures restent un cas d’usage courant pour FP16, particulièrement sur du matériel grand public (cartes RTX 2080/3060 qui ne supportent pas toujours BF16 sur Tensor Cores).

GPU anciens

Si votre infrastructure repose sur des GPU V100 (encore très courants dans les clusters cloud), FP16 est votre seule option en 16 bits. Le V100 ne supporte pas BF16. L’entraînement en FP16 avec GradScaler sur V100 est une configuration éprouvée et fiable.

Edge et mobile

Sur les accélérateurs embarqués (NVIDIA Jetson, Intel Movidius, Apple Neural Engine), FP16 est souvent le format le plus efficace. Ces puces ont un support matériel FP16 optimisé mais pas toujours BF16.

Conversion et stockage en FP16

Les modèles sont souvent convertis en FP16 pour le stockage et la distribution, indépendamment du format d’entraînement. Sur Hugging Face, la plupart des modèles open-source sont disponibles en FP16 (taille = nombre de paramètres × 2 octets). Un modèle 7B en FP16 pèse environ 14 Go, contre 28 Go en FP32.

En PyTorch, la conversion est triviale :

# Convertir un modèle FP32 en FP16
model = model.half()  # ou model.to(torch.float16)

# Sauvegarder en FP16
torch.save(model.state_dict(), "model_fp16.pt")

# Charger et inférer en FP16
model.load_state_dict(torch.load("model_fp16.pt"))
output = model(input.half())  # Input aussi en FP16
FP16 vs quantization pour l’inférence FP16 réduit la taille du modèle de 2x (vs FP32). La quantization en INT8 réduit de 4x, et en INT4 de 8x. Pour l’inférence sur GPU avec beaucoup de VRAM, FP16 offre le meilleur compromis qualité/performance. Pour les GPU à mémoire limitée ou le déploiement edge, la quantization est préférable.

Optimiser l’utilisation des Tensor Cores en FP16

Activer le FP16 ne suffit pas pour exploiter pleinement les Tensor Cores. Plusieurs conditions doivent être remplies pour obtenir l’accélération maximale.

Dimensions multiples de 8

Les Tensor Cores opèrent sur des tuiles matricielles de taille fixe. Pour FP16, les trois dimensions d’une multiplication matricielle (M, N, K) doivent être des multiples de 8. Si vos couches linéaires ont des dimensions impaires (par exemple, un hidden size de 769), les Tensor Cores ne seront pas utilisés efficacement. C’est pourquoi les architectures de Transformers modernes utilisent des hidden sizes en puissances de 2 ou multiples de 64/128 (768, 1024, 4096, etc.).

TF32 : l’alternative transparente sur Ampere+

Sur les GPU Ampere (A100) et plus récents, NVIDIA propose TensorFloat-32 (TF32), un format interne de 19 bits qui utilise la mantisse de FP16 (10 bits) avec l’exposant de FP32 (8 bits). TF32 est activé par défaut pour les opérations torch.matmul sur Ampere+ et accélère les calculs FP32 de ~3x sans aucune modification de code. C’est un « gratuit » qui s’ajoute aux gains du mixed precision.

# TF32 est activé par défaut sur Ampere+, mais vous pouvez le contrôler :
torch.backends.cuda.matmul.allow_tf32 = True   # Matmul en TF32
torch.backends.cudnn.allow_tf32 = True          # Convolutions en TF32

Alignement du batch size

Le batch size influence aussi l’efficacité des Tensor Cores. Des batch sizes multiples de 8 (ou idéalement 64) maximisent le tiling sur les Tensor Cores. Un batch size de 13 sera moins efficace qu’un batch de 16, même si la différence de mémoire est minime.

FP16 en entraînement distribué

En data parallelism distribué (DDP), les gradients communiqués entre GPU sont en FP16 quand le mixed precision est activé. Cela réduit le volume de communication de 50%, ce qui est un avantage non négligeable sur les clusters multi-noeuds où la bande passante réseau est le goulot d’étranglement.

Avec DeepSpeed, les optimisations de communication compressée (1-bit Adam, ZeRO++) vont encore plus loin en compressant les gradients FP16 en formats encore plus compacts. Avec FSDP, les paramètres fragmentés sont communiqués en FP16/BF16 puis castés en FP32 uniquement pour la mise à jour.

Un point d’attention : en gradient accumulation avec FP16, les gradients accumulés doivent être stockés en FP32 pour éviter l’accumulation d’erreurs d’arrondi sur de nombreux micro-batches. PyTorch AMP gère cela automatiquement quand le GradScaler est utilisé correctement.


Questions fréquentes sur le FP16

FP16 et BF16 donnent-ils la même accélération sur Tensor Cores ?

Oui, sur les GPU Ampere (A100) et plus récents, FP16 et BF16 offrent le même débit sur les Tensor Cores (312 TFLOPS sur A100). La différence n’est pas dans la vitesse mais dans la stabilité numérique. Sur les GPU Volta (V100), seul FP16 est disponible. Le choix entre les deux ne devrait donc jamais être motivé par la performance brute, mais par la compatibilité matérielle et la stabilité de l’entraînement.

Peut-on entraîner un LLM entièrement en FP16 sans FP32 ?

C’est techniquement possible mais fortement déconseillé. Sans poids maîtres FP32, les mises à jour incrémentielles (learning rate × gradient) sont arrondies à zéro pour les poids de grande magnitude, empêchant la convergence fine. Certains chercheurs ont publié des résultats en FP16 pur pour des modèles simples (classifieurs d’images), mais pour les LLM, la copie maître FP32 est considérée comme indispensable. Les états de l’optimiseur Adam (moments) doivent aussi rester en FP32.

Faut-il le GradScaler avec BF16 ?

Non. BF16 a la même plage dynamique que FP32, donc les gradients ne risquent pas d’underflow ou d’overflow comme en FP16. Le GradScaler n’est nécessaire qu’avec FP16. Si vous passez un GradScaler à un entraînement BF16, il fonctionne mais est un no-op (il ne fait rien). Cela peut être utile si vous écrivez du code compatible FP16 et BF16.

Comment vérifier si mon GPU supporte FP16 sur Tensor Cores ?

En PyTorch : torch.cuda.get_device_properties(0).major >= 7 (Volta = compute capability 7.0). Pour BF16 : torch.cuda.is_bf16_supported(). Les GPU avec compute capability 7.0+ (Volta) supportent FP16 Tensor Cores, et 8.0+ (Ampere) supportent en plus BF16. Les cartes grand public RTX 2080/3060/4060 supportent toutes FP16 Tensor Cores.

FP16 est-il encore pertinent face à la quantization INT8/INT4 ?

Oui, pour des cas d’usage différents. FP16 est un format de calcul (entraînement et inférence), tandis qu’INT8/INT4 sont principalement des formats de stockage et d’inférence. Pour l’entraînement, FP16/BF16 restent la norme car la quantization à si faible précision dégrade trop les gradients. Pour l’inférence, la quantization offre des gains supérieurs (4-8x de réduction mémoire vs 2x pour FP16), mais FP16 reste le format de base avant quantization et le format de référence pour mesurer la dégradation de qualité.

Polydesk.ai — Footer