• Aucun résultat trouvé

Implémentation dans cette thèse

A.3.1

Format de stockage des matrices

La méthode utilisée dans les algorithmes décrits dans cette thèse combine les

avantages des deux approches précédentes. Une classeGridCoordinates2Da été

introduite dans ces algorithmes pour servir de clé dans les tables de hachage contenant des matrices. Son but est de stocker des coordonnées (𝑖, 𝑗), et de fournir une fonction de hachage. Ainsi, une seule HashMap est utilisée pour stocker une carte, et il n’est pas nécessaire de connaitre les bornes des coordonnées (𝑖, 𝑗). La fonction de hachage est celle-ci :

ℎ(𝑖, 𝑗) = 𝑖 + 127𝑗 (A.2)

Le coefficient 127 a été choisi arbitrairement, mais donne de bons résultats, puisque dans les algorithmes, la valeur de 𝑖 est en général comprise entre −128 et 128. Par exemple, la carte de convergence est stockée dans une grille dont les cellules font 5 NM de côté (soit 9,26 km). Or, les scénarios utilisés pour valider les algorithmes Plectre et Yard impliquent du trafic aérien Français. Les dimensions de la France étant d’environ 1 000 km du nord au sud et d’est en ouest, cette grille fait environ 100 cases sur 100.

D’autres classes ont été définies à partir deGridCoordinates2Dpour stocker

des coordonnées incluant le temps :

ℎ(𝑖, 𝑗, 𝑡) = 𝑖 + 127𝑗 + 312𝑡, (A.3)

ou un angle :

ℎ(𝑖, 𝑗, 𝜃) = 8(𝑖 + 127𝑗) +𝜃

Figure A.1 – HashMap utilisée pour faire une recherche de points au voisinage d’une position donnée (croix rouge). Le voisinage est représenté par le cercle vert.

A.3.2

Recherche spatiale basée sur les HashMaps

Les HashMaps peuvent également être utilisées pour indexer et rechercher des éléments positionnés dans l’espace. Ici, la HashMap stocke des listes de points dans chacune des cases non-vides. La signature est similaire à :

HashMap<Coordinate, List<Point2D>>

Comme le montre la figure A.1, la recherche spatiale se déroule en deux étapes. On cherche tous les points situés à moins d’une certaine distance d’une position donnée, symbolisée par la croix rouge. La première étape consiste à collecter l’ensemble des points situés dans les cases voisines de celle contenant les coordonnées de départ (carré hachuré en bleu). Comme la HashMap ne stocke que les cellules non-vides, les seules cellules effectivement explorées sont quatre cases colorées en bleu foncé.

Dans un deuxième temps, la distance entre le point rouge et chaque point dé- couvert durant la première étape (croix bleues) est calculée. Seuls les points situés à l’intérieur du cercle vert sont conservés (croix vertes).

Il faut noter que les meilleurs résultats sont obtenus quand la taille des cellules de la grille spatiale est égale au rayon du cercle, de manière à minimiser le nombre de cellules à explorer durant la première étape. Cette méthode donne donc les meilleurs résultats pour les problèmes dont la taille du voisinage est fixe et connu à l’avance. On peut par exemple s’en servir dans la détection de conflits, puisque la distance de séparation entre avions est fixée à 5 NM. Si la dimension de la fenêtre n’est pas connue ou constante, d’autres algorithmes d’indexation spatiale pourraient mieux fonctionner, comme les k-d trees ou les R-trees.

Sans cette indexation spatiale, une recherche des points situés à l’intérieur du cercle nécessitent de mesurer la distance entre le point rouge et tous les autres points, ce qui est extrêmement coûteux en temps de calcul.

Implémentation de Yard sur carte

graphique

Les calculs de l’algorithme Yard sont relativement lents. La partie de l’algorithme la plus consommatrice en temps de calcul est la version centralisée de l’agrégation de trajectoires décrite dans la section 5.1.2. Par exemple, le scénario de la figure 5.11 agrège 4 trajectoires, ce qui nécessite 8 minutes de temps de calcul.

Même si Yard était utilisé dans la phase tactique, ce délai de 8 minutes serait compatible avec les contraintes opérationnelles, puisque l’horizon temporel habituel- lement considéré pour cette phase est de 20 minutes. Par contre, ce temps de calcul rend d’autant plus long l’ajustement précis des paramètres de l’algorithme durant son développement, puisque ce processus nécessite un grand nombre d’essais successifs. Pour tenter de réduire ce délai, il a été décidé de porter l’algorithme d’agrégation de trajectoires sans prise en compte du cap (la version avec prise en compte du cap a été développée par la suite) dans le langage OpenCL, pour profiter des performances des cartes graphiques sur ce type de calcul.

