OpenACC Tutorial - Adding directives/fr: Difference between revisions

Updating to match new version of source page
(Created page with "Reprenons l'exemple de produit matrice-vecteur avec la directive <tt>parallel loop</tt>;")
(Updating to match new version of source page)
 
(93 intermediate revisions by 4 users not shown)
Line 13: Line 13:


== Transfert vers un processeur graphique (GPU) ==  
== Transfert vers un processeur graphique (GPU) ==  
Avant de porter du code sur un GPU, il faut savoir qu'ils ne partagent pas la même mémoire que le processeur (CPU);  autrement dit, le GPU n'a pas un accès direct à la mémoire de départ. Cette dernière est en général plus grande, mais plus lente que la mémoire du GPU. Pour pouvoir utiliser un GPU, les données doivent passer par le bus PCI, dont la bande passante est moins grande que celles du CPU et du GPU. Il est donc de la plus haute importance de bien gérer les transferts entre la mémoire de départ et le GPU.  En anglais, ce processus s'appelle ''offloading''.
Avant de porter du code sur un GPU, il faut savoir que ceux-ci ne partagent pas la même mémoire que le CPU de l'hôte.
* la mémoire de l'hôte est en général plus grande, mais plus lente que la mémoire du GPU;
* un GPU n'a pas d'accès direct à la mémoire de l'hôte;
* pour pouvoir utiliser un GPU, les données doivent passer par le bus PCI, dont la bande passante est moins grande que celles du CPU et du GPU;
* il est donc de la plus haute importance de bien gérer les transferts entre la mémoire de départ et le GPU.  En anglais, ce processus s'appelle ''offloading''.


==Directives OpenACC== <!--T:4-->
==Directives OpenACC== <!--T:4-->
Les directives OpenAcc sont semblables aux directives OpenMP. En C/C++, ce sont des <tt>pragmas</tt>  et en Fortran, des commentaires. L'emploi de directives comporte plusieurs avantages. Premièrement, puisque le code est peu affecté, les modifications peuvent se faire de manière incrémentale, un <tt>pragma</tt> à la fois;  ceci est particulièrement utile pour le débogage puisqu'il est ainsi facile d'identifier le changement précis qui crée le bogue. Deuxièmement, OpenACC peut être désactivé au moment de la compilation;  les <tt>pragmas</tt> sont alors vus comme étant des commentaires et ne sont pas considérés par le compilateur, ce qui permet de compiler une version accélérée et une version normale à partir du même code source. Troisièmement, comme le compilateur fait tout le travail de transfert, le même code peut être compilé pour différents types d'accélérateurs, que ce soit un GPU, un Xeon Phi (MIC) ou un CPU;    ainsi, un changement du matériel exigera simplement la mise à jour du compilateur, sans modification au code.  
Les directives OpenAcc sont semblables aux directives [[OpenMP/fr|OpenMP]]. En C/C++, ce sont des énoncés <tt>pragmas</tt>  et en Fortran, des commentaires. L'emploi de directives comporte plusieurs avantages&nbsp;:
* Premièrement, puisque le code est peu affecté, les modifications peuvent se faire de manière incrémentale, un <tt>pragma</tt> à la fois;  ceci est particulièrement utile pour le débogage puisqu'il est ainsi facile d'identifier le changement précis qui crée le bogue.
* Deuxièmement, OpenACC peut être désactivé au moment de la compilation;  les <tt>pragmas</tt> sont alors vus comme étant des commentaires et ne sont pas considérés par le compilateur, ce qui permet de compiler une version accélérée et une version normale à partir du même code source.
* Troisièmement, comme le compilateur fait tout le travail de transfert, le même code peut être compilé pour différents types d'accélérateurs, que ce soit un GPU ou des instructions SIMD sur un CPU;    ainsi, un changement du matériel exigera simplement la mise à jour du compilateur, sans modification au code.  


