Mixed Precision Training
Le mixed precision training (entraînement en précision mixte) est une technique qui combine des formats numériques de basse précision (FP16, BF16, FP8) pour le calcul et de haute précision (FP32) pour les opérations critiques, réduisant la consommation mémoire de ~50% et accélérant l’entraînement de 2 à 4x sur les GPU modernes.
C’est l’une des optimisations les plus rentables en deep learning. Activer le mixed precision ne coûte rien en qualité de modèle dans l’immense majorité des cas, tout en divisant la mémoire par deux et en accélérant l’entraînement grâce aux Tensor Cores des GPU NVIDIA (ou équivalents AMD/Google). Tous les LLM modernes sont entraînés en mixed precision, sans exception.
- Catégorie
- Optimisation de l’entraînement
- Principe
- Calcul en 16 bits, accumulation et poids maîtres en 32 bits
- Formats courants
- FP16, BF16, FP8 (émergent)
- Gain mémoire
- ~50% sur activations et gradients
- Gain vitesse
- 2-4x sur Tensor Cores (vs FP32 standard)
- API PyTorch
torch.amp.autocast+GradScaler- GPU requis
- Volta+ (FP16), Ampere+ (BF16), Hopper+ (FP8)
Pourquoi le mixed precision fonctionne
Les réseaux de neurones sont remarquablement tolérants à la réduction de précision. Les poids, activations et gradients n’ont pas besoin de 32 bits de précision pour que le modèle converge correctement. Les valeurs manipulées pendant l’entraînement sont rarement proches des limites de la précision FP32, et les petites erreurs d’arrondi introduites par le passage en 16 bits se comportent comme un bruit régularisant plutôt que comme une source d’instabilité.
L’intuition est simple : si vous stockez un poids de valeur 0.35721 en FP16 (qui le représente comme ~0.3572), l’erreur d’arrondi est de l’ordre de 10⁻⁴. Pour un réseau de neurones qui apprend par gradient stochastique (lui-même bruité), cette erreur est négligeable.
Le gain vient des Tensor Cores, des unités matérielles spécialisées dans les multiplications matricielles en précision réduite. Un GPU NVIDIA A100 délivre ~312 TFLOPS en FP16/BF16 Tensor Core, contre ~19,5 TFLOPS en FP32 standard, soit un facteur 16x en débit théorique de calcul. En pratique, les gains observés sont de 2 à 4x sur l’entraînement complet (le forward et backward ne sont pas 100% du temps GPU).
Les formats de précision
Pour comprendre le mixed precision, il faut connaître les formats numériques en jeu. Chaque format flottant se compose de trois champs : un bit de signe, des bits d’exposant (qui déterminent la plage de valeurs, ou range) et des bits de mantisse (qui déterminent la précision).
| Format | Bits | Exposant | Mantisse | Plage max | GPU requis |
|---|---|---|---|---|---|
| FP32 | 32 | 8 | 23 | ~3,4 × 10³⁸ | Tout GPU |
| FP16 | 16 | 5 | 10 | ~6,55 × 10⁴ | Volta+ (V100) |
| BF16 | 16 | 8 | 7 | ~3,4 × 10³⁸ | Ampere+ (A100) |
| FP8 (E4M3) | 8 | 4 | 3 | ~448 | Hopper+ (H100) |
| FP8 (E5M2) | 8 | 5 | 2 | ~57 344 | Hopper+ (H100) |
FP16 : le pionnier
FP16 a été le premier format de mixed precision largement adopté, popularisé par NVIDIA avec les GPU Volta (V100) et le paper de Micikevicius et al. (2018). Avec 5 bits d’exposant et 10 bits de mantisse, FP16 offre une bonne précision mais une plage dynamique limitée (max ~65 504). Ce range étroit pose deux problèmes concrets :
Underflow des gradients. Les petits gradients (fréquents dans les couches profondes d’un réseau) deviennent zéro en FP16. Si un gradient vaut 1e-8, il est en dessous du plus petit nombre représentable en FP16 et disparaît.
Overflow des activations. Les grandes valeurs d’activation ou de loss dépassent 65 504 et deviennent inf ou NaN, provoquant la divergence de l’entraînement.
Pour contourner ces problèmes, FP16 nécessite le loss scaling (voir section suivante).
BF16 : le format préféré des LLM
BF16 (Brain Floating Point) a été développé par Google pour les TPU, puis adopté par NVIDIA à partir de l’architecture Ampere (A100). Sa particularité : 8 bits d’exposant (comme FP32) et seulement 7 bits de mantisse. La plage dynamique est identique à FP32 (~3,4 × 10³⁸), ce qui élimine quasiment tous les problèmes d’overflow et d’underflow.
Le compromis est une précision réduite (7 bits de mantisse vs 10 pour FP16 et 23 pour FP32). En pratique, cette perte de précision est négligeable pour l’entraînement de réseaux de neurones. BF16 est devenu le format par défaut pour l’entraînement de LLM car il combine les avantages de vitesse du 16 bits avec la stabilité du FP32, sans nécessiter de loss scaling.
FP8 : la frontière émergente
FP8 divise encore par deux la taille des données par rapport à FP16/BF16. Il existe en deux variantes : E4M3 (4 bits exposant, 3 bits mantisse) pour le forward, et E5M2 (5 bits exposant, 2 bits mantisse) pour le backward. Le GPU H100 de NVIDIA possède des Tensor Cores FP8 dédiés qui offrent un débit théorique 2x supérieur au BF16.
DeepSeek V3 a été l’un des premiers modèles de très grande taille à utiliser l’entraînement FP8 pour les couches linéaires, combiné avec BF16 pour les opérations sensibles. À GTC 2025, plusieurs organisations (iGenius, DeepL, Zoho) ont présenté des cas d’usage de pré-entraînement FP8 avec des résultats encourageants.
Les mécanismes du mixed precision
Poids maîtres en FP32
Le coeur du mixed precision est de maintenir une copie maître des poids en FP32. À chaque itération, les poids sont castés en 16 bits pour le forward et le backward (rapide), puis les gradients résultants sont utilisés pour mettre à jour les poids FP32 (précis). Sans cette copie maître, les mises à jour incrémentielles petites (learning rate × gradient) seraient arrondies à zéro en 16 bits.
Concrètement, si un poids vaut 1.0 et que la mise à jour est -0.0001, en FP16 le résultat serait arrondi à 1.0 (la mise à jour disparaît). En FP32, le poids devient correctement 0.9999. La copie maître FP32 est donc essentielle pour la convergence.
Loss Scaling (FP16 uniquement)
En FP16, les petits gradients tombent sous le seuil de représentation et deviennent zéro. Le loss scaling contourne ce problème en multipliant la loss par un facteur d’échelle (souvent 2¹⁶ = 65 536) avant le backward. Les gradients résultants sont proportionnellement plus grands, ce qui les maintient dans la plage représentable de FP16. Avant la mise à jour des poids, les gradients sont divisés par le même facteur pour annuler l’effet.
Le dynamic loss scaling (utilisé par PyTorch AMP) ajuste automatiquement le facteur d’échelle : il l’augmente quand c’est possible (pour maximiser la précision) et le réduit quand un overflow est détecté (pour éviter les NaN). Si le facteur chute constamment, c’est un signe d’instabilité numérique.
BF16 n’a généralement pas besoin de loss scaling grâce à sa plage dynamique identique à FP32.
Autocast : quelles opérations en quels formats
PyTorch AMP (Automatic Mixed Precision) utilise torch.amp.autocast pour décider automatiquement quelle opération s’exécute en quel format. Les règles générales :
En 16 bits (rapide) : multiplications matricielles (torch.matmul, nn.Linear), convolutions (nn.Conv2d). Ce sont les opérations dominées par le calcul qui bénéficient des Tensor Cores.
En FP32 (stable) : opérations de réduction (sommes, moyennes), normalisation (LayerNorm, BatchNorm), softmax, loss functions. Ces opérations sont sensibles à la précision car elles accumulent de nombreuses petites valeurs.
Implémentation PyTorch
Mixed precision BF16 (recommandé)
import torch
from torch.amp import autocast
model = MonModele().cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
for batch in dataloader:
optimizer.zero_grad()
# Forward en BF16 (pas de GradScaler nécessaire)
with autocast(device_type="cuda", dtype=torch.bfloat16):
outputs = model(batch)
loss = criterion(outputs, labels)
# Backward et update classiques
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
Mixed precision FP16 (avec GradScaler)
import torch
from torch.amp import autocast, GradScaler
scaler = GradScaler() # Dynamic loss scaling
model = MonModele().cuda()
optimizer = torch.optim.AdamW(model.parameters(), lr=3e-4)
for batch in dataloader:
optimizer.zero_grad()
# Forward en FP16
with autocast(device_type="cuda", dtype=torch.float16):
outputs = model(batch)
loss = criterion(outputs, labels)
# Backward avec scaling
scaler.scale(loss).backward()
# Unscale pour gradient clipping
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# Step avec ajustement dynamique du scale
scaler.step(optimizer)
scaler.update()
Avec Hugging Face
from transformers import TrainingArguments
# BF16
args = TrainingArguments(output_dir="./out", bf16=True)
# FP16
args = TrainingArguments(output_dir="./out", fp16=True)
Une seule ligne suffit. Le Trainer gère automatiquement l’autocast, le GradScaler (pour FP16), et la compatibilité avec DeepSpeed et FSDP.
FP16 vs BF16 : quel format choisir
| Critère | FP16 | BF16 |
|---|---|---|
| Plage dynamique | Limitée (~6,5 × 10⁴) | Identique à FP32 (~3,4 × 10³⁸) |
| Précision (mantisse) | 10 bits (meilleure) | 7 bits (moins bonne) |
| Loss scaling requis | Oui (GradScaler) | Non (généralement pas) |
| Risque d’overflow/underflow | Élevé | Très faible |
| GPU minimum | Volta (V100, 2017) | Ampere (A100, 2020) |
| Support GPU consommateur | RTX 2080+ (toutes) | RTX 3090+ (Ampere+) |
| Performance Tensor Core | Similaire à BF16 | Similaire à FP16 |
| Recommandation | GPU anciens uniquement | Par défaut pour tout le reste |
Notre verdict : BF16 quand c’est possible, FP16 sinon. La quasi-totalité des entraînements de LLM en production utilisent BF16. L’avantage de stabilité de BF16 (pas de loss scaling, pas de NaN inattendus) élimine une catégorie entière de bugs d’entraînement.
Combinaison avec d’autres techniques
Le mixed precision est presque toujours combiné avec d’autres optimisations :
Avec le data parallelism (DDP/FSDP). Les gradients communiqués entre GPU sont en 16 bits, réduisant le volume de communication de 50%. FSDP et ZeRO de DeepSpeed supportent nativement la mixed precision.
Avec le gradient accumulation. La réduction mémoire du mixed precision permet d’augmenter le micro-batch size avant de recourir à l’accumulation. Activez toujours le mixed precision avant d’augmenter les steps d’accumulation.
Avec la quantization post-entraînement. Le mixed precision concerne l’entraînement, la quantization concerne l’inférence. Un modèle entraîné en BF16 est ensuite quantizé en INT8 ou INT4 pour le déploiement.
Avec le model parallelism. Le tensor parallelism fragmenté utilise le mixed precision pour les calculs internes de chaque fragment. Megatron-LM supporte BF16 et FP8 nativement.
Problèmes courants et solutions
Loss NaN ou divergence
En FP16, une loss NaN signifie généralement un overflow des activations ou gradients. Solutions : passer à BF16, réduire le learning rate, ajouter du gradient clipping, ou vérifier que les couches de normalisation sont correctement configurées. Si vous utilisez un GradScaler et que les NaN persistent, vérifiez si des opérations personnalisées dans votre modèle produisent des valeurs hors plage FP16.
Loss plate (ne descend pas)
En FP16, cela peut indiquer un underflow des gradients (ils deviennent zéro). Le GradScaler devrait résoudre ce problème. Vérifiez que le scale factor ne chute pas à des valeurs très basses (loggez scaler.get_scale() à chaque step), signe que le modèle produit fréquemment des overflow/underflow. Si le scale factor oscille, essayez d’augmenter le growth_interval du GradScaler pour le rendre moins agressif.
Pas d’accélération visible
Le mixed precision n’accélère que les opérations compute-bound (multiplications matricielles de grande taille). Si votre modèle est memory-bound (petites matrices, beaucoup d’I/O disque, preprocessing lourd), le gain sera minime. Utilisez le PyTorch Profiler ou NVIDIA Nsight Systems pour vérifier que les Tensor Cores sont effectivement utilisés. Activez torch.backends.cuda.matmul.allow_tf32 = True et torch.backends.cudnn.allow_tf32 = True pour maximiser l’utilisation des Tensor Cores même en FP32 (TF32 est un format intermédiaire sur Ampere+ qui accélère les opérations FP32 sans changement de code).
Opérations sensibles à la précision
Certaines opérations personnalisées (custom loss, attention exotique, calculs de distance) peuvent nécessiter un forçage en FP32. Utilisez with autocast(enabled=False): ou castez manuellement les tenseurs avec .float() pour ces opérations spécifiques. Les frameworks comme PyTorch maintiennent une liste d’opérations qui sont automatiquement exécutées en FP32 même sous autocast (softmax, cross-entropy, layer norm, etc.).
Impact mémoire détaillé
Le mixed precision réduit la mémoire de façon ciblée. Voici la décomposition pour un modèle de 7B paramètres :
| Composant | FP32 pur | Mixed Precision (BF16) | Économie |
|---|---|---|---|
| Poids maîtres | 28 Go (FP32) | 28 Go (FP32, copie maître) | 0% |
| Poids de calcul | inclus ci-dessus | 14 Go (BF16, pour forward/backward) | Cast temporaire |
| Gradients | 28 Go (FP32) | 14 Go (BF16) | 50% |
| Activations | Variable (FP32) | Variable (BF16, ~50% plus petit) | ~50% |
| États optimiseur | 56 Go (FP32) | 56 Go (FP32, toujours) | 0% |
L’économie nette est d’environ 30-40% sur la mémoire totale (pas 50% comme souvent annoncé, car les poids maîtres et les états optimiseur restent en FP32). L’économie la plus importante est sur les activations, qui dominent la mémoire pour les grands batch sizes et les longues séquences.
Questions fréquentes sur le mixed precision
Le mixed precision dégrade-t-il la qualité du modèle ?
Non, dans l’immense majorité des cas. Des milliers de modèles (GPT, Llama, BERT, ResNet, etc.) ont été entraînés en mixed precision sans dégradation mesurable de la qualité. La copie maître FP32 des poids assure que les mises à jour restent précises. Les rares exceptions concernent des tâches nécessitant une précision numérique extrême (certains calculs scientifiques, quelques architectures exotiques). Pour l’entraînement de LLM, le mixed precision est considéré comme standard et non optionnel.
BF16 ou FP16 pour le fine-tuning d’un LLM ?
BF16 si votre GPU le supporte (A100, H100, RTX 3090/4090, AMD MI250+). BF16 est plus stable, ne nécessite pas de GradScaler, et élimine les risques d’overflow/underflow. Utilisez FP16 uniquement sur les GPU Volta (V100) ou les RTX 2080, qui ne supportent pas BF16. Sur un GPU consommateur récent (RTX 4090), BF16 est le choix par défaut pour le fine-tuning avec LoRA ou en full fine-tuning.
Peut-on entraîner entièrement en FP16 sans poids FP32 ?
En théorie oui, mais c’est déconseillé. Sans poids maîtres FP32, les petites mises à jour (learning rate × gradient) sont arrondies à zéro en FP16, ce qui empêche la convergence fine. Certains modèles très tolérants (comme certains classifieurs d’images) peuvent converger en FP16 pur, mais pour les LLM, la copie FP32 est indispensable. Le coût mémoire des poids FP32 est compensé par l’économie sur les activations et gradients.
Quand le FP8 sera-t-il standard ?
Le FP8 est déjà utilisé en production par quelques organisations avancées (DeepSeek V3, iGenius Colosseum 355B). Cependant, il nécessite des GPU Hopper (H100) ou plus récents, et demande un tuning minutieux (choix de couches en FP8 vs BF16, ajustement du learning rate). Selon les tendances d’adoption, FP8 pourrait devenir le standard pour le pré-entraînement à grande échelle vers 2027-2028. Pour le moment, BF16 reste le format recommandé par défaut pour la plupart des praticiens.
Le mixed precision fonctionne-t-il sur CPU ?
Partiellement. Les CPU modernes (Intel Sapphire Rapids, AMD Genoa) supportent les instructions BF16, mais le gain est bien moindre que sur GPU car les CPU n’ont pas d’équivalent aux Tensor Cores. Sur CPU, le principal avantage est la réduction mémoire (utile pour l’inférence), pas l’accélération du calcul. Pour l’entraînement, le mixed precision n’a de sens pratique que sur GPU ou TPU.