« Mon CPU ne peut pas alimenter mon GPU » : vérité vs mème

Cet article vous a aidé ?

Vous avez acheté le gros GPU. Le tableau de bord affiche toujours 20–40 % d’utilisation. L’entraînement est lent, l’inférence saccade, des images sont perdues, et quelqu’un dans le chat lance la phrase : « Votre CPU ne peut pas alimenter votre GPU. »

Parfois c’est vrai. Parfois c’est du culte cargo. Et parfois le véritable coupable est le stockage, le PCIe, ou une petite synchronisation que vous ne saviez pas avoir écrite. La solution dépend du type de « nourriture » dont on parle : soumission de travail, livraison des données, ou maintien d’assez de parallélisme pour occuper le GPU.

Ce que signifie réellement « alimenter le GPU »

« Alimenter le GPU » est une expression floue qui regroupe trois pipelines distincts sous un même ressenti :

  1. Soumission de travail : l’hôte (CPU) lance des kernels, prépare des buffers de commandes, planifie des CUDA Graphs, place des copies en file, et synchronise les streams. Si l’hôte ne lance pas assez vite, le GPU reste inactif entre les kernels.
  2. Livraison des données : l’hôte récupère les données depuis le stockage, les décode/prétraite et les transfère via PCIe/NVLink dans la mémoire du GPU. Si c’est lent, le GPU attend le lot suivant.
  3. Disponibilité de travail parallèle : même si la soumission et la livraison sont parfaites, le GPU a besoin d’assez de travail indépendant pour remplir les SMs. Des lots trop petits, des kernels trop petits, ou trop de dépendances sérielles peuvent maintenir une faible utilisation sans aucun problème côté CPU.

Alors quand quelqu’un dit « le CPU ne peut pas alimenter le GPU », exigez une clarification : quelle alimentation ? S’ils ne peuvent pas répondre, vous venez de trouver le premier goulot : la qualité du diagnostic.

Une citation à garder en poche (idée paraphrasée) de Donald Knuth : L’optimisation prématurée est une façon courante de perdre du temps ; mesurez d’abord, puis optimisez ce qui compte.

Et oui, je vais être pénible sur la mesure. Les systèmes de production ne vivent pas de la confiance.

Vérité vs mème : quand le CPU est vraiment le goulot

Les cas « vrais » (le CPU limite réellement le débit GPU)

Ceux-ci sont ennuyeusement réels :

  • Le coût de lancement des kernels domine. Vous lancez des milliers de petits kernels par étape. Le thread CPU passe sa vie dans des appels au pilote, et le GPU passe la sienne à attendre le lancement suivant.
  • Pipeline d’entrée mono-thread. Le décodage des données, l’augmentation, la tokenisation ou l’ingénierie des features tourne sur un seul cœur parce que quelqu’un a mis workers=0 « pour la déterminisme ». Le GPU attend le lot suivant comme s’il était bloqué derrière une caissière lente.
  • Synchronisations excessives. Des équivalents cachés de cudaDeviceSynchronize() (directement ou indirectement) sérialisent le pipeline. Le CPU bloque, puis le GPU bloque, et vous accusez le CPU d’être « trop faible ».
  • Prétraitement limité par le CPU. Pensez au décodage JPEG, décodage vidéo sans accélération matérielle, parsing JSON, ou décompression. Les GPU sont rapides ; votre CPU reste soumis à la physique et à la prédiction de branchement.
  • Starvation NUMA et bande passante mémoire. Le CPU a « beaucoup de cœurs », mais ils tirent tous les données à travers une frontière de socket parce que votre processus et votre GPU sont sur des nœuds NUMA différents.
  • Surcharge pilote/firmware et interruptions. Particulièrement sur des serveurs multi-GPU avec beaucoup d’E/S. Le CPU n’est pas « faible », il joue le rôle d’ordonnanceur d’E/S et d’épongueur d’interruptions.

Les cas « mème » (le CPU n’est pas le facteur limitant)

