\section{Pourquoi ?}
S'il fallait en réduire les raisons à une seule, c'est vraisemblablement la concurrence commerciale et la croissance du marché des jeux vidéos qui a poussé les fabricants de cartes graphiques à une innovation permanente qui a donnée naissance aux GPUs. 
Le rendu graphique dans ce cadre est une opération exigeante en calcul mais intrinsèquement parallèle car chaque pixel y est traité individuellement. L'amélioration des résolutions a aussi contribué à faire évoluer la nature de ces besoins de \textit{parallèle} à \textit{massivement parallèle}, un écran actuel pouvant comporter plus de  2,5 millions de pixels. 

La technologie de fabrication des GPUs étant identique à celle des CPUs, c'est donc au niveau de la répartition des fonctionnalités que les GPUs se distinguent : là où un CPU comporte quelques c\oe urs de calcul et beaucoup de transistors dédiés à la réalisation de mémoire cache et de contrôle de flux, un GPU présente plusieurs unités comportant chacune une grande quantité de c\oe urs de calcul ne disposant que de très peu de mémoire cache et des  capacités de contrôle rudimentaires, comme l'illustrent les schémas de la figure \ref{fig-gpucpu1}.

\begin{figure}[h]
  \centering
  \includegraphics[width=10cm]{Chapters/chapter1b/img/gpucpu1.png}
  \caption{Comparaison des structures d'un c\oe ur de GPU et d'un c\oe ur de CPU (d'après \cite{CUDAPG}). ALU = Arithmetical \& Logical Unit.}
  \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.  

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}}
  \caption{Comparaison des performances des GPUs Nvidia et des CPU Intel (d'après \cite{CUDAPG}).}
  \label{fig-gpucpu2}
\end{figure}

Malgré des caractéristiques prometteuses, les GPGPUs n'ont pas immédiatement déclenché une vague d'expérimentations scientifiques dans des domaines variés, l'essentiel des travaux étant liés à la construction et la visualisation des données issues des instruments d'imagerie médicale. 

C'est la parution de l'extension de langage CUDA qui a réellement démocratisé l'emploi des GPGPUs et favorisé l'émergence de travaux variés. CUDA est une extension de haut niveau du langage C permettant d'écrire facilement des fonctions s'exécutant en parallèle sur le GPU, que Nvidia nomme \textit{kernels}. L'extension CUDA permet également de gérer de manière transparente les changements d'échelle du parallélisme d'une architecture à une autre ou tout simplement les dimensions de la grille de calcul.

\section{Comment ?} 
\subsection{Le matériel}
Pour bien tirer parti des capacités des GPUs, il est important d'en comprendre l'organisation matérielle. Nous limiterons cette présentation à l'architecture dite \textit{Fermi} à laquelle appartient le GPU C2070 qui a servi à l'essentiel de nos expérimentations et dont la constitution  est détaillée par les diagrammes de la figure \ref{fig-c2070}.

Le circuit se divise en 4 groupes de processeurs de flux (les SMs = \textit{Streaming Multiprocessors}), chaque groupe en comprenant 3 ou 4 pour un total de 14.
Chaque SM héberge à son tour 32 c\oe urs de calcul (les SPs = \textit{Streaming Processors}).
Les threads sont exécutés par \textit{warps} de 2$\times$16, soit un par c\oe ur de calcul. 

\begin{figure}[h]
  \centering
  \subfigure[Organisation en groupes de SMs ]{\includegraphics[height=6cm]{Chapters/chapter1b/img/fermi.png}}\quad
  \subfigure[Constitution d'un SM.]{\includegraphics[height=6cm]{Chapters/chapter1b/img/fermi-sm.png}}
  \caption{Organisation des GPUs d'architecture Fermi, comme le C2070 (d'après www.hpcresearch.nl).}
  \label{fig-c2070}
\end{figure}

Les ressources mémoire sont de nature diverse, tant du point de vue du volume disponible, que de la portée ou du débit. Retenons que les registres, peu nombreux et embarqués sur le circuit (\textit{on-chip}), présentent les meilleures performances alors que la mémoire principale, dite globale, externe au circuit (\textit{off-chip}) a une grande capacité mais présente des latences importantes de plusieurs centaines de cycles d'horloge.
Le fabricant fournit des indications essentiellement qualitatives concernant les latences d'accès aux mémoires, mais ne fournit pas de chiffres sur les latences réelles en fonction des accès et de la proximité du cache lorsqu'un des 3 niveaux est sollicité. Le tableau \ref{tab-gpu-memoire} présente une synthèse des caractéristiques du modèle C2070, issue d'expérimentations menées par nos soins à l'aide des micro-tests présentés dans  \cite{wong2010demystifying}.

Une petite quantité de mémoire on-chip est présente sur chaque SM et permet la communication entre les threads s'exécutant sur ce SM. Cette mémoire est appelée \textit{mémoire partagée} et permet des débits bien supérieurs à la mémoire globale.

\begin{table}[h]
  \centering
\begin{tabular}[htb]{lcccccc}
      \toprule
      &\multicolumn{4}{c}{Propriétés}\\
              \cmidrule(r){2-6}
              &Emplacement  &Portée&Latence &débit &Taille  \\
              &(on/off-chip)&      &(cycles)&(Go/s)&(octets)\\
              \cmidrule(r){2-6}
          Registres&   on-chip   &thread&1         &8000& 32K par SM  \\
          Partagée &   on-chip   &bloc  &38        &1300&  48K        \\
          \midrule
          Constante&   off-chip  &grille&370/46/140 &8000& 64K        \\
          Texture  &   off-chip  &grille&500/260/372&N/C & 6G         \\
          Locale   &   off-chip  &thread&550        &N/C & 512K       \\
          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}.}
      \label{tab-gpu-memoire}
