Des articles approfondis sur les technologies qui façonnent l'avenir.

Quand votre GPU manque de VRAM : que faire ?

La VRAM du GPU est le goulot d'étranglement des charges IA et graphiques. Mémoire unifiée, déport de VRAM et swap sur NVMe : le fonctionnement réel.

Carte graphique débordant de données lumineuses qui se déversent dans un disque de stockage en dessous

Le message d'erreur le plus courant en machine learning n'est ni une trace Python ni une erreur de dimensions. C'est CUDA out of memory. Votre modèle est trop gros, votre taille de batch trop importante, ou vos activations intermédiaires ne tiennent pas dans la VRAM de votre GPU. Une RTX 4090 dispose de 24 Go. Un modèle de 70 milliards de paramètres en demi-précision en nécessite environ 140 Go. Le calcul ne tient pas, et acheter des GPU plus gros ne fait que repousser le problème : un H100 avec 80 Go ne suffit toujours pas pour charger les plus grands modèles sur une seule carte.

Mais et si vous pouviez étendre de manière transparente la mémoire du GPU en utilisant la RAM système, voire le stockage NVMe ? Des outils comme Greenboost de NVIDIA et d'autres projets similaires font exactement cela : ils exploitent la hiérarchie mémoire (VRAM → RAM système → SSD) pour faire tourner des charges de travail qui ne devraient pas tenir sur votre matériel. Les compromis en termes de performances sont réels, mais étonnamment gérables pour de nombreux cas d'usage. Pour comprendre comment cela fonctionne, il faut d'abord comprendre la hiérarchie mémoire du GPU elle-même.

Pourquoi la mémoire GPU est différente de la mémoire CPU

La mémoire CPU et la mémoire GPU répondent à des schémas d'accès fondamentalement différents. Les charges CPU sont sensibles à la latence : un thread a besoin d'une donnée et reste bloqué jusqu'à son arrivée. Les caches CPU sont conçus pour minimiser la latence sur des accès aléatoires.

Les charges GPU, elles, sont sensibles au débit : des milliers de threads ont chacun besoin de leurs données, et le GPU peut basculer d'un thread à l'autre pour masquer la latence. La mémoire GPU (HBM sur les GPU de datacenter, GDDR sur les cartes grand public) est conçue pour la bande passante : elle livre d'énormes volumes de données par seconde, même si chaque accès individuel prend plus de temps qu'un hit dans le cache CPU.

Memory bandwidth comparison (approximate):
RTX 4090 GDDR6X:     1,000 GB/s
H100 HBM3:           3,350 GB/s
DDR5 System RAM:       50 GB/s
PCIe 5.0 x16:          64 GB/s (theoretical max)
NVMe SSD:               7 GB/s
The bandwidth cliff between VRAM and system RAM is ~20x.
Between VRAM and NVMe it's ~140x.
This is why naive offloading to system RAM kills performance —
you're trying to feed a 1,000 GB/s appetite through a 50 GB/s straw.

Cet écart de bande passante explique pourquoi rendre la mémoire GPU « virtuelle », en paginant les données vers la RAM système comme le CPU le fait avec le disque, ne fonctionne pas naïvement. Un CPU supporte les défauts de page avec un ralentissement de l'ordre de 10x. Un GPU qui accède à la RAM système au lieu de la VRAM voit sa bande passante chuter d'un facteur 20 : pour les charges limitées par la bande passante (c'est le cas de la plupart des inférences ML), cela signifie un ralentissement de 20x.

Comment le déport de VRAM fonctionne réellement

L'astuce n'est pas de traiter la RAM système comme une VRAM lente. Il s'agit de précharger les données depuis la RAM système vers la VRAM avant que le GPU en ait besoin, en masquant la latence derrière le calcul. C'est l'idée clé derrière toutes les techniques pratiques d'extension de VRAM.

Lors de l'inférence d'un réseau de neurones, le calcul s'enchaîne couche après couche. Pendant que le GPU traite la couche 5, il sait que la couche 6 est la suivante. Un système de déport intelligent peut donc commencer à transférer les poids de la couche 6 depuis la RAM système vers la VRAM pendant le calcul de la couche 5. Si le calcul prend plus de temps que le transfert (c'est souvent le cas pour les grosses couches), le transfert est totalement masqué : le GPU ne cale jamais.