C’est là que les gens perdent des semaines :

  • L’utilisation GPU est faible parce que le GPU fait des rafales courtes. L’utilisation moyenne masque des micro-interruptions ; le GPU attend en réalité la mémoire à l’intérieur du kernel, pas le CPU.
  • Vous êtes limité par le PCIe. Les copies saturent le PCIe. Changer de CPU n’élargira pas magiquement un lien PCIe Gen3 x8.
  • Vous êtes limité par la VRAM. Vous réduisez la taille de lot pour tenir en mémoire, ce qui diminue l’intensité arithmétique et donne l’impression que le GPU est « mal nourri ». Ce n’est pas le CPU ; c’est la taille du working set.
  • Vos kernels sont inefficaces. Faible occupancy, mauvais coalescing mémoire, branches divergentes. Le GPU est « occupé » à être inefficace, pas à attendre le CPU.
  • Votre travail est lié à la latence (comme l’inférence petit lot). L’utilisation GPU peut ne jamais être élevée parce que la charge n’a pas assez de parallélisme. « 100 % GPU » n’est pas une loi de la nature.

Blague #1 : Le GPU n’est pas « affamé », il est difficile — si vous lui servez un crouton à la fois, il vous regardera comme si vous étiez le problème.

Faits intéressants et un peu d’histoire (parce que ça explique les douleurs d’aujourd’hui)

  • Fait 1 : Les premiers modèles de programmation GPU (avant CUDA) étaient essentiellement des API graphiques déguisées ; « alimenter le GPU » signifiait littéralement garder le pipeline graphique plein de triangles. Les kernels compute d’aujourd’hui ont hérité de la même mentalité de débit.
  • Fait 2 : Le modèle de lancement de CUDA a facilité la mise en file des kernels, mais les bonnes pratiques initiales encourageaient beaucoup de petits kernels ; les recommandations modernes préconisent souvent la fusion et les CUDA Graphs pour réduire le coût de lancement.
  • Fait 3 : Le PCIe s’est amélioré régulièrement, mais pas au même rythme que les FLOPS GPU. L’écart explique pourquoi les transferts hôte→device restent un goulot fréquent même dans des serveurs « monstres ».
  • Fait 4 : NUMA est devenu un point de douleur courant quand les serveurs double-socket ont dominé les datacenters ; l’affinité GPU et le placement sur le CPU « le plus proche » comptent parce que la latence mémoire entre sockets n’est pas négligeable.
  • Fait 5 : La mémoire épinglée (page-locked) est plus rapide pour les transferts DMA, mais trop de mémoire épinglée peut nuire à l’OS et aux autres processus en réduisant la flexibilité de la RAM paginable.
  • Fait 6 : NVLink existe en grande partie parce que le PCIe ne suffisait pas pour les charges multi-GPU ; mais il ne corrige pas le prétraitement côté CPU, le coût de lancement des kernels, ou l’ingestion depuis le stockage.
  • Fait 7 : Les compteurs d’« utilisation GPU » ont été initialement conçus pour le graphique et pour des kernels de longue durée. Les interpréter pour l’ML training avec mélange copie/compute peut être trompeur sans vue temporelle.
  • Fait 8 : L’essor du ML centré sur les données a fait des pipelines d’entrée (décodage, augmentation, tokenisation) des problèmes de performance de première classe ; votre « job d’entraînement » se comporte souvent comme un travail ETL avec un GPU attaché.

Quatre types de goulots que vous confondez sans cesse

1) Goulot de soumission CPU (launch-bound)

Symptômes : le GPU montre des creux entre les kernels, beaucoup de petits kernels, le thread CPU est bloqué en temps système ou dans des appels pilote, le temps de step varie avec le « nombre de kernels », pas avec la taille du lot.

Corrections typiques : fusionner les kernels, augmenter la taille du lot, utiliser CUDA Graphs, réduire la charge Python, éviter les appels device par échantillon, réduire les synchronisations, utiliser des kernels persistants si approprié.

2) Goulot de prétraitement CPU (décodage/augmentation/tokenisation)

Symptômes : cœurs CPU saturés en espace utilisateur, lectures disque/réseau correctes, le GPU attend l’entrée, augmenter le nombre de workers aide jusqu’à atteindre la contention.

Corrections typiques : paralléliser le prétraitement, vectoriser, mettre en cache les données décodées, déplacer des transformations vers le GPU, utiliser des codecs plus rapides, réduire le coût des augmentations, utiliser des lots plus grands pour amortir le surcoût.

3) Goulot I/O et stockage (votre « serveur GPU » est en réalité un client de stockage)

Symptômes : iowait élevé, latences de lecture longues, débit incohérent, utilisation GPU bruyante, amélioration lorsque le dataset est sur NVMe local ou en cache.

Corrections typiques : cache local, prefetch, lectures séquentielles plus grosses, meilleurs formats de fichiers, éviter les petites lectures aléatoires, vérifier que le système de fichiers et le réseau ne sont pas bridés.

