3.3.6 : Performances



Lançons notre premier tests de performance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
make plot_hadamardCuda
-- Found headers CUDA : /usr/local/cuda/include
-- Found lib CUDA : /usr/local/cuda/lib64/libcudart_static.a;-lpthread;dl;/usr/lib/x86_64-linux-gnu/librt.so
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pierre/projects/COURS/PERFORMANCE_WITH_STENCIL_GPU/Examples/HadamardProductCuda/build
[ 33%] Built target asterics_hpc_cuda
[ 41%] Linking CXX shared library libhadamard_product_cuda.so
[ 58%] Built target hadamard_product_cuda
[ 66%] Linking CXX executable hadamard_product_gpu_cuda
[ 75%] Built target hadamard_product_gpu_cuda
Scanning dependencies of target run_hadamard_product_gpu_cuda
[ 83%] Run hadamard_product_gpu_cuda program
Hadamard product
asterics_getNbCudaDevice : Detected 1 CUDA Capable device(s)
evaluateHadamardProduct : nbElement = 1000, cyclePerElement = 1988.27 cy/el, elapsedTime = 1988268 cy, res = 0
evaluateHadamardProduct : nbElement = 2000, cyclePerElement = 885.846 cy/el, elapsedTime = 1771692 cy, res = 0
evaluateHadamardProduct : nbElement = 3000, cyclePerElement = 592.623 cy/el, elapsedTime = 1777868 cy, res = 0
evaluateHadamardProduct : nbElement = 5000, cyclePerElement = 362.2 cy/el, elapsedTime = 1810998 cy, res = 0
evaluateHadamardProduct : nbElement = 10000, cyclePerElement = 186.35 cy/el, elapsedTime = 1863500 cy, res = 0
evaluateHadamardProduct : nbElement = 20000, cyclePerElement = 99.0022 cy/el, elapsedTime = 1980044 cy, res = 0
evaluateHadamardProduct : nbElement = 50000, cyclePerElement = 46.2507 cy/el, elapsedTime = 2312537 cy, res = 0
evaluateHadamardProduct : nbElement = 100000, cyclePerElement = 43.342 cy/el, elapsedTime = 4334204 cy, res = 0
evaluateHadamardProduct : nbElement = 500000, cyclePerElement = 19.0436 cy/el, elapsedTime = 9521781 cy, res = 0
evaluateHadamardProduct : nbElement = 1000000, cyclePerElement = 15.021 cy/el, elapsedTime = 15020975 cy, res = 0
evaluateHadamardProduct : nbElement = 10000000, cyclePerElement = 11.8336 cy/el, elapsedTime = 118335692 cy, res = 0
[ 83%] Built target run_hadamard_product_gpu_cuda
Scanning dependencies of target plot_hadamardCuda
[ 91%] Call gnuplot hadamardCuda
[100%] Built target plot_hadamardCuda


Ce qui nous donne les graphes de la figure 5.

nothing nothing

Figure 5 : En haut : le temps total d'exécution de notre appel Cuda. En bas : le temps d'exécution par élement.



Même si le temps d'exécution total à le mérite d'augmenter avec le nombre d'éléments, il faut reconnaître que le temps par élément décroissant indique qu'il y a une chose que nous n'avons pas prise en compte dans notre test de performance.

Note : je vois d'ici le lecteur perspicace s'agiter au fond de la salle, en hurlant tout ce qu'il peut car il a remarquer l'erreur depuis un bon quart d'heure.


Effectivement, notre test a un gros défaut : nous évaluons le temps total d'exécution et de transfert de données et pas le temps d'exécution seul. On pourrait retorquer qu'il s'agit d'un accélérateur, et comme tout système de cacul externe on doit également prendre en compte le temps de transfert.

À cela je répondrai : certes, mais comment faire pour évaluer le temps de calcul seul ? Car enfin, c'est une chose de dire que l'ensemble est lent, mais il est plus scientifique de pouvoir dire si c'est le calcul qui doit être optimisé ou le transfert.

De même, se plaindre que le GPU est lent tout en lui demandant de faire un calcul à la fois n'a pas de sens.

