diff --git a/b_asic/resources.py b/b_asic/resources.py index 160fd8d9aa1360270953eeb3489d854ebfab5061..d8d2615de244218f8e4c743d55388f9ae2c6c241 100644 --- a/b_asic/resources.py +++ b/b_asic/resources.py @@ -99,6 +99,30 @@ def _sanitize_port_option( return read_ports, write_ports, total_ports +def _get_source( + var: MemoryVariable, pes: list["ProcessingElement"] +) -> "ProcessingElement": + name = var.name.split(".")[0] + for pe in pes: + pe_names = [proc.name for proc in pe.collection] + if name in pe_names: + return pe + raise ValueError("Source could not be found for the given variable.") + + +def _get_destination( + var: MemoryVariable, pes: list["ProcessingElement"] +) -> "ProcessingElement": + name = var.name.split(".")[0] + for pe in pes: + for process in pe.processes: + for input in process.operation.inputs: + input_op = input.connected_source.operation + if input_op.graph_id == name: + return pe + raise ValueError("Destination could not be found for the given variable.") + + def draw_exclusion_graph_coloring( exclusion_graph: nx.Graph, color_dict: dict[Process, int], @@ -984,6 +1008,38 @@ class ProcessCollection: processing_elements, amount_of_sets, ) + elif heuristic == "ilp_min_output_mux": + if processing_elements is None: + raise ValueError( + "processing_elements must be provided if heuristic = 'ilp_min_output_mux'" + ) + if amount_of_sets is None: + raise ValueError( + "amount_of_sets must be provided if heuristic = 'ilp_min_output_mux'" + ) + return self._split_ports_ilp_min_output_mux_graph_color( + read_ports, + write_ports, + total_ports, + processing_elements, + amount_of_sets, + ) + elif heuristic == "ilp_min_total_mux": + if processing_elements is None: + raise ValueError( + "processing_elements must be provided if heuristic = 'ilp_min_total_mux'" + ) + if amount_of_sets is None: + raise ValueError( + "amount_of_sets must be provided if heuristic = 'ilp_min_total_mux'" + ) + return self._split_ports_ilp_min_total_mux_graph_color( + read_ports, + write_ports, + total_ports, + processing_elements, + amount_of_sets, + ) elif heuristic == "greedy_graph_color": return self._split_ports_greedy_graph_color( read_ports, write_ports, total_ports @@ -1455,28 +1511,180 @@ class ProcessCollection: # x[node, color] - whether node is colored in a certain color # c[color] - whether color is used # y[pe, color] - whether a color has nodes generated from a certain pe + x = LpVariable.dicts("x", (nodes, colors), cat=LpBinary) + c = LpVariable.dicts("c", colors, cat=LpBinary) + y = LpVariable.dicts("y", (processing_elements, colors), cat=LpBinary) + problem = LpProblem() + problem += lpSum(y[pe][i] for pe in processing_elements 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 - if node is colored then enable the PE which generates that node (variable) + 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] + for node in nodes: + pe = _get_source(node, processing_elements) + for color in colors: + problem += x[node][color] <= y[pe][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 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_ports_ilp_min_output_mux_graph_color( + self, + read_ports: int, + write_ports: int, + total_ports: int, + processing_elements: list["ProcessingElement"], + amount_of_colors: 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()) + colors = range(amount_of_colors) + + # minimize the amount of output muxes connecting PEs to memories + # by minimizing the amount of PEs connected to each memory + + # binary variables: + # x[node, color] - whether node is colored in a certain color + # c[color] - whether color is used + # y[pe, color] - whether a color has nodes writing to a certain PE x = LpVariable.dicts("x", (nodes, colors), cat=LpBinary) c = LpVariable.dicts("c", colors, cat=LpBinary) y = LpVariable.dicts("y", (processing_elements, colors), cat=LpBinary) problem = LpProblem() problem += lpSum(y[pe][i] for pe in processing_elements for i in colors) - def _get_source( - var: MemoryVariable, pes: list["ProcessingElement"] - ) -> "ProcessingElement": - name = var.name.split(".")[0] - for pe in pes: - pe_names = [proc.name for proc in pe.collection] - if name in pe_names: - return pe - raise ValueError("Source could not be found for the given variable.") + # constraints: + # 1 - nodes have exactly one color + # 2 - adjacent nodes cannot have the same color + # 3 - only permit assignments if color is used + # 4 - if node is colored then enable the PE reads from that node (variable) + 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] + for node in nodes: + pe = _get_destination(node, processing_elements) + for color in colors: + problem += x[node][color] <= y[pe][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 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_ports_ilp_min_total_mux_graph_color( + self, + read_ports: int, + write_ports: int, + total_ports: int, + processing_elements: list["ProcessingElement"], + amount_of_colors: 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()) + + colors = range(amount_of_colors) + + # minimize the amount of total muxes connecting PEs to memories + # by minimizing the amount of PEs connected to each memory (input & output) + + # binary variables: + # x[node, color] - whether node is colored in a certain color + # c[color] - whether color is used + # y[pe, color] - whether a color has nodes generated from a certain pe + # z[pe, color] - whether a color has nodes writing to a certain PE + x = LpVariable.dicts("x", (nodes, colors), cat=LpBinary) + c = LpVariable.dicts("c", colors, cat=LpBinary) + y = LpVariable.dicts("y", (processing_elements, colors), cat=LpBinary) + z = LpVariable.dicts("z", (processing_elements, colors), cat=LpBinary) + problem = LpProblem() + problem += lpSum( + y[pe][i] + z[pe][i] for pe in processing_elements 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 - if node is colored then enable the PE which generates that node (variable) + # 5 - if node is colored then enable the PE reads from that node (variable) for node in nodes: problem += lpSum(x[node][i] for i in colors) == 1 for u, v in edges: @@ -1489,6 +1697,10 @@ class ProcessCollection: pe = _get_source(node, processing_elements) for color in colors: problem += x[node][color] <= y[pe][color] + for node in nodes: + pe = _get_destination(node, processing_elements) + for color in colors: + problem += x[node][color] <= z[pe][color] status = problem.solve() @@ -1571,7 +1783,7 @@ class ProcessCollection: coloring: dict[Process, int] | None = None, ) -> list["ProcessCollection"]: """ - Perform assignment of the processes in this collection using graph coloring. + Perform assignment of the processes in this collection using greedy graph coloring. Two or more processes can share a single resource if, and only if, they have no overlapping execution time. diff --git a/test/integration/test_sfg_to_architecture.py b/test/integration/test_sfg_to_architecture.py index 2dbc6602bc9bd20f48b6d7d2172747ef1b57aecf..4caa3211d7d646f463d2cc774ec1b490ab2c3cec 100644 --- a/test/integration/test_sfg_to_architecture.py +++ b/test/integration/test_sfg_to_architecture.py @@ -395,3 +395,51 @@ def test_different_resource_algorithms(): ) assert len(arch.processing_elements) == 5 assert len(arch.memories) == 4 + + # ILP COLOR MIN OUTPUT MUX + mem_vars_set = mem_vars.split_on_ports( + read_ports=1, + write_ports=1, + total_ports=2, + heuristic="ilp_min_output_mux", + processing_elements=processing_elements, + amount_of_sets=4, + ) + + 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 + + # ILP COLOR MIN TOTAL MUX + mem_vars_set = mem_vars.split_on_ports( + read_ports=1, + write_ports=1, + total_ports=2, + heuristic="ilp_min_total_mux", + processing_elements=processing_elements, + amount_of_sets=4, + ) + + 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 ac8bcc5383aa7acfc3cfbc4d11dc4fb976790788..990f506ce4ddf1e88c4aaa17f6b9837430369929 100644 --- a/test/unit/test_resources.py +++ b/test/unit/test_resources.py @@ -90,6 +90,42 @@ class TestProcessCollectionPlainMemoryVariable: processing_elements=[], ) + with pytest.raises( + ValueError, + match="processing_elements must be provided if heuristic = 'ilp_min_output_mux'", + ): + simple_collection.split_on_ports( + heuristic="ilp_min_output_mux", total_ports=1 + ) + + with pytest.raises( + ValueError, + match="amount_of_sets must be provided if heuristic = 'ilp_min_output_mux'", + ): + simple_collection.split_on_ports( + heuristic="ilp_min_output_mux", + total_ports=1, + processing_elements=[], + ) + + with pytest.raises( + ValueError, + match="processing_elements must be provided if heuristic = 'ilp_min_total_mux'", + ): + simple_collection.split_on_ports( + heuristic="ilp_min_total_mux", total_ports=1 + ) + + with pytest.raises( + ValueError, + match="amount_of_sets must be provided if heuristic = 'ilp_min_total_mux'", + ): + simple_collection.split_on_ports( + heuristic="ilp_min_total_mux", + total_ports=1, + processing_elements=[], + ) + with pytest.raises( ValueError, match="processing_elements must be provided if heuristic = 'left_edge_min_pe_to_mem'",