Le code de notre exemple contient deux boucles&nbsp;: la première initialise deux vecteurs et la seconde effectue une opération de [https://fr.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms niveau 1] d'addition des vecteurs.  
Le code de notre exemple contient deux boucles&nbsp;: la première initialise deux vecteurs et la seconde effectue une opération de [https://fr.wikipedia.org/wiki/Basic_Linear_Algebra_Subprograms niveau 1] d'addition des vecteurs.  


{| class="wikitable" width="100%"
{| class="wikitable" width="100%"
Line 49: Line 56:
</syntaxhighlight>
</syntaxhighlight>
|}
|}
Dans les deux cas, le compilateur identifie deux noyaux (''kernels''). En C/C++, les deux noyaux sont à l'intérieur de chaque boucle. En Fortran, les noyaux sont à l'intérieur de la première boucle et à l'intérieur de la boucle implicite effectuée lors d'une opération sur des tableaux.
Dans les deux cas, le compilateur identifie deux noyaux (''kernels'')&nbsp;:
* en C/C++, les deux noyaux sont à l'intérieur de chaque boucle.;
* en Fortran, les noyaux sont à l'intérieur de la première boucle et à l'intérieur de la boucle implicite effectuée lors d'une opération sur des tableaux.


Remarquez que le bloc OpenACC est délimité en C/C++ par des accolades; en Fortran, le commentaire est placé une fois au début et une dernière fois à la fin, avec l'ajout cette fois de <tt>end</tt>.
Remarquez que le bloc OpenACC est délimité en C/C++ par des accolades; en Fortran, le commentaire est placé une fois au début et une dernière fois à la fin, avec l'ajout cette fois de <tt>end</tt>.


=== Boucles et kernels ===
=== Boucles et noyaux ===
Quand le compilateur lit la directive OpenACC <tt>kernels</tt>, il analyse le code pour identifier les sections pouvant être parallélisées. Ceci correspond souvent au corps d'une boucle. Dans ce cas, le compilateur délimite le début et la fin du corps du code avec la fonction ''kernel''. Les appels à cette fonction ne seront pas affectés par les autres appels. La fonction est compilée et peut ensuite être exécutée sur un accélérateur. Comme chaque appel est indépendant, chacun des milliers de cœurs de l'accélérateur peut exécuter la fonction en parallèle pour un index spécifique.
 
Quand le compilateur lit la directive OpenACC <tt>kernels</tt>, il analyse le code pour identifier les sections pouvant être parallélisées.
Ceci correspond souvent au corps d'une boucle qui a des itérations indépendantes.
Dans ce cas, le compilateur délimite le début et la fin du corps du code avec la fonction [https://en.wikipedia.org/wiki/Compute_kernel ''kernel''].
Les appels à cette fonction ne seront pas affectés par les autres appels.
La fonction est compilée et peut ensuite être exécutée sur un accélérateur.
Comme chaque appel est indépendant, chacun des centaines de cœurs de l'accélérateur peut exécuter la fonction en parallèle pour un index spécifique.
 
{| class="wikitable" width="100%"
{| class="wikitable" width="100%"
|-
|-
Line 65: Line 81:
}
}
</syntaxhighlight> || <syntaxhighlight lang="cpp" line>
</syntaxhighlight> || <syntaxhighlight lang="cpp" line>
void loopBody(A,B,C,i)
void kernelName(A, B, C, i)
{
{
   C[i] = A[i] + B[i];
   C[i] = A[i] + B[i];
Line 71: Line 87:
</syntaxhighlight>
</syntaxhighlight>
|-
|-
|Calcule de 0-N, dans l'ordre || Chaque cœur de calcul traite une valeur de <tt>i</tt>.
| Calcule séquentiellement de <tt>i=0</tt> à <tt>i=N-1</tt>, inclusivement. || Chaque unité de calcul exécute la fonction pour une seule valeur de <tt>i</tt>.
|}
|}


== La directive <tt>kernels</tt> ==
== La directive <code>kernels</code> ==
Cette directive est dite ''descriptive''.  Le programmeur l'utilise pour signifier au compilateur les portions qui selon lui peuvent être parallélisées. Le compilateur fait ce qu'il veut de cette information et adopte la stratégie qui lui semble la meilleure pour exécuter le code, '''incluant''' son exécution séquencielle. De façon générale, le compilateur  
Cette directive est dite ''descriptive''.  Le programmeur l'utilise pour signifier au compilateur les portions qui selon lui peuvent être parallélisées. Le compilateur fait ce qu'il veut de cette information et adopte la stratégie qui lui semble la meilleure pour exécuter le code, '''incluant''' son exécution séquentielle. De façon générale, le compilateur  
# analyse le code pour détecter le parallélisme,
# analyse le code pour détecter le parallélisme,
# s'il détecte du parallélisme, identifie les données à transférer et décide quand faire le transfert,
# s'il détecte du parallélisme, identifie les données à transférer et décide quand faire le transfert,
Line 86: Line 102:
#pragma acc kernels
#pragma acc kernels
{
{
for (int i=0; i<N; i++)
  for (int i=0; i<N; i++)
{
  {
  C[i] = A[i] + B[i];
    C[i] = A[i] + B[i];
}
  }
}
}
</syntaxhighlight>  
</syntaxhighlight>  


Il est rare que du code soit aussi simple et il faut se baser sur la rétroaction du compilateur pour trouver les portions qu'il a négligé de paralléliser.  
Il est rare que le code soit aussi simple et il faut se baser sur la [[OpenACC_Tutorial_-_Profiling/fr#Renseignements_sur_le_compilateur| rétroaction du compilateur]] pour trouver les portions qu'il a négligé de paralléliser.  
 
{{Callout
{{Callout
|title=Description ou prescription
|title=Description ou prescription
|content=
|content=
Les utilisateurs d'[https://docs.computecanada.ca/wiki/OpenMP/fr OpenMP] retrouveront dans OpenACC le principe de directives. Il existe cependant d'importantes différences entre les directives OpenMP et OpenACC. Les directives OpenMP sont à la base ''prescriptives''. Ceci signifie que le compilateur est forcé d'accomplir la parallélisation, peu importe que l'effet détériore ou améliore la performance. Le résultat est prévisible pour tous les compilateurs. De plus, la parallélisation se fera de la même manière, peu importe le matériel utilisé pour exécuter le code. Par contre, le même code peut connaitre une moins bonne performance, dépendant de l'architecture. Il peut donc être préférable par exemple de changer l'ordre des boucles. Pour paralléliser du code avec OpenMP et obtenir une performance optimale dans différentes architectures, il faudrait avoir un ensemble différent de directives pour chaque architecture.  
Si vous avez déjà utilisé [[OpenMP/fr|OpenMP]], vous retrouverez dans OpenACC le principe de ''directives''. Il existe cependant d'importantes différences entre les directives OpenMP et OpenACC&nbsp;:
* Les directives OpenMP sont à la base ''prescriptives''. Ceci signifie que le compilateur est forcé d'accomplir la parallélisation, peu importe que l'effet détériore ou améliore la performance. Le résultat est prévisible pour tous les compilateurs. De plus, la parallélisation se fera de la même manière, peu importe le matériel utilisé pour exécuter le code. Par contre, le même code peut connaitre une moins bonne performance, dépendant de l'architecture. Il peut donc être préférable par exemple de changer l'ordre des boucles. Pour paralléliser du code avec OpenMP et obtenir une performance optimale dans différentes architectures, il faudrait avoir un ensemble différent de directives pour chaque architecture.  


Pour leur part, plusieurs directives OpenACC sont de nature ''descriptive''. Ici, le compilateur est libre de compiler le code de la façon qu'il juge la meilleure, selon l'architecture visée. Dans certains cas, le code ne sera pas parallélisé du tout. Le '''même code''' exécuté sur un GPU, sur un Xeon Phi ou sur un CPU peut donner du code binaire différent. Ceci signifie que la performance pourrait varier selon le compilateur  et que les compilateurs d'une nouvelle génération seront plus efficaces, surtout en présence de nouveau matériel.
* Pour leur part, plusieurs directives OpenACC sont de nature ''descriptive''. Ici, le compilateur est libre de compiler le code de la façon qu'il juge la meilleure, selon l'architecture visée. Dans certains cas, le code ne sera pas parallélisé du tout. Le '''même code''' exécuté sur un GPU ou sur un CPU peut donner du code binaire différent. Ceci signifie que la performance pourrait varier selon le compilateur  et que les compilateurs d'une nouvelle génération seront plus efficaces, surtout en présence de nouveau matériel.
}}
}}


=== Exemple : porter un produit matrice-vecteur ===  
=== Exemple : porter un produit matrice-vecteur ===  
Pour notre exemple, nous utilisons du code provenant du  [https://github.com/calculquebec/cq-formation-openacc répertoire Github], particulièrement une portion de code du fichier <tt>matrix_functions.h</tt>. Le code Fortran équivalent se trouve dans la sousroutine <tt>matvec</tt> contenue dans le fichier <tt>matrix.F90</tt>. Le code original est comme suit&nbsp;:
Pour notre exemple, nous utilisons du code provenant du  [https://github.com/calculquebec/cq-formation-openacc répertoire Github], particulièrement une portion de code [https://github.com/calculquebec/cq-formation-openacc/blob/main/cpp/matrix_functions.h#L20 fichier <code>cpp/matrix_functions.h</code>]. Le code Fortran équivalent se trouve dans la sous-routine [https://github.com/calculquebec/cq-formation-openacc/blob/main/f90/matrix.F90#L101 <code>matvec</code> contenue dans le fichier <code>matrix.F90</code>]. Le code C++ est comme suit&nbsp;:
<syntaxhighlight lang="cpp" line>
<syntaxhighlight lang="cpp" line start="29">
for(int i=0;i<num_rows;i++) {
  for(int i=0;i<num_rows;i++) {
  double sum=0;
    double sum=0;
  int row_start=row_offsets[i];
    int row_start=row_offsets[i];
  int row_end=row_offsets[i+1];
    int row_end=row_offsets[i+1];
  for(int j=row_start;j<row_end;j++) {
    for(int j=row_start;j<row_end;j++) {
    unsigned int Acol=cols[j];
      unsigned int Acol=cols[j];
    double Acoef=Acoefs[j];
      double Acoef=Acoefs[j];
    double xcoef=xcoefs[Acol];
      double xcoef=xcoefs[Acol];
    sum+=Acoef*xcoef;
      sum+=Acoef*xcoef;
    }
    ycoefs[i]=sum;
   }
   }
  ycoefs[i]=sum;
}
</syntaxhighlight>  
</syntaxhighlight>  


Le premier changement à faire au code est d'ajouter la directive <tt>kernels</tt>  pour essayer de le faire exécuter sur le GPU. Pour l'instant, nous n'avons pas à nous préoccuper du transfert des données ou à fournir des renseignements au compilateur.
Le [https://github.com/calculquebec/cq-formation-openacc/blob/main/cpp/step1.kernels/matrix_functions.h#L29  premier changement] à faire au code est d'ajouter la directive <tt>kernels</tt>  pour essayer de le faire exécuter sur le GPU.
<syntaxhighlight lang="cpp" line highlight="1,2,15">
Pour l'instant, nous n'avons pas à nous préoccuper du transfert des données ou à fournir des renseignements au compilateur.
 
<syntaxhighlight lang="cpp" line start="29" highlight="1,2,15">
#pragma acc kernels
#pragma acc kernels
   {
   {
Line 138: Line 158:
</syntaxhighlight>  
</syntaxhighlight>  


==== Développer avec OpenACC ====  
==== Construire avec OpenACC ====
Dans ce tutoriel, nous travaillons avec la version 16.3 des [http://www.pgroup.com/support/download_pgi2016.php?view=current compilateurs du Portland Group]. Nous utilisons l'option <tt>-ta</tt> (pour ''target accelerator'') pour porter le code sur les accélérateurs et la sous-option <tt>tesla:managed</tt> pour signifier au compilateur que les GPU Tesla sont la cible et que nous voulons utiliser une mémoire autogérée, ce qui simplifie le transfert de données dans les deux directions. Cette option ne sera pas utilisée dans un prochain exemple. Nous utilisons aussi l'option d'optimisation <tt>-fast</tt>.
 
En résultat, nous constatons que le compilateur n'a pu paralléliser les deux boucles;  nous verrons plus loin comment traiter ce cas.
Les compilateurs NVidia utilisent l'option <code>-acc</code> pour permettre la compilation pour un accélérateur. Nous utilisons la sous-option <code>-gpu=managed</code> pour indiquer au compilateur que nous voulons utiliser la [https://developer.nvidia.com/blog/unified-memory-cuda-beginners/ mémoire gérée]  pour simplifier le transfert de données en provenance et à destination du périphérique; nous n'utiliserons pas cette option dans un prochain exemple. Nous utilisons aussi l'option <code>-fast</code> pour l'optimisation.


{{Command
{{Command
|pgc++ -fast -Minfo{{=}}accel -ta{{=}}tesla:managed main.cpp -o challenge
|nvc++ -fast -Minfo{{=}}accel -acc -gpu{{=}}managed main.cpp -o challenge
|result=
|result=
...
...
matvec(const matrix &, const vector &, const vector &):
matvec(const matrix &, const vector &, const vector &):
     23, include "matrix_functions.h"
     23, include "matrix_functions.h"
           27, Generating copyout(ycoefs[:num_rows])
           30, Generating implicit copyin(cols[:],row_offsets[:num_rows+1],Acoefs[:]) [if not already present]
               Generating copyin(xcoefs[:],Acoefs[:],cols[:],row_offsets[:num_rows+1])
               Generating implicit copyout(ycoefs[:num_rows]) [if not already present]
           29, Complex loop carried dependence of row_offsets-> prevents parallelization
              Generating implicit copyin(xcoefs[:]) [if not already present]
              Loop carried dependence of ycoefs-> prevents parallelization
           31, Loop carried dependence of ycoefs-> prevents parallelization
               Loop carried backward dependence of ycoefs-> prevents vectorization
               Loop carried backward dependence of ycoefs-> prevents vectorization
               Complex loop carried dependence of cols->,Acoefs->,xcoefs-> prevents parallelization
               Complex loop carried dependence of Acoefs->,xcoefs-> prevents parallelization
               Accelerator kernel generated
               Generating NVIDIA GPU code
               Generating Tesla code
               31, #pragma acc loop seq
               33, #pragma acc loop vector(128) /* threadIdx.x */
               35, #pragma acc loop vector(128) /* threadIdx.x */
              37, Sum reduction generated for sum
                  Generating implicit reduction(+:sum)
           33, Loop is parallelizable
           35, Loop is parallelizable
}}
As we can see in the compiler output, the compiler could not parallelize the two loops. We will see in the following sections how to deal with this.
{{Callout
|title=Choix du compilateur
|content=
En date de mai 2016, relativement peu de compilateurs offraient les fonctionnalités d'OpenACC. Les plus avancés en ce sens sont les compilateurs du [http://www.pgroup.com/ Portland Group] de [http://www.nvidia.com/content/global/global.php NVidia] et ceux de [http://www.cray.com/ Cray]. Pour ce qui de [https://gcc.gnu.org/wiki/OpenACC GNU], l'implémentation d'OpenACC était expérimentale et devrait être complète dans la version 6.
 
Dans ce tutoriel, nous utilisons la version 16.3 des [http://www.pgroup.com/support/download_pgi2016.php?view=current compilateurs du Portland Group]  qui sont gratuits pour des fins de recherche universitaire.
}}
}}


Le résultat montre que la boucle externe sur la ligne 31 n'a pas pu être parallélisée par le compilateur. Dans la prochaine section, nous expliquons comment traiter ces dépendances.


== Réparer les fausses dépendances de boucles ==
== Réparer les fausses dépendances de boucles ==
Même lorsque le programmeur sait qu'une boucle peut être parallélisée, il arrive que le compilateur ne le remarque pas. Un cas commun en C/C++ est connu sous le nom de '[https://en.wikipedia.org/wiki/Pointer_aliasing ''pointer aliasing'']. Contrairement au Fortran, C/C++ ne possèdent pas comme tel de tableaux (''arrays''), mais plutôt des pointeurs. Le concept d'alias s'applique à deux pointeurs dirigés vers la même mémoire. Si le compilateur ne sait pas que des pointeurs ne sont pas des alias, il doit cependant le supposer. Dans l'exemple précédent, on voit clairement pourquoi le compilateur ne pouvait pas paralléliser la boucle. En supposant que les pointeurs sont identiques, il y a forcément dépendance des itérations de la boucle.  
Même lorsque le programmeur sait qu'une boucle peut être parallélisée, il arrive que le compilateur ne le remarque pas. Un cas commun en C/C++ est connu sous le nom de [https://en.wikipedia.org/wiki/Pointer_aliasing ''pointer aliasing'']. Contrairement au Fortran, C/C++ ne possèdent pas comme tel de tableaux (''arrays''), mais plutôt des pointeurs. Le concept d'alias s'applique à deux pointeurs dirigés vers la même mémoire. Si le compilateur ne sait pas que des pointeurs ne sont pas des alias, il doit cependant le supposer. Dans l'exemple précédent, on voit clairement pourquoi le compilateur ne pouvait pas paralléliser la boucle. En supposant que les pointeurs sont identiques, il y a forcément dépendance des itérations de la boucle.  


===Mot-clé <tt>restrict</tt> ===  
===Mot-clé <tt>restrict</tt> ===  
Une des manières de dire au compilateur que les pointeurs '''ne sont pas''' des alias est d'utiliser  le mot-clé  <tt>restrict</tt>, introduit à cette fin dans C99.  Il n'y a toujours pas de manière standard pour ce faire en C++, mais chaque compilateur possède un mot-clé qui lui est propre. Dépendant du compilateur, on peut utiliser <tt>__restrict</tt> ou <tt>__restrict__</tt>. Les compilateurs du Portland Group utilisent <tt>__restrict</tt>. Pour savoir pourquoi il n'existe pas de standard en C++, consultez [http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3988.pdf ce document]. Ce concept est important pour OpenACC comme pour toute programmation C/C++, car les compilateurs peuvent effectuer plusieurs autres optimisations si les pointeurs ne sont pas des alias. Remarquez que le mot-clé se place '''après''' le pointeur puisque c'est à ce dernier qu'il se réfère, et non au type; autrement dit, la déclaration doit se lire <code>float * __restrict A;</code> plutôt que <code>float __restrict * A;</code>.  
Une des manières de dire au compilateur que les pointeurs '''ne sont pas''' des alias est d'utiliser  le mot-clé  <tt>restrict</tt>, introduit à cette fin dans C99.  Il n'y a toujours pas de manière standard pour ce faire en C++, mais chaque compilateur possède un mot-clé qui lui est propre. Dépendant du compilateur, on peut utiliser <tt>__restrict</tt> ou <tt>__restrict__</tt>. Les compilateurs du Portland Group et de NVidia utilisent <tt>__restrict</tt>. Pour savoir pourquoi il n'existe pas de standard en C++, consultez [http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n3988.pdf ce document]. Ce concept est important pour OpenACC comme pour toute programmation C/C++, car les compilateurs peuvent effectuer plusieurs autres optimisations si les pointeurs ne sont pas des alias. Remarquez que le mot-clé se place '''après''' le pointeur puisque c'est à ce dernier qu'il se réfère, et non au type; autrement dit, la déclaration doit se lire <code>float * __restrict A;</code> plutôt que <code>float __restrict * A;</code>.  




Line 180: Line 193:
|title=Utilisation du mot-clé <tt>restrict</tt>
|title=Utilisation du mot-clé <tt>restrict</tt>
|content=
|content=
En déclarant un pointeur comme étant ''restraint'', on s'assure qu'uniquement ce pointeur ou une valeur dérivée (comme <tt>ptr +1</tt>) pourra accéder à l'objet auquel il réfère, et ce pour la durée de vie du pointeur. Ceci est une garantie que le programmeur donne au compilateur;  si le programmeur manque à son obligation, le comportement n'est pas défini. Pour plus d'information, consultez l'article Wikipédia [https://en.wikipedia.org/wiki/Fstab restrict].  
En déclarant un pointeur comme étant ''restreint'', on s'assure qu'uniquement ce pointeur ou une valeur dérivée (comme <tt>ptr +1</tt>) pourra accéder à l'objet auquel il réfère, et ce pour la durée de vie du pointeur. Ceci est une garantie que le programmeur donne au compilateur;  si le programmeur manque à son obligation, le comportement n'est pas défini. Pour plus d'information, consultez l'article Wikipédia [https://en.wikipedia.org/wiki/Fstab restrict].  
}}
}}


=== Boucle avec clause <tt>independent</tt> ===  
=== Boucle avec clause <tt>independent</tt> ===  
Une autre façon de s'assurer que le compilateur traite les boucles de manière indépendante est de le spécifier explicitement  avec la clause <tt>independent</tt>. Comme toute autre directive ''prescriptive'', le compilateur y est obligé et l'analyse qu'il pourrait faire ne sera pas considérée. En reprenant l'exemple de la section ''La directive <tt>kernels</tt>'', nous avons&nbsp;: :
Une autre façon de s'assurer que le compilateur traite les boucles de manière indépendante est de le spécifier explicitement  avec la clause <tt>independent</tt>. Comme toute autre directive ''prescriptive'', le compilateur y est obligé et l'analyse qu'il pourrait faire ne sera pas considérée. En reprenant l'exemple de la section ''La directive <tt>kernels</tt>'' ci-dessus, nous avons&nbsp;:
<syntaxhighlight lang="cpp" line highlight="3">
<syntaxhighlight lang="cpp" line highlight="3">
#pragma acc kernels
#pragma acc kernels
Line 197: Line 211:


=== Produit matrice-vecteur ===  
=== Produit matrice-vecteur ===  
Revenons au cas du produit matrice-vecteur présenté plus haut. Notre recommandation pour éviter les faux alias est de définir les pointeurs comme étant restreints en remplaçant le code de <tt>matrix_functions.h</tt>.  
Revenons au cas du produit matrice-vecteur présenté plus haut. Notre recommandation pour éviter les faux alias est de définir les pointeurs comme étant restreints en remplaçant le code de <tt>matrix_functions.h</tt>.  
<syntaxhighlight lang="cpp" line>
<syntaxhighlight lang="cpp" line>
   double *Acoefs=A.coefs;
   double *Acoefs=A.coefs;
Line 211: Line 225:
Remarquez que les autres pointeurs n'ont pas besoin d'être restreints puisque le compilateur ne les rapporte pas comme causant des problèmes. En recompilant avec les changements que nous venons de faire, le compilateur émet le message suivant&nbsp;:  
Remarquez que les autres pointeurs n'ont pas besoin d'être restreints puisque le compilateur ne les rapporte pas comme causant des problèmes. En recompilant avec les changements que nous venons de faire, le compilateur émet le message suivant&nbsp;:  
{{Command
{{Command
|pgc++ -fast -Minfo{{=}}accel -ta{{=}}tesla:managed main.cpp -o challenge
|nvc++ -fast -Minfo{{=}}accel -acc -gpu{{=}}managed main.cpp -o challenge
|result=
|result=
matvec(const matrix &, const vector &, const vector &):
matvec(const matrix &, const vector &, const vector &):
     23, include "matrix_functions.h"
     23, include "matrix_functions.h"
           27, Generating copyout(ycoefs[:num_rows])
           27, Generating implicit copyout(ycoefs[:num_rows]) [if not already present]
               Generating copyin(xcoefs[:],Acoefs[:],cols[:],row_offsets[:num_rows+1])
               Generating implicit copyin(xcoefs[:],row_offsets[:num_rows+1],Acoefs[:],cols[:]) [if not already present]
           29, Loop is parallelizable
           30, Loop is parallelizable
              Accelerator kernel generated
               Generating Tesla code
               Generating Tesla code
               29, #pragma acc loop gang, vector(128) /* blockIdx.x threadIdx.x */
               30, #pragma acc loop gang /* blockIdx.x */
           33, Loop is parallelizable
              34, #pragma acc loop vector(128) /* threadIdx.x */
                  Generating implicit reduction(+:sum)
           34, Loop is parallelizable
}}
}}


== Performance du code porté ==  
== Performance du code porté ==  
Maintenant que le code est porté sur le GPU, nous pouvons analyser sa performance et vérifier si les résultats sont corrects. L'exécution du code original sur un des nœuds GPU de Guillimin produit ceci&nbsp;:
Maintenant que le code est porté sur le GPU, nous pouvons analyser sa performance et vérifier si les résultats sont corrects. L'exécution du code original sur un nœud GPU produit ceci&nbsp;:
{{Command
{{Command
|./cg.x  
|./cg.x  
Line 260: Line 275:
}}
}}
[[File:Openacc profiling1.png|thumbnail|Cliquez pour agrandir.]]
[[File:Openacc profiling1.png|thumbnail|Cliquez pour agrandir.]]
Les résultats sont corrects,  toutefois, loin de gagner en vitesse, l'opération a pris près de quatre fois plus de temps! Utilisons le NVIDIA Visual Profiler (<tt>nvvp</tt>) pour voir ce qui se passe.   
Les résultats sont corrects,  toutefois, loin de gagner en vitesse, l'opération a pris près de quatre fois plus de temps! Utilisons le NVidia Visual Profiler (<tt>nvvp</tt>) pour voir ce qui se passe.
# Démarrez  <tt>nvvp</tt> avec la commande <tt>nvvp &</tt> , où le symbole <tt>&</tt> permet de démarrer en arrière-plan.
# Sélectionnez ''File -> New Session''.
# Dans le champ "File:", cherchez l'exécutable; dans notre exemple, nous utilisons  <tt>challenge</tt> .
# Cliquez sur "Next" jusqu'à ce que vous puissiez cliquer sur "Finish".  


Le programme est exécuté et on obtient un tableau chronologique du déroulement (voir l'image).     On remarque que le transfert de données entre le départ et l'arrivée occupe la plus grande partie du temps d'exécution, ce qui est fréquent quand du code est porté d'un CPU vers un GPU. Nous verrons comment ceci peut être amélioré dans la prochaine partie, [https://docs.computecanada.ca/wiki/OpenACC_Tutorial_-_Data_movement/fr Mouvement des données].
=== NVIDIA Visual Profiler ===
[[File:Nvvp-pic0.png|thumbnail|300px|Profileur NVVP|right]]
[[File:Nvvp-pic1.png|thumbnail|300px|Recherche de l'exécutable que vous voulez profiler|right]]
 
[https://developer.nvidia.com/nvidia-visual-profiler NVIDIA Visual Profiler (NVVP)] est un profileur graphique pour les applications OpenACC.
C'est un outil d'analyse pour les '''codes écrits avec les directives OpenACC et CUDA C/C++'''.
En conséquence, si l'exécutable n'utilise pas le GPU, ce profileur ne fournira aucun résultat.
 
Quand [[Visualization#Fenêtres_à_distance_avec_redirection_X11|X11 est redirigé vers un serveur X-Server]] ou quand vous utilisez un [[VNC/fr|environnement bureau Linux]] (aussi via [[JupyterHub/fr#Bureau|JupyterHub]] avec 2 cœurs CPU, 5000M de mémoire et 1 GPU),
vous pouvez lancer NVVP à partir d'un terminal&nbsp;:
{{Command
|module load cuda/11.7 java/1.8
}}
{{Command
|nvvp
}}
 
# Après l'affichage de la fenêtre de lancement de NVVP, vous devez entrer le répertoire ''Workspace'' qui sera employé pour les fichiers temporaires. Dans le chemin suggéré, remplacez <code>home</code> par <code>scratch</code> et cliquez sur ''OK''.
# Sélectionnez ''File > New Session'' ou cliquez sur le bouton correspondant dans la barre d'outils.
# Cliquez sur le bouton ''Browse'' à la droite du champ ''File'' pour le chemin.
## Changez le répertoire s'il y a lieu.
## Sélectionnez un exécutable construit avec des codes écrits avec des directives OpenACC et CUDA C/C++.
# Sous le champ ''Arguments'', sélectionnez l'option ''Profile current process only''.
# Cliquez sur ''Next >'' pour voir les autres options de profilage.
# Cliquez sur ''Finish'' pour lancer le profilage de l'exécutable.
 
Pour faire ceci, suivez ces étapes&nbsp;:
# Lancez <tt>nvvp</tt> avec la commande <tt>nvvp &</tt>  (le symbole <tt>&</tt> commande le lancement en arrière-plan).
# Sélectionnez '' File -> New Session''.
# Dans le champ ''File:'', cherchez l'exécutable (nommé dans notre exemple <tt>challenge</tt>).
# Cliquez sur ''Next'' jusqu'à ce que vous puissiez cliquer sur ''Finish''.
 
Le programme est exécuté et on obtient un tableau chronologique du déroulement (voir l'image). On remarque que le transfert de données entre le départ et l'arrivée occupe la plus grande partie du temps d'exécution, ce qui est fréquent quand du code est porté d'un CPU vers un GPU. Nous verrons comment ceci peut être amélioré dans la prochaine partie, [[OpenACC Tutorial - Data movement/fr|Mouvement des données]].


== La directive <tt>parallel loop</tt> ==  
== La directive <tt>parallel loop</tt> ==  
Avec la directive <tt>kernels</tt>, c'est le compilateur qui fait toute l'analyse; ceci est une approche ''descriptive'' pour porter du code. OpenACC offre aussi une approche ''prescriptive'' avec la directive <tt>parallel</tt> qui peut être combinée à la directive <tt>loop</tt> ainsi&nbsp;:
Avec la directive <tt>kernels</tt>, c'est le compilateur qui fait toute l'analyse; ceci est une approche ''descriptive'' pour porter du code. OpenACC offre aussi une approche ''prescriptive'' avec la directive <tt>parallel</tt> qui peut être combinée à la directive <tt>loop</tt> ainsi&nbsp;:
<syntaxhighlight lang="cpp" line highlight="1">
<syntaxhighlight lang="cpp" line highlight="1">
#pragma acc parallel loop
#pragma acc parallel loop
Line 281: Line 324:
Pour utiliser cette directive dans notre exemple du produit matrice-vecteur, nous avons besoin des clauses <tt>private</tt> et <tt>reduction</tt> pour gérer le flux des données dans la zone parallèle.   
Pour utiliser cette directive dans notre exemple du produit matrice-vecteur, nous avons besoin des clauses <tt>private</tt> et <tt>reduction</tt> pour gérer le flux des données dans la zone parallèle.   
* Avec la clause <tt>private</tt>, une copie de la variable est faite pour chaque itération de la boucle; la valeur de la variable est ainsi indépendante des autres itérations.  
* Avec la clause <tt>private</tt>, une copie de la variable est faite pour chaque itération de la boucle; la valeur de la variable est ainsi indépendante des autres itérations.  
* Avec la clause <tt>reduction</tt> , les valeurs de la variable dans chaque itération est ''réduite'' à une valeur unique.  La clause s'utilise entre autres avec les opérations addition (+), multiplication (*), maximum (max) et minimum (min).  
* Avec la clause <tt>reduction</tt>, les valeurs de la variable dans chaque itération est ''réduite'' à une valeur unique.  La clause s'utilise entre autres avec les opérations addition (+), multiplication (*), maximum (max) et minimum (min).  
Ces clauses ne sont pas nécessaires avec la directive <tt>kernels</tt> puisque celle-ci fait le travail pour vous.  
Ces clauses ne sont pas nécessaires avec la directive <tt>kernels</tt> puisque celle-ci fait le travail pour vous.  


Line 301: Line 344:
   }
   }
</syntaxhighlight>
</syntaxhighlight>
Compiling this code yields the following compiler feedback:
La compilation produit le message suivant&nbsp;:
{{Command
{{Command
|pgc++ -fast -Minfo{{=}}accel -ta{{=}}tesla:managed main.cpp -o challenge
|nvc++ -fast -Minfo{{=}}accel -acc -gpu{{=}}managed main.cpp -o challenge
|result=
|result=
matvec(const matrix &, const vector &, const vector &):
matvec(const matrix &, const vector &, const vector &):
Line 317: Line 360:
}}
}}


== Parallel loop vs kernel ==
==Différences entre <tt>parallel loop</tt> et <tt>kernels</tt>==
{| class="wikitable" width="100%"
{| class="wikitable" width="100%"
|-
|-
! PARALLEL LOOP  !! KERNEL
! PARALLEL LOOP  !! KERNELS
|-
|-
|
|
   
   
* It is the programmer's responsibility to ensure that parallelism is safe
* l'intégrité du code parallélisé revient au programmeur
* Enables parallelization of sections that the compiler may miss
* le programmeur voit la parallélisation que le compilateur pourrait manquer
* Straightforward path from OpenMP
* le fonctionnement est identique en OpenMP
||  
||  
* It is the compiler's responsibility to analyze the code and determine what is safe to parallelize.
* la responsabilité d'analyser le code et de garantir son intégrité revient au compilateur
* A single directive can cover a large area of code
* une seule directive peut s'appliquer à une grande portion de code  
* The compiler has more room to optimize
* le compilateur est libre d'optimiser le code
|}
|}
Both approaches are equally valid and can perform equally well.
Les deux approches sont valides et leur performance est comparable.


  {{Challenge
   
|title=Challenge: Add OpenACC directives <tt>kernels</tt> or <tt>parallel loop</tt>  
{{Challenge
|title=Exercice : utiliser <tt>kernels</tt> ou <tt>parallel loop</tt>  
|content=
|content=
# Modify the functions <tt>matvec</tt>, <tt>waxpby</tt> and <tt>dot</tt> to use OpenACC. You may use either the <tt>kernels</tt> or the <tt>parallel loop</tt> directives. The directories <tt>step1.*</tt> contain the solution.  
# Modifiez les fonctions <tt>matvec</tt>, <tt>waxpby</tt> et <tt>dot</tt>. Vous pouvez utiliser soit <tt>kernels</tt>, soit <tt>parallel loop</tt>. La solution se trouve dans les répertoires <tt>step1. de [https://github.com/calculquebec/cq-formation-openacc Github] *</tt>.  
# Modify the Makefile to add <tt>-ta{{=}}tesla:managed</tt> and <tt>-Minfo{{=}}accel</tt> to your compiler flags.  
# Modifiez le Makefile en ajoutant <tt>-acc -gpu{{=}}managed</tt> et <tt>-Minfo{{=}}accel</tt> aux indicateurs pour le compilateur.  
}}
}}


[[OpenACC Tutorial - Data movement|Onward to the next unit: Data movement]]<br>
[[OpenACC Tutorial - Profiling/fr|<- Page précédente, Profileurs]] | [[OpenACC Tutorial/fr|^- Retour au début du tutoriel]] | [[OpenACC Tutorial - Data movement/fr|Page suivante, Mouvement des données ->]]
[[OpenACC Tutorial|Back to the lesson plan]]
38,760

edits