# Conceptual overlap of compute and transfer
# (simplified pseudocode)
def inference_with_offloading(model, input_data):
# Only 2 layers fit in VRAM at a time
# Rest are in system RAM
for i, layer in enumerate(model.layers):
# Start async transfer of NEXT layer while computing current
if i + 1 < len(model.layers):
async_transfer_to_gpu(model.layers[i + 1])
# Compute on current layer (GPU is busy, transfer happens in parallel)
output = layer.forward(input_data)
# Evict current layer from VRAM (it's done)
transfer_to_ram(layer)
# Wait for next layer transfer to complete (usually already done)
sync_transfer()
input_data = output
return output

Cette approche par pipeline fonctionne bien pour l'inférence, car le graphe de calcul est prévisible : vous savez exactement quels poids seront nécessaires ensuite. L'entraînement est plus difficile, car les passes arrière ont besoin des activations de la passe avant, ce qui crée des schémas de mouvement de données plus complexes.

Les approches en pratique

CUDA Unified Memory

La CUDA Unified Memory de NVIDIA crée un espace d'adressage unique couvrant à la fois la VRAM du GPU et la RAM système. Le runtime CUDA migre automatiquement les pages entre les deux selon les schémas d'accès. Lorsque le GPU accède à une page située dans la RAM système, cela déclenche un défaut de page et la page est migrée vers la VRAM.

L'avantage : c'est transparent pour l'application. Votre code CUDA n'a pas besoin de gérer le placement des données. L'inconvénient : les défauts de page coûtent cher, et les heuristiques de migration du runtime ne correspondent pas toujours au schéma d'accès de l'application. Pour des charges prévisibles comme l'inférence de réseaux de neurones, une gestion explicite fait mieux que la migration automatique.

Déport couche par couche

Des outils comme Hugging Face Accelerate, DeepSpeed ZeRO-Inference et le flag --mmap de llama.cpp implémentent un déport explicite couche par couche. Ils ne gardent en VRAM que les couches actives et streament le reste depuis la RAM système ou le disque. Le modèle n'a pas besoin de tenir entièrement en VRAM : il lui suffit de tenir une ou deux couches à la fois.

C'est ainsi que faire tourner des modèles de 70B sur du matériel grand public fonctionne réellement. Un modèle de 70B quantifié en 4 bits nécessite environ 40 Go au total, mais une couche isolée n'en demande qu'environ 1 à 2 Go. Avec 24 Go de VRAM et du préchargement, vous pouvez faire tourner le modèle avec un impact modeste sur les performances : peut-être 30 à 50 % plus lent que s'il tenait entièrement en VRAM.

Le NVMe comme mémoire étendue

L'approche la plus agressive utilise les SSD NVMe comme troisième niveau de mémoire GPU. La bande passante est médiocre comparée à la VRAM (~7 Go/s contre ~1 000 Go/s), mais la capacité est pratiquement illimitée. Un SSD NVMe de 4 To coûte 200 $ et peut stocker simultanément des dizaines de gros modèles.

Des projets comme Greenboost de NVIDIA implémentent cela de manière transparente : le système de mémoire du GPU s'étend jusqu'au NVMe, avec un préchargement intelligent pour minimiser l'impact de la limite de bande passante. Pour les charges d'inférence où le GPU passe beaucoup de temps à calculer (et pas seulement à déplacer des données), la latence du NVMe peut être entièrement masquée par le recouvrement avec le calcul.

