\label{fig-gpucpu1}
\end{figure}
-Cette spécialisation des circuits GPU a permis d'en améliorer les performances brutes beaucoup plus rapidement que pour les CPUs, au fil des évolutions de la technologie. Il en est allé de même pour les débits mémoire théoriques. Les graphiques de la figure \ref{fig-gpucpu2} comparent les rythmes de ces évolutions pour les GPUs Nvidia\textregistered et pour les CPUs Intel\textregistered.
+Cette spécialisation des circuits GPU a permis d'en améliorer les performances brutes beaucoup plus rapidement que pour les CPUs, au fil des évolutions de la technologie. Il en est allé de même pour les débits mémoire théoriques. Les graphiques de la figure \ref{fig-gpucpu2} comparent les rythmes de ces évolutions pour les GPUs Nvidia\textregistered~ et pour les CPUs Intel\textregistered.
Les problèmes requérant les capacités de calcul spécifiques des GPUs ne sont cependant pas limités aux questions de rendu graphique, aussi les scientifiques ont-ils très vite cherché à tirer parti de la puissance de calcul croissante des GPUs pour traiter d'autres types de problèmes, faisant sens à l'acronyme GPGPU (General Purpose Graphical Processing Unit).
\begin{figure}[h]
\centering
- \subfigure[Nombre maximum théorique d'opérations en virgule flottante par seconde en fonction de l'année et de l'architecture.]{\includegraphics[height=5cm]{Chapters/chapter1b/img/gpucpu2a.png}}\quad
- \subfigure[Bande passante théorique maximale des diverses architectures.]{\includegraphics[height=5cm]{Chapters/chapter1b/img/gpucpu2b.png}}
+ \subfigure[Nombre maximum théorique d'opérations en virgule flottante par seconde en fonction de l'année et de l'architecture.]{\includegraphics[width=9cm]{Chapters/chapter1b/img/gpucpu2a.png}}\\
+ \subfigure[Bande passante théorique maximale des diverses architectures.]{\includegraphics[width=9cm]{Chapters/chapter1b/img/gpucpu2b.png}}
\caption{Comparaison des performances des GPUs Nvidia et des CPU Intel (d'après \cite{CUDAPG}).}
\label{fig-gpucpu2}
\end{figure}
Globale & off-chip &grille&580/80/350 &144 & 6G \\
\bottomrule
\end{tabular}
- \caption{Caractéristiques des différents types de mémoire disponibles sur le GPU. Pour les mémoires cachées, les latences sont données selon l'accès \textit{sans-cache/L1/L2}. Les mesures ont été obtenues à l'aide des microprogrammes de test de \cite{wong2010demystifying}.}
+ \caption{Caractéristiques des différents types de mémoire disponibles sur le GPU. Pour les mémoires cachées, les latences sont données selon l'accès \textit{sans-cache/L1/L2} et ont été obtenues à l'aide des microprogrammes de test de \cite{wong2010demystifying}. Les valeurs de débit sont données par le constructeur.}
\label{tab-gpu-memoire}
\end{table}
Nous verrons que cette notion d'\textit{occupancy}, si elle conserve du sens, peut toutefois être remise en question en optimisant d'autres aspects permettant d'arriver à une réduction de l'effet des latences, comme le parallélisme d'instructions ou l'augmentation du volume des transactions. En effet, ces techniques, et surtout l'utilisation avisée des différents types de mémoire du GPU permettent d'obtenir des performances élevées, parfois inenvisageables en suivant les prescriptions du constructeur.
-
-
+\section{Contraintes de conception}
+Certaines de ces contraintes ont déjà été évoquées rapidement, mais il nous semble important d'en donner ici une présentation synthétique.
+\begin{enumerate}
+\item \textbf{Contiguïté}.
+Les accès aléatoires à la mémoire globale sont en règle générale très pénalisants. Toutefois, il est possible de tirer parti du cache de niveau 1 (à une dimension) en organisant les données pour que tous les threads d'un même warp accèdent à des données appartenant au même bloc de 128 octets de mémoire. Le non-respect de cette contrainte de contiguïté (\textit{coalescence}) induit des accès réalisés en plusieurs transactions serialisées et donc une perte potentiellement importante de performances.
+\item \textbf{Conflits de banques}.
+La mémoire partagée, plus rapide que la mémoire globale, peut sembler une solution évidente pour obtenir des performances élevées. Cependant, elle est physiquement organisée en 32 \og banques \fg{} de largeur 32 bits et présente elle aussi une contrainte majeure. Sur architecture Fermi, l'exécution des threads d'un warp est assurée par deux \textit{moteurs d'exécution} qui activent en parallèle chacun des deux demi-warp.
+Un \textbf{conflit de banque} se produit lorsque deux threads n'appartenant pas au même demi-warp accèdent à des données localisées dans la même banque de mémoire partagée. La transaction parallèle est alors interrompue et sérialisée.
+Ici encore, la perte de performance peut être importante, mais il peut s'avérer très complexe, coûteux, voire impossible d'organiser les données en mémoire partagée de sorte à éviter tout conflit de banque.
+\item \textbf{Branches divergentes}.
+Toute branche d'exécution divergente entraine une sérialisation des exécution des threads du warp auquel ils appartiennent. Il convient donc d'éviter cette situation et d'organiser les traitements en conséquence en privilégiant le plus souvent un découpage plus fin en plusieurs kernels élémentaires plutôt que des kernels \og lourds \fg{}.
+\item \textbf{Transferts GPU$\leftrightarrow$CPU}.
+Les transferts de données entre la mémoire globale du GPU et celle de son hôte CPU peuvent représenter l'essentiel du temps de traitement total et doivent donc être optimisées pour en réduire la fréquence et le volume des données à copier. Cela peut parfois être contradictoire avec la multiplication de petits kernels élémentaires.
+Il est toutefois possible, lorsque la séquence de traitement le permet, de réaliser des transferts en temps masqué, pendant l'exécution d'un kernel, en créant plusieurs flux d'exécution.
+\item \textbf{Partage des ressources d'un SM}
+Le paragraphe sur l'\textit{occupancy} a abordé cet aspect par un exemple. Il faut retenir que chaque SM possède des ressources mémoire (registres, mémoire partagée) que les threads des blocs logiques de la grille de calcul se partagent au cours de l'exécution d'un kernel. L'équilibre entre l'utilisation de ces ressources et le dimensionnement de la grille de calcul relève d'un compromis parfois délicat à trouver pour obtenir les meilleures performances possibles.
+\item \textbf{Les latences}. L'exécution des kernels subit l'effet de latences d'origines diverses. Les latences d'accès aux mémoires (voir Table \ref{tab-gpu-memoire}), les latences des différentes instructions arithmétiques ou encore les latences crées par l'inter dépendance d'instructions consécutives. Il est impératif de les prendre en considération et de mettre en \oe uvre des techniques adaptées pour les masquer au mieux.
+\item \textbf{La mise au point}. L'ordonnancement des threads n'est pas prévisible et les quelques outils d'aide à la mise au point (debug) permettent simplement de cibler un thread présélectionné de la cible. Cela ne permet en aucun cas de déceler, par exemple, les conflits de banques provoqués par l'interaction d'au moins deux threads. Un outil de profilage développé par le fabricant fournit des informations importantes sur le nombre de conflits de banques et les origines probables des limitations de performance des kernels d'un programme. Il ne s'appuie cependant que sur un bloc de threads pour en extrapoler les résultats à l'ensemble de la grille.
+\end{enumerate}
+L'ensemble de ces aspects rend difficile la conception d'implémentations GPU rapides car rares sont les transcriptions directes d'un code CPU qui ne se heurtent pas sévèrement à l'une ou l'autre des contraintes que l'on vient d'énumérer. Les performances qui en résultent sont alors très en deça de celles attendues, voire inférieures à celles de l'implémentation CPU. La mise au point étant par ailleurs très délicate, il nous semble important de proposer des kernels élémentaires dont on peut aisément garantir les résultats par des méthodes de test ne nécessitant pas de devoir implémenter conjointement les versions CPU équivalentes des algorithmes concernés.