From af76e3c42270591749067edc51d78cb70056a263 Mon Sep 17 00:00:00 2001 From: Simon Bjurek <simbj106@student.liu.se> Date: Tue, 15 Apr 2025 13:05:45 +0200 Subject: [PATCH] Add ILP split_on_execution_time method --- b_asic/architecture.py | 10 +- b_asic/resources.py | 116 +++++++++++++++++-- examples/memory_constrained_scheduling.py | 4 +- test/integration/test_sfg_to_architecture.py | 26 ++--- test/unit/test_list_schedulers.py | 4 +- test/unit/test_resources.py | 12 +- 6 files changed, 141 insertions(+), 31 deletions(-) diff --git a/b_asic/architecture.py b/b_asic/architecture.py index 7f0722a4..9ceaacc7 100644 --- a/b_asic/architecture.py +++ b/b_asic/architecture.py @@ -561,7 +561,12 @@ class Memory(Resource): pass return "" - def assign(self, strategy: str = "left_edge") -> None: + def assign( + self, + strategy: Literal[ + "left_edge", "greedy_graph_color", "ilp_graph_color" + ] = "left_edge", + ) -> None: """ Perform assignment of the memory variables. @@ -573,7 +578,8 @@ class Memory(Resource): * 'RAM' * 'left_edge': Left-edge algorithm. - * 'graph_color': Graph-coloring based on exclusion graph. + * 'greedy_graph_color': Greedy graph-coloring based on exclusion graph. + * 'ilp_graph_color': Optimal graph-coloring based on exclusion graph. * 'register' * ... """ diff --git a/b_asic/resources.py b/b_asic/resources.py index e09af3ad..95b42d82 100644 --- a/b_asic/resources.py +++ b/b_asic/resources.py @@ -913,21 +913,27 @@ class ProcessCollection: def split_on_execution_time( self, - strategy: Literal["graph_color", "left_edge"] = "left_edge", + strategy: Literal[ + "left_edge", + "greedy_graph_color", + "ilp_graph_color", + ] = "left_edge", coloring_strategy: str = "saturation_largest_first", + max_colors: int | None = None, + solver: PULP_CBC_CMD | GUROBI | None = None, ) -> list["ProcessCollection"]: """ Split based on overlapping execution time. Parameters ---------- - strategy : {'graph_color', 'left_edge'}, default: 'left_edge' + strategy : {'ilp_graph_color', 'greedy_graph_color', 'left_edge'}, default: 'left_edge' The strategy used when splitting based on execution times. coloring_strategy : str, default: 'saturation_largest_first' Node ordering strategy passed to :func:`networkx.algorithms.coloring.greedy_color`. - This parameter is only considered if *strategy* is set to 'graph_color'. + This parameter is only considered if *strategy* is set to 'greedy_graph_color'. One of * 'largest_first' @@ -938,12 +944,24 @@ class ProcessCollection: * 'connected_sequential_dfs' or 'connected_sequential' * 'saturation_largest_first' or 'DSATUR' + max_colors : int, optional + The maximum amount of colors to split based on, + only required if strategy is an ILP method. + + solver : PuLP MIP solver object, optional + Only used if strategy is an ILP method. + Valid options are: + * PULP_CBC_CMD() - preinstalled with the package + * GUROBI() - required licence but likely faster + Returns ------- A list of new ProcessCollection objects with the process splitting. """ - if strategy == "graph_color": - return self._graph_color_assignment(coloring_strategy) + if strategy == "ilp_graph_color": + return self._ilp_graph_color_assignment(max_colors, solver) + elif strategy == "greedy_graph_color": + return self._greedy_graph_color_assignment(coloring_strategy) elif strategy == "left_edge": return self._left_edge_assignment() else: @@ -1841,7 +1859,92 @@ class ProcessCollection: def __iter__(self): return iter(self._collection) - def _graph_color_assignment( + def _ilp_graph_color_assignment( + self, + max_colors: int | None = None, + solver: PULP_CBC_CMD | GUROBI | None = None, + ) -> list["ProcessCollection"]: + for process in self: + if process.execution_time > self.schedule_time: + raise ValueError( + f"{process} has execution time greater than the schedule time" + ) + + cell_assignment: dict[int, ProcessCollection] = {} + exclusion_graph = self.create_exclusion_graph_from_execution_time() + + nodes = list(exclusion_graph.nodes()) + edges = list(exclusion_graph.edges()) + + if max_colors is None: + # get an initial estimate using NetworkX greedy graph coloring + coloring = nx.coloring.greedy_color( + exclusion_graph, strategy="saturation_largest_first" + ) + max_colors = len(set(coloring.values())) + colors = range(max_colors) + + # find the minimal amount of colors (memories) + + # binary variables: + # x[node, color] - whether node is colored in a certain color + # c[color] - whether color is used + x = LpVariable.dicts("x", (nodes, colors), cat=LpBinary) + c = LpVariable.dicts("c", colors, cat=LpBinary) + problem = LpProblem() + problem += lpSum(c[i] for i in colors) + + # constraints: + # (1) - nodes have exactly one color + # (2) - adjacent nodes cannot have the same color + # (3) - only permit assignments if color is used + # (4) - reduce solution space by assigning colors to the largest clique + # (5 & 6) - reduce solution space by ignoring the symmetry caused + # by cycling the graph colors + for node in nodes: + problem += lpSum(x[node][i] for i in colors) == 1 + for u, v in edges: + for color in colors: + problem += x[u][color] + x[v][color] <= 1 + for node in nodes: + for color in colors: + problem += x[node][color] <= c[color] + max_clique = next(nx.find_cliques(exclusion_graph)) + for color, node in enumerate(max_clique): + problem += x[node][color] == c[color] == 1 + for color in colors: + problem += c[color] <= lpSum(x[node][color] for node in nodes) + for color in colors[:-1]: + problem += c[color + 1] <= c[color] + + if solver is None: + solver = PULP_CBC_CMD() + + status = problem.solve(solver) + + if status != LpStatusOptimal: + raise ValueError( + "Optimal solution could not be found via ILP, use another method." + ) + + node_colors = {} + for node in nodes: + for i in colors: + if value(x[node][i]) == 1: + node_colors[node] = i + + # reduce the solution by removing unused colors + sorted_unique_values = sorted(set(node_colors.values())) + coloring_mapping = {val: i for i, val in enumerate(sorted_unique_values)} + coloring = {key: coloring_mapping[node_colors[key]] for key in node_colors} + + for process, cell in coloring.items(): + if cell not in cell_assignment: + cell_assignment[cell] = ProcessCollection([], self._schedule_time) + cell_assignment[cell].add_process(process) + return list(cell_assignment.values()) + + def _greedy_graph_color_assignment( self, coloring_strategy: str = "saturation_largest_first", *, @@ -1869,7 +1972,6 @@ class ProcessCollection: """ for process in self: if process.execution_time > self.schedule_time: - # Can not assign process to any cell raise ValueError( f"{process} has execution time greater than the schedule time" ) diff --git a/examples/memory_constrained_scheduling.py b/examples/memory_constrained_scheduling.py index fe87b123..d7fc95e8 100644 --- a/examples/memory_constrained_scheduling.py +++ b/examples/memory_constrained_scheduling.py @@ -73,7 +73,7 @@ for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) mem.show(title=f"{memory.entity_name}") - memory.assign("graph_color") + memory.assign("greedy_graph_color") memory.show_content(title=f"Assigned {memory.entity_name}") direct.show(title="Direct interconnects") @@ -130,7 +130,7 @@ for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) mem.show(title=f"{memory.entity_name}") - memory.assign("graph_color") + memory.assign("greedy_graph_color") memory.show_content(title=f"Assigned {memory.entity_name}") direct.show(title="Direct interconnects") diff --git a/test/integration/test_sfg_to_architecture.py b/test/integration/test_sfg_to_architecture.py index 7ac22550..821cdcfd 100644 --- a/test/integration/test_sfg_to_architecture.py +++ b/test/integration/test_sfg_to_architecture.py @@ -72,7 +72,7 @@ def test_pe_constrained_schedule(): # for i, mem in enumerate(mem_vars_set): # memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") # memories.append(memory) - # memory.assign("graph_color") + # memory.assign("greedy_graph_color") # arch = Architecture( # {mads0, mads1, reciprocal_pe, pe_in, pe_out}, @@ -137,7 +137,7 @@ def test_pe_and_memory_constrained_schedule(): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("greedy_graph_color") arch = Architecture( {bf_pe, mul_pe, pe_in, pe_out}, @@ -172,7 +172,7 @@ def test_left_edge(mem_variables_fft32): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("greedy_graph_color") arch = Architecture( processing_elements, @@ -199,7 +199,7 @@ def test_min_pe_to_mem(mem_variables_fft32): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("greedy_graph_color") arch = Architecture( processing_elements, @@ -226,7 +226,7 @@ def test_min_mem_to_pe(mem_variables_fft32): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("greedy_graph_color") arch = Architecture( processing_elements, @@ -253,7 +253,7 @@ def test_greedy_graph_coloring(mem_variables_fft32): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("greedy_graph_color") arch = Architecture( processing_elements, @@ -280,7 +280,7 @@ def test_equitable_color(mem_variables_fft32): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("greedy_graph_color") arch = Architecture( processing_elements, @@ -306,7 +306,7 @@ def test_ilp_color(mem_variables_fft16): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("ilp_graph_color") arch = Architecture( processing_elements, @@ -333,7 +333,7 @@ def test_ilp_color_with_colors_given(mem_variables_fft16): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("ilp_graph_color") arch = Architecture( processing_elements, @@ -360,7 +360,7 @@ def test_ilp_color_input_mux(mem_variables_fft16): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("ilp_graph_color") arch = Architecture( processing_elements, @@ -387,7 +387,7 @@ def test_ilp_color_output_mux(mem_variables_fft16): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("ilp_graph_color") arch = Architecture( processing_elements, @@ -415,7 +415,7 @@ def test_ilp_color_total_mux(mem_variables_fft16): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("ilp_graph_color") arch = Architecture( processing_elements, @@ -480,7 +480,7 @@ def test_ilp_resource_algorithm_custom_solver(): for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("ilp_graph_color") arch = Architecture( processing_elements, diff --git a/test/unit/test_list_schedulers.py b/test/unit/test_list_schedulers.py index e36a3152..8a0a5f01 100644 --- a/test/unit/test_list_schedulers.py +++ b/test/unit/test_list_schedulers.py @@ -1857,7 +1857,7 @@ class TestListScheduler: for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("greedy_graph_color") arch = Architecture( processing_elements, @@ -1939,7 +1939,7 @@ class TestListScheduler: for i, mem in enumerate(mem_vars_set): memory = Memory(mem, memory_type="RAM", entity_name=f"memory{i}") memories.append(memory) - memory.assign("graph_color") + memory.assign("greedy_graph_color") arch = Architecture( processing_elements, diff --git a/test/unit/test_resources.py b/test/unit/test_resources.py index e7a93ba9..e64f4cea 100644 --- a/test/unit/test_resources.py +++ b/test/unit/test_resources.py @@ -160,7 +160,7 @@ class TestProcessCollectionPlainMemoryVariable: collection = generate_matrix_transposer(4, min_lifetime=5) assignment_left_edge = collection._left_edge_assignment() assignment_graph_color = collection.split_on_execution_time( - strategy="graph_color", coloring_strategy="saturation_largest_first" + strategy="greedy_graph_color", coloring_strategy="saturation_largest_first" ) assert len(assignment_left_edge) == 18 assert len(assignment_graph_color) == 16 @@ -182,7 +182,9 @@ class TestProcessCollectionPlainMemoryVariable: collection = generate_matrix_transposer( rows=rows, cols=cols, min_lifetime=0 ) - assignment = collection.split_on_execution_time(strategy="graph_color") + assignment = collection.split_on_execution_time( + strategy="greedy_graph_color" + ) collection.generate_memory_based_storage_vhdl( filename=( "b_asic/codegen/testbench/" @@ -334,12 +336,12 @@ class TestProcessCollectionPlainMemoryVariable: assert exclusion_graph.degree(p2) == 1 assert exclusion_graph.degree(p3) == 3 - def test_left_edge_maximum_lifetime(self): + def test_split_on_execution_time_maximum_lifetime(self): a = PlainMemoryVariable(2, 0, {0: 1}, "cmul1.0") b = PlainMemoryVariable(4, 0, {0: 7}, "cmul4.0") c = PlainMemoryVariable(5, 0, {0: 4}, "cmul5.0") collection = ProcessCollection([a, b, c], schedule_time=7, cyclic=True) - for strategy in ("graph_color", "left_edge"): + for strategy in ("greedy_graph_color", "left_edge", "ilp_graph_color"): assignment = collection.split_on_execution_time(strategy) assert len(assignment) == 2 a_idx = 0 if a in assignment[0] else 1 @@ -349,7 +351,7 @@ class TestProcessCollectionPlainMemoryVariable: def test_split_on_execution_lifetime_assert(self): a = PlainMemoryVariable(3, 0, {0: 10}, "MV0") collection = ProcessCollection([a], schedule_time=9, cyclic=True) - for strategy in ("graph_color", "left_edge"): + for strategy in ("greedy_graph_color", "left_edge", "ilp_graph_color"): with pytest.raises( ValueError, match="MV0 has execution time greater than the schedule time", -- GitLab