diff --git a/elasticai/creator/nn/binary/mac/__init__.py b/elasticai/creator/nn/binary/mac/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/elasticai/creator/nn/binary/mac/_mac_test.py b/elasticai/creator/nn/binary/mac/_mac_test.py new file mode 100644 index 00000000..0c48ffd5 --- /dev/null +++ b/elasticai/creator/nn/binary/mac/_mac_test.py @@ -0,0 +1,49 @@ +import pytest +import torch + +from elasticai.creator.file_generation.on_disk_path import OnDiskPath +from elasticai.creator.vhdl.ghdl_simulation import GHDLSimulation + +from .layer import MacLayer + +sw_function_test_data = [ + ((1, 1, 1), (-1, 1, -1), -1), + ((1, -1, -1, 1, 1), (1, -1, 1, -1, 1), 1), +] + + +@pytest.mark.parametrize(["x1", "x2", "expected"], sw_function_test_data) +def test_sw_function(x1, x2, expected): + x1 = torch.tensor((1, 1, 1), dtype=torch.float32) + x2 = torch.tensor((-1, 1, -1), dtype=torch.float32) + mac = MacLayer(vector_width=x1.shape[0]) + expected = -1 + y = mac(x1, x2).item() + assert expected == y + + +test_data = [ + # 00010 * 00010 -> 00000 00100 -> 000(00 001)00 -> 00001 + ((1, 1, 1), (1, 1, 1)), + # 00010 * 01000 -> 00000 10000 -> 000(00 100)00 -> 00100 + ((-1, 1), (-1, -1)), + ((-1, -1, 1), (1, 1, 1)), +] + + +@pytest.mark.simulation +@pytest.mark.parametrize(["x1", "x2"], test_data) +def test_mac_hw_for_integers(tmp_path, x1, x2): + root_dir_path = str(tmp_path) + root = OnDiskPath("main", parent=root_dir_path) + mac = MacLayer(vector_width=len(x1)) + test_bench_name = "testbench_fxp_mac" + y = mac(torch.tensor(x1), torch.tensor(x2)).item() + testbench = mac.create_testbench(test_bench_name) + testbench.set_inputs(x1=x1, x2=x2) + testbench.save_to(root) + runner = GHDLSimulation(workdir=f"{root_dir_path}", top_design=testbench) + runner.initialize() + runner.run() + actual = testbench.parse_reported_content(runner.getReportedContent()) + assert y == actual diff --git a/elasticai/creator/nn/binary/mac/bin_mac.vhd b/elasticai/creator/nn/binary/mac/bin_mac.vhd new file mode 100644 index 00000000..b7c2a1eb --- /dev/null +++ b/elasticai/creator/nn/binary/mac/bin_mac.vhd @@ -0,0 +1,40 @@ +library IEEE; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; + +entity $name is + port ( + reset : in std_logic; + next_sample : in std_logic; + x1 : in std_logic_vector($total_width-1 downto 0); + x2 : in std_logic_vector($total_width-1 downto 0); + sum : out std_logic := '0'; + done : out std_logic := '0' + ); +end; + +architecture rtl of $name is + function popcount(x: std_logic_vector($total_width-1 downto 0)) return integer is + variable num_ones : natural := 0; + variable result : integer range -${total_width} to ${total_width} := 0; + begin + for i in x'range loop + if x(i) = '1' then + num_ones := num_ones + 1; + end if; + end loop; + result := 2*num_ones - $total_width; + return result; + end function; + + function is_positive(x: integer) return std_logic is + variable result : std_logic := '1'; + begin + if x < 0 then + result := '0'; + end if; + return result; + end function; +begin + sum <= is_positive(popcount(x1 xnor x2)); +end rtl; diff --git a/elasticai/creator/nn/binary/mac/design.py b/elasticai/creator/nn/binary/mac/design.py new file mode 100644 index 00000000..d2537cd6 --- /dev/null +++ b/elasticai/creator/nn/binary/mac/design.py @@ -0,0 +1,19 @@ +from elasticai.creator.file_generation.savable import Path, Savable +from elasticai.creator.file_generation.template import InProjectTemplate + + +class MacDesign(Savable): + def __init__(self, name: str, vector_width: int): + self._name = name + self._vector_width = vector_width + + def save_to(self, destination: Path) -> None: + core_component = InProjectTemplate( + package="elasticai.creator.nn.binary.mac", + file_name="bin_mac.vhd", + parameters={ + "total_width": str(self._vector_width), + "name": self._name, + }, + ) + destination.create_subpath("bin_mac").as_file(".vhd").write(core_component) diff --git a/elasticai/creator/nn/binary/mac/layer.py b/elasticai/creator/nn/binary/mac/layer.py new file mode 100644 index 00000000..bb095e10 --- /dev/null +++ b/elasticai/creator/nn/binary/mac/layer.py @@ -0,0 +1,26 @@ +from fixed_point.number_converter import FXPParams + +from elasticai.creator.file_generation.savable import Savable + +from .._math_operations import MathOperations +from .design import MacDesign +from .mactestbench import MacTestBench + + +class MacLayer: + def __init__(self, vector_width: int): + self.ops = MathOperations() + self._vector_width = vector_width + + def __call__(self, a, b): + return self.ops.matmul(a, b) + + def create_design(self, name: str) -> Savable: + return MacDesign(vector_width=self._vector_width, name=name) + + def create_testbench(self, name: str) -> MacTestBench: + return MacTestBench( + uut=self.create_design("mac_wrapper_test"), + uut_name="mac_wrapper_test", + name=name, + ) diff --git a/elasticai/creator/nn/binary/mac/mactestbench.py b/elasticai/creator/nn/binary/mac/mactestbench.py new file mode 100644 index 00000000..62aedd7e --- /dev/null +++ b/elasticai/creator/nn/binary/mac/mactestbench.py @@ -0,0 +1,54 @@ +from elasticai.creator.file_generation.savable import Path +from elasticai.creator.file_generation.template import InProjectTemplate + + +class MacTestBench: + def __init__(self, uut, name, uut_name): + self._uut = uut + self._uut_name = uut_name + self._inputs = None + self._destination = None + self.name = name + + def set_inputs(self, **inputs: dict[str, tuple[float, ...]]): + self._inputs = inputs["x1"], inputs["x2"] + + def parse_reported_content(self, outputs: list[str]): + return 2 * int(outputs[0]) - 1 + + @property + def _width(self) -> str: + return str(len(self._inputs[0])) + + def save_to(self, destination: Path): + self._destination = destination + inputs = self._prepare_inputs_for_test_bench(self._inputs) + test_bench = InProjectTemplate( + package="elasticai.creator.nn.binary.mac", + file_name="testbench.tpl.vhd", + parameters=inputs + | { + "uut_name": self._uut_name, + "name": self.name, + "total_width": self._width, + }, + ) + self._uut.save_to(destination) + destination.create_subpath(self.name).as_file(".vhd").write(test_bench) + + def _prepare_inputs_for_test_bench(self, inputs): + x1, x2 = inputs + + def zero_one(xs): + return ["0" if x < 0 else "1" for x in xs] + + def to_string(xs): + return '"{}"'.format("".join(xs)) + + x1 = zero_one(x1) + x2 = zero_one(x2) + inputs = { + "x1": to_string(x1), + "x2": to_string(x2), + } + return inputs diff --git a/elasticai/creator/nn/binary/mac/testbench.tpl.vhd b/elasticai/creator/nn/binary/mac/testbench.tpl.vhd new file mode 100644 index 00000000..59b4ed0e --- /dev/null +++ b/elasticai/creator/nn/binary/mac/testbench.tpl.vhd @@ -0,0 +1,56 @@ +library ieee; +use ieee.std_logic_1164.all; +use ieee.numeric_std.all; +use std.textio.all; +use ieee.std_logic_textio.all; +use std.env.finish; + + +entity $name is + +end; + +architecture Behavioral of $name is + constant TOTAL_WIDTH : integer := $total_width; + constant total_clock_cycles: integer := 4; + signal clock_period : time := 2 ps; + signal clock : std_logic := '0'; + signal reset : std_logic := '0'; + subtype input_t is std_logic_vector(TOTAL_WIDTH-1 downto 0); + signal next_sample : std_logic; + signal x1 : input_t := (others => '0'); + signal x2 : input_t := (others => '0'); + signal sum : std_logic; + signal done : std_logic; + + signal x1_values : input_t := $x1; + signal x2_values : input_t := $x2; + + + +begin + UUT : entity work.${uut_name} + port map (reset => reset, next_sample => next_sample, x1 => x1, x2 => x2, sum => sum, done => done); + + next_sample <= clock; + clock <= not clock after clock_period/2; + + testbench_1 : process(clock, done) + variable iteration_id : integer := 0; + variable reset_performed : std_logic := '0'; + variable value_id : integer := 0; + constant max_iterations : integer := 5; + begin + if rising_edge(clock) then + if iteration_id = 0 then + x1 <= x1_values; + x2 <= x2_values; + else + report to_string(sum); + finish; + end if; + iteration_id := iteration_id + 1; + end if; + end process; + +end Behavioral;