4) Goulot côté GPU (il est occupé, simplement pas dans le bon sens)

Symptômes : l’utilisation GPU peut être élevée ou faible, mais le profiling montre des stalls mémoire, faible occupancy, tensor cores inactifs, ou mauvaise efficacité des kernels. Le CPU est majoritairement inactif.

Corrections typiques : optimisation des kernels, meilleures bibliothèques, mixed precision, changements de layout, opérations fusionnées plus larges, corriger l’accès mémoire non coalescé, s’assurer d’utiliser le backend adapté.

Mode d’emploi pour un diagnostic rapide (premier/deuxième/troisième)

Premier : établir si le GPU attend ou travaille

  • Vérifiez l’utilisation GPU, la puissance, les fréquences, l’usage mémoire, et—critique—si l’utilisation est en rafales.
  • Regardez si les moteurs de copie sont actifs vs l’activité compute (H2D/D2H vs activité SM).
  • Si le GPU est vraiment inactif beaucoup, il attend quelque chose en amont (soumission CPU, prétraitement, I/O, synchronisation).

Second : séparer la soumission CPU du prétraitement CPU

  • Si un thread CPU est chaud et que le temps système est élevé : suspectez l’overhead de lancement ou la synchronisation.
  • Si beaucoup de cœurs CPU sont chauds en temps utilisateur : suspectez le prétraitement ou la décompression/décodage/tokenisation.
  • Si le CPU est majoritairement inactif mais le GPU sous-utilisé : suspectez une inefficacité côté GPU ou une charge trop petite.

Troisième : valider le chemin de transport (PCIe/NUMA) et le chemin stockage

  • Confirmez la largeur/vitesse du lien PCIe. « x16 » n’est pas une impression ; c’est un état négocié.
  • Vérifiez la localité NUMA : CPU, mémoire et GPU doivent être alignés lorsque possible.
  • Vérifiez la latence de stockage et les tailles de lecture. Des lectures aléatoires 4KB depuis un filesystem réseau humilieront votre H100.

Blague #2 : Acheter un CPU plus rapide pour corriger un goulot PCIe, c’est comme acheter une caissière plus rapide parce que le camion de livraison est coincé dans les embouteillages.

Tâches pratiques : commandes, sorties et décisions

Voici les étapes « arrêtez de discuter, commencez à vérifier ». Chaque tâche inclut une commande, une sortie d’exemple, ce que cela signifie, et la décision à prendre.

Task 1: Check live GPU utilization, clocks, and power

cr0x@server:~$ nvidia-smi dmon -s pucvmt
# gpu    pwr gtemp mtemp  sm   mem   enc   dec   mclk   pclk
# Idx      W     C     C   %     %     %     %    MHz    MHz
    0     92    64     -  28     18     0     0   5001   1410
    0     88    63     -  31     20     0     0   5001   1410
    0     55    60     -   4      6     0     0   5001    705

Ce que cela signifie : SM% oscillant de ~30 % à ~4 % avec des chutes de fréquence indique un travail en rafales ou des stalls. Les baisses de puissance/fréquence signifient souvent que le GPU est suffisamment inactif pour réduire sa fréquence.

Décision : Si les rafales correspondent aux frontières de batch, cherchez en amont (entrée, sync). Si SM% est stable mais bas, examinez l’efficacité des kernels ou la taille des lots.

Task 2: See per-process GPU usage (are you even looking at the right job?)

cr0x@server:~$ nvidia-smi pmon -s um
# gpu        pid  type    sm   mem   enc   dec   command
    0      27431     C     9    12     0     0   python
    0      29902     G     0     1     0     0   Xorg

Ce que cela signifie : Votre processus Python utilise à peine les SM. La mémoire est allouée, le calcul ne l’est pas.

Décision : Profilez l’entrée et la synchronisation. N’assumez pas que « VRAM allouée » signifie « GPU occupé ».

Task 3: Check PCIe link speed and width

cr0x@server:~$ nvidia-smi -q | sed -n '/PCI/,/Replay/p'
    PCI
        Bus                             : 00000000:81:00.0
        Link Width                      : 8x
        Link Speed                      : 8.0 GT/s
        Replay Counter                  : 0

Ce que cela signifie : C’est du PCIe Gen3 x8. Si vous attendiez Gen4 x16, vous venez de trouver une classe de goulot.

Décision : Corrigez les réglages BIOS, le placement du slot, les risers, ou l’incompatibilité platforme avant de réécrire le code.

