4.3 Modèles de programmation parallèle
Objectifs d'apprentissage
À la fin de cette section, vous serez capable de :
- Définir le calcul parallèle et la terminologie associée
- Discuter des approches de programmation parallèle
Jusqu'à présent, nos programmes s'exécutaient sur un seul à la fois et l'hypothèse était que la machine sous-jacente ne prenait en charge qu'un seul cœur de GPU. Un cœur (core) de CPU est une puce composée de milliards de transistors qui fonctionnent selon une instruction ou un opcode. C'est comme un processeur unique. Bien que nous puissions faire beaucoup avec ces programmes monocœurs, il est également nécessaire que les programmes s'exécutent en parallèle, ce qui signifie qu'ils exécutent du code sur plusieurs CPU, cœurs ou ordinateurs simultanément. En programmation parallèle, les tâches plus importantes sont divisées en tâches plus petites, et elles sont traitées en parallèle, partageant la même mémoire. La programmation parallèle tend à être de plus en plus nécessaire et répandue au fil du temps. De nombreux ordinateurs sont désormais équipés d'une unité de traitement graphique (GPU) (graphics processing unit (GPU)), qui est un processeur massivement parallèle qui complète un CPU. Les GPU ont été conçus à l'origine pour le rendu graphique en temps réel dans les jeux vidéo et sont parfois appelés « cartes vidéo ». Un GPU typique possède des milliers de cœurs, bien que chacun soit plus faible qu'un cœur de CPU. Les techniques parallèles sont essentielles pour utiliser les GPU.
Aperçu du calcul parallèle
Au 20e siècle, un ordinateur n'avait généralement qu'un seul processeur. Aujourd'hui, une puce CPU contient généralement non pas un seul processeur, mais plusieurs processeurs intégrés dans une seule puce informatique. Chaque processeur individuel intégré dans un CPU est un cœur. Un processeur multicœur (multicore) est une puce CPU qui possède plusieurs cœurs. Les CPU multicœurs sont répandus ; les smartphones et les PC économiques ont généralement deux à quatre cœurs, et les PC haut de gamme ont huit cœurs ou plus. La tendance est à l'augmentation de ces nombres de cœurs au fil du temps.
Par défaut, un programme s'exécute sur un seul cœur à la fois. Cela signifie qu'un ordinateur à quatre cœurs peut exécuter jusqu'à quatre programmes à pleine vitesse simultanément. Cette capacité est parfois utile, mais plus souvent, un utilisateur souhaite qu'un seul programme à forte demande utilise pleinement son ordinateur. C'est le cas des logiciels de productivité, des jeux, des systèmes embarqués et des logiciels de serveur Web. Pour que cela fonctionne, le programme doit être codé de manière à diviser explicitement le travail entre plusieurs cœurs.
Fondamentalement, pour utiliser plusieurs cœurs, un programme doit fonctionner en « parallèle ». Cela signifie que plusieurs cœurs travaillent ensemble simultanément (Figure 4.30). Un exemple concret de travail parallèle est une chaîne de montage en usine. Si une chaîne de montage compte vingt travailleurs, alors à tout moment, vingt personnes travaillent en parallèle. Ce concept de travail parallèle s'applique également aux logiciels. Les concepts de parallélisme abordés ici sont :
- ordinateur parallèle (parallel computer) : un système multiprocesseur qui prend en charge la programmation parallèle.
- calcul parallèle (parallel computing) : la pratique consistant à utiliser de manière productive les ordinateurs parallèles.
- programmation parallèle (parallel programming) : une technique de programmation informatique qui permet d'exécuter du code en parallèle sur plusieurs processeurs.
Il y a deux termes liés mais distincts que nous devrions définir à ce stade : la programmation concurrente (concurrent programming), qui fait référence à toute situation où plusieurs programmes ou tâches s'exécutent simultanément, qu'ils utilisent plusieurs processeurs ou partagent un seul processeur ; et le calcul distribué (distributed computing), qui est une forme plus spécifique de programmation parallèle où les processeurs travaillent ensemble en parallèle, mais les processeurs se trouvent dans plusieurs ordinateurs connectés, et non dans un seul ordinateur. La programmation concurrente est un terme plus large que la programmation parallèle, tandis que le calcul distribué fait généralement référence à des programmes massivement parallèles qui s'exécutent sur des centaines ou des milliers de serveurs, généralement dans de grandes entreprises telles qu'Amazon, Google, la NSA et le NIH.
Réfléchissez
Applications des GPU
Les GPU sont des processeurs parallèles haute performance. Parmi les principales applications des GPU figurent le minage de cryptomonnaies, les jeux vidéo et les ordinateurs de tableau de bord intégrés aux automobiles. En 2021, il y a eu une pénurie de GPU en raison d'une « tempête parfaite » d'événements mondiaux. La pandémie de COVID-19 a compliqué la fabrication, limitant le rythme de production des GPU. En réponse à la pandémie, la demande d'ordinateurs a augmenté, car de nombreux travailleurs ont été contraints de travailler à domicile. La demande de jeux vidéo a également augmenté, les gens recherchant des divertissements à domicile. Au même moment, les prix des cryptomonnaies ont augmenté, ce qui a stimulé l'intérêt pour le minage de cryptomonnaies, de sorte que encore plus de personnes ont essayé d'acheter des GPU simultanément.
Tous ces événements ont provoqué une grave pénurie. Les clients ont rencontré de longues listes d'attente pour les GPU, ou ont constaté qu'ils étaient totalement indisponibles. Les revendeurs à la sauvette vendaient des GPU avec un surcoût substantiel. Certaines personnes n'ont pas pu acheter de jeux vidéo ou les ordinateurs dont elles avaient besoin pour travailler. Les pénuries ont affecté l'industrie lourde ; les constructeurs automobiles ont dû arrêter leurs usines, ce qui a eu un impact sur les moyens de subsistance des ouvriers d'usine et a déclenché une pénurie d'automobiles.
Cette situation a opposé les travailleurs du savoir, les joueurs, les spéculateurs boursiers et les fabricants dans une lutte pour des ressources rares.
Dans quelle mesure est-ce un problème ? Les professionnels de l'informatique ont-ils la responsabilité d'offrir une solution technique, telle qu'une alternative technologique aux GPU ? Ont-ils la responsabilité d'anticiper ce type de conséquences imprévues ? Comment les décideurs politiques devraient-ils gérer une pénurie de ressources critiques ?
Programmation parallèle
La programmation parallèle implique d'écrire du code qui divise la tâche d'un programme en parties, travaille en parallèle sur différents processeurs, fait en sorte que les processeurs signalent quand ils ont terminé, et s'arrête de manière ordonnée. Le C n'a pas été conçu en tenant compte de la programmation parallèle, nous devons donc utiliser des bibliothèques tierces pour la programmation parallèle en C. Certains langages plus récents ont été conçus dès le départ avec des facilités de programmation parallèle.
Modèles et langages de programmation parallèle
Un modèle de programmation (programming model) parallèle est une conception de haut niveau de la manière dont le programmeur peut contrôler les processeurs et les données qui circulent entre eux.
- Mémoire partagée : Dans le modèle de programmation à mémoire partagée (shared memory), les processus/tâches partagent un espace d'adressage commun, qu'ils lisent et écrivent de manière asynchrone. Divers mécanismes tels que les verrous/sémaphores sont utilisés pour contrôler l'accès à la mémoire partagée, résoudre les contentions et prévenir les conditions de concurrence et les interblocages. SHMEM en est un exemple.
- Fils d'exécution : Ce modèle de programmation est un type de programmation à mémoire partagée. Un fil d'exécution (thread) est un processus "lourd" unique qui peut avoir plusieurs chemins d'exécution concurrents "légers". Un exemple simple de fil d'exécution inclut une fonctionnalité de chat, de vidéo ou d'audio dans une application comme Microsoft Teams. Les exemples incluent Pthreads, OpenMP, Microsoft Threads, les fils d'exécution Java et Python, et les fils d'exécution CUDA pour les GPU.
- Passage de messages : Une approche de programmation parallèle où des processus distincts communiquent uniquement en envoyant des messages, sans partager de mémoire. Chaque ensemble de tâches utilise sa propre mémoire locale pendant le calcul. Plusieurs tâches peuvent résider sur la même machine physique et/ou sur un nombre arbitraire de machines. Un exemple est l'Interface de passage de messages (MPI) (Message Passing Interface (MPI)) qui a été développée pour la première fois dans les années 1990.
- Modèle hybride : Un modèle hybride combine plusieurs des modèles de programmation décrits précédemment ; actuellement, un exemple courant de modèle hybride est la combinaison de MPI avec le modèle de fils d'exécution. D'autres exemples de modèles hybrides incluent MPI avec CPU-GPU utilisant CUDA, MPI avec Pthreads, et MPI avec non-GPU.
Pour programmer en parallèle, vous pouvez étendre les compilateurs (compiler) (c'est-à-dire traduire des programmes séquentiels en programmes parallèles), étendre les langages (c'est-à-dire ajouter des opérations parallèles en plus du langage séquentiel), ajouter une couche de langage parallèle au-dessus du langage séquentiel, et définir un système de langage et de compilateur parallèle totalement nouveau. La stratégie d'extension de langage (2) est la plus populaire, et MPI/OpenMP en sont des exemples.
Réfléchissez
Programmation parallèle multithread
Pourquoi est-il important à l'heure actuelle pour les développeurs d'applications de se tourner vers le paradigme de programmation parallèle multithread et les nouvelles technologies informatiques émergentes pour leurs besoins d'applications ?
Conception de programmes parallèles
La conception et le développement de programmes parallèles ont toujours été un processus très manuel. Le programmeur est généralement responsable à la fois de l'identification et de l'implémentation effective du parallélisme. Le développement de code parallèle est souvent un processus long, complexe, sujet aux erreurs et itératif. Depuis plusieurs années, divers outils sont disponibles pour aider le programmeur à convertir des programmes séquentiels en programmes parallèles. Le type d'outil le plus courant utilisé pour paralléliser automatiquement un programme séquentiel est un compilateur parallélisant ou un préprocesseur. Un compilateur parallélisant fonctionne généralement de deux manières différentes : entièrement automatique ou dirigée par le programmeur.
Dans la méthode entièrement automatique, le compilateur analyse le code source (source code) et identifie les opportunités de parallélisme. L'analyse inclut l'identification des inhibiteurs de parallélisme, et elle peut déterminer si le parallélisme améliorerait réellement les performances. Les boucles sont la cible la plus fréquente de la parallélisation automatique.
Dans la méthode dirigée par le programmeur, le programmeur indique explicitement au compilateur comment paralléliser le code à l'aide de « directives de compilateur » ou éventuellement d'indicateurs de compilateur. Cette approche peut être utilisée conjointement avec un certain degré de parallélisation automatique. La parallélisation la plus courante générée par le compilateur est réalisée à l'aide de la mémoire partagée sur nœud et des fils d'exécution.
Si vous partez d'un code séquentiel existant et que vous avez des contraintes de temps ou de budget, la parallélisation automatique peut être la solution. Cependant, plusieurs mises en garde importantes s'appliquent à la parallélisation automatique : des résultats erronés peuvent être produits, les performances peuvent en fait se dégrader, elle peut être beaucoup moins flexible que la parallélisation manuelle, elle est limitée à un sous-ensemble de code, et elle peut en fait ne pas paralléliser le code si l'analyse du compilateur suggère qu'il y a des inhibiteurs ou que le code est trop complexe.
La première étape du développement de logiciels parallèles consiste à (1) comprendre le problème que vous souhaitez résoudre en parallèle. Les étapes suivantes incluent (2) le partitionnement, ou la division du problème en "morceaux" de travail discrets ; (3) l'identification du besoin de communications entre les tâches ; (4) la synchronisation de la séquence de travail et des tâches effectuées ; (5) l'identification des dépendances de données entre les instructions du programme ; (6) l'équilibrage de charge pour distribuer des quantités de travail approximativement égales entre les tâches afin que toutes les tâches soient occupées tout le temps ; (7) l'établissement de la granularité comme mesure qualitative du rapport calcul/communication ; (8) la gestion des opérations d'E/S qui sont généralement considérées comme des inhibiteurs de parallélisme ; (9) le débogage (une technique où le programme est lu ligne par ligne pour vérifier la présence de bogues) du code parallèle ; et (10) l'analyse et l'optimisation des performances du programme parallèle. La Figure 4.31 montre ces étapes.
Lien vers l'apprentissage
La programmation parallèle est un sujet approfondi offrant de nombreuses pistes d'étude, des détails de bas niveau du matériel et de la programmation à la conception d'algorithmes parallèles de haut niveau. Apprenez-en davantage sur l'Introduction to Parallel Computing Tutorial au Lawrence Livermore National Laboratory.
Utilisation de C avec les bibliothèques parallèles MPI et OpenMP
Nous nous concentrons ici sur la manière dont les programmes parallèles peuvent être écrits en langage C en utilisant une API, ce qui est la méthode la plus populaire. Certains langages de programmation (programming language) prennent en charge la programmation parallèle (parallel programming) et peuvent également être utilisés pour programmer des applications parallèles en utilisant des fonctionnalités de passage de messages intégrées au langage lui-même. Une fonctionnalité de passage de messages (message passing) est une approche de programmation parallèle où des processus distincts communiquent uniquement en envoyant des messages, sans partager de mémoire. Le modèle de multiprocesseur symétrique (SMP) (symmetric multiprocessor (SMP)) s'applique lors de la programmation de plusieurs processeurs pratiquement identiques. OpenMP est une bibliothèque (library) pour la programmation parallèle dans le modèle SMP. Lors de la programmation avec OpenMP, tous les fils d'exécution (threads) partagent la mémoire et les données. OpenMP prend en charge C, C++ et Fortran. Les fonctions (function) OpenMP sont incluses dans un fichier d'en-tête appelé omp.h. Un programme OpenMP a des sections séquentielles et des sections parallèles. En général, un programme OpenMP commence par une section séquentielle dans laquelle il configure l'environnement, initialise les variables, etc. Lors de l'exécution, un programme OpenMP utilisera un fil d'exécution dans les sections séquentielles, et plusieurs fils d'exécution dans les sections parallèles. Le fil parent (parent thread) est le fil d'exécution qui s'exécute du début à la fin du programme, et démarre et gère les fils d'exécution enfants. Un fil d'exécution enfant (child thread) est démarré par le fil parent et ne s'exécute que pendant une période limitée dans une section parallèle. Une section de code qui doit être exécutée en parallèle est marquée par une directive spéciale qui provoquera la formation de fils d'exécution enfants. Chaque fil d'exécution exécute la section parallèle du code indépendamment. Lorsqu'un fil d'exécution se termine, il rejoint le parent. Lorsque tous les fils d'exécution se terminent, le parent continue avec le code suivant la section parallèle.
Pleins feux sur l'industrie
Réseaux de neurones artificiels
Le domaine de l'intelligence artificielle utilise intensivement le calcul parallèle. Les réseaux de neurones artificiels sont une technologie largement utilisée qui simule le flux d'impulsions à travers les cellules nerveuses d'un cerveau. Un RNA doit être « entraîné » en lui fournissant de nombreux exemples des types d'entrées et de sorties qu'il traitera. Ce processus d'entraînement bénéficie grandement de la programmation parallèle. Un RNA typique possède des milliers de cellules simulées et est entraîné sur des milliers d'exemples. Cela représente des millions, voire des milliards, de calculs ; le calcul parallèle est un grand avantage car ce processus d'entraînement peut être effectué en parallèle. Les fabricants de matériel, y compris NVIDIA, Intel et Tesla, ont même créé des ordinateurs basés sur GPU spécifiquement pour la tâche d'entraînement des RNA. La Figure 4.32 illustre le modèle d'un réseau de neurones.