From 9207170caaffbf597106d799f88e0c3c39bac8c0 Mon Sep 17 00:00:00 2001 From: Simon Bjurek <simbj106@student.liu.se> Date: Wed, 12 Mar 2025 08:48:23 +0000 Subject: [PATCH] Recursive loops now seperate sfg property, fixed 0 input in Division and Reciprocal etc --- b_asic/core_operations.py | 4 + b_asic/schedule.py | 46 ++++++++- b_asic/scheduler_gui/scheduler_item.py | 13 ++- b_asic/signal_flow_graph.py | 82 +++++++++------ examples/scheduling_pipelining_retiming.py | 111 +++++++++++++++++++++ test/unit/test_core_operations.py | 9 ++ test/unit/test_list_schedulers.py | 42 ++++++++ test/unit/test_sfg.py | 21 ++++ 8 files changed, 297 insertions(+), 31 deletions(-) create mode 100644 examples/scheduling_pipelining_retiming.py diff --git a/b_asic/core_operations.py b/b_asic/core_operations.py index 1928e81a..faf5d31d 100644 --- a/b_asic/core_operations.py +++ b/b_asic/core_operations.py @@ -523,6 +523,8 @@ class Division(AbstractOperation): return TypeName("div") def evaluate(self, a, b): + if b == 0: + return float("inf") return a / b @property @@ -1372,6 +1374,8 @@ class Reciprocal(AbstractOperation): return TypeName("rec") def evaluate(self, a): + if a == 0: + return float("inf") return 1 / a diff --git a/b_asic/schedule.py b/b_asic/schedule.py index 6dfe5809..7658fdb3 100644 --- a/b_asic/schedule.py +++ b/b_asic/schedule.py @@ -365,10 +365,10 @@ class Schedule: 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 + available_time -= self._schedule_time * self._laps[signal.graph_id] else: available_time = ( cast(int, source.latency_offset) @@ -458,6 +458,26 @@ class Schedule: raise ValueError( f"New schedule time ({time}) too short, minimum: {max_end_time}." ) + + # if updating the scheduling time -> update laps due to operations + # reading and writing in different iterations (across the edge) + if self._schedule_time is not None: + for signal_id in self._laps.keys(): + port = self._sfg.find_by_id(signal_id).destination + + source_port = port.signals[0].source + source_op = source_port.operation + source_port_start_time = self._start_times[source_op.graph_id] + source_port_latency_offset = source_op.latency_offsets[ + f"out{source_port.index}" + ] + if ( + source_port_start_time + source_port_latency_offset + > self._schedule_time + and source_port_start_time + source_port_latency_offset <= time + ): + self._laps[signal_id] += 1 + self._schedule_time = time return self @@ -855,6 +875,13 @@ class Schedule: ): new_start = self._schedule_time self._laps[op.input(0).signals[0].graph_id] -= 1 + if ( + new_start == 0 + and isinstance(op, Input) + and self._laps[op.output(0).signals[0].graph_id] != 0 + ): + new_start = 0 + self._laps[op.output(0).signals[0].graph_id] -= 1 # Set new start time self._start_times[graph_id] = new_start return self @@ -936,7 +963,24 @@ class Schedule: destination_laps = [] for signal_id, lap in self._laps.items(): port = new_sfg.find_by_id(signal_id).destination + + # if an operation lies across the scheduling time, place a delay after it + source_port = port.signals[0].source + source_op = source_port.operation + source_port_start_time = self._start_times[source_op.graph_id] + source_port_latency_offset = source_op.latency_offsets[ + f"out{source_port.index}" + ] + if ( + source_port_start_time + source_port_latency_offset + > self._schedule_time + ): + lap += ( + source_port_start_time + source_port_latency_offset + ) // self._schedule_time + destination_laps.append((port.operation.graph_id, port.index, lap)) + for op, port, lap in destination_laps: for delays in range(lap): new_sfg = new_sfg.insert_operation_before(op, Delay(), port) diff --git a/b_asic/scheduler_gui/scheduler_item.py b/b_asic/scheduler_gui/scheduler_item.py index fbb84fcd..56a6e7fb 100644 --- a/b_asic/scheduler_gui/scheduler_item.py +++ b/b_asic/scheduler_gui/scheduler_item.py @@ -28,6 +28,7 @@ from b_asic.scheduler_gui.axes_item import AxesItem from b_asic.scheduler_gui.operation_item import OperationItem from b_asic.scheduler_gui.scheduler_event import SchedulerEvent from b_asic.scheduler_gui.signal_item import SignalItem +from b_asic.special_operations import Output from b_asic.types import GraphID @@ -121,7 +122,10 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): raise ValueError("No schedule installed.") new_start_time = floor(pos) - floor(self._x_axis_indent) slacks = self.schedule.slacks(item.graph_id) - op_start_time = self.schedule.start_time_of_operation(item.graph_id) + op_start_time = ( + self.schedule.start_time_of_operation(item.graph_id) + % self.schedule.schedule_time + ) if not -slacks[0] <= new_start_time - op_start_time <= slacks[1]: # Cannot move due to dependencies return False @@ -245,6 +249,13 @@ class SchedulerItem(SchedulerEvent, QGraphicsItemGroup): op_start_time = self.schedule.start_time_of_operation(item.graph_id) new_start_time = floor(pos) - floor(self._x_axis_indent) move_time = new_start_time - op_start_time + op = self._schedule.sfg.find_by_id(item.graph_id) + if ( + isinstance(op, Output) + and op_start_time == self.schedule.schedule_time + and new_start_time < self.schedule.schedule_time + ): + move_time = new_start_time if move_time: self.schedule.move_operation(item.graph_id, move_time) print(f"schedule.move_operation({item.graph_id!r}, {move_time})") diff --git a/b_asic/signal_flow_graph.py b/b_asic/signal_flow_graph.py index c0c71483..de1133ba 100644 --- a/b_asic/signal_flow_graph.py +++ b/b_asic/signal_flow_graph.py @@ -1767,13 +1767,52 @@ class SFG(AbstractOperation): ------- The iteration period bound. """ + loops = self.loops + if not loops: + return -1 + + op_and_latency = {} + for op in self.operations: + for loop in loops: + for element in loop: + if op.type_name() not in op_and_latency: + op_and_latency[op.type_name()] = op.latency + t_l_values = [] + + for loop in loops: + loop.pop() + time_of_loop = 0 + number_of_t_in_loop = 0 + for element in loop: + if ''.join([i for i in element if not i.isdigit()]) == 't': + number_of_t_in_loop += 1 + for key, item in op_and_latency.items(): + if key in element: + time_of_loop += item + if number_of_t_in_loop in (0, 1): + t_l_values.append(Fraction(time_of_loop, 1)) + else: + t_l_values.append(Fraction(time_of_loop, number_of_t_in_loop)) + return max(t_l_values) + + @property + def loops(self) -> list[list[GraphID]]: + """ + Return the recursive loops found in the SFG. + + If -1, the SFG does not have any loops. + + Returns + ------- + A list of the recursive loops. + """ inputs_used = [] for used_input in self._used_ids: if 'in' in str(used_input): used_input = used_input.replace("in", "") inputs_used.append(int(used_input)) if inputs_used == []: - raise ValueError("No inputs to sfg") + return [] for input in inputs_used: input_op = self._input_operations[input] queue: Deque[Operation] = deque([input_op]) @@ -1795,39 +1834,24 @@ class SFG(AbstractOperation): visited.add(new_op) else: raise ValueError("Destination does not exist") - if not dict_of_sfg: - raise ValueError( - "the SFG does not have any loops and therefore no iteration period bound." - ) cycles = [ [node] + path for node in dict_of_sfg for path in self._dfs(dict_of_sfg, node, node) ] - if not cycles: - return -1 - op_and_latency = {} - for op in self.operations: - for lista in cycles: - for element in lista: - if op.type_name() not in op_and_latency: - op_and_latency[op.type_name()] = op.latency - t_l_values = [] - for loop in cycles: - loop.pop() - time_of_loop = 0 - number_of_t_in_loop = 0 - for element in loop: - if ''.join([i for i in element if not i.isdigit()]) == 't': - number_of_t_in_loop += 1 - for key, item in op_and_latency.items(): - if key in element: - time_of_loop += item - if number_of_t_in_loop in (0, 1): - t_l_values.append(Fraction(time_of_loop, 1)) - else: - t_l_values.append(Fraction(time_of_loop, number_of_t_in_loop)) - return max(t_l_values) + + loops = self._get_non_redundant_cycles(cycles) + return loops + + def _get_non_redundant_cycles(self, loops): + unique_lists = [] + seen_cycles = set() + for loop in loops: + operation_set = frozenset(loop) + if operation_set not in seen_cycles: + unique_lists.append(loop) + seen_cycles.add(operation_set) + return unique_lists def state_space_representation(self): """ diff --git a/examples/scheduling_pipelining_retiming.py b/examples/scheduling_pipelining_retiming.py new file mode 100644 index 00000000..38a9601f --- /dev/null +++ b/examples/scheduling_pipelining_retiming.py @@ -0,0 +1,111 @@ +""" +========================================= +Scheduling and Pipelining/Retiming +========================================= + +When scheduling cyclically (modulo) there is implicit pipelining/retiming taking place. +B-ASIC can easily be used to showcase this. +""" + +import matplotlib.pyplot as plt +import numpy as np +from scipy import signal + +from b_asic.core_operations import Addition, ConstantMultiplication +from b_asic.schedule import Schedule +from b_asic.scheduler import ALAPScheduler +from b_asic.sfg_generators import direct_form_1_iir +from b_asic.signal_generator import Impulse +from b_asic.simulation import Simulation + +# %% +# Design a simple direct form IIR low-pass filter. +N = 3 +Wc = 0.2 +b, a = signal.butter(N, Wc, btype="lowpass", output="ba") + +# %% +# Generate the corresponding signal-flow-graph (SFG). +sfg = direct_form_1_iir(b, a) +sfg + +# %% +# Set latencies and execution times of the operations. +sfg.set_latency_of_type(Addition.type_name(), 1) +sfg.set_latency_of_type(ConstantMultiplication.type_name(), 3) +sfg.set_execution_time_of_type(Addition.type_name(), 1) +sfg.set_execution_time_of_type(ConstantMultiplication.type_name(), 1) + +# %% +# Print the critical path Tcp and the iteration period bound Tmin. +T_cp = sfg.critical_path_time() +print("Tcp:", T_cp) +T_min = sfg.iteration_period_bound() +print("Tmin:", T_min) + +# %% +# Create an ALAP schedule +schedule = Schedule(sfg, scheduler=ALAPScheduler(), cyclic=True) +schedule.show() + +# %% +# Move some operations "over the edge" in order to reach Tcp = Tmin. +schedule.move_operation('out0', 2) +schedule.move_operation('add2', 2) +schedule.move_operation('add0', 2) +schedule.move_operation('add3', 2) +schedule.set_schedule_time(5) +schedule.show() + +# %% +# Print the new critical path Tcp that is now equal to Tmin. +T_cp = schedule.sfg.critical_path_time() +print("Tcp:", T_cp) +T_min = schedule.sfg.iteration_period_bound() +print("Tmin:", T_min) + +# %% +# Show the reconstructed SFG that is now pipelined/retimed compared to the original. +schedule.sfg + +# %% +# Simulate the impulse response of the original and reconstructed SFGs. +# Plot the frequency responses of the original filter, the original SFG and the reconstructed SFG to verify +# that the schedule is valid. +sim1 = Simulation(sfg, [Impulse()]) +sim1.run_for(1000) + +sim2 = Simulation(schedule.sfg, [Impulse()]) +sim2.run_for(1000) + +w, h = signal.freqz(b, a) + +# Plot 1: Original filter +spectrum_0 = 20 * np.log10(np.abs(h)) +plt.figure() +plt.plot(w / np.pi, spectrum_0) +plt.title("Original filter") +plt.xlabel("Normalized frequency (x pi rad/sample)") +plt.ylabel("Magnitude (dB)") +plt.grid(True) +plt.show() + +# Plot 2: Simulated SFG +spectrum_1 = 20 * np.log10(np.abs(signal.freqz(sim1.results['0'])[1])) +plt.figure() +plt.plot(w / np.pi, spectrum_1) +plt.title("Simulated SFG") +plt.xlabel("Normalized frequency (x pi rad/sample)") +plt.ylabel("Magnitude (dB)") +plt.grid(True) +plt.show() + +# Plot 3: Recreated SFG +spectrum_2 = 20 * np.log10(np.abs(signal.freqz(sim2.results['0'])[1])) +plt.figure() +plt.plot(w / np.pi, spectrum_2) +plt.title("Pipelined/retimed SFG") +plt.xlabel("Normalized frequency (x pi rad/sample)") +plt.ylabel("Magnitude (dB)") +plt.grid(True) +plt.show() diff --git a/test/unit/test_core_operations.py b/test/unit/test_core_operations.py index e8241e63..3e2230cc 100644 --- a/test/unit/test_core_operations.py +++ b/test/unit/test_core_operations.py @@ -185,6 +185,11 @@ class TestDivision: test_operation = Division(Addition(Input(), Constant(3)), Constant(3)) assert test_operation.is_linear + def test_zero_input(self): + test_operation = Division() + assert test_operation.evaluate_output(0, [0, 1]) == 0 + assert test_operation.evaluate_output(0, [1, 0]) == float("inf") + class TestSquareRoot: """Tests for SquareRoot class.""" @@ -577,6 +582,10 @@ class TestReciprocal: test_operation = Reciprocal() assert test_operation.evaluate_output(0, [1 + 1j]) == 0.5 - 0.5j + def test_zero_input(self): + test_operation = Reciprocal() + assert test_operation.evaluate_output(0, [0]) == float("inf") + class TestDepends: def test_depends_addition(self): diff --git a/test/unit/test_list_schedulers.py b/test/unit/test_list_schedulers.py index 34daa4d8..d788abd2 100644 --- a/test/unit/test_list_schedulers.py +++ b/test/unit/test_list_schedulers.py @@ -468,6 +468,7 @@ class TestMaxFanOutScheduler: "out0": 36, } assert schedule.schedule_time == 36 + _validate_recreated_sfg_ldlt_matrix_inverse(schedule, 3) class TestHybridScheduler: @@ -829,6 +830,7 @@ class TestHybridScheduler: "out0": 16, } assert schedule.schedule_time == 16 + _validate_recreated_sfg_ldlt_matrix_inverse(schedule, 2) def test_ldlt_inverse_2x2_specified_IO_times_cyclic(self): sfg = ldlt_matrix_inverse(N=2) @@ -876,6 +878,21 @@ class TestHybridScheduler: } assert schedule.schedule_time == 16 + # validate regenerated sfg with random 2x2 real s.p.d. matrix + A = np.random.rand(2, 2) + A = np.dot(A, A.T) + A_inv = np.linalg.inv(A) + input_signals = [] + for i in range(2): + for j in range(i, 2): + input_signals.append(Constant(A[i, j])) + + sim = Simulation(schedule.sfg, input_signals) + sim.run_for(2) + assert np.allclose(sim.results["0"], [A_inv[0, 0], A_inv[0, 0]]) + assert np.allclose(sim.results["1"], [0, A_inv[0, 1]]) + assert np.allclose(sim.results["2"], [0, A_inv[1, 1]]) + def test_invalid_max_resources(self): sfg = ldlt_matrix_inverse(N=2) @@ -1157,6 +1174,7 @@ class TestHybridScheduler: direct, mem_vars = schedule.get_memory_variables().split_on_length() assert mem_vars.read_ports_bound() == 3 assert mem_vars.write_ports_bound() == 1 + _validate_recreated_sfg_ldlt_matrix_inverse(schedule, 3) def test_32_point_fft_custom_io_times(self): POINTS = 32 @@ -1672,6 +1690,7 @@ class TestHybridScheduler: } assert all([val == 0 for val in schedule.laps.values()]) + _validate_recreated_sfg_ldlt_matrix_inverse(schedule, 3) def test_latency_offsets_cyclic(self): sfg = ldlt_matrix_inverse( @@ -1953,3 +1972,26 @@ def _validate_recreated_sfg_fft(schedule: Schedule, points: int) -> None: a = res[str(i)] b = exp_res[i] assert np.isclose(a, b) + + +def _validate_recreated_sfg_ldlt_matrix_inverse(schedule: Schedule, N: int) -> None: + # random real s.p.d matrix + A = np.random.rand(N, N) + A = np.dot(A, A.T) + + # iterate through the upper diagonal and construct the input to the SFG + input_signals = [] + for i in range(N): + for j in range(i, N): + input_signals.append(Constant(A[i, j])) + + A_inv = np.linalg.inv(A) + sim = Simulation(schedule.sfg, input_signals) + sim.run_for(1) + + # iterate through the upper diagonal and check + count = 0 + for i in range(N): + for j in range(i, N): + assert np.isclose(sim.results[str(count)], A_inv[i, j]) + count += 1 diff --git a/test/unit/test_sfg.py b/test/unit/test_sfg.py index 471a8bde..090a11dd 100644 --- a/test/unit/test_sfg.py +++ b/test/unit/test_sfg.py @@ -1913,6 +1913,27 @@ class TestIterationPeriodBound: assert sfg_two_inputs_two_outputs.iteration_period_bound() == -1 +class TestLoops: + def test_accumulator(self, sfg_simple_accumulator): + loops = sfg_simple_accumulator.loops + assert loops == [["add0", "t0", "add0"]] + + def test_simple_filter(self, sfg_simple_filter): + loops = sfg_simple_filter.loops + assert loops == [["add0", "t0", "cmul0", "add0"]] + + def test_direct_form_iir_filter(self, sfg_direct_form_iir_lp_filter): + loops = sfg_direct_form_iir_lp_filter.loops + assert loops == [ + ["add0", "t0", "cmul4", "add1", "add0"], + ["add0", "t0", "t1", "cmul3", "add1", "add0"], + ] + + def test_empty_sfg(self): + loops = SFG([], []).loops + assert loops == [] + + class TestStateSpace: def test_accumulator(self, sfg_simple_accumulator): ss = sfg_simple_accumulator.state_space_representation() -- GitLab