GPUs : les bases pour comprendre leurs performances

Aujourd’hui je vais vous parler des GPUs (Graphics Processing Units). Bien sûr vous connaissez tous le terme, et vous avez sans doute tous un GPU dans votre ordinateur. Mais savez vous comment sont architecturés ces processeurs et pourquoi ils sont aussi utiles, pour le graphisme, mais aussi et surtout pour l’IA désormais ?

Mon objectif est, sans rentrer dans la technique, de vous faire comprendre ce qui constitue les caractéristiques des GPUs, et leurs différences par rapport à d’autres types de processeurs.

Les GPUs n’ont pas toujours été les stars de la tech (et du Nasdaq). A la fin des années 90 ils ne servaient qu’à faire du rendu graphique, notamment pour avoir de la 3D plus fluide. C’est NVidia qui va sortir ce que l’on considère (à tort) comme le premier GPU avec la GeForce 256 qui sort en octobre 1999. Le point fort de cette carte est en fait d’être compatible direct3D, mais c’est une autre histoire. 


Avec les années les GPUs ont rapidement progressé et dans les premières années du troisième millénaire va apparaître la notion de GPGPU (General Purpose GPU), c’est-à-dire l’utilisation de GPUs pour faire du calcul parallèle en dehors du contexte du rendu graphique. En 2010 NVidia va lancer une nouvelle architecture (codename FERMI), disponible dans la GTX 480 pour commencer. C’est cette architecture qui va être le point de bascule du GPU pour l’intelligence artificielle, avec un premier fait d’arme qui sera l’entraînement d’AlexNet (par Alex Krizhevsky, avec l’aide de Geoffrey Hinton et Ilya Sutskever, que des grands noms de l’IA moderne) en 2012 à l’aide de GPUs.

Depuis, les GPUs se sont imposés comme un standard incontournable pour l’IA (mais pas que, ils sont indispensables pour faire de la simulation physique sérieusement par exemple).

Cependant la marche du progrès ne s’est pas arrêtée avec eux, et on voit apparaître récemment des processeurs encore plus spécialisés.
On les regroupe sous le nom de NPUs (Neural Processing Units). Parmi eux on trouve les TPUs de Google lancés en 2016, les IPUs (Intelligence Processing Units) de Graphcore (j’ai même écrit un article scientifique sur ce sujet), et bien d’autres bien sûr, le hardware étant un enjeu majeur de l’IA moderne. Je finirais en mentionnant les architectures récentes mises en avant par Apple (M3 et M4) ou Qualcomm (Snapdragon X elite), mais sans rien en dire ^^

Bref, vous l’avez compris, une histoire qui n’est pas si récente pour les GPUs, et pourtant vous n’avez probablement pas l’intuition de comment ils fonctionnent, on va régler ce problème aujourd’hui !

Le GPU, une architecture conçu pour le parallélisme

Pour vous dire comment fonctionne un GPU, je vais commencer par parler des CPU, c’est-à-dire des processeurs classiques.

Un CPU a une architecture qui est conçue pour la polyvalence : il peut faire des tâches très différentes, mais généralement de manière séquentielle. Pour obtenir les meilleures performances possibles, le CPU va faire de la prédiction de branchement, ce qui permet de faire de l’exécution spéculative : il charge des instructions en avance dans sa chaîne de traitement, car elles sont probablement celles qui vont être exécutées. Ce mécanisme, qui parfois se trompe bien sûr, prend de la place dans le processeur, place qui dans un GPU qui n’a pas ce mécanisme permettra de caser plus de transistors ^^
Le CPU va aussi faire de l’exécution dans le désordre, c’est-à-dire qu’il va ordonner les instructions dans un autre ordre que celui proposé par le programme qui tourne, toujours dans le but de gagner du temps de calcul. Ce mécanisme aussi prend de la place sur le processeur.

Pour le reste, le CPU est constitué de plusieurs coeurs (cores en anglais), généralement 4 à 16 dans les processeurs actuels. Ces coeurs peuvent gérer des tâches variées et des calculs complexes.

Vous pouvez voir sur le schéma l’architecture de très haut niveau d’un CPU : quelques unités de calcul (les ALU, Arithmetic Logic Unit) contrôlées par une CU (Control Unit) et de la mémoire cache qui va permettre de minimiser les latences (moments où les unités de calcul attendent la data). Il y a bien sûr une mémoire plus standard à plus haut niveau.

Le GPU est organisé totalement différemment, et n’a pas du tout les mêmes objectifs. Leur architecture est constituée de milliers de coeurs plus simples, qui ne savent faire que des tâches répétitives et basiques sur un très grand nombre d’éléments simultanément. Ces coeurs sont organisés en blocs et grilles, avec des SM (Streaming Multiprocessors) qui gèrent la répartition des calculs. Dans l’architecture Fermi de NVidia (2010), il y avait par exemple 16 SM pilotant chacun 32 coeurs (donc 512 coeurs en tout).

Le schéma ci-dessous montre à quoi ressemble l’architecture d’un GPU à haut niveau. Les ALU ici sont beaucoup plus simples que celles d’un CPU.

Le GPU va donc pouvoir faire des tâches très balisées (on en parle plus tard), mais massivement. C’est donc l’outil de choix pour toute ce qui demande de la puissance brute pour faire des choses simples rapidement et en grand nombre.
Une analogie entre le CPU et le GPU serait de dire que le CPU c’est un outil comme une perceuse-visseuse qui peut faire des choses différentes, et efficacement quand il n’y a qu’une chose à faire à la fois, tandis que le GPU c’est une trousse de 50 tournevis, ce qui permet d’aller plus vite si on a 50 personne pour s’en servir, et 50 vis à visser (oui, belle analogie n’est-ce pas ^^).

