Multi-GPU

Combiner plusieurs cartes graphiques dans un PC pour gagner en performances. L'idée n'est pas mauvaise : si une seule carte graphique ne suffit pas, profiter des performances de plusieurs cartes graphiques peut aider à obtenir les performances voulues. Ces dernières années, les deux principaux fabricants de cartes graphiques non-intégrées ont sorti des techniques multi-GPU : le SLI et le Crossfire. Ces technologies sont destinées aux jeux vidéos, et permettent aux joueurs d'obtenir des performances excellentes avec des graphismes sublimes.

Toutefois, le multi-GPU ne sert pas qu'aux joueurs. Combiner la puissance de plusieurs cartes graphiques peut servir à bien d'autres applications. Par exemple, on pourrait citer les applications de réalité virtuelle, l'imagerie médicale haute précision, les applications de conception par ordinateur, etc. Utiliser plusieurs cartes graphiques est alors une nécessité.

Mine de rien, c'est ce genre de choses qui se cachent derrière les films d'animation : Pixar ou Disney ont vraiment besoin de rendre des images très complexes, avec beaucoup d'effets. Et ne parlons pas des effets spéciaux créés par ordinateur. Une seule carte graphique a tendance à rapidement rendre l’âme dans ces situations.

Le multi-GPU peut se présenter sous plusieurs formes. La plus simple consiste à utiliser plusieurs cartes graphiques séparées, reliées à la même carte mère. Nos cartes graphiques sont alors utilisées de concert, à condition que les drivers de l'ordinateur le supportent.

Dans certains cas, deux cartes graphiques sont simplement connectées à la carte mère via PCI-Express. Si les deux cartes ont besoin d’échanger des informations, les transferts passent par le bus PCI-Express. Sur certaines cartes, on trouve un connecteur qui relie les deux cartes, sans passer par le bus PCI-Express : les échanges d'informations entre les cartes graphiques passent alors par ce connecteur spécialisé. Généralement, la solution la plus rapide est celle utilisant le connecteur. Si vous ne l'avez pas branché, faites-le pour obtenir de meilleures performances.

Dans d'autres cas, les deux, trois, quatre GPUs sont câblés sur une seule carte. Les communications entre GPUs s'effectuent alors via un bus ou un système d'interconnexion spécialisé, placé sur la carte. Il n'y a pas de différences de performances avec la solution utilisant des cartes séparées reliées avec un connecteur.

Contrairement à ce qu'on pourrait penser, le multi-GPU n'est pas une technique inventée par Nvidia ou AMD. Il s'agit d'une techniques très ancienne. Pensez donc qu'en 1998, il était possible de combiner dans un même PC deux cartes graphiques Voodoo 2, de marque 3dfx (un ancien fabricant de cartes graphiques, aujourd'hui disparu). Autre exemple : dans les années 2006, le fabricant de cartes graphiques S3 avait introduit cette technologie pour ses cartes graphiques Chrome.

Tout le problème des solutions multi-GPU est de répartir les calculs sur plusieurs cartes graphiques. Et c'est loin d'être chose facile. Il existe diverses techniques, chacune avec ses avantages et ses inconvénients. Cet article va vous présenter quelles sont les techniques de répartitions des calculs sur plusieurs GPUs. Vous apprendrez ainsi des choses très intéressantes, et vous pourrez mieux configurer vos drivers de cartes graphiques ou vos jeux, pour gagner en performances.

Split Frame Rendering

Une des techniques les plus simples pour répartir nos calculs sur plusieurs cartes graphiques consiste à découper l'image en morceaux, qui sont répartis sur des cartes graphiques différentes. Ce principe s'appelle le Split Frame Rendering.

Ce principe a été décliné en plusieurs versions, et nous allons les passer en revue. Nous pouvons commencer par faire la différence entre les méthodes de distribution statiques et dynamiques. Dans les méthodes statiques, la manière de découper l'image est toujours la même : celle-ci sera découpée en blocs, en lignes, en colonnes, etc; de la même façon quelque soit l'image. Dans les techniques dynamique, la taille des blocs, lignes, etc ; s'adapte en fonction de la complexité de l'image. Nous allons commencer par aborder les méthodes statiques.

Scan Line Interleave

Historiquement, la première technique multi-GPU fût utilisée par les cartes graphiques Voodoo 2. Cette technique s'appelait le Scan Line Interleave.

Comme vous le savez, l'image à rendre est composée de pixels, organisés en lignes et en colonnes. Avec cette technique, chaque carte graphique calculait une ligne sur deux. La première carte rendait les lignes paires, et l'autre les lignes impaires. On peut adapter la technique à un nombre arbitraire de GPU. Il suffit de faire calculer par chaque GPU une ligne sur 3, 4, 5, etc. Pour n GPUs, chaque GPU calculera une ligne sur n.