Task 4: Validate negotiated PCIe state via lspci

cr0x@server:~$ sudo lspci -s 81:00.0 -vv | egrep -i 'LnkCap|LnkSta'
LnkCap: Port #0, Speed 16GT/s, Width x16
LnkSta: Speed 8GT/s (downgraded), Width x8 (downgraded)

Ce que cela signifie : La carte peut faire Gen4 x16 mais est actuellement rétrogradée. Cela peut arriver à cause du câblage du slot, d’un BIOS forcé, ou d’un riser défectueux.

Décision : Considérez cela comme un problème matériel/plateforme. Aucune quantité de « workers dataloader » ne réparera un lien rétrogradé.

Task 5: Check GPU topology and NUMA affinity

cr0x@server:~$ nvidia-smi topo -m
        GPU0    CPU Affinity    NUMA Affinity
GPU0     X      0-15           0

Ce que cela signifie : GPU0 est le plus proche des cœurs CPU 0–15 sur le nœud NUMA 0.

Décision : Pincez votre processus et les allocations mémoire sur ces cœurs/nœud si vous effectuez un prétraitement CPU intensif ou des transferts H2D importants.

Task 6: Check CPU saturation and run queue pressure

cr0x@server:~$ mpstat -P ALL 1 3
Linux 6.5.0 (server)  01/10/2026  _x86_64_  (32 CPU)

12:40:20 PM  CPU   %usr %nice %sys %iowait %irq %soft %idle
12:40:21 PM  all   210.0 0.00 35.0  0.50    0.0  1.2   753.3
12:40:21 PM    7    98.0 0.00  2.0  0.00    0.0  0.0     0.0
12:40:21 PM    8     4.0 0.00 60.0  0.00    0.0  0.0    36.0

Ce que cela signifie : Un cœur (CPU 7) est saturé en temps utilisateur tandis que le système global est majoritairement inactif. Le CPU 8 montre un temps système élevé (travail pilote/kernel). Cela sent le goulot mono-thread (GIL Python, thread de lancement, ou sync).

Décision : Optimisez le chemin du thread hôte : réduisez l’overhead Python par étape, utilisez le batching, CUDA Graphs, évitez les appels sync fréquents.

Task 7: Catch I/O wait and storage latency hints

cr0x@server:~$ iostat -xz 1 3
avg-cpu:  %user %nice %system %iowait  %steal %idle
          18.2   0.0    6.1     22.9     0.0  52.8

Device            r/s   rkB/s  rrqm/s  %util  await
nvme0n1          85.0  4200.0    0.0   78.0   9.8

Ce que cela signifie : iowait est élevé et l’await NVMe est ~10ms sous charge. Pour un pipeline d’entrée effectuant beaucoup de petites lectures, c’est douloureux.

Décision : Augmentez la taille de lecture, prefetch, regroupez les données en shards plus larges, mettez en cache localement, ou déplacez le dataset hors d’un stockage en contention.

Task 8: Confirm the dataset access pattern (small random reads vs streaming)

cr0x@server:~$ sudo strace -f -e trace=openat,read -p 27431 -s 80 -tt 2>&1 | head -n 8
12:41:10.102334 openat(AT_FDCWD, "/data/ds/img_000812.jpg", O_RDONLY) = 57
12:41:10.102801 read(57, "\377\330\377\340\0\20JFIF\0\1\1\0\0\1\0\1\0\0", 4096) = 4096
12:41:10.103122 read(57, "...", 4096) = 4096
12:41:10.103444 openat(AT_FDCWD, "/data/ds/img_000813.jpg", O_RDONLY) = 58

Ce que cela signifie : Beaucoup de petites lectures 4KB à travers de nombreux fichiers. C’est le classique « ça marche bien sur mon portable » qui s’effondre à grande échelle.

Décision : Consolidez les fichiers (tar/shards), utilisez des lectures séquentielles, adoptez des schémas favorables au cache de pages, et préchargez les batches.

Task 9: Check CPU frequency scaling (the quiet throughput killer)

cr0x@server:~$ cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
powersave

Ce que cela signifie : Le CPU est autorisé à réduire fortement sa fréquence. Pour un prétraitement en rafales, cela peut augmenter la latence tail et affamer le GPU entre les lots.

Décision : Sur des nœuds dédiés à l’entraînement, utilisez le gouverneur performance ou un tuning adapté à la plateforme, puis mesurez à nouveau.

