Data Parallelism
Le data parallelism (parallélisme de données) est une technique d’entraînement distribué qui réplique un modèle de deep learning sur plusieurs GPU, répartit les données d’entraînement entre eux, et synchronise les gradients pour mettre à jour une seule copie cohérente du modèle.
C’est la forme de parallélisme la plus simple et la plus utilisée en deep learning. Le concept est direct : chaque GPU possède une copie complète du modèle, reçoit un sous-ensemble différent du mini-batch, calcule ses gradients locaux, puis tous les GPU échangent et moyennent ces gradients via une opération all-reduce avant de mettre à jour leurs poids de façon identique. Résultat : N GPU = N fois plus de données traitées par étape, sans que l’utilisateur ait à modifier l’architecture du modèle.
Le data parallelism est la première technique qu’un praticien ML rencontre quand il passe d’un seul GPU à plusieurs. En PyTorch, il suffit d’envelopper le modèle avec DistributedDataParallel (DDP) pour scaler l’entraînement sur un cluster entier. C’est le socle sur lequel toutes les autres stratégies de parallélisme se construisent.
- Catégorie
- Entraînement distribué / Parallélisme
- Principe
- Réplique le modèle, partitionne les données
- Variantes
- DDP, FSDP (ZeRO-3), Distributed Optimizer (ZeRO-1/2)
- Communication
- All-reduce des gradients (backward uniquement)
- API PyTorch
torch.nn.parallel.DistributedDataParallel- Prérequis
- Le modèle doit tenir en mémoire sur un seul GPU
- Opposé à
- Model Parallelism (qui partitionne le modèle)
Comment fonctionne le data parallelism
Le data parallelism repose sur un principe simple en quatre étapes, répétées à chaque itération d’entraînement :
1. Réplication du modèle. Au démarrage, le GPU de rang 0 broadcast ses paramètres initiaux à tous les autres GPU. Chaque processus détient une copie identique du modèle.
2. Distribution des données. Le DistributedSampler partitionne le dataset de sorte que chaque GPU reçoive un sous-ensemble exclusif du mini-batch. Si le batch global est de 1024 échantillons et qu’il y a 4 GPU, chaque GPU traite 256 échantillons.
3. Calcul indépendant. Chaque GPU exécute la passe forward puis la passe backward de façon complètement indépendante sur ses données locales. Les gradients calculés sont locaux à chaque processus.
4. Synchronisation des gradients. Avant la mise à jour des poids, une opération all-reduce somme les gradients de tous les GPU et redistribue le résultat. Chaque GPU divise ensuite par le nombre de processus pour obtenir le gradient moyen. L’optimiseur (Adam, SGD, etc.) applique alors une mise à jour identique sur chaque réplique, maintenant les paramètres synchronisés.
DDP : DistributedDataParallel en détail
PyTorch propose deux implémentations du data parallelism. L’ancienne, DataParallel (DP), utilise un seul processus avec du multithreading. Elle est inefficace à cause du GIL Python et d’un goulot d’étranglement sur le GPU maître. La version moderne, DistributedDataParallel (DDP), lance un processus séparé par GPU, contourne le GIL et utilise NCCL pour la communication. C’est la méthode recommandée dans tous les cas.
Mécanismes internes de DDP
DDP n’attend pas la fin du backward complet pour commencer à communiquer. Il utilise deux optimisations clés :
Bucketing (regroupement par seaux). Les gradients des paramètres sont regroupés dans des « buckets » de taille configurable (25 Mo par défaut). Dès qu’un bucket est complet (tous ses gradients sont prêts), l’all-reduce démarre pour ce bucket, même si le backward continue pour les couches précédentes. Les paramètres sont alloués aux buckets dans l’ordre inverse de Model.parameters(), car les gradients deviennent disponibles dans cet ordre pendant le backward.
Overlap computation/communication. Grâce au bucketing, la communication du bucket N chevauche le calcul des gradients du bucket N+1. C’est ce recouvrement qui donne à DDP son efficacité : la communication est « gratuite » tant qu’elle est masquée par le calcul.
Implémentation PyTorch
Voici le squelette minimal pour utiliser DDP avec torchrun :
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data.distributed import DistributedSampler
# Initialisation du processus distribué
dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
# Modèle et DDP
model = MonModele().to(local_rank)
model = DDP(model, device_ids=[local_rank])
# DataLoader avec DistributedSampler
sampler = DistributedSampler(dataset)
loader = DataLoader(dataset, sampler=sampler, batch_size=64)
# Boucle d'entraînement classique
for epoch in range(num_epochs):
sampler.set_epoch(epoch) # Shuffle différent par epoch
for batch in loader:
loss = model(batch)
loss.backward() # All-reduce implicite ici
optimizer.step()
optimizer.zero_grad()
Lancement avec torchrun sur 4 GPU d’un seul noeud :
torchrun --nproc_per_node=4 train.py
Sur un cluster multi-noeud (2 noeuds × 8 GPU) :
# Sur le noeud 0 (master)
torchrun --nproc_per_node=8 --nnodes=2 --node_rank=0
--master_addr=192.168.1.1 --master_port=29500 train.py
# Sur le noeud 1
torchrun --nproc_per_node=8 --nnodes=2 --node_rank=1
--master_addr=192.168.1.1 --master_port=29500 train.py
DistributedSampler utilise le même shuffle à chaque epoch, ce qui signifie que chaque GPU voit exactement les mêmes données à chaque epoch. C’est un piège classique qui dégrade silencieusement la convergence.
Limites du DDP classique
Le data parallelism via DDP a une contrainte fondamentale : chaque GPU doit stocker une copie complète du modèle (poids + gradients + états optimiseur). Pour un modèle de 7B paramètres en BF16, cela représente environ :
| Composant | Taille par GPU | Détail |
|---|---|---|
| Poids (BF16) | ~14 Go | 7B × 2 octets |
| Gradients (BF16) | ~14 Go | Même taille que les poids |
| États optimiseur Adam (FP32) | ~56 Go | 2 états × 7B × 4 octets |
| Total par GPU | ~84 Go | Dépasse un H100 80 Go |
Même un modèle de 7B paramètres peut être problématique pour l’entraînement en DDP pur sur un H100 80 Go, sans compter la mémoire des activations. C’est ce plafond qui a motivé le développement de FSDP et ZeRO.
FSDP et ZeRO : au-delà du DDP classique
FSDP (Fully Sharded Data Parallelism) et ZeRO (Zero Redundancy Optimizer) de DeepSpeed résolvent le problème de mémoire en éliminant la redondance. L’idée : plutôt que de stocker une copie complète sur chaque GPU, on fragmente les données du modèle et on ne reconstruit que ce qui est nécessaire au moment du calcul.
Les trois stages de ZeRO
Microsoft Research a formalisé cette approche en trois niveaux progressifs de fragmentation :
| Stage | Ce qui est fragmenté | Économie mémoire | Communication |
|---|---|---|---|
| ZeRO-1 | États optimiseur uniquement | ~4× sur les états | Identique au DDP |
| ZeRO-2 | États optimiseur + gradients | ~8× sur états + gradients | Reduce-scatter au lieu d’all-reduce |
| ZeRO-3 / FSDP | États + gradients + poids | Proportionnelle au nombre de GPU | All-gather + reduce-scatter par couche |
ZeRO-1 est quasi-gratuit en termes de communication (même all-reduce que DDP) tout en libérant significativement de la mémoire. C’est le choix par défaut dans DeepSeek V3 pour la composante data-parallèle de son entraînement. ZeRO-3 (équivalent à FSDP) offre l’économie maximale mais ajoute un overhead de communication non négligeable.
FSDP en pratique
L’implémentation PyTorch FSDP (FSDP2 dans les versions récentes) fonctionne couche par couche :
Forward. Avant de calculer une couche, FSDP fait un all-gather pour reconstituer les poids complets de cette couche à partir des fragments distribués. Le calcul s’exécute normalement. Les poids reconstitués sont immédiatement libérés après usage.
Backward. Même processus : all-gather des poids, calcul du gradient, puis reduce-scatter pour distribuer les fragments de gradient vers chaque GPU propriétaire.
En mars 2026, NVIDIA a publié Megatron-FSDP, une implémentation compatible avec Megatron-LM qui promet jusqu’à 25% d’accélération et 23% d’économie mémoire par rapport à PyTorch FSDP2, grâce à une gestion optimisée des buffers de communication et au support de la précision mixte FP8.
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import ShardingStrategy
model = FSDP(
model,
sharding_strategy=ShardingStrategy.FULL_SHARD, # ZeRO-3
mixed_precision=MixedPrecision(
param_dtype=torch.bfloat16,
reduce_dtype=torch.bfloat16,
),
)
Synchrone vs Asynchrone
Le data parallelism existe en deux saveurs de synchronisation :
Synchrone (le standard). Tous les GPU attendent la fin de l’all-reduce avant de mettre à jour les poids. Les répliques restent toujours parfaitement synchronisées. C’est le mode par défaut de DDP et de presque tous les frameworks modernes. La convergence est identique à l’entraînement sur un seul GPU avec un batch plus grand.
Asynchrone (Parameter Server). Chaque GPU pousse ses gradients vers un serveur de paramètres et récupère les poids mis à jour sans attendre les autres. Les GPU ne sont jamais bloqués, ce qui maximise le débit matériel. Le problème : les gradients « stale » (calculés à partir de poids obsolètes) peuvent dégrader la convergence et la précision finale du modèle.
En pratique, l’entraînement synchrone domine largement. Les optimisations d’overlap (bucketing, overlap computation/communication) réduisent suffisamment le temps d’attente pour rendre l’asynchrone rarement nécessaire. Google, Meta, Anthropic et OpenAI utilisent tous de l’entraînement synchrone pour leurs LLM.
Scaling : batch size et learning rate
Quand vous ajoutez des GPU en data parallelism, vous augmentez le batch global (batch local × nombre de GPU). Cela a des implications directes sur la convergence.
La règle du linear scaling
La pratique standard est d’augmenter le learning rate proportionnellement au batch global. Si vous doublez le nombre de GPU (et donc le batch), vous doublez le learning rate. Cette « règle du linear scaling » a été formalisée par Goyal et al. (Facebook, 2017) pour l’entraînement d’ImageNet en 1 heure sur 256 GPU.
La règle a ses limites. Au-delà d’un certain batch size (qui dépend du modèle et du dataset), la convergence se dégrade même avec un learning rate ajusté. Pour les LLM, les batch sizes optimaux sont dictés par les scaling laws (lois de Chinchilla/Kaplan) et sont généralement fixés à l’avance, indépendamment du nombre de GPU.
Warmup learning rate
Avec des batch sizes très grands, un warmup graduel du learning rate pendant les premières itérations est essentiel. Le modèle commence avec un learning rate faible et l’augmente linéairement jusqu’à la valeur cible sur quelques centaines ou milliers de steps. Sans warmup, les très grands batch sizes provoquent souvent des divergences en début d’entraînement.
Gradient accumulation comme alternative
Si vous n’avez pas assez de GPU pour atteindre le batch global souhaité, le gradient accumulation simule un batch plus grand en accumulant les gradients sur plusieurs micro-batches avant de les synchroniser. DDP supporte cela nativement via le context manager model.no_sync(), qui désactive l’all-reduce pendant les itérations d’accumulation.
# Accumulation sur 4 micro-batches
for i, batch in enumerate(loader):
with model.no_sync() if (i + 1) % 4 != 0 else contextlib.nullcontext():
loss = model(batch) / 4
loss.backward()
if (i + 1) % 4 == 0:
optimizer.step()
optimizer.zero_grad()
Data Parallelism vs Model Parallelism
La distinction est fondamentale et souvent source de confusion. Voici un tableau comparatif complet :
| Critère | Data Parallelism | Model Parallelism |
|---|---|---|
| Ce qui est distribué | Les données (mini-batches) | Le modèle (poids, couches) |
| Copie du modèle | Complète sur chaque GPU | Fragment sur chaque GPU |
| Communication | All-reduce en backward uniquement | Forward ET backward (synchronisation constante) |
| Latence ajoutée | Faible (masquée par le calcul) | Significative (sur le chemin critique) |
| Complexité code | 1-3 lignes avec DDP | Restructuration du modèle souvent nécessaire |
| Scalabilité mémoire | Limitée (modèle complet par GPU) | Illimitée (partitionnement arbitraire) |
| Scalabilité calcul | Quasi-linéaire en nombre de GPU | Sous-linéaire (overhead communication) |
| Cas d’usage | Modèles qui tiennent sur 1 GPU | Modèles trop grands pour 1 GPU |
En réalité, les systèmes modernes combinent les deux. Le parallélisme 3D (TP × PP × DP) distribue le modèle via tensor et pipeline parallelism, puis réplique ces fragments via data parallelism. FSDP brouille la frontière en fragmentant les paramètres tout en restant dans le paradigme data-parallèle.
Frameworks pour le data parallelism
PyTorch DDP
L’implémentation de référence. DDP utilise NCCL comme backend de communication sur GPU NVIDIA, Gloo pour les CPU ou les environnements sans NCCL. Le backend ProcessGroup gère l’enregistrement des hooks autograd qui déclenchent les all-reduce pendant le backward. Depuis PyTorch 2.x, torchrun est le lanceur recommandé (remplaçant l’ancien torch.distributed.launch).
Horovod (Uber/LF AI)
Framework de data parallelism conçu pour être agnostique : il fonctionne avec PyTorch, TensorFlow et MXNet. Horovod utilise Ring All-Reduce via NCCL ou MPI. Son API est légèrement différente de DDP (il enveloppe l’optimiseur plutôt que le modèle). Historiquement très populaire, Horovod est progressivement remplacé par DDP natif dans la plupart des workflows PyTorch.
DeepSpeed
DeepSpeed de Microsoft intègre ZeRO (stages 1, 2, 3), ZeRO-Offload (qui décharge les états sur CPU ou NVMe), et ZeRO++ (optimisations de communication). DeepSpeed s’interface avec PyTorch et fournit son propre moteur d’entraînement qui remplace la boucle manuelle.
Hugging Face Accelerate
Couche d’abstraction qui détecte automatiquement le matériel disponible et configure DDP, FSDP ou DeepSpeed de façon transparente. Idéal pour le fine-tuning de modèles Hugging Face avec un minimum de code spécifique au distribué.
Bonnes pratiques pour le data parallelism
Vérifiez que le modèle tient en mémoire
Avant de lancer DDP, vérifiez la consommation mémoire sur un seul GPU. Si le modèle + gradients + optimiseur dépassent la VRAM, passez directement à FSDP ou ZeRO. Inutile de débugger des OOM (Out Of Memory) qui sont structurellement inévitables.
Calibrez le batch size global
Le batch global = batch local × nombre de GPU. Pour les LLM, le batch global optimal est souvent fixé par la recherche (ex. 4M tokens pour Llama 2). Le nombre de GPU détermine alors le batch local, pas l’inverse. Si le batch local descend en dessous de 1, vous avez atteint la limite du data parallelism et devez utiliser du model parallelism pour réduire le nombre de GPU data-parallèles.
Utilisez la précision mixte
L’entraînement en mixed precision (BF16 ou FP16) divise par deux la taille des activations et des gradients, ce qui réduit à la fois la mémoire et le volume de communication pour l’all-reduce. C’est compatible avec DDP et FSDP sans modification.
Surveillez le throughput, pas la loss
En data parallelism, le throughput (samples/seconde ou tokens/seconde) devrait augmenter quasi-linéairement avec le nombre de GPU. Si ce n’est pas le cas, le goulot d’étranglement est probablement la communication (bande passante réseau insuffisante) ou le chargement des données (I/O disque). Profitez des outils de profiling (PyTorch Profiler, NVIDIA Nsight) pour identifier le problème.
Checkpointing et tolérance aux pannes
Avec DDP, seul le processus de rang 0 a besoin de sauvegarder le checkpoint (tous les GPU ont les mêmes poids). Avec FSDP, tous les processus doivent participer à la sauvegarde car chacun détient un fragment différent. Utilisez torch.distributed.checkpoint pour gérer les checkpoints distribués de façon robuste.
Efficacité et scaling en pratique
L’efficacité du data parallelism dépend du ratio entre le temps de calcul et le temps de communication. Plus le modèle est grand (plus de calcul par step) et plus la bande passante est élevée (communication plus rapide), meilleur est le scaling.
| Configuration | Bande passante | Efficacité DDP typique |
|---|---|---|
| 4 GPU, même noeud, NVLink | 600-900 Go/s | 95-99% |
| 8 GPU, même noeud, NVLink | 600-900 Go/s | 90-97% |
| 16 GPU, 2 noeuds, InfiniBand 400G | ~50 Go/s inter-noeud | 85-93% |
| 64 GPU, 8 noeuds, InfiniBand 400G | ~50 Go/s inter-noeud | 75-90% |
| 256+ GPU, Ethernet 100G | ~12 Go/s inter-noeud | 50-75% |
L’efficacité chute significativement sur Ethernet standard. C’est pourquoi les clusters de training sérieux utilisent InfiniBand ou RoCE (RDMA over Converged Ethernet) avec GPUDirect RDMA, qui permet au réseau d’accéder directement à la mémoire GPU sans passer par le CPU.
Questions fréquentes sur le data parallelism
Quelle est la différence entre DataParallel et DistributedDataParallel en PyTorch ?
DataParallel (DP) utilise un seul processus Python avec du multithreading. Le GPU 0 collecte les sorties de tous les GPU, calcule la loss, puis redistribue les gradients. C’est simple (une seule ligne de code) mais très inefficace : le GIL Python limite le parallélisme, et le GPU 0 devient un goulot d’étranglement. DistributedDataParallel (DDP) lance un processus indépendant par GPU, contourne le GIL, et utilise l’all-reduce NCCL pour synchroniser les gradients directement entre GPU. DDP est systématiquement plus rapide et est la seule méthode recommandée. DP est considéré comme déprécié en pratique.
Combien de GPU peut-on utiliser en data parallelism ?
Il n’y a pas de limite théorique. En pratique, le scaling devient inefficace quand le batch local par GPU devient trop petit (car le GPU est sous-utilisé) ou quand la communication domine le temps de calcul. Pour un modèle de 7B paramètres, le data parallelism seul peut scaler efficacement à 64-128 GPU. Au-delà, il est préférable de combiner avec du tensor ou pipeline parallelism. Llama 2 70B a été entraîné sur 2000 GPU avec du parallélisme multi-dimensionnel, dont une composante data-parallèle.
FSDP est-il toujours meilleur que DDP ?
Non. FSDP introduit un overhead de communication significatif (all-gather à chaque forward et backward). Pour les modèles qui tiennent confortablement en mémoire avec DDP, FSDP est plus lent sans bénéfice. Utilisez FSDP uniquement quand la mémoire est le facteur limitant. Une règle simple : si votre modèle + optimiseur + activations tiennent avec marge dans la VRAM avec DDP, restez sur DDP. Si vous frôlez le OOM ou si vous devez réduire le batch local à 1, passez à FSDP.
Le data parallelism change-t-il la convergence du modèle ?
En théorie, non. Le data parallelism synchrone est mathématiquement équivalent à un entraînement sur un seul GPU avec un batch plus grand. Chaque GPU calcule des gradients sur des données différentes, et la moyenne de ces gradients est identique au gradient du batch global complet. En pratique, deux subtilités existent. D’abord, les opérations en virgule flottante ne sont pas parfaitement associatives : la somme de gradients dans un ordre différent peut donner des résultats très légèrement différents. Ensuite, un batch global plus grand peut nécessiter un ajustement du learning rate et du warmup pour converger aussi bien.
Peut-on utiliser le data parallelism pour l’inférence ?
Oui, mais on parle plutôt de « réplication de modèle » dans ce contexte. Pour l’inférence, chaque GPU héberge une copie indépendante du modèle et traite des requêtes différentes en parallèle. Il n’y a pas de synchronisation de gradients. C’est la façon la plus simple d’augmenter le throughput d’inférence. vLLM supporte ce mode avec le paramètre --data-parallel-size, qui réplique le modèle sur plusieurs groupes de GPU tout en utilisant optionnellement du tensor parallelism au sein de chaque groupe.