From e7ea2ce8ea4d41e3a3d19a87f504619807c2fc58 Mon Sep 17 00:00:00 2001 From: Simon Bjurek <simbj106@student.liu.se> Date: Fri, 28 Feb 2025 21:19:46 +0100 Subject: [PATCH] Output times now pushed correctly, updated logger and updated Scheduler interface. --- b_asic/list_schedulers.py | 78 +++++- b_asic/logger.py | 44 ++-- b_asic/schedule.py | 21 +- b_asic/scheduler.py | 157 +++++++++--- b_asic/scheduler_gui/compile.py | 2 +- b_asic/scheduler_gui/main_window.py | 2 +- test/unit/test_list_schedulers.py | 369 +++++++++++++++++++++++----- 7 files changed, 532 insertions(+), 141 deletions(-) diff --git a/b_asic/list_schedulers.py b/b_asic/list_schedulers.py index 3f53a3d7..dd9550ea 100644 --- a/b_asic/list_schedulers.py +++ b/b_asic/list_schedulers.py @@ -1,33 +1,87 @@ from b_asic.scheduler import ListScheduler +from b_asic.types import GraphID, TypeName class EarliestDeadlineScheduler(ListScheduler): """Scheduler that implements the earliest-deadline-first algorithm.""" - @property - def sort_indices(self) -> tuple[tuple[int, bool]]: - return ((1, True),) + def __init__( + self, + max_resources: dict[TypeName, int] | None = None, + max_concurrent_reads: int | None = None, + max_concurrent_writes: int | None = None, + input_times: dict["GraphID", int] | None = None, + output_delta_times: dict["GraphID", int] | None = None, + cyclic: bool | None = False, + ) -> None: + super().__init__( + max_resources=max_resources, + max_concurrent_reads=max_concurrent_reads, + max_concurrent_writes=max_concurrent_writes, + input_times=input_times, + output_delta_times=output_delta_times, + sort_order=((1, True),), + ) class LeastSlackTimeScheduler(ListScheduler): """Scheduler that implements the least slack time first algorithm.""" - @property - def sort_indices(self) -> tuple[tuple[int, bool]]: - return ((2, True),) + def __init__( + self, + max_resources: dict[TypeName, int] = None, + max_concurrent_reads: int = None, + max_concurrent_writes: int = None, + input_times: dict["GraphID", int] = None, + output_delta_times: dict["GraphID", int] = None, + ) -> None: + super().__init__( + max_resources=max_resources, + max_concurrent_reads=max_concurrent_reads, + max_concurrent_writes=max_concurrent_writes, + input_times=input_times, + output_delta_times=output_delta_times, + sort_order=((2, True),), + ) class MaxFanOutScheduler(ListScheduler): """Scheduler that implements the maximum fan-out algorithm.""" - @property - def sort_indices(self) -> tuple[tuple[int, bool]]: - return ((3, False),) + def __init__( + self, + max_resources: dict[TypeName, int] = None, + max_concurrent_reads: int = None, + max_concurrent_writes: int = None, + input_times: dict["GraphID", int] = None, + output_delta_times: dict["GraphID", int] = None, + ) -> None: + super().__init__( + max_resources=max_resources, + max_concurrent_reads=max_concurrent_reads, + max_concurrent_writes=max_concurrent_writes, + input_times=input_times, + output_delta_times=output_delta_times, + sort_order=((3, False),), + ) class HybridScheduler(ListScheduler): """Scheduler that implements a hybrid algorithm. Will receive a new name once finalized.""" - @property - def sort_indices(self) -> tuple[tuple[int, bool]]: - return ((2, True), (3, False)) + def __init__( + self, + max_resources: dict[TypeName, int] = None, + max_concurrent_reads: int = None, + max_concurrent_writes: int = None, + input_times: dict["GraphID", int] = None, + output_delta_times: dict["GraphID", int] = None, + ) -> None: + super().__init__( + max_resources=max_resources, + max_concurrent_reads=max_concurrent_reads, + max_concurrent_writes=max_concurrent_writes, + input_times=input_times, + output_delta_times=output_delta_times, + sort_order=((2, True), (3, False)), + ) diff --git a/b_asic/logger.py b/b_asic/logger.py index 53b510e3..2a948ce3 100644 --- a/b_asic/logger.py +++ b/b_asic/logger.py @@ -54,38 +54,34 @@ from logging import Logger from types import TracebackType -def getLogger(source: str, filename: str, loglevel: str = "INFO") -> Logger: +def getLogger( + name: str, filename: str | None = "", console_log_level: str = "warning" +) -> Logger: """ This function creates console- and filehandler and from those, creates a logger object. Parameters ---------- - source : str - Source filename. - filename : str - Output filename. - loglevel : str, optional - The minimum level that the logger will log. Defaults to 'INFO'. + name : str + Name of the logger, creates a new if needed. + filename : str, optional + Name of output logfile. Defaults to "". + console_log_level : str, optional + The minimum level that the logger will log. Defaults to 'info'. Returns ------- Logger : 'logging.Logger' object. """ - # logger = logging.getLogger(name) - # logger = logging.getLogger('root') - logger = logging.getLogger(source) + logger = logging.getLogger(name) - # if logger 'name' already exists, return it to avoid logging duplicate - # messages by attaching multiple handlers of the same type if logger.handlers: return logger - # if logger 'name' does not already exist, create it and attach handlers else: - # set logLevel to loglevel or to INFO if requested level is incorrect - loglevel = getattr(logging, loglevel.upper(), logging.INFO) - logger.setLevel(loglevel) + console_log_level = getattr(logging, console_log_level.upper()) + logger.setLevel(console_log_level) # set up the console logger c_fmt_date = "%T" @@ -96,7 +92,7 @@ def getLogger(source: str, filename: str, loglevel: str = "INFO") -> Logger: c_formatter = logging.Formatter(c_fmt, c_fmt_date) c_handler = logging.StreamHandler() c_handler.setFormatter(c_formatter) - c_handler.setLevel(logging.WARNING) + c_handler.setLevel(console_log_level) logger.addHandler(c_handler) # setup the file logger @@ -105,11 +101,13 @@ def getLogger(source: str, filename: str, loglevel: str = "INFO") -> Logger: "%(asctime)s %(filename)18s:%(lineno)-4s %(funcName)20s()" " %(levelname)-8s: %(message)s" ) - f_formatter = logging.Formatter(f_fmt, f_fmt_date) - f_handler = logging.FileHandler(filename, mode="w") - f_handler.setFormatter(f_formatter) - f_handler.setLevel(logging.DEBUG) - logger.addHandler(f_handler) + + if filename: + f_formatter = logging.Formatter(f_fmt, f_fmt_date) + f_handler = logging.FileHandler(filename, mode="w") + f_handler.setFormatter(f_formatter) + f_handler.setLevel(logging.DEBUG) + logger.addHandler(f_handler) if logger.name == "scheduler-gui.log": logger.info( @@ -121,13 +119,11 @@ def getLogger(source: str, filename: str, loglevel: str = "INFO") -> Logger: return logger -# log uncaught exceptions def handle_exceptions( exc_type: type[BaseException], exc_value: BaseException, exc_traceback: TracebackType | None, ) -> None: - # def log_exceptions(type, value, tb): """This function is a helper function to log uncaught exceptions. Install with: `sys.excepthook = <module>.handle_exceptions`""" if issubclass(exc_type, KeyboardInterrupt): diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 13433612..2c3910de 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -352,13 +352,20 @@ class Schedule: if source.operation.graph_id.startswith("dontcare"): available_time = 0 else: - available_time = ( - cast(int, source.latency_offset) - + self._start_times[source.operation.graph_id] - - self._schedule_time * self._laps[signal.graph_id] - ) - if available_time > self._schedule_time: - available_time -= self._schedule_time + if self._schedule_time is not None: + available_time = ( + cast(int, source.latency_offset) + + self._start_times[source.operation.graph_id] + - self._schedule_time * self._laps[signal.graph_id] + ) + if available_time > self._schedule_time: + available_time -= self._schedule_time + else: + available_time = ( + cast(int, source.latency_offset) + + self._start_times[source.operation.graph_id] + ) + input_slacks[signal] = usage_time - available_time return input_slacks diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py index 9a40ec02..0e9fa628 100644 --- a/b_asic/scheduler.py +++ b/b_asic/scheduler.py @@ -160,8 +160,27 @@ class ALAPScheduler(Scheduler): class ListScheduler(Scheduler, ABC): - - TIME_OUT_COUNTER_LIMIT = 100 + """ + List-based scheduler that schedules the operations while complying to the given + constraints. + + Parameters + ---------- + max_resources : dict[TypeName, int] | None, optional + Max resources available to realize the schedule, by default None + max_concurrent_reads : int | None, optional + Max number of conccurent reads, by default None + max_concurrent_writes : int | None, optional + Max number of conccurent writes, by default None + input_times : dict[GraphID, int] | None, optional + Specified input times, by default None + output_delta_times : dict[GraphID, int] | None, optional + Specified output delta times, by default None + cyclic : bool | None, optional + If the scheduler is allowed to schedule cyclically (modulo), by default False + sort_order : tuple[tuple[int, bool]] + Specifies which columns in the priority table to sort on and in which order, where True is ascending order. + """ def __init__( self, @@ -170,19 +189,19 @@ class ListScheduler(Scheduler, ABC): max_concurrent_writes: int | None = None, input_times: dict["GraphID", int] | None = None, output_delta_times: dict["GraphID", int] | None = None, - cyclic: bool | None = False, + sort_order=tuple[tuple[int, bool], ...], ) -> None: super() - self._logger = logger.getLogger(__name__, "list_scheduler.log", "DEBUG") + self._logger = logger.getLogger("list_scheduler") if max_resources is not None: if not isinstance(max_resources, dict): - raise ValueError("max_resources must be a dictionary.") + raise ValueError("Provided max_resources must be a dictionary.") for key, value in max_resources.items(): if not isinstance(key, str): - raise ValueError("max_resources key must be a valid type_name.") + raise ValueError("Provided max_resources keys must be strings.") if not isinstance(value, int): - raise ValueError("max_resources value must be an integer.") + raise ValueError("Provided max_resources values must be integers.") self._max_resources = max_resources else: self._max_resources = {} @@ -192,16 +211,57 @@ class ListScheduler(Scheduler, ABC): if Output.type_name() not in self._max_resources: self._max_resources[Output.type_name()] = 1 + if max_concurrent_reads is not None: + if not isinstance(max_concurrent_reads, int): + raise ValueError("Provided max_concurrent_reads must be an integer.") + if max_concurrent_reads <= 0: + raise ValueError("Provided max_concurrent_reads must be larger than 0.") self._max_concurrent_reads = max_concurrent_reads or sys.maxsize + + if max_concurrent_writes is not None: + if not isinstance(max_concurrent_writes, int): + raise ValueError("Provided max_concurrent_writes must be an integer.") + if max_concurrent_writes <= 0: + raise ValueError( + "Provided max_concurrent_writes must be larger than 0." + ) self._max_concurrent_writes = max_concurrent_writes or sys.maxsize - self._input_times = input_times or {} - self._output_delta_times = output_delta_times or {} + if input_times is not None: + if not isinstance(input_times, dict): + raise ValueError("Provided input_times must be a dictionary.") + for key, value in input_times.items(): + if not isinstance(key, str): + raise ValueError("Provided input_times keys must be strings.") + if not isinstance(value, int): + raise ValueError("Provided input_times values must be integers.") + if any(time < 0 for time in input_times.values()): + raise ValueError("Provided input_times values must be non-negative.") + self._input_times = input_times + else: + self._input_times = {} - @property - @abstractmethod - def sort_indices(self) -> tuple[tuple[int, bool]]: - raise NotImplementedError + if output_delta_times is not None: + if not isinstance(output_delta_times, dict): + raise ValueError("Provided output_delta_times must be a dictionary.") + for key, value in output_delta_times.items(): + if not isinstance(key, str): + raise ValueError( + "Provided output_delta_times keys must be strings." + ) + if not isinstance(value, int): + raise ValueError( + "Provided output_delta_times values must be integers." + ) + if any(time < 0 for time in output_delta_times.values()): + raise ValueError( + "Provided output_delta_times values must be non-negative." + ) + self._output_delta_times = output_delta_times + else: + self._output_delta_times = {} + + self._sort_order = sort_order def apply_scheduling(self, schedule: "Schedule") -> None: """Applies the scheduling algorithm on the given Schedule. @@ -216,7 +276,25 @@ class ListScheduler(Scheduler, ABC): self._schedule = schedule self._sfg = schedule.sfg - if self._schedule.cyclic and self._schedule.schedule_time is None: + for resource_type in self._max_resources.keys(): + if not self._sfg.find_by_type_name(resource_type): + raise ValueError( + f"Provided max resource of type {resource_type} cannot be found in the provided SFG." + ) + + for key in self._input_times.keys(): + if self._sfg.find_by_id(key) is None: + raise ValueError( + f"Provided input time with GraphID {key} cannot be found in the provided SFG." + ) + + for key in self._output_delta_times.keys(): + if self._sfg.find_by_id(key) is None: + raise ValueError( + f"Provided output delta time with GraphID {key} cannot be found in the provided SFG." + ) + + if self._schedule._cyclic and self._schedule.schedule_time is None: raise ValueError("Scheduling time must be provided when cyclic = True.") for resource_type, resource_amount in self._max_resources.items(): @@ -239,7 +317,7 @@ class ListScheduler(Scheduler, ABC): alap_start_times = alap_schedule.start_times self._schedule.start_times = {} - if not self._schedule.cyclic and self._schedule.schedule_time: + if not self._schedule._cyclic and self._schedule.schedule_time: if alap_schedule.schedule_time > self._schedule.schedule_time: raise ValueError( f"Provided scheduling time {schedule.schedule_time} cannot be reached, " @@ -268,7 +346,6 @@ class ListScheduler(Scheduler, ABC): self.remaining_reads = self._max_concurrent_reads self._current_time = 0 - self._time_out_counter = 0 self._op_laps = {} self._remaining_ops = [ @@ -310,7 +387,6 @@ class ListScheduler(Scheduler, ABC): op_id for op_id in self._remaining_ops if op_id != next_op.graph_id ] - self._time_out_counter = 0 self._schedule.place_operation(next_op, self._current_time) self._op_laps[next_op.graph_id] = ( (self._current_time) // self._schedule.schedule_time @@ -329,7 +405,7 @@ class ListScheduler(Scheduler, ABC): ready_ops_priority_table = self._get_ready_ops_priority_table() - self._go_to_next_time_step() + self._current_time += 1 self.remaining_reads = self._max_concurrent_reads self._logger.debug("--- Operation scheduling completed ---") @@ -353,22 +429,13 @@ class ListScheduler(Scheduler, ABC): self._schedule.sort_y_locations_on_start_times() self._logger.debug("--- Scheduling completed ---") - def _go_to_next_time_step(self): - self._time_out_counter += 1 - if self._time_out_counter >= self.TIME_OUT_COUNTER_LIMIT: - raise TimeoutError( - "Algorithm did not manage to schedule any operation for 10 time steps, " - "try relaxing the constraints." - ) - self._current_time += 1 - def _get_next_op_id( self, ready_ops_priority_table: list[tuple["GraphID", int, ...]] ) -> "GraphID": def sort_key(item): return tuple( (item[index] * (-1 if not asc else 1),) - for index, asc in self.sort_indices + for index, asc in self._sort_order ) sorted_table = sorted(ready_ops_priority_table, key=sort_key) @@ -492,7 +559,7 @@ class ListScheduler(Scheduler, ABC): def _handle_outputs(self) -> None: self._logger.debug("--- Output placement starting ---") - if self._schedule.cyclic: + if self._schedule._cyclic: end = self._schedule.schedule_time else: end = self._schedule.get_max_end_time() @@ -503,7 +570,7 @@ class ListScheduler(Scheduler, ABC): new_time = end + delta_time - if self._schedule.cyclic and self._schedule.schedule_time is not None: + if self._schedule._cyclic and self._schedule.schedule_time is not None: self._schedule.place_operation(output, new_time) else: self._schedule.start_times[output.graph_id] = new_time @@ -531,5 +598,33 @@ class ListScheduler(Scheduler, ABC): else new_time ) self._logger.debug(f" {output.graph_id} time: {modulo_time}") - self._logger.debug("--- Output placement completed ---") + + self._logger.debug("--- Output placement optimization starting ---") + min_slack = min( + self._schedule.backward_slack(op.graph_id) + for op in self._sfg.find_by_type_name(Output.type_name()) + ) + if min_slack > 0: + for output in self._sfg.find_by_type_name(Output.type_name()): + if self._schedule._cyclic and self._schedule.schedule_time is not None: + self._schedule.move_operation(output.graph_id, -min_slack) + else: + self._schedule.start_times[output.graph_id] = ( + self._schedule.start_times[output.graph_id] - min_slack + ) + new_time = self._schedule.start_times[output.graph_id] + if ( + not self._schedule._cyclic + and self._schedule.schedule_time is not None + ): + if new_time > self._schedule.schedule_time: + raise ValueError( + f"Cannot place output {output.graph_id} at time {new_time} " + f"for scheduling time {self._schedule.schedule_time}. " + "Try to relax the scheduling time, change the output delta times or enable cyclic." + ) + self._logger.debug( + f" {output.graph_id} moved {min_slack} time steps backwards to new time {new_time}" + ) + self._logger.debug("--- Output placement optimization completed ---") diff --git a/b_asic/scheduler_gui/compile.py b/b_asic/scheduler_gui/compile.py index 5a7fcff4..922b6ecf 100644 --- a/b_asic/scheduler_gui/compile.py +++ b/b_asic/scheduler_gui/compile.py @@ -20,7 +20,7 @@ from setuptools_scm import get_version try: import b_asic.logger as logger - log = logger.getLogger(__name__, "scheduler-gui.log") + log = logger.getLogger(__name__) sys.excepthook = logger.handle_exceptions except ModuleNotFoundError: log = None diff --git a/b_asic/scheduler_gui/main_window.py b/b_asic/scheduler_gui/main_window.py index 09756521..9bb7dad9 100644 --- a/b_asic/scheduler_gui/main_window.py +++ b/b_asic/scheduler_gui/main_window.py @@ -81,7 +81,7 @@ from b_asic.scheduler_gui.ui_main_window import Ui_MainWindow if TYPE_CHECKING: from logging import Logger -log: "Logger" = logger.getLogger(__name__, "scheduler-gui.log") +log: "Logger" = logger.getLogger(__name__) sys.excepthook = logger.handle_exceptions diff --git a/test/unit/test_list_schedulers.py b/test/unit/test_list_schedulers.py index 252baa3d..fa349f87 100644 --- a/test/unit/test_list_schedulers.py +++ b/test/unit/test_list_schedulers.py @@ -621,14 +621,14 @@ class TestHybridScheduler: "in7": 7, } output_times = { - "out0": -2, - "out1": -1, - "out2": 0, - "out3": 1, - "out4": 2, - "out5": 3, - "out6": 4, - "out7": 5, + "out0": 0, + "out1": 1, + "out2": 2, + "out3": 3, + "out4": 4, + "out5": 5, + "out6": 6, + "out7": 7, } schedule = Schedule( sfg, @@ -665,14 +665,14 @@ class TestHybridScheduler: "cmul1": 15, "bfly10": 16, "bfly4": 17, - "out0": 18, - "out1": 19, - "out2": 20, - "out3": 1, - "out4": 2, - "out5": 3, - "out6": 4, - "out7": 5, + "out0": 17, + "out1": 18, + "out2": 19, + "out3": 20, + "out4": 1, + "out5": 2, + "out6": 3, + "out7": 4, } assert schedule.schedule_time == 20 @@ -696,14 +696,14 @@ class TestHybridScheduler: "in7": 7, } output_times = { - "out0": -2, - "out1": -1, - "out2": 0, - "out3": 1, - "out4": 2, - "out5": 3, - "out6": 4, - "out7": 5, + "out0": 0, + "out1": 1, + "out2": 2, + "out3": 3, + "out4": 4, + "out5": 5, + "out6": 6, + "out7": 7, } schedule = Schedule( sfg, @@ -739,16 +739,16 @@ class TestHybridScheduler: "cmul1": 15, "bfly10": 16, "bfly4": 17, - "out0": 18, - "out1": 19, - "out2": 20, - "out3": 21, - "out4": 22, - "out5": 23, - "out6": 24, - "out7": 25, + "out0": 17, + "out1": 18, + "out2": 19, + "out3": 20, + "out4": 21, + "out5": 22, + "out6": 23, + "out7": 24, } - assert schedule.schedule_time == 25 + assert schedule.schedule_time == 24 def test_ldlt_inverse_2x2(self): sfg = ldlt_matrix_inverse(N=2) @@ -833,7 +833,7 @@ class TestHybridScheduler: } assert schedule.schedule_time == 16 - def test_max_invalid_resources(self): + def test_invalid_max_resources(self): sfg = ldlt_matrix_inverse(N=2) sfg.set_latency_of_type(MADS.type_name(), 3) @@ -842,27 +842,256 @@ class TestHybridScheduler: sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) resources = 2 - with pytest.raises(ValueError, match="max_resources must be a dictionary."): + with pytest.raises( + ValueError, match="Provided max_resources must be a dictionary." + ): Schedule(sfg, scheduler=HybridScheduler(resources)) resources = "test" - with pytest.raises(ValueError, match="max_resources must be a dictionary."): + with pytest.raises( + ValueError, match="Provided max_resources must be a dictionary." + ): Schedule(sfg, scheduler=HybridScheduler(resources)) resources = [] - with pytest.raises(ValueError, match="max_resources must be a dictionary."): + with pytest.raises( + ValueError, match="Provided max_resources must be a dictionary." + ): Schedule(sfg, scheduler=HybridScheduler(resources)) resources = {1: 1} with pytest.raises( - ValueError, match="max_resources key must be a valid type_name." + ValueError, match="Provided max_resources keys must be strings." ): Schedule(sfg, scheduler=HybridScheduler(resources)) resources = {MADS.type_name(): "test"} - with pytest.raises(ValueError, match="max_resources value must be an integer."): + with pytest.raises( + ValueError, match="Provided max_resources values must be integers." + ): Schedule(sfg, scheduler=HybridScheduler(resources)) + def test_invalid_max_concurrent_writes(self): + sfg = ldlt_matrix_inverse(N=2) + + sfg.set_latency_of_type(MADS.type_name(), 3) + sfg.set_latency_of_type(Reciprocal.type_name(), 2) + sfg.set_execution_time_of_type(MADS.type_name(), 1) + sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) + + max_concurrent_writes = "5" + with pytest.raises( + ValueError, match="Provided max_concurrent_writes must be an integer." + ): + Schedule( + sfg, + scheduler=HybridScheduler(max_concurrent_writes=max_concurrent_writes), + ) + + max_concurrent_writes = 0 + with pytest.raises( + ValueError, match="Provided max_concurrent_writes must be larger than 0." + ): + Schedule( + sfg, + scheduler=HybridScheduler(max_concurrent_writes=max_concurrent_writes), + ) + + max_concurrent_writes = -1 + with pytest.raises( + ValueError, match="Provided max_concurrent_writes must be larger than 0." + ): + Schedule( + sfg, + scheduler=HybridScheduler(max_concurrent_writes=max_concurrent_writes), + ) + + def test_invalid_max_concurrent_reads(self): + sfg = ldlt_matrix_inverse(N=2) + + sfg.set_latency_of_type(MADS.type_name(), 3) + sfg.set_latency_of_type(Reciprocal.type_name(), 2) + sfg.set_execution_time_of_type(MADS.type_name(), 1) + sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) + + max_concurrent_reads = "5" + with pytest.raises( + ValueError, match="Provided max_concurrent_reads must be an integer." + ): + Schedule( + sfg, + scheduler=HybridScheduler(max_concurrent_reads=max_concurrent_reads), + ) + + max_concurrent_reads = 0 + with pytest.raises( + ValueError, match="Provided max_concurrent_reads must be larger than 0." + ): + Schedule( + sfg, + scheduler=HybridScheduler(max_concurrent_reads=max_concurrent_reads), + ) + + max_concurrent_reads = -1 + with pytest.raises( + ValueError, match="Provided max_concurrent_reads must be larger than 0." + ): + Schedule( + sfg, + scheduler=HybridScheduler(max_concurrent_reads=max_concurrent_reads), + ) + + def test_invalid_input_times(self): + sfg = ldlt_matrix_inverse(N=2) + + sfg.set_latency_of_type(MADS.type_name(), 3) + sfg.set_latency_of_type(Reciprocal.type_name(), 2) + sfg.set_execution_time_of_type(MADS.type_name(), 1) + sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) + + input_times = 5 + with pytest.raises( + ValueError, match="Provided input_times must be a dictionary." + ): + Schedule(sfg, scheduler=HybridScheduler(input_times=input_times)) + + input_times = "test1" + with pytest.raises( + ValueError, match="Provided input_times must be a dictionary." + ): + Schedule(sfg, scheduler=HybridScheduler(input_times=input_times)) + + input_times = [] + with pytest.raises( + ValueError, match="Provided input_times must be a dictionary." + ): + Schedule(sfg, scheduler=HybridScheduler(input_times=input_times)) + + input_times = {3: 3} + with pytest.raises( + ValueError, match="Provided input_times keys must be strings." + ): + Schedule(sfg, scheduler=HybridScheduler(input_times=input_times)) + + input_times = {"in0": "foo"} + with pytest.raises( + ValueError, match="Provided input_times values must be integers." + ): + Schedule(sfg, scheduler=HybridScheduler(input_times=input_times)) + + input_times = {"in0": -1} + with pytest.raises( + ValueError, match="Provided input_times values must be non-negative." + ): + Schedule(sfg, scheduler=HybridScheduler(input_times=input_times)) + + def test_invalid_output_delta_times(self): + sfg = ldlt_matrix_inverse(N=2) + + sfg.set_latency_of_type(MADS.type_name(), 3) + sfg.set_latency_of_type(Reciprocal.type_name(), 2) + sfg.set_execution_time_of_type(MADS.type_name(), 1) + sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) + + output_delta_times = 10 + with pytest.raises( + ValueError, match="Provided output_delta_times must be a dictionary." + ): + Schedule( + sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times) + ) + + output_delta_times = "test2" + with pytest.raises( + ValueError, match="Provided output_delta_times must be a dictionary." + ): + Schedule( + sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times) + ) + + output_delta_times = [] + with pytest.raises( + ValueError, match="Provided output_delta_times must be a dictionary." + ): + Schedule( + sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times) + ) + + output_delta_times = {4: 4} + with pytest.raises( + ValueError, match="Provided output_delta_times keys must be strings." + ): + Schedule( + sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times) + ) + + output_delta_times = {"out0": "foo"} + with pytest.raises( + ValueError, match="Provided output_delta_times values must be integers." + ): + Schedule( + sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times) + ) + + output_delta_times = {"out0": -1} + with pytest.raises( + ValueError, match="Provided output_delta_times values must be non-negative." + ): + Schedule( + sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times) + ) + + def test_resource_not_in_sfg(self): + sfg = ldlt_matrix_inverse(N=3) + + sfg.set_latency_of_type(MADS.type_name(), 3) + sfg.set_latency_of_type(Reciprocal.type_name(), 2) + sfg.set_execution_time_of_type(MADS.type_name(), 1) + sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) + + resources = { + MADS.type_name(): 1, + Reciprocal.type_name(): 1, + Addition.type_name(): 2, + } + with pytest.raises( + ValueError, + match="Provided max resource of type add cannot be found in the provided SFG.", + ): + Schedule(sfg, scheduler=HybridScheduler(resources)) + + def test_input_not_in_sfg(self): + sfg = ldlt_matrix_inverse(N=2) + + sfg.set_latency_of_type(MADS.type_name(), 3) + sfg.set_latency_of_type(Reciprocal.type_name(), 2) + sfg.set_execution_time_of_type(MADS.type_name(), 1) + sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) + + input_times = {"in100": 4} + with pytest.raises( + ValueError, + match="Provided input time with GraphID in100 cannot be found in the provided SFG.", + ): + Schedule(sfg, scheduler=HybridScheduler(input_times=input_times)) + + def test_output_not_in_sfg(self): + sfg = ldlt_matrix_inverse(N=2) + + sfg.set_latency_of_type(MADS.type_name(), 3) + sfg.set_latency_of_type(Reciprocal.type_name(), 2) + sfg.set_execution_time_of_type(MADS.type_name(), 1) + sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) + + output_delta_times = {"out90": 2} + with pytest.raises( + ValueError, + match="Provided output delta time with GraphID out90 cannot be found in the provided SFG.", + ): + Schedule( + sfg, scheduler=HybridScheduler(output_delta_times=output_delta_times) + ) + def test_ldlt_inverse_3x3_read_and_write_constrained(self): sfg = ldlt_matrix_inverse(N=3) @@ -886,27 +1115,6 @@ class TestHybridScheduler: assert mem_vars.read_ports_bound() == 3 assert mem_vars.write_ports_bound() == 1 - def test_read_constrained_too_tight(self): - sfg = ldlt_matrix_inverse(N=2) - - sfg.set_latency_of_type(MADS.type_name(), 3) - sfg.set_latency_of_type(Reciprocal.type_name(), 2) - sfg.set_execution_time_of_type(MADS.type_name(), 1) - sfg.set_execution_time_of_type(Reciprocal.type_name(), 1) - - resources = {MADS.type_name(): 1, Reciprocal.type_name(): 1} - with pytest.raises( - TimeoutError, - match="Algorithm did not manage to schedule any operation for 10 time steps, try relaxing the constraints.", - ): - Schedule( - sfg, - scheduler=HybridScheduler( - max_resources=resources, - max_concurrent_reads=2, - ), - ) - def test_32_point_fft_custom_io_times(self): POINTS = 32 sfg = radix_2_dif_fft(POINTS) @@ -930,10 +1138,7 @@ class TestHybridScheduler: for i in range(POINTS): assert schedule.start_times[f"in{i}"] == i - assert ( - schedule.start_times[f"out{i}"] - == schedule.get_max_non_io_end_time() + i - ) + assert schedule.start_times[f"out{i}"] == 95 + i # Too slow for pipeline right now # def test_64_point_fft_custom_io_times(self): @@ -989,7 +1194,13 @@ class TestHybridScheduler: for i in range(POINTS): assert schedule.start_times[f"in{i}"] == i - assert schedule.start_times[f"out{i}"] == 96 if i == 0 else i + if i == 0: + expected_value = 95 + elif i == 1: + expected_value = 96 + else: + expected_value = i - 1 + assert schedule.start_times[f"out{i}"] == expected_value def test_cyclic_scheduling(self): sfg = radix_2_dif_fft(points=4) @@ -1311,3 +1522,31 @@ class TestHybridScheduler: 's3': 0, } assert schedule.schedule_time == 4 + + def test_invalid_output_delta_time(self): + sfg = radix_2_dif_fft(points=4) + + sfg.set_latency_of_type(Butterfly.type_name(), 1) + sfg.set_latency_of_type(ConstantMultiplication.type_name(), 3) + sfg.set_execution_time_of_type(Butterfly.type_name(), 1) + sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1) + + resources = { + Butterfly.type_name(): 1, + ConstantMultiplication.type_name(): 1, + Input.type_name(): 2, + Output.type_name(): 2, + } + output_delta_times = {"out0": 0, "out1": 1, "out2": 2, "out3": 3} + + with pytest.raises( + ValueError, + match="Cannot place output out2 at time 6 for scheduling time 5. Try to relax the scheduling time, change the output delta times or enable cyclic.", + ): + Schedule( + sfg, + scheduler=HybridScheduler( + resources, output_delta_times=output_delta_times + ), + schedule_time=5, + ) -- GitLab