Task 10: Spot NUMA misplacement (process running “far” from the GPU)

cr0x@server:~$ numactl --show
policy: default
preferred node: current
physcpubind: 16 17 18 19 20 21 22 23
membind: 1

Ce que cela signifie : Votre processus est épinglé au nœud NUMA 1, mais la topologie plus haut montrait que GPU0 préfère le nœud NUMA 0. Cela provoque du trafic cross-socket pour chaque buffer DMA et sortie de prétraitement.

Décision : Ré-épingler vers le nœud NUMA local au GPU (ou déplacer le job vers un GPU attaché au nœud 1). C’est souvent un correctif à deux chiffres en pourcentage.

Task 11: Check for throttling and thermal constraints

cr0x@server:~$ nvidia-smi -q | sed -n '/Clocks/,/Applications Clocks/p'
    Clocks
        Graphics                        : 705 MHz
        SM                              : 705 MHz
        Memory                          : 5001 MHz
    Applications Clocks
        Graphics                        : 1410 MHz
        Memory                          : 5001 MHz

Ce que cela signifie : La fréquence SM actuelle est bien inférieure à la fréquence applicative. Si cela persiste sous charge, vous pouvez être power/thermal throttled ou simplement idle.

Décision : Si l’utilisation est élevée mais que les fréquences sont basses, investiguez les limites de puissance, le refroidissement et le flux d’air du châssis. Si l’utilisation est faible, il est probable que le GPU soit inactif et se downclocke normalement.

Task 12: Identify a kernel-launch-heavy pattern (lots of tiny GPU work)

cr0x@server:~$ sudo perf top -p 27431 -g --stdio
Samples: 3K of event 'cycles'
  18.40%  libcuda.so.1        [.] cuLaunchKernel
  11.22%  libc.so.6           [.] memcpy
   9.87%  libpthread.so.0     [.] pthread_mutex_lock
   6.31%  python3.10          [.] _PyEval_EvalFrameDefault

Ce que cela signifie : Le processus passe beaucoup de cycles à lancer des kernels et à exécuter des frames de l’interpréteur Python. C’est de l’« overhead de soumission ».

Décision : Fusionnez les ops, utilisez des graphs compilés, réduisez les appels côté Python par op, et envisagez des kernels/batches plus grands.

Task 13: Check network filesystem impact (if your dataset is remote)

cr0x@server:~$ nfsstat -c
Client rpc stats:
calls      retrans    authrefrsh
248391     1203       0

Client nfs v4:
ops         count
read        182744
open        50322
getattr     411802

Ce que cela signifie : Il y a des retransmissions, et le volume d’open/getattr est énorme. Le bavardage metadata plus les lectures distantes peuvent absolument affamer un GPU.

Décision : Stagez sur NVMe local, diminuez le nombre de fichiers, augmentez le caching client quand c’est sûr, ou changez le format du dataset.

Task 14: Verify huge pages / pinned memory pressure signals (host memory path)

cr0x@server:~$ grep -E 'MemAvailable|Dirty|Writeback' /proc/meminfo
MemAvailable:   1842332 kB
Dirty:           482912 kB
Writeback:        12984 kB

Ce que cela signifie : Faible MemAvailable et Dirty élevé suggèrent une pression mémoire et du writeback. Cela peut ralentir le prétraitement et provoquer des latences bruitées, même si le GPU semble « ok ».

Décision : Réduisez l’usage de mémoire épinglée, évitez le caching excessif dans l’application, ou fournissez plus de RAM / isolez les voisins bruyants.

Trois mini-histoires en entreprise (comment les équipes se plantent réellement)

Mini-histoire 1 : L’incident causé par une mauvaise hypothèse

Ils avaient un nouveau cluster GPU. Tout le monde était excité, et la checklist de lancement contenait les éléments habituels : versions pilotes, runtime CUDA, checks de santé, et un rapide entraînement test. L’utilisation GPU était faible, donc la conclusion est arrivée vite : « Ces CPUs sont trop petits. On a fait l’économie. »

L’équipe a escaladé. Le service achat s’est retrouvé entraîné dans un appel d’ingénierie. Quelqu’un a proposé de remplacer toute la SKU du nœud. Une semaine plus tard, un SRE a posé une question pénible : « Quel état PCIe avons-nous réellement négocié ? »

