diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 0fc22917663047498659d997eb0130138973f199..67ec8f9b9a0b7046da6c1f20589270d06b26b621 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -29,7 +29,7 @@ from b_asic._preferences import ( SIGNAL_LINEWIDTH, SPLINE_OFFSET, ) -from b_asic.core_operations import DontCare +from b_asic.core_operations import DontCare, Sink from b_asic.graph_component import GraphID from b_asic.operation import Operation from b_asic.port import InputPort, OutputPort @@ -187,7 +187,9 @@ class Schedule: f"Negative backward slack detected in Schedule for operation: {graph_id}, " f"slack: {self.backward_slack(graph_id)}" ) - if time > self._schedule_time and not graph_id.startswith("dontcare"): + if time > self._schedule_time and not isinstance( + self._sfg.find_by_id(graph_id), DontCare + ): raise ValueError( f"Start time larger than scheduling time detected in Schedule for operation {graph_id}" ) @@ -210,7 +212,7 @@ class Schedule: max_end_time = 0 for graph_id, op_start_time in self._start_times.items(): operation = cast(Operation, self._sfg.find_by_id(graph_id)) - if graph_id.startswith("out"): + if isinstance(operation, Output): max_end_time = max(max_end_time, op_start_time) else: for outport in operation.outputs: @@ -225,7 +227,7 @@ class Schedule: max_end_time = 0 for graph_id, op_start_time in self._start_times.items(): operation = cast(Operation, self._sfg.find_by_id(graph_id)) - if graph_id.startswith("out"): + if isinstance(operation, Output): continue else: for outport in operation.outputs: @@ -1044,8 +1046,8 @@ class Schedule: start_time, cast(Operation, self._sfg.find_by_id(graph_id)) ) for graph_id, start_time in self._start_times.items() - if not graph_id.startswith("dontcare") - and not graph_id.startswith("sink") + if not isinstance(self._sfg.find_by_id(graph_id), DontCare) + and not isinstance(self._sfg.find_by_id(graph_id), Sink) }, self.schedule_time, self.cyclic, diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py index 86e16798367d3707a6a725c81bd7fcd0659f4043..8af7b929d5a66531c0819d01aa5c854de2a7de40 100644 --- a/b_asic/scheduler.py +++ b/b_asic/scheduler.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, cast import b_asic.logger as logger from b_asic.core_operations import DontCare, Sink from b_asic.port import OutputPort -from b_asic.special_operations import Delay, Output +from b_asic.special_operations import Delay, Input, Output from b_asic.types import TypeName if TYPE_CHECKING: @@ -143,18 +143,32 @@ class ALAPScheduler(Scheduler): Schedule to apply the scheduling algorithm on. """ ASAPScheduler().apply_scheduling(schedule) + self.op_laps = {} # move all outputs ALAP before operations for output in schedule.sfg.find_by_type_name(Output.type_name()): output = cast(Output, output) + self.op_laps[output.graph_id] = 0 schedule.move_operation_alap(output.graph_id) # move all operations ALAP for step in reversed(schedule.sfg.get_precedence_list()): for outport in step: if not isinstance(outport.operation, Delay): + new_unwrapped_start_time = schedule.start_times[ + outport.operation.graph_id + ] + schedule.forward_slack(outport.operation.graph_id) + self.op_laps[outport.operation.graph_id] = ( + new_unwrapped_start_time // schedule.schedule_time + ) schedule.move_operation_alap(outport.operation.graph_id) + # adjust the scheduling time if empty time slots have appeared in the start + slack = min(schedule.start_times.values()) + for op_id in schedule.start_times.keys(): + schedule.move_operation(op_id, -slack) + schedule.set_schedule_time(schedule.schedule_time - slack) + schedule.sort_y_locations_on_start_times() @@ -319,8 +333,9 @@ class ListScheduler(Scheduler): output_offsets = [ pair[1] for pair in self._cached_latency_offsets[op_id].items() - if pair[0].startswith("out") + if isinstance(self._sfg.find_by_id(pair[0]), Output) ] + start_time += self._alap_op_laps[op_id] * self._alap_schedule_time deadlines[op_id] = ( start_time + min(output_offsets) if output_offsets else start_time ) @@ -328,7 +343,8 @@ class ListScheduler(Scheduler): def _calculate_alap_output_slacks(self) -> dict["GraphID", int]: return { - op_id: start_time for op_id, start_time in self._alap_start_times.items() + op_id: start_time + self._alap_op_laps[op_id] * self._alap_schedule_time + for op_id, start_time in self._alap_start_times.items() } def _calculate_fan_outs(self) -> dict["GraphID", int]: @@ -368,7 +384,7 @@ class ListScheduler(Scheduler): if time < start_time + max( self._cached_execution_times[other_op_id], 1 ): - if other_op_id.startswith(op.type_name()): + if isinstance(self._sfg.find_by_id(other_op_id), type(op)): if other_op_id != op.graph_id: count += 1 return count @@ -383,7 +399,7 @@ class ListScheduler(Scheduler): def _op_satisfies_concurrent_writes(self, op: "Operation") -> bool: tmp_used_writes = {} - if not op.graph_id.startswith("out"): + if not isinstance(op, Output): for i in range(len(op.outputs)): output_ready_time = ( self._current_time @@ -550,8 +566,11 @@ class ListScheduler(Scheduler): alap_schedule = copy.copy(self._schedule) alap_schedule._schedule_time = None - ALAPScheduler().apply_scheduling(alap_schedule) + alap_scheduler = ALAPScheduler() + alap_scheduler.apply_scheduling(alap_schedule) self._alap_start_times = alap_schedule.start_times + self._alap_op_laps = alap_scheduler.op_laps + self._alap_schedule_time = alap_schedule.schedule_time self._schedule.start_times = {} for key in self._schedule._laps.keys(): self._schedule._laps[key] = 0 @@ -566,9 +585,7 @@ class ListScheduler(Scheduler): self._remaining_resources = self._max_resources.copy() - self._remaining_ops = self._sfg.operations - self._remaining_ops = [op.graph_id for op in self._remaining_ops] - + self._remaining_ops = [op.graph_id for op in self._sfg.operations] self._cached_latency_offsets = { op_id: self._sfg.find_by_id(op_id).latency_offsets for op_id in self._remaining_ops @@ -577,6 +594,31 @@ class ListScheduler(Scheduler): op_id: self._sfg.find_by_id(op_id).execution_time for op_id in self._remaining_ops } + self._remaining_ops = [ + op_id + for op_id in self._remaining_ops + if not isinstance(self._sfg.find_by_id(op_id), DontCare) + ] + self._remaining_ops = [ + op_id + for op_id in self._remaining_ops + if not isinstance(self._sfg.find_by_id(op_id), Delay) + ] + self._remaining_ops = [ + op_id + for op_id in self._remaining_ops + if not ( + isinstance(self._sfg.find_by_id(op_id), Output) + and op_id in self._output_delta_times + ) + ] + + for op_id in self._remaining_ops: + if self._sfg.find_by_id(op_id).execution_time is None: + raise ValueError( + "All operations in the SFG must have a specified execution time. " + f"Missing operation: {op_id}." + ) self._deadlines = self._calculate_deadlines() self._output_slacks = self._calculate_alap_output_slacks() @@ -588,18 +630,6 @@ class ListScheduler(Scheduler): self._current_time = 0 self._op_laps = {} - self._remaining_ops = [ - op for op in self._remaining_ops if not op.startswith("dontcare") - ] - self._remaining_ops = [ - op for op in self._remaining_ops if not op.startswith("t") - ] - self._remaining_ops = [ - op - for op in self._remaining_ops - if not (op.startswith("out") and op in self._output_delta_times) - ] - def _schedule_nonrecursive_ops(self) -> None: self._logger.debug("--- Non-Recursive Operation scheduling starting ---") while self._remaining_ops: @@ -665,7 +695,9 @@ class ListScheduler(Scheduler): f" {input_id} time: {self._schedule.start_times[input_id]}" ) self._remaining_ops = [ - elem for elem in self._remaining_ops if not elem.startswith("in") + op_id + for op_id in self._remaining_ops + if not isinstance(self._sfg.find_by_id(op_id), Input) ] self._logger.debug("--- Input placement completed ---") @@ -689,7 +721,9 @@ class ListScheduler(Scheduler): count = -1 for op_id, time in self._schedule.start_times.items(): - if time == new_time and op_id.startswith("out"): + if time == new_time and isinstance( + self._sfg.find_by_id(op_id), Output + ): count += 1 modulo_time = ( diff --git a/b_asic/scheduler_gui/main_window.py b/b_asic/scheduler_gui/main_window.py index ea8cfce7f2e334197d720c36a443940c716ae975..2afeced8ce4c5593a02870bf16ae70bba07dbc6d 100644 --- a/b_asic/scheduler_gui/main_window.py +++ b/b_asic/scheduler_gui/main_window.py @@ -916,18 +916,20 @@ class ScheduleMainWindow(QMainWindow, Ui_MainWindow): self.info_table.insertRow(si) self.info_table.setItem(si, 0, QTableWidgetItem("Forward slack")) - self.info_table.setItem( - si, 1, QTableWidgetItem(str(self.schedule.forward_slack(graph_id))) - ) + forward_slack = self.schedule.forward_slack(graph_id) + if forward_slack < sys.maxsize: + self.info_table.setItem(si, 1, QTableWidgetItem(str(forward_slack))) + else: + self.info_table.setItem(si, 1, QTableWidgetItem("∞")) si += 1 self.info_table.insertRow(si) self.info_table.setItem(si, 0, QTableWidgetItem("Backward slack")) - self.info_table.setItem( - si, - 1, - QTableWidgetItem(str(self.schedule.backward_slack(graph_id))), - ) + backward_slack = self.schedule.backward_slack(graph_id) + if backward_slack < sys.maxsize: + self.info_table.setItem(si, 1, QTableWidgetItem(str(backward_slack))) + else: + self.info_table.setItem(si, 1, QTableWidgetItem("∞")) si += 1 def info_table_clear(self) -> None: diff --git a/test/unit/test_list_schedulers.py b/test/unit/test_list_schedulers.py index 5a1c31f4c14bcc60179a8dedf1a1faadc694cb8c..beba875f62bfe81d7501c47ed2476ad7aade96cf 100644 --- a/test/unit/test_list_schedulers.py +++ b/test/unit/test_list_schedulers.py @@ -18,7 +18,7 @@ from b_asic.list_schedulers import ( MaxFanOutScheduler, ) from b_asic.schedule import Schedule -from b_asic.scheduler import RecursiveListScheduler +from b_asic.scheduler import ListScheduler, RecursiveListScheduler from b_asic.sfg_generators import ( direct_form_1_iir, direct_form_2_iir, @@ -1709,6 +1709,87 @@ class TestHybridScheduler: ) +class TestListScheduler: + def test_latencies_and_execution_times_not_set(self): + N = 3 + Wc = 0.2 + b, a = signal.butter(N, Wc, btype="lowpass", output="ba") + sfg = direct_form_1_iir(b, a) + + sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2) + + resources = { + Addition.type_name(): 1, + ConstantMultiplication.type_name(): 1, + Input.type_name(): 1, + Output.type_name(): 1, + } + + with pytest.raises( + ValueError, + match="Input port 0 of operation add4 has no latency-offset.", + ): + Schedule( + sfg, + scheduler=ListScheduler( + sort_order=((1, True), (3, False), (4, False)), + max_resources=resources, + ), + ) + + sfg.set_latency_offsets_of_type(Addition.type_name(), {"in0": 0, "in1": 0}) + with pytest.raises( + ValueError, + match="Output port 0 of operation add4 has no latency-offset.", + ): + Schedule( + sfg, + scheduler=ListScheduler( + sort_order=((1, True), (3, False), (4, False)), + max_resources=resources, + ), + ) + + sfg.set_latency_of_type(Addition.type_name(), 3) + sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), None) + sfg.set_execution_time_of_type(Addition.type_name(), None) + + with pytest.raises( + ValueError, + match="All operations in the SFG must have a specified execution time. Missing operation: cmul0.", + ): + Schedule( + sfg, + scheduler=ListScheduler( + sort_order=((1, True), (3, False), (4, False)), + max_resources=resources, + ), + ) + + sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1) + + with pytest.raises( + ValueError, + match="All operations in the SFG must have a specified execution time. Missing operation: add0.", + ): + Schedule( + sfg, + scheduler=ListScheduler( + sort_order=((1, True), (3, False), (4, False)), + max_resources=resources, + ), + ) + + sfg.set_execution_time_of_type(Addition.type_name(), 1) + + Schedule( + sfg, + scheduler=ListScheduler( + sort_order=((1, True), (3, False), (4, False)), max_resources=resources + ), + ) + + class TestRecursiveListScheduler: def test_empty_sfg(self, sfg_empty): with pytest.raises( diff --git a/test/unit/test_schedule.py b/test/unit/test_schedule.py index e7c51569764277edde7162f49d407bee5bbb93bc..357e3cb4b320ed79862bb4e3989227b10f461bde 100644 --- a/test/unit/test_schedule.py +++ b/test/unit/test_schedule.py @@ -110,21 +110,21 @@ class TestInit: start_times_names[op_name] = schedule.start_time_of_operation(op_id) assert start_times_names == { - "IN1": 8, - "C0": 8, - "B1": 4, - "B2": 4, - "ADD2": 7, - "ADD1": 11, - "Q1": 15, - "A0": 18, - "A1": 14, - "A2": 14, - "ADD3": 17, - "ADD4": 21, - "OUT1": 25, + "IN1": 8 - 4, + "C0": 8 - 4, + "B1": 4 - 4, + "B2": 4 - 4, + "ADD2": 7 - 4, + "ADD1": 11 - 4, + "Q1": 15 - 4, + "A0": 18 - 4, + "A1": 14 - 4, + "A2": 14 - 4, + "ADD3": 17 - 4, + "ADD4": 21 - 4, + "OUT1": 25 - 4, } - assert schedule.schedule_time == 25 + assert schedule.schedule_time == 25 - 4 def test_complicated_single_outputs_normal_latency_alap_too_short_schedule_time( self, precedence_sfg_delays diff --git a/test/unit/test_scheduler.py b/test/unit/test_scheduler.py index 9ec682b28f2ce9fd3fb4e0d96872c26057a78303..90bf6f549fa973bbf7e6cf7da052600e1b7decf6 100644 --- a/test/unit/test_scheduler.py +++ b/test/unit/test_scheduler.py @@ -204,19 +204,19 @@ class TestALAPScheduler: ) assert schedule.start_times == { - "cmul3": 7, - "cmul4": 7, - "add1": 11, - "in0": 16, - "cmul2": 16, - "cmul1": 16, - "add0": 16, - "add3": 20, - "cmul0": 21, - "add2": 25, - "out0": 30, + "cmul3": 7 - 7, + "cmul4": 7 - 7, + "add1": 11 - 7, + "in0": 16 - 7, + "cmul2": 16 - 7, + "cmul1": 16 - 7, + "add0": 16 - 7, + "add3": 20 - 7, + "cmul0": 21 - 7, + "add2": 25 - 7, + "out0": 30 - 7, } - assert schedule.schedule_time == 30 + assert schedule.schedule_time == 30 - 7 def test_radix_2_fft_8_points(self): sfg = radix_2_dif_fft(points=8)