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