• Aucun résultat trouvé

4.4 La technologie Compute Unified Device Architecture (CUDA)

4.4.5 Parallélismes dynamiques

Jusqu’à l’architecture Fermi, seul le CPU pouvait générer des opérations sur le GPU. L’arrivée de l’architecture Kepler a permis d’introduire un nouveau concept de parallélisme dynamique, permettant au GPU de pouvoir auto-générer des opérations.

CPU GPU Architecture  Fermi CPU GPU Architecture  Kepler

Figure 4.13 – Représentation de la différence de fonctionnement entre les architectures Fermi et Kepler.

Comme vu précédemment, une grille est constituée de blocs formés de threads. Grâce à l’architecture Kepler, il est maintenant possible d’effectuer une programmation, en utilisant le parallélisme dynamique d’une grille parent qui va pouvoir lancer des grilles filles. Ces grilles filles héritent de certains attributs et aussi de certaines limites de la grille parent, tel que la mémoire cache L1.

Potentiellement, chaque thread de la grille parent peut lancer un nouveau kernel, ce qui signifie que si la grille parent possède NB blocs avec NT threads, alors la grille pourra effectuer

NB× NT kernels. Il est possible de spécifier à un seul thread de la grille parent de lancer un nouveau kernel. Un exemple de code utilisant le parallélisme dynamique est présenté ci-dessous

Code 4.11 – Exemple utilisant le parallélisme dynamique.

1

2 // Fonction utilisée par la grille fille et exécutée sur le GPU

3 __global__ void child_launch(int *data){

4 data[threadIdx.x] = data[threadIdx.x]+1;

5 }

6

7 // Fonction utilisée par la grille parent et exécutée sur le GPU

8 __global__ void parent_launch(int *data){

9 data[threadIdx.x] = threadIdx.x;

10 __syncthreads();

11 if(threadIdx.x == 0){

13 child_launch<<< 1, 256 >>>(data);

14 }

15 }

16 17

18 // Fonction exécutée sur le CPU

19 void host_launch(int *data){

20 // Lancement de la grille parent

21 parent_launch<<< 1, 256 >>>(data);

22 }

Lorsqu’on utilise le parallélisme dynamique, les grilles lancées sont entièrement imbriquées. Autrement dit, la grille parent ne se terminera pas tant que la grille fille n’est pas terminée, et cela même si aucune synchronisation explicite n’est utilisée. Dans la Figure 4.14, on a représenté schématiquement cette imbrication, afin de mieux comprendre le fonctionnement de ce parallélisme.

Figure 4.14 – Représentation de l’imbrication des grilles parent et fille.

Bien évidemment, si la grille parent a besoin des résultats de la grille fille à un moment bien précis, il faut s’assurer que cette dernière a bien achevé son exécution. Pour cela, on est obligé

de réaliser une synchronisation explicite grâce à la fonction cudaDeviceSynchronize().

Cette fonction permet d’attendre que toutes les grilles lancées précédemment par le bloc de threads soient terminées. Notons que l’utilisation de cette fonction est coûteuse, car elle va mettre en pause le bloc en cours d’exécution.

La mémoire possède une certaine cohérence entre les différentes grilles. Cela signifie que si la grille-parent écrit dans la mémoire, puis lance une grille fille alors cette dernière pourra accé-der à la valeur. De façon similaire, si la grille-fille écrit dans un emplacement de la mémoire, alors si la grille parent effectue une synchronisation, alors elle aura accès à cette zone de la

mémoire. La même remarque peut être faite si plusieurs grilles filles sont lancées séquentiel-lement. Si chaque grille effectue des écritures dans la mémoire, alors les grilles exécutées à la suite auront un accès à ces données, sans qu’aucune synchronisation ne soit faite entre chaque lancement.

Malgré cela, il faut faire attention lors de l’utilisation des différentes mémoires. Par exemple, toutes les variables définies dans la grille parent ne sont pas forcément utilisables comme argument lors du lancement d’une grille fille. Le tableau 4.1 résume les limitations liées aux pointeurs que l’on peut utiliser ou non pour l’exécution d’une grille fille.

Peut-être passé Ne peut pas être passé

- Mémoire globale - Mémoire partagée (variables partagées également)

- Mémoire hôte - Mémoire locale

- Mémoire constante

Tableau 4.1 – Limitations des pointeurs qui peuvent-être passés ou non lors du lancement d’une grille fille

Il faut savoir que le code contenant une erreur de déclaration entrainera un problème lors de la compilation. Il est néanmoins possible de contourner cette erreur, en passant par une structure qui contiendra le pointeur concerné. Mais il vaut mieux éviter cette solution, car cela peut conduire à une corruption des données et à des erreurs pratiquement indétectables, car CUDA n’identifiera pas ces erreurs lors de la compilation.

Comme pour les grilles lancées depuis le CPU, celles exécutées depuis un bloc de thread sont séquentielles. Comme vu dans la section 4.4.3, il est possible d’utiliser des streams au sein d’une grille parent, lors du lancement des grilles filles, permettant ainsi d’obtenir plus de concurrence. Le mode de fonctionnement des streams dans ce cas précis ne diffère pas de ceux utilisés dans la section 4.4.3. On remarquera que les streams par défaut, créés au sein de blocs différents, sont considérés comme différents. Malgré cette différence, une exécution simultanée des streams par défaut n’est pourtant pas garantie, comme on pourrait s’y attendre. Elle sert surtout à une meilleure gestion des ressources du GPU. Pour avoir une véritable concurrence, le programmeur est donc obligé de faire un effort dans le développement de son code, en utilisant les streams de façon optimale. De plus, une fois qu’un stream a été créé, il peut être utilisé par n’importe quel thread appartenant au même bloc. Il ne pourra plus être utilisé, lorsque le bloc a terminé son exécution ou par tout autre bloc. Finalement, la destruction d’un stream sur le GPU se fait de manière équivalente à ceux déclarés sur le CPU (voir la section 4.4.3).

De manière identique aux streams, les événements peuvent être utilisés sur GPU, en utilisant les mêmes fonctions décrites dans la section 4.4.4

4.4.6 Conclusion

Dans ce chapitre, nous avons introduit les bases ainsi que certains concepts de CUDA. Cela permettra donc de développer nos programmes sur GPU. Le but de cette présentation est d’utiliser, dans la suite, toutes ces méthodes et outils, pour pouvoir les appliquer à la résolution de notre problème monodomaine et bidomaine, permettant de modéliser l’activité électrique dans un tissu neuronal.

méthodes de parallélisation au

modèle monodomaine

5.1 Introduction

Avant de résoudre le problème décrit par le modèle monodomaine, je vais m’intéresser à l’intégration de certains modèles de neurones, afin de mieux analyser leurs différences, ainsi que leurs caractéristiques. Comme nous l’avons vu dans la section 2.1, ces différents modèles sont décrits à l’aide d’une ou plusieurs EDO. Il convient ainsi d’utiliser les méthodes vues dans la section 3.1 pour pouvoir les résoudre de façon numérique, d’abord de façon séquentiel puis de développer la méthode de résolution pararéel avec les technologies MPI (section 4.1), et CUDA (section 4.4). L’objectif de ce chapitre est de donner un premier aperçu des gains de temps obtenus grâce à ces différentes méthodes. Je m’intéresserai à deux modèles en particulier, un modèle complexe : Hodgkin-Huxley, puis un modèle simplifié : Fitzhugh-Nagumo.

Le développement et les différents tests effectués dans ce chapitre ont été intégralement implémentés en Python, pour la partie séquentielle et MPI.