Cette technique avait un avantage certain pour l'époque. Avant, la résolution des images était limitée par la quantité de mémoire vidéo. Dans toutes les cartes graphiques, l'image à afficher est calculée, puis stockée dans une portion de la mémoire vidéo qu'on appelle le framebuffer. Celui-ci avait une taille limitée matériellement : une Voodoo 2 ne pouvait pas dépasser une résolution de 800 * 600. Avec le scan line interleave, ce framebuffer était utilisé pour seulement une moitié de l'image. Tout se passait comme si les deux framebuffers des deux cartes étaient combinés en un seul framebuffer plus gros, capable de supporter des résolutions plus élevées.

Cette technique a toutefois un gros défaut : l’utilisation de la mémoire vidéo n'est pas optimale. Comme vous le savez, la mémoire vidéo sert à stocker les objets géométriques de la scène à rendre, les textures, et d'autres choses encore. Avec le scan line interleave, chaque objet et texture est présent dans la mémoire vidéo de chaque carte graphique. Il faut dire que ces objets et textures sont assez grands : la carte graphique devant rendre une ligne sur deux, il est très rare qu'un objet doive être rendu totalement par une des cartes et pas l'autre. Avec d'autres techniques, cette consommation de mémoire peut être mieux gérée.

Checker Board

Autre technique : ne pas découper l'image en lignes, mais en carrés de plusieurs pixels. Chaque carré sera alors attribué à une carte graphique bien précise. Dans le cas le plus simple, les carrés ont une taille fixe : par exemple, on peut découper une image en blocs de 16 pixels.

Si les carrés sont suffisamment gros, il arrive qu'ils puissent contenir totalement un objet géométrique. Dans ces conditions, une seule carte graphique devra calculer ce qui a rapport à cet objet géométrique. Elle seule aura donc besoin des données géométriques et des textures de l'objet. L'autre carte n'en aura pas besoin, et n'aura pas à charger ces données dans sa mémoire vidéo. Le gain en terme de mémoire peut être appréciable si les blocs sont suffisamment gros.

Mais il arrive souvent qu'un objet soit à la frontière entre deux blocs : il doit donc être rendu par les deux cartes, et sera stocké dans les deux mémoires vidéos. Pour plus d'efficacité, on peut passer d'un découpage statique, où tous les carrés ont la même taille, à un découpage dynamique, dans lequel on découpe l'image en rectangles dont la longueur et la largeur varient. En faisant varier le mieux possible la taille et la longueur de ces rectangles, on peut faire en sorte qu'un maximum de rectangles contiennent totalement un objet géométrique. Le gain en terme de mémoire et de rendu peut être appréciable. Néanmoins, découper des blocs dynamiquement est très complexe, et le faire efficacement est un casse-tête pour les développeurs de drivers.

Scan-lines contigues

Une technique permet de garder le gain en mémoire sans trop compliquer la tâche des développeurs de drivers. L'idée consiste à simplement couper l'image en deux, horizontalement. La partie haute de l'image ira sur un GPU, et la partie basse sur l'autre. Cette technique peut être adaptée avec plusieurs GPU : il suffit de découper l'image en autant de parties qu'il y a de GPUs, et attribuer chaque portion à un GPU.

Ainsi, le gain en terme de mémoire et de rendu est appréciable. De nombreux objets n'apparaissent que dans une portion de l'image, et pas dans l'autre. Le drivers peut ainsi répartir les données géométriques et les textures pour éviter toute duplication, comme dans la technique des carrés. Cela demande du travail au driver, mais cela en vaut la peine.

Le découpage de l'image peut reposer sur une technique statique : la moitié haute de l'image pour le premier GPU, et le bas pour l'autre. Et cette technique s'adapte aussi avec plus de deux portions en découpant l'image en N portions identiques.

Ceci dit, quelques complications peuvent survenir dans certains jeux : les FPS notamment. Dans ces jeux, plus ou moins réalistes, le bas de l'image est plus chargé que le haut. Dans le bas de l'image, on trouve un sol assez complexe, des murs, les ennemis, etc. Le haut représente souvent le ciel ou un plafond, assez simple géométriquement. Toute l'agitation a lieu vers le bas ou le milieu de l'écran, et le haut est presque vide. Ainsi, le rendu de la partie haute sera plus rapide que celui du bas, et une des cartes 3D finira par attendre l'autre.

Mieux répartir les calculs devient alors nécessaire. Après tout, il vaut mieux que chaque carte doive faire 50% du travail, au lieu d'avoir une carte qui se tape 70% du boulot, et l'autre qui ne fait que 30% du rendu. Pour cela, on peut choisir un découpage statique adapté, dans lequel la partie haute envoyée au premier GPU est plus grande que la partie basse.

