Parallel Computing
Parallel Computing
Parallel Computing
Onesime MBULAYI
Doctorant
Machine Learning & Bioinformatics
Email: mbulayi.onesime@unikin.ac.cd
Novembre 2022
Introduction
L’objectif principal du calcul parallèle est d’effectuer des calculs plus rapidement
que ceux qui peuvent être effectués avec un seul processeur en utilisant plusieurs
processeurs simultanément. Le besoin de solutions plus rapides et de résolution
de problèmes de plus grande taille se pose dans une grande variété d’applications.
Ceux-ci incluent la dynamique des fluides, les prévisions météorologiques, la
modélisation et la simulation de grands systèmes, le traitement et l'extraction
d'informations, le traitement d'images, l'intelligence artificielle et
l'automatisation de la fabrication.
Trois facteurs principaux ont contribué à la forte tendance actuelle en faveur du
traitement parallèle.
Premièrement, le coût du matériel a diminué régulièrement ; il est donc
désormais possible de construire des systèmes dotés de nombreux processeurs
à un coût raisonnable.
Introduction
Ces dernières années ont été marquées par une révolution dans les applications du
calcul scientifique à haute performance. Le séquençage du génome humain par le
Consortium international de séquençage du génome humain et Celera, Inc. a
ouvert de nouvelles frontières passionnantes en bioinformatique. La
caractérisation fonctionnelle et structurelle des gènes et des protéines promet de
comprendre et d’influencer fondamentalement les processus biologiques.
L’analyse de séquences biologiques en vue de développer de nouveaux
médicaments et remèdes contre des maladies et des pathologies nécessite des
algorithmes innovants ainsi qu’une puissance de calcul à grande échelle. En effet,
certaines des technologies de calcul parallèle les plus récentes sont spécifiquement
destinées aux applications en bioinformatique.
2. Applications scientifiques
Les progrès en physique et en chimie computationnelles se sont concentrés sur la
compréhension de processus allant des phénomènes quantiques aux structures
macromoléculaires. Celles-ci ont abouti à la conception de nouveaux matériaux, à la
compréhension des voies chimiques et à des processus plus efficaces. Les applications en
astrophysique ont exploré l'évolution des galaxies, les processus thermonucléaires et
l’analyse d’ensembles de données extrêmement volumineux provenant de télescopes. La
modélisation météorologique, la prospection minière, la prévision des inondations, etc.,
reposent en grande partie sur des ordinateurs parallèles et ont un impact très important sur
la vie quotidienne.
La bioinformatique et l'astrophysique présentent également certains des problèmes les plus
difficiles en ce qui concerne l'analyse d'ensembles de données extrêmement volumineux. Les
bases de données sur les protéines et les gènes (telles que PDB, SwissProt, ENTREZ et NDB)
ainsi que les ensembles de données Sky Survey (tels que Sloan Digital Sky Surveys)
représentent certains des plus grands ensembles de données scientifiques. L’analyse efficace
de ces ensembles de données nécessite une puissance de calcul considérable et constitue la
clé de découvertes scientifiques significatives.
3. Applications commerciales
Avec l'utilisation généralisée du Web et du contenu statique et dynamique associé,
l'accent est de plus en plus mis sur des serveurs rentables capables de fournir des
performances évolutives. Les plates-formes parallèles allant des multiprocesseurs
aux clusters Linux sont fréquemment utilisées comme serveurs Web et de bases de
données. Par exemple, les jours de gros volume, les grandes maisons de courtage
de Wall Street gèrent des centaines de milliers de sessions utilisateur simultanées
et des millions de commandes. Plateformes telles que IBM SP
des supercalculateurs et des serveurs Sun Ultra HPC alimentent ces sites critiques
pour l'entreprise. Bien que peu visibles, certains des plus grands réseaux de calcul
intensif sont hébergés à Wall Street.
La disponibilité de données sur les transactions à grande échelle a également
suscité un intérêt considérable pour l’exploration et l’analyse des données afin
d’optimiser les décisions commerciales et marketing.
Le volume considérable et la nature géographiquement répartie de ces données
nécessitent l'utilisation d'algorithmes parallèles efficaces pour résoudre des
problèmes tels que l'exploration de règles d'association, le regroupement, la
classification et l'analyse de séries chronologiques.
4. Applications dans les systèmes informatiques
À mesure que les systèmes informatiques deviennent omniprésents et que les
calculs se propagent sur le réseau, les problèmes de traitement parallèle
s’enracinent dans diverses applications. En sécurité informatique, la détection des
intrusions constitue un défi de taille. Dans le cas de la détection d'intrusion sur le
réseau, les données sont collectées sur des sites distribués et doivent être analysées
rapidement pour signaler une intrusion. L’impossibilité de collecter ces données à
un emplacement central pour analyse nécessite des algorithmes parallèles et
distribués efficaces. Dans le domaine de la cryptographie, certaines des
applications les plus spectaculaires du calcul parallèle sur Internet se sont
concentrées sur la factorisation d’entiers extrêmement grands.
Les systèmes embarqués s'appuient de plus en plus sur des algorithmes de
contrôle distribué pour accomplir diverses tâches. Une automobile moderne se
compose de dizaines de processeurs communiquant pour effectuer des tâches
complexes visant à optimiser la maniabilité et les performances.
Dans de tels systèmes, les algorithmes traditionnels parallèles et distribués pour la
sélection des leaders, l'ensemble indépendant maximal, etc., sont fréquemment
utilisés.
Alors que l'informatique parallèle s'est traditionnellement limitée aux plates-
formes dotées d'éléments de calcul et de réseau performants dans lesquels les
pannes et les erreurs ne jouent pas un rôle significatif, il existe des enseignements
précieux qui s'étendent aux calculs sur des environnements ad hoc, mobiles ou
défectueux.
I. PLAN
Exactitude :
Vérifier que les sous-tâches sont bien indépendantes.
Rendre les modèles de synchronisation sans blocage.
Clusters : vérification des modèles de communication.
Efficacité:
Attribuer des charges de travail égales à tous les processeurs.
Prise en compte du calcul et de la communication.
Optimisation pour un ordinateur parallèle spécifique.
Bibliographies
1. Pourquoi utiliser la programmation parallèle ?
Depuis que les systèmes informatiques ont évolué, ils ont commencé à fournir des
mécanismes qui nous permettent d'exécuter des éléments indépendants d'un
programme spécifique en parallèle les uns avec les autres, améliorant ainsi la
réponse et la performance générale. De plus, nous pouvons facilement vérifier que
les machines sont équipées de plus de processeurs et ceux-ci avec beaucoup plus
de cœurs. Alors pourquoi ne pas profiter de cette architecture ?
La programmation parallèle est une réalité dans tous les contextes de
développement de systèmes, depuis les téléphones intelligents et les tablettes
jusqu'à l'informatique intensive dans les centres de recherche. Une base solide en
programmation parallèle permettra à un développeur d'optimiser les
performances d'une application. Cela se traduit par une amélioration de
l'expérience utilisateur ainsi que par une consommation de ressources
informatiques, prenant ainsi moins de temps de traitement pour
l'accomplissement de tâches complexes.
À titre d'exemple de parallélisme, imaginons un scénario dans lequel une
application qui, entre autres tâches, sélectionne des informations dans une base de
données, et cette base de données a une taille considérable. Considérez également
que l'application est séquentielle, dans laquelle les tâches doivent être exécutées
les unes après les autres dans un ordre logique. Lorsqu'un utilisateur demande
des données, le reste du système sera bloqué jusqu'à ce que le retour des données
ne soit pas terminé. Cependant, en utilisant la programmation parallèle, nous
pourrons créer un nouveau travailleur qui cherchera des informations dans cette
base de données sans bloquer d'autres fonctions de l'application, améliorant ainsi
son utilisation.
Explorer les formes courantes de parallélisation
Il existe une certaine confusion lorsque l'on tente de définir les principales formes
de mise en parallèle systèmes. Il est courant de trouver des cotations sur des
systèmes parallèles et concurrents comme si les deux signifiaient la même chose.
Il existe néanmoins de légères différences entre eux. Dans la programmation
simultanée, nous avons un scénario dans lequel un programme envoie plusieurs
travailleurs et ces travailleurs se disputent pour utiliser le processeur pour
exécuter une tâche. L'étape à laquelle survient le conflit est contrôlée par le
planificateur CPU, dont la fonction est de définir quel travailleur est apte à utiliser
la ressource à un moment précis. Dans la plupart des cas, le planificateur du
processeur exécute la tâche de ratissage des processus si rapidement que nous
pourrions avoir l'impression d'un pseudo-parallélisme. La programmation
concurrente est donc une abstraction de la programmation parallèle.
Les systèmes simultanés se disputent le même processeur pour exécuter des
tâches.
Le diagramme suivant montre un schéma de programme concurrent
La programmation parallèle peut être définie comme une approche dans laquelle
les données du programme créent des travailleurs pour exécuter des tâches
spécifiques simultanément dans un environnement multicœur sans avoir besoin
de concurrence entre eux pour accéder à un processeur.
Les systèmes parallèles exécutent des tâches simultanément.
La figure suivante montre le concept de systèmes parallèles :
La programmation distribuée vise la possibilité de partager les traitements en
échangeant des données via des messages entre machines (nœuds) de calcul,
qui sont physiquement séparés. La programmation distribuée devient de plus en
plus populaire pour de nombreuses raisons : ils sont explorés comme suit :
Tolérance aux pannes : le système étant décentralisé, nous pouvons répartir le
traitement sur différentes machines d'un réseau, et ainsi effectuer une
maintenance individuelle de machines spécifiques sans affecter le fonctionnement
du système dans son ensemble.
• Évolutivité horizontale : Nous pouvons augmenter la capacité de traitement
dans les systèmes distribués en général. Nous pouvons relier de nouveaux
équipements sans avoir besoin d’interrompre l’exécution des applications. On
peut dire que c'est moins cher et plus simple par rapport à l'évolutivité verticale.
La programmation distribuée vise la possibilité de partager les traitements en
échangeant des données via des messages entre machines (nœuds) de calcul,
qui sont physiquement séparés. La programmation distribuée devient de plus
en plus populaire pour de nombreuses raisons : ils sont explorés comme suit :
Tolérance aux pannes : le système étant décentralisé, nous pouvons répartir le
traitement sur différentes machines d'un réseau, et ainsi effectuer une
maintenance individuelle de machines spécifiques sans affecter le
fonctionnement du système dans son ensemble.
Évolutivité horizontale : Nous pouvons augmenter la capacité de traitement
dans les systèmes distribués en général. Nous pouvons relier de nouveaux
équipements sans avoir besoin d’interrompre l’exécution des applications. On
peut dire que c'est moins cher et plus simple par rapport à l'évolutivité
verticale.
Cloud computing : Avec la réduction des coûts du matériel, nous avons besoin
de la croissance de ce type d'entreprise où nous pouvons disposer d'énormes
parcs de machines agissant de manière coopérative et exécutant des
programmes de manière transparente pour leurs utilisateurs.: Avec la réduction
des coûts du matériel, nous avons besoin de la croissance de ce type
d'entreprise où nous pouvons disposer d'énormes parcs de machines agissant
de manière coopérative et exécutant des programmes de manière transparente
pour leurs utilisateurs.
Les systèmes distribués exécutent des tâches au sein de nœuds physiquement séparés.
La figure suivante montre un schéma de système distribué :
3. Communiquer en programmation parallèle
Dans la programmation parallèle, les travailleurs envoyés pour effectuer une tâche
doivent souvent établir une communication afin qu’il puisse y avoir une
coopération pour résoudre un problème.
Dans la plupart des cas, cette communication est établie de telle manière que les
données puissent être échangées entre les travailleurs. Il existe deux formes de
communication plus connues en matière de programmation parallèle : l'état
partagé et la transmission de messages. Dans les sections suivantes, une brève
description des deux sera présentée.
Etat partagé
L’État partagé est l’une des formes de communication les plus connues parmi les
travailleurs. L'état partagé semble simple à utiliser mais comporte de nombreux
pièges car une opération non valide effectuée sur la ressource partagée par l'un
des processus affectera tous les autres, produisant ainsi de mauvais résultats. Cela
rend également impossible la distribution du programme entre plusieurs
machines pour des raisons évidentes.
Pour illustrer cela, nous utiliserons un cas réel. Supposons que vous soyez client
d’une banque spécifique et que cette banque n’ait qu’un seul caissier. Lorsque
vous vous rendez à la banque, vous devez faire la queue et attendre votre chance.
Une fois dans la file d'attente, vous remarquez qu'un seul client à la fois peut
utiliser le caissier et qu'il serait impossible pour le caissier de s'occuper de deux
clients simultanément sans risquer de commettre des erreurs. L'informatique
fournit des moyens d'accéder aux données de manière contrôlée, et il existe
plusieurs techniques, telles que le mutex.
Mutex peut être compris comme une variable de processus spéciale qui indique le niveau de
disponibilité pour accéder aux données. Autrement dit, dans notre exemple réel, le client a
un numéro, et à un moment précis, ce numéro sera activé et le caissier sera disponible
exclusivement pour ce client. A la fin du processus, ceci le client libérera la caisse pour le
client suivant, et ainsi de suite.
Il existe des cas dans lesquels les données ont une valeur constante dans une variable pendant
l'exécution du programme et les données sont partagées uniquement à des fins de lecture. Ainsi, le
contrôle d’accès n’est pas nécessaire car il ne présentera jamais de problèmes d’intégrité.
Messages Passing
La transmission de messages (Message passing) est utilisée lorsque nous visons à
éviter les problèmes de contrôle d’accès aux données et de synchronisation
provenant de l’état partagé. La transmission de messages consiste en un
mécanisme d'échange de messages dans les processus en cours d'exécution. Il est
très couramment utilisé chaque fois que nous développons des programmes à
architecture distribuée, où les échanges de messages au sein du réseau dans
lesquels ils sont placés sont nécessaires. Des langages comme Erlang, par exemple,
utilisent ce modèle pour implémenter la communication dans son architecture
parallèle. Une fois les données copiées à chaque échange de messages, il est
impossible que des problèmes surviennent en termes de concurrence d'accès. Bien
que l’utilisation de la mémoire semble être plus élevée que dans l’état de mémoire
partagée, l’utilisation de ce modèle présente des avantages. Ils sont les suivants :
Avantages à l’utilisation de ce modèle. Ils sont les suivants :
Absence de concurrence en matière d’accès aux données
Les messages peuvent être échangés localement (processus divers) ou dans des
environnements distribués
Cela rend moins probable l'apparition de problèmes d'évolutivité et permet
l'interopérabilité des différents systèmes.
En général, il est facile à maintenir selon les programmeurs
Problèmes de programmation parallèle
Il existe des problèmes classiques auxquels les courageux guerriers du clavier peuvent être
confrontés lorsqu'ils se battent dans les pays où habitent les fantômes de la programmation
parallèle. Beaucoup de ces problèmes surviennent plus souvent lorsque des programmeurs
inexpérimentés utilisent des travailleurs combinés à un état partagé. Certaines de ces
questions seront décrites dans les sections suivantes.
Deadlock
Deadlock (blocage ou Impasse) est une situation dans laquelle deux travailleurs ou plus
attendent indéfiniment la libération d'une ressource, qui est bloquée par un travailleur du
même groupe pour une raison quelconque. Pour une meilleure compréhension, nous
utiliserons un autre cas réel. Imaginez la banque dont l'entrée a une porte tournante. Le client
A se dirige sur le côté, ce qui lui permettra d'entrer dans la banque, tandis que le client B
tente de sortir de la banque en utilisant le côté entrée de cette porte tournante afin que les
deux clients soient coincés en forçant la porte mais ne se dirigeant nulle part. Cette situation
serait hilarante dans la vraie vie mais tragique en programmation.
Le blocage est un phénomène dans lequel les processus attendent une condition pour libérer leurs
tâches, mais cette condition ne se produira jamais.
Starvation
Il s'agit d'un problème dont les effets secondaires sont causés par le ratissage
injuste d'un ou plusieurs processus qui prennent beaucoup plus de temps pour
exécuter une tâche. Imaginez un groupe de processus, A, qui exécute des tâches
lourdes et a la priorité sur le processeur de données. Imaginez maintenant qu'un
processus A avec une priorité élevée consomme constamment le processeur,
tandis qu'un processus B de priorité inférieure n'en a jamais l'occasion. Par
conséquent, on peut dire que le processus B manque de cycles CPU.
Starvation est causée par des politiques de classement des processus mal ajustées.
Race conditions
Lorsque le résultat d'un processus dépend d'une séquence de faits, et que cette
séquence est cassé en raison du manque de mécanismes de synchronisation, nous
sommes confrontés à des conditions de concurrence. Ils résultent de problèmes
extrêmement difficiles à filtrer dans des systèmes plus importants. Par exemple,
un couple possède un compte joint ; le solde initial avant opérations est 100 $. Le
tableau suivant montre le cas normal, dans lequel il existe des mécanismes de
protection et l’enchaînement des faits attendus, ainsi que le résultat :
Dans le tableau suivant, le scénario problématique est présenté. Supposons que le
compte ne dispose pas de mécanismes de synchronisation et que l'ordre des
opérations ne soit pas celui attendu.
Il existe une incohérence notable dans le résultat final en raison du manque inattendu de
synchronisation dans la séquence des opérations. L'une des caractéristiques de la
programmation parallèle est le non-déterminisme. Il est impossible de prévoir le moment où
deux ouvriers se présenteront, ni même lequel d'entre eux se présentera en premier. Les
mécanismes de synchronisation sont donc essentiels.
Le non-déterminisme, s'il est combiné à l'absence de mécanismes de synchronisation, peut
entraîner des problèmes de concurrence critique.
Les Outils de programmation parallèle avec Python
Le langage Python, créé par Guido Van Rossum, est un langage multi-paradigmes
et langue polyvalente. Il a été largement accepté dans le monde entier en raison de
sa puissance, de sa simplicité et de son entretien facile. On l'appelle aussi la langue
qui a des piles incluses. Il existe une large gamme de modules pour rendre son
utilisation plus fluide. Dans la programmation parallèle, Python possède des
modules intégrés et externes qui simplifient la mise en œuvre.
Le module Threading en Python
Le module de thread Python offre une couche d'abstraction au module _thread,
qui est un module de niveau inférieur. Il fournit des fonctions qui aident le
programmeur dans la tâche difficile de développer des systèmes parallèles basés
sur des threads. Les documents officiels du module de threading peuvent être
trouvés sur Search — Python 3.11.5 documentation
Le module multiprocessing Python
Le module multiprocessing vise à fournir une API simple pour l'utilisation du
parallélisme basé sur les processus. Ce module est similaire au module threading,
qui simplifie les alternances entre les processus sans difficultés majeures.
L'approche basée sur les processus est très populaire au sein de la communauté
des utilisateurs de Python car elle constitue une alternative aux réponses aux
questions sur l'utilisation des threads CPU-Bound et du GIL présents dans
Python. Les documents officiels du module multitraitement peuvent être trouvés
sur
http://docs.python.org/3/library/multiprocessing.html
Le module Python parallèle est externe et propose une API riche pour la création
de systèmes parallèles et distribués utilisant l'approche processus. Ce module
promet d'être léger et facile à installer, et s'intègre à d'autres programmes Python.
Le module Python parallèle peut être trouvé à l'adresse
The parallel Python module is external and offers a rich API for the creation of
parallel and distributed systems making use of the processes approach. This
module promises to be light and easy to install, and integrates with other Python
programs. The parallel Python module can be found at http://parallelpython.com
Parmi certaines fonctionnalités, nous pouvons souligner les suivantes :
Détection automatique de la configuration optimale
Le fait qu'un certain nombre de processus de travail peuvent être modifiés
pendant l'exécution
Equilibrage de charge dynamique
Tolérance aux pannes
Découverte automatique des ressources informatiques
Celeri – une file d'attente de tâches distribuée
Celery est un excellent module Python utilisé pour créer des systèmes distribués et
doté d'une excellente documentation. Il utilise au moins trois types d'approches
différents pour exécuter des tâches sous forme simultanée : multiprocessing,
Eventlet et Gevent. Ce travail concentrera cependant les efforts sur l’utilisation de
l’approche multiprocessing. Aussi, le lien entre les uns et les autres est une
question de configuration, et il reste une étude pour que le lecteur puisse établir
des comparaisons avec ses propres expériences. Le module Celery peut être
obtenu sur la page officielle du projet à l'adresse http://celeryproject.org
Python GIL
GIL est un mécanisme utilisé dans l'implémentation du Python standard, connu sous le nom
de CPython, pour éviter les bytecodes exécutés simultanément par différents threads.
L’existence de GIL en Python suscite de vives discussions parmi les utilisateurs de ce
langage. GIL a été choisi pour protéger la mémoire interne utilisée par l'interpréteur
Cpython, qui n'implémente pas de mécanismes de synchronisation pour les accès
concurrents des threads. Dans tous les cas, GIL pose un problème lorsque nous décidons
d'utiliser des threads, et ceux-ci ont tendance à être liés au processeur. Les threads d'E/S, par
exemple, sortent du champ d'application de GIL. Peut-être que le mécanisme apporte plus
d’avantages à l’évolution de Python que de mal. Évidemment, on ne peut pas considérer
uniquement la vitesse comme un seul argument pour déterminer si quelque chose est bon ou
non.
Il existe des cas dans lesquels l'approche de l'utilisation de processus pour des tâches axée
sur la transmission de messages apporte de meilleures relations entre maintenabilité,
évolutivité et performances. Même ainsi, il existe des cas dans lesquels il y aura un réel
besoin de threads, qui seraient soumis à GIL.
Dans ces cas-là, il est possible d’écrire des morceaux de code sous forme
d’extensions en langage C et de les intégrer dans le programme Python. Il existe
donc des alternatives ; c'est au développeur d'analyser la réelle nécessité. Alors se
pose la question : GIL, d’une manière générale, est-il un méchant ? Il est important
de rappeler que l'équipe PyPy travaille sur une implémentation STM afin de
supprimer GIL de Python. Pour plus de détails sur le projet, visitez http://
pypy.org/tmdonate.html
Summary
Dans ce chapitre, nous avons appris quelques concepts de programmation
parallèle et découvert certains modèles, leurs avantages et leurs inconvénients.
Certains des problèmes et problèmes potentiels liés à la réflexion sur le
parallélisme ont été présentés dans de brèves explications. Nous avons également
eu une brève introduction à certains modules Python, intégrés et externes, qui
facilitent la vie du développeur lors de la construction de systèmes parallèles.
Dans le prochain chapitre, nous étudierons quelques techniques pour concevoir
des algorithmes parallèles.
Chapitre 2 : Concevoir des algorithmes
parallèles
Introduction
La conception d’algorithmes parallèles ne se réduit pas facilement à de simples
recettes. Cela nécessite plutôt le type de pensée intégrative que l'on appelle
communément « créativité ». Cependant, cela peut bénéficier d'une approche
méthodique qui maximise l'éventail des options envisagées, qui fournit des
mécanismes pour évaluer les alternatives et qui réduit les coûts. de revenir sur de
mauvais choix. Nous décrivons une telle approche et illustrons son application à
une série de problèmes. Notre objectif est de proposer un cadre dans lequel la
conception d’algorithmes parallèles peut être explorée. Ce faisant, nous espérons
que vous développerez votre intuition quant à ce qui constitue un bon algorithme
parallèle.
Après avoir étudié ce chapitre, vous devriez être capable de concevoir des
algorithmes parallèles simples de manière méthodique et de reconnaître les
défauts de conception qui compromettent l'efficacité ou l'évolutivité.
Vous devez être capable de partitionner les calculs, en utilisant à la fois des
techniques de décomposition de domaine et fonctionnelles, et savoir reconnaître et
mettre en œuvre des structures de communication locales et globales, statiques et
dynamiques, structurées et non structurées, et synchrones et asynchrones. Vous
devez également être capable d'utiliser l'agglomération comme moyen de réduire
les coûts de communication et de mise en œuvre et devez être familier avec une
gamme de stratégies d'équilibrage de charge.
2.1 Conception méthodique
La plupart des problèmes de programmation ont plusieurs solutions parallèles. La
meilleure solution peut différer de celle suggérée par les algorithmes séquentiels
existants. La méthodologie de conception que nous décrivons vise à favoriser une
approche exploratoire de la conception dans laquelle les problèmes indépendants
de la machine, tels que la concurrence, sont pris en compte dès le début et les
aspects de conception spécifiques à la machine sont retardés jusque tard dans le
processus de conception. Cette méthodologie structure le processus de conception
en quatre étapes distinctes : cloisonnement, communication, agglomération et
cartographie. (L'acronyme PCAM peut servir de rappel utile de cette structure.)
Dans les deux premières étapes, nous nous concentrons sur la concurrence et
l'évolutivité et cherchons à découvrir des algorithmes présentant ces qualités. Aux
troisième et quatrième étapes, l’attention se porte sur la localité et sur d’autres
questions liées à la performance. Les quatre étapes sont illustrées dans la figure 2.1
et peuvent être résumées comme suit :
Partitionnement. Le calcul à effectuer et les données exploitées par ce calcul sont
décomposés en petites tâches. Les problèmes pratiques tels que le nombre de processeurs
dans l'ordinateur cible sont ignorés et l'attention se concentre sur la reconnaissance des
opportunités d'exécution parallèle.
Communications. La communication requise pour coordonner l'exécution des tâches est
déterminée et les structures et algorithmes de communication appropriés sont définis.
Agglomération. Les structures de tâches et de communication définies au cours des deux
premières étapes d'une conception sont évaluées en fonction des exigences de
performance et des coûts de mise en œuvre. Si nécessaire, les tâches sont combinées en
tâches plus vastes pour améliorer les performances ou réduire les coûts de
développement.
Cartographie. Chaque tâche est attribuée à un processeur de manière à tenter de satisfaire
les objectifs concurrents consistant à maximiser l'utilisation du processeur et à minimiser
les coûts de communication. Le mappage peut être spécifié de manière statique ou
déterminé au moment de l'exécution par des algorithmes d'équilibrage de charge.
Le résultat de ce processus de conception peut être un programme qui crée et détruit des
tâches de manière dynamique, en utilisant des techniques d'équilibrage de charge pour
contrôler le mappage des tâches aux processeurs. Alternativement, il peut s'agir d'un
programme SPMD qui crée exactement une tâche par processeur. Le même processus de
découverte d'algorithmes s'applique dans les deux cas, bien que si l'objectif est de produire
un programme SPMD, les problèmes associés à la cartographie sont intégrés dans la phase
d'agglomération de la conception.
La conception d'algorithmes est présentée ici comme une activité séquentielle. En pratique,
cependant, il s’agit d’un processus très parallèle, dans lequel de nombreuses préoccupations
sont prises en compte simultanément. De plus, même si nous cherchons à éviter les retours
en arrière, l'évaluation d'une conception partielle ou complète peut nécessiter des
modifications des décisions de conception prises lors des étapes précédentes.
Dans les lignes qui suivent nous présentons un examen détaillé des quatre étapes du
processus de conception. réalistes.
2.2 Partitionnement
L'étape de partitionnement d'une conception vise à exposer les opportunités
d'exécution parallèle. Par conséquent, l’accent est mis sur la définition d’un grand
nombre de petites tâches afin d’obtenir ce que l’on appelle une décomposition fine
d’un problème. Tout comme le sable fin se coule plus facilement qu’un tas de
briques, une décomposition à grain fin offre la plus grande flexibilité en termes
d’algorithmes parallèles potentiels. Lors des étapes de conception ultérieures,
l'évaluation des exigences de communication, de l'architecture cible ou des
problèmes d'ingénierie logicielle peut nous conduire à renoncer aux opportunités
d'exécution parallèle identifiées à ce stade. Une bonne partition divise en petits
morceaux à la fois le calcul associé à un problème et les données sur lesquelles ce
calcul opère. Lors de la conception d'une partition, les programmeurs se
concentrent généralement d'abord sur les données associées à un problème, puis
déterminent une partition appropriée pour les données et enfin déterminent
comment associer le calcul aux données.
Cette technique de partitionnement est appelée décomposition de domaine. L'approche
alternative consistant à décomposer d'abord le calcul à effectuer puis à traiter les données est
appelée décomposition fonctionnelle. Ce sont des techniques complémentaires qui peuvent
être appliquées à différentes composantes d’un même problème ou même appliquées au
même problème pour obtenir des algorithmes parallèles alternatifs.
Dans cette première étape d’une conception, nous cherchons à éviter la réplication des
calculs et des données ; c'est-à-dire que nous cherchons à définir des tâches qui divisent à la
fois le calcul et les données en ensembles disjoints. Comme la granularité, c’est un aspect de
la conception sur lequel nous reviendrons peut-être plus tard. Il peut être intéressant de
répliquer les calculs ou les données si cela nous permet de réduire les besoins en
communication.
2.2.1 Décomposition du domaine
Dans l'approche de décomposition de domaine pour le partitionnement des problèmes, nous
cherchons d'abord à décomposer les données associées à un problème. Si possible, nous
divisons ces données en petits morceaux de taille à peu près égale. Ensuite, nous
partitionnons le calcul à effectuer, généralement en associant chaque opération aux données
sur lesquelles elle opère. Ce partitionnement génère un certain nombre de tâches, chacune
comprenant des données et un ensemble d'opérations sur ces données. Une opération peut
nécessiter des données provenant de plusieurs tâches. Dans ce cas, une communication est
nécessaire pour déplacer les données entre les tâches. Cette exigence est abordée dans la
phase suivante du processus de conception. Les données décomposées peuvent être l'entrée
du programme, la sortie calculée par le programme ou des valeurs intermédiaires
maintenues par le programme. Différentes partitions peuvent être possibles, basées sur
différentes structures de données. Les bonnes règles empiriques consistent à se concentrer
d’abord sur la structure de données la plus grande ou sur la structure de données la plus
fréquemment consultée.
2.2.2 Décomposition fonctionnelle
La décomposition fonctionnelle représente une manière différente et complémentaire de
penser les problèmes. Dans cette approche, l’accent est initialement mis sur le calcul à
effectuer plutôt que sur les données manipulées par le calcul. Si nous parvenons à diviser ce
calcul en tâches disjointes, nous examinons les exigences en matière de données de ces
tâches. Ces exigences en matière de données peuvent être disjointes, auquel cas la partition
est complète. Alternativement, ils peuvent se chevaucher de manière significative, auquel cas
une communication considérable sera nécessaire pour éviter la réplication des données. C’est
souvent le signe qu’une approche de décomposition de domaine devrait plutôt être
envisagée. Alors que la décomposition de domaine constitue la base de la plupart des
algorithmes parallèles, la décomposition fonctionnelle est précieuse en tant que manière
différente d’appréhender les problèmes. Pour cette seule raison, il convient d’en tenir compte
lors de l’exploration d’algorithmes parallèles possibles. Un focus sur les calculs qui doivent
être réalisées peuvent parfois révéler la structure d'un problème, et donc des opportunités
d'optimisation, qui ne seraient pas évidentes à partir d'une seule étude des données.
Exemple: Arbre de recherche à la recherche de nœuds qui correspondent à des « solutions ».
une décomposition fonctionnelle qui divise non seulement le calcul à effectuer mais
également le code qui effectue ce calcul est susceptible de réduire la complexité de la
conception globale. C'est souvent le cas dans les modèles informatiques de systèmes
complexes, qui peuvent être structurés comme des collections de modèles plus simples
connectés via des interfaces. Par exemple, une simulation du climat terrestre peut
comprendre des composants représentant l'atmosphère, l'océan, l'hydrologie, la glace, les
sources de dioxyde de carbone, etc. Bien que chaque composant puisse être plus
naturellement parallélisé en utilisant des techniques de décomposition de domaine,
l'algorithme parallèle dans son ensemble est plus simple si le système est d'abord décomposé
à l'aide de techniques de décomposition fonctionnelle, même si ce processus ne produit pas
un grand nombre de tâches .
une décomposition fonctionnelle qui divise non seulement le calcul à effectuer mais
également le code qui effectue ce calcul est susceptible de réduire la complexité de la
conception globale. C'est souvent le cas dans les modèles informatiques de systèmes
complexes, qui peuvent être structurés comme des collections de modèles plus simples
connectés via des interfaces. Par exemple, une simulation du climat terrestre peut
comprendre des composants représentant l'atmosphère, l'océan, l'hydrologie, la glace, les
sources de dioxyde de carbone, etc. Bien que chaque composant puisse être plus
naturellement parallélisé en utilisant des techniques de décomposition de domaine,
l'algorithme parallèle dans son ensemble est plus simple si le système est d'abord décomposé
à l'aide de techniques de décomposition fonctionnelle, même si ce processus ne produit pas
un grand nombre de tâches .
2.2.3 Liste de contrôle de conception de partitionnement(Partitioning Design Checklist)
La différence numéro entre un core et un thread est qu’un core est physiquement
présent sur la puce électronique qui constitue le processeur. C’est-à-dire que
vous pouvez distinguer physiquement chaque core de processeur sur le circuit
électronique du processeur.
I.La principale différence entre un Core et un Thread
Les Threads sont uniquement logiques. C’est une séquence d’instruction que doit
exécuter le processeur. Avec le multi-threading le processeur prend en charge 2
threads à la fois. Cela dit les CPUs core doivent être conçus pour supporter
plusieurs threads aussi appelé le Multi-threading.
Un core qui n’est pas conçu et fabriqué pour supporter le multi-threading ne peut
pas les activer avec une mise à jour logiciel. Cela dit certains processeurs
peuvent supporter plusieurs threads, c’est juste que le fabricant à décider de
désactiver cette fonctionnalité pour segmenter sa gamme de produit. Aujourd’hui
avec la technologie du multi-threading un processeur gérer 2 threads à fois. On
les appelle alors des « cœurs logiques » par opposition au « cœurs physiques ».
Cœur avec Multithreading et Cœur sans Multi-threading
Avoir des cœurs logiques en plus par exemple un CPU qui 4 cœurs
et 8 Threads, sera plus performant qu’un CPU avec 4 cœurs et 4
threads. En effet un processeur 6 cœurs sera en général plus
performant que notre i7 à 8 threads (à supposer qu’il s’agit de la
même architecture CPU)
Processus et Threads
Plateformes de programmation parallèle :
La vision logique traditionnelle d'un ordinateur séquentiel consiste en une
mémoire connectée à un processeur via un chemin de données. Ces trois
composants - processeur, mémoire et chemin de données - constituent des
goulots d'étranglement pour le taux de traitement global d'un système
informatique. Au fil des ans, un certain nombre d'innovations architecturales ont
permis de remédier à ces goulets d'étranglement. L'une des innovations les plus
importantes est la multiplicité - dans les unités de traitement, les chemins de
données et les unités de mémoire. Cette multiplicité est soit entièrement cachée
au programmeur, comme dans le cas du parallélisme implicite, soit exposée au
programmeur sous différentes formes. D
Pipelining
Les processeurs s'appuient depuis longtemps sur les pipelines pour améliorer les
taux d'exécution. En superposant différentes étapes de l'exécution des
instructions (récupération, ordonnancement, décodage, récupération des
opérandes, exécution, stockage, entre autres), le pipelining permet une exécution
plus rapide. Le pipelining est une technique qui consiste à diviser une tâche en
plusieurs sous-tâches et à exécuter ces sous-tâches en parallèle avec plusieurs
unités matérielles. Dans un processeur de pipelining, plusieurs instructions sont
exécutées simultanément à différents stades du cycle d'instruction dans
différentes sections du processeur. L'objectif du traitement en pipeline est
d'augmenter le débit grâce au chevauchement entre l'exécution des tâches
consécutives. Il existe deux types de pipelines : Pipeline d'instructions et
Pipeline arithmétique.
Pipelining
Un pipeline d'instructions divise les actions d'un cycle d'instructions en
plusieurs étapes qui sont exécutées une par une dans différentes sections du
processeur. Le nombre de sections dans le pipeline est conçu par l'architecte
informatique.
Considérons que le cycle d'instruction est divisé en quatre étapes :
Récupération des instructions (IR)
Décodage des instructions (ID)
Exécution (EX)
Résultat d'écriture (WR)
Supposons qu'il y ait m instructions et que ces instructions soient exécutées dans
n sections du processeur pipeline.
Processeurs à mots d'instruction très longs
Le temps pris pour la première instruction = ntc , où tc est la durée du cycle
d'horloge.
Temps pris pour les (m-1) instructions restantes = (m-1)tc
Temps total pris pour m instructions = ntc + (m-1)tc = (n+m-1)tc
Si le processeur n'est pas pipellé, alors le temps total pris pour m instructions
est mntc .
Par conséquent, le gain de performance dû au pipeline = mntc /(m+n-1)tc
Un moyen évident d'améliorer le taux d'exécution des instructions au-delà de ce
niveau est d'utiliser des pipelines multiples. Au cours de chaque cycle d'horloge,
plusieurs instructions sont introduites dans le processeur en parallèle. Ces
instructions sont exécutées sur plusieurs unités fonctionnelles.
Processeurs à mots d'instruction très longs (VLIW) :
Le parallélisme extrait par les processeurs superscalaires est souvent limité par le
look- ahead des instructions. La logique matérielle pour l'analyse de dépendance
dynamique est typiquement de l'ordre de 5-10% de la logique totale sur les
microprocesseurs conventionnels (environ 5% sur le superscalaire à quatre voies
Sun UltraSPARC). Cette complexité croît à peu près quadratiquement avec le
nombre de problèmes et peut devenir un goulot d'étranglement.
Un autre concept d'exploitation du parallélisme au niveau des instructions utilisé
dans les processeurs VLIW (very long instruction word) s'appuie sur le
compilateur pour résoudre les dépendances et la disponibilité des ressources au
moment de la compilation. Les instructions qui peuvent être exécutées
simultanément sont regroupées et transmises au processeur sous la forme d'un
seul long mot d'instruction (d'où son nom) pour être exécutées sur plusieurs
unités fonctionnelles en même temps.
Processeurs à mots d'instruction très longs (VLIW) :
Le concept VLIW présente à la fois des avantages et des inconvénients par
rapport aux processeurs superscalaires. L'ordonnancement étant effectué par
logiciel, les mécanismes de décodage et d'émission des instructions sont plus
simples dans les processeurs VLIW. Le compilateur dispose d'un contexte plus
large pour sélectionner les instructions et peut utiliser une variété de
transformations pour optimiser le parallélisme par rapport à une unité d'émission
matérielle. Des instructions parallèles supplémentaires sont généralement mises à
la disposition du compilateur pour contrôler l'exécution parallèle. les
performances des processeurs VLIW sont très sensibles à la capacité des
compilateurs à détecter les dépendances de données et de ressources ainsi que les
risques de lecture et d'écriture, et à programmer les instructions pour un
parallélisme maximal.
Le déroulement des boucles, la prédiction des branchements et l'exécution
spéculative jouent tous un rôle important dans les performances des processeurs
VLIW. Si les processeurs superscalaires et VLIW ont réussi à exploiter le
parallélisme implicite, ils sont généralement limités à des échelles de
concurrence plus petites, de l'ordre du parallélisme à quatre ou huit voies.
Limites de la performance du système de mémoire
L'exécution effective d'un programme sur un ordinateur ne dépend pas seulement
de la vitesse du processeur, mais aussi de la capacité du système de mémoire à
fournir des données au processeur. Au niveau logique, un système de mémoire,
éventuellement constitué de plusieurs niveaux d'antémémoire, reçoit une requête
pour un mot de mémoire et renvoie un bloc de données de taille b contenant le
mot demandé après l nanosecondes. Ici, l est appelé la latence de la mémoire. La
vitesse à laquelle les données peuvent être pompées de la mémoire vers le
processeur détermine la bande passante du système de mémoire.
Dichotomie des plates-formes de calcul parallèle
Nous explorons d'abord une dichotomie basée sur l'organisation logique et
physique des plates-formes parallèles. L'organisation logique fait référence à la
vision de la plate-forme par le programmeur, tandis que l'organisation physique
fait référence à l'organisation matérielle réelle de la plate-forme. Du point de vue
du programmeur, les deux composantes essentielles du calcul parallèle sont les
moyens d'exprimer les tâches parallèles et les mécanismes permettant de
spécifier
l'interaction entre ces tâches. Le premier élément est parfois appelé structure de
contrôle et le second, modèle de communication.
Dichotomie des plates-formes de calcul parallèle
1. Structure de contrôle des plates-formes parallèles
Les tâches parallèles peuvent être spécifiées à différents niveaux de granularité.
À un extrême, chaque programme d'un ensemble de programmes peut être
considéré comme une tâche parallèle. À l'autre extrême, les instructions
individuelles dans un programme peuvent être considérés comme des tâches
parallèles. Entre ces deux extrêmes se trouve une gamme de modèles permettant
de spécifier la structure de contrôle des programmes et le support architectural
correspondant. Exemple : Parallélisme à partir d'une seule instruction sur
plusieurs processeurs Considérons le segment de code suivant qui additionne
deux vecteurs :
for (i = 0 ; i < 1000 ; i++)
c[i] = a[i] + b[i] ;
Dans cet exemple, les différentes itérations de la boucle sont indépendantes les
unes des autres ; c'està-dire que c[0] = a[0] + b[0] ; c[1] = a[1] + b[1] ; , etc.
peuvent toutes être exécutées indépendamment les unes des autres. Par
conséquent, s'il existe un mécanisme permettant d'exécuter la même instruction,
dans ce cas ajouter sur tous les processeurs avec les données appropriées, nous
pourrions exécuter cette boucle beaucoup plus rapidement.
Les unités de traitement des ordinateurs parallèles fonctionnent soit sous le
contrôle centralisé d'une seule unité de commande, soit de manière indépendante.
Dans les architectures appelées SIMD (single instruction stream, multiple data
stream), une seule unité de commande envoie des instructions à chaque unité de
traitement. Dans un ordinateur parallèle SIMD, la même instruction est exécutée
de manière synchrone par toutes les unités de traitement.
Nota: Si le concept SIMD fonctionne bien pour les calculs structurés sur des
structures de données parallèles telles que les tableaux, il est souvent nécessaire
de désactiver sélectivement les opérations sur certains éléments de données. Pour
cette raison, la plupart des paradigmes de programmation SIMD prévoient un
"masque d'activité". Il s'agit d'un masque binaire associé à chaque élément de
données et opération qui spécifie s'il doit participer à l'opération ou non. Des
primitives telles que where (condition) then <stmnt> <elsewhere stmnt> sont
utilisées pour prendre en charge l'exécution sélective. L'exécution conditionnelle
peut être préjudiciable aux performances des processeurs SIMD et doit donc être
utilisée avec précaution.
Contrairement aux architectures SIMD, les ordinateurs dans lesquels chaque
élément de traitement est capable d'exécuter un programme différent
indépendamment des autres éléments de traitement sont appelés ordinateurs
MIMD (multiple instruction stream, multiple data stream). Une variante simple
de ce modèle, appelée modèle SPMD (single program multiple data), repose sur
l'exécution de plusieurs instances du même programme sur des données
différentes. Il est facile de voir que le modèle SPMD a la même expressivité que
le modèle MIMD puisque chacun des multiples programmes peut être inséré
dans un grand bloc if-else dont les conditions sont spécifiées par les
identificateurs de tâches.
Modèle de communication des plates-formes parallèles
Il existe deux formes principales d'échange de données entre les tâches parallèles
: l'accès à un espace de données partagé et l'échange de messages.
Plateformes d'espace d'adressage partagé
La vision "espace d'adressage partagé" d'une plate-forme parallèle prend en
charge un espace de données commun accessible à tous les processeurs. Les
processeurs interagissent en modifiant les objets de données stockés dans cet
espace d'adressage partagé. Les plates-formes à espace d'adressage partagé
prenant en charge la programmation SPMD sont également appelées
multiprocesseurs. La mémoire des plates-formes à espace d'adressage partagé
peut être locale (exclusive à un processeur) ou globale (commune à tous les
processeurs).
Modèle de communication des plates-formes parallèles
Si le temps mis par un processeur pour accéder à n'importe quel mot de mémoire
du système (global ou local) est identique, la plate-forme est classée comme un
multi-ordinateur à accès mémoire uniforme (UMA). En revanche, si le temps
d'accès à certains mots de mémoire est plus long que d'autres, la plate-forme est
qualifiée de multi-ordinateur à accès mémoire non uniforme (NUMA). Par
conséquent, même un uniprocesseur ne serait pas qualifié d'UMA si les temps
d'accès au cache sont pris en compte. Pour cette raison, nous définissons les
architectures NUMA et UMA uniquement en termes de temps d'accès à la
mémoire et non de temps d'accès au cache. La distinction entre les plateformes
UMA et NUMA est importante. Si l'accès à la mémoire locale est moins cher que
l'accès à la mémoire globale, les algorithmes doivent tenir compte de la localité
et structurer les données et les calculs en conséquence.
Plateformes de transmission de messages
La vue logique de la machine d'une plate-forme à passage de messages se
compose de p nœuds de traitement, chacun ayant son propre espace d'adressage
exclusif. Chacun de ces nœuds de traitement peut être soit un processeur unique,
soit un multiprocesseur à espace d'adressage partagé - une tendance qui gagne
rapidement du terrain dans les ordinateurs parallèles modernes à passage de
messages. Les exemples d'une telle vue proviennent naturellement des stations
de travail en grappe et des multi-ordinateurs à espace d'adressage non partagé.
Sur ces plates-formes, les interactions entre les processus s'exécutant sur
différents nœuds doivent être accomplies à l'aide de messages, d'où le nom de
passage de messages. Cet échange de messages est utilisé pour transférer des
données, du travail et pour synchroniser les actions entre les processus. Dans sa
forme la plus générale, les paradigmes de passage de messages permettent
l'exécution d'un programme différent sur chacun des p nœuds.
Plateformes de transmission de messages
La vue logique de la machine d'une plate-forme à passage de messages se
compose de p nœuds de traitement, chacun ayant son propre espace d'adressage
exclusif. Chacun de ces nœuds de traitement peut être soit un processeur unique,
soit un multiprocesseur à espace d'adressage partagé - une tendance qui gagne
rapidement du terrain dans les ordinateurs parallèles modernes à passage de
messages. Les exemples d'une telle vue proviennent naturellement des stations
de travail en grappe et des multi-ordinateurs à espace d'adressage non partagé.
Sur ces plates-formes, les interactions entre les processus s'exécutant sur
différents nœuds doivent être accomplies à l'aide de messages, d'où le nom de
passage de messages. Cet échange de messages est utilisé pour transférer des
données, du travail et pour synchroniser les actions entre les processus. Dans sa
forme la plus générale, les paradigmes de passage de messages permettent
l'exécution d'un programme différent sur chacun des p nœuds.
Les interactions étant réalisées par l'envoi et la réception de messages, les
opérations de base de ce paradigme de programmation sont l'envoi et la réception
(les appels correspondants peuvent différer d'une API à l'autre, mais la
sémantique est largement identique). En outre, comme les opérations d'envoi et
de réception doivent spécifier des adresses cibles, il doit exister un mécanisme
permettant d'attribuer une identification unique ou ID à chacun des multiples
processus exécutant un programme parallèle. Cet ID est généralement mis à la
disposition du programme à l'aide d'une fonction telle que whoami, qui renvoie
au processus appelant son ID. Une autre fonction est généralement nécessaire
pour compléter l'ensemble de base des opérations de passage de messages -
numprocs, qui spécifie le nombre de processus participant à l'ensemble. Avec ces
quatre opérations de base, il est possible d'écrire n'importe quel programme de
passage de messages.
Différentes API de passage de messages, telles que l'interface MPI (Message
Passing Interface) et la PVM (Parallel Virtual Machine), prennent en charge ces
opérations de base et une variété de fonctionnalités de plus haut niveau sous
différents noms de fonctions.
Organisation physique des plates-formes parallèles
Architecture d'un ordinateur parallèle idéal
Une extension naturelle du modèle de calcul en série (la machine à accès
aléatoire, ou RAM) consiste en p processeurs et une mémoire globale de taille
non limitée, accessible uniformément à tous les processeurs. Tous les processeurs
accèdent au même espace d'adressage. Les processeurs partagent une horloge
commune mais peuvent exécuter des instructions différentes dans chaque cycle.
Ce modèle idéal est également appelé machine parallèle à accès aléatoire
(PRAM). Étant donné que les PRAM permettent l'accès simultané à divers
emplacements de mémoire, selon la façon dont les accès simultanés à la mémoire
sont traités, les PRAM peuvent être divisées en quatre sous-classes.
Organisation physique des plates-formes parallèles
PRAM à lecture exclusive et écriture exclusive (EREW). Dans cette classe,
l'accès à un emplacement mémoire est exclusif. Aucune opération
concurrente de lecture ou d'écriture n'est autorisée. Il s'agit du modèle de
PRAM le plus faible, offrant une concurrence minimale dans l'accès à la
mémoire.
PRAM à lecture simultanée et écriture exclusive (CREW). Dans cette classe,
les accès multiples en lecture à un emplacement mémoire sont autorisés.
Cependant, les accès multiples en écriture à un emplacement de mémoire sont
sérialisés.
PRAM à lecture exclusive et écriture simultanée (ERCW). Les accès
multiples en écriture sont autorisés sur un emplacement mémoire, mais les
accès multiples en lecture sont sérialisés.
Organisation physique des plates-formes parallèles
PRAM à lecture et écriture simultanées (CRCW). Cette classe permet des
accès multiples en lecture et en écriture à un emplacement mémoire commun.
Il s'agit du modèle de PRAM le plus puissant.
Python Parallel Programming
Organisation physique des plates-formes parallèles
Ce dont vous avez besoin pour la pratique. Tous les exemples sera et peuvent
être testés sur une machine Windows ou Linux avec l version de python 3.3.
Les modules suivants (tous téléchargeables gratuitement) sont requis :
mpich
Pyro
pip
PyCSP
mpi4py
DISCO
asyncio
NumbaPro compiler
Celery
PyOpenCL
Numpy
Flower
SCOOP
Organisation physique des plates-formes parallèles
L'architecture de la mémoire de calcul parallèle en fonction du nombre
d'instructions et de données pouvant être traitées simultanément, les systèmes
informatiques sont classés en quatre catégories :
Instruction unique, données uniques (SISD)
Instruction unique, données multiples (SIMD)
Instruction multiple, données uniques (MISD)
Instruction multiple, données multiples (MIMD)
Cette classification est connue sous le nom de taxonomie de Flynn
Organisation physique des plates-formes parallèles
L'architecture de la mémoire de calcul parallèle en fonction du nombre
d'instructions et de données pouvant être traitées simultanément, les systèmes
informatiques sont classés en quatre catégories :
Instruction unique, données uniques (SISD)
Instruction unique, données multiples (SIMD)
Instruction multiple, données uniques (MISD)
Instruction multiple, données multiples (MIMD)
Cette classification est connue sous le nom de taxonomie de Flynn
SISD
L'architecture de la mémoire de calcul parallèle en fonction du nombre
d'instructions. Le système informatique SISD est une machine monoprocesseur.
Il exécute une seule instruction qui fonctionne sur un seul flux de données. Dans
SISD, les instructions machine sont traitées séquentiellement. Dans un cycle
d'horloge, la CPU exécute les opérations suivantes :
Fetch : le CPU récupère les données et les instructions d'une zone mémoire,
appelée registre.
Decode : Le CPU décode les instructions.
Executer : L'instruction est exécutée sur les données. Le résultat de
l'opération est stocké dans un autre registre.
Une fois l'étape d'exécution terminée, le CPU se prépare à commencer un autre
cycle CPU.
Les algorithmes qui s'exécutent sur ces types d'ordinateurs sont séquentiels (ou
série), car ils ne contiennent aucun parallélisme. Des exemples d'ordinateurs
SISD sont des systèmes matériels avec un seul processeur. Les principaux
éléments de ces architectures (architectures Von Neumann) sont :
Unité de mémoire centrale : elle est utilisée pour stocker à la fois les
instructions et les données de programme.
CPU : il est utilisé pour obtenir l'instruction et/ou les données de l'unité
de mémoire, qui décode les instructions et les implémente
séquentiellement
Le système d'E/S : Il s'agit des données d'entrée et des données de sortie
du programme. Les ordinateurs conventionnels à processeur unique sont
classés comme systèmes SISD.
La figure suivante montre spécifiquement quelles zones d'un processeur sont
utilisées dans les étapes d'extraction, de décodage et d'exécution :
MISD
Dans ce modèle, n processeurs, chacun avec sa propre unité de contrôle,
partagent une seule unité de mémoire. A chaque cycle d'horloge, les données
reçues de la mémoire sont traitées simultanément par tous les processeurs,
chacun selon les instructions reçues de son unité de contrôle. Dans ce cas, le
parallélisme (parallélisme au niveau des instructions) est obtenu en effectuant
plusieurs opérations sur la même donnée. Les types de problèmes qui peuvent
être résolus efficacement dans ces architectures sont assez particuliers, comme
ceux concernant le chiffrement des données ; pour cette raison, l'ordinateur
MISD n'a pas trouvé de place dans le secteur commercial. Les ordinateurs MISD
sont plus un exercice intellectuel qu'une configuration pratique.
SIMD
Un ordinateur SIMD est constitué de n processeurs identiques, chacun avec sa
propre mémoire locale, où il est possible de stocker des données. Tous les
processeurs fonctionnent sous le contrôle d'un seul flux d'instructions ; en plus
de cela, il y a n flux de données, un pour chaque processeur. Les processeurs
travaillent simultanément sur chaque étape et exécutent la même instruction,
mais sur des éléments de données différents. Ceci est un exemple de parallélisme
au niveau des données. Les architectures SIMD sont beaucoup plus polyvalentes
que les architectures MISD. De nombreux problèmes couvrant un large éventail
d'applications peuvent être résolus par des algorithmes parallèles sur des
ordinateurs SIMD. Une autre caractéristique intéressante est que les algorithmes
de ces ordinateurs sont relativement faciles à concevoir, analyser et mettre en
œuvre.
La limite est que seuls les problèmes décomposables en plusieurs sous-
problèmes (qui sont tous identiques, dont chacun sera alors résolu
simultanément, par le même jeu d'instructions) peuvent être traités avec le
calculateur SIMD. Au supercalculateur développé selon ce paradigme, il faut
citer Connection Machine (1985 Thinking Machine) et MPP (NASA - 1983).
Comme nous le verrons au chapitre 6, Programmation GPU avec Python,
l'avènement du processeur graphique (GPU) moderne, construit avec de
nombreuses unités embarquées SIMD, a conduit à une utilisation plus répandue
de ce paradigme informatique.
MIMD
Cette classe d'ordinateurs parallèles est la classe la plus générale et la plus
puissante selon la classification de Flynn. Il y a n processeurs, n flux
d'instructions et n flux de données. Chaque processeur possède sa propre unité de
contrôle et sa propre mémoire locale, ce qui rend les architectures MIMD plus
puissantes en termes de calcul que celles utilisées dans SIMD. Chaque
processeur fonctionne sous le contrôle d'un flux d'instructions émises par sa
propre unité de contrôle ; par conséquent, les processeurs peuvent
potentiellement exécuter différents programmes sur différentes données,
résolvant des sous-problèmes différents et pouvant faire partie d'un seul
problème plus vaste. Dans MIMD, l'architecture est réalisée à l'aide du niveau de
parallélisme avec des threads et/ou des processus. Cela signifie également que
les processeurs fonctionnent généralement de manière asynchrone.
MIMD
Les ordinateurs de cette classe sont utilisés pour résoudre les problèmes qui n'ont
pas une structure régulière requise par le modèle SIMD. De nos jours, cette
architecture est appliquée à de nombreux PC, supercalculateurs et réseaux
informatiques. Cependant, il y a un compteur que vous devez considérer : les
algorithmes asynchrones sont difficiles à concevoir, analyser et mettre en œuvre.
Memory organization
Un autre aspect que nous devons considérer pour évaluer une architecture
parallèle est l'organisation de la mémoire ou plutôt la manière dont les données
sont accédées. Quelle que soit la vitesse de l'unité de traitement, si la mémoire ne
peut pas maintenir et fournir des instructions et des données à une vitesse
suffisante, il n'y aura aucune amélioration des performances. Le principal
problème qu'il faut surmonter pour rendre le temps de réponse de la mémoire
compatible avec la vitesse du processeur est le temps de cycle mémoire, qui est
défini comme le temps qui s'est écoulé entre deux opérations successives. Le
temps de cycle du processeur est généralement beaucoup plus court que le temps
de cycle de la mémoire.
Memory organization
Lorsque le processeur commence à transférer des données (vers ou depuis la
mémoire), la mémoire restera occupée pendant toute la durée du cycle mémoire :
pendant cette période, aucun autre périphérique (contrôleur d'E/S, processeur ou
même le processeur lui-même qui la requête) peut utiliser la mémoire car elle
sera engagée pour répondre à la requête.
Memory organization
Les solutions au problème d'accès à la mémoire ont abouti à une dichotomie des
architectures MIMD. Dans le premier type de système, connu sous le nom de
système à mémoire partagée, la mémoire virtuelle est élevée et tous les
processeurs ont un accès égal aux données et aux instructions de cette mémoire.
L'autre type de système est le modèle à mémoire distribuée, dans lequel chaque
processeur possède une mémoire locale qui n'est pas accessible aux autres
processeurs. La différence entre la mémoire partagée et la mémoire distribuée
réside dans la structure de la mémoire virtuelle ou de la mémoire du point de vue
du processeur. Physiquement, presque chaque mémoire système est divisée en
composants distincts accessibles indépendamment. Ce qui distingue une
mémoire partagée d'une mémoire distribuée est la gestion des accès mémoire par
l'unité de traitement.
Memory organization
Si un processeur devait exécuter l'instruction load R0, i, c'est-à-dire charger dans
le registre R0 le contenu de l'emplacement mémoire i, la question est maintenant
que devrait-il se passer ? Dans un système à mémoire partagée, l'index i est une
adresse globale et l'emplacement mémoire i est le même pour chaque processeur.
Si deux processeurs exécutaient cette instruction en même temps, ils chargeraient
les mêmes informations dans leurs registres R0. Dans un système de mémoire
distribuée, i est une adresse locale. Si deux processeurs devaient charger
l'instruction R0 en même temps, des valeurs différentes pourraient se retrouver
dans le R0 du registre respectif, puisque, dans ce cas, les cellules de mémoire
sont attribuées une pour chaque mémoire locale.
Memory organization
La distinction entre mémoire partagée et mémoire distribuée est très importante
pour les programmeurs car elle détermine la manière dont les différentes parties
d'un programme parallèle doivent communiquer. Dans un système, la mémoire
partagée est suffisante pour construire une structure de données en mémoire et
passer au sous-programme parallèle, qui sont les variables de référence de cette
structure de données. De plus, une machine à mémoire distribuée doit faire des
copies des données partagées dans chaque mémoire locale. Ces copies sont
créées par l'envoi d'un message contenant les données à partager d'un processeur
à l'autre. Un inconvénient de cette organisation mémoire est que parfois, ces
messages peuvent être très volumineux et prendre un temps de transfert
relativement long.
Memoire partagée
Le schéma d'un système multiprocesseur à mémoire partagée est présenté dans la
figure suivante. Les connexions physiques ici sont assez simples. La structure du
bus autorise un nombre arbitraire de périphériques partageant le même canal. Les
protocoles de bus ont été conçus à l'origine pour permettre à un seul processeur
et à un ou plusieurs disques ou contrôleurs de bande de communiquer via la
mémoire partagée ici. A noter que chaque processeur a été associé à une
mémoire cache, car on suppose que la probabilité qu'un processeur ait besoin de
données ou d'instructions présentes dans la mémoire locale est très élevée. Le
problème se produit lorsqu'un processeur modifie des données stockées dans le
système de mémoire qui sont simultanément utilisées par d'autres processeurs.
La nouvelle valeur passera du cache du processeur qui a été modifié à la
mémoire partagée ; plus tard, cependant, il doit également être transmis à tous les
autres processeurs, afin qu'ils ne fonctionnent pas avec la valeur obsolète.
Memoire partagée
Ce problème est connu sous le nom de problème de cohérence du cache, un cas
particulier du problème de cohérence de la mémoire, qui nécessite des
implémentations matérielles capables de gérer les problèmes de concurrence et
de synchronisation similaires à ceux ayant une programmation par thread.
Les principales caractéristiques des systèmes de mémoire partagée sont :
La mémoire est la même pour tous les processeurs, par exemple, tous les
processeurs associés à la même structure de données travailleront avec les
mêmes adresses mémoire logiques, accédant ainsi aux mêmes emplacements
mémoire.
La synchronisation est rendue possible en contrôlant l'accès des processeurs à
la mémoire partagée. En effet, un seul processeur à la fois peut avoir accès
aux ressources mémoire.
Un emplacement de mémoire partagée ne doit pas être modifié à partir d'une
tâche pendant qu'une autre tâche y accède.
Le partage de données est rapide ; le temps nécessaire à la communication
entre deux tâches est égal au temps de lecture d'un seul emplacement
mémoire (il dépend de la vitesse d'accès mémoire).
Les accès à la mémoire dans les systèmes de mémoire partagée sont les suivants :
Uniform memory access (UMA) : La caractéristique fondamentale de ce
système est le temps d'accès à la mémoire qui est constant pour chaque
processeur et pour n'importe quelle zone de la mémoire. Pour cette raison, ces
systèmes sont également appelés multiprocesseurs symétriques (SMP). Ils
sont relativement simples à mettre en œuvre, mais peu évolutifs ; le
programmeur est responsable de la gestion de la synchronisation en insérant
les contrôles, sémaphores, verrous, etc. appropriés dans le programme qui
gère les ressources.
Accès mémoire non uniforme (NUMA) : Ces architectures divisent la zone
mémoire en une zone d'accès rapide qui est affectée à chaque processeur et
une zone commune pour l'échange de données, avec un accès plus lent. Ces
systèmes sont également appelés systèmes de mémoire partagée distribués
(DSM). Ils sont très évolutifs, mais complexes à développer.
Pas d'accès mémoire distant (NORMA) : La mémoire est physiquement répartie
entre les processeurs (mémoire locale). Toutes les mémoires locales sont privées et
ne peuvent accéder qu'au processeur local. La communication entre les processeurs
se fait à travers un protocole de communication utilisé pour l'échange de messages,
le protocole de passage de messages.
Accès à la mémoire cache uniquement (COMA) : ces systèmes sont équipés
uniquement de mémoires cache. Lors de l'analyse des architectures NUMA, il a été
remarqué que ces architectures conservaient les copies locales des données dans le
cache et que ces données étaient stockées en double dans la mémoire principale.
Cette architecture supprime les doublons et ne conserve que les mémoires caches,
la mémoire est physiquement répartie entre les processeurs (mémoire locale).
Toutes les mémoires locales sont privées et ne peuvent accéder qu'au processeur
local. La communication entre les processeurs se fait à travers un protocole de
communication pour l'échange de messages, le protocole de passage de messages.
Mémoire distribuée
Dans un système à mémoire distribuée, la mémoire est associée à chaque
processeur et un processeur ne peut adresser que sa propre mémoire. Certains
auteurs qualifient ce type de système de "multi-ordinateur", reflétant le fait que
les éléments du système sont eux-mêmes de petits systèmes complets d'un
processeur et d'une mémoire, comme vous pouvez le voir sur la figure suivante :
Ce type d'organisation présente plusieurs avantages. Dans un premier temps, il
n'y a pas de conflits au niveau du bus de communication ou du switch. Chaque
processeur peut utiliser toute la bande passante de sa propre mémoire locale sans
aucune interférence des autres processeurs. Deuxièmement, l'absence de bus
commun signifie qu'il n'y a pas de limite intrinsèque au nombre de processeurs,
la taille du système n'est limitée que par le réseau utilisé pour connecter les
processeurs. Troisièmement, il n'y a pas de problèmes de cohérence du cache.
Chaque processeur est responsable de ses propres données et n'a pas à se soucier
de la mise à niveau des copies. Le principal inconvénient est que la
communication entre processeurs est plus difficile à mettre en œuvre. Si un
processeur a besoin de données dans la mémoire d'un autre processeur, les deux
processeurs doivent nécessairement échanger des messages via le protocole de
passage de messages.
Cela introduit deux sources de ralentissement ; construire et envoyer un message
d'un processeur à un autre prend du temps, et aussi, tout processeur doit être
arrêté afin de gérer les messages reçus des autres processeurs. Un programme
conçu pour fonctionner sur une machine à mémoire distribuée doit être organisé
comme un ensemble de tâches indépendantes qui communiquent via des
messages.
Les principales caractéristiques des systèmes de mémoire distribuée sont les
suivantes :
La mémoire est physiquement répartie entre les processeurs ; chaque
mémoire locale n'est directement accessible que par son processeur.
La synchronisation est obtenue en déplaçant les données (même s'il ne s'agit
que du message lui-même) entre les processeurs (communication).
La subdivision des données dans les mémoires locales affecte les
performances de la machine, il est essentiel de faire une subdivision précise,
afin de minimiser la communication entre les CPU. De plus, le processeur qui
coordonne ces opérations de décomposition et de composition doivent
communiquer efficacement avec les processeurs qui opèrent sur les parties
individuelles des structures de données.
Le protocole de transmission de messages est utilisé pour que les CPU
puissent communiquer entre elles via l'échange de paquets de données. Les
messages sont des unités d'information discrètes ; dans le sens où ils ont une
identité bien définie, il est donc toujours possible de les distinguer les uns des
autres.
Traitement massivement parallèle
Les machines MPP sont composées de centaines de processeurs (qui peuvent
atteindre des centaines de milliers dans certaines machines) qui sont connectés
par un réseau de communication. Les ordinateurs les plus rapides au monde sont
basés sur ces architectures ; quelques exemples de systèmes de ces architectures
sont : Earth Simulator, Blue Gene, ASCI White, ASCI Red et ASCI Purple and
Red Storm.
Un cluster de postes de travail
Ces systèmes de traitement sont basés sur des calculateurs classiques reliés par
des réseaux de communication. Les grappes de calcul entrent dans cette
classification.
Dans une architecture en cluster, nous définissons un nœud comme une unité de
calcul unique qui participe au cluster. Pour l'utilisateur, le cluster est totalement
transparent : toute la complexité matérielle et logicielle est masquée et les
données et applications sont rendues accessibles comme si elles provenaient
toutes d'un seul nœud. Ici, nous avons identifié trois types de clusters :
Le cluster de basculement : dans celui-ci, l'activité du nœud est surveillée en
permanence, et lorsqu'une machine cesse de fonctionner, une autre machine
prend en charge ces activités. L'objectif est d'assurer un service continu grâce
à la redondance de l'architecture.
Le cluster d'équilibrage de charge : dans ce système, une demande de tâche
est envoyée au nœud qui a le moins d'activité. Cela garantit que moins de
temps est nécessaire pour terminer le processus.
Le cluster de calcul haute performance : Dans celui-ci, chaque nœud est
configuré pour fournir des performances extrêmement élevées. Le processus
est également divisé en plusieurs tâches sur plusieurs nœuds. Les travaux sont
parallélisés et seront distribués sur différentes machines.
L'architecture hétérogène
L'introduction des accélérateurs GPU dans le monde homogène du supercalcul a
changé la nature de la façon dont les supercalculateurs étaient à la fois utilisés et
programmés auparavant. Malgré les hautes performances offertes par les GPU,
ils ne peuvent pas être considérés comme une unité de traitement autonome car
ils doivent toujours être accompagnés d'une combinaison de CPU. Le paradigme
de programmation est donc très simple ; le CPU prend le contrôle et calcule en
série, affectant à l'accélérateur graphique les tâches qui sont très coûteuses en
calcul et qui ont un haut degré de parallélisme.
La communication entre un CPU et un GPU peut s'effectuer non seulement
grâce à l'utilisation d'un bus à haut débit, mais aussi grâce au partage d'une
seule zone de mémoire à la fois physique ou virtuelle. En effet, dans le cas
où les deux appareils ne sont pas équipés de leurs propres zones mémoire, il
est possible de se référer à une zone mémoire commune en utilisant les
bibliothèques logicielles fournies par les différents modèles de
programmation, tels que CUDA et OpenCL. Ces architectures sont appelées
architectures hétérogènes, dans lesquelles les applications peuvent créer des
structures de données dans un seul espace d'adressage et envoyer une tâche
au matériel de l'appareil approprié pour la résolution de la tâche. Plusieurs
tâches de traitement peuvent s'opérer en toute sécurité sur les mêmes régions
pour éviter les problèmes de cohérence des données, grâce aux opérations
atomiques.
Ainsi, malgré le fait que le CPU et le GPU ne semblent pas fonctionner
efficacement ensemble, avec l'utilisation de cette nouvelle architecture, nous
pouvons optimiser leur interaction avec et les performances des applications
parallèles.
Modèles de programmation parallèle
Dans ce modèle, les tâches partagent une seule zone de mémoire partagée, où
l'accès (lecture et écriture de données) aux ressources partagées est asynchrone.
Il existe des mécanismes qui permettent au programmeur de contrôler l'accès à la
mémoire partagée, par exemple, des verrous ou des sémaphores. Ce modèle offre
l'avantage que le programmeur n'a pas à clarifier la communication entre les
tâches. Un inconvénient important en termes de performances est qu'il devient
plus difficile de comprendre et de gérer la localité des données ; conserver les
données locales au processeur qui y travaille conserve les accès à la mémoire, les
actualisations du cache et le trafic de bus qui se produisent lorsque plusieurs
processeurs utilisent les mêmes données.
Modèle multithread
Dans ce modèle, un processus peut avoir plusieurs flux d'exécution, par exemple,
une partie séquentielle est créée et par la suite, une série de tâches sont créées qui
peuvent être exécutées en parallèle. Habituellement, ce type de modèle est utilisé
sur des architectures à mémoire partagée. Ainsi, il sera très important pour nous de
gérer la synchronisation entre les threads, car ils fonctionnent sur de la mémoire
partagée, et le programmeur doit empêcher plusieurs threads de mettre à jour les
mêmes emplacements en même temps. Les processeurs de la génération actuelle
sont multithreads dans le logiciel et le matériel. Les threads Posix sont l'exemple
classique de l'implémentation du multithreading sur un logiciel. La technologie
Intel Hyper-threading implémente le multithreading sur le matériel en basculant
entre deux threads lorsque l'un est bloqué ou en attente d'E/S. Le parallélisme peut
être obtenu à partir de ce modèle même si l'alignement des données n'est pas
linéaire.
Modèle de transmission de messages
Le modèle de transmission de messages est généralement appliqué dans le cas où
chaque processeur possède sa propre mémoire (systèmes à mémoire distribuée).
Plusieurs tâches peuvent résider sur la même machine physique ou sur un nombre
arbitraire de machines. Le programmeur est chargé de déterminer le parallélisme
et l'échange de données qui se produit à travers les messages. La mise en œuvre de
ce modèle de programmation parallèle nécessite l'utilisation de bibliothèques
logicielles (ad hoc) à utiliser dans le code. De nombreuses implémentations du
modèle de transmission de messages ont été créées : certains des exemples sont
disponibles depuis les années 1980, mais seulement à partir du milieu des années
90, ont été créés pour un modèle standardisé, aboutissant à une norme de facto
appelée MPI (l'interface de transmission de messages).
Modèle de transmission de messages
Le modèle MPI est clairement conçu avec de la mémoire distribuée, mais étant
des modèles de programmation parallèle, la multiplateforme peut également être
utilisée avec une machine à mémoire partagée.
Modèle parallèle de données
Dans ce modèle, nous avons plus de tâches qui fonctionnent sur la même structure
de données, mais chaque tâche fonctionne sur une partie différente des données.
Dans l'architecture de mémoire partagée, toutes les tâches ont accès aux données
via des architectures de mémoire partagée et de mémoire distribuée, où la
structure de données est divisée et réside dans la mémoire locale de chaque tâche.
Pour mettre en œuvre ce modèle, un programmeur doit développer un programme
qui spécifie la distribution et l'alignement des données. Les GPU de la génération
actuelle fonctionnent parfaitement avec les données alignées.
Comment concevoir un programme parallèle
Dans cette étape, le mécanisme par lequel la tâche sera répartie entre les
différents processus est spécifié. Cette phase est très importante car elle
établit la répartition de la charge de travail entre les différents processeurs.
L'équilibre de charge est ici crucial ; en fait, tous les processeurs doivent
fonctionner avec continuité, en évitant un état d'inactivité pendant une
longue période. Pour ce faire, le programmeur prend en compte l'éventuelle
hétérogénéité du système qui tente d'attribuer plus de tâches à des
processeurs plus performants. Enfin, pour une plus grande efficacité de la
parallélisation, il faut limiter au maximum les communications entre
processeurs, car elles sont souvent à l'origine de ralentissements et de
consommation de ressources.
Une agglomération
La plupart des communications ont des coûts qui sont non seulement
proportionnels à la quantité de données transférées, mais entraînent
également un coût fixe pour chaque opération de communication (comme la
latence inhérente à l'établissement d'une connexion TCP). Si les tâches sont
trop petites, ce coût fixe peut facilement rendre la conception inefficace.
Mapping (Cartographie)
Dans l'étape de mappage du processus de conception de l'algorithme
parallèle, nous spécifions où chaque tâche doit être exécutée. L'objectif est
de minimiser le temps d'exécution total. Ici, vous devez souvent faire des
compromis, car les deux principales stratégies entrent souvent en conflit :
Les tâches qui communiquent fréquemment doivent être placées dans le
même processeur pour augmenter la localité.
Les tâches qui peuvent être exécutées simultanément doivent être placées
dans des processeurs différents pour améliorer la simultanéité.
C'est ce qu'on appelle le problème de mappage, et il est connu pour être
NP-complet. En tant que tel, il n'existe pas de solutions en temps
polynomial au problème dans le cas général. Pour des tâches de taille
égale et des tâches avec des modèles de communication facilement
identifiables, le mappage est simple (nous pouvons également effectuer
une agglomération ici pour combiner des tâches qui correspondent au
même processeur.)Cependant, si les tâches ont des modèles de
communication difficiles à prévoir ou si la quantité de travail varie d'une
tâche à l'autre, il est difficile de concevoir un schéma de cartographie et
d'agglomération efficace. Pour ces types de problèmes, des algorithmes
d'équilibrage de charge peuvent être utilisés pour identifier les stratégies
d'agglomération et de mappage pendant l'exécution.
Les problèmes les plus difficiles sont ceux dans lesquels la quantité de
communication ou le nombre de tâches change pendant l'exécution du
programme. Pour ce type de problèmes, des algorithmes d'équilibrage de
charge dynamique peuvent être utilisés, qui s'exécutent périodiquement
pendant l'exécution.
Dynamic mapping (Cartographie dynamique)
Il existe de nombreux algorithmes d'équilibrage de charge pour divers
problèmes, à la fois globaux et locaux. Les algorithmes globaux nécessitent
une connaissance globale du calcul effectué, ce qui ajoute souvent beaucoup
de surcharge. Les algorithmes locaux reposent uniquement sur des
informations locales à la tâche en question, ce qui réduit les frais généraux
par rapport aux algorithmes globaux, mais sont généralement moins
performants pour trouver une agglomération et une cartographie optimales.
Cependant, la surcharge réduite peut réduire le temps d'exécution même si le
mappage est moins bon en soi. Si les tâches communiquent rarement
autrement qu'au début et à la fin de l'exécution, un algorithme de
planification des tâches est souvent utilisé qui mappe simplement les tâches
aux processeurs lorsqu'ils deviennent inactifs. Dans un algorithme de
planification de tâches, un pool de tâches est maintenu. Les tâches sont
placées dans ce pool et en sont extraites par les travailleurs. Il existe trois
approches courantes dans ce modèle, qui sont expliquées ci-après.
Gestionnaire/travailleur
Il s'agit du schéma de mappage dynamique de base dans lequel tous les
travailleurs se connectent à un gestionnaire centralisé. Le responsable envoie
à plusieurs reprises des tâches aux travailleurs et collecte les résultats. Cette
stratégie est probablement la meilleure pour un nombre relativement
restreint de processeurs. La stratégie de base peut être améliorée en
récupérant les tâches à l'avance afin que la communication et le calcul se
chevauchent.
Responsable hiérarchique/travailleur
Il s'agit de la variante d'un gestionnaire/travailleur qui a une disposition
semi-distribuée ; les travailleurs sont divisés en groupes, chacun avec son
propre responsable. Ces responsables de groupe communiquent avec le
responsable central (et éventuellement entre eux également), tandis que les
Cela répartit la charge entre plusieurs gestionnaires et peut ainsi gérer un
plus grand nombre de processeurs si tous les travailleurs demandent des
tâches au même gestionnaire.
Décentraliser
Dans ce schéma, tout est décentralisé. Chaque processeur maintient son
propre pool de tâches et communique avec les autres processeurs afin de
demander des tâches. La manière dont les processeurs choisissent d'autres
processeurs pour demander des tâches varie et est déterminée en fonction du
problème.
Comment évaluer les performances d'un programme parallèle
Le développement de la programmation parallèle a créé le besoin de
métriques de performance et d'un outil logiciel pour évaluer les
performances d'un algorithme parallèle afin de décider si son utilisation est
pratique ou non. En effet, l'objectif du calcul parallèle est de résoudre de
gros problèmes en un temps relativement court. Les facteurs qui contribuent
à la réalisation de cet objectif sont, par exemple, le type de matériel utilisé,
le degré de parallélisme du problème et le modèle de programmation
parallèle adopté. Pour faciliter cela, une analyse des concepts de base a été
introduite, qui compare l'algorithme parallèle obtenu à partir de la séquence
d'origine.
La performance est obtenue en analysant et en quantifiant le nombre de
threads et/ou le nombre de processus utilisés. Pour analyser cela, quelques
indices de performance sont introduits : accélération, efficacité et mise à
l'échelle. Les limitations d'un calcul parallèle sont introduites par la loi
d'Ahmdal pour évaluer le degré d'efficacité de la parallélisation d'un
algorithme séquentiel on a la loi de Gustafson.
Speedup (Accélérer)
L'accélération est la mesure qui affiche l'avantage de résoudre un problème
en parallèle. Il est défini comme le rapport du temps mis pour résoudre un
problème sur un seul élément de traitement, , au temps nécessaire pour
résoudre le même problème sur éléments de traitement identiques, . On note
accélération par
Nous avons une accélération linéaire, où si , cela signifie que la vitesse
d'exécution augmente avec le nombre de processeurs. Bien sûr, c'est un cas
idéal. Alors que l'accélération est absolue lorsque est le temps d'exécution
du meilleur algorithme séquentiel, l'accélération est relative lorsque est le
temps d'exécution de l'algorithme parallèle pour un seul processeur.
Récapitulons ces conditions :
S = p est une accélération linéaire ou idéale
S < p est l'accélération réelle
S > p est une accélération superlinéaire
Efficacité
Dans un monde idéal, un système parallèle avec éléments de traitement peut
nous donner une accélération égale à . Cependant, cela est très rarement
atteint. Habituellement, un certain temps est gaspillé à ne rien faire ou à
communiquer. L'efficacité est une mesure de performance évaluant le niveau
d'utilisation des processeurs dans la résolution d'une tâche, par rapport à la
quantité d'efforts gaspillés dans la communication et la synchronisation. On
le note et on peut le définir comme.
Les algorithmes avec accélération linéaire ont la valeur de ; dans les autres
cas, la valeur de est inférieure à .
Efficacité
Les trois cas sont identifiés comme suit :
Quand E = 1, c'est un cas linéaire
Quand E < 1, c'est un cas réel
Lorsque E<< 1, c'est un problème parallélisable à faible efficacité
Mise à l'échelle (Scaling)
La mise à l'échelle est définie comme la capacité à être efficace sur une
machine parallèle. Il identifie la puissance de calcul (vitesse d'exécution)
proportionnellement au nombre de processeurs. En augmentant la taille du
problème et en même temps le nombre de processeurs, il n'y aura aucune
perte en termes de performances. Le système évolutif, en fonction des
incréments des différents facteurs, peut conserver la même efficacité ou
l'améliorer.
La loi d'Amdahl
La loi d'Amdahl est une loi largement utilisée pour concevoir des
processeurs et des algorithmes parallèles. Il indique que l'accélération
maximale pouvant être atteinte est limitée par le composant série du
programme :
largement utilisées.
Pratiquement, tous les systèmes d'exploitation existants prennent en charge
existe des mécanismes que vous pouvez utiliser pour implémenter des
qu'un thread est contenu à l'intérieur d'un processus et que différents threads
processus, même les threads ont leur propre état d'exécution et peuvent se
synchroniser les uns avec les autres. Les états d'exécution d'un thread sont
modernes, où chaque cœur peut exécuter un seul thread. L'avantage des threads
résoudre par la programmation avec les threads soit lié à la gestion de cet
espace.
Utilisation du module de thread Python
Python gère un thread via le package de threading fourni par la bibliothèque
standard Python. Ce module fournit des fonctionnalités très intéressantes qui
facilitent grandement l'approche basée sur les threads. en fait, le module de
threading fournit plusieurs des mécanismes de synchronisation très simples à
mettre en oeuvre. Les principaux composants du module de threading sont :
The thread object
The Lock object
The RLock object
The semaphore object
The condition object
The event object
Comment définir un thread
La façon la plus simple d'utiliser un thread est de l'instancier avec une
fonction cible, puis d'appeler la méthode start() pour lui permettre de
commencer son travail. Le threading du module Python a la méthode
Thread() qui est utilisée pour exécuter des processus et des fonctions dans un
thread différent :
class threading.Thread(group=None,
target=None,
name=None,
args=(),
kwargs={})
Comment définir un thread
Dans le code précédent :
Il est utile de générer un thread et de lui transmettre des arguments qui lui
indiquent ce qu'il doit faire. Cet exemple passe un nombre, qui est le numéro de
thread, puis imprime le résultat.
group : il s'agit de la valeur du groupe qui doit être 0
Target : il s'agit de la fonction qui doit être exécutée lorsque vous démarrez
une activité de thread
name : C'est le nom du thread ; par défaut, un nom unique de la forme
Thread-N lui est attribué
args : il s'agit du tuple d'arguments à transmettre à une cible.
kwargs : il s'agit du dictionnaire des arguments de mots clés à utiliser pour
la fonction cible
Il est utile de générer un thread et de lui transmettre des arguments qui lui
indiquent ce qu'il doit faire. Cet exemple passe un nombre, qui est le numéro de
thread, puis imprime le résultat.
Comment faire…
Voyons comment définir un thread avec le module threading, pour cela,
quelques lignes de code sont nécessaires :
Comment faire…
Pour importer le module de threading, nous utilisons simplement la
commande Python :
Import thread
Dans le programme principal, nous instancions un thread, en utilisant l'objet
Thread avec une fonction cible appelée fonction. De plus, nous passons un
argument à la fonction qui sera incluse dans le message de sortie :
t = threading.Thread(target=fonction , args=(i,))
Le thread ne commence pas à s'exécuter tant que la méthode start() n'est pas
appelée, et que join() fait attendre le thread appelant jusqu'à ce que le thread
ait terminé l'exécution :
t.start()
t.join()
Comment déterminer le thread en cours
L'utilisation d'arguments pour identifier ou nommer le thread est fastidieuse
et inutile. Chaque instance de Thread a un nom avec une valeur par défaut
qui peut être modifiée lors de la création du thread. Nommer les threads est
utile dans les processus serveur avec plusieurs threads de service qui gèrent
différentes opérations.
Comment faire…
Pour déterminer quel thread est en cours d'exécution, nous créons trois
fonctions cibles et importons le module de temps pour introduire une
suspension d'exécution de deux secondes :
Comment utiliser un thread dans une sous-classe
Pratique en Python