Répartition de charge dynamique dans un syst`eme
Transcription
Répartition de charge dynamique dans un syst`eme
Rapport de TER Répartition de charge dynamique dans un système distribuée Tuteur : Stephane Mancini Benjamin Petit Grenoble INP - Ensimag Mai 2010 Table des matières 1 Introduction 4 1.1 Contexte pratique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.2 Contexte scientifique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.3 Mon travail . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 2 La répartition de charge 5 2.1 Quelques notions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 2.2 Clefs de la répartition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 2.3 Algorithmes de répartitions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.3.1 Répartition statique . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 2.3.2 Répartition dynamique . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 2.3.3 Problème de cohérence dans un modèle distribué . . . . . . . . . . . . . . 9 3 Simulation 11 3.1 But de la simulation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.2 Structure d’une tâche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.3 Structure d’un message . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 3.4 Structure d’un noeud . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.4.1 L’unité de calcul . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 3.4.2 Liste de tâche en attente . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.4.3 Le routeur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 3.4.4 Le gestionnaire de charge . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 Résultats et limites . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 3.5 4 Bilan 16 5 Références 17 2 Remerciements Je remercie Stephane Mancini et le laboratoire GISPA-lab pour m’avoir accueilli durant ce TER. Je remercie égalemment le personnel enseignant de l’Ensimag qui a permis de mettre en place ce module. 3 1 1.1 Introduction Contexte pratique J’ai effectué ce Travail d’Études et de Recherche au sein du laboratoire Grenoble Images Parole Signal Automatique (GIPSA-lab). Le GIPSA-lab s’investit dans la recherche fondamentale sur le traitement du signal, la commande et le diagnostic des systèmes, sur la parole et la cognition. J’ai été affecté au le département Images et Signal et dans l’équipe Géométrie, Perception, Images, Gestes sous la responsabilité de Stéphane Mancini. 1.2 Contexte scientifique Pour augmenter les performances des processeurs, dans un premier temps, les fondeurs ont sans cesse cherché à augmenter la fréquence de fonctionnement de leurs puces. Cependant, il semblerait qu’aujourd’hui nous soyons arrivé à des fréquence qu’il va être difficile de dépasser. Pour augmenter les performances, les fabricants s’efforcent désormais de produire des puces dotées de plusieurs unités de calcul. De nos jours, nous utilisons de plus en plus des machines de type multi-processeurs ou multicoeurs. Afin de répartir le travail de manière intelligente sur toutes ces unités disponibles, il est nécessaire d’utiliser un système dit de répartition de charge ou load balancing en anglais. Beaucoup de ces systèmes multi-processeurs sont dit homogènes, c’est à dire que les différentes unités de calcul sont les mêmes : elles tournent à la même fréquence et ont des capacités de calcul similaires. Ce modèle présente tout de même des limites : la production de processeur possédant des coeurs de plus en plus nombreux coûtent de plus en plus cher aux fondeurs, du fait de la complexité des architectures. Dans les systèmes actuels, c’est le rôle du système d’exploitation de répartir correctement la charge sur les différents processeurs disponibles. 1.3 Mon travail Ici je vais traiter des systèmes hétérogènes, c’est à dire des systèmes qui possèdent des unités parfois très différentes les unes des autres ; on peut imaginer par exemple que le système possède des processeurs génériques ainsi que des accélérateurs matériels, très spécialisés, mais très performants pour une tâche donnée. De plus, je vais me concentrer essentiellement sur la répartition de charge dite distribuée, contrairement à la répartition de charge où la décision est centralisée en un point. 4 2 La répartition de charge 2.1 Quelques notions On suppose que le programme qui s’exécute peut être découpé dans des tâches bien connues, sur un ensemble d’unités de calculs aussi appelées noeuds. Un noeud exécute une tâche à la fois, et peut avoir une liste de tâches a exécuter dans une liste d’attente. Figure 1 – Grille de calcul 4x4 composée de 16 noeuds Les noeuds ne sont pas forcement interconnectés comme dans la figure 1 : on peut imaginer d’autres topologies possible pour une grille de calcul. Chaque tâche peut, après avoir été calculée, démarrer d’autres sous-tâches. On néglige ici les arguments et les retour de résultats des différentes sous-tâches. C’est pour cela qu’on étudie principalement la répartition de charge distribuée : chaque noeud choisira à qui envoyer sa ou ses sous-tâches. Chaque programme peut donc être représenté par un arbre comme celui-ci : Figure 2 – Représentation d’un programme en arbre de tâches Chacune des tâches nécessite une puissance de calcul qui peut être prédite à l’avance. De même, pour chaque noeud, on connait leur capacité de traiter telle ou telle tâche. La figure 2 montre bien que plusieurs tâches peuvent être exécutées en parallèle ; et comme tous les noeuds n’ont pas les mêmes capacités de calcul, il faut choisir soigneusement le noeud qui s’occupera d’une tâche donnée : l’efficacité de l’algorithme de répartition de charge est donc primordiale. 5 2.2 Clefs de la répartition Dans un système homogène, il est relativement aisé de distribuer les tâches de manière efficace ; comme tous les noeuds de calculs ont les même capacités, il suffit de donner du travail aux noeuds les moins chargés. Dans un système hétérogène, c’est plus complexe. Tous les noeuds n’ayant pas les mêmes capacités de calcul il faut choisir soigneusement quel noeud on va utiliser pour telle tâche. On ne prend pas forcement le noeud le plus rapide, car il est peut-être déjà très chargé, tout comme on n’utilise pas forcement le noeud le moins occupé parce que trop lent pour la tâche. Prenons par exemple une grille de calcul, dont l’état est le suivant : Noeud Disponible dans 1 5ms 2 11ms 3 8ms Capacités Tâche A 10ms Tâche B 8ms Tâche C 20ms Tâche A 6ms Tâche B 8ms Tâche C 5ms Si la prochaine tâche à traiter est une tâche de type A : – Noeud 1 : fin estimée de la tâche A : 5ms + 10ms = 15ms – Noeud 2 : fin estimée de la tâche A : 11ms + 6ms = 17ms – Noeud 3 : impossible d’effectuer cette tâche Dans cette exemple, si on a une tâche A a traité, il vaut mieux l’envoyer sur le noeud 1, même si il est moins rapide que le noeud 2 pour cette tâche. On voit aussi ici que l’algorithme de répartition de charge doit être très rapide : plus la prise de décision est longue, plus la décision pourra être fausse. Bien sûr dans cette exemple, on estime que le coût de la transmission de charge est gratuite , c’est à dire que la transmission de messages entre les noeuds est instantanée et que le lien ne peut être saturé. Cependant, dans le cas des Network On Chip (NoC), ce facteur peut être très limitant. D’autres facteurs peuvent rentrer en compte : la topologie du réseau, la fragmentation des noeuds exécutés, des impératifs de consommations. . . Pour prendre des décisions efficaces dans un système hétérogène, il faut donc connaı̂tre la charge des différents modules, ainsi que de leurs capacités de calcul respectives. Mais comment garantir une cohérence de la vision de la charge des noeuds dans le système entier ? 6 2.3 Algorithmes de répartitions Il existe deux grands types d’algorithmes de répartitions : les algorithmes de répartitions de charges dit statiques, que l’on peut définir à la compilation, et les algorithmes dit dynamiques, calculé pendant l’exécution du programme. Les algorithmes dit statiques sont certainement les plus efficaces, mais ne peuvent être mis en place que si le programme n’est pas interactif. De même, pour un programme distribué en binaire, il n’est pas forcement possible de savoir sur quel type d’architecture va être exécuté le programme. Dans tous ces cas il faut utiliser des algorithmes dynamiques. Cependant, même en utilisant un algorithme dynamique, il peut être intéressant d’utiliser un algorithme pseudo-statique pour le mappage initial des tâches, comme nous allons le voir dans le point suivant. 2.3.1 Répartition statique La répartition statique peut être calculé au moment de la compilation ; si le programme n’est pas interactif, c’est avec cette méthode qu’on obtiendra les meilleurs temps de calculs, du fait qu’il n’y aura pas d’algorithme à exécuter pour choisir le noeud qui exécutera une tâche donnée. Mais comme soulevé dans [1], même dans le cas de programme dynamique, il est utile de faire appel à une stratégie de placement statique. Voyons un exemple dans une grille de calcul 4x4. Figure 3 – Mauvais placement Figure 4 – Bon placement Dans la figure 3, la tâche initiale est située sur le noeud le plus foncé. On voit que si elle se divise en deux sous-tâches, les communications avec les autres noeuds seront plus compliqués, du fait que pour transmettre d’autres tâches ou des résultats, les chemins seront plus longs et nécessiteront des sauts. Au contraire, la situation dans la figure 4 est meilleure ; le noeud contenant la tâche initiale a moins de chance d’être isolé. On voit donc bien que même dans le cadre d’algorithme dynamique, le mappage initial des tâches (et donc par extension, la mise en place d’une grille de calcul) est très important. Cette stratégie de mappage initiale doit être implémentée dans la grille de calcul elle-même, non pas à la compilation si l’on veut que le code soit portable. 7 2.3.2 Répartition dynamique Pour palier au manque des algorithmes de répartitions statiques, des chercheurs ont expérimentés plusieurs types de répartitions dynamique. Le choix du meilleur noeud étant plus ou moins facile suivant les données qui influenceront la prise de décision (cf la partie 2.2), la plupart des travaux se concentrent sur l’optimisation des communications entre noeuds, notemment éviter la congestion réseau. Ces algorithmes visent à choisir entre deux noeuds qui pourraient accueillir une même tâche, dans des délais équivalents. Bien que dans mon travail, je n’ai pas traité de la partie pénalité dûe au réseau , je pense qu’il est important d’en citer quelques un. Les trois algorithmes suivant sous tirés de [1]. On y trouve aussi d’autres stratégies, mais j’en ai retenues trois : 1. First Fee (FF) Cette algorithme est le plus simple ; il n’est jamais utilisé en pratique, mais on l’utilise pour comparer plusieurs méthodes différents. Il consiste simplement à prendre le premier noeud disponible le plus proche, en parcourant le réseau colonne à colonne. Cette algorithme n’a pas de coût d’évaluation. 2. Nearest Neighbor (NN) Cette algorithme ressemble à la stratégie précédente ; elle n’a pas de coût d’évaluation. Cette stratégie consiste à chercher le noeud le plus proche en testant tous les voisins à une distance n, n variant de 1 au nombre de noeuds disponible dans la grille. 3. Path Load (PL) Le troisième algorithme est le plus compliqué. Les algorithmes précédent ne tiennent pas compte de la bande passante disponible entre les noeuds de la grille : cette dernière stratégie tente de diminuer la congestion réseau en prenant compte de cette donnée. Cet algorithme calcul le coût des transmissions entre chaque noeud à l’aide de l’équation suivante : costk = X ratec(i,j) + X ratec(i,j) Où ratec(i,j) et ratec(j,i) correspondent à la vitesse de transmission entre deux noeuds, du sens i → j et du sens j → i (les communications ne sont pas nécessairement symétriques) La stratégie PL est certainement la plus complète. Cependant, les expériences [1] ont montré que les résultats obtenus avec cette méthode sont très proches de ceux obtenus avec la stratégie NN. Celà est sans doute dû au calcul qui est nécessaire pour la stratégie PL. Bien que l’on peut difficilement dire quel est l’algorithme le meilleur (cela dépend essentiellement de la nature de la grille de calcul ainsi que du programme à faire tourner), cette expérience montre bien que l’algorithme de répartition de charge, pour être efficace, doit être très rapide et simple à exécuter. 8 2.3.3 Problème de cohérence dans un modèle distribué Dans un système classique centralisée, les décisions de délégation de tâches dont prises en un point. Ce point maı̂tre connait avec exactitude l’état des noeuds de calcul, car lui et lui seul envoie des tâches. Dans un système distribuée, les décisions sont prises par chacun des noeuds : or, comment garantir que l’état des noeuds est correct en chacun des points de la grille ? On appelera état réel l’état dans lequel le noeud est à l’instant t, et état supposé l’état d’un noeud vu par un autre noeud à l’instant t. Noeud Disponible dans 1 5ms 2 11ms 3 8ms États supposés des Noeud 2 Noeud 3 Noeud 1 Noeud 3 Noeud 1 Noeud 2 autres noeuds 2ms 8ms 10ms 8ms 5ms 11ms Figure 5 – États réels des noeuds et états supposés Dans la figure 5, on voit un exemple d’incohérence : l’état supposé du noeud 2 par le noeud 1 est fausse (2ns contre 11ms en réalité). Le challenge majeur pour une répartition dynamique est donc de réduire au maximum l’écart entre état réel et état supposé. Pour cela, plusieurs solutions sont possibles : Avertir tous les noeuds qu’une tâche a été affecté En théorie, probablement une des stratégies les meilleures. Cependant, son application en pratique pose d’évident problème de communication dans la grille de calcul : si des milliers de tâches sont exécutées sur une grille, le réseau va très vite être saturé. Envoyer aux autres noeuds son état de manière régulière Cette stratégie consiste à envoyer toutes les période t son état aux autres noeuds. Pour éviter une congestion du réseau, il vaut mieux éviter que plusieurs broadcast de l’état de noeuds aient lieu en même temps. Cette valeur peut être différente selon chaque noeud. De plus, elle dépend énormément de la durée du traitement des tâches, et de la nature de la grille de calcul, et doit être déterminée au cas par cas. Avertir les autres noeuds que son état a changé de manière significative Cela rejoint un peu l’idée précédente : on envoie à tous les autres noeuds son état actuel, lorsque qu’il a changé de manière significative. Le problème est de quantifier le delta qui déterminera quand envoyer une mise à jour d’état. Ici aussi le paramètre de cette stratégie est très dépendent de la durée des calculs et de la nature de la grille de calcul. Avertir les autres noeuds qu’une décision en lui convient pas Cette fois ci, un noeud enverra son nouveau statut lorsqu’une tâche lui aura été envoyé, alors que, selon lui, cette tâche devrait être exécutée sous un autre noeud. Bien sûr, on peut le combiner avec la stratégie précédente, ce qui veut dire qu’il tolèrera une certaine marge d’erreur. 9 Noeud si exécution sur noeud courant) 1 14ms si exécution sur autre noeud Noeud 2 2ms Noeud 3 19ms Figure 6 – Décision litigieuse La figure 6 montre que le noeud 1 a reçu une tâche, qu’il aura finie de calculer dans 14ms. Or, il lui semble, selon sa vision de la grille, qu’il serait plus judicieux de l’envoyer sur le noeud 2. Il prend quand même la tâche qui lui a été envoyée, mais il envoie son statut à jour aux autres noeuds. Cette dernière stratégie semble celle qui a le moins d’inconvénients, par rapport aux autres. Cependant, elle n’est pas parfaite, et des erreurs de décisions peuvent être prises. On peut ainsi compléter cette solution avec une boucle d’auto-correction comme cela est proposé dans [2] Le principe de cette boucle est simple : à intervalle régulier, on recalcule la politique de répartition de charge pour chacune des tâches présentes dans la file d’attente du noeud. Mais le problème est de bien choisir la durée de ces intervalles. 10 3 Simulation Dans cette partie je vais parler du petit simulateur en C que j’ai développé afin de tester divers algorithmes de répartition de charge. 3.1 But de la simulation Le simulateur doit donc simuler le fonctionnement d’une grille de calcul, dans laquelle se trouve des noeuds de calculs. On devra exécuter des programmes comme celui de la figure 2. On néglige la question de passage de paramètre ou de gestion de résultat. Nous ne prenons pas en compte la topologie de la grille et les performances du réseau ; pour simplifier, on estime que tous les noeuds sont reliés ensemble et qu’il n’y a aucun problème de communication dans le réseau. Enfin, le code doit être le plus modulaire possible afin de pouvoir changer des modules, que je détaille par la suite, afin de pouvoir changer le comportement de ces derniers, afin d’améliorer le simulateur. 3.2 Structure d’une tâche Initialement, chaque tâche devait exécuter des fonctions C, compilées avec des options différentes selon les noeuds, afin de simuler une différence de rapidité de traitements entre ces derniers. Par manque de temps, une tâche ne contient qu’un temps incompressible de calcul. On peut facilement changer le contenu de cette structure (via le fichier task.h ainsi que le traitement de celle-ci dans la fonction traite task (dans node.c). 3.3 Structure d’un message Divers messages peuvent être échangé dans la grille de calcul entre noeuds. Ces messages peuvent être du type : – STATUS : un noeud envoie un message de ce type afin d’envoyer son état réel aux autres noeuds du système. – TASK : un noeud envoie une tâche à un autre grâce à ce message. – TASK IN : utilisé pour l’envoie d’une tâche en interne (dans le cadre d’une création d’une sous-tâche - voir le point sur l’unité de calcul). On pourrait imaginer d’autres types de messages : des messages d’erreur, pour signaler qu’un noeud est H.S., des messages de recalibration, pour mettre à jour les capacités de chaque noeud. . . On peut ajouter des types de messages différents dans le fichier msg.h 11 3.4 Structure d’un noeud Chaque noeud de la grille est lancé par un thread qui lui est propre, pour simuler l’exécution parallèle de plusieurs noeuds. Figure 7 – Composition d’un noeud Pour cette simulation, on considère qu’un noeud (voir figure 7) est composé de : – une unité de calcul – une liste de tâches en attente – un gestionnaire de charge – un routeur 3.4.1 L’unité de calcul Figure 8 – Traitements réalisés dans l’unité de calcul L’unité de calcul simule le calcul, en attendant le temps spécifié par la tâche qu’il doit traiter. En plus de ce temps, il ajoute une pénalité, qui dépend de sa capacité à traiter la tâche : ceci permet de simuler des différences de performances dans le traitement des tâches. Si une des tâches qu’il traite nécessite de lancer une ou plusieurs sous-tâche, il les envoie au gestionnaire de charge. 12 Lorsque la tâche courante a fini d’être calculée (c’est à dire que le délai d’attente est écoulé), elle va chercher la première tâche située dans la liste de tâche en attente. L’unité de calcul s’exécute dans un thread qui lui ai propre. 3.4.2 Liste de tâche en attente La liste des tâches en attente est tout simplement une liste FIFO qui contient la liste des tâches à passer à l’unité de calcul. Dans le simulateur elle est implémenté avec un pipe entre le gestionnaire de charge et l’unité de calcul. 3.4.3 Le routeur Le routeur se charge des communications entre les différents noeuds de la grille. C’est lui qui forgera les paquets à envoyer aux autres noeuds selon la nature du message. C’est également lui qui traitera les paquets reçus et qui les enverra ensuite au gestionnaire de charge. Dans le simulateur, les liaisons entre les noeuds sont implémenté avec des pipe. On peut facilement changer l’implémentation du routeur sans toucher au reste du code, par exemple pour simuler plus précisément les problèmes de communications qui peuvent avoir lieu. Tout comme l’unité de calcul, le routeur s’exécute dans un thread à part. 3.4.4 Le gestionnaire de charge Le gestionnaire de charge est l’élément central de la simulation. C’est lui qui intègre l’algorithme de répartition de charge ainsi que la boucle correctrice, exécutée à intervalle régulier. Lorsque le gestionnaire de charge reçoit une tâche provenant du module réseau, il regarde d’abord dans un premier temps si selon lui, il est acceptable d’exécuter cette tâche. Si oui, il l’envoie directement à la liste des tâches à traiter. Sinon il demande au module réseau d’envoyer un message de mise à jour de son statut réel aux autres noeuds. Pour éviter une partie de ping-pong entre plusieurs noeuds, on accepte de prendre la tâche, même si le noeud courant est très chargé. C’est le rôle de la boucle de correction de palier à ce problème : évaluée à un bon intervalle, elle doit limiter le nombre d’erreurs. Le gestionnaire de charge reçoit également des tâches provenant du module de calcul, lorsque ce dernier demande à exécuter une ou plusieurs sous-tâches. Il choisit alors où cette ou ces dernières doivent s’exécuter, soit localement, soit sur un autre noeud. Il est a noter que lors d’un envoie de tâche à un autre noeud, il met à jour l’état supposé de celui-ci, afin d’éviter de le surcharger en lui envoyant une dizaine de tâche dans un délai court. Les fonctions utilisées par le gestionnaire doivent être très efficace, pour que la prise de décision soit rapide. Ainsi, les erreurs de jugement sont limités. Tout comme l’unité de calcul et le module réseau, pour la simulation, le gestionnaire de charge s’exécute dans un thread à part. 13 Figure 9 – Traitements réalisés dans le gestionnaire de charge Le nombre de threads pour simuler un noeud est donc de trois. Malheureusement, les machines utilisées pour faire tourner le simulateur ne dispose pas d’un nombre de processeur illimité. Pour limiter le nombre de thread, on fait tourner le module réseau et le gestionnaire de charge dans le même thread. Cela a théoriquement peu d’impact dans la simulation actuelle, du fait de la grande rapidité des méthodes utilisées dans le gestionnaire de charge et que le module réseau n’a aucun traitement complexe à réaliser. 14 3.5 Résultats et limites Dans les expérimentations, les résultats obtenus avec le simulateur se sont révélés très proches des meilleurs résultats possibles. Mais j’ai utilisé des stubs assez grossier pour simuler les temps de calcul (l’unité de calcul se contentait de faire des sleep de quelques secondes). L’utilisation de la boucle de correction n’a eu que très peu d’impact sur le résultat. Cependant, en augmentant les niveaux de tolérance qui régissent l’envoi d’une mise à jour de statut, afin de réduire les échanges au sein de la grille, cette boucle permet de limiter les erreurs de répartition. De même, en augmentant de manière artificielle le temps d’exécution de l’algorithme de répartition de charge, on note une très forte augmentation d’erreurs de choix de répartitions, ce qui souligne l’importance d’avoir des algorithmes simple si les tâches a exécuter sont rapides. Pour simuler d’une manière plus exhaustive une grille de calcul, il faudrait prendre en compte le réseau qui relie tous les noeuds, ainsi que la transmission des arguments et des résultats produits par les tâches. 15 4 Bilan J’ai effectué mon stage de fin de DUT dans le laboratoire LIMSI, Laboratoire d’Informatique pour la Mécanique et les Sciences de l’Ingénieur, à Orsay (Paris XI). J’avais donc déjà une petite idée de la recherche publique. Cependant, j’ai eu surtout un rôle de développeur pendant ce stage. C’est pour cela que j’ai eu envie de faire un TER. Celui-ci m’a permis de m’initier réellement à la recherche. J’ai appris à chercher des articles sur des sites spécialisées ainsi qu’à lire de long documents universitaires. Ce fût une expérience enrichissante, qui en plus de m’avoir fait découvrir le monde de la recherche, m’a permis de travailler sur un domaine que je n’aurai probablement pas eu l’occasion de traiter dans les cours classiques, la répartition de charge distribuée. 16 5 Références [1] Carvalho, E. ; Calazabs, M. ; Moraes,F. ; Heuristic for Dynamic Task Mapping in Noc-based Heterogenous MPSoCs. [2] Mancini S. ; Architecture materielle pour la synthèse d’image par lancer de rayon. 2000 [3] Bertozzi S. ; Acquaviva A. ; Bertozzi D. ; Poggiali A. ; Supporting Task Migration in MuliProcessor Systems-on-chip : A Feasibility Study 17