12 novembre 2026 · Équipe VRAMfarm
Entraînement multi-GPU : DDP, FSDP et ZeRO sur une machine louée de 2 à 8 GPU
Quatre cartes louées, le script lancé avec python train.py, et nvidia-smi affiche une carte à 100 % et trois à 0 %. Rien n’est cassé. Un script d’entraînement utilise un seul GPU, sauf s’il est démarré par un lanceur qui exécute un processus par carte, et le travail doit être réparti d’une manière adaptée à votre modèle. Il y a deux façons de le répartir : donner à chaque carte une copie du modèle et une part des données, ou découper le modèle lui-même en morceaux. Celle qu’il vous faut dépend d’une question : le modèle tient-il sur une seule carte ?
D’abord : avez-vous besoin de plus d’une carte ?
Il n’y a que deux raisons d’utiliser plusieurs GPU : la tâche ne tient pas dans la mémoire d’une carte, ou elle prend trop de temps sur une seule. Avant de prendre une machine multi-GPU, vérifiez si une seule carte plus grande règle le problème. Une carte, c’est toujours plus simple : pas de lanceur, pas de communication entre cartes, pas d’erreurs NCCL.
| Tâche | Une carte qui suffit | Quand plusieurs cartes ont du sens |
|---|---|---|
| LoRA ou QLoRA sur un modèle jusqu’à 70B | 24 à 80 Go, voir le guide de la VRAM | Rarement ; pour entraîner plus vite sur un gros jeu de données |
| Fine-tuning complet d’un modèle 7 à 8B | Une carte de 80 Go avec un optimiseur 8 bits, ou une H200 (141 Go) | Pour utiliser l’optimiseur standard, des séquences plus longues ou un entraînement plus court |
| Fine-tuning complet d’un modèle 13 à 70B | Aucune avec l’optimiseur standard ; un 13B ne tient sur une carte de 141 à 192 Go qu’avec un optimiseur 8 bits | Presque toujours : 4 à 8 cartes avec FSDP ou ZeRO-3 |
| Entraîner un petit modèle sur beaucoup de données | N’importe quelle carte qui le contient | Pour ramener un entraînement de 40 heures à 10 |
| Servir un modèle 70B en fp8 | H200 | 2 × H100 avec vLLM, voir H200 ou H100 |
Les plus grosses cartes du catalogue sont la B200 avec 192 Go, la H200 avec 141 Go et la RTX PRO 6000 avec 96 Go. Si l’une d’elles contient votre tâche, commencez par là.
Parallélisme de données : le même modèle sur chaque carte
L’entraînement en parallélisme de données (PyTorch DDP) exécute un processus par GPU. Chaque processus détient une copie complète du modèle, prend une tranche différente de chaque lot, calcule ses gradients, puis toutes les cartes font la moyenne de leurs gradients avant l’étape de l’optimiseur. Les cartes restent identiques, et N cartes traitent N fois plus de données par étape.
Une seule condition : le modèle, ses gradients, les états de son optimiseur et les activations doivent tenir sur une carte. Si c’est le cas, DDP est le moyen le plus simple d’aller plus vite. Vous démarrez le script avec un lanceur au lieu de python :
torchrun --nproc_per_node=4 train.py # PyTorch standard, Hugging Face Trainer
accelerate launch --num_processes 4 train.py # Hugging Face Accelerate
Le Trainer de Hugging Face et la plupart des outils de fine-tuning (Axolotl, LLaMA-Factory, torchtune) passent d’eux-mêmes en DDP quand ils sont lancés ainsi. En PyTorch pur, vous enveloppez le modèle dans DistributedDataParallel et ajoutez un DistributedSampler.
Le prix à payer, c’est la communication : après chaque étape, les cartes s’échangent une copie des gradients. Pour un modèle 1B en bf16, cela représente environ 2 Go par étape, ce que le PCIe absorbe pendant que la rétropropagation tourne encore. Comptez qu’avec deux cartes vous irez environ 1,7 à 1,9 fois plus vite qu’avec une, et avec quatre cartes environ 3 à 3,7 fois, moins pour les gros modèles sur des machines sans NVLink.
Quand le modèle ne tient pas : FSDP et DeepSpeed ZeRO
Avec l’optimiseur AdamW standard en précision mixte, chaque paramètre coûte environ 16 octets : 2 pour les poids en bf16, 2 pour les gradients et 12 pour l’optimiseur (une copie maîtresse en fp32 et deux estimations de moments). Pour un modèle 8B, cela fait 128 Go avant les activations, plus que n’importe quelle carte de 80 Go.
Le sharding répartit ces 16 octets entre les cartes au lieu de les copier. DeepSpeed appelle cela ZeRO et propose trois niveaux ; FSDP, dans PyTorch, fait la même chose sous d’autres noms. Pour un modèle 8B :
| Méthode | Ce que détient chaque carte | Par carte, 4 cartes | Par carte, 8 cartes |
|---|---|---|---|
| DDP | Tout | 128 Go | 128 Go |
| ZeRO-1 | Poids, gradients, 1/N de l’optimiseur | 56 Go | 44 Go |
ZeRO-2, FSDP SHARD_GRAD_OP |
Poids, 1/N des gradients et de l’optimiseur | 44 Go | 30 Go |
ZeRO-3, FSDP FULL_SHARD |
1/N de tout | 32 Go | 16 Go |
Les activations s’ajoutent, de quelques Go à plusieurs dizaines de Go par carte selon la longueur des séquences et la taille du lot ; le gradient checkpointing les réduit au prix d’environ un tiers de calcul en plus. Lecture du tableau : 4 cartes de 80 Go font tourner confortablement un fine-tuning complet de 8B avec ZeRO-2 ou ZeRO-3, 4 cartes de 48 Go avec ZeRO-3, et des cartes de 24 Go seulement à 8, ou avec déchargement de l’optimiseur vers la RAM du CPU.
FSDP fait partie de PyTorch ; DeepSpeed se configure avec un fichier JSON. Le Trainer de Hugging Face et Accelerate prennent en charge les deux. Le déchargement vers le CPU (ZeRO-Offload, cpu_offload de FSDP) place les états de l’optimiseur dans la RAM système et permet à de petites cartes d’entraîner de gros modèles, lentement, et seulement si la machine a la RAM nécessaire.
ZeRO-3 a un coût qui compte sur une machine louée : les poids eux-mêmes circulent entre les cartes à chaque étape. Pour un modèle 8B sur quatre cartes, cela fait en gros 35 à 50 Go par carte et par étape. En NVLink sur une machine H100, cela prend une fraction de seconde ; en PCIe, cela peut prendre des secondes, et les cartes attendent. Voilà pourquoi le type de liaison compte.
Parallélisme tensoriel et pipeline, et ce qu’utilise l’inférence
Le parallélisme tensoriel découpe chaque matrice de poids en tranches, une par carte, et échange des données à chaque couche : il ne fonctionne donc bien qu’en NVLink. Le parallélisme pipeline place des couches différentes sur des cartes différentes, avec moins de trafic, mais des cartes inactives le temps que le pipeline se remplisse. Les frameworks de pré-entraînement comme Megatron combinent les deux ; pour le fine-tuning, FSDP ou ZeRO suffit généralement.
Pour l’inférence, c’est plus simple. vLLM découpe un modèle avec --tensor-parallel-size 2 (ou 4, 8), et llama.cpp comme Ollama répartissent d’eux-mêmes les couches entre les cartes. L’inférence échange peu de données entre cartes : elle tourne donc correctement même en PCIe.
Un piège : device_map="auto" dans Hugging Face Transformers répartit les couches sur les cartes et les exécute l’une après l’autre. Le modèle tient, mais une seule carte travaille à un instant donné. C’est très bien pour essayer un gros modèle, pas pour la vitesse d’entraînement.
NVLink ou PCIe sur les machines louées
| Liaison | Débit entre cartes, approximatif | Où on la trouve |
|---|---|---|
| PCIe 4.0 x16 | 25 à 32 Go/s par sens | RTX 3090, RTX 4090, A6000, la plupart des cartes PCIe |
| PCIe 5.0 x16 | 50 à 64 Go/s par sens | RTX 5090, RTX PRO 6000, serveurs récents |
| Pont NVLink, RTX 3090 | environ 112 Go/s | Rare sur les machines louées |
| NVLink, A100 SXM | 600 Go/s | Cartes mères A100 SXM à 4 et 8 cartes |
| NVLink, H100 / H200 SXM | 900 Go/s | Cartes mères SXM à 4 et 8 cartes |
Les cartes grand public ajoutent une limite : la RTX 4090 ne prend pas en charge les transferts directs pair à pair, et les données entre cartes passent donc par la mémoire système. Les machines à plusieurs 4090 conviennent bien à l’inférence et à DDP sur de petits modèles, mal à ZeRO-3 sur de gros modèles.
Le catalogue affiche le nom de la carte avant le lancement. « SXM » ou « SXM4 » signifie que les cartes sont montées sur une carte mère conçue pour NVLink ; les cartes PCIe peuvent avoir des ponts, ou pas. Le filtre « PCIe » du catalogue indique le débit du bus entre le CPU et les GPU. Il compte pour charger les données et les modèles (une carte sur un emplacement x4 étroit charge lentement), mais il ne dit rien des liaisons entre cartes. Pour cela, lancez ceci juste après le démarrage :
nvidia-smi topo -m
NV4, NV12 ou NV18 entre deux cartes signifie NVLink. PIX et PXB désignent un switch PCIe, PHB le pont PCIe du CPU, et NODE ou SYS un chemin qui traverse le CPU, le plus lent.
Louer une machine multi-GPU sur VRAMfarm
- Choisissez la machine. Dans le catalogue des serveurs, réglez « Cartes par machine » sur 2×, 4× ou 8×. Le prix est à l’heure pour toute la machine, à peu près le nombre de cartes multiplié par le prix d’une seule : à partir de 2,36 € de l’heure pour 4 × RTX 4090, à partir de 31,20 € pour 8 × H100. Chaque ligne indique le nom de la carte, les cœurs CPU, la RAM, le disque, le pilote, la version de CUDA et le pays.
- Utilisez les filtres qui comptent pour l’entraînement. VRAM par machine, RAM (au moins la VRAM totale), cœurs CPU (4 à 8 par carte), disque libre (un checkpoint complet d’un modèle 8B avec les états de l’optimiseur dépasse 100 Go), et « Durée de location », le nombre de jours d’affilée pendant lesquels la machine peut être gardée. Prévoyez une marge pour les longs entraînements.
- Choisissez un serveur vide. PyTorch avec Jupyter, ou votre propre image Docker. Pour lancer, votre solde doit couvrir une heure de toute la machine ; c’est une vérification, pas un prélèvement, et la facturation se fait à la minute.
- Vérifiez la machine dans les premières minutes. Lancez
nvidia-smi,nvidia-smi topo -metpython -c "import torch; print(torch.cuda.device_count())". Si quelque chose ne va pas, arrêtez le serveur : vous payez le minimum de 15 minutes, pas l’heure. - Faites un court test de communication avant la vraie tâche :
# nccl_check.py : à lancer avec torchrun --nproc_per_node=4 nccl_check.py
import os, time, torch, torch.distributed as dist
rank = int(os.environ["LOCAL_RANK"]); torch.cuda.set_device(rank)
dist.init_process_group("nccl")
x = torch.ones(512 * 1024**2, dtype=torch.bfloat16, device="cuda") # 1 Gio
for _ in range(3): dist.all_reduce(x)
torch.cuda.synchronize(); t = time.time()
for _ in range(10): dist.all_reduce(x)
torch.cuda.synchronize()
if rank == 0: print(f"{10 / (time.time() - t):.1f} all-reduces of 1 GiB per second")
dist.destroy_process_group()
S’il affiche un nombre, les cartes communiquent ; une machine NVLink devrait être bien plus rapide qu’une machine PCIe. S’il reste bloqué, voyez le premier piège ci-dessous.
L’API prend le nombre de cartes dans count : un script peut ainsi démarrer une machine à 8 cartes pour un entraînement de nuit et l’arrêter une fois la tâche terminée.
Les pièges courants
- NCCL se bloque ou affiche « unhandled system error ». Lancez avec
NCCL_DEBUG=INFOpour voir où il s’arrête. Sur les cartes grand public,NCCL_P2P_DISABLE=1règle souvent les blocages. Si un processus a planté, les autres l’attendent jusqu’au délai d’expiration : cherchez la première erreur dans le log, pas la dernière. - Mémoire partagée trop petite. Dans les conteneurs,
/dev/shmne fait parfois que quelques dizaines de mégaoctets, ce qui casse NCCL et les data loaders avec une « bus error ». Vérifiez avecdf -h /dev/shm; baisseznum_workers, ou essayezNCCL_SHM_DISABLE=1. - La taille de lot a changé sans que vous le remarquiez. Lot global = lot par carte × nombre de cartes × étapes d’accumulation de gradient. Passer de 1 à 4 cartes avec la même configuration rend le lot quatre fois plus grand et l’époque quatre fois plus courte en nombre d’étapes. Réduisez les étapes d’accumulation pour garder le même lot global, ou augmentez le taux d’apprentissage : à peu près linéairement pour SGD, plus prudemment (racine carrée) pour Adam, avec un warm-up.
- Toutes les cartes voient les mêmes données. Sans
DistributedSampler, toutes les cartes s’entraînent sur des lots identiques ; sanssampler.set_epoch(epoch), chaque époque garde le même ordre. Le Trainer de Hugging Face gère les deux. - Quatre copies de chaque log et de chaque checkpoint. N’écrivez des fichiers que sur le rank 0. Un checkpoint FSDP complet est rassemblé dans la RAM CPU d’un seul processus ; les checkpoints fragmentés évitent cela.
- « System not yet initialized » sur une machine SXM à 8 cartes. Cela vient de la structure NVLink de l’hôte, pas de votre code. Arrêtez le serveur, choisissez une autre machine et prévenez le support.
Les longs entraînements demandent deux habitudes. Lancez l’entraînement dans tmux, pour qu’il survive à un ordinateur portable refermé. Enregistrez des checkpoints toutes les 30 à 60 minutes dans /workspace, ne gardez que les deux derniers, et copiez-les hors de la machine pour les entraînements de plus d’une journée : le disque du serveur est effacé à l’arrêt, sauf si vous enregistrez le dossier (ce que deviennent vos fichiers). Tant que les GPU travaillent, la détection d’inactivité laisse le serveur tranquille. Quand l’entraînement se termine, elle commence à compter : après 2 heures sans charge GPU ni connexion SSH ou Jupyter, vous recevez un e-mail, et une heure plus tard le dossier est enregistré et le serveur s’arrête. Vous pouvez désactiver ce comportement, et les serveurs démarrés depuis votre propre image Docker ne sont pas arrêtés automatiquement. Une option plus économique : une dernière ligne dans votre script qui arrête le serveur via l’API.
Commencez avec deux cartes et un entraînement court, mesurez le temps par étape, puis passez à quatre ou huit. Si deux cartes ne vont pas presque deux fois plus vite, davantage de cartes n’y changera rien : regardez d’abord la liaison entre elles.