{{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 \mbox{intensité computationnelle} = \frac{\mbox{opérations de calcul}}{\mbox{opérations en mémoire}} 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'' ->]]