| 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. |