B.1

Calcul sur carte graphique

Le General-purpose processing on graphics processing units (GPGPU) est une famille de technologies permettant d’effectuer des calculs sur cartes graphiques, pour profiter de leur capacité à effectuer des calculs massivement parallèles.

Les CPU appartiennent à la classe d’architecture matérielle appelée Single instruc- tion on single data (SISD). Leur principe de fonctionnement est d’exécuter séquen- tiellement un ensemble d’instructions sur une donnée à la fois. Ils sont pourvus d’un petit nombre de cœurs de calcul optimisés pour exécuter cette séquence d’opérations le plus rapidement possible.

Contrairement aux CPU, les GPU appartiennent à la classe Single instruction multiple data (SIMD). Ils sont prévus pour exécuter le même ensemble d’opérations sur un ensemble de données en parallèle. Ils sont pourvus d’un grand nombre de cœurs de calcul (plusieurs centaines), tous exécutant la même instruction en même temps1sur un élément différent de l’ensemble de données.

1. En pratique, le fonctionnement d’un GPU est plus complexe : il est capable d’exécuter plusieurs fonctions différentes en même temps, et alloue dynamiquement les calculs sur chaque cœur pour accélérer les calculs.

B.1.1

Historique des méthodes de calcul sur GPU

Jusqu’au début des années 2000, il n’était pas possible de faire du calcul général sur un GPU [Fra+10]. Celui-ci prenait en entrée un modèle 3D (des polyèdres définis par un ensemble de triangles) et des textures à y appliquer (des grilles 2D de pixels). La carte graphique effectuait alors un ensemble figé d’opérations qui permettait de calculer l’image à afficher à l’écran (une grille 2D de pixels).

Puis il est devenu possible d’interagir avec ce processus au moyens de shaders, c’est-à-dire de programmes permettant d’effectuer des traitements entre deux étapes du rendu d’une scène 3D. Notamment, les vertex shaders permettent de modifier les sommets du modèle 3D, et les pixel shaders permettent de modifier les pixels des textures ou ceux de l’image finale.

Les shaders peuvent être détournés de leur but initial pour effectuer des calculs généraux. Par exemple, on peut écrire une matrice 2D sous forme de texture, puis appliquer un shader sur cette « texture », qui effectue en fait un calcul sur la matrice, et récupérer la texture transformée par le shader pour lire le résultat.

En 2007, Nvidia publie le langage CUDA (Compute Unified Device Architecture), qui permet d’écrire un calcul généraliste dans un langage proche du C, sans devoir écrire son programme sous forme de shader. Le CUDA est utilisable uniquement sur les cartes graphiques Nvidia.

En 2009, le Khronos Group, déjà à l’origine d’OpenGL, propose un langage similaire à CUDA, l’OpenCL. À la différence de son prédécesseur, l’OpenCL peut être exécuté sur une grande variété de matériel, dont les CPU et les GPU, et pas uniquement sur les GPU Nvidia.

B.1.2

Principe de fonctionnement

Algorithme 6Addition de deux vecteurs sur CPU.

functionAdditionVecteurs(double[] 𝑎, double[] 𝑏, taille) 𝑐 ←double[taille] for 𝑖 ← 0..taille do 𝑐[𝑖] ← 𝑎[𝑖] + 𝑏[𝑖] end for return 𝑐 end function

Les algorithmes 6 et 7 montrent le fonctionnement général de l’addition de deux vecteurs respectivement sur CPU et GPU. Pour additionner deux vecteurs sur CPU (algorithme 6), le programme itère sur chaque élément des vecteurs 𝑎 et 𝑏, et calcule la somme de 𝑎 et 𝑏 à l’index 𝑖, avant de stocker le résultat. Toutes les opérations sont effectuées séquentiellement.

L’adaptation de ce calcul sur GPU est légèrement plus complexe, comme le montre l’algorithme 7. Un GPU possède sa propre mémoire vive. Il faut commencer par transférer les vecteurs 𝑎 et 𝑏 sur la mémoire du GPU, et y allouer la mémoire nécessaire à stocker le résultat.

Le corps de la boucle de l’algorithme sur CPU est transféré dans une procé-

dure exécutée sur le GPU. Dans la fonctionadditionVecteurs(), l’appel à la

fonctionexécuteSurGPU()demande au GPU d’exécuter la fonctionaddition-

Algorithme 7Addition de deux vecteurs sur GPU.

functionAdditionVecteurs(double[] 𝑎, double[] 𝑏, taille)