\end{table}

\subsection{Le logiciel}
Dans le modèle CUDA, chaque \textit{kernel} est exécuté par un certain nombre de threads. Chaque thread possède un identifiant unique, accessible à l'intérieur du kernel, ce qui permet d'en individualiser le traitement.
L'ensemble des threads est organisé en plusieurs blocs indépendants, eux-même rassemblés en une grille. Pour faciliter la représentation de modèles variés, les blocs de threads ainsi que la grille de blocs peuvent être décrits par des tableaux à une, deux ou trois dimensions.
Le nombre total de threads appartenant à un même bloc est cependant limité, selon la version de GPU, à 512 ou 1024. Le diagramme de la figure \ref{fig-threads} illustre l'organisation d'une grille de calcul à 2 dimensions et de ses blocs, également à 2 dimensions, situation qui correspond à la majorité des modèles d'exécution de nos \textit{kernels} de traitement d'image. 

\begin{figure}[h]
  \centering
  \includegraphics[height=8cm]{Chapters/chapter1b/img/threads.png}
  \caption{Représentation d'une grille de calcul en 2D et des blocs de threads, à 2 dimensions, qui la composent.}
  \label{fig-threads}
\end{figure}

Au sein d'un même bloc, les threads peuvent communiquer efficacement entre eux grâce à la mémoire partagée, rapide et présentant une faible latence.
En revanche, la communication inter-blocs passe nécessairement par la mémoire globale dont l'emploi est pénalisant. 

L'emploi de la mémoire partagée n'est cependant pas transparent comme peut l'être un cache de niveau 1 (L1) et les motifs d'accès doivent respecter un certain nombre de conditions pour obtenir les performances attendues. Le non-respect de ces contraintes conduit le plus souvent à des fragments de code dont l'exécution s'avère plus lente que leurs équivalents CPU.

\subsection{L'occupancy}
Pour atteindre les meilleures performances possible, le fabricant Nvidia recommande d'avoir toujours suffisamment de threads dans chaque bloc et de blocs dans la grille et ce, pour masquer les latences des accès aux mémoires, mais aussi celles des instructions arithmétiques.
Il définit un indice nommé \textit{occupancy}, que l'on  pourrait franciser par \textit{charge} qui représente, à chaque instant, le rapport du nombre de threads actifs par SM sur le nombre maximum de threads actifs par SM (1536 sur C2070). 

La valeur de l'\textit{occupancy} peut se trouver limitée par un usage trop intensif des ressources mémoire par les threads (registres, mémoire partagée) ou bien par une grille de calcul mal dimensionnée. L'ensemble des paramètres intervenant dans le calcul de l'\textit{occupancy} est pris en compte dans l'\textit{occupancy calculator}, feuille de calcul fournie par le fabricant pour aider les développeurs à bien employer les ressources des divers modèles de GPU.  
 
Les limitations de l'\textit{occupancy} ont pour origine :
\begin{enumerate}
\item {\bf l'usage des registres}. Si chaque thread utilise le maximum de $64$ registres possible ($63$ pour l'utilisateur $+1$ pour le processeur), le bloc de threads affecté au SM ne peut donc activer simultanément que $32K/64=512$ threads, soit une \textit{occupancy} de $512/1536=0.33$.
\item {\bf l'usage de la mémoire partagée}. L'architecture Fermi permet de choisir la répartition entre cache L1 et mémoire partagée, soit 16K/48K, soit 48K/16K. En configuration 48K de mémoire partagée, si chaque thread en emploie 48 octets, le GPU ne peut activer que $48K/48=1024$ threads, soit une \textit{occupancy} de $1024/1536=0.66$.
\item {\bf la taille des blocs}. Un SM ne pouvant activer que 8 blocs simultanément, la taille des blocs limite donc potentiellement l'\textit{occupancy}. Si on exécute un \textit{kernel} sur une grille de calcul dont les blocs ont le minimum de 32 threads, les 8 blocs actifs représenteront alors 256 threads, soit une \textit{occupancy} de $256/1536=0.16$.
\end{enumerate}

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.