Nous n'allons pas feindre plus longtemps de ne pas savoir ce que nous faisons, car nous avons déjà développé la fonction qui appelle le calcul plusieurs fois sur le GPU directement sans refaire le transfert de données.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
make plot_hadamardKernel
[ 31%] Built target asterics_hpc_cuda
[ 43%] Built target hadamard_product_cuda
[ 56%] Built target hadamard_product_gpu_cuda_kernel
Scanning dependencies of target run_hadamard_product_gpu_cuda_kernel
[ 62%] Run hadamard_product_gpu_cuda_kernel program
Hadamard product Kernel
asterics_getNbCudaDevice : Detected 1 CUDA Capable device(s)
hadamard_product_cuda_clock : nbElement = 1000, cyclePerElement = 58.941000 cy/el, elapsedTime = 58941 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 2000, cyclePerElement = 29.722500 cy/el, elapsedTime = 59445 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 3000, cyclePerElement = 19.878000 cy/el, elapsedTime = 59634 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 5000, cyclePerElement = 11.872800 cy/el, elapsedTime = 59364 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 10000, cyclePerElement = 6.156800 cy/el, elapsedTime = 61568 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 20000, cyclePerElement = 4.012300 cy/el, elapsedTime = 80246 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 50000, cyclePerElement = 1.613600 cy/el, elapsedTime = 80680 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 100000, cyclePerElement = 1.126940 cy/el, elapsedTime = 112694 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 500000, cyclePerElement = 0.632056 cy/el, elapsedTime = 316028 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 1000000, cyclePerElement = 0.572616 cy/el, elapsedTime = 572616 cy, res = 0.000000
hadamard_product_cuda_clock : nbElement = 10000000, cyclePerElement = 0.513069 cy/el, elapsedTime = 5130695 cy, res = 0.000000
[ 62%] Built target run_hadamard_product_gpu_cuda_kernel
[ 75%] Built target hadamard_product_gpu_cuda
[ 81%] Run hadamard_product_gpu_cuda program
Hadamard product
asterics_getNbCudaDevice : Detected 1 CUDA Capable device(s)
evaluateHadamardProduct : nbElement = 1000, cyclePerElement = 2030.2 cy/el, elapsedTime = 2030203 cy, res = 0
evaluateHadamardProduct : nbElement = 2000, cyclePerElement = 907.588 cy/el, elapsedTime = 1815176 cy, res = 0
evaluateHadamardProduct : nbElement = 3000, cyclePerElement = 609.226 cy/el, elapsedTime = 1827678 cy, res = 0
evaluateHadamardProduct : nbElement = 5000, cyclePerElement = 370.129 cy/el, elapsedTime = 1850647 cy, res = 0
evaluateHadamardProduct : nbElement = 10000, cyclePerElement = 192.664 cy/el, elapsedTime = 1926642 cy, res = 0
evaluateHadamardProduct : nbElement = 20000, cyclePerElement = 101.609 cy/el, elapsedTime = 2032183 cy, res = 0
evaluateHadamardProduct : nbElement = 50000, cyclePerElement = 47.1497 cy/el, elapsedTime = 2357487 cy, res = 0
evaluateHadamardProduct : nbElement = 100000, cyclePerElement = 44.1872 cy/el, elapsedTime = 4418718 cy, res = 0
evaluateHadamardProduct : nbElement = 500000, cyclePerElement = 20.6719 cy/el, elapsedTime = 10335969 cy, res = 0
evaluateHadamardProduct : nbElement = 1000000, cyclePerElement = 16.8271 cy/el, elapsedTime = 16827106 cy, res = 0
evaluateHadamardProduct : nbElement = 10000000, cyclePerElement = 13.0012 cy/el, elapsedTime = 130012225 cy, res = 0
[ 81%] Built target run_hadamard_product_gpu_cuda
Scanning dependencies of target plot_hadamardKernel
[ 87%] Call gnuplot hadamardKernel
[100%] Built target plot_hadamardKernel


La figure 6 compare la version naïve à la version où la boucle de répétition est dans le GPU. On voit immédiatement que cette histoire de transfert de données fausse completement les résultats d'un facteur 25 !

nothing nothing

Figure 6 : Comparaison de la version naïve à la version où la boucle de répétition est dans le GPU. En haut : le temps total d'exécution de notre appel Cuda. En bas : le temps d'exécution par élement.



Il y a cependant un autre phénomène en cause, sinon la nouvelle courbe de performance par élément serait plate (donc la courbe d'en bas). Or un début de plateau n'apparaît qu'a partir de 10^6 éléments par tableau, sachant que nous avons répété le calcul 10000 fois.

Cela nous donne environ 10^{10} calculs pour arriver à ce que l'on pourrait appeler la vitesse de croissière de notre GPU, et il ne sagit que d'une Quadro M2200.

Note : il est important d'insister sur ce point, quand on dit qu'il faut beaucoup de calcul pour utiliser un GPU efficacement, c'est une réalité. Et même si on peut mettre en évidence le même comportement sur CPU (c'est pour cela que l'on répète les calculs) le facteur d'échelle est différent car le transfert des données sur GPU (généralement dans les deux sens, mais au moins dans un sens) fait "perdre" un temps où l'on pourrait calculer sur le CPU.

Encore une fois, ce phénomène existe aussi sur CPU, mais on peut le contourner plus facilement.


Vectoriser un calcul sur 8 élements que l'on fait une seule fois, ne sert à rien question performance, car il faudra attendre que les données arrivent en registres. Pour le GPU c'est pareil.

Note : la quantité de calcul necéssaire pour traiter chaque élément à transférer joue un rôle important dans la performance finalement obtenue. Plus la quantité de calcul est grande ou plus le nombre d'éléments est faible et plus l'utilisation du GPU sera efficace.

Lancer des tests de performances en faisant varier le nombre d'éléments à traiter permet d'évaluer dans quel régime se situe le programme. Si le temps de calcul par élément diminue noteMais que le temps total contnue à augmenter bien entendu. Sinon il y a un problème. c'est que le programme ne traite pas assez d'éléments. Si, au contraire, le temps par élément est stable en fonction du nombre d'élément traités, c'est que le GPU est a priori utilisé efficacement. Mais attention, cela ne veut pas dire que ce calcul ne peut pas être optimisé.