diff --git a/b_asic/schedule.py b/b_asic/schedule.py index a4655e076b73193af06817cb75cdb3116bc520b5..c80466a29f1bfe2070f72f35009c8559c81c4e0d 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -72,15 +72,22 @@ class Schedule: algorithm. cyclic : bool, default: False If the schedule is cyclic. - scheduling_algorithm : {'ASAP', 'provided'}, optional - The scheduling algorithm to use. Currently, only "ASAP" is supported. + algorithm : {'ASAP', 'ALAP', 'provided'}, optional + The scheduling algorithm to use. The following algorithm are available: + * ``'ASAP'``: As-soon-as-possible scheduling. + * ``'ALAP'``: As-late-as-possible scheduling. If 'provided', use provided *start_times* and *laps* dictionaries. start_times : dict, optional Dictionary with GraphIDs as keys and start times as values. - Used when *scheduling_algorithm* is 'provided'. + Used when *algorithm* is 'provided'. laps : dict, optional Dictionary with GraphIDs as keys and laps as values. - Used when *scheduling_algorithm* is 'provided'. + Used when *algorithm* is 'provided'. + max_resources : dict, optional + Dictionary like ``{'cmul': 2}`` denoting the maximum number of resources + for a given operation type if the scheduling algorithm considers that. + If not provided, or an operation type is not provided, at most one resource is + used. """ _sfg: SFG @@ -95,9 +102,10 @@ class Schedule: sfg: SFG, schedule_time: Optional[int] = None, cyclic: bool = False, - scheduling_algorithm: str = "ASAP", + algorithm: str = "ASAP", start_times: Optional[Dict[GraphID, int]] = None, laps: Optional[Dict[GraphID, int]] = None, + max_resources: Optional[Dict[TypeName, int]] = None, ): """Construct a Schedule from an SFG.""" if not isinstance(sfg, SFG): @@ -109,9 +117,12 @@ class Schedule: self._laps = defaultdict(_laps_default) self._cyclic = cyclic self._y_locations = defaultdict(_y_locations_default) - if scheduling_algorithm == "ASAP": + self._schedule_time = schedule_time + if algorithm == "ASAP": self._schedule_asap() - elif scheduling_algorithm == "provided": + elif algorithm == "ALAP": + self._schedule_alap() + elif algorithm == "provided": if start_times is None: raise ValueError("Must provide start_times when using 'provided'") if laps is None: @@ -120,9 +131,7 @@ class Schedule: self._laps.update(laps) self._remove_delays_no_laps() else: - raise NotImplementedError( - f"No algorithm with name: {scheduling_algorithm} defined." - ) + raise NotImplementedError(f"No algorithm with name: {algorithm} defined.") max_end_time = self.get_max_end_time() @@ -130,8 +139,6 @@ class Schedule: self._schedule_time = max_end_time elif schedule_time < max_end_time: raise ValueError(f"Too short schedule time. Minimum is {max_end_time}.") - else: - self._schedule_time = schedule_time def start_time_of_operation(self, graph_id: GraphID) -> int: """ @@ -636,10 +643,24 @@ class Schedule: # schedule period if new_available == 0 and (new_slack > 0 or next_usage == 0): new_available = self._schedule_time - if next_usage < new_available: + if ( + next_usage < new_available + and self._start_times[signal.destination_operation.graph_id] + != self.schedule_time + ): laps += 1 self._laps[signal.graph_id] = laps + # Outputs should not start at 0, but at schedule time + op = self._sfg.find_by_id(graph_id) + if ( + new_start == 0 + and isinstance(op, Output) + and self._laps[op.input(0).signals[0].graph_id] != 0 + ): + new_start = self._schedule_time + self._laps[op.input(0).signals[0].graph_id] -= 1 + print(f"Moved {graph_id}") # Set new start time self._start_times[graph_id] = new_start return self @@ -715,6 +736,29 @@ class Schedule: del self._laps[delay_input_id] delay_list = self._sfg.find_by_type_name(Delay.type_name()) + def _schedule_alap(self) -> None: + """Schedule the operations using as-late-as-possible scheduling.""" + precedence_list = self._sfg.get_precedence_list() + self._schedule_asap() + max_end_time = self.get_max_end_time() + + if self.schedule_time is None: + self._schedule_time = max_end_time + elif self.schedule_time < max_end_time: + raise ValueError(f"Too short schedule time. Minimum is {max_end_time}.") + + for output in self._sfg.find_by_type_name(Output.type_name()): + output = cast(Output, output) + self.move_operation_alap(output.graph_id) + for step in reversed(precedence_list): + graph_ids = { + outport.operation.graph_id + for outport in step + if not isinstance(outport.operation, Delay) + } + for graph_id in graph_ids: + self.move_operation_alap(graph_id) + def _schedule_asap(self) -> None: """Schedule the operations using as-soon-as-possible scheduling.""" precedence_list = self._sfg.get_precedence_list() diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index 7bcb273ea369611eec98e3568c59b8f18efb7518..464513bbc2f6b67f1cf2ccdbfe4511518a395095 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -1548,7 +1548,7 @@ class SFG(AbstractOperation): # Import here needed to avoid circular imports from b_asic.schedule import Schedule - return Schedule(self, scheduling_algorithm="ASAP").schedule_time + return Schedule(self, algorithm="ASAP").schedule_time def iteration_period_bound(self) -> int: """ diff --git a/test/fixtures/schedule.py b/test/fixtures/schedule.py index 4091bd4047750996e84b87af3c7dcae0ab1ab51d..f6ec65bf677825bdb06b61aa426ba3f893c315a1 100644 --- a/test/fixtures/schedule.py +++ b/test/fixtures/schedule.py @@ -10,7 +10,7 @@ def secondorder_iir_schedule(precedence_sfg_delays): precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 4) precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") return schedule @@ -23,7 +23,7 @@ def secondorder_iir_schedule_with_execution_times(precedence_sfg_delays): ConstantMultiplication.type_name(), 1 ) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") return schedule @@ -37,9 +37,7 @@ def schedule_direct_form_iir_lp_filter(sfg_direct_form_iir_lp_filter: SFG): sfg_direct_form_iir_lp_filter.set_execution_time_of_type( ConstantMultiplication.type_name(), 1 ) - schedule = Schedule( - sfg_direct_form_iir_lp_filter, scheduling_algorithm="ASAP", cyclic=True - ) + schedule = Schedule(sfg_direct_form_iir_lp_filter, algorithm="ASAP", cyclic=True) schedule.move_operation('cmul4', -1) schedule.move_operation('cmul3', -1) schedule.move_operation('cmul4', -10) diff --git a/test/test_schedule.py b/test/test_schedule.py index e5fc64b375ee55da016372bbffc8a4261ee3b09c..14ba916a9dd0d7baba24eafb6c870d6bdaba7c7f 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -31,7 +31,7 @@ class TestInit: precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 4) precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") for op in schedule._sfg.get_operations_topological_order(): print(op.latency_offsets) @@ -58,6 +58,39 @@ class TestInit: } assert schedule.schedule_time == 21 + def test_complicated_single_outputs_normal_latency_alap( + self, precedence_sfg_delays + ): + precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 4) + precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) + + schedule = Schedule(precedence_sfg_delays, algorithm="ALAP") + + for op in schedule._sfg.get_operations_topological_order(): + print(op.latency_offsets) + + start_times_names = {} + for op_id, start_time in schedule._start_times.items(): + op_name = precedence_sfg_delays.find_by_id(op_id).name + start_times_names[op_name] = start_time + + assert start_times_names == { + "IN1": 4, + "C0": 4, + "B1": 0, + "B2": 0, + "ADD2": 3, + "ADD1": 7, + "Q1": 11, + "A0": 14, + "A1": 10, + "A2": 10, + "ADD3": 13, + "ADD4": 17, + "OUT1": 21, + } + assert schedule.schedule_time == 21 + def test_complicated_single_outputs_normal_latency_from_fixture( self, secondorder_iir_schedule ): @@ -120,7 +153,7 @@ class TestInit: {"in0": 6, "in1": 7, "out0": 9} ) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") start_times_names = {} for op_id, start_time in schedule._start_times.items(): @@ -148,7 +181,7 @@ class TestInit: def test_independent_sfg(self, sfg_two_inputs_two_outputs_independent_with_cmul): schedule = Schedule( sfg_two_inputs_two_outputs_independent_with_cmul, - scheduling_algorithm="ASAP", + algorithm="ASAP", ) start_times_names = {} @@ -177,7 +210,7 @@ class TestSlacks: precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1) precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") assert ( schedule.forward_slack( precedence_sfg_delays.find_by_name("ADD3")[0].graph_id @@ -206,7 +239,7 @@ class TestSlacks: precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1) precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") assert schedule.slacks( precedence_sfg_delays.find_by_name("ADD3")[0].graph_id ) == (0, 7) @@ -220,7 +253,7 @@ class TestRescheduling: precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 4) precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") schedule.move_operation( precedence_sfg_delays.find_by_name("ADD3")[0].graph_id, 4 @@ -252,7 +285,7 @@ class TestRescheduling: precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1) precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") add3_id = precedence_sfg_delays.find_by_name("ADD3")[0].graph_id schedule.move_operation(add3_id, 4) assert schedule.forward_slack(add3_id) == 3 @@ -274,7 +307,7 @@ class TestRescheduling: precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1) precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") with pytest.raises( ValueError, match="Operation add4 got incorrect move: -4. Must be between 0 and 7.", @@ -287,7 +320,7 @@ class TestRescheduling: precedence_sfg_delays.set_latency_of_type(Addition.type_name(), 1) precedence_sfg_delays.set_latency_of_type(ConstantMultiplication.type_name(), 3) - schedule = Schedule(precedence_sfg_delays, scheduling_algorithm="ASAP") + schedule = Schedule(precedence_sfg_delays, algorithm="ASAP") with pytest.raises( ValueError, match="Operation add4 got incorrect move: 10. Must be between 0 and 7.", @@ -315,51 +348,43 @@ class TestRescheduling: # Move and scheduling algorithm behaves differently schedule.move_operation("out1", 0) - assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 1 + assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 0 assert schedule.laps[sfg.find_by_id("add1").input(0).signals[0].graph_id] == 1 assert schedule.laps[sfg.find_by_id("add1").input(1).signals[0].graph_id] == 0 - assert schedule._start_times["out1"] == 0 + assert schedule._start_times["out1"] == 1 assert schedule._start_times["add1"] == 0 # Increase schedule time schedule.set_schedule_time(2) - assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 1 + assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 0 assert schedule.laps[sfg.find_by_id("add1").input(0).signals[0].graph_id] == 1 assert schedule.laps[sfg.find_by_id("add1").input(1).signals[0].graph_id] == 0 - assert schedule._start_times["out1"] == 0 + assert schedule._start_times["out1"] == 1 assert schedule._start_times["add1"] == 0 # Move out one time unit schedule.move_operation("out1", 1) - assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 1 + assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 0 assert schedule.laps[sfg.find_by_id("add1").input(0).signals[0].graph_id] == 1 assert schedule.laps[sfg.find_by_id("add1").input(1).signals[0].graph_id] == 0 - assert schedule._start_times["out1"] == 1 + assert schedule._start_times["out1"] == 2 assert schedule._start_times["add1"] == 0 # Move add one time unit schedule.move_operation("add1", 1) assert schedule.laps[sfg.find_by_id("add1").input(0).signals[0].graph_id] == 1 assert schedule.laps[sfg.find_by_id("add1").input(1).signals[0].graph_id] == 0 - assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 1 - assert schedule._start_times["add1"] == 1 - assert schedule._start_times["out1"] == 1 - - # Move out back one time unit - schedule.move_operation("out1", -1) - assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 1 - assert schedule._start_times["out1"] == 0 - assert schedule.laps[sfg.find_by_id("add1").input(0).signals[0].graph_id] == 1 - assert schedule.laps[sfg.find_by_id("add1").input(1).signals[0].graph_id] == 0 + assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 0 assert schedule._start_times["add1"] == 1 + assert schedule._start_times["out1"] == 2 # Move add back one time unit schedule.move_operation("add1", -1) assert schedule.laps[sfg.find_by_id("add1").input(0).signals[0].graph_id] == 1 assert schedule.laps[sfg.find_by_id("add1").input(1).signals[0].graph_id] == 0 - assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 1 + assert schedule.laps[sfg.find_by_id("out1").input(0).signals[0].graph_id] == 0 assert schedule._start_times["add1"] == 0 - assert schedule._start_times["out1"] == 0 + assert schedule._start_times["out1"] == 2 class TestTimeResolution: @@ -368,7 +393,7 @@ class TestTimeResolution: ): schedule = Schedule( sfg_two_inputs_two_outputs_independent_with_cmul, - scheduling_algorithm="ASAP", + algorithm="ASAP", ) old_schedule_time = schedule.schedule_time assert schedule.get_possible_time_resolution_decrements() == [1] @@ -402,7 +427,7 @@ class TestTimeResolution: ): schedule = Schedule( sfg_two_inputs_two_outputs_independent_with_cmul, - scheduling_algorithm="ASAP", + algorithm="ASAP", ) old_schedule_time = schedule.schedule_time @@ -439,7 +464,7 @@ class TestTimeResolution: ): schedule = Schedule( sfg_two_inputs_two_outputs_independent_with_cmul, - scheduling_algorithm="ASAP", + algorithm="ASAP", ) old_schedule_time = schedule.schedule_time assert schedule.get_possible_time_resolution_decrements() == [1] @@ -567,7 +592,7 @@ class TestErrors: with pytest.raises( NotImplementedError, match="No algorithm with name: foo defined." ): - Schedule(sfg_simple_filter, scheduling_algorithm="foo") + Schedule(sfg_simple_filter, algorithm="foo") class TestGetUsedTypeNames: