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.
- 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.float16outorch.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
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.
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
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é.