Les performances dépendent fortement de la charge. Les opérations limitées par le calcul (grandes multiplications de matrices) masquent bien la latence des transferts. Les opérations limitées par la mémoire (mécanismes d'attention avec un long contexte) ne le font pas. En pratique, le déport sur NVMe fonctionne mieux pour l'inférence par lots de gros modèles avec de petites tailles de batch, c'est-à-dire exactement le cas d'usage de l'inférence LLM locale.

L'avantage de la mémoire unifiée d'Apple

L'architecture de mémoire unifiée des Apple Silicon adopte une approche totalement différente : supprimer la distinction entre VRAM et RAM. Le CPU et le GPU partagent le même pool de mémoire physique. Il n'y a pas de « déport » parce qu'il n'y a pas de séparation : le GPU accède à la même mémoire que le CPU.

Cela ne règle pas le problème de bande passante : celle de la mémoire des puces M (~400 Go/s pour la M4 Max) est inférieure à celle d'un GPU dédié. Mais cela élimine le goulot d'étranglement du PCIe qui ralentit le déport vers un GPU discret. Un Mac avec 128 Go de mémoire unifiée peut charger un modèle de 70B entièrement dans la mémoire accessible au GPU, sans aucune surcharge de déport.

Le compromis : un débit de pointe plus faible pour les charges qui tiennent entièrement dans la VRAM d'un GPU dédié, mais des performances bien meilleures pour celles qui n'y tiennent pas. Pour l'inférence de grands modèles, lorsque le modèle dépasse la VRAM du GPU discret, un Apple Silicon est souvent plus rapide qu'un GPU discret avec déport, malgré une puissance de calcul brute moindre.

Ce que cela change pour les développeurs

Si vous construisez des applications qui utilisent les GPU (inférence ML, graphismes, calcul scientifique), la contrainte de VRAM influence concrètement vos choix d'architecture.

  • Connaissez la taille de votre jeu de travail. Profilez l'utilisation mémoire de votre GPU. Non pas l'allocation maximale, mais le jeu de travail à un instant donné. Si votre pic est de 48 Go mais qu'aucune opération ne nécessite plus de 8 Go de données actives, le déport fonctionnera bien. Si une seule opération a réellement besoin de 48 Go simultanément, il vous faut un GPU plus gros.
  • Choisissez votre stratégie de déport selon le schéma d'accès. Un accès séquentiel (inférence couche par couche) fonctionne très bien avec le préchargement. Un accès aléatoire (attention sur de longs contextes) non. Identifiez le schéma suivi par votre charge.
  • La quantification coûte généralement moins cher que le déport. Passer votre modèle de FP16 à INT4 divise la mémoire par 4 avec un impact modeste sur la qualité. Le déport vers la RAM système ajoute de la latence sans aucun impact sur la qualité, mais avec des économies de mémoire limitées. Commencez par la quantification, puis passez au déport.
  • La taille du batch est votre levier de réglage. Des batchs plus grands demandent plus de mémoire mais amortissent mieux les coûts fixes. Des batchs plus petits consomment moins de mémoire mais traitent moins d'éléments par seconde. Quand vous approchez de la limite de VRAM, réduire la taille du batch est la solution la plus simple.
  • Surveillez la fragmentation mémoire. L'allocation mémoire CUDA peut fragmenter la VRAM avec le temps, surtout avec des entrées de longueur variable. Vous pouvez avoir 8 Go libres au total sans aucun bloc contigu de 2 Go. torch.cuda.memory_stats() de PyTorch permet de la diagnostiquer. torch.cuda.empty_cache() peut aider, sans être une solution miracle pour autant.

La mémoire GPU restera toujours le goulot d'étranglement des calculs à grande échelle. Les modèles grossissent plus vite que la VRAM. Mais les outils pour gérer ce goulot (mémoire unifiée, déport intelligent, cache multi-niveaux) deviennent suffisamment bons pour que « ça ne tient pas en VRAM » ne soit plus une barrière infranchissable. C'est un compromis de performances, et de plus en plus un compromis gérable.