Un exemple jouet, mais plus concret

Pour faire passer l’idée, on va faire l’exemple bien connu de l’addition de plusieurs nombres entre eux.

Comment trouver le résultat de l’opération 2 + 34 + 65 + 12 + 91 + 3 + 77 + 43 ? Et surtout combien de cycles de calcul va t’il me falloir pour faire le calcul ?

Commençons par le cas du CPU. Pour faire le calcul il va travailler séquentiellement, c’est-à-dire faire la première addition 2 + 34, puis la seconde, puis …
Le schéma ci-dessous illustre ce calcul.

Il faut 7 cycles pour faire l’addition des 8 nombres.

Passons maintenant au même calcul, mais chez Monsieur GPU ! Le GPU a plein de coeurs, il va ici en utiliser 4 pour l’opération. Le schéma illustre le fonctionnement. C1, C2, C3 et C4 sont les noms des coeurs.

Il faut ici 3 cycles pour faire le calcul, et on voit que les coeurs sont libérés pour une bonne partie avant la fin du calcul. Le GPU est donc beaucoup plus efficace que le CPU sur cette tâche !

Bien entendu la réalité est plus complexe que cela, mais vous avez l’idée. Si on a une grande masse de données et qu’il faut faire une opération de bourrin en répétant en masse la même chose, le GPU est très efficace.

L’IA neuronale, le domaine de prédilection des GPUs

Je suis désolé par avance, mais pour expliquer l’intérêt des GPUs pour l’IA, je dois commencer par parler de l’IA et des réseaux de neurones. L’IA est un domaine riche, qui utilise de très nombreuses approches différentes. Depuis plusieurs années l’approche à base de réseaux de neurones a montré son efficacité, en particulier en faisant les calculs sur GPUs.

Pour tout vous expliquer, je passe par un exemple très simple de petit réseau de neurones, deux entrées, deux sorties, et une couche cachée. w_1,1 indique le poids de la connexion entre x1 et h1. v_1,1 le poids entre h1 et y1, etc.
le schéma ci-dessous représente ce réseau.

Il y a de nombreuses opérations qui vont être effectuées sur ce type de réseau, mais une opération typique est l’étape du feed-forward. Il s’agit dans cette opération de passer de la data en entrée et de calculer les valeurs que le réseau va produire en sortie (au niveau de y1 et y2 donc). C’est typiquement ce qui va se passer lors de l’inférence au niveau d’un modèle de langue.

Et bien tout ceci se fait avec une série de multiplications et d’additions de matrices, telle qu’illustrée dans la figure suivante.

Alors bien entendu c’est sans doute totalement incompréhensible si vous n’avez pas déjà de bons rudiments de maths et quelques connaissances sur les réseaux de neurones. Mais si on résume on voit que ce qu’envoie la couche cachée vers les sorties est une somme pondérée des valeurs d’entrée, avec des pondérations qui sont les poids indiqués par les w_1,1 et cie.

Puis on voit que, selon certains choix sur les caractéristiques du réseau (linéaire ou non par exemple,) on va avoir un calcul plus ou moins complexe pour les valeurs de sortie, mais que ce sera toujours un produit entre des matrices, et aussi des sommes entre vecteurs.

Et c’est là que la “magie” des GPUs opèrent car leur architecture est conçue pour faire efficacement cette opération de multiplication et addition de matrices entre elles. Plus précisément, l’opération dite FMA pour Fused Multiply-Add, est implantée en dur dans les architectures de GPUs depuis Fermi (2010), mais aussi ensuite dans Kepler (2012), Maxwel (2016), Volta (2017), etc.
Je ne vais pas rentrer dans les détails, mais ce qui permet AU GPU de réaliser les opérations matricielles très rapidement c’est la conjonction entre la facilité du GPU à traiter cette opération de multiplication-addition sur les flottants, et l’aspect massivement parallèle de son architecture.

La figure ci-dessous illustre le fait qu’en donnant des blocs différents des matrices d’entrées qu’une multiplication à plusieurs unités de calcul, on peut faire un calcul beaucoup plus vite.

Avec 4 unités de calcul en parallèle on fait ce calcul en un seul cycle, à condition que la data soit bien mis aux bons endroits.

Je résume

Au final c’est conceptuellement très simple un GPU : sur un bout de silicium on va caser plus d’unités de calcul, toutes plus simples et qui sont bien pilotées et qui ont accès à de la data via des cache “locaux” à des blocs d’unités de calcul.

En faisant ça, on réalise dans chaque cœur des calculs plus simples, mais très très vite. Si on a un problème qui peut bénéficier d’une découpe simple de sa data en blocs de sous-données faciles à traiter de la même manière, alors le GPU fera des miracles !

Le miracle actuel de l’IA c’est largement cela, avec des processeurs qui maintenant vont au-delà des GPUs, et qui sont de plus en plus spécialisés. La plupart des NPUs vont ainsi travailler sur des données numériques de très faible précision. Par exemple lorsque l’on fait un benchmark des NPUs apple (du M4 au A17 de l’iphone 15), on fournit généralement des chiffres de performances liés aux multiplication-addition entre INT (entiers), là où pour les GPUs on s’intéresse aux FLOAT (flottants), il s’agit donc d’un contexte d’usage très différent. Les NPUs vont par exemple être plus efficaces pour l’inférence, mais assez peu utiles pour l’entraînement de modèles.

Voilà, j’espère que tout cela vous aura donné quelques intuitions concernant les GPUs, et je vous dis à bientôt pour un nouvel article, toujours sur ce même blog !