Le calcul parallèle - Cerema Eau, mer et fleuves

publicité
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
Téléchargement