{{Objectives
|title=Objectifs d'apprentissage
|content=
* comprendre ce qu'est un profileur
* savoir utiliser le profileur NVPROF
* comprendre la performance du code
* savoir concentrer vos efforts et réécrire les routines qui exigent beaucoup de temps
}}
== Profiler du code ==
Pourquoi auriez-vous besoin de profiler du code? Parce que c'est la seule façon de comprendre
* comment le temps est employé aux points critiques (''hotspots''),
* comprendre la performance du code,
* savoir comment mieux employer votre temps de développement.
Pourquoi est-ce important de connaitre les points critiques dans le code?
D'après [https://en.wikipedia.org/wiki/Amdahl%27s_law la loi d'Amdahl], paralléliser les routines qui exigent le plus de temps d'exécution (les points critiques) produit le plus d'impact.
== Préparer le code pour l'exercice ==
Pour l'exemple suivant, nous utilisons du code provenant de [https://github.com/calculquebec/cq-formation-ce dépôt de données Git].
[https://github.com/calculquebec/cq-formation-openacc/archive/refs/heads/main.zip Téléchargez et faites l'extraction du paquet] et positionnez-vous dans le répertoire cpp
ou f90
. Le but de cet exemple est de compiler et lier le code pour obtenir un exécutable pour en profiler le code source avec un profileur.
{{Callout
|title=Choix du compilateur
|content=
Mis de l'avant par [https://www.cray.com/ Cray] et par [https://www.nvidia.com NVIDIA] via sa division
[https://www.pgroup.com/support/release_archive.php Portland Group] jusqu'en 2020 puis via [https://developer.nvidia.com/hpc-sdk sa trousse HPC SDK], ceux deux types de compilateurs offrent le support le plus avancé pour OpenACC.
Quant aux [https://gcc.gnu.org/wiki/OpenACC compilateurs GNU], le support pour OpenACC 2.x continue de s'améliorer depuis la version 6 de GCC.
En date de juillet 2022, les versions GCC 10, 11 et 12 supportent la version 2.6 d'OpenACC.
Dans ce tutoriel, nous utilisons la version 22.7 de [https://developer.nvidia.com/nvidia-hpc-sdk-releases NVIDIA HPC SDK]. Notez que les compilateurs NVIDIA sont gratuits à des fins de recherche universitaire.
}}
{{Command
|module load nvhpc/22.7
|result=
Lmod is automatically replacing "intel/2020.1.217" with "nvhpc/22.7".
The following have been reloaded with a version change:
1) gcccore/.9.3.0 => gcccore/.11.3.0 3) openmpi/4.0.3 => openmpi/4.1.4
2) libfabric/1.10.1 => libfabric/1.15.1 4) ucx/1.8.0 => ucx/1.12.1
}}
{{Command
|make
|result=
nvc++ -c -o main.o main.cpp
nvc++ main.o -o cg.x
}}
Une fois l'exécutable cg.x
créé, nous allons profiler son code source. Le profileur mesure les appels des fonctions en exécutant et en surveillant ce programme.
'''Important :''' Cet exécutable utilise environ 3Go de mémoire et un cœur CPU presque à 100 %. '''L'environnement de test devrait donc avoir 4Go de mémoire disponible et au moins deux (2) cœurs CPU'''.
{{Callout
|title=Choix du profileur
|content=
Dans ce tutoriel, nous utilisons deux profileurs :
* '''[https://docs.nvidia.com/cuda/profiler-users-guide/ nvprof
de NVIDIA]''' , un profileur en ligne de commande capable d'analyser des codes non GPU
* '''[[OpenACC_Tutorial_-_Adding_directives#NVIDIA_Visual_Profiler|nvvp
(NVIDIA Visual Profiler) ]]''', un outil d'analyse multiplateforme pour des programmes écrits avec OpenACC et CUDA C/C++.
Puisque cg.x
que nous avons construit n'utilise pas encore le GPU, nous allons commencer l'analyse avec le profileur nvprof
.
}}
=== Profileur en ligne de commande NVIDIA nvprof
===
Dans sa trousse de développement pour le calcul de haute performance, NVIDIA fournit habituellement nvprof
, mais la version qu'il faut utiliser sur nos grappes est incluse dans un module CUDA.
{{Command
|module load cuda/11.7
}}
Pour profiler un exécutable CPU pur, nous devons ajouter les arguments --cpu-profiling on
à la ligne de commande.
{{Command
|nvprof --cpu-profiling on ./cg.x
|result=
...
...
======== CPU profiling result (bottom up):
Time(%) Time Name
83.54% 90.6757s matvec(matrix const &, vector const &, vector const &)
83.54% 90.6757s {{!}} main
7.94% 8.62146s waxpby(double, vector const &, double, vector const &, vector const &)
7.94% 8.62146s {{!}} main
5.86% 6.36584s dot(vector const &, vector const &)
5.86% 6.36584s {{!}} main
2.47% 2.67666s allocate_3d_poisson_matrix(matrix&, int)
2.47% 2.67666s {{!}} main
0.13% 140.35ms initialize_vector(vector&, double)
0.13% 140.35ms {{!}} main
...
======== Data collected at 100Hz frequency
}}
Dans le résultat, la fonction matvec()
utilise 83.5 % du temps d'exécution; son appel se trouve dans la fonction main()
.
==Renseignements sur le compilateur==
Avant de travailler sur la routine, nous devons comprendre ce que fait le compilateur; posons-nous les questions suivantes :
* Quelles sont les optimisations qui ont été automatiquement appliquées par le compilateur?
* Qu'est-ce qui a empêché d'optimiser davantage?
* La performance serait-elle affectée par les petites modifications?
Le compilateur NVIDIA offre l'indicateur -Minfo
avec les options suivantes :
* all
, pour imprimer presque tous les types d'information, incluant
** accel
pour les opérations du compilateur en rapport avec l'accélérateur
** inline
pour l'information sur les fonctions extraites et alignées
** loop,mp,par,stdpar,vect
pour les renseignements sur l'optimisation et la vectorisation des boucles
* intensity
, pour imprimer l'information sur l'intensité des boucles
* (aucune option) produit le même résultat que l'option all
, mais sans l'information fournie par inline
.
== Obtenir les renseignements sur le compilateur ==
* Modifiez le Makefile.
CXX=nvc++
CXXFLAGS=-fast -Minfo=all,intensity
LDFLAGS=${CXXFLAGS}
* Effectuez un nouveau build.
{{Command
|make clean; make
|result=
...
nvc++ -fast -Minfo=all,intensity -c -o main.o main.cpp
initialize_vector(vector &, double):
20, include "vector.h"
36, Intensity = 0.0
Memory set idiom, loop replaced by call to __c_mset8
dot(const vector &, const vector &):
21, include "vector_functions.h"
27, Intensity = 1.00
Generated vector simd code for the loop containing reductions
28, FMA (fused multiply-add) instruction(s) generated
waxpby(double, const vector &, double, const vector &, const vector &):
21, include "vector_functions.h"
39, Intensity = 1.00
Loop not vectorized: data dependency
Generated vector simd code for the loop
Loop unrolled 2 times
FMA (fused multiply-add) instruction(s) generated
40, FMA (fused multiply-add) instruction(s) generated
allocate_3d_poisson_matrix(matrix &, int):
22, include "matrix.h"
43, Intensity = 0.0
Loop not fused: different loop trip count
44, Intensity = 0.0
Loop not vectorized/parallelized: loop count too small
45, Intensity = 0.0
Loop unrolled 3 times (completely unrolled)
57, Intensity = 0.0
59, Intensity = 0.0
Loop not vectorized: data dependency
matvec(const matrix &, const vector &, const vector &):
23, include "matrix_functions.h"
29, Intensity = (num_rows*((row_end-row_start)* 2))/(num_rows+(num_rows+(num_rows+((row_end-row_start)+(row_end-row_start)))))
33, Intensity = 1.00
Generated vector simd code for the loop containing reductions
37, FMA (fused multiply-add) instruction(s) generated
main:
38, allocate_3d_poisson_matrix(matrix &, int) inlined, size=41 (inline) file main.cpp (29)
43, Intensity = 0.0
Loop not fused: different loop trip count
44, Intensity = 0.0
Loop not vectorized/parallelized: loop count too small
45, Intensity = 0.0
Loop unrolled 3 times (completely unrolled)
57, Intensity = 0.0
Loop not fused: function call before adjacent loop
59, Intensity = 0.0
Loop not vectorized: data dependency
42, allocate_vector(vector &, unsigned int) inlined, size=3 (inline) file main.cpp (24)
43, allocate_vector(vector &, unsigned int) inlined, size=3 (inline) file main.cpp (24)
44, allocate_vector(vector &, unsigned int) inlined, size=3 (inline) file main.cpp (24)
45, allocate_vector(vector &, unsigned int) inlined, size=3 (inline) file main.cpp (24)
46, allocate_vector(vector &, unsigned int) inlined, size=3 (inline) file main.cpp (24)
48, initialize_vector(vector &, double) inlined, size=5 (inline) file main.cpp (34)
36, Intensity = 0.0
Memory set idiom, loop replaced by call to __c_mset8
49, initialize_vector(vector &, double) inlined, size=5 (inline) file main.cpp (34)
36, Intensity = 0.0
Memory set idiom, loop replaced by call to __c_mset8
52, waxpby(double, const vector &, double, const vector &, const vector &) inlined, size=10 (inline) file main.cpp (33)
39, Intensity = 0.0
Memory copy idiom, loop replaced by call to __c_mcopy8
53, matvec(const matrix &, const vector &, const vector &) inlined, size=19 (inline) file main.cpp (20)
29, Intensity = [symbolic], and not printable, try the -Mpfi -Mpfo options
Loop not fused: different loop trip count
33, Intensity = 1.00
Generated vector simd code for the loop containing reductions
54, waxpby(double, const vector &, double, const vector &, const vector &) inlined, size=10 (inline) file main.cpp (33)
27, FMA (fused multiply-add) instruction(s) generated
36, FMA (fused multiply-add) instruction(s) generated
39, Intensity = 0.67
Loop not fused: different loop trip count
Loop not vectorized: data dependency
Generated vector simd code for the loop
Loop unrolled 4 times
FMA (fused multiply-add) instruction(s) generated
56, dot(const vector &, const vector &) inlined, size=9 (inline) file main.cpp (21)
27, Intensity = 1.00
Loop not fused: function call before adjacent loop
Generated vector simd code for the loop containing reductions
61, Intensity = 0.0
62, waxpby(double, const vector &, double, const vector &, const vector &) inlined, size=10 (inline) file main.cpp (33)
39, Intensity = 0.0
Memory copy idiom, loop replaced by call to __c_mcopy8
65, dot(const vector &, const vector &) inlined, size=9 (inline) file main.cpp (21)
27, Intensity = 1.00
Loop not fused: different controlling conditions
Generated vector simd code for the loop containing reductions
67, waxpby(double, const vector &, double, const vector &, const vector &) inlined, size=10 (inline) file main.cpp (33)
39, Intensity = 0.67
Loop not fused: different loop trip count
Loop not vectorized: data dependency
Generated vector simd code for the loop
Loop unrolled 4 times
72, matvec(const matrix &, const vector &, const vector &) inlined, size=19 (inline) file main.cpp (20)
29, Intensity = [symbolic], and not printable, try the -Mpfi -Mpfo options
Loop not fused: different loop trip count
33, Intensity = 1.00
Generated vector simd code for the loop containing reductions
73, dot(const vector &, const vector &) inlined, size=9 (inline) file main.cpp (21)
27, Intensity = 1.00
Loop not fused: different loop trip count
Generated vector simd code for the loop containing reductions
77, waxpby(double, const vector &, double, const vector &, const vector &) inlined, size=10 (inline) file main.cpp (33)
39, Intensity = 0.67
Loop not fused: different loop trip count
Loop not vectorized: data dependency
Generated vector simd code for the loop
Loop unrolled 4 times
78, waxpby(double, const vector &, double, const vector &, const vector &) inlined, size=10 (inline) file main.cpp (33)
39, Intensity = 0.67
Loop not fused: function call before adjacent loop
Loop not vectorized: data dependency
Generated vector simd code for the loop
Loop unrolled 4 times
88, free_vector(vector &) inlined, size=2 (inline) file main.cpp (29)
89, free_vector(vector &) inlined, size=2 (inline) file main.cpp (29)
90, free_vector(vector &) inlined, size=2 (inline) file main.cpp (29)
91, free_vector(vector &) inlined, size=2 (inline) file main.cpp (29)
92, free_matrix(matrix &) inlined, size=5 (inline) file main.cpp (73)
}}
== Interpréter le résultat ==
L'''intensité computationnelle'' d'une boucle représente la quantité de travail accompli par la boucle en fonction des opérations effectuées en mémoire, soit
Dans le résultat, une valeur supérieure à 1 pour Intensity
indique que la boucle serait bien exécutée sur un processeur graphique (GPU).
== Comprendre le code ==
Regardons attentivement la boucle principale de
[https://github.com/calculquebec/cq-formation-openacc/blob/main/cpp/matrix_functions.h#L29 la fonction matvec()
implémentée dans matrix_functions.h
]:
for(int i=0;i
On trouvera les dépendances de données en se posant les questions suivantes :
* Une itération en affecte-t-elle d'autres?
** par exemple, quand une '''[https://fr.wikipedia.org/wiki/Suite_de_Fibonacci suite de Fibonacci]''' est générée, chaque nouvelle valeur dépend des deux valeurs qui la précèdent. Il est donc très difficile, sinon impossible, d'implémenter un parallélisme efficace.
* L'accumulation des valeurs dans sum
est-elle une dépendance?
** Non, c'est une''' [https://en.wikipedia.org/wiki/Reduction_operator réduction]'''! Et les compilateurs modernes optimisent bien ce genre de réduction.
* Est-ce que les itérations de boucle écrivent et lisent dans les mêmes vecteurs de sorte que les valeurs sont utilisées ou écrasées par d'autres itérations?
** Heureusement, ceci ne se produit pas dans le code ci-dessus.
Maintenant que le code est analysé, nous pouvons ajouter des directives au compilateur.
[[OpenACC Tutorial - Introduction/fr|<- Page précédente, ''Introduction'']] | [[OpenACC Tutorial/fr|^- Retour au début du tutoriel]] | [[OpenACC Tutorial - Adding directives/fr|Page suivante, ''Ajouter des directives'' ->]]