𝑎𝐺 ←TransfèreSurGPU(𝑎) 𝑏𝐺 ←TransfèreSurGPU(𝑏) 𝑐𝐺 ←AlloueSurGPU(double[taille]) ExécuteSurGPU(AdditionVecteursGPU, 𝑎𝐺, 𝑏𝐺, 𝑐𝐺, taille) 𝑐 ←TransfèreDepuisGPU(𝑐𝐺) return 𝑐 end function

procedureAdditionVecteursGPU(double[] 𝑎𝐺, double[] 𝑏𝐺, double[] 𝑐𝐺, 𝑖)

𝑐[𝑖] ← 𝑎[𝑖] + 𝑏[𝑖] end procedure 14 33 46 104 22 11 42 -15 47 150 33 27 197 60 257 + + + + + + +

Figure B.1 – Addition de 8 nombres par réduction successive. Les nombres sont additionnés deux à deux. Toutes les additions sont effectuées en parallèle à chaque étape : la somme est calculée en 3 itérations au lieu de 7 dans le cas d’un calcul par accumulation.

appels parallèles est appelé thread. Ici, un thread correspond à l’appel de la fonction

additionVecteursGPU()pour un index 𝑖 particulier.

Ainsi, pour adapter un calcul pour le GPU, il faut identifier les portions de calcul pouvant être effectuées en parallèle (ici l’intérieur de la bouclefor). Il faut alors

déplacer cette portion des calculs dans une procédure exécutée par le GPU. Le pilotage de l’appel à cette fonction est effectué par le code exécuté par le CPU.

Certains calculs nécessitent une transformation plus importante pour tirer pleine- ment parti du GPU. Par exemple, additionner tous les éléments d’un tableau, ou en chercher la valeur minimale ou maximale peut être effectuée en faisant une réduction progressive de l’ensemble de valeurs pour en extraire une seule.

Ce processus [Cat10] est décrit par la figure B.1. À partir de la liste de valeurs de la première ligne, quatre additions sont effectuées en parallèle. Les quatre résultats intermédiaires sont à leur tour additionnés deux à deux en parallèle. Le processus itère jusqu’à l’obtention du résultat final. Pour 𝑛 éléments de départ, le processus nécessite log2𝑛itérations, à comparer aux 𝑛 −1 itérations nécessaires pour une implémentation sur CPU, qui additionne les éléments deux par deux.

B.1.3

Spécificités du GPGPU

Il est nécessaire de prendre en compte les spécificités de l’architecture d’un GPU dans le développement d’un programme GPGPU. Dans le modèle OpenCL, la carte graphique dispose d’une mémoire globale dédiée. Elle est relativement lente, mais peut stocker une grande quantité de données. Le programme hôte (sur le CPU) peut lire et écrire sur la mémoire globale.

Les cœurs de calcul sont rassemblés en groupes, chaque groupe disposant d’une mémoire locale plus réduite, mais plus rapide. celle-ci n’est accessible que depuis les threads, qui doivent se charger du transfert de données entre la mémoire globale et locale.

Enfin, chaque cœur possède une mémoire privée, qui permet de stocker les va- riables d’un thread. C’est la plus rapide, mais aussi la plus petite.

Pour donner un exemple, la Nvidia GeForce GT 730M utilisée durant le doctorat est équipée d’environ un gigaoctet de mémoire globale. Les cœurs sont rassemblés en deux groupes de 192 cœurs (soit 384 cœurs au total), chacun se partageant une mémoire locale de 48 kio2.

D’autre part, la fréquence du GPU est faible comparée à celle d’un CPU. Par exemple, la carte graphique utilisée est cadencée à 758 MHz, alors que le CPU (un Intel Core i7-4900MQ) est équipé de 8 cœurs cadencés à 2,8 GHz. Cette lenteur relative du GPU est compensée par le nombre bien plus élevé de cœurs. Ainsi, le GPU est plus performant que le CPU quand il doit exécuter un traitement sur au moins plusieurs centaines d’éléments, alors que le CPU est bien meilleur sur le traitement de quelques dizaines d’éléments.

Deux goulots d’étranglement peuvent ralentir l’exécution d’un programme sur un GPU. Le premier est le débit de transfert de données entre la mémoire RAM et la mémoire globale du GPU. Pour diminuer ce temps de latence, il faut éviter au maximum de transférer des données vers le GPU, par exemple en évitant de transférer des résultats intermédiaires entre le CPU et le GPU, et en calculant le plus de résultats intermédiaires possibles sur le GPU

Le deuxième goulot d’étranglement est le temps de latence avant le lancement d’un thread sur le GPU. Il est donc préférable de lancer une seule fonction complexe qui effectue la totalité des calculs plutôt que plusieurs fonctions élémentaires qui n’en font qu’une partie.