forked from TILOS-AI-Institute/MacroPlacement
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcoordinate_descent_placer.py
More file actions
346 lines (299 loc) · 13.5 KB
/
Copy pathcoordinate_descent_placer.py
File metadata and controls
346 lines (299 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
# coding=utf-8
# Copyright 2021 The Circuit Training Team Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Coordinate descent placer library."""
import os
import time
from typing import Callable, Dict, Optional, List, Text, Tuple
from absl import logging
from Plc_client import placement_util_os as placement_util
from Plc_client import plc_client as plc_client
import numpy as np
NS_ORIENTATIONS = ['N', 'FN', 'S', 'FS']
EW_ORIENTATIONS = ['E', 'FE', 'W', 'FW']
class CoordinateDescentPlacer(object):
"""Coordinate descent algorithm to place nodes."""
def __init__(self,
plc: plc_client.PlacementCost,
cost_fn: Callable[[plc_client.PlacementCost],
Tuple[float, Dict[Text, float]]],
epochs: int = 10,
use_stdcell_placer: bool = False,
stdcell_placer: Text = 'fd',
node_order: Text = 'random',
accept_bad_stdcell_moves: bool = False,
stdcell_place_every_n_macros: int = 10,
optimize_only_orientation: bool = False,
cell_search_prob: float = 1.0,
k_distance_bounded_search: bool = True,
k_distance_bound: Optional[int] = None) -> None:
"""Creates a CoordinateDescentPlacer.
Args:
plc: The placement cost object.
cost_fn: The cost function that gets the plc and returns cost and info.
epochs: Number of epochs (iterations) in coordinate descend algorithm.
use_stdcell_placer: If True, places stdcells using stdcell placer.
stdcell_placer: Standad cell placer.
node_order: Order of nodes to place using coordinate descent. Choose
random, descending_size_macro_first, random_macro_first.
accept_bad_stdcell_moves: If True, accept stdcell moves even if it leads
to a higher cost.
stdcell_place_every_n_macros: Run stdcell placement for every n macros. If
None, run stdcell placement once after all macros are placed.
optimize_only_orientation: If True, only search for best orientation of
the hard macros.
cell_search_prob: The probability to include a neighborhood cell to
search. When it is 1.0, descents at the steepest direction.'
k_distance_bounded_search: If True, only search best locations within k
grid distance from current placed location. Does not apply to FD stdcell
placer.
k_distance_bound: If k_distance_bounded_search is True, only search within
a neighborhood of at most k_distance_bound grid distance. If not
spesified, it is set to max(cols, rows) // 3.
"""
self.plc = plc
self.cost_fn = cost_fn
self._epochs = epochs
self._node_order = node_order
self._stdcell_place_every_n_macros = stdcell_place_every_n_macros
self._cell_search_prob = cell_search_prob
self._cols, self._rows = self.plc.get_grid_num_columns_rows()
self._k_distance_bound = k_distance_bound or max(self._cols,
self._rows) // 3
self._use_stdcell_placer = use_stdcell_placer
self._stdcell_placer = stdcell_placer
self._accept_bad_stdcell_moves = accept_bad_stdcell_moves
self._optimize_only_orientation = optimize_only_orientation
self._k_distance_bounded_search = k_distance_bounded_search
if self._cell_search_prob < 0 or self._cell_search_prob > 1:
raise ValueError(f'{self._cell_search_prob} should be between 0 and 1.')
# Turn off incremental cost calculation if placing stdcells.
if self._use_stdcell_placer:
plc.set_use_incremental_cost(False)
# Get legal node orientations.
self._node_to_ori = {}
for node in self.plc.get_macro_indices():
if not self.plc.is_node_soft_macro(node):
# TODO(wenjiej): Find orientation when a node is not placed initially.
# Needed only when running CD from an empty grid.
assert self.plc.is_node_placed(node)
cur_ori = self.plc.get_macro_orientation(node)
if cur_ori in NS_ORIENTATIONS:
self._node_to_ori[node] = NS_ORIENTATIONS
elif cur_ori in EW_ORIENTATIONS:
self._node_to_ori[node] = EW_ORIENTATIONS
else:
raise ValueError(f'Unexpected orientation {cur_ori} for node {node}.')
if self._use_stdcell_placer:
plc.allow_hard_macros_over_std_cells(True)
# If node order is random, will shuffle node orders for each iteration.
self._ordered_node_indices = placement_util.get_ordered_node_indices(
self._node_order, self.plc)
# Exclude fixed macros with pre-determined locations.
self._ordered_node_indices = [
m for m in self._ordered_node_indices if not self.plc.is_node_fixed(m)
]
self._soft_macro_indices = [
m for m in self._ordered_node_indices if self.plc.is_node_soft_macro(m)
]
if self._use_stdcell_placer:
# Only include hard macros in self._ordered_node_indices.
self._ordered_node_indices = [
i for i in self._ordered_node_indices
if not self.plc.is_node_soft_macro(i)
]
logging.info('Total number of ordered nodes: %d',
len(self._ordered_node_indices))
logging.info('ordered_node_indices: %s', self._ordered_node_indices)
logging.info('Cost of initial placement: %s', self.report_cost())
def find_best_location(self, node: int, mask: List[int],
locations: List[int]) -> Optional[int]:
"""Given a soft macro, search the best location."""
best_loc = None
best_cost = float('inf')
for loc in locations:
assert mask[loc] == 1
self.plc.place_node(node, loc)
new_cost, _ = self.cost_fn(self.plc)
self.plc.unplace_node(node)
if new_cost < best_cost:
best_loc = loc
best_cost = new_cost
return best_loc
def find_best_location_orientation(
self, node: int, locations: List[int],
orientations: List[Text]) -> Tuple[Optional[int], Optional[Text]]:
"""Given a hard macro, search the best location and orientation."""
assert orientations
best_loc = None
best_ori = None
best_cost = float('inf')
for loc in locations:
for ori in orientations:
self.plc.place_node(node, loc)
self.plc.update_macro_orientation(node, ori)
new_cost, _ = self.cost_fn(self.plc)
self.plc.unplace_node(node)
if new_cost < best_cost:
best_loc = loc
best_ori = ori
best_cost = new_cost
return best_loc, best_ori
def find_best_orientation(self, node: int,
orientations: List[Text]) -> Optional[Text]:
"""Given a hard macro, search the best orientation."""
assert orientations
best_ori = None
best_cost = float('inf')
for ori in orientations:
self.plc.update_macro_orientation(node, ori)
new_cost, _ = self.cost_fn(self.plc)
if new_cost < best_cost:
best_ori = ori
best_cost = new_cost
return best_ori
def _get_row_col_from_cell(self, cell: int) -> Tuple[int, int]:
return cell // self._cols, cell % self._cols
def _get_cell_from_row_col(self, row: int, col: int) -> int:
return int(row * self._cols + col)
def _k_distance_bounded_locations(self, curr: int, k: int,
locations: List[int]) -> List[int]:
"""Find k grid distance bounded locations from current cell."""
curr_row, curr_col = self._get_row_col_from_cell(curr)
bounded = []
for c in locations:
if c == curr:
# Always include current location to search.
bounded.append(c)
continue
row, col = self._get_row_col_from_cell(c)
if abs(row - curr_row) + abs(col - curr_col) <= k:
if np.random.random() <= self._cell_search_prob:
bounded.append(c)
return bounded
def place_node(self, node: int) -> None:
"""Given a node, greedily place the node on the best location wrt cost."""
if not self.plc.is_node_soft_macro(node):
orientations = self._node_to_ori[node]
if self._optimize_only_orientation:
# Placing and unplacing macros cause wiered problems in FD.
# See cl/316830807. Avoid unplacing for orientation optimization.
best_ori = self.find_best_orientation(node, orientations)
self.plc.update_macro_orientation(node, best_ori)
return
# Unplace the node from its current location to prepare placing node.
curr_cell = self.plc.get_grid_cell_of_node(node)
self.plc.unplace_node(node)
mask = self.plc.get_node_mask(node)
locations = [i for i, m in enumerate(mask) if m > 0]
if not locations:
# FD or DP are run between macro moves (_stdcell_place_every_n_macros).
# They may place stdcells in a way that invalidates prior macro locations.
# Stay with previous macro locations in this case.
locations = [curr_cell]
logging.info(
'Cannot find feasible locations for node %d. '
'Use its current location %d.', node, curr_cell)
if self._k_distance_bounded_search:
k = self._k_distance_bound
# Increase search scope until there is at least one feasible location.
while True:
bounded = self._k_distance_bounded_locations(curr_cell, k, locations)
if bounded:
locations = bounded
break
else:
k += self._k_distance_bound
if self.plc.is_node_soft_macro(node):
best_loc = self.find_best_location(node, mask, locations)
self.plc.place_node(node, best_loc)
else:
best_loc, best_ori = self.find_best_location_orientation(
node, locations, orientations)
self.plc.place_node(node, best_loc)
self.plc.update_macro_orientation(node, best_ori)
def place_stdcells(self) -> None:
"""Place stdcells."""
logging.info('Place stdcells using %s', self._stdcell_placer)
old_cost, _ = self.cost_fn(self.plc)
old_coordinates = [
self.plc.get_node_location(m) for m in self._soft_macro_indices
]
if self._stdcell_placer == 'fd':
# Use default FD schedule.
# Use current stdcell location to incrementally change stdcell locations
# between iterations.
placement_util.fd_placement_schedule(self.plc, use_current_loc=True)
else:
raise ValueError(
f'stdcell placer {self._stdcell_placer} is not supported')
new_cost, _ = self.cost_fn(self.plc)
if new_cost > old_cost and not self._accept_bad_stdcell_moves:
logging.info('Bad stdcell placement moves not accepted.')
# Revert to old node coordinates.
for i, (x, y) in enumerate(old_coordinates):
self.plc.update_node_coords(self._soft_macro_indices[i], x, y)
def optimize(self, epoch: int) -> None:
"""Performs one iteration (epoch) of coordinate descent on all nodes."""
logging.info('Starts optimization in epoch %d.', epoch)
start_time = time.time()
node_indices = self._ordered_node_indices
if self._node_order == 'random':
np.random.shuffle(node_indices)
for i, node in enumerate(node_indices):
if i % 25 == 0:
logging.info('Number of nodes placed by CD: %d', i)
self.place_node(node)
if (self._use_stdcell_placer and self._stdcell_place_every_n_macros and
(i + 1) % self._stdcell_place_every_n_macros == 0):
self.place_stdcells()
# Always run stdcell placement after all macros are placed.
if self._use_stdcell_placer:
self.place_stdcells()
logging.info('One iteration of coordinate descent takes %f seconds.',
(time.time() - start_time))
def report_cost(self) -> Text:
proxy_cost, info = self.cost_fn(self.plc)
wirelength = info['wirelength']
congestion = info['congestion']
density = info['density']
return ('(Objective cost, wirelength, congestion, density): ' +
'({:.4f}, {:.4f}, {:.4f}, {:.4f}'.format(proxy_cost, wirelength,
congestion, density))
def place(self) -> None:
"""Place all nodes using coordinate descent algorithm for some iterations."""
# Run stdcell placement at the beginning of the optimization loop if needed.
# Use stdcell locations from initial placement.
if self._use_stdcell_placer:
self.place_stdcells()
prev_cost, _ = self.cost_fn(self.plc)
for i in range(self._epochs):
self.optimize(i)
logging.info('Cost after %d epochs: %s', i + 1, self.report_cost())
curr_cost, _ = self.cost_fn(self.plc)
if (prev_cost - curr_cost) / prev_cost < 1e-3:
break
prev_cost = curr_cost
def save_placement(self, output_dir: Text, plc_filename: Text) -> None:
"""Saves a placement with current plc."""
proxy_cost, info = self.cost_fn(self.plc)
wirelength = info['wirelength']
congestion = info['congestion']
density = info['density']
plc_filename_with_cost = 'cost_{:.4f}_w_{:.4f}_c_{:.4f}_d_{:.4f}_{}'.format(
proxy_cost, wirelength, congestion, density, plc_filename)
output_plc_file = os.path.join(output_dir, plc_filename_with_cost)
placement_util.save_placement(self.plc, output_plc_file)
# TODO(wenjiej): Enable saving plc view.
# placement_util.save_as_svg(self.plc, f'{output_plc_file}.svg')