From cf8a00c5b85fd557178e08d720cd34c09513f4fc Mon Sep 17 00:00:00 2001 From: Simon Bjurek <simbj106@student.liu.se> Date: Mon, 3 Mar 2025 15:15:57 +0000 Subject: [PATCH] Misc fixes, added method for getting min resources, now checks iteration bound --- b_asic/schedule.py | 5 +- b_asic/scheduler.py | 61 +++++++++++-------- b_asic/signal_flow_graph.py | 27 ++++++++ .../auto_scheduling_with_custom_io_times.py | 26 +++++--- test/unit/test_list_schedulers.py | 48 +++++++++++++++ test/unit/test_sfg.py | 57 +++++++++++++++++ 6 files changed, 192 insertions(+), 32 deletions(-) diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 2c3910de..c62c5f2e 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -29,6 +29,7 @@ from b_asic._preferences import ( SIGNAL_LINEWIDTH, SPLINE_OFFSET, ) +from b_asic.core_operations import DontCare from b_asic.graph_component import GraphID from b_asic.operation import Operation from b_asic.port import InputPort, OutputPort @@ -349,7 +350,9 @@ class Schedule: usage_time = start_time + cast(int, input_port.latency_offset) for signal in input_port.signals: source = cast(OutputPort, signal.source) - if source.operation.graph_id.startswith("dontcare"): + if isinstance(source.operation, DontCare): + available_time = 0 + elif isinstance(source.operation, Delay): available_time = 0 else: if self._schedule_time is not None: diff --git a/b_asic/scheduler.py b/b_asic/scheduler.py index 0e9fa628..89edd46b 100644 --- a/b_asic/scheduler.py +++ b/b_asic/scheduler.py @@ -1,7 +1,6 @@ import copy import sys from abc import ABC, abstractmethod -from math import ceil from typing import TYPE_CHECKING, cast import b_asic.logger as logger @@ -294,18 +293,21 @@ class ListScheduler(Scheduler, ABC): 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(): - total_exec_time = sum( - [op.execution_time for op in self._sfg.find_by_type_name(resource_type)] - ) - if self._schedule.schedule_time is not None: - resource_lower_bound = ceil( - total_exec_time / self._schedule.schedule_time + if self._schedule._cyclic: + if self._schedule.schedule_time is None: + raise ValueError("Scheduling time must be provided when cyclic = True.") + iteration_period_bound = self._sfg.iteration_period_bound() + if self._schedule.schedule_time < iteration_period_bound: + raise ValueError( + f"Provided scheduling time {self._schedule.schedule_time} must be larger or equal to the" + f" iteration period bound: {iteration_period_bound}." ) - if resource_amount < resource_lower_bound: + + if self._schedule.schedule_time is not None: + for resource_type, resource_amount in self._max_resources.items(): + if resource_amount < self._sfg.resource_lower_bound( + resource_type, self._schedule.schedule_time + ): raise ValueError( f"Amount of resource: {resource_type} is not enough to " f"realize schedule for scheduling time: {self._schedule.schedule_time}." @@ -447,6 +449,7 @@ class ListScheduler(Scheduler, ABC): for op_id in self._remaining_ops if self._op_is_schedulable(self._sfg.find_by_id(op_id)) ] + memory_reads = self._calculate_memory_reads(ready_ops) return [ ( @@ -454,6 +457,7 @@ class ListScheduler(Scheduler, ABC): self._deadlines[op_id], self._output_slacks[op_id], self._fan_outs[op_id], + memory_reads[op_id], ) for op_id in ready_ops ] @@ -479,6 +483,27 @@ class ListScheduler(Scheduler, ABC): for op_id, start_time in alap_start_times.items() } + def _calculate_memory_reads( + self, ready_ops: list["GraphID"] + ) -> dict["GraphID", int]: + op_reads = {} + for op_id in ready_ops: + reads = 0 + for op_input in self._sfg.find_by_id(op_id).inputs: + source_op = op_input.signals[0].source.operation + if isinstance(source_op, DontCare): + continue + if isinstance(source_op, Delay): + reads += 1 + continue + if ( + self._schedule.start_times[source_op.graph_id] + != self._current_time - 1 + ): + reads += 1 + op_reads[op_id] = reads + return op_reads + def _op_satisfies_resource_constraints(self, op: "Operation") -> bool: if self._schedule.schedule_time is not None: time_slot = self._current_time % self._schedule.schedule_time @@ -580,18 +605,6 @@ class ListScheduler(Scheduler, ABC): if time == new_time and op_id.startswith("out"): count += 1 - self._remaining_resources = self._max_resources - self._remaining_resources[Output.type_name()] -= count - - self._current_time = new_time - if not self._op_is_schedulable(output): - raise ValueError( - "Cannot schedule outputs according to the provided output_delta_times. " - f"Failed output: {output.graph_id}, " - f"at time: { self._schedule.start_times[output.graph_id]}, " - "try relaxing the constraints." - ) - modulo_time = ( new_time % self._schedule.schedule_time if self._schedule.schedule_time diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index abc380ec..839536b2 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -10,6 +10,7 @@ import warnings from collections import defaultdict, deque from collections.abc import Iterable, MutableSet, Sequence from io import StringIO +from math import ceil from numbers import Number from queue import PriorityQueue from typing import ( @@ -1729,6 +1730,32 @@ class SFG(AbstractOperation): continue fringe.append((next_state, path + [next_state])) + def resource_lower_bound(self, type_name: str, schedule_time: int) -> int: + """Return the lowest amount of resources of the given type needed to reach the scheduling time. + + Parameters + ---------- + type_name : str + Type name of the given resource. + schedule_time : int + Scheduling time to evaluate for. + """ + ops = self.find_by_type_name(type_name) + if not ops: + return 0 + if schedule_time <= 0: + raise ValueError( + f"Schedule time must be positive, current schedule time is: {schedule_time}." + ) + exec_times = [op.execution_time for op in ops] + if any(time is None for time in exec_times): + raise ValueError( + f"Execution times not set for all operations of type {type_name}." + ) + + total_exec_time = sum([op.execution_time for op in ops]) + return ceil(total_exec_time / schedule_time) + def iteration_period_bound(self) -> int: """ Return the iteration period bound of the SFG. diff --git a/examples/auto_scheduling_with_custom_io_times.py b/examples/auto_scheduling_with_custom_io_times.py index 2064b9ec..f85710a0 100644 --- a/examples/auto_scheduling_with_custom_io_times.py +++ b/examples/auto_scheduling_with_custom_io_times.py @@ -15,7 +15,7 @@ points = 8 sfg = radix_2_dif_fft(points=points) # %% -# The SFG is +# The SFG is: sfg # %% @@ -26,7 +26,7 @@ sfg.set_execution_time_of_type(Butterfly.type_name(), 1) sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1) # %% -# Generate an ASAP schedule for reference +# Generate an ASAP schedule for reference. schedule = Schedule(sfg, scheduler=ASAPScheduler()) schedule.show() @@ -46,8 +46,7 @@ schedule = Schedule( schedule.show() # %% -# Generate a new Schedule with cyclic scheduling enabled -output_delta_times = {f"out{i}": i for i in range(points)} +# Generate a new Schedule with cyclic scheduling enabled. schedule = Schedule( sfg, scheduler=HybridScheduler( @@ -61,8 +60,7 @@ schedule = Schedule( schedule.show() # %% -# Generate a new Schedule with even less scheduling time -output_delta_times = {f"out{i}": i + 1 for i in range(points)} +# Generate a new Schedule with even less scheduling time. schedule = Schedule( sfg, scheduler=HybridScheduler( @@ -76,7 +74,21 @@ schedule = Schedule( schedule.show() # %% -# Try scheduling for 12 cycles, which gives full butterfly usage +# Try scheduling for 12 cycles, which gives full butterfly usage. +schedule = Schedule( + sfg, + scheduler=HybridScheduler( + resources, + input_times=input_times, + output_delta_times=output_delta_times, + ), + schedule_time=12, + cyclic=True, +) +schedule.show() + +# %% +# Push output times one step to prevent lap for out3. output_delta_times = {f"out{i}": i + 2 for i in range(points)} schedule = Schedule( sfg, diff --git a/test/unit/test_list_schedulers.py b/test/unit/test_list_schedulers.py index fa349f87..b7cd272a 100644 --- a/test/unit/test_list_schedulers.py +++ b/test/unit/test_list_schedulers.py @@ -1395,6 +1395,30 @@ class TestHybridScheduler: schedule_time=5, ) + sfg = radix_2_dif_fft(points=8) + + 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, + } + with pytest.raises( + ValueError, + match="Amount of resource: bfly is not enough to realize schedule for scheduling time: 6.", + ): + Schedule( + sfg, + scheduler=HybridScheduler( + resources, max_concurrent_reads=2, max_concurrent_writes=2 + ), + schedule_time=6, + cyclic=True, + ) + def test_scheduling_time_not_enough(self): sfg = ldlt_matrix_inverse(N=3) @@ -1550,3 +1574,27 @@ class TestHybridScheduler: ), schedule_time=5, ) + + def test_iteration_period_bound(self): + sfg = direct_form_1_iir([1, 2, 3], [1, 2, 3]) + + sfg.set_latency_of_type(ConstantMultiplication.type_name(), 2) + sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1) + sfg.set_latency_of_type(Addition.type_name(), 3) + sfg.set_execution_time_of_type(Addition.type_name(), 1) + + resources = { + Addition.type_name(): 1, + ConstantMultiplication.type_name(): 1, + } + + with pytest.raises( + ValueError, + match="Provided scheduling time 5 must be larger or equal to the iteration period bound: 8.", + ): + Schedule( + sfg, + scheduler=EarliestDeadlineScheduler(max_resources=resources), + schedule_time=5, + cyclic=True, + ) diff --git a/test/unit/test_sfg.py b/test/unit/test_sfg.py index bc4e93a9..d042d071 100644 --- a/test/unit/test_sfg.py +++ b/test/unit/test_sfg.py @@ -1811,6 +1811,63 @@ class TestInsertDelays: assert source4.type_name() == bfly.type_name() +class TestResourceLowerBound: + def test_empty_sfg(self): + sfg = SFG() + assert sfg.resource_lower_bound("add", 2) == 0 + assert sfg.resource_lower_bound("cmul", 1000) == 0 + + def test_type_not_in_sfg(self, sfg_simple_accumulator): + sfg_simple_accumulator.resource_lower_bound("bfly", 2) == 0 + sfg_simple_accumulator.resource_lower_bound("bfly", 1000) == 0 + + def test_negative_schedule_time(self, precedence_sfg_delays): + precedence_sfg_delays.set_latency_of_type("add", 2) + precedence_sfg_delays.set_latency_of_type("cmul", 3) + precedence_sfg_delays.set_execution_time_of_type("add", 1) + precedence_sfg_delays.set_execution_time_of_type("cmul", 1) + + with pytest.raises( + ValueError, + match="Schedule time must be positive, current schedule time is: 0.", + ): + precedence_sfg_delays.resource_lower_bound("add", 0) + + with pytest.raises( + ValueError, + match="Schedule time must be positive, current schedule time is: -1.", + ): + precedence_sfg_delays.resource_lower_bound("cmul", -1) + + def test_accumulator(self, sfg_simple_accumulator): + sfg_simple_accumulator.set_latency_of_type('add', 2) + + with pytest.raises( + ValueError, + match="Execution times not set for all operations of type add.", + ): + sfg_simple_accumulator.resource_lower_bound("add", 2) + + sfg_simple_accumulator.set_execution_time_of_type("add", 2) + assert sfg_simple_accumulator.resource_lower_bound("add", 2) == 1 + assert sfg_simple_accumulator.resource_lower_bound("add", 1) == 2 + + def test_secondorder_iir(self, precedence_sfg_delays): + precedence_sfg_delays.set_latency_of_type("add", 2) + precedence_sfg_delays.set_latency_of_type("cmul", 3) + + precedence_sfg_delays.set_execution_time_of_type("add", 1) + assert precedence_sfg_delays.resource_lower_bound("add", 1) == 4 + assert precedence_sfg_delays.resource_lower_bound("add", 2) == 2 + assert precedence_sfg_delays.resource_lower_bound("add", 4) == 1 + + precedence_sfg_delays.set_execution_time_of_type("cmul", 1) + assert precedence_sfg_delays.resource_lower_bound("cmul", 1) == 7 + assert precedence_sfg_delays.resource_lower_bound("cmul", 2) == 4 + assert precedence_sfg_delays.resource_lower_bound("cmul", 4) == 2 + assert precedence_sfg_delays.resource_lower_bound("cmul", 7) == 1 + + class TestIterationPeriodBound: def test_accumulator(self, sfg_simple_accumulator): sfg_simple_accumulator.set_latency_of_type('add', 2) -- GitLab