| | 11 | |
| | 12 | {{{ |
| | 13 | #!html |
| | 14 | <h1>TME 8 : Partitionnement de Graphe/MinCut</h1> |
| | 15 | }}} |
| | 16 | |
| | 17 | = A. Introduction = |
| | 18 | |
| | 19 | Soit un graphe G(N, H), où N est l'ensemble des noeuds, et H l'ensemble des |
| | 20 | arêtes. Une bipartition de G est formée de 2 sous-ensembles L (left) et R |
| | 21 | (right) de l'ensemble des noeuds N, dont l'intersection est vide, et l'union |
| | 22 | est égale à N. |
| | 23 | |
| | 24 | En supposant que le nombre de noeud est pair, on dit qu'une bipartition est |
| | 25 | équilibrée si `L = R = N / 2` |
| | 26 | |
| | 27 | Pour ce qui nous concerne, on s'intéresse à une net-list de cellules |
| | 28 | précaractérisées, et on cherche à partitionner l'ensemble des cellules |
| | 29 | en deux sous-ensembles L et R tels que le nombre de signaux traversant la |
| | 30 | frontière entre les deux sous-ensembles soit minimal. |
| | 31 | |
| | 32 | Nous allons raisonner sur un graphe non-orienté, défini de la façon suivante: |
| | 33 | |
| | 34 | Les noeuds Ci représentent les cellules de la net-list `(0 < i < N-1)`. |
| | 35 | Il existe une arête entre deux cellules Ci et Cj chaque fois qu'il existe au |
| | 36 | moins un signal connectant un port de la cellule Ci à un port de la cellule Cj. |
| | 37 | |
| | 38 | Attention: avec cette définition, à un seul signal de la net-list peuvent |
| | 39 | correspondre plusieurs arêtes dans le graphe: si le signal de sortie d'une |
| | 40 | porte logique Cx attaque n portes C1 à Cn, on a n arêtes dans le graphe. |
| | 41 | |
| | 42 | On appelle «matrice d'adjacence» {Aij} la matrice carrée N * N qui représente |
| | 43 | la structure du graphe: |
| | 44 | * Aij = 1 si il existe une arête dans le graphe entre les noeuds Ci et Cj |
| | 45 | * Aij = 0 sinon |
| | 46 | |
| | 47 | On défini comme suit la fonction de coût du problème d'optimisation MinCut: |
| | 48 | |
| | 49 | {{{ |
| | 50 | FC = Somme Aij * Kij |
| | 51 | i,j |
| | 52 | |
| | 53 | avec Kij = 1 si les cellules Ci et Cj sont de part et d'autre de la frontière |
| | 54 | Kij = 0 sinon |
| | 55 | }}} |
| | 56 | |
| | 57 | = B. Algorithme MinCut glouton = |
| | 58 | |
| | 59 | On part d'une bi-partition équilibrée initiale arbitraire en deux ensembles |
| | 60 | L et R, et on cherche à optimiser la fonction de coût FC, sans forcément |
| | 61 | rechercher le minimum absolu, mais avec un temps de calcul très court. |
| | 62 | |
| | 63 | L'idée de base de l'algorithme est de pré-calculer, pour chaque cellule Ci |
| | 64 | un gain G(i) représentant la variation de la fonction de coût FC lorsqu'on |
| | 65 | déplace la cellule Ci d'un côté de la frontière à l'autre (sans déplacer |
| | 66 | aucune autre cellule). |
| | 67 | |
| | 68 | Le gain G(i) est défini par: |
| | 69 | {{{ |
| | 70 | G(i) = Somme Aij * Dij |
| | 71 | j |
| | 72 | avec Dij = +1 si les cellules Ci et Cj sont de part et d'autre de la frontière |
| | 73 | Dij = -1 sinon |
| | 74 | }}} |
| | 75 | Après avoir calculé le gain G(i) pour toutes les cellules Ci, on classe les |
| | 76 | cellules dans deux listes ordonnées par gain décroissant (une liste GAIN_L |
| | 77 | pour les cellules appartenant à L et une liste GAIN_R pour les cellules |
| | 78 | appartenant à R). |
| | 79 | |
| | 80 | On transfère la première cellule Ci de la liste ordonnée GAIN_L vers R, |
| | 81 | on met à jour les gains des cellules adjacentes à Ci, ainsi que l'ordre des |
| | 82 | listes GAIN_L et GAIN_R. La cellule Ci transférée dans R n'est pas rangée |
| | 83 | dans GAIN_R, car elle n'est plus autorisée à bouger. |
| | 84 | |
| | 85 | On transfère la première cellule Ci de la liste ordonnée GAIN_R vers L, |
| | 86 | on met à jour les gains des cellules adjacentes à Ci, ainsi que l'ordre des |
| | 87 | listes GAIN_L et GAIN_R. La cellule Ci transférée dans L n'est pas rangée |
| | 88 | dans GAIN_L, car elle n'est plus autorisée à bouger. |
| | 89 | |
| | 90 | On recommence les deux étapes ci-dessus tant que cet échange entraîne une |
| | 91 | diminution de la fonction de coût. |
| | 92 | |
| | 93 | = C. Structures de données = |
| | 94 | |
| | 95 | On a besoin de deux structures de données séparées pour représenter d'une part |
| | 96 | le graphe à partitionner, et d'autre part les listes GAIN_L et GAIN_R. Cette |
| | 97 | seconde structure de donnée étant utilisée pour calculer les mouvements de |
| | 98 | cellules à effectuer, est appelée «gestionnaire de mouvements». |
| | 99 | |
| | 100 | == C.1. représentation du graphe == |
| | 101 | |
| | 102 | Les noeuds du graphe sont identifiés par un index compris entre 0 et (NBNODE-1). |
| | 103 | |
| | 104 | La structure du graphe est représentée par un tableau NODE[ ] indexé par |
| | 105 | le numéro du noeud (la taille du tableau est donc égale à NBNODE). Chaque |
| | 106 | case du tableau NODE[i] contient un pointeur sur une liste de bi-pointeurs |
| | 107 | représentant l'ensemble des noeuds adjacents au noeud [i]. Il y a donc un |
| | 108 | bipointeur pour chaque arête attachée au noeud[i]. |
| | 109 | |
| | 110 | Un second tableau PART[ ] est lui aussi indexé par le numéro du noeud. Chaque |
| | 111 | case PART[i] contient un entier définissant à quel sous-ensemble appartient |
| | 112 | le noeud [i]: (valeur 0 pour l'ensemble L, et valeur 1 pour l'ensemble R). |
| | 113 | |
| | 114 | Le graphe est donc représenté par la structure suivante: |
| | 115 | {{{ |
| | 116 | typedef struct graph_t { |
| | 117 | unsigned NBNODE; // nombre de noeuds du graphe |
| | 118 | bip_t * * NODE; // pointeur sur le tableau NODE[NBNODE] |
| | 119 | unsigned * PART; // pointeur sur le tableau PART[NBNODE] |
| | 120 | } graph_t |
| | 121 | }}} |
| | 122 | |
| | 123 | == C2. représentation du gestionnaire des mouvements == |
| | 124 | |
| | 125 | On appelle arité d'un noeud du graphe, le nombre d'arêtes attachées à ce |
| | 126 | noeud. Le gain maximum G(i) associé au mouvement d'un noeud [i] est au plus |
| | 127 | égal à l'arité du noeud [i]: Ce gain peut être positif ou négatif, mais sa |
| | 128 | valeur absolue ne peut pas être supérieure au nombre des noeuds auquel il |
| | 129 | est connecté. |
| | 130 | Soit GMAX la valeur maximale de l'arité (en considérant tous les noeuds du graphe). |
| | 131 | Pour tout noeud [i] du graphe, on a donc GMAX < G(i) < GMAX |
| | 132 | |
| | 133 | Pour représenter la liste ordonnée GAIN_L, on effectue une partition de |
| | 134 | l'ensemble des cellules appartenant à L, en regroupant dans un même sous |
| | 135 | ensemble tous les noeuds possédant le même gain. Ce sous ensemble est |
| | 136 | représenté par une liste doublement chaînée, dont chaque élément de type |
| | 137 | bichain_t contient un index de noeud. On définit un tableau de pointeurs |
| | 138 | GAIN_L[ ] possèdant 2 * GMAX + 1 cases, et indexé par une valeur de gain |
| | 139 | comprise entre GMAX et GMAX. Chaque case correspond à une valeur de gain, et |
| | 140 | contient un pointeur sur une des listes doublement chaînées définies ci-dessus. |
| | 141 | |
| | 142 | On fait la même chose pour représenter la liste GAIN_R. |
| | 143 | |
| | 144 | Au début de l'algorithme, chaque noeud du graphe est inséré dans la liste |
| | 145 | chaînée qui correspond à sa valeur de gain. Au fur et à mesure que les noeuds |
| | 146 | sont transférés, les listes se vident, car un noeud qui a subi un mouvement |
| | 147 | n'est plus autorisé à bouger. Puisque le gain des noeuds changent au cours |
| | 148 | de l'algorithme, un même noeud peut être retiré d'une liste et inséré dans |
| | 149 | une autre, ce qui justifie l'utilisation de listes doublement chaînées. |
| | 150 | |
| | 151 | Dans la structure de donnée «graphe_t» représentant le graphe, les noeuds |
| | 152 | du graphe sont identifiés par leur index. On a donc besoin d'introduire dans |
| | 153 | la structure «move_manager_t» représentant le gestionnaire de mouvements, |
| | 154 | un tableau de pointeurs NODES [ ], indexé par le numéro du noeud, et contenant |
| | 155 | un pointeur sur la structure bichain_t représentant un les noeuds du graphe |
| | 156 | dans les listes doublement chaînées GAIN_L et GAIN_R. |
| | 157 | |
| | 158 | On utilise donc les structures suivantes: |
| | 159 | |
| | 160 | {{{ |
| | 161 | typedef struct bichain_t { |
| | 162 | bichain_t *NEXT; // pointeur sur le noeud suivant de même gain |
| | 163 | bichain_t *PREV; // pointeur sur le noeud précédent de même gain |
| | 164 | unsigned INODE; // index du nSud |
| | 165 | int IGAIN; // index dans le tableau des gains |
| | 166 | unsigned PART; // numero de la partie 0 pour R, 1 pour L |
| | 167 | } bichain_t |
| | 168 | }}} |
| | 169 | {{{ |
| | 170 | typedef struct move_manager_t { |
| | 171 | int GMAX; // la taille des tableaux GAIN_R et GAIN_L est égale à 2*GMAX+1 |
| | 172 | bichain_t **GAIN_L; // pointeur sur le tableau de pointeurs pour la partie gauche |
| | 173 | bichain_t **GAIN_R; // pointeur sur le tableau de pointeurs pour la partie droite |
| | 174 | int LX; // index de la première case non vide de GAIN_L (init à -1) |
| | 175 | int RX; // index de la première case non vide de GAIN_R (init à -1) |
| | 176 | bichain_t **NODES; // pointeur sur le tableau de pointeurs sur les noeuds |
| | 177 | } move_manager_t |
| | 178 | }}} |
| | 179 | |
| | 180 | = D. Fonctions d'accès aux structures de données = |
| | 181 | |
| | 182 | On dispose d'un ensemble de fonctions permettant de construire et de manipuler les deux structures de données graph_t et move_manager_t. |
| | 183 | |
| | 184 | == D.1. graph_t * parse_graph(char *filename) == |
| | 185 | |
| | 186 | Cette fonction prend pour argument un nom de fichier au format.dot |
| | 187 | représentant le graphe à partitionner, et construit en mémoire la structure |
| | 188 | graph_t correspondante, et renvoie un pointeur sur cette structure. Si aucune |
| | 189 | partition initiale n'est définie dans le fichier .dot, les noeuds d'index |
| | 190 | inférieur à NBNODE/2 sont arbitrairement rangés dans le sous-ensemble L, |
| | 191 | et les noeuds d'index supérieur à NBNODE/2 sont rangés dans le sous-ensemble R. |
| | 192 | |
| | 193 | Un exemple de fichier au format .dot est donné ci-dessous. Les noeuds du graphe |
| | 194 | sont identifiés par un index, il y a une ligne par arête, les deux sous-graphes |
| | 195 | cluster0 et cluster1 définissent la partition initiale et sont optionnels. |
| | 196 | |
| | 197 | {{{ |
| | 198 | graph |
| | 199 | { |
| | 200 | // les clusters sont optionnels |
| | 201 | subgraph cluster0 {0 1 2 3} |
| | 202 | subgraph cluster1 {4 5 6 7} |
| | 203 | 0--4 |
| | 204 | 0--5 |
| | 205 | 0--7 |
| | 206 | 1--5 |
| | 207 | 3--7 |
| | 208 | 1--2 |
| | 209 | 5--6 |
| | 210 | } |
| | 211 | }}} |
| | 212 | |
| | 213 | == D.2. void drive_graph(graph_t *p, char *filename) == |
| | 214 | |
| | 215 | Cette fonction prend en paramètre un pointeur p sur une structure graph_t, |
| | 216 | ainsi que le nom du fichier de sortie, et écrit sur disque un fichier au |
| | 217 | format .dot, en conservant dans le fichier les informations de partition |
| | 218 | (sous-ensemble L et R). Ce format est affichable sous forme graphique et |
| | 219 | permet de visualiser la partition. |
| | 220 | |
| | 221 | == D.3. bichain_t * cons_bichain (bichain_t *prev, bichain_t *next, unsigned inode, int igain, unsigned part) == |
| | 222 | |
| | 223 | Cette fonction alloue la mémoire nécessaire pour créer un élément d'une liste |
| | 224 | doublement chaînée, elle initialise les différents champs de cette structure, |
| | 225 | et renvoie un pointeur sur cette structure. |
| | 226 | |
| | 227 | == D.4. void free_bichain (bichain_t *p) == |
| | 228 | |
| | 229 | Cette fonction libère la mémoire allouée lors de la construction de l'objet |
| | 230 | bichain_t par la donction cons_bichain(). |
| | 231 | |
| | 232 | == D.5. move_manager_t * cons_move_manager(graph_t *p) == |
| | 233 | |
| | 234 | Cette fonction pend pour argument un pointeur sur la structure représentant |
| | 235 | le graphe partitionné, et construit en mémoire la structure move_manager_t |
| | 236 | correspondant à cette partition. |
| | 237 | |
| | 238 | == D.6. void add_node (move_manager_t *mm, bichain_t *node) == |
| | 239 | |
| | 240 | Cette fonction prend pour arguments un pointeur sur la structure représentant |
| | 241 | les gestionnaire de mouvements, un pointeur sur un élément de liste doublement |
| | 242 | chaînée (représentant un noeud), contenant l'index du nSud dans le graphe, |
| | 243 | un entier représentant l'index dans le tableau GAIN_R ou GAIN_L, et un entier |
| | 244 | définissant le sous-ensemble d'appartenance (0 pour R, 1 pour L). Elle range |
| | 245 | ce noeud dans la liste doublement chaînée correspondante, elle met à jour |
| | 246 | le tableau NODES et les index LX ou LR. Les champs NEXT et PREV du nSud ne |
| | 247 | sont pas utilisés, ils sont initialisés par la fonction add_node elle-même. |
| | 248 | |
| | 249 | == D.7. bichain_t * del_node(move_manager_t *mm, bichain_t *node) == |
| | 250 | |
| | 251 | cette fonction prend pour arguments un pointeur sur la structure représentant |
| | 252 | les gestionnaire de mouvements, et un pointeur sur un élément de liste |
| | 253 | doublement chaînée (représentant un noeud). Elle retire cet élément de la |
| | 254 | liste doublement chaînée, et renvoie un pointeur sur la structure bichain_t |
| | 255 | pour une éventuelle libération de la mémoire. |
| | 256 | |
| | 257 | == D.8. int compute_gain (graph_t *g, unsigned index) == |
| | 258 | |
| | 259 | Cette fonction prend pour argument un pointeur sur la structure représentant le |
| | 260 | graphe, un index désignant un noeud particulier, et calcule le gain associé |
| | 261 | à ce noeud, pour la partition courante. |
| | 262 | |
| | 263 | == D.9. int global_cost (graph_t *g) == |
| | 264 | |
| | 265 | Cette fonction prend pour argument un pointeur sur la structure représentant |
| | 266 | le graphe, calcule la fonction de coût globale pour la partition courante, |
| | 267 | et renvoie la valeur calculée. |
| | 268 | |
| | 269 | = E. Travail à réaliser = |
| | 270 | |
| | 271 | Nous vous proposons un programme à trous. Vous allez pouvoir remplacer |
| | 272 | progressivement les fichiers objets fournis par vos propres fichiers objet. |
| | 273 | |
| | 274 | == E.1. Construction et initialisation du gestionnaire de mouvements == |
| | 275 | |
| | 276 | C'est un point important puisque que c'est la structure de base de |
| | 277 | l'algorithme. Vous disposez pour vous aidez d'une fonction dump_move_manager() |
| | 278 | qui affiche le contenu de la structure. |
| | 279 | |
| | 280 | == E.2. Ecriture de l'algorithme MinCut glouton == |
| | 281 | |
| | 282 | Ecrivez la boucle réalisant les transferts de cellules tant que le transfert |
| | 283 | d'une paire de cellules améliore la fonction de coût FC. |
| | 284 | |
| | 285 | == E.3. Exécution et évaluation == |
| | 286 | |
| | 287 | Exécutez cet algorithme sur différents graphes que vous définirez |
| | 288 | vous-mêmes. La partition résultante est-elle toujours la partition |
| | 289 | optimale? Pourquoi? Donnez un contre-exemple. |
| | 290 | |
| | 291 | == E.4. Ecriture des fonctions == |
| | 292 | |
| | 293 | Ecrire les différentes fonctions d'accès aux structures de données définies |
| | 294 | dans la partie D, et validez ces fonctions en les intégrant peu à peu dans |
| | 295 | votre programme. |
| | 296 | |
| | 297 | == E.5. construction du graphe en mémoire == |
| | 298 | |
| | 299 | L'objectif est ici d'écrire la fonction parse_graph(), en utilisant lex et |
| | 300 | yacc. Nous vous suggérons de supposer qu'il n'y a pas de clusters dans le |
| | 301 | fichier, puis de les introduire. Si vous n'avez pas le temps de construire |
| | 302 | le graphe, étudiez au moins la grammaire de ce petit langage. |