Il s’est avéré que les nœuds étaient câblés via une configuration de riser qui avait silencieusement négocié le PCIe à une largeur et génération inférieures à celles attendues. Les GPUs allaient bien. Les CPUs aussi. Le chemin entre eux non. Les transferts H2D étaient plafonnés, et la boucle d’entraînement stagnait à chaque copie de batch.

Une fois le placement des slots et les réglages BIOS corrigés, l’utilisation a bondi sans toucher une ligne de code. Le postmortem ne parlait pas du PCIe. Il parlait d’hypothèses : ils avaient traité « le CPU ne peut pas alimenter le GPU » comme une explication au lieu d’une hypothèse.

Mini-histoire 2 : L’optimisation qui a mal tourné

Un groupe d’ingénierie données voulait « mieux nourrir le GPU », ils ont donc augmenté agressivement les workers du dataloader et activé la mémoire épinglée partout. Le débit s’est amélioré sur un nœud de test tranquille. Ils ont déployé la config sur la flotte d’entraînement partagée.

En un jour, les jobs d’entraînement ont commencé à échouer de façon imprévisible. Certains allaient vite. D’autres se bloquaient. Certains étaient tués par l’OOM killer même si la mémoire GPU était stable. La rotation d’astreinte a été pénible, principalement parce que les graphes donnaient l’air d’une « flakiness infrastructurelle aléatoire ».

La cause racine était banale : mémoire épinglée plus beaucoup de workers créaient une pression significative sur la RAM hôte et l’allocateur de pages. Sur des nœuds avec d’autres services colocés et des besoins de cache filesystem, l’« optimisation » s’est transformée en contention mémoire et pics de latence. Les workers ont aussi saturé le filesystem réseau avec plus de lectures concurrentes, augmentant la latence tail pour tous.

La correction n’était pas d’annuler le parallélisme ; c’était de le dimensionner correctement. Ils ont plafonné le nombre de workers par nœud, staged les datasets localement pour les jobs à haut débit, et n’ont utilisé la mémoire épinglée que là où le temps de transfert était réellement critique. Nourrir le GPU n’est pas une permission pour affamer l’OS.

Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une autre équipe gérait une plateforme d’inférence multi-tenant. Ils avaient une règle stricte : chaque incident de performance commence par une capture reproductible des métriques hôtes, des métriques GPU, et d’une courte trace de profil. Aucune exception, pas de « je suis presque sûr ».

Un vendredi, des alarmes latence ont sonné. L’utilisation GPU était faible, et la solution facile fut « le CPU est saturé ». Mais leur runbook imposait un contrôle rapide des queues CPU, des fréquences GPU, de l’état PCIe, et de la latence de stockage pour le cache de modèles. Le CPU n’était pas saturé. Le GPU n’attendait pas de lancements. Le PCIe était sain.

La trace montrait que le processus d’inférence bloquait sur des lectures de fichiers pour des shards de modèle après un déploiement. Un changement apparemment inoffensif avait déplacé le répertoire de cache modèle du NVMe local vers un mount réseau. Personne ne pensait que ça comptait parce que « le modèle tient en RAM », sauf que ce n’était pas toujours vrai sous churn de cache de pages.

Ils ont restauré le chemin de cache, réchauffé le cache délibérément, et l’incident s’est terminé sans débogage héroïque. La pratique qui les a sauvés n’était pas un profileur sophistiqué. C’était une checklist qui les a empêchés de courir après le mauvais scénario de goulot.

Erreurs courantes : symptôme → cause racine → correction

1) Symptom: GPU utilization low, VRAM high

Cause racine : Vous avez alloué des tenseurs sur le GPU, mais le calcul est minime ou bloqué par des synchronisations / attentes d’entrée.

Correction : Profilez pour détecter les gaps ; augmentez la taille du lot ; supprimez les sync par étape ; vérifiez le débit du dataloader ; recherchez les hotspots de prétraitement CPU.

2) Symptom: Utilization spiky, step time inconsistent

Cause racine : Jitter du pipeline d’entrée (latence stockage, filesystem distant, pauses GC, scaling de fréquence CPU).

Correction : Stagez les données localement ; shardez les fichiers ; prefetch ; mettez le gouverneur CPU en performance ; limitez le nombre de workers pour éviter le thrash.

3) Symptom: One CPU core pegged, others idle, GPU not busy

Cause racine : Lancement mono-thread ou overhead Python ; contention GIL ; trop de petits kernels ; appels fréquents au pilote.

Correction : Fusionnez les ops ; utilisez CUDA Graphs ; batcher le travail ; déplacez la logique hors de la boucle chaude Python ; réduisez les opérations device par échantillon.

