- Les GPU ont une vitesse de calcul très supérieure à la vitesse d’accès mémoire, si bien que la hiérarchie mémoire devient le principal goulot d’étranglement des performances
- Selon l’intensité arithmétique (Arithmetic Intensity, AI), une opération peut être limitée par la mémoire ou par le calcul ; pour le GPU A100, le point critique est d’environ 13 FLOPs/Byte
- Parmi les principales stratégies d’optimisation des performances, on trouve la fusion d’opérations (Fusion) et le tuilage (Tiling) ; la Fusion réduit les allers-retours mémoire inutiles, et le Tiling maximise la réutilisation des données
- Comprendre les caractéristiques structurelles du matériel GPU, comme la synchronisation, le Coalesced Load et la résolution des conflits de banques, est essentiel pour écrire des kernels haute performance
- D’autres éléments comme l’occupation (Occupancy), la minimisation de la divergence des threads et la quantification (Quantization) ont également un impact important sur les performances réelles
Structure de calcul et hiérarchie mémoire des GPU
- Les GPU ont en général une capacité de traitement arithmétique bien supérieure à leur bande passante mémoire
- Par exemple, le NVIDIA A100 offre environ 19,5 TFLOPS (en virgule flottante 32 bits), tandis que sa bande passante mémoire est d’environ 1,5 TB/s
- Comme il est possible d’effectuer des dizaines d’opérations pendant la lecture de 4 octets de données, le déplacement des données constitue le principal goulot d’étranglement des performances
- La mémoire globale (VRAM) est une mémoire hors puce lente où résident toutes les données, tandis que les Streaming Multiprocessors (SM) prennent en charge le calcul
- Chaque SM dispose d’une Shared Memory (SRAM) rapide sur puce, qui peut être utilisée comme un cache géré directement par le programme
- Le thread est la plus petite unité d’exécution, et chaque thread possède son propre ensemble de registres
- 32 threads forment un Warp, et un Block est une grille de threads exécutée sur un même SM
Régimes de performance : memory-bound vs compute-bound
- Les performances d’un kernel sont soit limitées par la mémoire (vitesse de déplacement des données), soit par le calcul (capacité de calcul des SM)
- L’intensité arithmétique (AI) est définie comme Total FLOPs / Total Bytes Accessed, et constitue un indicateur essentiel
- Le modèle Roofline représente les performances réalisables d’un kernel sur un graphique avec l’AI en abscisse et les FLOPS/s en ordonnée
- Si l’AI est faible et que le kernel est memory-bound, il se situe sur la diagonale (palier de bande passante mémoire)
- Si l’AI est élevée et que le kernel est compute-bound, il se situe sur l’horizontale (palier de performance de calcul maximale)
- Le Ridge Point de l’A100 vaut 19,5 TFLOPS / 1,5 TB/s ≈ 13 FLOPs/Byte
- Augmenter l’AI améliore les performances et peut permettre au kernel d’atteindre le régime compute-bound
Stratégies pour augmenter l’intensité arithmétique
- Modèle simple : un thread calcule une seule valeur C[i,j] → AI = 0,25 (très faible, donc memory-bound)
- Même si un thread calcule une tuile 2x2, AI = 0,5 (toujours faible)
- Pour augmenter l’AI, plusieurs threads doivent charger de grandes tuiles dans la Shared Memory au niveau du block afin de maximiser la réutilisation des données
- Grâce à la coopération des threads dans un block, il est possible de porter l’AI au-delà de 13 et d’entrer en régime compute-bound
État overhead-bound
- Un overhead peut apparaître lorsque le CPU (hôte) assigne des tâches au GPU
- Si les kernels GPU sont trop petits ou trop nombreux, le GPU peut se retrouver à attendre du travail
- Les frameworks modernes introduisent une exécution asynchrone, qui met en file le flux de commandes à l’avance afin de minimiser cet overhead
Deux stratégies clés pour améliorer les performances : Fusion et Tiling
Operator Fusion (fusion d’opérations)
- Dans une chaîne d’opérations simple, par exemple
y = relu(x + 1), si chaque opération s’exécute dans un kernel séparé, les données font des allers-retours vers la mémoire globale
- La Fusion regroupe plusieurs opérations dans un seul kernel, évite de stocker les valeurs intermédiaires en mémoire globale, effectue les calculs dans les registres, puis n’écrit que le résultat final
- Exemples : des compilateurs JIT comme Triton ou
torch.compile Inductor automatisent ce processus
Tiling (tuilage)
- Pour des opérations complexes comme la multiplication de matrices, un modèle à thread unique offre une AI faible
- Après avoir découpé le calcul en tuiles par block, tous les threads du block coopèrent pour charger les tuiles de données dans la Shared Memory, ce qui permet une réutilisation massive des données
- Le calcul suit un schéma en trois étapes : "Load (global -> Shared Memory) - Synchronize (synchronisation) - Compute (calcul)"
Coalesced Load et vectorisation
- Lors du transfert de données de la mémoire globale vers la Shared Memory, le Coalesced Access est essentiel (les 32 threads d’un warp accèdent à une zone contiguë de 128 octets)
- La vectorisation (par exemple
float4) permet de charger plusieurs données à la fois, d’économiser les ressources matérielles et de maximiser l’utilisation de la bande passante mémoire
- L’alignement des données est indispensable, et la valeur K en nombre d’octets dans la matrice doit être un multiple de 4 pour être efficace
Banques de Shared Memory et conflits de banques
- La Shared Memory est composée de 32 banques indépendantes ; pour éviter les conflits, les 32 threads d’un warp doivent accéder à des banques différentes
- Un accès par ligne ne crée pas de conflit, alors qu’un accès par colonne provoque des conflits (accès à la même banque)
- La tuile B est stockée transposée dans la Shared Memory selon une stratégie de "chargement puis transposition", afin d’éviter les conflits de banques pendant le calcul en privilégiant les accès par ligne
Schémas de calcul rapide sur puce
Stratégie de base 1 : un thread calcule une sortie
- Sous la contrainte
BLOCK_DIM=32, l’AI maximale est de 8, ce qui ne permet pas d’atteindre le régime compute-bound
Stratégie 2 : un thread calcule plusieurs sorties
- Avec
BLOCK_DIM=16 et TILE_DIM=64, un thread calcule 4x4 sorties → AI=16
- Comme AI>13, il est possible d’atteindre des performances compute-bound sur un A100
- Des chargements vectorisés comme
float4 depuis la Shared Memory permettent un calcul efficace
Limite pratique du tuilage : quantification des tuiles
- Si la taille de la matrice n’est pas un multiple de la taille de tuile, les blocks en bordure calculent une zone plus grande que nécessaire (calculs inutiles) et utilisent du padding
- Les threads de bord empêchent les accès mémoire inutiles via des conditions de garde, mais la boucle de calcul s’exécute quand même de la même manière, produisant des calculs inutiles (par ex.
C += A * 0)
Éléments supplémentaires d’optimisation des performances
Occupation (Occupancy) et masquage de latence
- Lorsqu’un warp attend longtemps, par exemple lors d’une lecture mémoire, le SM bascule immédiatement sur un autre warp afin de réduire le temps d’inactivité (masquage de latence, latency hiding)
- En assignant simultanément plusieurs Thread Blocks, on peut réduire le temps d’attente grâce à une occupation élevée
- Si la taille des blocks ou des tuiles devient trop grande, le nombre de blocks résidents diminue, l’occupation baisse et les performances se dégradent
Minimiser la divergence des threads
- Si une divergence
if-else apparaît au sein d’un warp, les deux chemins sont exécutés séquentiellement, ce qui peut réduire de moitié les performances effectives
- Il faut donc minimiser les branchements avec du code sans branchement, par exemple via
min ou max
Quantification (Quantization)
- En réduisant la précision de FP32 vers FP16/BFP16, le volume de données déplacées et le nombre de données traitables sont chacun multipliés par 2
- Sur un A100, le calcul FP16 peut atteindre 312 TFLOPS (jusqu’à 16 fois les 19,5 TFLOPS du FP32)
- La quantification permet d’atteindre simultanément la partie droite du Roofline (efficacité mémoire) et la partie haute (performance de calcul maximale)
Résumé général
- La limite fondamentale des performances GPU vient du déséquilibre entre la bande passante mémoire et la capacité de calcul sur puce
- L’amélioration des performances passe par la maximisation de la réutilisation des données (Tiling) et la réduction du trafic mémoire intermédiaire (Fusion)
- Il faut comprendre les caractéristiques matérielles (warp, banques, coalesced access, synchronisation) pour écrire et optimiser des kernels haute performance
- En pratique, des facteurs supplémentaires comme l’occupation, la réduction des divergences et la quantification influencent directement la vitesse réelle
- Concevoir des calculs GPU haute performance exige de combiner l’augmentation théorique de l’AI, l’exploitation des caractéristiques matérielles et l’adaptation à l’organisation et à la taille réelles des données
1 commentaires
Commentaire Hacker News
Curiosité sur le niveau d’optimisation globale d’un programme au niveau du compilateur, avec l’impression que l’approche actuelle consistant à optimiser chaque architecture de LLM une par une est un peu en retard
Partage d’une expérience consistant à faire tourner
llama.cppetvllmsur la même 4070 afin de traiter davantage de prompts en batch ; à partir d’un batch de 8,llama.cppdevient extrêmement lent, et même si l’utilisation GPU semble correcte, il y a en réalité un goulot d’étranglement, tandis quevllms’en sort bien mieuxvllmutilise un cache KV paginé et une disposition fully coalesced appréciée par le GPU, ce qui lui donne des performances optimisées pour le batch, alors quellama.cpputilise une disposition plate adaptée à un prompt unique ; en situation de batch, les motifs d’accès à la mémoire L2 se dégradent et les performances chutentEn intercalant dans
llama.cpple tenseur KV de la forme[seq, head, dim]vers[head, seq, dim], afin de se rapprocher de la manière dontvllmalimente les données dans son fused attention kernel, un gain immédiat d’environ 2x en performances de calcul a été observéLe goulot d’étranglement ne vient pas du GPU lui-même, mais de la façon de concevoir l’accès à la mémoire partagée et les lectures globales ; c’est précisément ce que
vllmaméliore en changeant la disposition mémoireIl a fallu plus de deux jours pour analyser ce goulot d’étranglement, impossible à identifier avec les seuls graphiques d’utilisation GPU, et la plupart des conclusions ont été obtenues par essais et erreurs
Question sur l’existence d’une méthode permettant de répéter ce type d’expérimentation plus facilement, dans un mode proche du hot reload
Remarque selon laquelle, même s’il a été dit que le GPU n’était pas le goulot d’étranglement, l’inefficacité de la disposition mémoire finissait en pratique par réduire l’efficacité de calcul du GPU et constituait donc bien un goulot d’étranglement
Mention du projet
nano-vllmpublié hier par un employé de DeepSeek, avec seulement 1 200 lignes de code mais des performances supérieures à celles devanilla vllmhttps://github.com/GeeeekExplorer/nano-vllmQuestion de savoir si cette modification de disposition dans
llama.cppa été proposée via une pull request, avec l’idée qu’un gain de 2x pourrait profiter à tout le mondeRecommandation d’essayer aussi le projet
ik_llama.cpphttps://github.com/ikawrakow/ik_llama.cppAvis indiquant qu’il s’agit d’un bon article informatif, mais qu’il parle surtout des choix faits par NVIDIA lors de la conception de l’architecture GPU, en insistant sur le fait qu’il ne faut pas mal comprendre les différences avec d’autres fabricants
Par exemple, l’AMD Instinct MI300 atteint jusqu’à 160 TFLOPS en FP32 et 6TB/s de bande passante HBM3/3E, ce qui déplace le ridge point à 27 FLOPs/byte, soit plus du double des 13 FLOPs/byte d’un A100 ; la grande capacité de mémoire HBM (128 à 256GB) modifie aussi les compromis réels entre profondeur de tiling et occupancy, avec en contrepartie un prix élevé et l’absence de support CUDA
Opinion selon laquelle, tant qu’AMD n’accordera pas davantage d’attention à ses logiciels de calcul, seuls les GPU NVIDIA continueront à réellement s’imposer
Spoiler : ce qui compte vraiment n’est pas tant le fonctionnement du GPU lui-même que la manière de l’utiliser pour les calculs de machine learning
reluet de la mention detorchAvis affirmant qu’il faut absolument utiliser des couleurs contrastées, avec insistance sur la lisibilité
Partage d’une expérience avec
font-weight: 300: la plupart des designers sur Mac développent en tenant compte des options de lissage des polices, ce qui donne généralement un rendu visuellement proche denormal, car Mac fait apparaître les polices fines comme semi-grasses ; les designers ont donc tendance à choisir des polices plus fines pour obtenir une impression de style « normal », avec un lien connexe https://news.ycombinator.com/item?id=23553486Supposition selon laquelle l’auteur a peut-être édité et mis en forme l’article en mode sombre, avec la remarque que
edge://flags/#enable-force-darkrend les liens plus lisiblesObservation selon laquelle les liens et les commentaires dans les blocs de code demandaient un effort particulier pour être lus, avec suggestion d’augmenter le contraste ; la qualité du contenu en elle-même a été jugée excellente
Critique du fait que le site web utilise une transparence alpha sur le texte, ce qui réduit fortement le contraste et constitue une grosse erreur
Suggestion qu’un titre comme « faits de base sur les GPU Nvidia » serait en réalité plus approprié, avec l’explication que le terme WARP est lui aussi propre aux GPU Nvidia modernes, alors que les GPU Nvidia vers 2003 étaient du matériel destiné uniquement au rendu de jeux vidéo ; ils sont donc totalement différents des GPU actuels de calcul généraliste, et au final le billet ne fournit pas une explication générale applicable à tous les GPU
Remerciement pour cette excellente ressource d’introduction : en faisant des recherches pendant plusieurs jours sur les GPU lors de l’assemblage d’un AI PC, ce texte s’est révélé très utile car il résume bien les points essentiels à connaître ainsi que les domaines d’application à forte valeur ajoutée comme l’IA générative ; le diagramme de la hiérarchie mémoire du GPU A100 a été jugé particulièrement utile
Étonnement face à l’utilisation de diagrammes ASCII