diff --git a/b_asic/resources.py b/b_asic/resources.py index 85d555fac220edbdf33c3c5f3a21321c2b7dfd28..5c5f426ee067614264981ac674bc271292e8a637 100644 --- a/b_asic/resources.py +++ b/b_asic/resources.py @@ -929,7 +929,9 @@ class ProcessCollection: The heuristic used when splitting this :class:`ProcessCollection`. Valid options are: - * "graph_color" + * "ilp_graph_color" + * "greedy_graph_color" + * "equitable_graph_color" * "left_edge" * "min_pe_to_mem" * "min_mem_to_pe" @@ -956,8 +958,18 @@ class ProcessCollection: read_ports, write_ports, total_ports = _sanitize_port_option( read_ports, write_ports, total_ports ) - if heuristic == "graph_color": - return self._split_ports_graph_color(read_ports, write_ports, total_ports) + if heuristic == "ilp_graph_color": + return self._split_ports_ilp_graph_color( + read_ports, write_ports, total_ports + ) + elif heuristic == "greedy_graph_color": + return self._split_ports_greedy_graph_color( + read_ports, write_ports, total_ports + ) + elif heuristic == "equitable_graph_color": + return self._split_ports_equitable_graph_color( + read_ports, write_ports, total_ports + ) elif heuristic == "left_edge": return self.split_ports_sequentially( read_ports, @@ -1255,7 +1267,7 @@ class ProcessCollection: break return count - def _split_ports_graph_color( + def _split_ports_greedy_graph_color( self, read_ports: int, write_ports: int, @@ -1286,15 +1298,103 @@ class ProcessCollection: * 'connected_sequential_dfs' or 'connected_sequential' * 'saturation_largest_first' or 'DSATUR' """ - # Create new exclusion graph. Nodes are Processes + # create new exclusion graph. Nodes are Processes exclusion_graph = self.create_exclusion_graph_from_ports( read_ports, write_ports, total_ports ) - # Perform assignment from coloring and return result + # perform assignment from coloring and return result coloring = nx.coloring.greedy_color(exclusion_graph, strategy=coloring_strategy) return self._split_from_graph_coloring(coloring) + def _split_ports_equitable_graph_color( + self, + read_ports: int, + write_ports: int, + total_ports: int, + ) -> list["ProcessCollection"]: + # create new exclusion graph. Nodes are Processes + exclusion_graph = self.create_exclusion_graph_from_ports( + read_ports, write_ports, total_ports + ) + + # perform assignment from coloring and return result + max_degree = max(dict(exclusion_graph.degree()).values()) + coloring = nx.coloring.equitable_color( + exclusion_graph, num_colors=max_degree + 1 + ) + return self._split_from_graph_coloring(coloring) + + def _split_ports_ilp_graph_color( + self, + read_ports: int, + write_ports: int, + total_ports: int, + ) -> list["ProcessCollection"]: + from pulp import ( + LpBinary, + LpProblem, + LpStatusOptimal, + LpVariable, + lpSum, + value, + ) + + # create new exclusion graph. Nodes are Processes + exclusion_graph = self.create_exclusion_graph_from_ports( + read_ports, write_ports, total_ports + ) + nodes = list(exclusion_graph.nodes()) + edges = list(exclusion_graph.edges()) + + # determine an upper bound on the number of colors + max_colors = len(nodes) + + # binary variables: + # x[node, color] - whether node is colored in a certain color + # c[color] - whether color is used + x = LpVariable.dicts("x", (nodes, range(max_colors)), cat=LpBinary) + c = LpVariable.dicts("c", range(max_colors), cat=LpBinary) + + # create the problem, objective function - minimize the number of colors used + problem = LpProblem() + problem += lpSum(c[i] for i in range(max_colors)) + + # constraints: + # 1 - nodes have exactly one color + # 2 - adjacent nodes cannot have the same color + # 3 - only permit assignments if color is used + for node in nodes: + problem += lpSum(x[node][i] for i in range(max_colors)) == 1 + for u, v in edges: + for color in range(max_colors): + problem += x[u][color] + x[v][color] <= c[color] + for node in nodes: + for color in range(max_colors): + problem += x[node][color] <= c[color] + + status = problem.solve() + + 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 range(max_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)} + minimal_coloring = { + key: coloring_mapping[node_colors[key]] for key in node_colors + } + + return self._split_from_graph_coloring(minimal_coloring) + def _split_from_graph_coloring( self, coloring: dict[Process, int], diff --git a/examples/ldlt_matrix_inverse.py b/examples/ldlt_matrix_inverse.py index 11a9684f71e2f0a1dfd93a116c4ba85f5d49c9e2..de7662772d6a4a2a7d0486aa48692c28a94b72d3 100644 --- a/examples/ldlt_matrix_inverse.py +++ b/examples/ldlt_matrix_inverse.py @@ -117,7 +117,7 @@ mem_vars.show(title="All memory variables") direct, mem_vars = mem_vars.split_on_length() mem_vars.show(title="Non-zero time memory variables") mem_vars_set = mem_vars.split_on_ports( - read_ports=1, write_ports=1, total_ports=2, heuristic="graph_color" + read_ports=1, write_ports=1, total_ports=2, heuristic="greedy_graph_color" ) # %% diff --git a/examples/memory_constrained_scheduling.py b/examples/memory_constrained_scheduling.py index 66cb3db1a254348c397e97fb0278473794505161..7ae0d015e34b893b4e94ee63ad665acd4b297ebd 100644 --- a/examples/memory_constrained_scheduling.py +++ b/examples/memory_constrained_scheduling.py @@ -64,7 +64,7 @@ mem_vars.show(title="All memory variables") direct, mem_vars = mem_vars.split_on_length() mem_vars.show(title="Non-zero time memory variables") mem_vars_set = mem_vars.split_on_ports( - read_ports=1, write_ports=1, total_ports=2, heuristic="graph_color" + read_ports=1, write_ports=1, total_ports=2, heuristic="greedy_graph_color" ) # %% @@ -121,7 +121,7 @@ pe_out = ProcessingElement(outputs, entity_name='output') mem_vars.show(title="Non-zero time memory variables") mem_vars_set = mem_vars.split_on_ports( - heuristic="graph_color", read_ports=1, write_ports=1, total_ports=2 + heuristic="greedy_graph_color", read_ports=1, write_ports=1, total_ports=2 ) # %% Allocate memories by graph coloring diff --git a/pyproject.toml b/pyproject.toml index 88282db0f0b68511b29d006c68f9adbac121e1c9..c664151ce2bdebff91076fbc7028dd68f5d592b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "matplotlib>=3.7", "setuptools_scm[toml]>=6.2", "networkx>=3", + "pulp", "qtawesome", ] classifiers = [ diff --git a/test/integration/test_sfg_to_architecture.py b/test/integration/test_sfg_to_architecture.py index 8266b7b9756688f54b900b1f325bc5b18ec4eb01..b530cd3c452087ea24747c22b343bee28aaafdee 100644 --- a/test/integration/test_sfg_to_architecture.py +++ b/test/integration/test_sfg_to_architecture.py @@ -126,11 +126,11 @@ def test_pe_and_memory_constrained_schedule(): mem_vars = schedule.get_memory_variables() direct, mem_vars = mem_vars.split_on_length() mem_vars_set = mem_vars.split_on_ports( - read_ports=1, write_ports=1, total_ports=2, heuristic="graph_color" + read_ports=1, write_ports=1, total_ports=2, heuristic="greedy_graph_color" ) mem_vars_set = mem_vars.split_on_ports( - read_ports=1, write_ports=1, total_ports=2, heuristic="graph_color" + read_ports=1, write_ports=1, total_ports=2, heuristic="greedy_graph_color" ) memories = [] @@ -268,12 +268,12 @@ def test_different_resource_algorithms(): assert len(arch.processing_elements) == 6 assert len(arch.memories) == 6 - # GRAPH COLORING + # GREEDY GRAPH COLORING mem_vars_set = mem_vars.split_on_ports( read_ports=1, write_ports=1, total_ports=2, - heuristic="graph_color", + heuristic="greedy_graph_color", processing_elements=processing_elements, ) @@ -290,3 +290,84 @@ def test_different_resource_algorithms(): ) assert len(arch.processing_elements) == 6 assert len(arch.memories) == 4 + + # EQUITABLE COLOR + mem_vars_set = mem_vars.split_on_ports( + read_ports=1, + write_ports=1, + total_ports=2, + heuristic="equitable_graph_color", + processing_elements=processing_elements, + ) + + memories = [] + 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") + + arch = Architecture( + processing_elements, + memories, + direct_interconnects=direct, + ) + assert len(arch.processing_elements) == 6 + assert len(arch.memories) == 7 + + # FOR ILP points is reduced due to time complexity + POINTS = 16 + sfg = radix_2_dif_fft(POINTS) + sfg.set_latency_of_type(Butterfly, 1) + sfg.set_latency_of_type(ConstantMultiplication, 3) + sfg.set_execution_time_of_type(Butterfly, 1) + sfg.set_execution_time_of_type(ConstantMultiplication, 1) + + schedule_2 = Schedule( + sfg, + scheduler=HybridScheduler( + resources, max_concurrent_reads=4, max_concurrent_writes=4 + ), + ) + + operations = schedule_2.get_operations() + bfs = operations.get_by_type_name(Butterfly.type_name()) + bfs = bfs.split_on_execution_time() + const_muls = operations.get_by_type_name(ConstantMultiplication.type_name()) + inputs = operations.get_by_type_name(Input.type_name()) + outputs = operations.get_by_type_name(Output.type_name()) + + bf_pe_1 = ProcessingElement(bfs[0], entity_name="bf1") + bf_pe_2 = ProcessingElement(bfs[1], entity_name="bf2") + + mul_pe_1 = ProcessingElement(const_muls, entity_name="mul1") + + pe_in = ProcessingElement(inputs, entity_name="input") + pe_out = ProcessingElement(outputs, entity_name="output") + + processing_elements = [bf_pe_1, bf_pe_2, mul_pe_1, pe_in, pe_out] + + mem_vars = schedule_2.get_memory_variables() + direct, mem_vars = mem_vars.split_on_length() + + # ILP COLOR + mem_vars_set = mem_vars.split_on_ports( + read_ports=1, + write_ports=1, + total_ports=2, + heuristic="ilp_graph_color", + processing_elements=processing_elements, + ) + + memories = [] + 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") + + arch = Architecture( + processing_elements, + memories, + direct_interconnects=direct, + ) + assert len(arch.processing_elements) == 5 + assert len(arch.memories) == 4 diff --git a/test/unit/test_resources.py b/test/unit/test_resources.py index e6c01c83f5fb509cb88f5c441ad327052bdf9070..c54cca949cf90bbe23fcac255ec9e900f9a79bfd 100644 --- a/test/unit/test_resources.py +++ b/test/unit/test_resources.py @@ -30,11 +30,11 @@ class TestProcessCollectionPlainMemoryVariable: generate_matrix_transposer(4).plot(ax=ax) # type: ignore return fig - def test_split_memory_variable_graph_color( + def test_split_memory_variable_greedy_graph_color( self, simple_collection: ProcessCollection ): collection_split = simple_collection.split_on_ports( - heuristic="graph_color", read_ports=1, write_ports=1, total_ports=2 + heuristic="greedy_graph_color", read_ports=1, write_ports=1, total_ports=2 ) assert len(collection_split) == 3