Le calcul parallèle - Direction technique Eau, mer et fleuves
Transcription
Le calcul parallèle - Direction technique Eau, mer et fleuves
Le calcul parallèle Introduction Lorsque l'on met en place un système de calcul distribué ou un système de calculs parallèle, le but est le même : la mise en commun de la puissance de calcul de plusieurs processeurs. Les calculs sont considérés comme parallèles si les différents processeurs ont accès physiquement aux même données et effectuent les mêmes traitements. Alors que dans le cas de calculs distribués, les données, mais également les traitements, peuvent être répartis sur différentes machines. Dans ce dernier cas, les communications, comme nous le verrons par la suite, jouent un rôle très important et influencent énormément les performances du calcul. Formellement, il y a peu de différences entre les calculs parallèles et les calculs distribués. L'une d'entre elles est l'absence de communication explicite entre les processeurs dans le cas d'un calcul parallèle. Les principales difficultés de mise en place sont les mêmes dans les deux cas. C'est pourquoi, par la suite, je ne ferai pas de différence entre les deux et j'emploierai le terme de processeur pour désigner une unité de calcul capable de communiquer avec les autres. Cette communication peut se faire par l'intermédiaire d'une mémoire partagée, par échanges de fichiers ou par envois de messages à l'aide d'un réseau. Il faut noter aussi qu'il n'y a pas de solution miracle. Il existe plusieurs solutions très différentes, aussi bien pour l'architecture matérielle que pour les algorithmes de calcul. Chaque solution répond à un problème particulier. Il faut donc bien étudier le problème afin de déterminer la solution qui va le mieux y répondre. Architecture matérielle Choix des processeurs Lorsque l'on met en place un système de calcul, on a le choix entre plusieurs types d'architecture processeur. La première est de prendre un petit nombre de processeurs mais qui ont une grande puissance de calcul, la deuxième de prendre un grand nombre de processeurs mais de faible puissance. Une dernière possibilité est de prendre un compromis entre les deux premières. Le choix de l'une de ces trois solutions est surtout une histoire de coût, les processeurs puissants étant plus chers. Il faut savoir aussi que plus le nombre de processeurs est élevé, plus le volume des communications nécessaires entre ces processeurs sera important, d'où une baisse d'efficacité. Choix d'une architecture mémoire Mémoire partagée Le premier type d'architecture mémoire pour une application distribuée est la mémoire partagée. Dans ce cas, plusieurs processeurs ont accès à la même mémoire physique. Ils peuvent opérer avec elle de manière indépendante et les changements fait par l'un des processeurs sont immédiatement visibles par les autres. Il existe deux types de mémoires partagées : • Les mémoires à accès uniformes (UMA : Uniform Memory Access) où l'accès à la mémoire est le même pour chaque processeur, on parle alors d'accès équitable. Pour cela, il faut que les processeurs soient identiques. Il y a aussi, dans ce type d'architecture mémoire, ce que l'on appelle un contrôleur de cache (CC-UMA) qui s'assure que la mémoire cache de chaque processeur est cohérente avec les données présentes en mémoire centrale. Ce type de mémoire est présent dans la plupart des systèmes SMP (Symetric MultiProcessor). Conception d'un système à haute performance - Le calcul parallèle 1/6 Copyright © CETMEF 2004 • Les mémoires à accès non uniformes (NUMA : Non Uniform Memory Access) où chaque processeur peut accéder à la mémoire indépendamment des autres. Ils peuvent donc être de types et de vitesses différentes. Comme pour l'architecture UMA, on peut y rajouter un système de cohérence de cache, mais l'implémentation de ce dernier est compliquée par ces accès non uniformes. Ce type de mémoire présente l'avantage de permettre un partage immédiat des données, facilitant la programmation. Mais cette solution coûte chère, ce qui limite le nombre de processeurs que l'on peut ajouter sur une même mémoire. De plus, les mécanismes de cohérence de cache sont coûteux en performance et plus on ajoute de processeurs, plus ce type de mécanisme devient indispensable. Si on ajoute le fait que le débit de la mémoire est limité, on voit bien que la hausse des performances ne suit pas linéairement le nombre des processeurs. Au niveau de la programmation, même si la communication est facilitée, il reste à la charge du programmeur de vérifier la cohérence des données en synchronisant les accès aux données critiques. Il faut éviter que deux processeurs puissent modifier une même variable sans tenir compte de la modification de l'autre processeur, sous peine de mettre en péril la cohérence des données et donc du résultat du calcul. Mémoire distribuée Dans ce cas, chaque processeur possède sa propre mémoire. La modification par l'un des processeurs de sa propre mémoire n'a pas d'influence directe sur celle des autres processeurs. Cela suppose donc de mettre en place une communication explicite entre les processeurs (souvent par l'intermédiaire d'un réseau). Ce type d'architecture présente l'avantage de permettre une hausse des performances processeurs / mémoires plus intéressante que dans le cas de la mémoire partagée, mais c'est au programmeur de gérer la plupart des détails de la communication entre les unités de calcul. Elle rend également difficiles les échanges complets de structures de données, pose des problèmes d'accès non uniformes dans le temps et elle rend la cohérence de données plus dure à maintenir. Mais elle présente l'intérêt de pouvoir s'agrandir facilement et on peut mettre ensemble des architectures processeurs et logiciels différentes si on le souhaite. Mémoire partagée et distribuée Ce dernier type de mémoire est un mélange des deux premiers. Dans cette architecture, il y a plusieurs groupes de processeurs partageant de la mémoire qui communiquent grâce à un réseau. Cela permet, dans une certaine mesure, de tirer les avantages de deux précédentes architectures et d'en réduire les inconvénients. Classification des systèmes parallèles En 1966, Flynn a proposé une classification en quatre groupes des systèmes parallèles : Single Instruction, Single Data (SISD) C'est dans cette catégorie que l'on retrouve les stations de travail traditionnelles. Elle correspond aux stations capables de ne traiter par cycle d'horloge qu'une instruction sur une donnée par cycle d'horloge. Ce n'est pas une architecture parallèle. Conception d'un système à haute performance - Le calcul parallèle 2/6 Copyright © CETMEF 2004 Single Instruction, Multiple Data (SIMD) Chaque processeur de cette architecture exécute la même instruction à chaque cycle d'horloge, mais les données traitées sont différentes. L'exécution est synchrone et déterministe sur chaque processeur. Multiple Instruction, Single Data (MISD) Il existe peu d'exemples de cette classe de systèmes parallèles. Elle correspond aux systèmes capables d'exécuter plusieurs instructions sur la même données durant le même cycle d'horloge. Multiple Instruction, Multiple Data (MIMD) C'est dans cette catégorie que l'on trouve le plus de systèmes parallèles. A chaque cycle d'horloge, chaque processeur exécute une instruction différente sur une donnée différente. Ces exécutions peuvent être synchrones ou non, déterministes ou non. Modèle de programmation parallèle Il existe plusieurs modèles pour la programmation d'applications parallèles. Ces modèles ne tiennent pas compte de l'architecture matérielle (processeurs et mémoires). Chacun d'entre eux peut être réalisé quel que soit le choix de l'architecture matérielle. Par exemple, il existe des méthodes pour partager une mémoire qui est physiquement distribuée et pour employer une méthode de communication par passage de message sur une architecture à mémoire partagée. Comme pour les architectures matérielles, il n'y a pas de meilleurs modèles, il y a juste des modèles qui répondent mieux à certains problèmes. Là encore, ce n'est qu'une étude au cas par cas qui permettra de décider quel modèle conviendra le mieux. Programmation par mémoire partagée Dans ce modèle les différentes tâches partagent le même adressage mémoire, elles peuvent lire et écrire dedans de manière indépendante et asynchrone. Cela permet de s'affranchir du problème de la communication des données entre les tâches. Mais le principal désavantage de ce modèle est que la cohérence des données et les accès concurrents doivent être gérés par le programmeur à l'aide de sémaphores ou de verrous, au risque de diminuer les performances du système. Programmation par threads Les threads correspondent à des exécutions simultanées d'un même code qui peut, si besoin est, avoir des chemins d'exécution différents. Un thread est crée à la demande du programmeur, ce dernier peut donc faire exécuter en parallèle différentes fonctions de son application. Les threads partagent le même espace mémoire, il faut donc veiller à s'assurer que deux threads ne modifient pas au même moment la valeur d'une même adresse globale. Ceci peut être fait en synchronisant les exécutions grâce à des verrous. Programmation par envoi de message Dans ce modèle, chaque processeur utilise sa propre mémoire locale. La communication des données et la synchronisation se font à l'aide de message dont le format est laissé à la discrétion du programmeur. Les différentes instances de l'application répartie doivent être synchronisées, en effet, l'envoi d'un message doit faire l'objet d'une réception explicite par le destinataire. Programmation sur des données en parallèle Dans ce modèle, les données sont découpées et distribuées vers les différentes unités de calcul. Ces dernières appliquent les même traitements aux données qui leur sont envoyées. Si on a une architecture à mémoire partagée, les différentes tâches accèdent aux données grâce à la mémoire globale, sinon, dans le cas de mémoires distribuées, les données sont transmissent à chaque unité qui les copie dans sa propre mémoire locale. Conception d'un système à haute performance - Le calcul parallèle 3/6 Copyright © CETMEF 2004 Autres modèles Hybride Dans ce modèle, plusieurs modèles de programmation présentés précédemment sont combinés. Par exemple, on peut citer l'utilisation simultanée de threads et de communications par passages de messages ou par de la mémoire partagée. Ce qui est souvent le cas pour les machines SMP mises en réseau. Single Program, Multiple Data (SPMD) Dans ce modèle, chaque tâche exécute le même jeu d'instructions. A chaque instant, les tâches peuvent être à des étapes différentes du programme. Elles n'exécutent pas forcément tout le programme dans son ensemble mais peuvent être limitées à certaines fonctions. Le programme doit donc prévoir ces cas de figure et agir en conséquence. De plus les données sur lesquelles travaillent les tâches ne sont pas forcément les mêmes. Ce modèle de haut niveau peut faire appel à plusieurs autres modèles plus simples décrits précédemment. Multiple Program, Multiple Data Ce modèle est le même que le modèle "Single Program, Multiple Data", sauf que chaque tâche peut exécuter un code différent et traiter des données différentes. Concevoir une application parallèle Comprendre le problème ou le programme Afin de tirer les meilleures performances d'une application parallèle, il faut commencer par bien comprendre le problème, si on part de rien, ou le programme, si on part d'un programme existant. En effet, il faut tout d'abord déterminer si une exécution parallèle de l'application peut apporter un gain de performance. Pour cela, il faut qu'il y ait une certaine indépendance dans le traitement des données. Soit le traitement sur une partie des données est indépendant du traitement des autres, soit il existe des tâches qui peuvent être traitées indépendamment des autres. Par exemple, le calcul de la suite Fibonacci ( F(k+2) = F(k+1) + K(k), chaque élément est égal à la somme des deux précédents) ne peut pas être distribué à cause de la trop grande dépendance des calculs. Il faut attendre le calcul des deux termes précédent pour pouvoir calculer le terme courant. Il faut aussi trouver les parties les plus gourmandes en terme de temps de calcul de l'application. En effet, c'est sur ces parties que le travail de mise en parallèle sera le plus efficace en terme de performance. Le partitionnement Le partitionnement est la première étape d'écriture d'un programme parallèle ou distribué. Il consiste à découper le problème, soit en terme de données indépendantes, soit en terme de fonctionnalités. C'est de ce partitionnement que va dépendre le gain apporté par la mise en parallèle du calcul. Chaque partie du calcul considérée comme indépendante pourra alors être exécutée par un processeur différent. Les communications Les communications entre les processeurs dépendent beaucoup de la tâche à effectuer. En effet, lorsque le traitement d'une donnée est complètement indépendant des autres, il n'est pas nécessaire de mettre en place un système de communication. Malheureusement la grande majorité des calculs, pour être mené à bien, ont besoin d'une partie des résultats des autres processeurs voisins. Il est donc nécessaire de diffuser les résultats vers ceux qui en ont besoin. Les communications jouent un rôle très important dans les performances globales d'un système distribué. Il y a plusieurs points à considérer : • Le coût des communications : il y a toujours un surcoût engendré par les communications, lorsque l'application envoie et reçoit des messages mais aussi lors qu'elle attend un message en provenance Conception d'un système à haute performance - Le calcul parallèle 4/6 Copyright © CETMEF 2004 • • d'un autre processeur. Durant ce traitement ou cette attente, le processeur n'est pas à la disposition de l'application et donc du calcul. La latence et le débit du réseau de communication sont aussi des points importants. La latence est le temps nécessaire pour envoyer un message de taille minimal (0 octet) sur le réseau, le débit est la quantité de données que le réseau peut transmettre par unité de temps. C'est de ces deux paramètres que va dépendre la vitesse des communications (si on ne tient pas compte du traitement du message par le système d'exploitation). Il est en général préférable de ne pas envoyer beaucoup de petits messages, en effet chaque envoi nécessite un temps de traitement minimum qui n'est plus négligeable par rapport au temps nécessaire à l'émission de petits messages. Pour remédier à cela, on peut, par exemple, regrouper les petits messages pour limiter cet effet en formant un message de taille plus importante. Les communications peuvent être synchrones ou non, bloquantes ou non. Les communications synchrones sont plus lentes et sont généralement bloquantes, c'est à dire que les deux processeurs engagés dans la communication doivent attendre la fin de la communication pour continuer. Par contre les communications asynchrones sont la plupart du temps non bloquantes. Quand un processeur veut envoyer un message à un autre, il envoie le message et peut immédiatement reprendre le cours de son exécution sans se soucier de quand l'autre processeur recevra le message. C'est là le principal avantage des communications asynchrones. Les synchronisations Il existe différents types de synchronisations : • • • Les barrières impliquent l'ensemble des tâches de l'application. Pour passer une barrière, il faut que toutes les tâches de l'application aient atteint cette barrière. Les verrous et sémaphores impliquent un nombre quelconque de tâches. Ils sont utilisés principalement pour protéger une donnée ou une section critique du code. Lorsqu'une tâche obtient le verrou, le système doit garantir qu'elle est la seule à l'avoir et qu'elle peut, en toute sécurité, accéder à la donnée ou rentrer dans la section critique. Les autres tâches cherchant à obtenir ce verrou pendant ce temps seront bloquées en attente du verrou. Les communications synchrones peuvent aussi jouer le rôle d'élément de synchronisation. La dépendance des données Il y a dépendance entre deux parties d'une application lorsque l'exécution de l'une affecte le résultat de l'autre. Une dépendance de donnée entraîne une utilisation de la valeur d'une même variable par des tâches différentes. Ces dépendances sont très importantes en ce qui concerne les applications parallèles puisque c'est l'un des principaux freins au développement d'applications parallèles. Ces dépendances peuvent être gérées soient par la communication des données entre les processeurs lors de points de synchronisation (dans le cas de mémoires distribuées) soit par la synchronisation des lectures / écritures (dans le cas de mémoires partagées). La répartition de charge Le but de la répartition de charge est d'utiliser au maximum les ressources disponibles. Dans le cas idéal, toutes les tâches sont occupées en permanence. Cette répartition des tâches joue également un rôle important dans les performances globales du système. Surtout dans le cas où il nécessaire de mettre en place des synchronisations, en effet, c'est la tâche la plus lente qui va déterminer la performance de l'ensemble, les tâches les plus rapides devant attendre les plus lentes. Cette répartition peut être statique si la vitesse des processeurs et le temps de calcul sont connus. Mais, dans le cas contraire, on peut mettre en place une répartition dynamique, par exemple les tâches, qui une fois qu'elles ont fini leurs traitements, viennent demander de nouvelles données. La granularité La granularité représente le volume de traitement qui il y a entre deux communications (ou synchronisations). C'est une mesure qualitative du ratio entre le volume de calcul et le volume des communications. Une granularité faible signifie que les périodes de calcul sont relativement courtes par rapport au période Conception d'un système à haute performance - Le calcul parallèle 5/6 Copyright © CETMEF 2004 communication. Elle facilite la répartition de charge mais entraîne un surcoût important de communication et laisse peu d'opportunités d'augmenter les performances. Une granularité forte, à l'inverse, signifie qu'il y a relativement peu de communication en comparaison avec les périodes de calcul. Cela laisse plus d'opportunités d'augmenter les performances mais la répartition de la charge est beaucoup moins évidente à mettre en place. Les limites des applications parallèles La loi d'Amdahl montre que le gain maximum que l'on peut avoir lors que l'on développe une application parallèle par rapport à la même application mais non parallèle est fonction du pourcentage du code qu'il est possible de rendre parallèle. Soit P le pourcentage de code parallélisable, le gain maximum est 1 / ( 1 - P ) Soit N le nombre de processeurs et S le pourcentage de code non parallèle, le gain est 1 / ( P/N + S) On voit bien que plus on ajoute de processeurs, plus le gain sera élevé. Mais ceci n'est que la limite théorique, elle ne prend pas en compte les problèmes soulevés par la mise en parallèle des calculs (communications, synchronisations, dépendances, ...). De plus, les applications parallèles sont plus complexes à développer. En effet, les exécutions simultanées et les flots de données entre ces exécutions ne sont pas des plus faciles à appréhender. Le choix de la technique dépend donc fortement du calcul à effectuer en fonction des propriétés de ce dernier. Il faut trouver un compromis entre la communication entre les sites qui est souvent coûteuse et la taille des données du calcul. Les facteurs limitant la vitesse d'un calcul parallèle sont : • • • • Le démarrage de la tâche Les synchronisations Les communications des données Les limites fixées par le compilateur, les bibliothèques, les outils, le système d'exploitation. Conclusion Comme je l'ai dit à plusieurs reprises, la conception d'un système de calcul parallèle et distribué est très fortement influencée par la ou les applications que devra faire fonctionner ce système. Mais une fois que l'architecture matérielle a été choisie, il convient également de bien choisir les algorithmes qui seront utilisés. En effet, certains mécanismes sont plus efficaces sur des architectures bien particulières. Par exemple, une application programmée à l'aide de threads sera plus adaptée sur des processeurs à mémoire partagée puisque les threads partagent le même espace mémoire. Si le volume des données est trop important pour tenir dans la mémoire d'un des processeurs, il est plus judicieux, si cela est possible, de la répartir sur les différents processeurs du système. Dans ce cas, les systèmes à base de mémoire distribuée seront à privilégier pour mieux répartir le volume des données entre les noeuds. Par contre, il est intéressant de noter, que si l'on souhaite lancer une même application sur des volumes limités de données différentes, les deux architectures peuvent convenir. Donc, si le système de calcul est amené à faire fonctionner un panel d'applications diverses, il peut être préférable de mélanger les deux types d'architecture mémoire et d'optimiser l'équilibrage de charge (et donc la répartition de la charge de travail) sur les processeurs les plus adaptés à la tâche. Conception d'un système à haute performance - Le calcul parallèle 6/6 Copyright © CETMEF 2004