4) Symptom: Copy engines busy, SM low

Cause racine : Bottleneck de transfert (PCIe saturé, copies depuis mémoire pageable, petites copies, mauvais état du lien).

Correction : Vérifiez Gen/width ; utilisez la mémoire épinglée judicieusement ; augmentez la taille du lot ; recouvrez copies et calcul ; réduisez le chitchat hôte-device.

5) Symptom: GPU clocks low under load

Cause racine : Limite de puissance, throttling thermique, ou le GPU est en réalité assez idle pour downclocker.

Correction : Vérifiez les limites de puissance, le refroidissement, le flux d’air du châssis ; si le GPU est idle, remontez en amont et trouvez l’attente.

6) Symptom: Performance got worse after “more workers”

Cause racine : Contention (tempêtes de metadata filesystem, pression RAM, coût de context switch, thrash de cache).

Correction : Dimensionnez le nombre de workers correctement ; utilisez des shards plus grands ; mettez en cache ; réduisez les transformations ; mesurez le débit end-to-end, pas seulement la vitesse du loader.

7) Symptom: Two identical servers differ massively

Cause racine : Différences d’état PCIe négocié, placement NUMA, réglages BIOS power, daemons en arrière-plan, ou chemins de stockage différents.

Correction : Comparez LnkSta PCIe, gouverneur CPU, bindings NUMA, et options de montage de stockage. Standardisez l’image nœud et le profil BIOS.

8) Symptom: GPU underutilized only at small batch sizes

Cause racine : La charge manque de parallélisme ; l’overhead de lancement et la latence mémoire dominent pour les petits lots.

Correction : Augmentez la taille du lot, utilisez du batching/queueing, ou acceptez une faible utilisation comme compromis latence. Ne poursuivez pas 100 % d’utilisation pour un SLA p99.

Listes de contrôle / plan pas-à-pas

Checklist A: Prove or disprove “CPU can’t feed GPU” in 20 minutes

  1. Observer le comportement GPU : utilisation, fréquences, puissance, mémoire, rafales.
  2. Vérifier l’état PCIe : gen/width négociés, erreurs, topologie.
  3. Vérifier la forme CPU : cœur unique saturé vs beaucoup de cœurs vs idle ; pression sur la run queue.
  4. Vérifier iowait et le chemin dataset : local vs réseau ; lectures aléatoires vs séquentielles.
  5. Confirmer la localité NUMA : affinité CPU du processus et binding mémoire vs attachement GPU.
  6. Prendre un court profil : identifiez si le temps est en lancements de kernels, memcpy, décodage, ou attente.

Checklist B: If it’s CPU submission (launch-bound)

  1. Réduire le nombre de kernels : fusionner opérations, réduire les boucles Python.
  2. Augmenter le travail par lancement : plus grands lots, tiles plus grands, moins de micro-kernels.
  3. Supprimer les points de synchronisation : éviter les synchronisations forcées device dans le chemin chaud.
  4. Envisager CUDA Graphs ou un chemin d’exécution compilé si votre framework le supporte.
  5. Retester avec le même dataset et une graine aléatoire fixe pour éviter de courir après du bruit.

Checklist C: If it’s CPU preprocessing (decode/augment/tokenize)

  1. Mesurer le timing par étape (read, decode, transform, batch, copy).
  2. Paralléliser prudemment : ajouter des workers jusqu’à la contention, puis s’arrêter.
  3. Préférer les opérations vectorisées et les bibliothèques utilisant SIMD efficacement.
  4. Mettre en cache les transformations coûteuses quand elles sont reproductibles.
  5. Déplacer des transformations vers le GPU si cela réduit le coût CPU plus que cela n’augmente le temps GPU.

Checklist D: If it’s I/O

  1. Stagez les datasets localement pour les runs à haut débit.
  2. Regroupez beaucoup de petits fichiers en shards ; évitez les open par échantillon.
  3. Prefetch et lisez séquentiellement ; augmentez la taille des requêtes.
  4. Surveillez les opérations metadata sur les filesystems réseau.
  5. Vérifiez que le stockage n’est pas partagé et saturé par d’autres jobs.