Cela peut aussi être fait dynamiquement : le découpage de l'image est alors choisi à l’exécution, et la balance entre partie haute et basse s'adapte aux circonstances. Comme cela, si vous voulez tirer une roquette sur une ennemi qui vient de prendre un jumper (vous ne jouez pas à UT ou Quake ?), vous ne subirez pas un gros coup de lag parce que le découpage statique était inadapté. Dans ce cas, c'est le driver qui gère ce découpage : il dispose d'algorithmes plus ou moins complexes capables de déterminer assez précisément comment découper l'image au mieux. Mais il va de soit que ces algorithmes ne sont pas parfaits.

Alternate Frame Rendering

Comme je l'ai dit, la répartition des différentes données entre les GPUs et la répartition des calculs pose de nombreux problèmes. Si elle est mal faite, les performances s'effondrent. Et même bien faite, on n'obtient pas un gain proportionnel au nombre de GPUs : des transferts entre les cartes graphiques sont toujours nécessaires, et la répartition ne peut pas être parfaite.

L'alternate Frame Rendering, ou AFR ne pose pas ce genre de problèmes. Cette technique consiste à répartir non pas des blocs ou lignes de pixels, mais des images complètes sur les différents GPUs. Au lieu de découper une image en morceaux et de répartir les morceaux sur les différents GPUs, ce sont des images complètes qui sont envoyées à chaque GPU.

Le problème mentionné plus haut disparaît alors. Plus besoin de réfléchir longuement pour savoir comment découper l'image et répartir les morceaux au mieux. Dans sa forme la plus simple, un GPU calcule une image, et l'autre GPU calcule la suivante en parallèle. Cette idée s'adapte aussi avec plus de deux GPUs.

Cette technique est supportée par la majorité des cartes graphiques actuelles. De même, la technique consistant à découper l'image en deux et à répartir les deux portions sur deux GPUs l'est aussi. Dans les drivers AMD et Nvidia, vous avez ainsi le choix entre AFR et SFR, le SFR étant la technique de découpage d'image en deux. Il faut toutefois noter que cette technique n'est pas nouvelle : c'est ATI qui a inventé cette technologie sur ses cartes graphiques Rage Fury, afin de faire concurrence à la Geforce 256, la toute première Geforce.

Évidemment, on retrouve un vieux problème présent dans certaines des techniques vues avant. Chaque objet géométrique devra être présent dans la mémoire vidéo de chaque carte graphique, vu qu'elle devra l'afficher à l'écran. Il est donc impossible de répartir les différents objets dans les mémoires des cartes graphiques. Mais d'autres problèmes peuvent survenir.

Micro-stuttering

Un des défauts de cette approche, c'est le micro-stuttering. Lorsque nos images sont réparties sur nos cartes graphiques, elles le sont dès qu'une carte graphique est libre et qu'une image est disponible. Suivant le moment où une carte graphique devient libre, et le temps mis au calcul de l'image, on peut avoir des temps de latences entre images qui varient beaucoup.

Dans des situations où le processeur est peu puissant, les temps entre deux images peuvent se mettre à varier très fortement, et d'une manière beaucoup moins imprévisible. Le nombre d'images par seconde se met à varier rapidement sur de petites périodes de temps. Alors certes, on ne parle que de quelques millisecondes, mais cela se voit à l’œil nu.

Cela cause une impression de micro-saccades, que notre cerveau peut percevoir consciemment, même si le temps entre deux images est très faible. Suivant les joueurs, des différences de 10 à 20 millisecondes peuvent rendre une partie de jeu injouable. Le phénomène est bien connu, surtout depuis la publication d'un article sur le site TheTechReport.

Pour diminuer l'ampleur de ce phénomène, les cartes graphiques récentes incorporent des circuits pour limiter la casse. Ceux-ci se basent sur un principe simple : pour égaliser le temps entre deux images, et éviter les variations, le mieux est d’empêcher des images de s'afficher trop tôt. Si une image a été calculée en très peu de temps, on retarde son affichage durant un moment. Le temps d'attente idéal est alors calculé en fonction de la moyenne du framerate mesuré précédemment.

Dépendances inter-frames

Il arrive que deux images soient dépendantes les unes des autres : les informations nées lors du calcul d'une image peuvent devoir être réutilisées dans le calcul des images suivantes. Cela arrive quand des données géométriques traitées par la carte graphique sont enregistrées dans des textures (dans les Streams Out Buffers pour être précis), dans l'utilisation de fonctionnalités de DirectX ou d'Open GL qu'on appelle le Render To Texture, ainsi que dans quelques autres situations.

Évidemment, avec l'AFR, cela pose quelques problèmes : les deux cartes doivent synchroniser leurs calculs pour éviter que l'image suivante rate des informations utiles, et soit affichée n'importe comment. Sans compter qu'en plus, les données doivent être transférées dans la mémoire du GPU qui calcule l'image suivante.