• Aucun résultat trouvé

6.3 Simulations en dimension deux

6.3.3 Simulations avec la méthode pararéel en CUDA

Dans cette section, je présente la méthode pararéel sur GPU, déjà développée dans le chapitre 5 au problème d’EDO, au modèle monodomaine. Mais, il faut maintenant aller plus loin. En effet, il est souhaitable d’obtenir une solution dont la précision sur l’échelle de temps est excellente. Or, cette contrainte peut également s’appliquer à la dimension spatiale. C’est dans ce but, que je vais tirer partie de la notion, déjà introduite dans la section 4.4 : parallélisme dynamique.

L’intérêt est de lancer une grille CUDA effectuant la simulation grâce au pararéel, comme lors de l’application aux modèles de neurone. Chaque thread intégrant le sous-problème associé à

chaque sous-intervalle, exécutera à son tour une grille calculant les nouvelles valeurs de Vmet

w sur chaque maille (Figure 6.10).

Figure 6.10 – Principe de la parallèlisation massive sur GPU.

L’avantage de CUDA est de pouvoir paralléliser massivement un problème. Après avoir discré-tisé l’espace de définition, on définit une grille CUDA ayant la même taille que le maillage, puis on affecte à chaque thread de cette grille, une maille bien précise. Dans ce cas, on ne passe plus par la forme matricielle découlant de la discrétisation de ∇ · (Mi∇Vm), mais uniquement par l’expression : cx 2u ∂x2+ cy2u ∂y2 = cx ui −1,j− 2ui ,j+ ui +1,j ∆x2 + cy ui ,j −1− 2ui ,j+ ui ,j +1 ∆y2 . (6.12)

Autrement dit, le thread ayant comme coordonnées (i ; j ) au sein de la grille, a à sa charge la valeur de ui ,j. A partir de ces coordonnées, le thread courant fait appel aux valeurs ui ±1,j±1 des ses threads voisins, afin de mettre à jour ui ,j. Les conditions aux limites doivent aussi être prises en compte, en distinguant les différents cas où les mailles sont concernées. Tout est basé sur le jeu des coordonnées du thread dans la grille, déterminées grâce aux différentes informations fournies, déjà introduites auparavant (sections 4.4 et 5) telles que, le nombre de blocs, la dimension d’un blocs, etc. On passe par cette formulation explicite, pour éviter de devoir copier la matrice A du CPU vers le GPU, une opération qui peut devenir très vite coûteuse en temps, lorsque le problème est important.

Nous avons déjà vu dans la section précédente, qu’il est obligatoire de faire communiquer toutes les valeurs à la fin des sous-intervalles, au sein du maillage. Cela entraîne un grand nombre de valeurs à enregistrer en mémoire, notamment si le maillage est très grand.

Il est donc pratiquement impossible de faire appel à la mémoire partagée du bloc effectuant la méthode parareél.

Fondamentalement, la manière de procéder pour la parallélisation en temps est identique à celle utilisée dans le chapitre 5. Mais contrairement aux cas précédents, on ne peut pas lancer la méthode pararéel sur une grille contenant un bloc de 1000 threads, par exemple. Il faut savoir que lorsque l’on lance une grille dynamique, toutes les grilles s’exécutent séquentiellement. On perd ainsi toute concurrence entre elles, et au final toute intérêt de simuler le modèle en parallèlisant totalement les calculs. Pour pouvoir réintroduire de la concurrence, il est obligatoire d’utiliser des "streams" (voir section 4.4). Chaque action lancée dans des streams différents sera effectuée en totale concurrence. Un problème se pose néanmoins, il y a un nombre limite au nombre de streams que l’on peut exécuter en concurrence. Ce nombre était encore de 8 lorsque les GPU étaient basés sur l’architecture Fermi, alors qu’il est de 32 depuis l’architecture Kepler, sur laquelle est basée notre Tesla K20C. Ce nombre augmentera encore lorsque les Tesla utiliseront l’architecture Maxwell.