Checklist E: If it’s GPU inefficiency

  1. Utilisez un profileur timeline pour voir les stalls (mémoire vs calcul vs sync).
  2. Vérifiez que vous atteignez les bons kernels (tensor cores, bibliothèques optimisées).
  3. Ajustez taille de lot et précision pour augmenter l’intensité arithmétique.
  4. Corrigez le layout et les schémas d’accès mémoire ; évitez les petits kernels.
  5. Arrêtez de blâmer le CPU quand c’est le GPU qui fait un mauvais travail.

FAQ

1) Is low GPU utilization always bad?

Non. Pour l’inférence sensible à la latence avec de petits lots, une faible utilisation peut être attendue. Optimisez pour p95/p99 de latence, pas pour rendre un graphique d’utilisation impressionnant.

2) What’s a quick sign it’s a dataloader bottleneck?

L’utilisation GPU chute aux frontières de batch, des cœurs CPU montent en charge en temps utilisateur, et le débit s’améliore quand vous mettez les données en cache localement ou augmentez les workers (jusqu’à la contention). Aussi : des temps de step en rafales.

3) How do I tell PCIe bottleneck vs CPU bottleneck?

Si les moteurs de copie sont occupés et que les SM sont faibles, suspectez PCIe/transfert. Validez la largeur/vitesse négociée du lien. Si le CPU est chaud dans les appels pilotes et que vous voyez beaucoup de petits kernels, suspectez l’overhead de soumission.

4) Why does “more dataloader workers” sometimes slow things down?

Parce que la concurrence crée de la contention : tempêtes de metadata filesystem, thrash de cache, pression RAM (surtout avec mémoire épinglée), et overhead de context switching. Le débit a un pic ; trouvez-le.

5) Does pinned memory always help?

Ça aide pour les transferts DMA, mais ce n’est pas gratuit. Trop de mémoire épinglée réduit la flexibilité de l’OS et peut augmenter l’instabilité sous charge multi-tenant. Utilisez-la quand H2D est réellement sur le chemin critique.

6) Can the CPU “feed” the GPU on a single thread?

Parfois. Pour de grands kernels et beaucoup de calcul, un seul thread hôte peut suffire. Pour des charges avec beaucoup de petits kernels, de nombreux lancements, ou des appels device par échantillon, un thread devient le goulot.

7) Why do two “identical” nodes perform differently?

Parce qu’elles ne sont pas identiques sur les points qui comptent : état PCIe négocié, localité NUMA, réglages BIOS power, I/O en arrière-plan, ou chemins de stockage diffèrent. Mesurez cela en premier.

8) What’s the most common misunderstanding behind “CPU can’t feed GPU”?

Les gens traitent « utilisation GPU » comme une métrique unique et absolue. C’est une moyenne d’une timeline complexe. Vous devez savoir si le GPU est idle, en copie, en stall mémoire, ou simplement en train d’exécuter des rafales.

9) Should I upgrade CPU or GPU first if training is slow?

Si vous n’avez pas mesuré, ni l’un ni l’autre. Si le profiling montre que le prétraitement hôte ou l’overhead de lancement domine, un CPU (ou des changements logiciels) aidera. Si les kernels GPU dominent et que vous êtes lié par le calcul, le GPU aidera. Si vous êtes I/O-bound, achetez de la bande passante stockage et de meilleurs formats de données.

Étapes suivantes (faites ça, pas des impressions)

Si vous devez retenir une leçon opérationnelle du mème « le CPU ne peut pas alimenter le GPU », retenez ceci : l’expression n’est pas un diagnostic. C’est une invitation à instrumenter le pipeline.

  1. Exécutez le mode d’emploi de diagnostic rapide et classez le goulot : soumission, prétraitement, I/O, ou inefficacité GPU.
  2. Validez la vérité physique : état du lien PCIe, affinité NUMA, fréquences et throttling. Corrigez la plateforme avant de toucher au code.
  3. Choisissez une métrique qui représente la valeur utilisateur (samples/sec, p99 latency, coût par batch) et optimisez vers elle. Pas vers une belle courbe d’utilisation.
  4. Faites des changements réversibles et mesurables : une variable à la fois, capturée avec la même tranche de dataset et un run reproductible.
  5. Rédigez le runbook que vous auriez voulu avoir. Votre futur vous sera reconnaissant et moins fatigué.

Alimenter le GPU est un problème système. Le CPU n’est qu’un des serveurs en attente.

← Précédent
Limitation de débit Postfix : prévenir les abus sans bloquer les utilisateurs réels
Suivant →
Ubuntu 24.04 : Quand les offloads GRO/LRO/TSO cassent tout — Comment tester et désactiver en toute sécurité

Laisser un commentaire