A partir de cette information, je définis un tableau de streams (Code 6.6), puis chaque grille fille effectuant le calcul en espace sera lancée au sein de l’un d’eux. Afin d’avoir le maximum de concurrence, la grille parent doit contenir le même nombre de threads que le nombre de streams souhaité. Autrement dit, si on veut le maximum de concurrence, on doit définir 32 streams, ce qui entraine l’utilisation d’une grille parent contenant un bloc de 32 threads.

Code 6.6 – Définition d’un tableau de streams CUDA.

1 # BLOCK_SIZE 32

2

3 // Allocation d’un tableau de BLOC_SIZE streams

4 cudaStream_t *streams = (cudaStream_t *) malloc(BLOCK_SIZE * sizeof(cudaStream_t));

5

6 // Création de chaque stream

7 for (int i = 0 ; i < BLOCK_SIZE ; i++){

8 cudaStreamCreateWithFlags(&(streams[i]), cudaStreamNonBlocking);

9 }

Le code 6.7 montre l’utilisation des grilles filles au sein de la grille parent. Chaque itération en temps entraîne l’exécution de la grille fille mettant à jour les valeurs des variables de potentiel

Vmet de recouvrement w sur chaque maille. Même si dans cette exemple, la concurrence ne

paraît pas évidente, elle servira dans les étapes d’après puisque elle permettra à la résolution fine de démarrer, une fois l’intégration grossière terminée, alors qu’il aurait fallu attendre que tous les threads de la grille parent terminent leur résolution grossière avant que les autres puissent continuer. Sans l’utilisation des streams, le pararéel n’aurait rien apporté dans ce cas.

Code 6.7 – Aperçu de l’utilisation des streams au sein de la méthode pararéel.

1 __global__ void parareal(double *U, double *W, double *Utild, double *Wtild, double *

Uchap, double *Wchap, double *UF, double *WF, Param p){

2

3 // Exécution avant

4 ...

5 6

7 // Allocation du tableau de streams

8 cudaStream_t *streams = (cudaStream_t *) malloc(BLOCK_SIZE * sizeof(cudaStream_t));

9

10 // Création de chaque stream

11 for (int i = 0 ; i < BLOCK_SIZE ; i++){

12 cudaStreamCreateWithFlags(&(streams[i]), cudaStreamNonBlocking);

13 }

14

15 // Dimension des grilles filles

16 dim3 block(THREAD_NUM_X,THREAD_NUM_Y, 1);

17 dim3 grid(BLOCK_NUM_X,BLOCK_NUM_Y, 1);

18

19 // Résolution permettant l’initialisation de la méthode pararéel

20 for(int i=0;i<BLOCK_SIZE;i++){

21

22 if(i == idx){

23

24 // Copie de la condition initiale

25 cp_next_data<<<grid, block,0,streams[idx]>>>(Utild,Wtild,U,W,i);

26

27 // Résolution grossière au sein du stream[idx]

28 for(int j=0;j<Nb1;j++){

29 coarseRK4_iteration<<<grid, block,0,streams[idx]>>>(Utild,Wtild,p,idx,j)

;

30 }

31

32 // Copie de la solution finale

33 cp_data<<<grid, block,0,streams[idx]>>>(U,W,Utild,Wtild,i+1); 34 35 cudaDeviceSynchronize(); 36 } 37 __syncthreads(); 38 39 } 40 41 // Exécution après 42 ...

A nouveau, l’utilisateur peut utiliser la structureParamafin de définir tous les paramètres de simulation et de stimulation du modèle, permettant une grande flexibilité. Suite à l’im-plémentation de cette méthode, j’ai effectué divers tests, en utilisant un nombre de streams différents. La figure 6.11 présente les erreurs quadratiques moyennes entre la solution parallèle

et séquentielle, lors de différentes itérations. On constate bien une diminution de l’erreur lorsque le nombre d’itérations augmentent.

Figure 6.11 – Comparaison pour différents nombres de processus, de l’erreur quadratique moyenne à chaque itération, lors de l’utilisation de la méthode pararéel en CUDA.

Les speed-up sont référencés dans la figure 6.12. On observe un gain très important en cou-plant les deux parallélismes. Chaque thread a très peu d’opérations à effectuer, entraînant une accélération très importante de 328 lorsqu’on utilise le nombre limite de streams utilisables en concurrence.