From d2f0621e4f69c3bd26c63225f7943678c02b2949 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 16 Oct 2025 15:28:30 +0200 Subject: [PATCH 001/170] Reworked and parallel simulations initial commit --- .../transformation/fpgadataflow/simulation.py | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/finn/transformation/fpgadataflow/simulation.py diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py new file mode 100644 index 0000000000..d36adbfeb7 --- /dev/null +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -0,0 +1,126 @@ +"""Manage FINN simulation variants.""" +import onnx +import os +from concurrent.futures import Future, ProcessPoolExecutor +from copy import deepcopy +from onnx import NodeProto, TensorProto +from qonnx.core.modelwrapper import ModelWrapper +from qonnx.custom_op.registry import getCustomOp +from typing import TYPE_CHECKING, Any, cast + +from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP +from finn.transformation.fpgadataflow.set_fifo_depths import xsi_fifosim +from finn.util.exception import FINNInternalError + +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp + + +class Simulation: + """Manage simulations in FINN.""" + + def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: + """Create a new simulation instance.""" + self.model = model + self.fpgapart = fpgapart + self.clk_ns = clk_ns + + def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: + """Return a modelwrapper that has only the specified node. + + Args: + by_node: If int, used as the index of the specified node. If string, assumed to be + the name of the node. + + Returns: + ModelWrapper: The isolated-node modelwrapper. + """ + # Find the node + index = 0 + if type(by_node) is int: + if by_node < 0 or by_node >= len(self.model.graph.node): + raise FINNInternalError( + f"Cannot isolate node index {by_node}. Model has" + f"{len(self.model.graph.node)} nodes." + ) + index = by_node + elif type(by_node) is str: + node_name = self.model.get_node_from_name(by_node) + if node_name is None: + raise FINNInternalError(f"Cannot isolate node {by_node}. No such node found.") + index = [n.name for n in self.model.graph.node].index(cast("str", node_name)) + elif type(by_node) is NodeProto: + try: + index = self.model.graph.node.index(by_node) + except Exception as e: + raise FINNInternalError(f"Node {by_node.name} not found in the model.") from e + else: + raise FINNInternalError( + f"Cannot find node to isolate: {by_node}. Specify either " + f"the index (int), node name (str) or the object itself " + f"(NodeProto)." + ) + + # Copy model to modify + node_model = deepcopy(self.model) + + # Remove any other node + # TODO: Refactor this following section + for i, node in enumerate(self.model.graph.node): + if i != index: + node_model.graph.node.remove(node) + target_op: HWCustomOp = getCustomOp(self.model.graph.node[0]) + inp = onnx.helper.make_tensor_value_info( + "inp", TensorProto.FLOAT, target_op.get_folded_input_shape() + ) + outp = onnx.helper.make_tensor_value_info( + "outp", TensorProto.FLOAT, target_op.get_normal_output_shape() + ) + + # Remove old io + for _ in range(len(node_model.graph.node[0].input)): + node_model.graph.node[0].input.pop() + for _ in range(len(node_model.graph.node[0].output)): + node_model.graph.node[0].output.pop() + + # Set new io + node_model.graph.node[0].input.append("inp") + node_model.graph.node[0].output.append("outp") + + # Remove graph io + for _ in range(len(node_model.graph.input)): + node_model.graph.input.pop() + for _ in range(len(node_model.graph.output)): + node_model.graph.output.pop() + + # Set new graph io + node_model.graph.input.append(inp) + node_model.graph.output.append(outp) + + return node_model + + def run_sim_node_parallel(self, inputs: int) -> dict[int, Any]: + """Run a fast simulation by simulating every node in parallel.""" + + def _run_simulation(node_index: int) -> Any: + nodemodel = self._isolated_node_model(node_index) + nodemodel = nodemodel.transform(CreateStitchedIP(self.fpgapart, self.clk_ns)) + # TODO: Remove xsi_fifosim from set_fifo_depths.py / change simulation functions + return xsi_fifosim(nodemodel, inputs) + + workers = int(os.environ["NUM_DEFAULT_WORKERS"]) + futures: list[Future] = [] + results = {} + with ProcessPoolExecutor(max_workers=workers) as pool: + for i in range(len(self.model.graph.node)): + futures.append(pool.submit(_run_simulation, i)) + pool.shutdown(wait=True) + for i, future in enumerate(futures): + results[i] = future.result() + return results + + def run_sim_complete(self) -> Any: + raise NotImplementedError() + + def run_sim_single_node(self, node: Any) -> Any: + raise NotImplementedError() From 5774681b91a8a96609493975181dfe3e8930d66c Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 16 Oct 2025 15:35:57 +0200 Subject: [PATCH 002/170] Correct simulation function naming --- src/finn/transformation/fpgadataflow/simulation.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index d36adbfeb7..d45ef2bba3 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -99,8 +99,10 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: return node_model - def run_sim_node_parallel(self, inputs: int) -> dict[int, Any]: - """Run a fast simulation by simulating every node in parallel.""" + def run_sim_node_parallel_isolated(self, inputs: int) -> dict[int, Any]: + """Simulate the given number of inputs for every layer. Layers are completely isolated + and simulated in parallel. + """ def _run_simulation(node_index: int) -> Any: nodemodel = self._isolated_node_model(node_index) @@ -119,6 +121,11 @@ def _run_simulation(node_index: int) -> Any: results[i] = future.result() return results + def run_sim_node_parallel_connected(self, inputs: int) -> Any: + """Simulate a whole model, with all layers simulated in parallel.""" + # TODO: Enable control through either Python or a seperate C++ driver + raise NotImplementedError() + def run_sim_complete(self) -> Any: raise NotImplementedError() From 0e10a334b3be396b05084dec659325494d6accb2 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 16 Oct 2025 16:44:33 +0200 Subject: [PATCH 003/170] Add new sim bindings --- finn_xsi/finn_xsi/CMakeLists.txt | 89 +++++ finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 15 + finn_xsi/finn_xsi/Makefile | 6 +- .../finn_xsi/cmake/CompilerWarnings.cmake | 115 ++++++ .../cmake/InterproceduralOptimization.cmake | 7 + finn_xsi/finn_xsi/include/AXIS_Control.h | 84 +++++ finn_xsi/finn_xsi/include/AXI_Control.h | 40 ++ finn_xsi/finn_xsi/include/Clock.h | 31 ++ finn_xsi/finn_xsi/include/Design.h | 53 +++ finn_xsi/finn_xsi/include/Kernel.h | 133 +++++++ finn_xsi/finn_xsi/include/Port.h | 64 ++++ finn_xsi/finn_xsi/include/SharedLibrary.h | 69 ++++ finn_xsi/finn_xsi/include/Simulation.hpp | 75 ++++ finn_xsi/finn_xsi/include/helper.h | 18 + finn_xsi/finn_xsi/rtlsim_config.hpp.template | 20 +- finn_xsi/finn_xsi/src/AXIS_Control.cpp | 51 +++ finn_xsi/finn_xsi/src/AXI_Control.cpp | 188 +++++++++ finn_xsi/finn_xsi/src/Clock.cpp | 35 ++ finn_xsi/finn_xsi/src/Design.cpp | 51 +++ finn_xsi/finn_xsi/src/Kernel.cpp | 168 +++++++++ finn_xsi/finn_xsi/src/Port.cpp | 208 ++++++++++ finn_xsi/finn_xsi/src/SharedLibrary.cpp | 120 ++++++ finn_xsi/finn_xsi/xsi_bind.cpp | 12 +- finn_xsi/finn_xsi/xsi_finn.cpp | 346 ----------------- finn_xsi/finn_xsi/xsi_finn.hpp | 356 ------------------ 25 files changed, 1640 insertions(+), 714 deletions(-) create mode 100644 finn_xsi/finn_xsi/CMakeLists.txt create mode 100644 finn_xsi/finn_xsi/LayerSimulationBackend.cpp create mode 100644 finn_xsi/finn_xsi/cmake/CompilerWarnings.cmake create mode 100644 finn_xsi/finn_xsi/cmake/InterproceduralOptimization.cmake create mode 100644 finn_xsi/finn_xsi/include/AXIS_Control.h create mode 100644 finn_xsi/finn_xsi/include/AXI_Control.h create mode 100644 finn_xsi/finn_xsi/include/Clock.h create mode 100644 finn_xsi/finn_xsi/include/Design.h create mode 100644 finn_xsi/finn_xsi/include/Kernel.h create mode 100644 finn_xsi/finn_xsi/include/Port.h create mode 100644 finn_xsi/finn_xsi/include/SharedLibrary.h create mode 100644 finn_xsi/finn_xsi/include/Simulation.hpp create mode 100644 finn_xsi/finn_xsi/include/helper.h create mode 100644 finn_xsi/finn_xsi/src/AXIS_Control.cpp create mode 100644 finn_xsi/finn_xsi/src/AXI_Control.cpp create mode 100644 finn_xsi/finn_xsi/src/Clock.cpp create mode 100644 finn_xsi/finn_xsi/src/Design.cpp create mode 100644 finn_xsi/finn_xsi/src/Kernel.cpp create mode 100644 finn_xsi/finn_xsi/src/Port.cpp create mode 100644 finn_xsi/finn_xsi/src/SharedLibrary.cpp delete mode 100644 finn_xsi/finn_xsi/xsi_finn.cpp delete mode 100644 finn_xsi/finn_xsi/xsi_finn.hpp diff --git a/finn_xsi/finn_xsi/CMakeLists.txt b/finn_xsi/finn_xsi/CMakeLists.txt new file mode 100644 index 0000000000..e2e517df8a --- /dev/null +++ b/finn_xsi/finn_xsi/CMakeLists.txt @@ -0,0 +1,89 @@ +cmake_minimum_required(VERSION 3.10) +project(LayerSimulationBackend) + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +# Require C++20 +set(CMAKE_CXX_EXTENSIONS ON) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +message(STATUS "Using C++ Standard ${CMAKE_CXX_STANDARD}") +SET(CMAKE_COLOR_MAKEFILE ON) + +message(STATUS "CMake cwd: ${CMAKE_CURRENT_SOURCE_DIR}") + +# Export compile commands for clangd +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# INCLUDES + +#Threads +set(THREADS_PREFER_PTHREAD_FLAG ON) +find_package(Threads REQUIRED) + +#OpenMP +find_package(OpenMP REQUIRED) + +#Compiler Options +add_library(fifosim_options INTERFACE) +add_library(fifosim::options ALIAS fifosim_options) + +OPTION(FIFOSIM_ENABLE_ALLOPT "Enable all optimizations" ON) +if(${FIFOSIM_ENABLE_ALLOPT}) + message(STATUS "All optimizations are enabled") + target_compile_options( + fifosim_options + INTERFACE -Ofast -ffast-math -march=native -mtune=native -fstack-protector-strong -fopenmp -ffunction-sections -fdata-sections -pipe -funroll-loops -shared -fPIC) + #target_link_options(fifosim_options INTERFACE -fsanitize=undefined,address) +endif() + +### Enable compiler warnings +option(FIFOSIM_ENABLE_WARNINGS "Enable warnings" ON) +if (FIFOSIM_ENABLE_WARNINGS) + include(cmake/CompilerWarnings.cmake) + fifosim_set_project_warnings( + fifosim_options + OFF + "" + "" + "" + "") +endif (FIFOSIM_ENABLE_WARNINGS) + +# +# Create options for including cmake files from the cmake folder with a bit of output. +# +macro(check_include) + if(NOT ${ARGC} EQUAL 3) + message(FATAL_ERROR "Call to 'check_include' with ${ARGC} arguments instead of 3") + endif() + OPTION(${ARGV0} "Enable ${ARGV0}" ON) + if (${ARGV0}) + message(STATUS "${ARGV1}: enabled") + include(cmake/${ARGV2}) + else() + message(STATUS "${ARGV1}: disabled") + endif() +endmacro() + +message(STATUS "Checks:") +list(APPEND CMAKE_MESSAGE_INDENT " ") #indent +1 +check_include(FIFOSIM_IPO "InterproceduralOptimization" InterproceduralOptimization.cmake) +list(POP_BACK CMAKE_MESSAGE_INDENT) #indent -1 + +# Write configuration header +#configure_file("${CMAKE_BINARY_DIR}/simulation_config.hpp.in" "${CMAKE_BINARY_DIR}/simulation_config.hpp") + +# Main +file(GLOB_RECURSE CORE_SRC src/*.cpp) +add_executable(LayerSimulationBackend LayerSimulationBackend.cpp ${CORE_SRC}) + +# Include the rtlsim wrapper directory itself +target_include_directories(LayerSimulationBackend PUBLIC "${CMAKE_BINARY_DIR}") + +# Add xsim includes +target_include_directories(LayerSimulationBackend PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") +target_include_directories(LayerSimulationBackend PUBLIC "include") + +# Link libraries +target_link_libraries(LayerSimulationBackend fifosim::options Threads::Threads OpenMP::OpenMP_CXX -ldl -lrt) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp new file mode 100644 index 0000000000..f21da89ff0 --- /dev/null +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -0,0 +1,15 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +int main(){ + Simulation<1, 1> sim("kernel_lib", "design_lib", "xsim_log_file", "trace_file", + std::array{StreamDescriptor{"input", 1024, 10}}, + std::array{StreamDescriptor{"output", 1024, 10}}); + return 0; +} diff --git a/finn_xsi/finn_xsi/Makefile b/finn_xsi/finn_xsi/Makefile index 5379a23d8c..76eed5cde7 100644 --- a/finn_xsi/finn_xsi/Makefile +++ b/finn_xsi/finn_xsi/Makefile @@ -17,9 +17,9 @@ default: xsi.so # Python Binding -xsi.so: xsi_bind.cpp xsi_finn.cpp - g++ --std=c++17 -Wall -O3 -shared -fPIC \ - $$(python3.10-config --includes) -I$(XILINX_VIVADO)/data/xsim/include \ +xsi.so: xsi_bind.cpp src/Port.cpp src/Design.cpp src/Kernel.cpp src/SharedLibrary.cpp + g++ --std=c++20 -Wall -O3 -shared -fPIC \ + $$(python3.10-config --includes) -I$(XILINX_VIVADO)/data/xsim/include -I./include \ -o$@ $^ -ldl -lrt diff --git a/finn_xsi/finn_xsi/cmake/CompilerWarnings.cmake b/finn_xsi/finn_xsi/cmake/CompilerWarnings.cmake new file mode 100644 index 0000000000..a606ab5163 --- /dev/null +++ b/finn_xsi/finn_xsi/cmake/CompilerWarnings.cmake @@ -0,0 +1,115 @@ +# from here: +# +# https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md + +function( + fifosim_set_project_warnings + project_name + WARNINGS_AS_ERRORS + MSVC_WARNINGS + CLANG_WARNINGS + GCC_WARNINGS + CUDA_WARNINGS) + if("${MSVC_WARNINGS}" STREQUAL "") + set(MSVC_WARNINGS + /W4 # Baseline reasonable warnings + /w14242 # 'identifier': conversion from 'type1' to 'type2', possible loss of data + /w14254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data + /w14263 # 'function': member function does not override any base class virtual member function + /w14265 # 'classname': class has virtual functions, but destructor is not virtual instances of this class may not + # be destructed correctly + /w14287 # 'operator': unsigned/negative constant mismatch + /we4289 # nonstandard extension used: 'variable': loop control variable declared in the for-loop is used outside + # the for-loop scope + /w14296 # 'operator': expression is always 'boolean_value' + /w14311 # 'variable': pointer truncation from 'type1' to 'type2' + /w14545 # expression before comma evaluates to a function which is missing an argument list + /w14546 # function call before comma missing argument list + /w14547 # 'operator': operator before comma has no effect; expected operator with side-effect + /w14549 # 'operator': operator before comma has no effect; did you intend 'operator'? + /w14555 # expression has no effect; expected expression with side- effect + /w14619 # pragma warning: there is no warning number 'number' + /w14640 # Enable warning on thread un-safe static member initialization + /w14826 # Conversion from 'type1' to 'type2' is sign-extended. This may cause unexpected runtime behavior. + /w14905 # wide string literal cast to 'LPSTR' + /w14906 # string literal cast to 'LPWSTR' + /w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied + /permissive- # standards conformance mode for MSVC compiler. + ) + endif() + + if("${CLANG_WARNINGS}" STREQUAL "") + set(CLANG_WARNINGS + -Wall + -Wextra # reasonable and standard + -Wshadow # warn the user if a variable declaration shadows one from a parent context + -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps + # catch hard to track down memory errors + -Wold-style-cast # warn for c-style casts + -Wcast-align # warn for potential performance problem casts + -Wunused # warn on anything being unused + -Woverloaded-virtual # warn if you overload (not override) a virtual function + -Wpedantic # warn if non-standard C++ is used + -Wconversion # warn on type conversions that may lose data + -Wsign-conversion # warn on sign conversions + -Wnull-dereference # warn if a null dereference is detected + -Wdouble-promotion # warn if float is implicit promoted to double + -Wformat=2 # warn on security issues around functions that format output (ie printf) + -Wimplicit-fallthrough # warn on statements that fallthrough without an explicit annotation + ) + endif() + + if("${GCC_WARNINGS}" STREQUAL "") + set(GCC_WARNINGS + ${CLANG_WARNINGS} + -Wmisleading-indentation # warn if indentation implies blocks where blocks do not exist + -Wduplicated-cond # warn if if / else chain has duplicated conditions + -Wduplicated-branches # warn if if / else branches have duplicated code + -Wlogical-op # warn about logical operations being used where bitwise were probably wanted + -Wuseless-cast # warn if you perform a cast to the same type + ) + endif() + + if("${CUDA_WARNINGS}" STREQUAL "") + set(CUDA_WARNINGS + -Wall + -Wextra + -Wunused + -Wconversion + -Wshadow + # TODO add more Cuda warnings + ) + endif() + + if(WARNINGS_AS_ERRORS) + message(TRACE "Warnings are treated as errors") + list(APPEND CLANG_WARNINGS -Werror) + list(APPEND GCC_WARNINGS -Werror) + list(APPEND MSVC_WARNINGS /WX) + endif() + + if(MSVC) + set(PROJECT_WARNINGS_CXX ${MSVC_WARNINGS}) + elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + set(PROJECT_WARNINGS_CXX ${CLANG_WARNINGS}) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(PROJECT_WARNINGS_CXX ${GCC_WARNINGS}) + else() + message(AUTHOR_WARNING "No compiler warnings set for CXX compiler: '${CMAKE_CXX_COMPILER_ID}'") + # TODO support Intel compiler + endif() + + # use the same warning flags for C + set(PROJECT_WARNINGS_C "${PROJECT_WARNINGS_CXX}") + + set(PROJECT_WARNINGS_CUDA "${CUDA_WARNINGS}") + + target_compile_options( + ${project_name} + INTERFACE # C++ warnings + $<$:${PROJECT_WARNINGS_CXX}> + # C warnings + $<$:${PROJECT_WARNINGS_C}> + # Cuda warnings + $<$:${PROJECT_WARNINGS_CUDA}>) +endfunction() diff --git a/finn_xsi/finn_xsi/cmake/InterproceduralOptimization.cmake b/finn_xsi/finn_xsi/cmake/InterproceduralOptimization.cmake new file mode 100644 index 0000000000..c5c513d14a --- /dev/null +++ b/finn_xsi/finn_xsi/cmake/InterproceduralOptimization.cmake @@ -0,0 +1,7 @@ +include(CheckIPOSupported) +check_ipo_supported(RESULT result OUTPUT output) +if(result) +set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON) +else() +message(SEND_ERROR "IPO is not supported: ${output}") +endif() diff --git a/finn_xsi/finn_xsi/include/AXIS_Control.h b/finn_xsi/finn_xsi/include/AXIS_Control.h new file mode 100644 index 0000000000..415ed48401 --- /dev/null +++ b/finn_xsi/finn_xsi/include/AXIS_Control.h @@ -0,0 +1,84 @@ +#ifndef AXIS_CONTROL +#define AXIS_CONTROL + +#include +#include +#include + +// Fwd declarations +namespace xsi { + class Design; + class Port; +} // namespace xsi +class Clock; + +class AXIS_Control { + public: + // Constructor/destructor + AXIS_Control(xsi::Design& design, Clock& clock, size_t job_size, const std::string& prefix = "s_axis_"); + AXIS_Control() = default; + virtual ~AXIS_Control() noexcept = default; + + AXIS_Control(AXIS_Control&& other) = default; + AXIS_Control& operator=(AXIS_Control&& other) = default; + + void inititialized_or_throw(); + + // Core functions - immediate writes + void valid(bool value = true); + bool is_valid() const noexcept; + void ready(bool value = true); + bool is_ready() const noexcept; + + // Deferred write functions + std::reference_wrapper set_valid(bool value = true); + std::reference_wrapper set_ready(bool value = true); + + // Job Size and Transaction Statistics + size_t job_size; + size_t job_txns; // [0:job_size] + size_t total_txns; + size_t first_complete; // First completion timestamp + + // AXI interface prefix + std::string name; + + private: + const xsi::Design* design; + const Clock* clk; + + xsi::Port* port_vld; + xsi::Port* port_rdy; +}; + +class S_AXIS_Control : public AXIS_Control { + public: + // Constructor/destructor + S_AXIS_Control(xsi::Design& design, Clock& clock, size_t job_size, size_t job_ticks, const std::string& prefix = "s_axis_"); + S_AXIS_Control() = default; + ~S_AXIS_Control() noexcept = default; + + S_AXIS_Control(S_AXIS_Control&& other) = default; + S_AXIS_Control& operator=(S_AXIS_Control&& other) = default; + + size_t job_ticks; // throttle if job_size < job_ticks + size_t await_iter; // iteration allowing start of next job +}; + +class M_AXIS_Control : public AXIS_Control { + public: + // Constructor/destructor + M_AXIS_Control(xsi::Design& design, Clock& clock, size_t job_size, const std::string& prefix = "m_axis_"); + M_AXIS_Control() = default; + ~M_AXIS_Control() noexcept = default; + + M_AXIS_Control(M_AXIS_Control&& other) = default; + M_AXIS_Control& operator=(M_AXIS_Control&& other) = default; + + size_t last_complete = 0; + size_t interval; + size_t latency = 0; + size_t min_latency = std::numeric_limits::max(); // Minimum latency observed +}; + +#endif /* AXIS_CONTROL */ diff --git a/finn_xsi/finn_xsi/include/AXI_Control.h b/finn_xsi/finn_xsi/include/AXI_Control.h new file mode 100644 index 0000000000..64eaf3740f --- /dev/null +++ b/finn_xsi/finn_xsi/include/AXI_Control.h @@ -0,0 +1,40 @@ +#ifndef AXI_CONTROL +#define AXI_CONTROL + +#include +#include + +// Fwd declarations +namespace xsi { + class Design; + class Port; +} // namespace xsi +class Clock; + +class AXI_Control { + public: + // Constructor/destructor + AXI_Control(xsi::Design& design, Clock& clock, const std::string& axi_prefix = "AXI_Control_0_0_"); + ~AXI_Control() noexcept = default; + + // // Core register access functions + void write_register(uint32_t addr, uint32_t data); + uint32_t read_register(uint32_t addr); + + private: + // AXI interface prefix + std::string prefix; + xsi::Design& design; + Clock& clk; + + // Helper functions for multi-bit signal handling + void write_addr(const std::string& signal, uint32_t addr); + void write_data(const std::string& signal, uint32_t data); + void write_strb(const std::string& signal, uint32_t strb); + uint32_t read(const std::string& signal); + void set_bool(const std::string& signal); + void clear_bool(const std::string& signal); + bool chk_bool(const std::string& signal); +}; + +#endif /* AXI_CONTROL */ diff --git a/finn_xsi/finn_xsi/include/Clock.h b/finn_xsi/finn_xsi/include/Clock.h new file mode 100644 index 0000000000..e943cfa804 --- /dev/null +++ b/finn_xsi/finn_xsi/include/Clock.h @@ -0,0 +1,31 @@ +#ifndef CLOCK +#define CLOCK + +#include + +// Fwd declarations +namespace xsi { + class Design; +} + +class Clock { + xsi::Design& design; + + Clock(Clock const&) = delete; + Clock& operator=(Clock const&) = delete; + Clock(xsi::Design& design); + template + friend class Simulation; + + public: + Clock(Clock&&) noexcept = default; + Clock& operator=(Clock&&) noexcept = default; + ~Clock() noexcept = default; + + std::function cycle; + + + void toggle_clk() noexcept; +}; + +#endif /* CLOCK */ diff --git a/finn_xsi/finn_xsi/include/Design.h b/finn_xsi/finn_xsi/include/Design.h new file mode 100644 index 0000000000..83a9016c43 --- /dev/null +++ b/finn_xsi/finn_xsi/include/Design.h @@ -0,0 +1,53 @@ +#ifndef DESIGN +#define DESIGN + +#include + +namespace xsi { + + // - non-copyable handle for exposing simulation control. + class Design { + xsi::Kernel _kernel; + + public: + Design(xsi::Kernel& kernel, const std::string& design_lib, const s_xsi_setup_info& setup_info); + Design(xsi::Kernel& kernel, const std::string& design_lib, const char* const log_file = nullptr, const char* const wdb_file = nullptr); + ~Design(); + + private: + Design(Design const&) = delete; + Design& operator=(Design const&) = delete; + + public: + // Move constructor + Design(Design&& other) noexcept; + + // Move assignment operator + Design& operator=(Design&& other) noexcept; + + //----------------------------------------------------------------------- + // Forwarded Access to Open Simulation + + // Simulation Control & Status + public: + void trace_all(); + void run(const XSI_INT64 step); + void restart(); + + int get_status() const noexcept; + const char* get_error_info() const noexcept; + + // Port Access + public: + int num_ports() const noexcept; + + xsi::Port& getPort(const std::string& name); + const xsi::Port& getPort(const std::string& name) const; + + std::span ports() noexcept; + std::span ports() const noexcept; + + }; // class Design +} // namespace xsi + +#endif /* DESIGN */ diff --git a/finn_xsi/finn_xsi/include/Kernel.h b/finn_xsi/finn_xsi/include/Kernel.h new file mode 100644 index 0000000000..c7713ea00d --- /dev/null +++ b/finn_xsi/finn_xsi/include/Kernel.h @@ -0,0 +1,133 @@ +#ifndef KERNEL_H_ +#define KERNEL_H_ + +#include + +#include +#include +#include +#include + +#include "xsi.h" + +namespace xsi { + + // Forward declarations + class Design; + class Port; + + class Kernel { + //----------------------------------------------------------------------- + // Dispatch Table for XSI Functions + class Xsi { + //- Statics --------------------- + public: + // Function Indeces + static constexpr unsigned get_value = 0, put_value = 1, get_int_port = 2, get_str_port = 3, + + get_int = 4, get_port_number = 5, + + trace_all = 6, run = 7, restart = 8, get_status = 9, get_error_info = 10, + + close = 11; + + private: + // Function Names & Types + static constexpr unsigned EXTENT = 12; + static char const* const FUNC_NAMES[EXTENT]; + using type_map = std::tuple< + // Port Access + t_fp_xsi_get_value, t_fp_xsi_put_value, t_fp_xsi_get_int_port, t_fp_xsi_get_str_port, + + // Design Inspection + t_fp_xsi_get_int, t_fp_xsi_get_port_number, + + // Simulation Control & Status + t_fp_xsi_trace_all, t_fp_xsi_run, t_fp_xsi_restart, t_fp_xsi_get_status, t_fp_xsi_get_error_info, + + // Closing + t_fp_xsi_close>; + + //- Actual Contents ------------- + private: + xsiHandle _hdl; + void* _func[EXTENT]; + + //- Lifecycle: in-place structure inside Kernel only + public: + Xsi(xsi::SharedLibrary& lib); + ~Xsi() {} + + private: + Xsi(Xsi const&) = delete; + Xsi& operator=(Xsi const&) = delete; + + public: + // Move constructor + Xsi(Xsi&& other) noexcept; // Move assignment operator + Xsi& operator=(Xsi&& other) noexcept; + + //- Handle Update --------------- + public: + void setHandle(xsiHandle hdl) noexcept; + bool hasValidHandle() const noexcept; + + //- XSI Function Invocation ----- + public: + template + auto invoke(Args&&... args) const { + auto const f = decltype(std::get(type_map()))(_func[FID]); + return (*f)(_hdl, std::forward(args)...); + } + + }; // class Xsi + + private: + // Instance State + xsi::SharedLibrary _kernel_lib; // Backing Kernel Library + Xsi _xsi; // XSI Dispatch Table + + // Optional State once a Design in open + xsi::SharedLibrary _design_lib; + std::vector _ports; + + public: + Kernel(const std::string& kernel_lib); + Kernel(Kernel const&) = delete; + Kernel& operator=(Kernel const&) = delete; + + // Move constructor + Kernel(Kernel&& other) noexcept; + // Move assignment operator + Kernel& operator=(Kernel&& other) noexcept; + + ~Kernel(); + + // Interface reserved for forwarded access through open Design + private: + friend Design; + friend Port; + template + auto xsi(Args&&... args) const { + return _xsi.invoke(std::forward(args)...); + } + + // Port Accessors inlined below and public through Design + Port& getPort(const char* const name); + const Port& getPort(const char* const name) const; + std::span ports() noexcept; + std::span ports() const noexcept; + + // Design con- & destruction hooks + void open(const std::string& design_lib, const s_xsi_setup_info& setup_info); + void close() noexcept; + + public: + // Port count accessor for Design class + size_t port_count() const noexcept; + + }; // class Kernel + +} // namespace xsi + +#endif /* KERNEL_H_ */ diff --git a/finn_xsi/finn_xsi/include/Port.h b/finn_xsi/finn_xsi/include/Port.h new file mode 100644 index 0000000000..0b75b0ecfa --- /dev/null +++ b/finn_xsi/finn_xsi/include/Port.h @@ -0,0 +1,64 @@ +#ifndef PORT_H_ +#define PORT_H_ + +#include +#include + +#include "xsi.h" + +namespace xsi { + + class Kernel; // Forward declaration + + // Only exists within controlled environment within Kernel with open Design. + class Port { + Kernel& _kernel; + unsigned const _id; + std::vector buffer; + + private: + friend Kernel; + // Con- and destruction under full control of Kernel + Port(Port const&) = delete; + Port& operator=(Port const&) = delete; + Port(Kernel& kernel, const unsigned id); + + public: + Port(Port&& other) noexcept; + ~Port() noexcept; + + public: + const char* name() const noexcept; + int dir() const noexcept; + unsigned width() const noexcept; + + bool isInput() const noexcept; + bool isOutput() const noexcept; + bool isInout() const noexcept; + + public: + // Buffer Synchronization + Port& read(); + void write_back(); + + // Inspection + bool hasUnknown() const noexcept; + bool isZero() const noexcept; + bool operator[](const unsigned idx) const noexcept; + + bool as_bool() const noexcept; + unsigned as_unsigned() const noexcept; + std::string as_binstr() const; + std::string as_hexstr() const; + + // Manipulation + Port& clear(); + Port& set(const unsigned val); + Port& set_binstr(const std::string& val); + Port& set_hexstr(const std::string& val); + + }; // class Port + +} // namespace xsi + +#endif /* PORT_H_ */ diff --git a/finn_xsi/finn_xsi/include/SharedLibrary.h b/finn_xsi/finn_xsi/include/SharedLibrary.h new file mode 100644 index 0000000000..0f5e768f6c --- /dev/null +++ b/finn_xsi/finn_xsi/include/SharedLibrary.h @@ -0,0 +1,69 @@ +#ifndef SHAREDLIBRARY_H_ +#define SHAREDLIBRARY_H_ + +#include +#include +#include + +#if defined(_WIN32) + #include +#else + #include +#endif + +namespace xsi { + class SharedLibrary { + public: + static char const library_suffix[]; + + private: + using handle_type = +#if defined(_WIN32) + HINSTANCE; +#else + void*; +#endif + + //----------------------------------------------------------------------- + // Instance State + private: + handle_type _lib; + std::string _path; + + //----------------------------------------------------------------------- + // Life Cycle + public: + SharedLibrary(); + SharedLibrary(const std::string& path); + ~SharedLibrary(); + + private: + SharedLibrary(SharedLibrary const&) = delete; + SharedLibrary& operator=(SharedLibrary const&) = delete; + + public: + // Move constructor + SharedLibrary(SharedLibrary&& other) noexcept; + + // Move assignment operator + SharedLibrary& operator=(SharedLibrary&& other) noexcept; + + public: + operator bool() const noexcept; + SharedLibrary& open(const std::string& path); + SharedLibrary& close() noexcept; + + private: + static handle_type load(const std::string& path); + void unload() noexcept; + + //----------------------------------------------------------------------- + // Accessors + public: + const std::string& path() const noexcept; + std::optional getsymbol(const char* const name); + + }; // class SharedLibrary +} // namespace xsi + +#endif /* SHAREDLIBRARY_H_ */ diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp new file mode 100644 index 0000000000..5c9f65bd9c --- /dev/null +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -0,0 +1,75 @@ +#ifndef SIMULATION +#define SIMULATION +#include +#include +#include +#include +#include +#include +#include + +template< size_t IStreamsSize, size_t OStreamsSize > +class Simulation { +public: + xsi::Kernel kernel; + xsi::Design top; + std::array istreams; + std::array ostreams; + Clock clk; + + void clearPorts() noexcept { + // Clear all input ports + for (xsi::Port &p : top.ports()) { + if (p.isInput()) { + p.clear().write_back(); + } + } + } + + void reset() noexcept { + xsi::Port &rst_n = top.getPort("ap_rst_n"); + // Reset all Inputs, Wait for Reset Period + rst_n.set(0).write_back(); + for (unsigned i = 0; i < 16; i++) { + clk.toggle_clk(); + } + rst_n.set(1).write_back(); + } + + Simulation(const std::string &kernel_lib, const std::string &design_lib, + const char *xsim_log_file, const char *trace_file, std::array istream_descs, + std::array ostream_descs) + : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), + clk(top) { + if (trace_file) { + top.trace_all(); + } + + // Find I/O Streams and initialize their Status + for (size_t i = 0; i < istream_descs.size(); ++i) { + istreams[i] = + S_AXIS_Control{top, clk, std::data(istream_descs)[i].job_size, + std::data(istream_descs)[i].job_ticks, + std::data(istream_descs)[i].name}; + } + for (size_t i = 0; i < ostream_descs.size(); ++i) { + ostreams[i] = + M_AXIS_Control{top, clk, std::data(ostream_descs)[i].job_size, + std::data(ostream_descs)[i].name}; + } + + // Find Global Control & Run Startup Sequence + clearPorts(); + reset(); + + // Make all Inputs valid & all Outputs ready + for (auto &&s : istreams) { + s.valid(); + } + for (auto &&s : ostreams) { + s.ready(); + } + } +}; + +#endif /* SIMULATION */ diff --git a/finn_xsi/finn_xsi/include/helper.h b/finn_xsi/finn_xsi/include/helper.h new file mode 100644 index 0000000000..49a896fcb3 --- /dev/null +++ b/finn_xsi/finn_xsi/include/helper.h @@ -0,0 +1,18 @@ +#ifndef HELPER_H_ +#define HELPER_H_ + +#include +#include + +constexpr std::array XZ10 = {'0', '1', 'Z', 'X'}; +constexpr std::array HEX = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + +struct StreamDescriptor { + std::string name; + std::size_t job_size; + // Next job can only start this many clock ticks after start of predecessor. + std::size_t job_ticks; +}; + +#endif /* HELPER_H_ */ diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index 3e0b35cc87..9fa8db4e24 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -11,6 +11,22 @@ * prior to compilation. ***************************************************************************/ +#include +#include + +/**** The following two defines are relevant for FIFO Simulation only ****/ + +#cmakedefine MPI_FOUND + +// If set, use this as the FIFO start size and directly +// skip to iterative sizing +#cmakedefine FIFO_START_SIZE ${FIFO_START_SIZE} + + + + +/**** General RTLSIM Configuration Parameters ****/ + struct stream_desc { char const *name; size_t job_size; @@ -27,10 +43,10 @@ static char const design_libname[] = "xsim.dir/@TOP_MODULE_NAME@/xsimk.so"; // AXI stream descriptors {stream_name, transactions_per_inference} // input AXI stream descriptors -static std::initializer_list const istream_descs { @ISTREAM_DESC@ }; +static constexpr std::initializer_list const istream_descs { @ISTREAM_DESC@ }; // output AXI stream descriptors -static std::initializer_list const ostream_descs { @OSTREAM_DESC@ }; +static constexpr std::initializer_list const ostream_descs { @OSTREAM_DESC@ }; // number of inferences to perform constexpr unsigned n_inferences = @N_INFERENCES@; diff --git a/finn_xsi/finn_xsi/src/AXIS_Control.cpp b/finn_xsi/finn_xsi/src/AXIS_Control.cpp new file mode 100644 index 0000000000..5dc584ba57 --- /dev/null +++ b/finn_xsi/finn_xsi/src/AXIS_Control.cpp @@ -0,0 +1,51 @@ +#include +#include +#include +#include + +#include + +std::string sanitize_prefix(const std::string& prefix) { + if (prefix.empty()) { + throw std::invalid_argument("AXI prefix cannot be empty."); + } + std::string sanitized = prefix; + if (sanitized.back() != '_') { + sanitized += "_"; + } + return sanitized; +} + +AXIS_Control::AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, const std::string& prefix) + : job_size(job_sz), job_txns(0), total_txns(0), first_complete(0), name(sanitize_prefix(prefix)), design(&des), clk(&clock), port_vld(&des.getPort(name + "tvalid")), port_rdy(&des.getPort(name + "tready")) {} + +void AXIS_Control::inititialized_or_throw() { + if (!design || !clk || !port_rdy || !port_vld) { + throw std::runtime_error("AXIS Control object not correctly initialized! Aborting!"); + } +} + +void AXIS_Control::valid(bool value) { port_vld->set(value ? 1 : 0).write_back(); } + +bool AXIS_Control::is_valid() const noexcept { return port_vld->read().as_bool(); } + +void AXIS_Control::ready(bool value) { port_rdy->set(value ? 1 : 0).write_back(); } + +bool AXIS_Control::is_ready() const noexcept { return port_rdy->read().as_bool(); } + +// Deferred write functions +std::reference_wrapper AXIS_Control::set_valid(bool value) { return std::ref(port_vld->set(value ? 1 : 0)); } + +std::reference_wrapper AXIS_Control::set_ready(bool value) { return std::ref(port_rdy->set(value ? 1 : 0)); } + +S_AXIS_Control::S_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, size_t job_tks, const std::string& prefix) : AXIS_Control(des, clock, job_sz, prefix), job_ticks(job_tks), await_iter(job_tks) { + if (job_sz < 1 || job_tks < 1) { + throw std::invalid_argument("Job size and ticks must be greater than 0."); + } +} + +M_AXIS_Control::M_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, const std::string& prefix) : AXIS_Control(des, clock, job_sz, prefix), last_complete(0), interval(0) { + if (job_sz < 1) { + throw std::invalid_argument("Job size must be greater than 0."); + } +} diff --git a/finn_xsi/finn_xsi/src/AXI_Control.cpp b/finn_xsi/finn_xsi/src/AXI_Control.cpp new file mode 100644 index 0000000000..78717b8c27 --- /dev/null +++ b/finn_xsi/finn_xsi/src/AXI_Control.cpp @@ -0,0 +1,188 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +using namespace xsi; + +// Constructor +AXI_Control::AXI_Control(xsi::Design& des, Clock& clock, const std::string& axi_prefix) : prefix(axi_prefix), design(des), clk(clock) { + // Check if the prefix is valid + if (prefix.empty()) { + throw std::invalid_argument("AXI prefix cannot be empty."); + } + + // Ensure the prefix ends with an underscore + if (prefix.back() != '_') { + prefix += "_"; + } +} + +// Helper functions for multi-bit signal handling +void AXI_Control::write_addr(const std::string& signal, uint32_t addr) { + // Convert addr to binary string + std::string addr_bin = std::bitset<32>(addr).to_string(); + + // Remove leading zeros to get the actual size used in the simulation + addr_bin.erase(0, addr_bin.find_first_not_of('0')); + + + // Get port size + Port& port = design.getPort(signal); + auto n_bits = port.width(); + + // Ensure the string is the right length + if (addr_bin.length() < n_bits) { + addr_bin = std::string(n_bits - addr_bin.length(), '0') + addr_bin; + } else if (addr_bin.length() > n_bits) { + addr_bin = addr_bin.substr(addr_bin.length() - n_bits); + } + + port.set_binstr(addr_bin).write_back(); +} + +void AXI_Control::write_data(const std::string& signal, uint32_t data) { + // Similar to write_addr + std::string data_bin = std::bitset<32>(data).to_string(); + + // Get port size + Port& port = design.getPort(signal); + auto n_bits = port.width(); + + if (data_bin.length() < n_bits) { + data_bin = std::string(n_bits - data_bin.length(), '0') + data_bin; + } else if (data_bin.length() > n_bits) { + data_bin = data_bin.substr(data_bin.length() - n_bits); + } + + port.set_binstr(data_bin).write_back(); +} + +void AXI_Control::write_strb(const std::string& signal, uint32_t strb) { + // Similar to write_addr + std::string strb_bin = std::bitset<4>(strb).to_string(); + + // Get port size + Port& port = design.getPort(signal); + auto n_bits = port.width(); + + if (strb_bin.length() < n_bits) { + strb_bin = std::string(n_bits - strb_bin.length(), '0') + strb_bin; + } else if (strb_bin.length() > n_bits) { + strb_bin = strb_bin.substr(strb_bin.length() - n_bits); + } + + port.set_binstr(strb_bin).write_back(); +} + +uint32_t AXI_Control::read(const std::string& signal) { + Port& port = design.getPort(signal); + return port.read().as_unsigned(); +} + +void AXI_Control::set_bool(const std::string& signal) { + Port& port = design.getPort(signal); + port.set(1).write_back(); +} + +void AXI_Control::clear_bool(const std::string& signal) { + Port& port = design.getPort(signal); + port.set(0).write_back(); +} + +bool AXI_Control::chk_bool(const std::string& signal) { + Port& port = design.getPort(signal); + return port.read().as_bool(); +} + +void AXI_Control::write_register(uint32_t addr, uint32_t data) { + // Assert BREADY to receive response + set_bool(prefix + "bready"); + // Set address + write_addr(prefix + "awaddr", addr); + // Set data and strobe (full 32-bit word) + write_data(prefix + "wdata", data); + write_strb(prefix + "wstrb", 0xF); // All bytes enabled + + // Assert AWVALID + set_bool(prefix + "awvalid"); + + // Assert WVALID + set_bool(prefix + "wvalid"); + + // Wait for AWREADY + while (!chk_bool(prefix + "awready")) { + clk.toggle_clk(); + } + + // Wait for WREADY + while (!chk_bool(prefix + "wready")) { + clk.toggle_clk(); + } + + clk.toggle_clk(); // Make sure that for at least one cycle the signals were set + + // Deassert AWVALID and WVALID + clear_bool(prefix + "awvalid"); + clear_bool(prefix + "wvalid"); + + + // Wait for BVALID + while (!chk_bool(prefix + "bvalid")) { + clk.toggle_clk(); + } + + // Check BRESP (optional, could add error handling) + uint32_t bresp = read(prefix + "bresp"); + if (bresp != 0) { + std::cerr << "AXI write error: BRESP = " << bresp << std::endl; + } + + // Deassert BREADY + clear_bool(prefix + "bready"); + + clk.toggle_clk(); +} + +uint32_t AXI_Control::read_register(uint32_t addr) { + // Assert RREADY to receive data + set_bool(prefix + "rready"); + // Set address + write_addr(prefix + "araddr", addr); + + // Assert ARVALID + set_bool(prefix + "arvalid"); + + // Wait for ARREADY + while (!chk_bool(prefix + "arready")) { + clk.toggle_clk(); + } + + // Wait for RVALID + while (!chk_bool(prefix + "rvalid")) { + clk.toggle_clk(); + } + + // Deassert ARVALID + clear_bool(prefix + "arvalid"); + + // Read data + uint32_t data = read(prefix + "rdata"); + + // Check RRESP (optional, could add error handling) + uint32_t rresp = read(prefix + "rresp"); + if (rresp != 0) { + std::cerr << "AXI read error: RRESP = " << rresp << std::endl; + } + + // Deassert RREADY + clear_bool(prefix + "rready"); + clk.toggle_clk(); + + return data; +} diff --git a/finn_xsi/finn_xsi/src/Clock.cpp b/finn_xsi/finn_xsi/src/Clock.cpp new file mode 100644 index 0000000000..eb6d1dd77c --- /dev/null +++ b/finn_xsi/finn_xsi/src/Clock.cpp @@ -0,0 +1,35 @@ +#include +#include +#include + +using namespace xsi; + +Clock::Clock(xsi::Design& des) : design(des) { + // Find Global Control & Run Startup Sequence + Port& clk = des.getPort("ap_clk"); + auto ports = des.ports(); + + Port* clk2x = nullptr; + for (auto&& p : ports) { + if (p.name() == std::string("ap_clk2x")) { + clk2x = &p; + break; + } + } + cycle = clk2x ? std::function([&des, &clk, clk2x](bool const up) mutable { + clk.set(up).write_back(); + clk2x->set(1).write_back(); + des.run(5); + clk2x->set(0).write_back(); + des.run(5); + }) + : std::function([&des, &clk](bool const up) mutable { + clk.set(up).write_back(); + des.run(5); + }); +} + +void Clock::toggle_clk() noexcept { + cycle(1); + cycle(0); +} diff --git a/finn_xsi/finn_xsi/src/Design.cpp b/finn_xsi/finn_xsi/src/Design.cpp new file mode 100644 index 0000000000..fcd85738eb --- /dev/null +++ b/finn_xsi/finn_xsi/src/Design.cpp @@ -0,0 +1,51 @@ +#include + +using namespace xsi; + +// Constructors +Design::Design(xsi::Kernel& kernel, const std::string& design_lib, const s_xsi_setup_info& setup_info) : _kernel(std::move(kernel)) { _kernel.open(design_lib, setup_info); } + +Design::Design(xsi::Kernel& kernel, const std::string& design_lib, const char* const log_file, const char* const wdb_file) + : Design(kernel, design_lib, s_xsi_setup_info{.logFileName = const_cast(log_file), .wdbFileName = const_cast(wdb_file)}) {} + +// Destructor +Design::~Design() { _kernel.close(); } + +// Move constructor +Design::Design(Design&& other) noexcept : _kernel(std::move(other._kernel)) { + // The kernel now manages the moved design + // No additional work needed as the kernel handles the XSI state +} + +// Move assignment operator +Design& Design::operator=(Design&& other) noexcept { + if (this != &other) { + _kernel.close(); // Close current design + // Note: _kernel is a reference and cannot be reassigned + // The move semantics here are limited since we hold a reference + _kernel = std::move(other._kernel); + } + return *this; +} + +// Simulation Control & Status +void Design::trace_all() { _kernel.xsi(); } + +void Design::run(const XSI_INT64 step) { _kernel.xsi(step); } + +void Design::restart() { _kernel.xsi(); } + +int Design::get_status() const noexcept { return _kernel.xsi(); } + +const char* Design::get_error_info() const noexcept { return _kernel.xsi(); } + +// Port Access +int Design::num_ports() const noexcept { return static_cast(_kernel.port_count()); } + +xsi::Port& Design::getPort(const std::string& name) { return _kernel.getPort(name.c_str()); } + +const xsi::Port& Design::getPort(const std::string& name) const { return _kernel.getPort(name.c_str()); } + +std::span Design::ports() noexcept { return _kernel.ports(); } + +std::span Design::ports() const noexcept { return _kernel.ports(); } diff --git a/finn_xsi/finn_xsi/src/Kernel.cpp b/finn_xsi/finn_xsi/src/Kernel.cpp new file mode 100644 index 0000000000..8b5b16657a --- /dev/null +++ b/finn_xsi/finn_xsi/src/Kernel.cpp @@ -0,0 +1,168 @@ +#include +#include +#include + +#include +#include +#include + +using namespace xsi; + +void* resolve_or_throw(xsi::SharedLibrary& lib, char const* const sym) { + auto const res = lib.getsymbol(sym); + if (!res) { + throw std::runtime_error(std::string("Failed to resolve ").append(sym).append(" in ").append(lib.path())); + } + return *res; +} + +char const* const Kernel::Xsi::FUNC_NAMES[EXTENT] = {"xsi_get_value", "xsi_put_value", + "xsi_get_int_port", "xsi_get_str_port", + + "xsi_get_int", "xsi_get_port_number", + + "xsi_trace_all", "xsi_run", + "xsi_restart", "xsi_get_status", + "xsi_get_error_info", + + "xsi_close"}; + + +Kernel::Xsi::Xsi(xsi::SharedLibrary& lib) : _hdl(nullptr) { + // Resolve XSI Functions + for (unsigned i = 0; i < EXTENT; i++) { + _func[i] = resolve_or_throw(lib, FUNC_NAMES[i]); + } +} + +// Xsi Move constructor +Kernel::Xsi::Xsi(Xsi&& other) noexcept : _hdl(other._hdl) { + std::copy(std::begin(other._func), std::end(other._func), std::begin(_func)); + other._hdl = nullptr; + std::fill(std::begin(other._func), std::end(other._func), nullptr); +} + +// Xsi Move assignment operator +Kernel::Xsi& Kernel::Xsi::operator=(Xsi&& other) noexcept { + if (this != &other) { + _hdl = other._hdl; + std::copy(std::begin(other._func), std::end(other._func), std::begin(_func)); + other._hdl = nullptr; + std::fill(std::begin(other._func), std::end(other._func), nullptr); + } + return *this; +} + +// Xsi Handle management +void Kernel::Xsi::setHandle(xsiHandle hdl) noexcept { _hdl = hdl; } + +bool Kernel::Xsi::hasValidHandle() const noexcept { return _hdl != nullptr; } +//--------------------------------------------------------------------------- +// Life Cycle + +// Move constructor +Kernel::Kernel(Kernel&& other) noexcept : _kernel_lib(std::move(other._kernel_lib)), _xsi(std::move(other._xsi)), _design_lib(std::move(other._design_lib)), _ports() { + // Reset source + other._ports.clear(); + + // Recreate ports if design is open + if (_design_lib && _xsi.hasValidHandle()) { + // Enumerate Ports + unsigned const port_count = static_cast(xsi(xsiNumTopPorts)); + _ports.reserve(port_count); + for (unsigned i = 0; i < port_count; ++i) { + _ports.emplace_back(Port(*this, i)); + } + } +} + +// Move assignment operator +Kernel& Kernel::operator=(Kernel&& other) noexcept { + if (this != &other) { + // Clean up current state + close(); + + // Move from other + _kernel_lib = std::move(other._kernel_lib); + _xsi = std::move(other._xsi); + _design_lib = std::move(other._design_lib); + + // Reset ports in source + other._ports.clear(); + + // Recreate ports if design is open + if (_design_lib && _xsi.hasValidHandle()) { + // Enumerate Ports + unsigned const port_count = static_cast(xsi(xsiNumTopPorts)); + _ports.reserve(port_count); + for (unsigned i = 0; i < port_count; i++) { + _ports.emplace_back(Port(*this, i)); + } + } + } + return *this; +} + +Kernel::Kernel(const std::string& kernel_lib) : _kernel_lib(kernel_lib), _xsi(_kernel_lib) {} + +Kernel::~Kernel() { + if (_design_lib) + std::cerr << "Disposing XSI Kernel with open Design." << std::endl; +} + +void Kernel::open(const std::string& design_lib, const s_xsi_setup_info& setup_info) { + _design_lib.open(design_lib); + try { + auto const f = t_fp_xsi_open(resolve_or_throw(_design_lib, "xsi_open")); + xsiHandle const hdl = f(const_cast(&setup_info)); + if (!hdl) + throw std::runtime_error("Loading of design failed"); + _xsi.setHandle(hdl); + + // Enumerate Ports + unsigned const port_count = static_cast(xsi(xsiNumTopPorts)); + _ports.reserve(port_count); + for (unsigned i = 0; i < port_count; i++) { + _ports.emplace_back(Port(*this, i)); + } + } catch (...) { + std::cerr << "Exception during design open, closing design library." << std::endl; + _design_lib.close(); + throw; + } +} +void Kernel::close() noexcept { + xsi(); + _xsi.setHandle(nullptr); + _design_lib.close(); + + // Clear ports - unique_ptr will handle destruction automatically + _ports.clear(); + + // Clean up Library State + std::optional vptr = _kernel_lib.getsymbol("svTypeInfo"); + if (vptr) + *vptr = nullptr; +} + +Port& Kernel::getPort(const char* const name) { + int const id = xsi(name); + + if (id == -1 || id >= static_cast(_ports.size())) { + throw std::runtime_error(std::string("Port not found: ").append(name)); + } + return _ports[static_cast(id)]; +} +const Port& Kernel::getPort(const char* const name) const { + int const id = xsi(name); + + if (id == -1 || id >= static_cast(_ports.size())) { + throw std::runtime_error(std::string("Port not found: ").append(name)); + } + return _ports[static_cast(id)]; +} +std::span Kernel::ports() noexcept { return std::span(_ports.data(), _ports.data() + _ports.size()); } +std::span Kernel::ports() const noexcept { return std::span(_ports.data(), _ports.data() + _ports.size()); } + +// Port count accessor for Design class +size_t Kernel::port_count() const noexcept { return _ports.size(); } diff --git a/finn_xsi/finn_xsi/src/Port.cpp b/finn_xsi/finn_xsi/src/Port.cpp new file mode 100644 index 0000000000..436c2f4778 --- /dev/null +++ b/finn_xsi/finn_xsi/src/Port.cpp @@ -0,0 +1,208 @@ +#include +#include +#include + +using namespace xsi; + +Port::Port(Kernel& kernel, const unsigned id) : _kernel(kernel), _id(id), buffer((width() + 31) / 32) {} + +Port::Port(Port&& other) noexcept : _kernel(other._kernel), _id(other._id), buffer(std::move(other.buffer)) { + // Note: _kernel and _id are reference and const respectively, so they're initialized from other + // The buffer is moved from the other object +} + +Port::~Port() noexcept {} + +bool Port::hasUnknown() const noexcept { + for (auto&& elem : buffer) { + if (elem.bVal) + return true; + } + return false; +} + +bool Port::isZero() const noexcept { + for (auto&& elem : buffer) { + if (elem.aVal) + return false; + } + return true; +} + +std::string Port::as_binstr() const { + unsigned const w = width(); + std::string res(w, '?'); + + auto buffer_iter = buffer.cbegin(); + auto res_iter = res.rbegin(); // Use reverse iterator to fill from right to left + + uint32_t a = 0; + uint32_t b = 0; + for (unsigned i = 0; i < w; i++) { + if ((i & 31) == 0) { + a = buffer_iter->aVal; + b = buffer_iter->bVal; + ++buffer_iter; + } + *res_iter++ = XZ10[((b & 1) << 1) | (a & 1)]; + a >>= 1; + b >>= 1; + } + + return res; +} + +std::string Port::as_hexstr() const { + unsigned l = (width() + 3) / 4; + std::string res(l, '?'); + auto buffer_iter = buffer.cbegin(); + auto res_iter = res.rbegin(); // Use reverse iterator to fill from right to left + + while (l > 0) { + uint32_t a = buffer_iter->aVal; + uint32_t b = buffer_iter->bVal; + ++buffer_iter; + + unsigned m = std::min(8u, l); + l -= m; + for (unsigned i = 0; i < m; ++i) { + unsigned const bm = b & 0xF; + unsigned const am = a & 0xF; + + *res_iter++ = !bm ? HEX[am] : XZ10[3 - !(am & bm)]; + a >>= 4; + b >>= 4; + } + } + return res; +} + +Port& Port::clear() { + std::fill(buffer.begin(), buffer.end(), s_xsi_vlog_logicval{.aVal = 0u, .bVal = 0u}); + return *this; +} + +const char* Port::name() const noexcept { return _kernel.xsi(static_cast(_id), xsiNameTopPort); } + +int Port::dir() const noexcept { return _kernel.xsi(static_cast(_id), xsiDirectionTopPort); } + +unsigned Port::width() const noexcept { return static_cast(_kernel.xsi(static_cast(_id), xsiHDLValueSize)); } + +bool Port::isInput() const noexcept { return dir() == xsiInputPort; } + +bool Port::isOutput() const noexcept { return dir() == xsiOutputPort; } + +bool Port::isInout() const noexcept { return dir() == xsiInoutPort; } + +Port& Port::read() { + _kernel.xsi(static_cast(_id), buffer.data()); + return *this; +} + +void Port::write_back() { _kernel.xsi(static_cast(_id), buffer.data()); } + +bool Port::operator[](const unsigned idx) const noexcept { return (buffer[idx / 32].aVal >> (idx % 32)) & 1; } + +bool Port::as_bool() const noexcept { return buffer[0].aVal & 1; } + +unsigned Port::as_unsigned() const noexcept { return buffer[0].aVal; } + +Port& Port::set(const unsigned val) { + s_xsi_vlog_logicval* const p = buffer.data(); + p->aVal = val; + p->bVal = 0; + return *this; +} + +Port& Port::set_binstr(const std::string& val) { + auto val_iter = val.crbegin(); // Process from right to left + + size_t chars_processed = 0; + const size_t val_length = val.length(); + + for (auto& elem : buffer) { + uint32_t a = 0; + uint32_t b = 0; + + // Process up to 32 characters for this buffer element + const size_t chars_to_process = std::min(32UL, val_length - chars_processed); + + for (size_t j = 0; j < chars_to_process; ++j) { + a <<= 1; + b <<= 1; + + if (val_iter != val.crend()) { + switch (*val_iter++) { + case '1': + a |= 1; + [[fallthrough]]; + case '0': + break; + default: + a |= 1; + [[fallthrough]]; + case 'Z': + case 'z': + b |= 1; + break; + } + } + } + + elem.aVal = a; + elem.bVal = b; + + chars_processed += chars_to_process; + if (chars_processed >= val_length) + break; + } + + return *this; +} + +Port& Port::set_hexstr(const std::string& val) { + auto val_iter = val.crbegin(); // Process from right to left + + size_t chars_processed = 0; + const size_t val_length = val.length(); + + for (auto& elem : buffer) { + uint32_t a = 0; + uint32_t b = 0; + + // Process up to 8 hex characters (32 bits) for this buffer element + const size_t chars_to_process = std::min(8UL, val_length - chars_processed); + + for (size_t j = 0; j < chars_to_process; ++j) { + a <<= 4; + b <<= 4; + + if (val_iter != val.crend()) { + char c = *val_iter++; + + if (('0' <= c) && c <= '9') { + a |= c & 0xF; + } else { + c |= 0x20; // Convert to lowercase + if (('a' <= c) && (c <= 'f')) { + a |= static_cast(c - ('a' - 10)); + } else { + b |= 0xF; + if (c != 'z') { + a |= 0xF; + } + } + } + } + } + + elem.aVal = a; + elem.bVal = b; + + chars_processed += chars_to_process; + if (chars_processed >= val_length) + break; + } + + return *this; +} diff --git a/finn_xsi/finn_xsi/src/SharedLibrary.cpp b/finn_xsi/finn_xsi/src/SharedLibrary.cpp new file mode 100644 index 0000000000..81ce1ff33e --- /dev/null +++ b/finn_xsi/finn_xsi/src/SharedLibrary.cpp @@ -0,0 +1,120 @@ +#include + +#include + +using namespace xsi; + +char const SharedLibrary::library_suffix[] = +#if defined(_WIN32) + ".lib"; +#else + ".so"; +#endif + +#if defined(_WIN32) +namespace { + std::string translate_error_message(DWORD errid) { + std::string msg; + LPTSTR bufptr; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, errid, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), &bufptr, 0, nullptr); + if (bufptr) + msg = reinterpret_cast(bufptr); + LocalFree(bufptr); + return msg; + } +} // namespace +#endif + +SharedLibrary& SharedLibrary::open(const std::string& path) { + if (_lib) + throw std::runtime_error("SharedLibrary still open for " + _path); + _lib = load(path); + _path = path; + return *this; +} + +SharedLibrary::handle_type SharedLibrary::load(const std::string& path) { + if (path.empty()) + throw std::domain_error("Empty library path."); + +#if defined(_WIN32) + SetLastError(0); + #ifdef UNICODE + // Use LoadLibraryA explicitly on windows if UNICODE is defined + handle_type const lib = LoadLibraryA(path.c_str()); + #else + handle_type const lib = LoadLibrary(path.c_str()); + #endif + if (!lib) + throw std::runtime_error(translate_error_message(GetLastError())); +#else + handle_type const lib = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); + if (!lib) + throw std::runtime_error(dlerror()); +#endif + return lib; +} + +void SharedLibrary::unload() noexcept { + if (_lib) { +#if defined(_WIN32) + FreeLibrary(_lib); +#else + dlclose(_lib); +#endif + } +} + +std::optional SharedLibrary::getsymbol(const char* const name) { + void* sym; +#if defined(_WIN32) + sym = (void*) GetProcAddress(_lib, name); + if (!sym) +#else + dlerror(); // clear error + sym = dlsym(_lib, name); + char const* const err = dlerror(); + if (err) +#endif + return std::nullopt; + return std::make_optional(sym); +} + +// Constructors +SharedLibrary::SharedLibrary() : _lib(nullptr), _path() {} + +SharedLibrary::SharedLibrary(const std::string& path) : _lib(load(path)), _path(path) {} + +// Destructor +SharedLibrary::~SharedLibrary() { unload(); } + +// Move constructor +SharedLibrary::SharedLibrary(SharedLibrary&& other) noexcept : _lib(other._lib), _path(std::move(other._path)) { other._lib = nullptr; } + +// Move assignment operator +SharedLibrary& SharedLibrary::operator=(SharedLibrary&& other) noexcept { + if (this != &other) { + // Clean up current state + unload(); + + // Move from other + _lib = other._lib; + _path = std::move(other._path); + + // Reset other + other._lib = nullptr; + } + return *this; +} + +// Member functions +SharedLibrary::operator bool() const noexcept { return bool(_lib); } + +SharedLibrary& SharedLibrary::close() noexcept { + unload(); + _lib = nullptr; + _path.clear(); + return *this; +} + +const std::string& SharedLibrary::path() const noexcept { return _path; } diff --git a/finn_xsi/finn_xsi/xsi_bind.cpp b/finn_xsi/finn_xsi/xsi_bind.cpp index 6530c84358..1edf80b01b 100644 --- a/finn_xsi/finn_xsi/xsi_bind.cpp +++ b/finn_xsi/finn_xsi/xsi_bind.cpp @@ -8,8 +8,11 @@ * @author Thomas B. Preußer ***************************************************************************/ +#include +#include +#include + #include -#include "xsi_finn.hpp" #include #include @@ -31,11 +34,6 @@ namespace { PYBIND11_MODULE(xsi, m) { - py::class_>(m, "Kernel") - .def(py::init()) - .def("hex_in_lower", &Kernel::hex_in_lower) - .def("hex_in_upper", &Kernel::hex_in_upper); - py::class_>(m, "Design") .def(py::init([]( std::shared_ptr const &kernel, @@ -54,7 +52,7 @@ PYBIND11_MODULE(xsi, m) { .def("get_status", &Design::get_status) .def("get_error_info", &Design::get_error_info) .def("num_ports", &Design::num_ports) - .def("getPort", static_cast(&Design::getPort)) + .def("getPort", static_cast(&Design::getPort)) .def("ports", [](Design &d) { auto const e = d.ports(); return py::make_iterator(e.begin(), e.end()); diff --git a/finn_xsi/finn_xsi/xsi_finn.cpp b/finn_xsi/finn_xsi/xsi_finn.cpp deleted file mode 100644 index 19134ac988..0000000000 --- a/finn_xsi/finn_xsi/xsi_finn.cpp +++ /dev/null @@ -1,346 +0,0 @@ -/**************************************************************************** - * Copyright (C) 2025, Advanced Micro Devices, Inc. - * All rights reserved. - * - * SPDX-License-Identifier: BSD-3-Clause - * - * @brief FINN XSI++: C++ XSI Binding used by FINN. - * @author Thomas B. Preußer - ***************************************************************************/ - -#include "xsi_finn.hpp" - -#include -#include - - -using namespace xsi; - -//=========================================================================== -// Local Helpers - -namespace { - void* resolve_or_throw(SharedLibrary &lib, char const *const sym) { - auto const res = lib.getsymbol(sym); - if(!res) { - throw std::runtime_error( - std::string("Failed to resolve ") - .append(sym).append(" in ").append(lib.path()) - ); - } - return *res; - } - char XZ10[4] = { '0', '1', 'Z', 'X' }; - char HEX[16] = { - '0', '1', '2', '3', '4', '5', '6', '7', - '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' - }; -} - -void Kernel::hex_in_lower() { - for(unsigned i = 2; i < 4; i++) XZ10[i] |= ' '; - for(unsigned i = 10; i < 16; i++) HEX [i] |= ' '; -} -void Kernel::hex_in_upper() { - for(unsigned i = 2; i < 4; i++) XZ10[i] &= ~' '; - for(unsigned i = 10; i < 16; i++) HEX [i] &= ~' '; -} - -//=========================================================================== -// Shared Library Representation - -char const SharedLibrary::library_suffix[] = -#if defined(_WIN32) - ".lib"; -#else - ".so"; -#endif - -#if defined(_WIN32) -namespace { - std::string translate_error_message(DWORD errid) { - std::string msg; - LPTSTR bufptr; - FormatMessage( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - nullptr, - errid, - MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - &bufptr, - 0, nullptr - ); - if(bufptr) msg = reinterpret_cast(bufptr); - LocalFree(bufptr); - return msg; - } -} -#endif - -SharedLibrary& SharedLibrary::open(std::string const &path) { - if(_lib) throw std::runtime_error("SharedLibrary still open for " + _path); - _lib = load(path); - _path = path; - return *this; -} - -SharedLibrary::handle_type SharedLibrary::load(std::string const &path) { - if(path.empty()) throw std::domain_error("Empty library path."); - -#if defined(_WIN32) - SetLastError(0); -#ifdef UNICODE - // Use LoadLibraryA explicitly on windows if UNICODE is defined - handle_type const lib = LoadLibraryA(path.c_str()); -#else - handle_type const lib = LoadLibrary(path.c_str()); -#endif - if(!lib) throw std::runtime_error(translate_error_message(GetLastError())); -#else - handle_type const lib = dlopen(path.c_str(), RTLD_LAZY | RTLD_GLOBAL); - if(!lib) throw std::runtime_error(dlerror()); -#endif - return lib; -} - -void SharedLibrary::unload() { - if(_lib) { -#if defined(_WIN32) - FreeLibrary(_lib); -#else - dlclose(_lib); -#endif - } -} - -std::optional SharedLibrary::getsymbol(char const *const name) { - void *sym; -#if defined(_WIN32) - sym = (void*)GetProcAddress(_lib, name); - if(!sym) -#else - dlerror(); // clear error - sym = dlsym(_lib, name); - char const *const err = dlerror(); - if(err) -#endif - return std::nullopt; - return std::make_optional(sym); -} - -//=========================================================================== -// xsi::Kernel - -char const *const Kernel::Xsi::FUNC_NAMES[EXTENT] = { - "xsi_get_value", "xsi_put_value", - "xsi_get_int_port", "xsi_get_str_port", - - "xsi_get_int", "xsi_get_port_number", - - "xsi_trace_all", "xsi_run", "xsi_restart", - "xsi_get_status", "xsi_get_error_info", - - "xsi_close" -}; - -#include -inline Kernel::Xsi::Xsi(SharedLibrary &lib) : _hdl(nullptr) { - // Resolve XSI Functions - for(unsigned i = 0; i < EXTENT; i++) { - _func[i] = resolve_or_throw(lib, FUNC_NAMES[i]); - } -} - -//--------------------------------------------------------------------------- -// Life Cycle -Kernel::Kernel(std::string const &kernel_lib) : _kernel_lib(kernel_lib), _xsi(_kernel_lib) {} - -Kernel::~Kernel() { - if(_design_lib) std::cerr << "Disposing XSI Kernel with open Design." << std::endl; -} - -void Kernel::open(std::string const &design_lib, s_xsi_setup_info const &setup_info) { - _design_lib.open(design_lib); - try { - auto const f = t_fp_xsi_open(resolve_or_throw(_design_lib, "xsi_open")); - xsiHandle const hdl = f(const_cast(&setup_info)); - if(!hdl) throw std::runtime_error("Loading of design failed"); - _xsi.setHandle(hdl); - - // Enumerate Ports - unsigned const port_count = xsi(xsiNumTopPorts); - std::unique_ptr ports { new Port[port_count] }; - for(unsigned i = 0; i < port_count; i++) new(&ports[i]) Port(*this, i); - _port_count = port_count; - _ports = std::move(ports); - } - catch(...) { - _design_lib.close(); - throw; - } -} -void Kernel::close() noexcept { - xsi(); - _xsi.setHandle(nullptr); - _design_lib.close(); - _ports.reset(); - - // Clean up Library State - std::optional const vptr = _kernel_lib.getsymbol("svTypeInfo"); - if(vptr) *((void**)*vptr) = nullptr; -} - -//=========================================================================== -// xsi::Port - -bool Port::hasUnknown() const { - unsigned const n = (width()+31) / 32; - s_xsi_vlog_logicval const *const p = buf(); - for(unsigned i = 0; i < n; i++) { - if(p[i].bVal) return true; - } - return false; -} - -bool Port::isZero() const { - unsigned const n = (width()+31) / 32; - s_xsi_vlog_logicval const *const p = buf(); - for(unsigned i = 0; i < n; i++) { - if(p[i].aVal) return false; - } - return true; -} - -std::string Port::as_binstr() const { - unsigned const w = width(); - std::string res(w, '?'); - - s_xsi_vlog_logicval const *si = buf(); - std::string::iterator di = res.end(); -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" - uint32_t a; - uint32_t b; - for(unsigned i = 0; i < w; i++) { - if((i & 31) == 0) { - a = si->aVal; - b = si->bVal; - si++; - } - *--di = XZ10[((b&1)<<1)|(a&1)]; - a >>= 1; - b >>= 1; - } -#pragma GCC diagnostic pop - return res; -} - -std::string Port::as_hexstr() const { - unsigned l = (width()+3)/4; - std::string res(l, '?'); - s_xsi_vlog_logicval const *si = buf(); - std::string::iterator di = res.end(); - - while(l > 0) { - uint32_t a = si->aVal; - uint32_t b = si->bVal; - si++; - - unsigned m = std::min(8u, l); - l -= m; - do { - unsigned const bm = b & 0xF; - unsigned const am = a & 0xF; - - *--di = !bm? HEX[am] : XZ10[3 - !(am&bm)]; - a >>= 4; - b >>= 4; - } - while(--m > 0); - } - return res; -} - -Port& Port::clear() { - unsigned const n = (width()+31) / 32; - s_xsi_vlog_logicval *const p = buf(); - std::fill(p, p+n, s_xsi_vlog_logicval { .aVal = 0u, .bVal = 0u }); - return *this; -} - -Port& Port::set_binstr(std::string const &val) { - std::string::const_iterator si = val.end(); - s_xsi_vlog_logicval *di = buf(); - - unsigned const n = (width()+31) / 32; - unsigned l = val.length(); - for(unsigned i = 0; i < n; i++) { - uint32_t a = 0; - uint32_t b = 0; - - unsigned const m = std::min(32u, l); - l -= m; - si -= m; - for(unsigned j = 0; j < m; j++) { - a <<= 1; - b <<= 1; - switch(*si++) { - case '1': - a |= 1; - case '0': - continue; - - default: - a |= 1; - case 'Z': - case 'z': - b |= 1; - continue; - } - } - si -= m; - - di->aVal = a; - di->bVal = b; - di++; - } - - return *this; -} - -Port& Port::set_hexstr(std::string const &val) { - std::string::const_iterator si = val.end(); - s_xsi_vlog_logicval *di = buf(); - - unsigned const n = (width()+31) / 32; - unsigned l = val.length(); - for(unsigned i = 0; i < n; i++) { - uint32_t a = 0; - uint32_t b = 0; - - unsigned const m = std::min(8u, l); - l -= m; - si -= m; - for(unsigned j = 0; j < m; j++) { - char c = *si++; - a <<= 4; - b <<= 4; - - if(('0' <= c) && c <= '9') a |= c & 0xF; - else { - c |= 0x20; - if(('a' <= c) && (c <= 'f')) a |= c - ('a'-10); - else { - b |= 0xF; - if(c != 'z') a |= 0xF; - } - } - } - si -= m; - - di->aVal = a; - di->bVal = b; - di++; - } - - return *this; -} diff --git a/finn_xsi/finn_xsi/xsi_finn.hpp b/finn_xsi/finn_xsi/xsi_finn.hpp deleted file mode 100644 index 4268657aef..0000000000 --- a/finn_xsi/finn_xsi/xsi_finn.hpp +++ /dev/null @@ -1,356 +0,0 @@ -/**************************************************************************** - * Copyright (C) 2025, Advanced Micro Devices, Inc. - * All rights reserved. - * - * SPDX-License-Identifier: BSD-3-Clause - * - * @brief FINN XSI++: C++ XSI Binding used by FINN. - * @author Thomas B. Preußer - ***************************************************************************/ -#ifndef XSI_FINN_HPP -#define XSI_FINN_HPP - -#include -#include -#include -#include -#include - -#include -#include - -#if defined(_WIN32) -# include -#else -# include -#endif - -#include "xsi.h" - - -namespace xsi { - -//=========================================================================== -// Shared Library Representation - -class SharedLibrary { -public: - static char const library_suffix[]; - -private: - using handle_type = -#if defined(_WIN32) - HINSTANCE; -#else - void*; -#endif - - //----------------------------------------------------------------------- - // Instance State -private: - handle_type _lib; - std::string _path; - - //----------------------------------------------------------------------- - // Life Cycle -public: - SharedLibrary() : _lib(nullptr), _path() {} - SharedLibrary(std::string const &path) : _lib(load(path)), _path(path) {} - ~SharedLibrary() { unload(); } - -private: - SharedLibrary(SharedLibrary const&) = delete; - SharedLibrary& operator=(SharedLibrary const&) = delete; - -public: - operator bool() const { return bool(_lib); } - SharedLibrary& open(std::string const &path); - SharedLibrary& close() { - unload(); - _lib = nullptr; - _path.clear(); - return *this; - } - -private: - static handle_type load(std::string const &path); - void unload(); - - //----------------------------------------------------------------------- - // Accessors -public: - std::string const& path() const { return _path; } - std::optional getsymbol(char const *const name); - -}; // class SharedLibrary - -//=========================================================================== -// xsi::Kernel - -template -class enumerator { - It _begin; - It _end; -public: - enumerator(It begin, It end) : _begin(begin), _end(end) {} - ~enumerator() {} -public: - It begin() const { return _begin; } - It end() const { return _end; } -}; - -class Design; -class Port; -class Kernel { - - //----------------------------------------------------------------------- - // Dispatch Table for XSI Functions - class Xsi { - //- Statics --------------------- - public: - // Function Indeces - static constexpr unsigned - get_value = 0, put_value = 1, - get_int_port = 2, get_str_port = 3, - - get_int = 4, get_port_number = 5, - - trace_all = 6, run = 7, restart = 8, - get_status = 9, get_error_info = 10, - - close = 11; - - private: - // Function Names & Types - static constexpr unsigned EXTENT = 12; - static char const *const FUNC_NAMES[EXTENT]; - using type_map = std::tuple< - // Port Access - t_fp_xsi_get_value, t_fp_xsi_put_value, - t_fp_xsi_get_int_port, t_fp_xsi_get_str_port, - - // Design Inspection - t_fp_xsi_get_int, t_fp_xsi_get_port_number, - - // Simulation Control & Status - t_fp_xsi_trace_all, t_fp_xsi_run, t_fp_xsi_restart, - t_fp_xsi_get_status, t_fp_xsi_get_error_info, - - // Closing - t_fp_xsi_close - >; - - //- Actual Contents ------------- - private: - xsiHandle _hdl; - void* _func[EXTENT]; - - //- Lifecycle: in-place structure inside Kernel only - public: - Xsi(SharedLibrary &lib); - ~Xsi() {} - private: - Xsi(Xsi const&) = delete; - Xsi& operator=(Xsi const&) = delete; - - //- Handle Update --------------- - public: - void setHandle(xsiHandle hdl) { _hdl = hdl; } - - //- XSI Function Invocation ----- - public: - template - auto invoke(Args&&... args) const { - auto const f = decltype(std::get(type_map()))(_func[FID]); - return (*f)(_hdl, std::forward(args)...); - } - - }; // class Xsi - -private: - // Instance State - SharedLibrary _kernel_lib; // Backing Kernel Library - Xsi _xsi; // XSI Dispatch Table - - // Optional State once a Design in open - SharedLibrary _design_lib; - unsigned _port_count; - std::unique_ptr _ports; - -public: - Kernel(std::string const &kernel_lib); - Kernel(Kernel const&) = delete; - Kernel& operator=(Kernel const&) = delete; - ~Kernel(); - - // Interface reserved for forwarded access through open Design -private: - friend Design; - friend Port; - template - auto xsi(Args&&... args) const { - return _xsi.invoke(std::forward(args)...); - } - - // Port Accessors inlined below and public through Design - Port* getPort(char const *const name); - Port const* getPort(char const *const name) const; - enumerator ports(); - enumerator ports() const; - - // Design con- & destruction hooks - void open(std::string const &design_lib, s_xsi_setup_info const &setup_info); - void close() noexcept; - -public: - // Hex printing manipulation - static void hex_in_lower(); - static void hex_in_upper(); - -}; // class Kernel - -//=========================================================================== -// xsi::Design - -// - non-copyable, non-movable handle for exposing simulation control. -class Design { - using Xsi = Kernel::Xsi; - Kernel &_kernel; - -public: - Design( - Kernel &kernel, - std::string const &design_lib, - s_xsi_setup_info const &setup_info - ) : _kernel(kernel) { kernel.open(design_lib, setup_info); } - Design( - Kernel &kernel, std::string const &design_lib, - char const *const log_file = nullptr, - char const *const wdb_file = nullptr - ) : Design(kernel, design_lib, s_xsi_setup_info { - .logFileName = const_cast(log_file), - .wdbFileName = const_cast(wdb_file) - }) {} - ~Design() { _kernel.close(); } - -private: - Design(Design const&) = delete; - Design& operator*(Design const&) = delete; - - //----------------------------------------------------------------------- - // Forwarded Access to Open Simulation - - // Simulation Control & Status -public: - void trace_all() { _kernel.xsi(); } - void run(XSI_INT64 const step) { _kernel.xsi(step); } - void restart() { _kernel.xsi(); } - - int get_status() const { return _kernel.xsi(); } - char const* get_error_info() const { return _kernel.xsi(); } - - // Port Access -public: - int num_ports() const { return _kernel._port_count; } - - Port* getPort(std::string const &name) { return _kernel.getPort(name.c_str()); } - Port const* getPort(std::string const &name) const { return _kernel.getPort(name.c_str()); } - - enumerator ports() { return _kernel.ports(); } - enumerator ports() const { return const_cast(_kernel).ports(); } - -}; // class Design - -//=========================================================================== -// xsi::Port - -// Only exists within controlled environment within Kernel with open Design. -class Port { - using Xsi = Kernel::Xsi; - Kernel &_kernel; - unsigned const _id; - std::unique_ptr const _buf; - -private: - // Con- and destruction under full control of Kernel - friend class Kernel; - Port() : _kernel(*static_cast(nullptr)), _id(0), _buf() {} - Port(Kernel &kernel, unsigned const id) - : _kernel(kernel), _id(id), - _buf(std::make_unique((width()+31)/32)) {} - Port(Port const&) = delete; - Port& operator=(Port const&) = delete; -public: - ~Port() {} - -public: - char const* name() const { return _kernel.xsi(_id, xsiNameTopPort); } - int dir() const { return _kernel.xsi(_id, xsiDirectionTopPort); } - unsigned width() const { return _kernel.xsi(_id, xsiHDLValueSize); } - - bool isInput() const { return dir() == xsiInputPort; } - bool isOutput() const { return dir() == xsiOutputPort; } - bool isInout() const { return dir() == xsiInoutPort; } - -private: - s_xsi_vlog_logicval* buf() { return _buf.get(); } - s_xsi_vlog_logicval const* buf() const { return _buf.get(); } - -public: - // Buffer Synchronization - Port& read() { - _kernel.xsi(_id, buf()); - return *this; - } - void write_back() { - _kernel.xsi(_id, buf()); - } - - // Inspection - bool hasUnknown() const; - bool isZero() const; - bool operator[](unsigned const idx) const { - return (buf()[idx/32].aVal >> (idx%32)) & 1; - } - - bool as_bool() const { return buf()->aVal & 1; } - unsigned as_unsigned() const { return buf()->aVal; } - std::string as_binstr() const; - std::string as_hexstr() const; - - // Manipulation - Port& clear(); - Port& set(unsigned val) { - s_xsi_vlog_logicval *const p = buf(); - p->aVal = val; - p->bVal = 0; - return *this; - } - Port& set_binstr(std::string const &val); - Port& set_hexstr(std::string const &val); - -}; // class Port - -// Inlined Kernel Port Accessors - -inline Port* Kernel::getPort(char const *const name) { - int const id = xsi(name); - return (id == -1)? nullptr : &_ports[id]; -} -inline Port const* Kernel::getPort(char const *const name) const { - int const id = xsi(name); - return (id == -1)? nullptr : &_ports[id]; -} - -inline enumerator Kernel::ports() { - Port *const beg = _ports.get(); - return { beg, beg + _port_count }; -} -inline enumerator Kernel::ports() const { - Port const *const beg = _ports.get(); - return { beg, beg + _port_count }; -} - -} // namespace xsi - -#endif From a047534ad1119c29311d6dfec04861bf8670d865 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 17 Oct 2025 17:42:03 +0200 Subject: [PATCH 004/170] Initial layer parallel simulation. Clang format, Python executed compilation. Added boost. --- .gitignore | 7 + finn_xsi/finn_xsi/.clang-format | 46 +++ finn_xsi/finn_xsi/CMakeLists.txt | 4 + finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 12 +- finn_xsi/finn_xsi/include/Clock.h | 2 +- finn_xsi/finn_xsi/include/Simulation.hpp | 232 +++++++++++---- finn_xsi/finn_xsi/rtlsim_config.hpp.template | 16 +- finn_xsi/finn_xsi/rtlsim_xsi.cpp | 273 ------------------ src/finn/core/rtlsim_exec.py | 104 ++++--- .../transformation/fpgadataflow/simulation.py | 18 +- 10 files changed, 323 insertions(+), 391 deletions(-) create mode 100644 finn_xsi/finn_xsi/.clang-format delete mode 100644 finn_xsi/finn_xsi/rtlsim_xsi.cpp diff --git a/.gitignore b/.gitignore index 8baee2396d..0115f7ac39 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,13 @@ poetry.lock *.code-workspace .env +# Cmake files +**/CMakeFiles +**/cmake_install.cmake +**/CMakeCache.txt +**/compile_commands.json +**/.cache + # Package files *.egg *.eggs/ diff --git a/finn_xsi/finn_xsi/.clang-format b/finn_xsi/finn_xsi/.clang-format new file mode 100644 index 0000000000..d4573c3508 --- /dev/null +++ b/finn_xsi/finn_xsi/.clang-format @@ -0,0 +1,46 @@ +BasedOnStyle: Chromium +AccessModifierOffset: '1' +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: 'true' +AlignTrailingComments: 'true' +AllowAllArgumentsOnNextLine: 'true' +AllowShortBlocksOnASingleLine: 'true' +AllowShortFunctionsOnASingleLine: 'true' +AllowShortCaseLabelsOnASingleLine: 'false' +AlwaysBreakTemplateDeclarations: 'Yes' +BinPackParameters: 'true' +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +ColumnLimit: '240' +Cpp11BracedListStyle: 'true' +FixNamespaceComments: 'true' +IndentCaseLabels: 'true' +IndentPPDirectives: BeforeHash +IndentWidth: '4' +IndentWrappedFunctionNames: 'true' +IncludeBlocks: Regroup +KeepEmptyLinesAtTheStartOfBlocks: 'false' +Language: Cpp +MaxEmptyLinesToKeep: '2' +NamespaceIndentation: All +PointerAlignment: Left +ReflowComments: 'true' +SortIncludes: 'true' +SortUsingDeclarations: 'true' +SpaceAfterCStyleCast: 'true' +SpaceAfterLogicalNot: 'false' +SpaceAfterTemplateKeyword: 'false' +SpaceBeforeCpp11BracedList: 'false' +SpaceBeforeCtorInitializerColon: 'true' +SpaceBeforeInheritanceColon: 'true' +SpaceInEmptyParentheses: 'false' +SpacesInAngles: 'false' +SpacesInCStyleCastParentheses: 'false' +SpacesInContainerLiterals: 'false' +SpacesInParentheses: 'false' +SpacesInSquareBrackets: 'false' +TabWidth: '4' +--- +Language: Json +BasedOnStyle: llvm diff --git a/finn_xsi/finn_xsi/CMakeLists.txt b/finn_xsi/finn_xsi/CMakeLists.txt index e2e517df8a..66bd79c1fe 100644 --- a/finn_xsi/finn_xsi/CMakeLists.txt +++ b/finn_xsi/finn_xsi/CMakeLists.txt @@ -78,6 +78,10 @@ list(POP_BACK CMAKE_MESSAGE_INDENT) #indent -1 file(GLOB_RECURSE CORE_SRC src/*.cpp) add_executable(LayerSimulationBackend LayerSimulationBackend.cpp ${CORE_SRC}) +# Add boost for IPC +find_package(Boost REQUIRED) +target_include_directories(LayerSimulationBackend SYSTEM PUBLIC ${Boost_INCLUDE_DIRS}) + # Include the rtlsim wrapper directory itself target_include_directories(LayerSimulationBackend PUBLIC "${CMAKE_BINARY_DIR}") diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index f21da89ff0..adbb7471e2 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -6,10 +6,16 @@ #include #include #include +#include +#include int main(){ - Simulation<1, 1> sim("kernel_lib", "design_lib", "xsim_log_file", "trace_file", - std::array{StreamDescriptor{"input", 1024, 10}}, - std::array{StreamDescriptor{"output", 1024, 10}}); + // TODO: Give proper names for previous and name + Simulation<1, 1, true, true> sim("prev", std::string(nodeName), kernel_libname, design_libname, "xsim_log_file.txt", "trace_file.txt", + std::array{StreamDescriptor{istream_descs[0].name, istream_descs[0].job_size, istream_descs[0].job_ticks}}, + std::array{StreamDescriptor{ostream_descs[0].name, ostream_descs[0].job_size, ostream_descs[0].job_ticks}}); + + // TODO: Run correct frames + sim.runForFrames(10); return 0; } diff --git a/finn_xsi/finn_xsi/include/Clock.h b/finn_xsi/finn_xsi/include/Clock.h index e943cfa804..2c13d9f6eb 100644 --- a/finn_xsi/finn_xsi/include/Clock.h +++ b/finn_xsi/finn_xsi/include/Clock.h @@ -14,7 +14,7 @@ class Clock { Clock(Clock const&) = delete; Clock& operator=(Clock const&) = delete; Clock(xsi::Design& design); - template + template friend class Simulation; public: diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 5c9f65bd9c..16be8a6871 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -7,69 +7,199 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include "boost/interprocess/creation_tags.hpp" +#include "boost/interprocess/interprocess_fwd.hpp" -template< size_t IStreamsSize, size_t OStreamsSize > +namespace ipc = boost::interprocess; + +enum class SimulationInterfaceType { PRODUCING, CONSUMING }; + +template +class SimulationInterface { + private: + ipc::managed_shared_memory shmem; + std::atomic_bool ready; + std::atomic_bool valid; + std::atomic_bool unread; + + public: + SimulationInterface(const char* shmIdentifier) { + ipc::shared_memory_object::remove(shmIdentifier); + shmem = ipc::managed_shared_memory(ipc::open_or_create, shmIdentifier, ShmemSize); + ready = shmem.find_or_construct("ready")(true); + valid = shmem.find_or_construct("valid")(false); + unread = shmem.find_or_construct("unread")(false); + } + + ~SimulationInterface() { + // TODO: Called implicitly? + shmem.destroy("ready"); + shmem.destroy("valid"); + shmem.destroy("unread"); + } + + /// Wait until predecessor has sent recent data. Then send ready. + bool communicate(bool sendReady) requires (T == SimulationInterfaceType::CONSUMING) { + while (!unread) {} + ready = sendReady; + auto validValue = valid.load(); + unread = false; + return validValue; + } + + /// Wait until successor has read the previous data. Then send valid. + bool communicate(bool sendValid) requires (T == SimulationInterfaceType::PRODUCING) { + while (unread) {} + valid = sendValid; + auto readyValue = ready.load(); + unread = true; + return readyValue; + } + +}; + + +template class Simulation { -public: - xsi::Kernel kernel; - xsi::Design top; - std::array istreams; - std::array ostreams; - Clock clk; - - void clearPorts() noexcept { - // Clear all input ports - for (xsi::Port &p : top.ports()) { - if (p.isInput()) { - p.clear().write_back(); - } + private: + using ConsumerInterface = SimulationInterface; + using ProducerInterface = SimulationInterface; + std::array, IStreamsSize> fromProducerInterface; + std::array, OStreamsSize> toConsumerInterface; + std::ofstream readyLog; + std::ofstream validLog; + + public: + xsi::Kernel kernel; + xsi::Design top; + std::array istreams; + std::array ostreams; + Clock clk; + + + void clearPorts() noexcept { + // Clear all input ports + for (xsi::Port& p : top.ports()) { + if (p.isInput()) { + p.clear().write_back(); + } + } } - } - - void reset() noexcept { - xsi::Port &rst_n = top.getPort("ap_rst_n"); - // Reset all Inputs, Wait for Reset Period - rst_n.set(0).write_back(); - for (unsigned i = 0; i < 16; i++) { - clk.toggle_clk(); + + void reset() noexcept { + xsi::Port& rst_n = top.getPort("ap_rst_n"); + // Reset all Inputs, Wait for Reset Period + rst_n.set(0).write_back(); + for (unsigned i = 0; i < 16; i++) { + clk.toggle_clk(); + } + rst_n.set(1).write_back(); } - rst_n.set(1).write_back(); - } - - Simulation(const std::string &kernel_lib, const std::string &design_lib, - const char *xsim_log_file, const char *trace_file, std::array istream_descs, - std::array ostream_descs) - : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), - clk(top) { - if (trace_file) { - top.trace_all(); + + Simulation(const std::string& previousName, const std::string& name, const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, + std::array _ostream_descs) + : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), clk(top) { + if (trace_file) { + top.trace_all(); + } + + // Find I/O Streams and initialize their Status + for (size_t i = 0; i < _istream_descs.size(); ++i) { + istreams[i] = S_AXIS_Control{top, clk, std::data(_istream_descs)[i].job_size, std::data(_istream_descs)[i].job_ticks, std::data(_istream_descs)[i].name}; + } + for (size_t i = 0; i < _ostream_descs.size(); ++i) { + ostreams[i] = M_AXIS_Control{top, clk, std::data(_ostream_descs)[i].job_size, std::data(_ostream_descs)[i].name}; + } + + // Find Global Control & Run Startup Sequence + clearPorts(); + reset(); + + // Make all Inputs valid & all Outputs ready + for (auto&& s : istreams) { + s.valid(); + } + for (auto&& s : ostreams) { + s.ready(); + } + + if constexpr(SingleNode) { + // Create simulation interfaces + for (std::size_t i = 0; i < IStreamsSize; ++i) { + fromProducerInterface[i] = std::make_unique((previousName + std::to_string(i)).c_str()); + } + for (std::size_t i = 0; i < OStreamsSize; ++i) { + toConsumerInterface[i] = std::make_unique((name + std::to_string(i)).c_str()); + } + + // Save simulation input output behaviour + if constexpr(LoggingEnabled) { + readyLog.open("ready_log.txt"); + validLog.open("valid_log.txt"); + } + } else { + // TODO + // Entire design in one simulation + } } - // Find I/O Streams and initialize their Status - for (size_t i = 0; i < istream_descs.size(); ++i) { - istreams[i] = - S_AXIS_Control{top, clk, std::data(istream_descs)[i].job_size, - std::data(istream_descs)[i].job_ticks, - std::data(istream_descs)[i].name}; + /// Read valid signal from producer, write own ready signal to it + void updateFromProducer() requires (SingleNode) { + for (std::size_t i = 0; i < IStreamsSize; ++i) { + istreams[i].valid(fromProducerInterface[i]->communicate(istreams[i].is_ready())); + } } - for (size_t i = 0; i < ostream_descs.size(); ++i) { - ostreams[i] = - M_AXIS_Control{top, clk, std::data(ostream_descs)[i].job_size, - std::data(ostream_descs)[i].name}; + + /// Read ready signal from consumer, write own valid signal to it. + void updateToConsumer() requires (SingleNode) { + for (std::size_t i = 0; i < OStreamsSize; ++i) { + ostreams[i].ready(toConsumerInterface[i]->communicate(ostreams[i].is_valid())); + } } - // Find Global Control & Run Startup Sequence - clearPorts(); - reset(); + void runSingleCycle() { + if constexpr(SingleNode) { + clk.toggle_clk(); + // Order: Send update forward to consumer, read update from producer second + updateFromProducer(); + updateToConsumer(); - // Make all Inputs valid & all Outputs ready - for (auto &&s : istreams) { - s.valid(); + // Log the signals that this simulations set (ready to predecessor, valid to successor) + if constexpr(LoggingEnabled) { + for (S_AXIS_Control& stream : istreams) { + readyLog << stream.is_ready() << " "; + } + readyLog << "\n"; + for (M_AXIS_Control& stream : ostreams) { + validLog << stream.is_valid() << " "; + } + validLog << "\n"; + } + } else { + // TODO + // Single design case + } } - for (auto &&s : ostreams) { - s.ready(); + + /// Run for the given number of frames (frames * job_size cycles or transactions) + void runForFrames(std::size_t frames) { + if constexpr(SingleNode) { + // TODO: Multiple IO streams: Current cycle count is hardcoded for the number of inputs of the first stream + std::size_t cycleCount = frames * istreams[0].job_size; + for (std::size_t i = 0; i < cycleCount; ++i) { + runSingleCycle(); + } + } else { + // TODO + // Single design case + } } - } }; #endif /* SIMULATION */ diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index 9fa8db4e24..af30540efa 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -14,18 +14,8 @@ #include #include -/**** The following two defines are relevant for FIFO Simulation only ****/ - -#cmakedefine MPI_FOUND - -// If set, use this as the FIFO start size and directly -// skip to iterative sizing -#cmakedefine FIFO_START_SIZE ${FIFO_START_SIZE} - - - - /**** General RTLSIM Configuration Parameters ****/ +const char* nodeName = "@NODE_NAME@"; struct stream_desc { char const *name; @@ -43,10 +33,10 @@ static char const design_libname[] = "xsim.dir/@TOP_MODULE_NAME@/xsimk.so"; // AXI stream descriptors {stream_name, transactions_per_inference} // input AXI stream descriptors -static constexpr std::initializer_list const istream_descs { @ISTREAM_DESC@ }; +std::array istream_descs { @ISTREAM_DESC@ }; // output AXI stream descriptors -static constexpr std::initializer_list const ostream_descs { @OSTREAM_DESC@ }; +std::array ostream_descs { @OSTREAM_DESC@ }; // number of inferences to perform constexpr unsigned n_inferences = @N_INFERENCES@; diff --git a/finn_xsi/finn_xsi/rtlsim_xsi.cpp b/finn_xsi/finn_xsi/rtlsim_xsi.cpp deleted file mode 100644 index d4fe79581d..0000000000 --- a/finn_xsi/finn_xsi/rtlsim_xsi.cpp +++ /dev/null @@ -1,273 +0,0 @@ -/**************************************************************************** - * Copyright (C) 2025, Advanced Micro Devices, Inc. - * All rights reserved. - * - * SPDX-License-Identifier: BSD-3-Clause - * - * @brief Driver harness demo running a FINN IP core. - * @author Yaman Umuroğlu - * @author Thomas B. Preußer - ***************************************************************************/ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "xsi_finn.hpp" -#include "rtlsim_config.hpp" - -int main(int argc, char *argv[]) { - - // Load Kernel and Design - xsi::Kernel kernel(kernel_libname); - xsi::Design top(kernel, design_libname, xsim_log_filename, trace_filename); - using Port = xsi::Port; - if(trace_filename) { - // TODO make tracing more finer-grain if possible? - top.trace_all(); - } - - // Ultimate Simulation Summary - std::string synopsis; - - { // RTL Simulation - - // Simulation Report Statistics - size_t iters = 0; - size_t timeout = 0; - size_t itodo = istream_descs.size(); - size_t otodo = ostream_descs.size(); - size_t omute = ostream_descs.size(); - - // Find I/O Streams and initialize their Status - struct stream_status { - char const *name; - Port &port_vld; - Port &port_rdy; - - // Job Size and Transaction Statistics - size_t job_size; - size_t job_txns; // [0:job_size] - size_t total_txns; - size_t first_complete; // First completion timestamp - - union { - // Input Stream - struct { - size_t job_ticks; // throttle if job_size < job_ticks - size_t await_iter; // iteration allowing start of next job - }; - // Output Stream - struct { - size_t last_complete; - size_t interval; - }; - }; - - public: - stream_status( - char const *name, Port &port_vld, Port &port_rdy, - size_t job_size, size_t job_ticks - ) : name(name), port_vld(port_vld), port_rdy(port_rdy), job_size(job_size), - job_txns(0), total_txns(0), - first_complete(0), job_ticks(job_ticks), await_iter(job_ticks) {} - }; - std::vector istreams; - std::vector ostreams; - for(auto t : { std::tie(istream_descs, istreams), std::tie(ostream_descs, ostreams) }) { - for(stream_desc const &desc : std::get<0>(t)) { - std::string const name(desc.name); - Port *const vld = top.getPort(name + "_tvalid"); - Port *const rdy = top.getPort(name + "_tready"); - if(!vld || !rdy) { - std::cerr << "Unable to find controls for " << desc.name << std::endl; - return 1; - } - - std::get<1>(t).emplace_back(desc.name, *vld, *rdy, desc.job_size, desc.job_ticks); - } - } - - // Find Global Control & Run Startup Sequence - std::function cycle; - { - Port *const clk = top.getPort("ap_clk"); - Port *const clk2x = top.getPort("ap_clk2x"); - Port *const rst_n = top.getPort("ap_rst_n"); - if(!clk) { - std::cerr << "No clock found on the design." << std::endl; - return 1; - } - cycle = clk2x? - std::function([&top, clk, clk2x](bool const up) mutable { - clk->set(up).write_back(); - clk2x->set(1).write_back(); - top.run(5); - clk2x->set(0).write_back(); - top.run(5); - }) : - std::function([&top, clk](bool const up) mutable { - clk->set(up).write_back(); - top.run(5); - }); - - // Reset all Inputs, Wait for Reset Period - for(Port &p : top.ports()) { if(p.isInput()) p.clear().write_back(); }; - if(rst_n) { - for(unsigned i = 0; i < 16; i++) { cycle(0); cycle(1); } - rst_n->set(1).write_back(); - } - } - - // Start Stream Feed and Capture - std::cout << "Starting data feed with idle-output timeout of " << max_iters << " cycles ...\n" << std::endl; - - // Make all Inputs valid & all Outputs ready - for(auto &s : istreams) s.port_vld.set(1).write_back(); - for(auto &s : ostreams) s.port_rdy.set(1).write_back(); - - // Enter Simulation Loop and track Progress - auto const begin = std::chrono::steady_clock::now(); - std::vector> to_write; - while(true) { - - //------------------------------------------------------------------- - // Clock down - then read signal updates from design - cycle(0); - - // check for transactions on input streams - for(auto &s : istreams) { - bool const vld = s.port_vld[0]; - bool const rdy = s.port_rdy.read()[0]; - if(vld && !rdy) continue; - - // Track successgul Transactions - if(vld) { - s.job_txns++; - if(++s.total_txns == s.job_size * n_inferences) itodo--; - } - - // Proceed according to Throttling Rate - if((s.job_txns < s.job_size) || !(iters < s.await_iter)) { - if(s.total_txns < s.job_size * n_inferences) { - if(!vld) to_write.emplace_back(s.port_vld.set(1)); - if(s.job_txns == s.job_size) { - s.job_txns = 0; - s.await_iter = iters + s.job_ticks; - } - continue; - } - } - if(vld) to_write.emplace_back(s.port_vld.set(0)); - } - - { // check for transactions on the output streams - bool dead = true; - for(auto &s : ostreams) { - if(s.port_rdy[0] && s.port_vld.read()[0]) { - size_t const txns = ++s.total_txns; - if(txns == s.job_size) { - s.first_complete = iters; - omute--; - } - if(++s.job_txns == s.job_size) { - s.interval = iters - s.last_complete; - s.last_complete = iters; - s.job_txns = 0; - } - if(txns >= s.job_size * n_inferences) { - if(txns == s.job_size * n_inferences) otodo--; - else { - std::cerr << "Spurious output on " << s.name << std::endl; - to_write.emplace_back(s.port_rdy.set(0)); - } - } - dead = false; - } - } - timeout = dead? timeout + 1 : 0; - } - - //------------------------------------------------------------------- - // Clock up - then write signal updates back to design - cycle(1); - - // Write back Ports with registered updates - for(Port &p : to_write) p.write_back(); - to_write.clear(); - - // Show a progress message once in a while - if(++iters % 10000 == 0) { - std::cout - << '@' << iters << " ticks / " - << std::chrono::duration_cast(std::chrono::steady_clock::now() - begin).count() << "s:"; - for(auto const &s : istreams) { - std::cout << '\t' << s.name << '=' << ((100 * s.total_txns) / (n_inferences * s.job_size)) << '%'; - } - for(auto const &s : ostreams) { - std::cout << '\t' << s.name << '=' << ((100 * s.total_txns) / (n_inferences * s.job_size)) << '%'; - } - std::cout << "\tMute Outputs: " << omute << std::endl; - } - - // Check for exit - if((timeout > max_iters) || (!itodo && !otodo)) break; - } - - size_t total_in_txns = 0; - for(auto const &s : istreams) total_in_txns += s.total_txns; - - size_t total_out_txns = 0; - size_t firstout_latency = 0; - size_t max_interval = 0; - for(auto const &s : ostreams) { - total_out_txns += s.total_txns; - firstout_latency = std::max(firstout_latency, s.first_complete); - max_interval = std::max(max_interval, s.interval); - } - - std::ostringstream bld; - bld << - "N_IN_TXNS\t" << total_in_txns << "\n" - "N_OUT_TXNS\t" << total_out_txns << "\n" - "cycles\t" << iters << "\n" - "N\t" << n_inferences << "\n" - "latency_cycles\t" << firstout_latency << "\n" - "interval_cycles\t" << max_interval << "\n" - "TIMEOUT\t" << (timeout > max_iters? "1" : "0") << "\n" - "UNFINISHED_INS\t" << itodo << "\n" - "UNFINISHED_OUTS\t" << otodo << "\n" - "RUNTIME_S\t" << std::chrono::duration_cast(std::chrono::steady_clock::now() - begin).count(); - synopsis = bld.str(); - - } // done simulation - - // Dump Simulation Statistics to stdout and results.txt - std::cout << '\n' << synopsis << std::endl; - - { // Log error info to file - std::ofstream error_file("fifosim.err", std::ios::out | std::ios::trunc); - error_file << top.get_error_info(); - } - - { // Synopsis and `max_count` readings to results file - std::ofstream results_file("results.txt", std::ios::out | std::ios::trunc); - results_file << synopsis << std::endl; - for(Port &p : top.ports()) { - if(p.isOutput()) { - char const *const name = p.name(); - if(std::strncmp(name, "maxcount", 8) == 0) { - p.read(); - results_file << name << '\t' << p.as_unsigned() << std::endl; - } - } - } - } - - return 0; -} diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index ab5626b334..57d5156924 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -33,6 +33,9 @@ import numpy as np import os +import shlex +import sys +from pathlib import Path from qonnx.custom_op.registry import getCustomOp from subprocess import CalledProcessError @@ -43,7 +46,8 @@ make_build_dir, ) from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy -from finn.util.exception import FINNError +from finn.util.exception import FINNError, FINNUserError +from finn.util.logging import log def prep_rtlsim_io_dict(model, execution_context): @@ -187,9 +191,6 @@ def rtlsim_exec_cppxsi( sim_rel = "xsim.dir" + sim_rel # prepare the C++ sim driver template finnxsi_dir = os.environ["FINN_XSI"] - fifosim_config_fname = finnxsi_dir + "/rtlsim_config.hpp.template" - with open(fifosim_config_fname, "r") as f: - fifsom_config_template = f.read() instream_iters = [] outstream_iters = [] @@ -249,62 +250,69 @@ def rtlsim_exec_cppxsi( "TOP_MODULE_NAME": top_module_name, # top-level AXI stream descriptors "ISTREAM_DESC": instream_descrs_str, + "ISTREAM_LEN": len(instream_names), "OSTREAM_DESC": outstream_descrs_str, + "OSTREAM_LEN": len(outstream_names), # control tracing and trace filename "TRACE_FILE": "nullptr" if trace_file is None else f'"{trace_file}"', # sim kernel .so to use (depends on Vivado version) "SIMKERNEL_SO": finnxsi.get_simkernel_so(), # log file for xsi (not the sim driver) "XSIM_LOG_FILE": '"xsi.log"', + # Node name in case of single-node simulation + "NODE_NAME": model.graph.node[0].name, } + + fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" + fsim_config = fifosim_config_fname.read_text() for key, val in template_dict.items(): - fifsom_config_template = fifsom_config_template.replace(f"@{key}@", str(val)) - with open(sim_base + "/rtlsim_config.hpp", "w") as f: - f.write(fifsom_config_template) - - vivado_incl_dir = get_vivado_root() + "/data/xsim/include" - # launch g++ to compile the rtlsim executable - build_cmd = [ - "g++", - f"-I{finnxsi_dir}", - f"-I{vivado_incl_dir}", - f"-I{sim_base}", - "-std=c++17", - "-O3", - "-o", - "rtlsim_xsi", - f"{finnxsi_dir}/rtlsim_xsi.cpp", - f"{finnxsi_dir}/xsi_finn.cpp", - "-ldl", - "-lrt", - ] - # write compilation command to a file for easy re-running/debugging - with open(sim_base + "/compile_rtlsim.sh", "w") as f: - f.write(" ".join(build_cmd)) + fsim_config = fsim_config.replace(f"@{key}@", str(val)) + + # Write the config to the simulation directory + rtlsim_config = Path(sim_base) / "rtlsim_config.hpp" + rtlsim_config.write_text(fsim_config) + + # Building the whole simulation + # Running CMake first + cmake_call = f"{sys.executable} -m cmake -S {finnxsi_dir} -B {sim_base}" + log.info(f"Running cmake on RTLSIM Wrapper in {sim_base}") + try: + launch_process_helper( + shlex.split(cmake_call), cwd=finnxsi_dir, print_stdout=True, proc_env=os.environ.copy() + ) + except CalledProcessError as e: + raise FINNError(f"Failed to run cmake in {sim_base}") from e + + # Calling make to actually build the simulation + makefile = Path(sim_base) / "Makefile" + if not makefile.exists(): + raise FINNUserError(f"Failed to create Makefile in {sim_base}!") try: - launch_process_helper(build_cmd, cwd=sim_base, print_stdout=False) - except CalledProcessError: - raise FINNError("Failed to compile rtlsim executable") - if not os.path.isfile(sim_base + "/rtlsim_xsi"): - raise FINNError("Failed to compile rtlsim executable") - - # launch the rtlsim executable - runsim_cmd = ["bash", "run_rtlsim.sh"] - with open(sim_base + "/run_rtlsim.sh", "w") as f: - f.write("./rtlsim_xsi > rtlsim_xsi_log.txt") - launch_process_helper(runsim_cmd, cwd=sim_base) + launch_process_helper(["make"], proc_env=os.environ.copy(), cwd=sim_base) + except CalledProcessError as e: + raise FINNUserError(f"Failed to create executable in {sim_base}!") from e + + # TODO: Fix name for general rtlsim + simulation_executable = Path(sim_base) / "LayerSimulationBackend" + assert simulation_executable.exists() + + # Prepare the script to run the simulation + # (important to specify LD_LIBRARY_PATH here for XSI to work correctly) + runsim = Path(sim_base) / "run_fifosim.sh" + ld_library_path = get_vivado_root() + "/lib/lnx64.o" + runsim.write_text(f"LD_LIBRARY_PATH={ld_library_path}:$LD_LIBRARY_PATH {simulation_executable}") + + # Actually run the simulation + out, err = launch_process_helper( + ["bash", runsim.name], cwd=sim_base, proc_env=os.environ.copy() + ) + + # TODO: remove output printing + log.warning(f"{model.graph.node[0].name}: {out}") # parse results file and return dict - results_filename = sim_base + "/results.txt" - with open(results_filename, "r") as f: - results = f.read().strip().split("\n") - ret_dict = {} - for result_line in results: - key, val = result_line.split("\t") - ret_dict[key] = int(val) - if "TIMEOUT" in ret_dict.keys(): - assert ret_dict["TIMEOUT"] == 0, f"XSI C++ simulation timed out, see {results_filename}" - return ret_dict + # TODO + return {} def rtlsim_exec_finnxsi(model, execution_context, pre_hook=None, post_hook=None): diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index d45ef2bba3..551304b7e9 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,11 +1,12 @@ """Manage FINN simulation variants.""" import onnx import os -from concurrent.futures import Future, ProcessPoolExecutor +from concurrent.futures import Future, ThreadPoolExecutor from copy import deepcopy from onnx import NodeProto, TensorProto from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp +from qonnx.transformation.base import Transformation from typing import TYPE_CHECKING, Any, cast from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -113,7 +114,7 @@ def _run_simulation(node_index: int) -> Any: workers = int(os.environ["NUM_DEFAULT_WORKERS"]) futures: list[Future] = [] results = {} - with ProcessPoolExecutor(max_workers=workers) as pool: + with ThreadPoolExecutor(max_workers=workers) as pool: for i in range(len(self.model.graph.node)): futures.append(pool.submit(_run_simulation, i)) pool.shutdown(wait=True) @@ -131,3 +132,16 @@ def run_sim_complete(self) -> Any: def run_sim_single_node(self, node: Any) -> Any: raise NotImplementedError() + + +# TODO: Just a test transformation. Will be integrated properly later +class RunLayerParallelSimulation(Transformation): # noqa + def __init__(self, fpgapart: str, clk_ns: float) -> None: # noqa + super().__init__() + self.fpgapart = fpgapart + self.clk_ns = clk_ns + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + sim = Simulation(model, self.fpgapart, self.clk_ns) + sim.run_sim_node_parallel_isolated(1) + return model, False From c2785728ead1b680422643fde9b12e50198ac5ba Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 17 Oct 2025 18:39:34 +0200 Subject: [PATCH 005/170] Passing node names to simulation. Improve simulation code --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 13 ++++-- finn_xsi/finn_xsi/include/Simulation.hpp | 40 ++++++++++++++----- finn_xsi/finn_xsi/rtlsim_config.hpp.template | 6 ++- src/finn/core/rtlsim_exec.py | 8 ++++ .../fpgadataflow/set_fifo_depths.py | 11 ++++- .../transformation/fpgadataflow/simulation.py | 14 +++++-- 6 files changed, 73 insertions(+), 19 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index adbb7471e2..375cf509c9 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -11,9 +11,16 @@ int main(){ // TODO: Give proper names for previous and name - Simulation<1, 1, true, true> sim("prev", std::string(nodeName), kernel_libname, design_libname, "xsim_log_file.txt", "trace_file.txt", - std::array{StreamDescriptor{istream_descs[0].name, istream_descs[0].job_size, istream_descs[0].job_ticks}}, - std::array{StreamDescriptor{ostream_descs[0].name, ostream_descs[0].job_size, ostream_descs[0].job_ticks}}); + Simulation<1, 1, true, SingleNode> sim( + kernel_libname, + design_libname, + "xsim_log_file.txt", + "trace_file.txt", + std::array{StreamDescriptor{istream_descs[0].name, istream_descs[0].job_size, istream_descs[0].job_ticks}}, + std::array{StreamDescriptor{ostream_descs[0].name, ostream_descs[0].job_size, ostream_descs[0].job_ticks}}, + previousNodeName, + currentNodeName + ); // TODO: Run correct frames sim.runForFrames(10); diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 16be8a6871..30f087dee1 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -10,11 +10,14 @@ #include #include #include +#include #include #include #include -#include "boost/interprocess/creation_tags.hpp" -#include "boost/interprocess/interprocess_fwd.hpp" +#include +#include +#include +#include namespace ipc = boost::interprocess; @@ -65,13 +68,15 @@ class SimulationInterface { }; +/// Create a new simulation. To run single-node simulations with IPC, enable SingleNode +/// and pass previousNodeName and nodeName to identify shared memory of adjacent node simulation processes template class Simulation { private: using ConsumerInterface = SimulationInterface; using ProducerInterface = SimulationInterface; - std::array, IStreamsSize> fromProducerInterface; - std::array, OStreamsSize> toConsumerInterface; + std::optional, IStreamsSize>> fromProducerInterface; + std::optional, OStreamsSize>> toConsumerInterface; std::ofstream readyLog; std::ofstream validLog; @@ -102,9 +107,16 @@ class Simulation { rst_n.set(1).write_back(); } - Simulation(const std::string& previousName, const std::string& name, const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, - std::array _ostream_descs) - : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), clk(top) { + Simulation( + const std::string& kernel_lib, + const std::string& design_lib, + const char* xsim_log_file, + const char* trace_file, + std::array _istream_descs, + std::array _ostream_descs, + std::optional previousNodeName = std::nullopt, + std::optional currentNodeName = std::nullopt + ) : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), clk(top) { if (trace_file) { top.trace_all(); } @@ -130,12 +142,18 @@ class Simulation { } if constexpr(SingleNode) { + if (!previousNodeName || !currentNodeName) { + throw std::runtime_error("Cannot construct single-node simulation without specifying the previous and this nodes names."); + } + fromProducerInterface = std::make_optional, IStreamsSize>>(); + toConsumerInterface = std::make_optional, OStreamsSize>>(); + // Create simulation interfaces for (std::size_t i = 0; i < IStreamsSize; ++i) { - fromProducerInterface[i] = std::make_unique((previousName + std::to_string(i)).c_str()); + (*fromProducerInterface)[i] = std::make_unique((*previousNodeName + std::to_string(i)).c_str()); } for (std::size_t i = 0; i < OStreamsSize; ++i) { - toConsumerInterface[i] = std::make_unique((name + std::to_string(i)).c_str()); + (*toConsumerInterface)[i] = std::make_unique((*currentNodeName + std::to_string(i)).c_str()); } // Save simulation input output behaviour @@ -152,14 +170,14 @@ class Simulation { /// Read valid signal from producer, write own ready signal to it void updateFromProducer() requires (SingleNode) { for (std::size_t i = 0; i < IStreamsSize; ++i) { - istreams[i].valid(fromProducerInterface[i]->communicate(istreams[i].is_ready())); + istreams[i].valid((*fromProducerInterface)[i]->communicate(istreams[i].is_ready())); } } /// Read ready signal from consumer, write own valid signal to it. void updateToConsumer() requires (SingleNode) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - ostreams[i].ready(toConsumerInterface[i]->communicate(ostreams[i].is_valid())); + ostreams[i].ready((*toConsumerInterface)[i]->communicate(ostreams[i].is_valid())); } } diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index af30540efa..e9566888e5 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -13,9 +13,13 @@ #include #include +#include +#include /**** General RTLSIM Configuration Parameters ****/ -const char* nodeName = "@NODE_NAME@"; +const std::optional currentNodeName = "@NODE_NAME@"; +const std::optional previousNodeName = @PREVIOUS_NODE_NAME@; +constexpr bool SingleNode = @SINGLE_NODE@; struct stream_desc { char const *name; diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index 57d5156924..17b91274ae 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -122,6 +122,8 @@ def file_to_basename(x): def rtlsim_exec_cppxsi( model, execution_context, + is_single_node, + previous_node_name=None, dummy_data_mode=False, timeout_cycles=None, throttle_cycles=0, @@ -261,6 +263,12 @@ def rtlsim_exec_cppxsi( "XSIM_LOG_FILE": '"xsi.log"', # Node name in case of single-node simulation "NODE_NAME": model.graph.node[0].name, + # Previous node name (for single node simulation) + "PREVIOUS_NODE_NAME": "std::nullopt" + if previous_node_name is None + else f'"{previous_node_name}"', + # Whether to execute a single node simulation + "SINGLE_NODE": "true" if is_single_node else "false", } fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index 50b8d81d08..edead48341 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -190,7 +190,14 @@ def apply(self, model): return (model, False) -def xsi_fifosim(model, n_inferences, max_iters=None, throttle_cycles=0): +def xsi_fifosim( + model, + n_inferences, + is_single_node, + previous_node_name: str | None = None, + max_iters=None, + throttle_cycles=0, +): """Create a XSI model of stitched IP and use a simple C++ driver to drive the input stream. Useful for FIFO sizing, latency and throughput measurement. If max_iters is None, use the default @@ -209,6 +216,8 @@ def xsi_fifosim(model, n_inferences, max_iters=None, throttle_cycles=0): ret_dict = rtlsim_exec_cppxsi( model, ctx, + is_single_node, + previous_node_name=previous_node_name, dummy_data_mode=True, timeout_cycles=max_iters, throttle_cycles=throttle_cycles, diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 551304b7e9..8c145e5f42 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -105,18 +105,26 @@ def run_sim_node_parallel_isolated(self, inputs: int) -> dict[int, Any]: and simulated in parallel. """ - def _run_simulation(node_index: int) -> Any: + def _run_simulation(node_index: int, prev_node_name: str) -> Any: nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(CreateStitchedIP(self.fpgapart, self.clk_ns)) # TODO: Remove xsi_fifosim from set_fifo_depths.py / change simulation functions - return xsi_fifosim(nodemodel, inputs) + return xsi_fifosim( + nodemodel, inputs, is_single_node=True, previous_node_name=prev_node_name + ) workers = int(os.environ["NUM_DEFAULT_WORKERS"]) futures: list[Future] = [] results = {} with ThreadPoolExecutor(max_workers=workers) as pool: for i in range(len(self.model.graph.node)): - futures.append(pool.submit(_run_simulation, i)) + futures.append( + pool.submit( + _run_simulation, + i, + self.model.graph.node[i - 1].name if i >= 1 else None, # type: ignore + ) + ) pool.shutdown(wait=True) for i, future in enumerate(futures): results[i] = future.result() From ea1db07d0ea60c37a9620085b59087e502c5805a Mon Sep 17 00:00:00 2001 From: bwintermann Date: Mon, 20 Oct 2025 09:21:55 +0200 Subject: [PATCH 006/170] Group bools into struct --- finn_xsi/finn_xsi/include/Simulation.hpp | 32 +++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 30f087dee1..4b13353802 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -26,18 +26,22 @@ enum class SimulationInterfaceType { PRODUCING, CONSUMING }; template class SimulationInterface { private: + struct _SimulationInterface { + std::atomic_bool ready; + std::atomic_bool valid; + std::atomic_bool unread; + }; ipc::managed_shared_memory shmem; - std::atomic_bool ready; - std::atomic_bool valid; - std::atomic_bool unread; + _SimulationInterface interface; + public: SimulationInterface(const char* shmIdentifier) { ipc::shared_memory_object::remove(shmIdentifier); shmem = ipc::managed_shared_memory(ipc::open_or_create, shmIdentifier, ShmemSize); - ready = shmem.find_or_construct("ready")(true); - valid = shmem.find_or_construct("valid")(false); - unread = shmem.find_or_construct("unread")(false); + interface.ready = shmem.find_or_construct("ready")(true); + interface.valid = shmem.find_or_construct("valid")(false); + interface.unread = shmem.find_or_construct("unread")(false); } ~SimulationInterface() { @@ -49,19 +53,19 @@ class SimulationInterface { /// Wait until predecessor has sent recent data. Then send ready. bool communicate(bool sendReady) requires (T == SimulationInterfaceType::CONSUMING) { - while (!unread) {} - ready = sendReady; - auto validValue = valid.load(); - unread = false; + while (!interface.unread) {} + interface.ready = sendReady; + auto validValue = interface.valid.load(); + interface.unread = false; return validValue; } /// Wait until successor has read the previous data. Then send valid. bool communicate(bool sendValid) requires (T == SimulationInterfaceType::PRODUCING) { - while (unread) {} - valid = sendValid; - auto readyValue = ready.load(); - unread = true; + while (interface.unread) {} + interface.valid = sendValid; + auto readyValue = interface.ready.load(); + interface.unread = true; return readyValue; } From e0557ae3a4c56daebb2ff867493805f560926dd6 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Mon, 20 Oct 2025 16:03:43 +0200 Subject: [PATCH 007/170] Updated simulation structure --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 2 +- finn_xsi/finn_xsi/include/Clock.h | 2 +- finn_xsi/finn_xsi/include/Simulation.hpp | 237 +++++++++++------- finn_xsi/finn_xsi/rtlsim_config.hpp.template | 9 +- src/finn/core/rtlsim_exec.py | 21 +- .../fpgadataflow/set_fifo_depths.py | 4 + .../transformation/fpgadataflow/simulation.py | 9 +- 7 files changed, 174 insertions(+), 110 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 375cf509c9..d963a71100 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -11,7 +11,7 @@ int main(){ // TODO: Give proper names for previous and name - Simulation<1, 1, true, SingleNode> sim( + SingleNodeSimulation<1, 1, true, NodeIndex, TotalNodes> sim( kernel_libname, design_libname, "xsim_log_file.txt", diff --git a/finn_xsi/finn_xsi/include/Clock.h b/finn_xsi/finn_xsi/include/Clock.h index 2c13d9f6eb..ad2ce73ac3 100644 --- a/finn_xsi/finn_xsi/include/Clock.h +++ b/finn_xsi/finn_xsi/include/Clock.h @@ -14,7 +14,7 @@ class Clock { Clock(Clock const&) = delete; Clock& operator=(Clock const&) = delete; Clock(xsi::Design& design); - template + template friend class Simulation; public: diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 4b13353802..9222aa42aa 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -16,9 +17,17 @@ #include #include #include +#include #include #include +#include +#ifdef __cpp_lib_hardware_interference_size + using std::hardware_destructive_interference_size; +#else + constexpr std::size_t hardware_destructive_interference_size = 64; +#endif + namespace ipc = boost::interprocess; enum class SimulationInterfaceType { PRODUCING, CONSUMING }; @@ -27,18 +36,18 @@ template class SimulationInterface { private: struct _SimulationInterface { - std::atomic_bool ready; - std::atomic_bool valid; - std::atomic_bool unread; + alignas(hardware_destructive_interference_size) std::atomic_bool ready; + alignas(hardware_destructive_interference_size) std::atomic_bool valid; + alignas(hardware_destructive_interference_size) std::atomic_bool unread; }; ipc::managed_shared_memory shmem; _SimulationInterface interface; - + const std::string shmIdentifier; public: - SimulationInterface(const char* shmIdentifier) { - ipc::shared_memory_object::remove(shmIdentifier); - shmem = ipc::managed_shared_memory(ipc::open_or_create, shmIdentifier, ShmemSize); + SimulationInterface(const char* _shmIdentifier) : shmIdentifier(_shmIdentifier) { + ipc::shared_memory_object::remove(_shmIdentifier); + shmem = ipc::managed_shared_memory(ipc::open_or_create, _shmIdentifier, ShmemSize); interface.ready = shmem.find_or_construct("ready")(true); interface.valid = shmem.find_or_construct("valid")(false); interface.unread = shmem.find_or_construct("unread")(false); @@ -54,8 +63,8 @@ class SimulationInterface { /// Wait until predecessor has sent recent data. Then send ready. bool communicate(bool sendReady) requires (T == SimulationInterfaceType::CONSUMING) { while (!interface.unread) {} - interface.ready = sendReady; auto validValue = interface.valid.load(); + interface.ready = sendReady; interface.unread = false; return validValue; } @@ -63,24 +72,16 @@ class SimulationInterface { /// Wait until successor has read the previous data. Then send valid. bool communicate(bool sendValid) requires (T == SimulationInterfaceType::PRODUCING) { while (interface.unread) {} - interface.valid = sendValid; auto readyValue = interface.ready.load(); + interface.valid = sendValid; interface.unread = true; return readyValue; } - }; - -/// Create a new simulation. To run single-node simulations with IPC, enable SingleNode -/// and pass previousNodeName and nodeName to identify shared memory of adjacent node simulation processes -template +template class Simulation { - private: - using ConsumerInterface = SimulationInterface; - using ProducerInterface = SimulationInterface; - std::optional, IStreamsSize>> fromProducerInterface; - std::optional, OStreamsSize>> toConsumerInterface; + protected: std::ofstream readyLog; std::ofstream validLog; @@ -91,24 +92,14 @@ class Simulation { std::array ostreams; Clock clk; - - void clearPorts() noexcept { - // Clear all input ports - for (xsi::Port& p : top.ports()) { - if (p.isInput()) { - p.clear().write_back(); - } + /// Initialize streams to the correct valid and ready states. + void initStreams() { + for (auto&& s : istreams) { + s.valid(); } - } - - void reset() noexcept { - xsi::Port& rst_n = top.getPort("ap_rst_n"); - // Reset all Inputs, Wait for Reset Period - rst_n.set(0).write_back(); - for (unsigned i = 0; i < 16; i++) { - clk.toggle_clk(); + for (auto&& s : ostreams) { + s.ready(); } - rst_n.set(1).write_back(); } Simulation( @@ -117,9 +108,7 @@ class Simulation { const char* xsim_log_file, const char* trace_file, std::array _istream_descs, - std::array _ostream_descs, - std::optional previousNodeName = std::nullopt, - std::optional currentNodeName = std::nullopt + std::array _ostream_descs ) : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), clk(top) { if (trace_file) { top.trace_all(); @@ -133,93 +122,149 @@ class Simulation { ostreams[i] = M_AXIS_Control{top, clk, std::data(_ostream_descs)[i].job_size, std::data(_ostream_descs)[i].name}; } + // Save simulation input output behaviour + if constexpr(LoggingEnabled) { + readyLog.open("ready_log.txt"); + validLog.open("valid_log.txt"); + } + // Find Global Control & Run Startup Sequence clearPorts(); reset(); + initStreams(); + } - // Make all Inputs valid & all Outputs ready - for (auto&& s : istreams) { - s.valid(); + + void clearPorts() noexcept { + // Clear all input ports + for (xsi::Port& p : top.ports()) { + if (p.isInput()) { + p.clear().write_back(); + } } - for (auto&& s : ostreams) { - s.ready(); + } + + void reset() noexcept { + xsi::Port& rst_n = top.getPort("ap_rst_n"); + // Reset all Inputs, Wait for Reset Period + rst_n.set(0).write_back(); + for (unsigned i = 0; i < 16; i++) { + clk.toggle_clk(); } + rst_n.set(1).write_back(); + } +}; - if constexpr(SingleNode) { - if (!previousNodeName || !currentNodeName) { - throw std::runtime_error("Cannot construct single-node simulation without specifying the previous and this nodes names."); - } - fromProducerInterface = std::make_optional, IStreamsSize>>(); - toConsumerInterface = std::make_optional, OStreamsSize>>(); - // Create simulation interfaces +template +class SingleNodeSimulation : public Simulation { + private: + using ConsumingInterface = SimulationInterface; + using ProducingInterface = SimulationInterface; + std::optional, IStreamsSize>> fromProducerInterface; + std::optional, OStreamsSize>> toConsumerInterface; + + // Coordinating reads and writes + // Could be done without a pool but this is more flexible for large branching models + boost::asio::thread_pool communicationPool; + + public: + SingleNodeSimulation( + const std::string& kernel_lib, + const std::string& design_lib, + const char* xsim_log_file, + const char* trace_file, + std::array _istream_descs, + std::array _ostream_descs, + std::optional previousNodeName = std::nullopt, + std::optional currentNodeName = std::nullopt + ) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { + initStreams(); + + if ((NodeIndex != 0 && !previousNodeName) || !currentNodeName) { + throw std::runtime_error("Cannot construct single-node simulation without specifying the previous and this nodes names."); + } + + // Create producer facing interfaces + if constexpr(NodeIndex != 0) { + fromProducerInterface = std::make_optional, IStreamsSize>>(); for (std::size_t i = 0; i < IStreamsSize; ++i) { - (*fromProducerInterface)[i] = std::make_unique((*previousNodeName + std::to_string(i)).c_str()); - } - for (std::size_t i = 0; i < OStreamsSize; ++i) { - (*toConsumerInterface)[i] = std::make_unique((*currentNodeName + std::to_string(i)).c_str()); + (*fromProducerInterface)[i] = std::make_unique((*previousNodeName + std::to_string(i)).c_str()); } + } else { + fromProducerInterface = std::nullopt; + } - // Save simulation input output behaviour - if constexpr(LoggingEnabled) { - readyLog.open("ready_log.txt"); - validLog.open("valid_log.txt"); + // Create consumer facing interfaces + if constexpr(NodeIndex != TotalNodes - 1) { + toConsumerInterface = std::make_optional, OStreamsSize>>(); + for (std::size_t i = 0; i < OStreamsSize; ++i) { + (*toConsumerInterface)[i] = std::make_unique((*currentNodeName + std::to_string(i)).c_str()); } } else { - // TODO - // Entire design in one simulation + toConsumerInterface = std::nullopt; } } - /// Read valid signal from producer, write own ready signal to it - void updateFromProducer() requires (SingleNode) { - for (std::size_t i = 0; i < IStreamsSize; ++i) { - istreams[i].valid((*fromProducerInterface)[i]->communicate(istreams[i].is_ready())); + /// Init streams according to nodeindex + void initStreams() { + if constexpr(NodeIndex == 0) { + // The first node receives all valid input streams + for (auto&& s : this->istreams) { + s.valid(); + } + } + if constexpr(NodeIndex == TotalNodes - 1) { + // The last nodes receives all ready output streams + for (auto&& s : this->ostreams) { + s.ready(); + } } } - /// Read ready signal from consumer, write own valid signal to it. - void updateToConsumer() requires (SingleNode) { - for (std::size_t i = 0; i < OStreamsSize; ++i) { - ostreams[i].ready((*toConsumerInterface)[i]->communicate(ostreams[i].is_valid())); + /// Communicate with predecessors and successors and update their values and our own + void communicate() { + if constexpr(NodeIndex != TotalNodes - 1) { + for (std::size_t i = 0; i < OStreamsSize; ++i) { + boost::asio::dispatch(communicationPool, [this, i]() { + this->ostreams[i].ready((*toConsumerInterface)[i]->communicate(this->ostreams[i].is_valid())); + }); + } + } + if constexpr(NodeIndex != 0) { + for (std::size_t i = 0; i < IStreamsSize; ++i) { + boost::asio::dispatch(communicationPool, [this, i]() { + this->istreams[i].valid((*fromProducerInterface)[i]->communicate(this->istreams[i].is_ready())); + }); + } } + communicationPool.join(); } void runSingleCycle() { - if constexpr(SingleNode) { - clk.toggle_clk(); - // Order: Send update forward to consumer, read update from producer second - updateFromProducer(); - updateToConsumer(); - - // Log the signals that this simulations set (ready to predecessor, valid to successor) - if constexpr(LoggingEnabled) { - for (S_AXIS_Control& stream : istreams) { - readyLog << stream.is_ready() << " "; - } - readyLog << "\n"; - for (M_AXIS_Control& stream : ostreams) { - validLog << stream.is_valid() << " "; - } - validLog << "\n"; + this->clk.toggle_clk(); + communicate(); + + // Log the signals that this simulations set (ready to predecessor, valid to successor) + if constexpr(LoggingEnabled) { + for (S_AXIS_Control& stream : this->istreams) { + this->readyLog << stream.is_ready() << " "; } - } else { - // TODO - // Single design case + this->readyLog << "\n"; + for (M_AXIS_Control& stream : this->ostreams) { + this->validLog << stream.is_valid() << " "; + } + this->validLog << "\n"; } } /// Run for the given number of frames (frames * job_size cycles or transactions) void runForFrames(std::size_t frames) { - if constexpr(SingleNode) { - // TODO: Multiple IO streams: Current cycle count is hardcoded for the number of inputs of the first stream - std::size_t cycleCount = frames * istreams[0].job_size; - for (std::size_t i = 0; i < cycleCount; ++i) { - runSingleCycle(); - } - } else { - // TODO - // Single design case + // TODO: Multiple IO streams: Current cycle count is hardcoded for the number of inputs of the first stream + std::size_t cycleCount = frames * this->istreams[0].job_size; + for (std::size_t i = 0; i < cycleCount; ++i) { + std::cout << "Cycle " << i << std::endl; + runSingleCycle(); } } }; diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index e9566888e5..1fb8bdb03a 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -19,7 +19,14 @@ /**** General RTLSIM Configuration Parameters ****/ const std::optional currentNodeName = "@NODE_NAME@"; const std::optional previousNodeName = @PREVIOUS_NODE_NAME@; -constexpr bool SingleNode = @SINGLE_NODE@; + +// Which index node this simulation executes +// In a complete design simulation this is 0 +constexpr size_t NodeIndex = @NODE_INDEX@; + +// Number of total nodes in the simulation (over all processes) +// In a complete design simulation this is 1 +constexpr size_t TotalNodes = @TOTAL_NODES@; struct stream_desc { char const *name; diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index 17b91274ae..c55a40f119 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -34,6 +34,7 @@ import numpy as np import os import shlex +import subprocess import sys from pathlib import Path from qonnx.custom_op.registry import getCustomOp @@ -122,8 +123,10 @@ def file_to_basename(x): def rtlsim_exec_cppxsi( model, execution_context, - is_single_node, - previous_node_name=None, + is_single_node: bool, + total_nodes: int = 1, + current_node_index: int | None = None, + previous_node_name: str | None = None, dummy_data_mode=False, timeout_cycles=None, throttle_cycles=0, @@ -177,7 +180,8 @@ def rtlsim_exec_cppxsi( vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f: all_verilog_srcs = f.read().split() - single_src_dir = make_build_dir("rtlsim_" + top_module_name + "_") + rtlsim_name = model.graph.node[0].name if is_single_node else top_module_name + single_src_dir = make_build_dir("rtlsim_" + rtlsim_name + "_") debug = not (trace_file is None or trace_file == "") rtlsim_so = finnxsi.compile_sim_obj( top_module_name, all_verilog_srcs, single_src_dir, debug=debug @@ -267,8 +271,8 @@ def rtlsim_exec_cppxsi( "PREVIOUS_NODE_NAME": "std::nullopt" if previous_node_name is None else f'"{previous_node_name}"', - # Whether to execute a single node simulation - "SINGLE_NODE": "true" if is_single_node else "false", + "NODE_INDEX": current_node_index if is_single_node else 0, + "TOTAL_NODES": total_nodes, } fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" @@ -311,13 +315,10 @@ def rtlsim_exec_cppxsi( runsim.write_text(f"LD_LIBRARY_PATH={ld_library_path}:$LD_LIBRARY_PATH {simulation_executable}") # Actually run the simulation - out, err = launch_process_helper( - ["bash", runsim.name], cwd=sim_base, proc_env=os.environ.copy() + subprocess.run( + ["bash", runsim.name], cwd=sim_base, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) - # TODO: remove output printing - log.warning(f"{model.graph.node[0].name}: {out}") - # parse results file and return dict # TODO return {} diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index edead48341..18eebfb4e8 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -194,6 +194,8 @@ def xsi_fifosim( model, n_inferences, is_single_node, + total_nodes: int = 1, + current_node_index: int | None = None, previous_node_name: str | None = None, max_iters=None, throttle_cycles=0, @@ -217,6 +219,8 @@ def xsi_fifosim( model, ctx, is_single_node, + total_nodes=total_nodes, + current_node_index=current_node_index, previous_node_name=previous_node_name, dummy_data_mode=True, timeout_cycles=max_iters, diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 8c145e5f42..d4c9ffd3db 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -104,13 +104,20 @@ def run_sim_node_parallel_isolated(self, inputs: int) -> dict[int, Any]: """Simulate the given number of inputs for every layer. Layers are completely isolated and simulated in parallel. """ + for i, node in enumerate(self.model.graph.node): + print(f"{i}: {node.name}") def _run_simulation(node_index: int, prev_node_name: str) -> Any: nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(CreateStitchedIP(self.fpgapart, self.clk_ns)) # TODO: Remove xsi_fifosim from set_fifo_depths.py / change simulation functions return xsi_fifosim( - nodemodel, inputs, is_single_node=True, previous_node_name=prev_node_name + nodemodel, + inputs, + is_single_node=True, + total_nodes=len(self.model.graph.node), + current_node_index=node_index, + previous_node_name=prev_node_name, ) workers = int(os.environ["NUM_DEFAULT_WORKERS"]) From 9be1099e7e7bae7cfaa301d6384f78690659018b Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 22 Oct 2025 13:06:58 +0200 Subject: [PATCH 008/170] Bugfixes for IPC --- finn_xsi/finn_xsi/include/Simulation.hpp | 157 ++++++++++++----------- 1 file changed, 82 insertions(+), 75 deletions(-) diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 9222aa42aa..16d8d31bbd 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -7,12 +7,13 @@ #include #include #include -#include #include +#include #include #include +#include #include -#include +#include #include #include #include @@ -22,6 +23,8 @@ #include #include +#include +#include #ifdef __cpp_lib_hardware_interference_size using std::hardware_destructive_interference_size; #else @@ -30,54 +33,6 @@ namespace ipc = boost::interprocess; -enum class SimulationInterfaceType { PRODUCING, CONSUMING }; - -template -class SimulationInterface { - private: - struct _SimulationInterface { - alignas(hardware_destructive_interference_size) std::atomic_bool ready; - alignas(hardware_destructive_interference_size) std::atomic_bool valid; - alignas(hardware_destructive_interference_size) std::atomic_bool unread; - }; - ipc::managed_shared_memory shmem; - _SimulationInterface interface; - const std::string shmIdentifier; - - public: - SimulationInterface(const char* _shmIdentifier) : shmIdentifier(_shmIdentifier) { - ipc::shared_memory_object::remove(_shmIdentifier); - shmem = ipc::managed_shared_memory(ipc::open_or_create, _shmIdentifier, ShmemSize); - interface.ready = shmem.find_or_construct("ready")(true); - interface.valid = shmem.find_or_construct("valid")(false); - interface.unread = shmem.find_or_construct("unread")(false); - } - - ~SimulationInterface() { - // TODO: Called implicitly? - shmem.destroy("ready"); - shmem.destroy("valid"); - shmem.destroy("unread"); - } - - /// Wait until predecessor has sent recent data. Then send ready. - bool communicate(bool sendReady) requires (T == SimulationInterfaceType::CONSUMING) { - while (!interface.unread) {} - auto validValue = interface.valid.load(); - interface.ready = sendReady; - interface.unread = false; - return validValue; - } - - /// Wait until successor has read the previous data. Then send valid. - bool communicate(bool sendValid) requires (T == SimulationInterfaceType::PRODUCING) { - while (interface.unread) {} - auto readyValue = interface.ready.load(); - interface.valid = sendValid; - interface.unread = true; - return readyValue; - } -}; template class Simulation { @@ -155,6 +110,64 @@ class Simulation { } }; +enum class SimulationInterfaceType { PRODUCING, CONSUMING }; + +template +class SimulationInterface { + private: + struct _SimulationInterface { + alignas(hardware_destructive_interference_size) boost::ipc_atomic* ready; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* valid; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* unread; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* fifoOccupation; + }; + ipc::managed_shared_memory shmem; + _SimulationInterface interface; + const std::string shmIdentifier; + + /// Logging + std::ofstream out; + + public: + SimulationInterface(const char* _shmIdentifier) : shmIdentifier(_shmIdentifier) { + if (T == SimulationInterfaceType::PRODUCING) { + ipc::shared_memory_object::remove(_shmIdentifier); + } + shmem = ipc::managed_shared_memory(ipc::open_or_create, _shmIdentifier, ShmemSize); + interface.ready = shmem.find_or_construct>("ready")(true); + interface.valid = shmem.find_or_construct>("valid")(false); + interface.unread = shmem.find_or_construct>("unread")(false); + interface.fifoOccupation = shmem.find_or_construct>("fifoOccupation")(0); + } + + ~SimulationInterface() { + // TODO: Called implicitly? + shmem.destroy>("ready"); + shmem.destroy>("valid"); + shmem.destroy>("unread"); + shmem.destroy>("fifoOccupation"); + } + + /// Wait until predecessor has sent recent data. Then send ready. + bool communicate(bool sendReady) requires (T == SimulationInterfaceType::CONSUMING) { + while (!*(interface.unread)) {} + bool validValue = *(interface.valid); + *(interface.ready) = sendReady; + *(interface.unread) = false; + return validValue; + } + + /// Wait until successor has read the previous data. Then send valid. + bool communicate(bool sendValid) requires (T == SimulationInterfaceType::PRODUCING) { + while (*(interface.unread)) {} + bool readyValue = *(interface.ready); + *(interface.valid) = sendValid; + *(interface.unread) = true; + return readyValue; + } +}; + + template class SingleNodeSimulation : public Simulation { @@ -164,10 +177,6 @@ class SingleNodeSimulation : public Simulation, IStreamsSize>> fromProducerInterface; std::optional, OStreamsSize>> toConsumerInterface; - // Coordinating reads and writes - // Could be done without a pool but this is more flexible for large branching models - boost::asio::thread_pool communicationPool; - public: SingleNodeSimulation( const std::string& kernel_lib, @@ -178,7 +187,8 @@ class SingleNodeSimulation : public Simulation _ostream_descs, std::optional previousNodeName = std::nullopt, std::optional currentNodeName = std::nullopt - ) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { + ) : + Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { initStreams(); if ((NodeIndex != 0 && !previousNodeName) || !currentNodeName) { @@ -204,8 +214,25 @@ class SingleNodeSimulation : public Simulationostreams[i].ready((*toConsumerInterface)[i]->communicate(this->ostreams[i].is_valid())); + } + } + if constexpr(NodeIndex != 0) { + for (std::size_t i = 0; i < IStreamsSize; ++i) { + this->istreams[i].valid((*fromProducerInterface)[i]->communicate(this->istreams[i].is_ready())); + } + } } + public: /// Init streams according to nodeindex void initStreams() { if constexpr(NodeIndex == 0) { @@ -222,25 +249,6 @@ class SingleNodeSimulation : public Simulationostreams[i].ready((*toConsumerInterface)[i]->communicate(this->ostreams[i].is_valid())); - }); - } - } - if constexpr(NodeIndex != 0) { - for (std::size_t i = 0; i < IStreamsSize; ++i) { - boost::asio::dispatch(communicationPool, [this, i]() { - this->istreams[i].valid((*fromProducerInterface)[i]->communicate(this->istreams[i].is_ready())); - }); - } - } - communicationPool.join(); - } - void runSingleCycle() { this->clk.toggle_clk(); communicate(); @@ -263,7 +271,6 @@ class SingleNodeSimulation : public Simulationistreams[0].job_size; for (std::size_t i = 0; i < cycleCount; ++i) { - std::cout << "Cycle " << i << std::endl; runSingleCycle(); } } From da60b915edf684030ee4ae5427c0423321a62ce0 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 22 Oct 2025 16:27:59 +0200 Subject: [PATCH 009/170] Rework simulation framework on the FINN side --- finn_xsi/finn_xsi/rtlsim_config.hpp.template | 9 +- .../transformation/fpgadataflow/simulation.py | 355 ++++++++++++++++-- 2 files changed, 334 insertions(+), 30 deletions(-) diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index 1fb8bdb03a..87b6ed4e12 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -49,12 +49,9 @@ std::array istream_descs { @ISTREAM_DESC@ }; // output AXI stream descriptors std::array ostream_descs { @OSTREAM_DESC@ }; -// number of inferences to perform -constexpr unsigned n_inferences = @N_INFERENCES@; - // max number of cycles to wait for output activity on any stream before timeout -constexpr unsigned max_iters = @TIMEOUT_CYCLES@; +constexpr unsigned max_iters = @TIMEOUT_CYCLES@; // filename for trace and debug, if enabled. This needs xelab -debug option too. -static char const *const trace_filename = @TRACE_FILE@; -static char const *const xsim_log_filename = @XSIM_LOG_FILE@; +static const std::optional trace_filename = @TRACE_FILE@; +static const std::string xsim_log_filename = @XSIM_LOG_FILE@; diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index d4c9ffd3db..1fb60dc249 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,17 +1,30 @@ """Manage FINN simulation variants.""" +import numpy as np import onnx import os +import shlex +import subprocess +import sys from concurrent.futures import Future, ThreadPoolExecutor from copy import deepcopy from onnx import NodeProto, TensorProto +from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation +from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, cast from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP -from finn.transformation.fpgadataflow.set_fifo_depths import xsi_fifosim -from finn.util.exception import FINNInternalError +from finn.util.basic import get_vivado_root, launch_process_helper, make_build_dir +from finn.util.exception import FINNInternalError, FINNUserError +from finn.util.logging import log + +try: + import finn_xsi.adapter as finnxsi +except ModuleNotFoundError: + finnxsi = None + if TYPE_CHECKING: from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp @@ -100,42 +113,336 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: return node_model - def run_sim_node_parallel_isolated(self, inputs: int) -> dict[int, Any]: + def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, int]: + """Return the stream descriptions for the given model for the C++ sim config header. + + Used by for example _build_single_node_simulation(). + + Returns: + tuple[str, int, str, int]: Strings of stream descriptions together with + their count (in, out) + """ + # Get IO iterations required + instream_iters = [] + outstream_iters = [] + for top_inp in model.graph.input: + iname = top_inp.name + first_node = model.find_consumer(iname) + assert first_node is not None, "Failed to find consumer for " + iname + top_ind = list(first_node.input).index(iname) + ishape_folded = getCustomOp(first_node).get_folded_input_shape(ind=top_ind) + instream_iters.append(np.prod(ishape_folded[:-1])) + for top_out in model.graph.output: + oname = top_out.name + last_node = model.find_producer(oname) + assert last_node is not None, "Failed to find producer for " + oname + top_ind = list(last_node.output).index(oname) + oshape_folded = getCustomOp(last_node).get_folded_output_shape(ind=top_ind) + outstream_iters.append(np.prod(oshape_folded[:-1])) + interface_names = model.get_metadata_prop("vivado_stitch_ifnames") + if interface_names is None: + raise FINNUserError( + f"{model}: Could not find stitched-IP interface names. " + f"Did you run IP Stitching first?" + ) + + # TODO: Copied from rtlsim_exec_cppxsi. Remove eval(). + interface_names = eval(interface_names) + if "aximm" in interface_names.keys() and interface_names["aximm"] != []: + raise FINNUserError( + f"{model}: CPP XSI Sim does not know how to handle full " + f"AXI MM interfaces: {interface_names['aximm']}" + ) + instream_names = [x[0] for x in interface_names["s_axis"]] + outstream_names = [x[0] for x in interface_names["m_axis"]] + + # Format stream descriptions + def _format_descr_name(s: str) -> str: + for old, new in [("[", ""), ("]", ""), ("(", "{"), (")", "}"), ("'", '"')]: + s = s.replace(old, new) + return s + + # TODO: Change this since we don't have throttling + instream_descrs = [ + (instream_names[i], instream_iters[i], instream_iters[i]) + for i in range(len(instream_names)) + ] + instream_descrs_str = _format_descr_name(str(instream_descrs)) + + outstream_descrs = [ + (outstream_names[i], outstream_iters[i], outstream_iters[i]) + for i in range(len(outstream_names)) + ] + outstream_descrs_str = _format_descr_name(str(outstream_descrs)) + return instream_descrs_str, len(instream_names), outstream_descrs_str, len(outstream_names) + + def _create_sim_so( + self, + model: ModelWrapper, + top_module_name: str, + vivado_stitched_proj: Path, + build_dir: Path | None, + debug: bool, + ) -> tuple[Path, Path]: + """Create a new RTLSim .so file. If one exists already it is used. + + Returns: + tuple[Path, Path]: Return sim_base and sim_rel. + """ + rtlsim_so_str = model.get_metadata_prop("rtlsim_so") + if (rtlsim_so_str is None) or not Path(rtlsim_so_str).exists(): + all_verilog_srcs = Path(vivado_stitched_proj).read_text().split() + sim_dir = ( + make_build_dir(f"rtlsim_{model.graph.node[0].name}_") + if build_dir is None + else build_dir + ) + sim_base, sim_rel = finnxsi.compile_sim_obj( + top_module_name, all_verilog_srcs, sim_dir, debug=debug + ) # noqa # type: ignore + rtlsim_so = Path(sim_base) / Path(sim_rel) + model.set_metadata_prop("rtlsim_so", str(rtlsim_so)) + else: + sim_base, sim_rel = cast("str", rtlsim_so_str.split("xsim.dir")) + sim_rel = "xsim.dir" + sim_rel + return Path(sim_base), Path(sim_rel) + + def _compile_simulation(self, sim_base: Path) -> Path: + """Compile an existing RTLSIM directory. Requires _create_sim_so to be run before. Expects + rtlsim_config.hpp to be templated already. + + Returns: + Path: Path to the executable shell script to run the binary + """ + finnxsi_dir = os.environ["FINN_XSI"] + # Running CMake first + cmake_call = f"{sys.executable} -m cmake -S {finnxsi_dir} -B {sim_base}" + log.info(f"Running cmake on RTLSIM Wrapper in {sim_base}") + try: + launch_process_helper( + shlex.split(cmake_call), + cwd=finnxsi_dir, + print_stdout=True, + proc_env=os.environ.copy(), + ) + except CalledProcessError as e: + raise FINNUserError(f"Failed to run cmake in {sim_base}") from e + + # Calling make to actually build the simulation + makefile = Path(sim_base) / "Makefile" + if not makefile.exists(): + raise FINNUserError(f"Failed to create Makefile in {sim_base}!") + try: + launch_process_helper(["make"], proc_env=os.environ.copy(), cwd=sim_base) + except CalledProcessError as e: + raise FINNUserError(f"Failed to create executable in {sim_base}!") from e + + # TODO: Fix name for general rtlsim + simulation_executable = Path(sim_base) / "LayerSimulationBackend" + if not simulation_executable.exists(): + raise FINNUserError(f"Make call in {sim_base} failed!") + + # Prepare the script to run the simulation + # (important to specify LD_LIBRARY_PATH here for XSI to work correctly) + runsim = Path(sim_base) / "run_fifosim.sh" + ld_library_path = get_vivado_root() + "/lib/lnx64.o" + runsim.write_text( + f"LD_LIBRARY_PATH={ld_library_path}:" f"$LD_LIBRARY_PATH {simulation_executable}" + ) + return runsim + + def _template_rtlsim_config( + self, + model: ModelWrapper, + sim_base: Path, + node_name: str, + previous_node_name: str | None, + node_index: int, + total_nodes: int, + timeout_cycles: int, + top_module_name: str, + trace_file: str | None, + ) -> Path: + """Template finn_xsi/finn_xsi/rtlsim_config.hpp.template with the correct values and + return the templated file. + """ + finnxsi_dir = os.environ["FINN_XSI"] + # Prepare the C++ driver config template + ( + instream_descrs_str, + len_instreams, + outstream_descrs_str, + len_outstreams, + ) = self._get_stream_descriptions(model) + template_dict = { + "TIMEOUT_CYCLES": timeout_cycles, + # name of the top-level HDL module + "TOP_MODULE_NAME": top_module_name, + # top-level AXI stream descriptors + "ISTREAM_DESC": instream_descrs_str, + "ISTREAM_LEN": len_instreams, + "OSTREAM_DESC": outstream_descrs_str, + "OSTREAM_LEN": len_outstreams, + # control tracing and trace filename + "TRACE_FILE": "std::nullopt" if trace_file is None else f'"{trace_file}"', + # sim kernel .so to use (depends on Vivado version) + "SIMKERNEL_SO": finnxsi.get_simkernel_so(), + # log file for xsi (not the sim driver) + "XSIM_LOG_FILE": '"xsi.log"', + # Node name in case of single-node simulation + "NODE_NAME": node_name, + # Previous node name (for single node simulation) + "PREVIOUS_NODE_NAME": ( + "std::nullopt" if previous_node_name is None else f'"{previous_node_name}"' + ), + "NODE_INDEX": node_index, + "TOTAL_NODES": total_nodes, + } + + fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" + fsim_config = fifosim_config_fname.read_text() + for key, val in template_dict.items(): + fsim_config = fsim_config.replace(f"@{key}@", str(val)) + + # Write the config to the simulation directory + rtlsim_config = Path(sim_base) / "rtlsim_config.hpp" + rtlsim_config.write_text(fsim_config) + return rtlsim_config + + def _build_single_node_simulation( + self, + node_model: ModelWrapper, + node_index: int, + total_nodes: int, + previous_node_name: str | None, + build_dir: Path | None, + timeout_cycles: int = 0, + ) -> Path: + """Build the simulation binary for a single node. + + This can be used both by the connected node-by-node sim and the isolated node sim. + + Much of this is from the rtlsim_exec.py in core/ + + Args: + node_model: The single node ModelWrapper to build the simulation from. + node_index: The index of the simulated node. Used to determine whether a node shares IO + with successors or predecessors. + total_nodes: The total number of nodes in the complete design. + previous_node_name: Required by the connected simulation. In the simulation binary this + is used to get access to the correct shared memory segment between + this node and the previous one. + build_dir: If given, use this directory for building the simulation. Otherwise one is + created from the nodes name. + timeout_cycles: Number of cycles until simulation timeout. When set to 0 (default), no + timeout is given. + + Returns: + Path: The path to the simulation binary (shell script). + """ + # TODO: Check if something is an output node instead of checking the node index + # TODO: Requires changes in the C++ code as well + + # Sanity checks + if len(node_model.graph.node) > 1: + raise FINNUserError( + "Cannot create single-node simulation for a model with more than " + "1 node. Make sure to pass the ModelWrapper containing only" + "the relevant node." + ) + node_name = node_model.graph.node[0].name + + # Check that the relevant data exists + wrapper_filename = node_model.get_metadata_prop("wrapper_filename") + if wrapper_filename is None or not Path(wrapper_filename).exists(): + raise FINNUserError( + f"Call CreateStitchedIP prior to building " f"the simulation for {node_name}" + ) + + vivado_stitched_proj = node_model.get_metadata_prop("vivado_stitch_proj") + if vivado_stitched_proj is None or not Path(vivado_stitched_proj).exists(): + raise FINNUserError( + f"Call CreateStitchedIP prior to building " + f"the simulation for {node_name}. (vivado_stitch_proj not set!)" + ) + + trace_file = cast("str | None", node_model.get_metadata_prop("rtlsim_trace")) + debug = not (trace_file is None or trace_file == "") + + # Get the module name and path + top_module_file = Path(wrapper_filename).resolve().absolute() + top_module_name = top_module_file.name.strip(".v") + + # Build the simulation .so and save it in the "rtlsim_so" metadata prop + sim_base, _ = self._create_sim_so( + node_model, top_module_name, Path(vivado_stitched_proj), build_dir, debug + ) + + # Fill out the simulation config header + _ = self._template_rtlsim_config( + node_model, + sim_base, + node_name, + previous_node_name, + node_index, + total_nodes, + timeout_cycles, + top_module_name, + trace_file, + ) + + # Building the whole simulation + return self._compile_simulation(sim_base).absolute() + + def run_sim_node_parallel_isolated(self, inputs: int) -> None: """Simulate the given number of inputs for every layer. Layers are completely isolated and simulated in parallel. """ for i, node in enumerate(self.model.graph.node): print(f"{i}: {node.name}") - def _run_simulation(node_index: int, prev_node_name: str) -> Any: + def _build_simulation( + node_index: int, total_nodes: int, prev_node_name: str | None, build_dir: Path + ) -> Any: nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(CreateStitchedIP(self.fpgapart, self.clk_ns)) - # TODO: Remove xsi_fifosim from set_fifo_depths.py / change simulation functions - return xsi_fifosim( - nodemodel, - inputs, - is_single_node=True, - total_nodes=len(self.model.graph.node), - current_node_index=node_index, - previous_node_name=prev_node_name, + return self._build_single_node_simulation( + nodemodel, node_index, total_nodes, prev_node_name, build_dir ) + def _run_simulation(binary: Path) -> None: + subprocess.run(["bash", str(binary)], capture_output=True) + + # Build simulations in parallel + # TODO: Change to info when done + log.warning("BUILDING NODE SIMULATIONS") workers = int(os.environ["NUM_DEFAULT_WORKERS"]) - futures: list[Future] = [] - results = {} + total_nodes = len(self.model.graph.node) + futures: dict[int, Future] = {} + binaries: dict[int, Path] = {} + with ThreadPoolExecutor(max_workers=workers) as pool: + for i in range(total_nodes): + futures[i] = pool.submit( + _build_simulation, + i, + total_nodes, + self.model.graph.node[i - 1].name if i >= 1 else None, # type: ignore + Path(make_build_dir(f"rtlsim_{self.model.graph.node[i].name}_")), + ) + pool.shutdown(wait=True) + for i, future in futures.items(): + binaries[i] = future.result() + + # TODO: Change to info when done + log.warning("RUNNING NODE SIMULATIONS") with ThreadPoolExecutor(max_workers=workers) as pool: - for i in range(len(self.model.graph.node)): - futures.append( - pool.submit( - _run_simulation, - i, - self.model.graph.node[i - 1].name if i >= 1 else None, # type: ignore - ) + for i, binary in binaries.items(): + print( + f"Submitting thread for running simulation {i} / {total_nodes} " + f"({self.model.graph.node[i].name})" ) + pool.submit(_run_simulation, binary) pool.shutdown(wait=True) - for i, future in enumerate(futures): - results[i] = future.result() - return results def run_sim_node_parallel_connected(self, inputs: int) -> Any: """Simulate a whole model, with all layers simulated in parallel.""" From 4f9c2cadc4beee1d835be03710437dc629cf9832 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 23 Oct 2025 13:16:40 +0200 Subject: [PATCH 010/170] Bugfixes and code improvements for C++ Simulation --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 4 +- finn_xsi/finn_xsi/include/Simulation.hpp | 132 +++++++++++------- .../transformation/fpgadataflow/simulation.py | 6 +- 3 files changed, 89 insertions(+), 53 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index d963a71100..4fc5da449e 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -11,7 +11,9 @@ int main(){ // TODO: Give proper names for previous and name - SingleNodeSimulation<1, 1, true, NodeIndex, TotalNodes> sim( + constexpr bool communicateWithPredecessor = (NodeIndex != 0); + constexpr bool communicateWithSuccessor = (NodeIndex != TotalNodes - 1); + SingleNodeSimulation<1, 1, true, NodeIndex, TotalNodes, communicateWithPredecessor, communicateWithSuccessor> sim( kernel_libname, design_libname, "xsim_log_file.txt", diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 16d8d31bbd..710f4a25bd 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -14,23 +14,30 @@ #include #include #include +#include +#include #include #include #include #include #include +#include #include #include #include -#include -#include #ifdef __cpp_lib_hardware_interference_size using std::hardware_destructive_interference_size; #else constexpr std::size_t hardware_destructive_interference_size = 64; #endif +#ifdef NDEBUG + inline void debug(std::string_view s) {} +#else + inline void debug(std::string_view s) { std::cout << "[DBG] " << s << "\n"; } +#endif + namespace ipc = boost::interprocess; @@ -89,7 +96,6 @@ class Simulation { initStreams(); } - void clearPorts() noexcept { // Clear all input ports for (xsi::Port& p : top.ports()) { @@ -111,6 +117,14 @@ class Simulation { }; enum class SimulationInterfaceType { PRODUCING, CONSUMING }; +constexpr std::string_view to_string(SimulationInterfaceType t) { + if (t == SimulationInterfaceType::CONSUMING) { + return "CONSUMING"; + } else if (t == SimulationInterfaceType::PRODUCING) { + return "PRODUCING"; + } + return "UNKNOWN SIMULATION INTERFACE TYPE"; +} template class SimulationInterface { @@ -118,64 +132,80 @@ class SimulationInterface { struct _SimulationInterface { alignas(hardware_destructive_interference_size) boost::ipc_atomic* ready; alignas(hardware_destructive_interference_size) boost::ipc_atomic* valid; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* unread; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* read; alignas(hardware_destructive_interference_size) boost::ipc_atomic* fifoOccupation; }; - ipc::managed_shared_memory shmem; _SimulationInterface interface; + ipc::managed_shared_memory shmem; const std::string shmIdentifier; - /// Logging - std::ofstream out; +#ifdef NDEBUG + void simInterfaceDebug(std::string_view s) {} +#else + /// Log the given text with a header identifying the shared memory region and the interface type + void simInterfaceDebug(std::string_view s) { + debug(std::format("{} ({}): {}", shmIdentifier, to_string(T), s)); + } +#endif public: SimulationInterface(const char* _shmIdentifier) : shmIdentifier(_shmIdentifier) { + simInterfaceDebug("Creating simulation interface"); if (T == SimulationInterfaceType::PRODUCING) { ipc::shared_memory_object::remove(_shmIdentifier); + simInterfaceDebug("Removed previous shared memory objects."); } shmem = ipc::managed_shared_memory(ipc::open_or_create, _shmIdentifier, ShmemSize); + simInterfaceDebug("Shared memory constructed or found."); interface.ready = shmem.find_or_construct>("ready")(true); interface.valid = shmem.find_or_construct>("valid")(false); - interface.unread = shmem.find_or_construct>("unread")(false); + interface.read = shmem.find_or_construct>("read")(true); interface.fifoOccupation = shmem.find_or_construct>("fifoOccupation")(0); + simInterfaceDebug("Shared variables constructed or found."); } ~SimulationInterface() { // TODO: Called implicitly? shmem.destroy>("ready"); shmem.destroy>("valid"); - shmem.destroy>("unread"); + shmem.destroy>("read"); shmem.destroy>("fifoOccupation"); } + bool dataRead() { return *(interface.read); } + /// Wait until predecessor has sent recent data. Then send ready. bool communicate(bool sendReady) requires (T == SimulationInterfaceType::CONSUMING) { - while (!*(interface.unread)) {} + simInterfaceDebug("Waiting for predecessor data."); + while (dataRead()) {} bool validValue = *(interface.valid); *(interface.ready) = sendReady; - *(interface.unread) = false; + *(interface.read) = true; + simInterfaceDebug("Exchanged data with predecessor."); return validValue; } /// Wait until successor has read the previous data. Then send valid. bool communicate(bool sendValid) requires (T == SimulationInterfaceType::PRODUCING) { - while (*(interface.unread)) {} + simInterfaceDebug("Waiting for successor data."); + while (!dataRead()) {} bool readyValue = *(interface.ready); *(interface.valid) = sendValid; - *(interface.unread) = true; + *(interface.read) = false; + simInterfaceDebug("Exchanged data with successor."); return readyValue; } }; - -template +template class SingleNodeSimulation : public Simulation { private: using ConsumingInterface = SimulationInterface; using ProducingInterface = SimulationInterface; - std::optional, IStreamsSize>> fromProducerInterface; - std::optional, OStreamsSize>> toConsumerInterface; + std::array, IStreamsSize> fromProducerInterface; + std::array, OStreamsSize> toConsumerInterface; + std::size_t cyclesRun = 0; public: SingleNodeSimulation( @@ -189,71 +219,73 @@ class SingleNodeSimulation : public Simulation currentNodeName = std::nullopt ) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { - initStreams(); - - if ((NodeIndex != 0 && !previousNodeName) || !currentNodeName) { - throw std::runtime_error("Cannot construct single-node simulation without specifying the previous and this nodes names."); + if (CommunicatesWithPredecessor && !previousNodeName) { + throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); + } else if (!CommunicatesWithPredecessor && previousNodeName) { + std::cout << "Simulation was passed the previous nodes name but is NOT marked for communication with predecessor node. No shared memory will be created." << std::endl; + } + if (CommunicatesWithSuccessor && !currentNodeName) { + throw std::runtime_error("Cannot communicate with successor because current node name was not given!"); + } else if (!CommunicatesWithSuccessor && currentNodeName) { + std::cout << "Simulation was passed the current nodes name but is NOT marked for communication with successor node. No shared memory will be created." << std::endl; } + // Set valid and ready + // TODO: Currently uses NodeIndex to check if we are input or output or neither + // This is unstable because other nodes with neither last nor first index might + // also be inputs or outputs. + initStreams(); + // Create producer facing interfaces - if constexpr(NodeIndex != 0) { - fromProducerInterface = std::make_optional, IStreamsSize>>(); + debug(std::format("Creating {} interfaces for communication with predecessors.", IStreamsSize)); + if (NodeIndex != 0 && previousNodeName && CommunicatesWithPredecessor) { for (std::size_t i = 0; i < IStreamsSize; ++i) { - (*fromProducerInterface)[i] = std::make_unique((*previousNodeName + std::to_string(i)).c_str()); + fromProducerInterface[i]= std::make_unique(std::format("{}_{}", *previousNodeName, i).c_str()); } - } else { - fromProducerInterface = std::nullopt; } // Create consumer facing interfaces - if constexpr(NodeIndex != TotalNodes - 1) { - toConsumerInterface = std::make_optional, OStreamsSize>>(); + debug(std::format("Creating {} interfaces for communication with successors.", IStreamsSize)); + if (NodeIndex != TotalNodes - 1 && currentNodeName && CommunicatesWithSuccessor) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - (*toConsumerInterface)[i] = std::make_unique((*currentNodeName + std::to_string(i)).c_str()); + toConsumerInterface[i] = std::make_unique(std::format("{}_{}", *currentNodeName, i).c_str()); } - } else { - toConsumerInterface = std::nullopt; } - + debug("Finished initializing simulation.\n------------------------------\n"); } private: /// Communicate with predecessors and successors and update their values and our own void communicate() { - if constexpr(NodeIndex != TotalNodes - 1) { + if constexpr(NodeIndex != TotalNodes - 1 && CommunicatesWithSuccessor) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - this->ostreams[i].ready((*toConsumerInterface)[i]->communicate(this->ostreams[i].is_valid())); + this->ostreams[i].ready(toConsumerInterface[i]->communicate(this->ostreams[i].is_valid())); } } - if constexpr(NodeIndex != 0) { + if constexpr(NodeIndex != 0 && CommunicatesWithPredecessor) { for (std::size_t i = 0; i < IStreamsSize; ++i) { - this->istreams[i].valid((*fromProducerInterface)[i]->communicate(this->istreams[i].is_ready())); + this->istreams[i].valid(fromProducerInterface[i]->communicate(this->istreams[i].is_ready())); } } } public: /// Init streams according to nodeindex - void initStreams() { - if constexpr(NodeIndex == 0) { - // The first node receives all valid input streams - for (auto&& s : this->istreams) { - s.valid(); - } - } - if constexpr(NodeIndex == TotalNodes - 1) { - // The last nodes receives all ready output streams - for (auto&& s : this->ostreams) { - s.ready(); - } - } - } + void initStreams() requires (NodeIndex == 0) { for (auto&& s : this->istreams) { s.valid(); } } + void initStreams() requires (NodeIndex == TotalNodes - 1) { for (auto&& s : this->ostreams) { s.ready(); } } + void initStreams() requires (NodeIndex > 0 && NodeIndex < TotalNodes - 1) { } void runSingleCycle() { + debug("Running single cycle.\n------------------"); this->clk.toggle_clk(); communicate(); + if constexpr(LoggingEnabled) { + ++cyclesRun; + debug(std::format("Finished cycle {}\n\n", cyclesRun)); + } // Log the signals that this simulations set (ready to predecessor, valid to successor) + // TODO: Collect signals in vectors and only write to file after the sim for speedup if constexpr(LoggingEnabled) { for (S_AXIS_Control& stream : this->istreams) { this->readyLog << stream.is_ready() << " "; diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 1fb60dc249..14b8f8367b 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -191,14 +191,16 @@ def _create_sim_so( """ rtlsim_so_str = model.get_metadata_prop("rtlsim_so") if (rtlsim_so_str is None) or not Path(rtlsim_so_str).exists(): - all_verilog_srcs = Path(vivado_stitched_proj).read_text().split() + all_verilog_srcs = ( + (Path(vivado_stitched_proj) / "all_verilog_srcs.txt").read_text().split() + ) sim_dir = ( make_build_dir(f"rtlsim_{model.graph.node[0].name}_") if build_dir is None else build_dir ) sim_base, sim_rel = finnxsi.compile_sim_obj( - top_module_name, all_verilog_srcs, sim_dir, debug=debug + top_module_name, all_verilog_srcs, str(sim_dir), debug=debug ) # noqa # type: ignore rtlsim_so = Path(sim_base) / Path(sim_rel) model.set_metadata_prop("rtlsim_so", str(rtlsim_so)) From e5f5b1fbe5e69787f0506957ce960e782a1927dc Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 23 Oct 2025 14:15:15 +0200 Subject: [PATCH 011/170] Fixed bug in multithreaded simulation running from FINN --- src/finn/transformation/fpgadataflow/simulation.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 14b8f8367b..3e69b6458b 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -413,7 +413,9 @@ def _build_simulation( ) def _run_simulation(binary: Path) -> None: - subprocess.run(["bash", str(binary)], capture_output=True) + subprocess.run( + ["bash", str(binary)], stdout=sys.stdout, stderr=sys.stderr, cwd=binary.parent + ) # Build simulations in parallel # TODO: Change to info when done @@ -437,6 +439,9 @@ def _run_simulation(binary: Path) -> None: # TODO: Change to info when done log.warning("RUNNING NODE SIMULATIONS") + # TODO: Might be unnecessary. Remove later + sys.stdout = sys.stdout.console + sys.stderr = sys.stderr.console with ThreadPoolExecutor(max_workers=workers) as pool: for i, binary in binaries.items(): print( From 68f7e78c105e0e978e77edf51f3d23994f0cdf2a Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 23 Oct 2025 16:07:52 +0200 Subject: [PATCH 012/170] Added FIFO functionality to the simulation interfaces --- finn_xsi/finn_xsi/CMakeLists.txt | 1 + finn_xsi/finn_xsi/include/Simulation.hpp | 140 +++++++++++++++-------- 2 files changed, 96 insertions(+), 45 deletions(-) diff --git a/finn_xsi/finn_xsi/CMakeLists.txt b/finn_xsi/finn_xsi/CMakeLists.txt index 66bd79c1fe..a901181b69 100644 --- a/finn_xsi/finn_xsi/CMakeLists.txt +++ b/finn_xsi/finn_xsi/CMakeLists.txt @@ -49,6 +49,7 @@ if (FIFOSIM_ENABLE_WARNINGS) "" "") endif (FIFOSIM_ENABLE_WARNINGS) +target_compile_options(fifosim_options INTERFACE -Wno-interference-size) # # Create options for including cmake files from the cmake folder with a bit of output. diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 710f4a25bd..a7d5842303 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -33,7 +34,7 @@ #endif #ifdef NDEBUG - inline void debug(std::string_view s) {} + inline void debug([[maybe_unused]] std::string_view s) {} #else inline void debug(std::string_view s) { std::cout << "[DBG] " << s << "\n"; } #endif @@ -130,9 +131,10 @@ template class SimulationInterface { private: struct _SimulationInterface { - alignas(hardware_destructive_interference_size) boost::ipc_atomic* ready; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* valid; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* read; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* iReady; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* iCycle; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* oValid; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* oCycle; alignas(hardware_destructive_interference_size) boost::ipc_atomic* fifoOccupation; }; _SimulationInterface interface; @@ -140,7 +142,7 @@ class SimulationInterface { const std::string shmIdentifier; #ifdef NDEBUG - void simInterfaceDebug(std::string_view s) {} + void simInterfaceDebug([[maybe_unused]] std::string_view s) {} #else /// Log the given text with a header identifying the shared memory region and the interface type void simInterfaceDebug(std::string_view s) { @@ -149,7 +151,9 @@ class SimulationInterface { #endif public: - SimulationInterface(const char* _shmIdentifier) : shmIdentifier(_shmIdentifier) { + unsigned int maxDepth; + + SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier), maxDepth(initialMaxDepth) { simInterfaceDebug("Creating simulation interface"); if (T == SimulationInterfaceType::PRODUCING) { ipc::shared_memory_object::remove(_shmIdentifier); @@ -157,43 +161,68 @@ class SimulationInterface { } shmem = ipc::managed_shared_memory(ipc::open_or_create, _shmIdentifier, ShmemSize); simInterfaceDebug("Shared memory constructed or found."); - interface.ready = shmem.find_or_construct>("ready")(true); - interface.valid = shmem.find_or_construct>("valid")(false); - interface.read = shmem.find_or_construct>("read")(true); + + // Find variables. CONSUMING interfaces only communicate with o... signals, PRODUCING ones only with i... interfaces interface.fifoOccupation = shmem.find_or_construct>("fifoOccupation")(0); + interface.iReady = shmem.find_or_construct>("iReady")(maxDepth > 0); + interface.iCycle = shmem.find_or_construct>("iCycle")(0); + interface.oValid = shmem.find_or_construct>("oValid")(*(interface.fifoOccupation) > 9); + interface.oCycle = shmem.find_or_construct>("oCycle")(0); simInterfaceDebug("Shared variables constructed or found."); } ~SimulationInterface() { // TODO: Called implicitly? - shmem.destroy>("ready"); - shmem.destroy>("valid"); - shmem.destroy>("read"); + shmem.destroy>("iReady"); + shmem.destroy>("iCycle"); + shmem.destroy>("oValid"); + shmem.destroy>("oCycle"); shmem.destroy>("fifoOccupation"); } - bool dataRead() { return *(interface.read); } - - /// Wait until predecessor has sent recent data. Then send ready. - bool communicate(bool sendReady) requires (T == SimulationInterfaceType::CONSUMING) { - simInterfaceDebug("Waiting for predecessor data."); - while (dataRead()) {} - bool validValue = *(interface.valid); - *(interface.ready) = sendReady; - *(interface.read) = true; - simInterfaceDebug("Exchanged data with predecessor."); - return validValue; + /// Reset all interface data fields to their defaults + void reset() { + *interface.fifoOccupation = 0; + *interface.iReady = maxDepth > 0; + *interface.iCycle = 0; + *interface.oValid = *(interface.fifoOccupation) > 0; + *interface.oCycle = 0; } - /// Wait until successor has read the previous data. Then send valid. - bool communicate(bool sendValid) requires (T == SimulationInterfaceType::PRODUCING) { - simInterfaceDebug("Waiting for successor data."); - while (!dataRead()) {} - bool readyValue = *(interface.ready); - *(interface.valid) = sendValid; - *(interface.read) = false; - simInterfaceDebug("Exchanged data with successor."); - return readyValue; + /// Communicate with the interface from the consumer side. Pass in the consuming nodes' input_ready. + /// If the interface has valid data, it will do the exchange. + /// The function returns the interfaces (FIFOs) output_valid signal, which should be + /// read by the consumer and set on their simulation port. + bool communicate(bool consumerReady) requires (T == SimulationInterfaceType::CONSUMING) { + // The input side must always be one cycle ahead of the output side + // Wait until input catches up (and overtakes) + simInterfaceDebug("Waiting for input side to catch up."); + while (*interface.iCycle < *interface.oCycle + 1) {} + *interface.oValid = *interface.fifoOccupation > 0; + if (*interface.oValid && consumerReady) { + --(*interface.fifoOccupation); + } + ++(*interface.oCycle); + simInterfaceDebug("Exchanged data with consumer."); + return *interface.oValid; + } + + /// Communicate with the interface from the producer side. Pass in the producing nodes' output_valid. + /// If the interface is ready to receive data, it will do the exchange. + /// The function returns the interfaces (FIFOs) input_ready signal, which should be + /// read by the producer and set on their simulation port. + bool communicate(bool producerValid) requires (T == SimulationInterfaceType::PRODUCING) { + // The input side must always be one cycle ahead of the output side + // Wait until output catches up + simInterfaceDebug("Waiting for output side to catch up."); + while (*interface.oCycle < *interface.iCycle - 1) {} + *interface.iReady = *interface.fifoOccupation < maxDepth; + if (*interface.iReady && producerValid) { + ++(*interface.fifoOccupation); + } + ++(*interface.iCycle); + simInterfaceDebug("Exchanged data with producer."); + return *interface.iReady; } }; @@ -215,18 +244,18 @@ class SingleNodeSimulation : public Simulation _istream_descs, std::array _ostream_descs, - std::optional previousNodeName = std::nullopt, - std::optional currentNodeName = std::nullopt + std::optional prevNodeName = std::nullopt, + std::optional nodeName = std::nullopt ) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { - if (CommunicatesWithPredecessor && !previousNodeName) { + if (CommunicatesWithPredecessor && !prevNodeName) { throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); - } else if (!CommunicatesWithPredecessor && previousNodeName) { + } else if (!CommunicatesWithPredecessor && prevNodeName) { std::cout << "Simulation was passed the previous nodes name but is NOT marked for communication with predecessor node. No shared memory will be created." << std::endl; } - if (CommunicatesWithSuccessor && !currentNodeName) { + if (CommunicatesWithSuccessor && !nodeName) { throw std::runtime_error("Cannot communicate with successor because current node name was not given!"); - } else if (!CommunicatesWithSuccessor && currentNodeName) { + } else if (!CommunicatesWithSuccessor && nodeName) { std::cout << "Simulation was passed the current nodes name but is NOT marked for communication with successor node. No shared memory will be created." << std::endl; } @@ -238,17 +267,17 @@ class SingleNodeSimulation : public Simulation(std::format("{}_{}", *previousNodeName, i).c_str()); + fromProducerInterface[i]= std::make_unique(std::format("{}_{}", *prevNodeName, i).c_str()); } } // Create consumer facing interfaces debug(std::format("Creating {} interfaces for communication with successors.", IStreamsSize)); - if (NodeIndex != TotalNodes - 1 && currentNodeName && CommunicatesWithSuccessor) { + if (NodeIndex != TotalNodes - 1 && nodeName && CommunicatesWithSuccessor) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - toConsumerInterface[i] = std::make_unique(std::format("{}_{}", *currentNodeName, i).c_str()); + toConsumerInterface[i] = std::make_unique(std::format("{}_{}", *nodeName, i).c_str()); } } debug("Finished initializing simulation.\n------------------------------\n"); @@ -275,6 +304,17 @@ class SingleNodeSimulation : public Simulationostreams) { s.ready(); } } void initStreams() requires (NodeIndex > 0 && NodeIndex < TotalNodes - 1) { } + /// Reset simulation (stream and current FIFO depth) + void reset() { + this->reset(); + for (std::size_t i = 0; i < OStreamsSize; ++i) { + toConsumerInterface[i]->reset(); + } + for (std::size_t i = 0; i < IStreamsSize; ++i) { + fromProducerInterface[i]->reset(); + } + } + void runSingleCycle() { debug("Running single cycle.\n------------------"); this->clk.toggle_clk(); @@ -301,10 +341,20 @@ class SingleNodeSimulation : public Simulationistreams[0].job_size; - for (std::size_t i = 0; i < cycleCount; ++i) { - runSingleCycle(); + auto start = std::chrono::high_resolution_clock::now(); + for (std::size_t i = 0; i < frames; ++i) { + for (std::size_t j = 0; j < this->istreams[0].job_size; ++j) { + runSingleCycle(); + } + // TODO: Unstable (inner nodes may also be outputs) + std::cout << NodeIndex << ": Finished frame " << i << std::endl; + } + auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); + if constexpr(NodeIndex == TotalNodes - 1) { + std::cout << "Total time: " << duration << " ms" << std::endl; + std::cout << "Average per frame: " << duration / static_cast(frames) << std::endl; } + } }; From 6f8db92e60510759605f3be41e12122af963d7af Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 23 Oct 2025 20:24:10 +0200 Subject: [PATCH 013/170] Several fixes, IPC mostly works. --- finn_xsi/finn_xsi/include/Simulation.hpp | 76 ++++++++++--------- .../transformation/fpgadataflow/simulation.py | 27 +++++-- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index a7d5842303..915996c211 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,7 @@ #include #include +#include #ifdef __cpp_lib_hardware_interference_size using std::hardware_destructive_interference_size; #else @@ -34,7 +36,7 @@ #endif #ifdef NDEBUG - inline void debug([[maybe_unused]] std::string_view s) {} + [[maybe_unused]] inline void debug([[maybe_unused]] std::string_view s) {} #else inline void debug(std::string_view s) { std::cout << "[DBG] " << s << "\n"; } #endif @@ -136,13 +138,14 @@ class SimulationInterface { alignas(hardware_destructive_interference_size) boost::ipc_atomic* oValid; alignas(hardware_destructive_interference_size) boost::ipc_atomic* oCycle; alignas(hardware_destructive_interference_size) boost::ipc_atomic* fifoOccupation; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* maxFifoDepth; }; _SimulationInterface interface; ipc::managed_shared_memory shmem; const std::string shmIdentifier; #ifdef NDEBUG - void simInterfaceDebug([[maybe_unused]] std::string_view s) {} + [[maybe_unused]] void simInterfaceDebug([[maybe_unused]] std::string_view s) {} #else /// Log the given text with a header identifying the shared memory region and the interface type void simInterfaceDebug(std::string_view s) { @@ -151,39 +154,58 @@ class SimulationInterface { #endif public: - unsigned int maxDepth; - - SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier), maxDepth(initialMaxDepth) { - simInterfaceDebug("Creating simulation interface"); + SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier) { + simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); if (T == SimulationInterfaceType::PRODUCING) { ipc::shared_memory_object::remove(_shmIdentifier); simInterfaceDebug("Removed previous shared memory objects."); + shmem = ipc::managed_shared_memory(ipc::create_only, _shmIdentifier, ShmemSize); + } else { + while(true) { + try { + shmem = ipc::managed_shared_memory(ipc::open_only, _shmIdentifier); + break; + } catch (const ipc::interprocess_exception& e) { + simInterfaceDebug("Producer shared memory not yet created. Waiting.."); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + } } - shmem = ipc::managed_shared_memory(ipc::open_or_create, _shmIdentifier, ShmemSize); simInterfaceDebug("Shared memory constructed or found."); // Find variables. CONSUMING interfaces only communicate with o... signals, PRODUCING ones only with i... interfaces interface.fifoOccupation = shmem.find_or_construct>("fifoOccupation")(0); - interface.iReady = shmem.find_or_construct>("iReady")(maxDepth > 0); + interface.maxFifoDepth = shmem.find_or_construct>("maxFifoDepth")(initialMaxDepth); + interface.iReady = shmem.find_or_construct>("iReady")(initialMaxDepth > 0); interface.iCycle = shmem.find_or_construct>("iCycle")(0); interface.oValid = shmem.find_or_construct>("oValid")(*(interface.fifoOccupation) > 9); interface.oCycle = shmem.find_or_construct>("oCycle")(0); simInterfaceDebug("Shared variables constructed or found."); + } ~SimulationInterface() { // TODO: Called implicitly? shmem.destroy>("iReady"); - shmem.destroy>("iCycle"); + shmem.destroy>("iCycle"); shmem.destroy>("oValid"); - shmem.destroy>("oCycle"); + shmem.destroy>("oCycle"); shmem.destroy>("fifoOccupation"); + shmem.destroy>("maxFifoDepth"); + } + + /// Set the max fifo depth in this interface. + void setMaxFifoDepth(unsigned int depth) { + simInterfaceDebug(std::format("Setting max FIFO depth to {}", depth)); + *interface.maxFifoDepth = depth; } /// Reset all interface data fields to their defaults - void reset() { + void reset(unsigned int maxFifoDepth = 2) { + simInterfaceDebug(std::format("Resetting simulation interface (with max FIFO depth {})", maxFifoDepth)); *interface.fifoOccupation = 0; - *interface.iReady = maxDepth > 0; + *interface.maxFifoDepth = maxFifoDepth; + *interface.iReady = true; *interface.iCycle = 0; *interface.oValid = *(interface.fifoOccupation) > 0; *interface.oCycle = 0; @@ -196,8 +218,7 @@ class SimulationInterface { bool communicate(bool consumerReady) requires (T == SimulationInterfaceType::CONSUMING) { // The input side must always be one cycle ahead of the output side // Wait until input catches up (and overtakes) - simInterfaceDebug("Waiting for input side to catch up."); - while (*interface.iCycle < *interface.oCycle + 1) {} + while (*interface.iCycle <= *interface.oCycle) {} *interface.oValid = *interface.fifoOccupation > 0; if (*interface.oValid && consumerReady) { --(*interface.fifoOccupation); @@ -214,9 +235,8 @@ class SimulationInterface { bool communicate(bool producerValid) requires (T == SimulationInterfaceType::PRODUCING) { // The input side must always be one cycle ahead of the output side // Wait until output catches up - simInterfaceDebug("Waiting for output side to catch up."); - while (*interface.oCycle < *interface.iCycle - 1) {} - *interface.iReady = *interface.fifoOccupation < maxDepth; + while (*interface.oCycle != *interface.iCycle) {} + *interface.iReady = *interface.fifoOccupation < *interface.maxFifoDepth; if (*interface.iReady && producerValid) { ++(*interface.fifoOccupation); } @@ -269,12 +289,12 @@ class SingleNodeSimulation : public Simulation(std::format("{}_{}", *prevNodeName, i).c_str()); + fromProducerInterface[i] = std::make_unique(std::format("{}_{}", *prevNodeName, i).c_str()); } } // Create consumer facing interfaces - debug(std::format("Creating {} interfaces for communication with successors.", IStreamsSize)); + debug(std::format("Creating {} interfaces for communication with successors.", OStreamsSize)); if (NodeIndex != TotalNodes - 1 && nodeName && CommunicatesWithSuccessor) { for (std::size_t i = 0; i < OStreamsSize; ++i) { toConsumerInterface[i] = std::make_unique(std::format("{}_{}", *nodeName, i).c_str()); @@ -338,24 +358,6 @@ class SingleNodeSimulation : public Simulationistreams[0].job_size; ++j) { - runSingleCycle(); - } - // TODO: Unstable (inner nodes may also be outputs) - std::cout << NodeIndex << ": Finished frame " << i << std::endl; - } - auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); - if constexpr(NodeIndex == TotalNodes - 1) { - std::cout << "Total time: " << duration << " ms" << std::endl; - std::cout << "Average per frame: " << duration / static_cast(frames) << std::endl; - } - - } }; #endif /* SIMULATION */ diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 3e69b6458b..79d689ac63 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -12,6 +12,7 @@ from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation +from random import Random from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, cast @@ -313,6 +314,7 @@ def _template_rtlsim_config( def _build_single_node_simulation( self, + node_name: str, node_model: ModelWrapper, node_index: int, total_nodes: int, @@ -327,6 +329,8 @@ def _build_single_node_simulation( Much of this is from the rtlsim_exec.py in core/ Args: + node_name: Despite the fact that we receive an isolated node model, we can still + manually pass a node name. This is useful to give unique names (e.g. for IPC) node_model: The single node ModelWrapper to build the simulation from. node_index: The index of the simulated node. Used to determine whether a node shares IO with successors or predecessors. @@ -352,7 +356,6 @@ def _build_single_node_simulation( "1 node. Make sure to pass the ModelWrapper containing only" "the relevant node." ) - node_name = node_model.graph.node[0].name # Check that the relevant data exists wrapper_filename = node_model.get_metadata_prop("wrapper_filename") @@ -404,12 +407,16 @@ def run_sim_node_parallel_isolated(self, inputs: int) -> None: print(f"{i}: {node.name}") def _build_simulation( - node_index: int, total_nodes: int, prev_node_name: str | None, build_dir: Path + node_name: str, + node_index: int, + total_nodes: int, + prev_node_name: str | None, + build_dir: Path, ) -> Any: nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(CreateStitchedIP(self.fpgapart, self.clk_ns)) return self._build_single_node_simulation( - nodemodel, node_index, total_nodes, prev_node_name, build_dir + node_name, nodemodel, node_index, total_nodes, prev_node_name, build_dir ) def _run_simulation(binary: Path) -> None: @@ -417,6 +424,15 @@ def _run_simulation(binary: Path) -> None: ["bash", str(binary)], stdout=sys.stdout, stderr=sys.stderr, cwd=binary.parent ) + # Create randomized names to avoid clashing with old IPC shared memory segments. + rand = Random() + rand.seed() + randomized_names = { + i: self.model.graph.node[i].name + + "".join(rand.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(5)) + for i in range(len(self.model.graph.node)) + } + # Build simulations in parallel # TODO: Change to info when done log.warning("BUILDING NODE SIMULATIONS") @@ -428,10 +444,11 @@ def _run_simulation(binary: Path) -> None: for i in range(total_nodes): futures[i] = pool.submit( _build_simulation, + randomized_names[i], i, total_nodes, - self.model.graph.node[i - 1].name if i >= 1 else None, # type: ignore - Path(make_build_dir(f"rtlsim_{self.model.graph.node[i].name}_")), + randomized_names[i - 1] if i >= 1 else None, # type: ignore + Path(make_build_dir(f"rtlsim_{randomized_names[i]}_")), ) pool.shutdown(wait=True) for i, future in futures.items(): From a20e1f9179cf12a47593d1d2e302a198baca7582 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 24 Oct 2025 00:50:14 +0200 Subject: [PATCH 014/170] Very slight optimizations --- finn_xsi/finn_xsi/include/Simulation.hpp | 36 ++++++++----------- finn_xsi/finn_xsi/src/AXIS_Control.cpp | 4 +-- .../transformation/fpgadataflow/simulation.py | 13 +++++-- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 915996c211..0d343391f8 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -133,12 +133,14 @@ template class SimulationInterface { private: struct _SimulationInterface { - alignas(hardware_destructive_interference_size) boost::ipc_atomic* iReady; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* iCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* oValid; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* oCycle; alignas(hardware_destructive_interference_size) boost::ipc_atomic* fifoOccupation; alignas(hardware_destructive_interference_size) boost::ipc_atomic* maxFifoDepth; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* iCycle; + alignas(hardware_destructive_interference_size) boost::ipc_atomic* oCycle; + + // Don't need to be atomic + alignas(hardware_destructive_interference_size) bool* iReady; + alignas(hardware_destructive_interference_size) bool* oValid; }; _SimulationInterface interface; ipc::managed_shared_memory shmem; @@ -167,7 +169,6 @@ class SimulationInterface { break; } catch (const ipc::interprocess_exception& e) { simInterfaceDebug("Producer shared memory not yet created. Waiting.."); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } } @@ -176,9 +177,9 @@ class SimulationInterface { // Find variables. CONSUMING interfaces only communicate with o... signals, PRODUCING ones only with i... interfaces interface.fifoOccupation = shmem.find_or_construct>("fifoOccupation")(0); interface.maxFifoDepth = shmem.find_or_construct>("maxFifoDepth")(initialMaxDepth); - interface.iReady = shmem.find_or_construct>("iReady")(initialMaxDepth > 0); + interface.iReady = shmem.find_or_construct("iReady")(initialMaxDepth > 0); interface.iCycle = shmem.find_or_construct>("iCycle")(0); - interface.oValid = shmem.find_or_construct>("oValid")(*(interface.fifoOccupation) > 9); + interface.oValid = shmem.find_or_construct("oValid")(*(interface.fifoOccupation) > 9); interface.oCycle = shmem.find_or_construct>("oCycle")(0); simInterfaceDebug("Shared variables constructed or found."); @@ -186,9 +187,9 @@ class SimulationInterface { ~SimulationInterface() { // TODO: Called implicitly? - shmem.destroy>("iReady"); + shmem.destroy("iReady"); shmem.destroy>("iCycle"); - shmem.destroy>("oValid"); + shmem.destroy("oValid"); shmem.destroy>("oCycle"); shmem.destroy>("fifoOccupation"); shmem.destroy>("maxFifoDepth"); @@ -215,16 +216,13 @@ class SimulationInterface { /// If the interface has valid data, it will do the exchange. /// The function returns the interfaces (FIFOs) output_valid signal, which should be /// read by the consumer and set on their simulation port. - bool communicate(bool consumerReady) requires (T == SimulationInterfaceType::CONSUMING) { + bool communicate(bool consumerReady) requires (T == SimulationInterfaceType::CONSUMING) { // The input side must always be one cycle ahead of the output side // Wait until input catches up (and overtakes) while (*interface.iCycle <= *interface.oCycle) {} *interface.oValid = *interface.fifoOccupation > 0; - if (*interface.oValid && consumerReady) { - --(*interface.fifoOccupation); - } + *interface.fifoOccupation -= static_cast(*interface.oValid && consumerReady); ++(*interface.oCycle); - simInterfaceDebug("Exchanged data with consumer."); return *interface.oValid; } @@ -237,11 +235,8 @@ class SimulationInterface { // Wait until output catches up while (*interface.oCycle != *interface.iCycle) {} *interface.iReady = *interface.fifoOccupation < *interface.maxFifoDepth; - if (*interface.iReady && producerValid) { - ++(*interface.fifoOccupation); - } + *interface.fifoOccupation += static_cast(*interface.iReady && producerValid); ++(*interface.iCycle); - simInterfaceDebug("Exchanged data with producer."); return *interface.iReady; } }; @@ -305,7 +300,7 @@ class SingleNodeSimulation : public Simulationostreams[i].ready(toConsumerInterface[i]->communicate(this->ostreams[i].is_valid())); @@ -335,8 +330,7 @@ class SingleNodeSimulation : public Simulationclk.toggle_clk(); communicate(); if constexpr(LoggingEnabled) { diff --git a/finn_xsi/finn_xsi/src/AXIS_Control.cpp b/finn_xsi/finn_xsi/src/AXIS_Control.cpp index 5dc584ba57..5fbc3f3060 100644 --- a/finn_xsi/finn_xsi/src/AXIS_Control.cpp +++ b/finn_xsi/finn_xsi/src/AXIS_Control.cpp @@ -25,11 +25,11 @@ void AXIS_Control::inititialized_or_throw() { } } -void AXIS_Control::valid(bool value) { port_vld->set(value ? 1 : 0).write_back(); } +void AXIS_Control::valid(bool value) { port_vld->set(static_cast(value)).write_back(); } bool AXIS_Control::is_valid() const noexcept { return port_vld->read().as_bool(); } -void AXIS_Control::ready(bool value) { port_rdy->set(value ? 1 : 0).write_back(); } +void AXIS_Control::ready(bool value) { port_rdy->set(static_cast(value)).write_back(); } bool AXIS_Control::is_ready() const noexcept { return port_rdy->read().as_bool(); } diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 79d689ac63..86261ee134 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,4 +1,5 @@ """Manage FINN simulation variants.""" +import multiprocessing import numpy as np import onnx import os @@ -419,9 +420,14 @@ def _build_simulation( node_name, nodemodel, node_index, total_nodes, prev_node_name, build_dir ) - def _run_simulation(binary: Path) -> None: + def _run_simulation(binary: Path, cpu: int | None) -> None: + command = "" + if cpu is not None: + command += f"taskset --cpu-list {cpu} " + # TODO: numactl + command += f"bash {binary}" subprocess.run( - ["bash", str(binary)], stdout=sys.stdout, stderr=sys.stderr, cwd=binary.parent + shlex.split(command), stdout=sys.stdout, stderr=sys.stderr, cwd=binary.parent ) # Create randomized names to avoid clashing with old IPC shared memory segments. @@ -465,7 +471,8 @@ def _run_simulation(binary: Path) -> None: f"Submitting thread for running simulation {i} / {total_nodes} " f"({self.model.graph.node[i].name})" ) - pool.submit(_run_simulation, binary) + # TODO: If more processes than CPU cores, group processes to their adjacent nodes + pool.submit(_run_simulation, binary, i % multiprocessing.cpu_count()) pool.shutdown(wait=True) def run_sim_node_parallel_connected(self, inputs: int) -> Any: From b6e49e2f2d56dacc4fa5a0d032b55a21efe08e4e Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 28 Oct 2025 13:17:34 +0100 Subject: [PATCH 015/170] Align data in shared memory --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 17 +- finn_xsi/finn_xsi/include/Simulation.hpp | 302 +++++++++++-------- 2 files changed, 198 insertions(+), 121 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 4fc5da449e..20a99ba904 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -5,15 +5,18 @@ #include #include #include -#include #include +#include #include +#define NDEBUG +#include + int main(){ // TODO: Give proper names for previous and name constexpr bool communicateWithPredecessor = (NodeIndex != 0); constexpr bool communicateWithSuccessor = (NodeIndex != TotalNodes - 1); - SingleNodeSimulation<1, 1, true, NodeIndex, TotalNodes, communicateWithPredecessor, communicateWithSuccessor> sim( + SingleNodeSimulation<1, 1, false, NodeIndex, TotalNodes, communicateWithPredecessor, communicateWithSuccessor> sim( kernel_libname, design_libname, "xsim_log_file.txt", @@ -25,6 +28,14 @@ int main(){ ); // TODO: Run correct frames - sim.runForFrames(10); + + auto start = std::chrono::high_resolution_clock::now(); + for (std::size_t j = 0; j < 100000; ++j) { + sim.runSingleCycle(); + } + auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); + if constexpr(NodeIndex == 0) { + std::cout << duration << " ms" << std::endl; + } return 0; } diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 0d343391f8..c91a7f0e91 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -7,38 +7,39 @@ #include #include #include + +#include #include +#include +#include #include #include +#include #include #include -#include #include #include #include #include +#include #include +#include #include -#include -#include -#include -#include -#include +#include +#include #include #include - -#include -#include +#include #ifdef __cpp_lib_hardware_interference_size - using std::hardware_destructive_interference_size; +using std::hardware_destructive_interference_size; #else - constexpr std::size_t hardware_destructive_interference_size = 64; +constexpr std::size_t hardware_destructive_interference_size = 64; #endif #ifdef NDEBUG - [[maybe_unused]] inline void debug([[maybe_unused]] std::string_view s) {} +[[maybe_unused]] inline void debug([[maybe_unused]] std::string_view s) {} #else - inline void debug(std::string_view s) { std::cout << "[DBG] " << s << "\n"; } +inline void debug(std::string_view s) { std::cout << "[DBG] " << s << "\n"; } #endif namespace ipc = boost::interprocess; @@ -46,11 +47,11 @@ namespace ipc = boost::interprocess; template class Simulation { - protected: + protected: std::ofstream readyLog; std::ofstream validLog; - public: + public: xsi::Kernel kernel; xsi::Design top; std::array istreams; @@ -67,14 +68,9 @@ class Simulation { } } - Simulation( - const std::string& kernel_lib, - const std::string& design_lib, - const char* xsim_log_file, - const char* trace_file, - std::array _istream_descs, - std::array _ostream_descs - ) : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), clk(top) { + Simulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, + std::array _ostream_descs) + : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), clk(top) { if (trace_file) { top.trace_all(); } @@ -88,7 +84,7 @@ class Simulation { } // Save simulation input output behaviour - if constexpr(LoggingEnabled) { + if constexpr (LoggingEnabled) { readyLog.open("ready_log.txt"); validLog.open("valid_log.txt"); } @@ -129,20 +125,41 @@ constexpr std::string_view to_string(SimulationInterfaceType t) { return "UNKNOWN SIMULATION INTERFACE TYPE"; } -template +template class SimulationInterface { - private: - struct _SimulationInterface { - alignas(hardware_destructive_interference_size) boost::ipc_atomic* fifoOccupation; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* maxFifoDepth; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* iCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic* oCycle; - - // Don't need to be atomic - alignas(hardware_destructive_interference_size) bool* iReady; - alignas(hardware_destructive_interference_size) bool* oValid; + private: + // Shared memory structure with proper cache-line alignment + struct SharedData { + alignas(hardware_destructive_interference_size) boost::ipc_atomic fifoOccupation; + alignas(hardware_destructive_interference_size) boost::ipc_atomic maxFifoDepth; + alignas(hardware_destructive_interference_size) boost::ipc_atomic iCycle; + alignas(hardware_destructive_interference_size) boost::ipc_atomic oCycle; + alignas(hardware_destructive_interference_size) boost::ipc_atomic iReady; + alignas(hardware_destructive_interference_size) boost::ipc_atomic oValid; + + SharedData() : fifoOccupation(0), maxFifoDepth(0), iCycle(0), oCycle(0), iReady(false), oValid(false) {} + SharedData(unsigned int fifoOcc, unsigned int maxDepth, unsigned int inCycle, unsigned int outCycle, bool inReady, bool outValid) + : fifoOccupation(fifoOcc), maxFifoDepth(maxDepth), iCycle(inCycle), oCycle(outCycle), iReady(inReady), oValid(outValid) {} + SharedData(const SharedData& other) + : fifoOccupation(other.fifoOccupation.load()), + maxFifoDepth(other.maxFifoDepth.load()), + iCycle(other.iCycle.load()), + oCycle(other.oCycle.load()), + iReady(other.iReady.load()), + oValid(other.oValid.load()) {} + SharedData& operator=(const SharedData& other) { + fifoOccupation.store(other.fifoOccupation.load()); + maxFifoDepth.store(other.maxFifoDepth.load()); + iCycle.store(other.iCycle.load()); + oCycle.store(other.oCycle.load()); + iReady.store(other.iReady.load()); + oValid.store(other.oValid.load()); + return *this; + } }; - _SimulationInterface interface; + + SharedData* sharedData = nullptr; + boost::ipc_atomic* refCount = nullptr; ipc::managed_shared_memory shmem; const std::string shmIdentifier; @@ -150,12 +167,15 @@ class SimulationInterface { [[maybe_unused]] void simInterfaceDebug([[maybe_unused]] std::string_view s) {} #else /// Log the given text with a header identifying the shared memory region and the interface type - void simInterfaceDebug(std::string_view s) { - debug(std::format("{} ({}): {}", shmIdentifier, to_string(T), s)); - } + void simInterfaceDebug(std::string_view s) { debug(std::format("{} ({}): {}", shmIdentifier, to_string(T), s)); } #endif - public: + public: + // Default constructor needed for std::array + SimulationInterface() : shmIdentifier("") { + // Uninitialized - will be move-assigned later + } + SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier) { simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); if (T == SimulationInterfaceType::PRODUCING) { @@ -163,106 +183,143 @@ class SimulationInterface { simInterfaceDebug("Removed previous shared memory objects."); shmem = ipc::managed_shared_memory(ipc::create_only, _shmIdentifier, ShmemSize); } else { - while(true) { + while (true) { try { shmem = ipc::managed_shared_memory(ipc::open_only, _shmIdentifier); break; - } catch (const ipc::interprocess_exception& e) { - simInterfaceDebug("Producer shared memory not yet created. Waiting.."); - } + } catch (const ipc::interprocess_exception& e) { simInterfaceDebug("Producer shared memory not yet created. Waiting.."); } } } simInterfaceDebug("Shared memory constructed or found."); - // Find variables. CONSUMING interfaces only communicate with o... signals, PRODUCING ones only with i... interfaces - interface.fifoOccupation = shmem.find_or_construct>("fifoOccupation")(0); - interface.maxFifoDepth = shmem.find_or_construct>("maxFifoDepth")(initialMaxDepth); - interface.iReady = shmem.find_or_construct("iReady")(initialMaxDepth > 0); - interface.iCycle = shmem.find_or_construct>("iCycle")(0); - interface.oValid = shmem.find_or_construct("oValid")(*(interface.fifoOccupation) > 9); - interface.oCycle = shmem.find_or_construct>("oCycle")(0); - simInterfaceDebug("Shared variables constructed or found."); + // Construct or find the reference counter (separate from SharedData) + refCount = shmem.find_or_construct>("refCount")(0); + // Increment reference count atomically + int currentRefCount = refCount->fetch_add(1, boost::memory_order_acq_rel) + 1; + simInterfaceDebug(std::format("Reference count incremented to {}", currentRefCount)); + + // Construct or find the entire SharedData struct in shared memory + sharedData = shmem.find_or_construct("data")(SharedData(0, initialMaxDepth, 0, 0, initialMaxDepth > 0, false)); + simInterfaceDebug("Shared data structure constructed or found."); + } + + // Delete copy operations + SimulationInterface(const SimulationInterface&) = delete; + SimulationInterface& operator=(const SimulationInterface&) = delete; + + // Move constructor + SimulationInterface(SimulationInterface&& other) noexcept + : sharedData(other.sharedData), refCount(other.refCount), shmem(std::move(other.shmem)), shmIdentifier(std::move(other.shmIdentifier)) { + // Mark other as moved-from + other.sharedData = nullptr; + other.refCount = nullptr; + } + + // Move assignment operator + SimulationInterface& operator=(SimulationInterface&& other) noexcept { + if (this != &other) { + sharedData = other.sharedData; + refCount = other.refCount; + // Note: managed_shared_memory has deleted assignment, use swap + shmem.swap(other.shmem); + const_cast(shmIdentifier) = std::move(other.shmIdentifier); + + // Mark other as moved-from + other.sharedData = nullptr; + other.refCount = nullptr; + } + return *this; } ~SimulationInterface() { - // TODO: Called implicitly? - shmem.destroy("iReady"); - shmem.destroy>("iCycle"); - shmem.destroy("oValid"); - shmem.destroy>("oCycle"); - shmem.destroy>("fifoOccupation"); - shmem.destroy>("maxFifoDepth"); + // Skip cleanup if moved-from or default-constructed + if (!refCount || !sharedData) { + return; + } + + // Decrement reference count atomically + int remainingRefs = refCount->fetch_sub(1, boost::memory_order_acq_rel) - 1; + simInterfaceDebug(std::format("Reference count decremented to {}", remainingRefs)); + + // If we're the last process, clean up the shared memory + if (remainingRefs == 0) { + simInterfaceDebug("Last process exiting - cleaning up shared memory"); + shmem.destroy("data"); + shmem.destroy>("refCount"); + + // Remove the shared memory segment completely + ipc::shared_memory_object::remove(shmIdentifier.c_str()); + simInterfaceDebug("Shared memory cleaned up successfully"); + } else { + simInterfaceDebug("Other processes still using shared memory - detaching only"); + } } /// Set the max fifo depth in this interface. void setMaxFifoDepth(unsigned int depth) { simInterfaceDebug(std::format("Setting max FIFO depth to {}", depth)); - *interface.maxFifoDepth = depth; + sharedData->maxFifoDepth.store(depth, boost::memory_order_release); } /// Reset all interface data fields to their defaults - void reset(unsigned int maxFifoDepth = 2) { - simInterfaceDebug(std::format("Resetting simulation interface (with max FIFO depth {})", maxFifoDepth)); - *interface.fifoOccupation = 0; - *interface.maxFifoDepth = maxFifoDepth; - *interface.iReady = true; - *interface.iCycle = 0; - *interface.oValid = *(interface.fifoOccupation) > 0; - *interface.oCycle = 0; + void reset(unsigned int newMaxFifoDepth = 2) { + simInterfaceDebug(std::format("Resetting simulation interface (with max FIFO depth {})", newMaxFifoDepth)); + sharedData->fifoOccupation.store(0, boost::memory_order_release); + sharedData->maxFifoDepth.store(newMaxFifoDepth, boost::memory_order_release); + sharedData->iReady.store(true, boost::memory_order_release); + sharedData->iCycle.store(0, boost::memory_order_release); + sharedData->oValid.store(false, boost::memory_order_release); + sharedData->oCycle.store(0, boost::memory_order_release); } /// Communicate with the interface from the consumer side. Pass in the consuming nodes' input_ready. /// If the interface has valid data, it will do the exchange. /// The function returns the interfaces (FIFOs) output_valid signal, which should be /// read by the consumer and set on their simulation port. - bool communicate(bool consumerReady) requires (T == SimulationInterfaceType::CONSUMING) { + bool communicate(bool consumerReady) + requires(T == SimulationInterfaceType::CONSUMING) + { // The input side must always be one cycle ahead of the output side // Wait until input catches up (and overtakes) - while (*interface.iCycle <= *interface.oCycle) {} - *interface.oValid = *interface.fifoOccupation > 0; - *interface.fifoOccupation -= static_cast(*interface.oValid && consumerReady); - ++(*interface.oCycle); - return *interface.oValid; + while (sharedData->iCycle <= sharedData->oCycle) {} + sharedData->oValid = sharedData->fifoOccupation > 0; + sharedData->fifoOccupation -= static_cast(sharedData->oValid && consumerReady); + ++(sharedData->oCycle); + return sharedData->oValid; } /// Communicate with the interface from the producer side. Pass in the producing nodes' output_valid. /// If the interface is ready to receive data, it will do the exchange. /// The function returns the interfaces (FIFOs) input_ready signal, which should be /// read by the producer and set on their simulation port. - bool communicate(bool producerValid) requires (T == SimulationInterfaceType::PRODUCING) { + bool communicate(bool producerValid) + requires(T == SimulationInterfaceType::PRODUCING) + { // The input side must always be one cycle ahead of the output side // Wait until output catches up - while (*interface.oCycle != *interface.iCycle) {} - *interface.iReady = *interface.fifoOccupation < *interface.maxFifoDepth; - *interface.fifoOccupation += static_cast(*interface.iReady && producerValid); - ++(*interface.iCycle); - return *interface.iReady; + while (sharedData->oCycle != sharedData->iCycle) {} + sharedData->iReady = sharedData->fifoOccupation < sharedData->maxFifoDepth; + sharedData->fifoOccupation += static_cast(sharedData->iReady && producerValid); + ++(sharedData->iCycle); + return sharedData->iReady; } }; template class SingleNodeSimulation : public Simulation { - private: + private: using ConsumingInterface = SimulationInterface; using ProducingInterface = SimulationInterface; - std::array, IStreamsSize> fromProducerInterface; - std::array, OStreamsSize> toConsumerInterface; + std::array fromProducerInterface; + std::array toConsumerInterface; std::size_t cyclesRun = 0; - public: - SingleNodeSimulation( - const std::string& kernel_lib, - const std::string& design_lib, - const char* xsim_log_file, - const char* trace_file, - std::array _istream_descs, - std::array _ostream_descs, - std::optional prevNodeName = std::nullopt, - std::optional nodeName = std::nullopt - ) : - Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { + public: + SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, + std::array _ostream_descs, std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt) + : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { if (CommunicatesWithPredecessor && !prevNodeName) { throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); } else if (!CommunicatesWithPredecessor && prevNodeName) { @@ -280,67 +337,77 @@ class SingleNodeSimulation : public Simulation(std::format("{}_{}", *prevNodeName, i).c_str()); + fromProducerInterface[i] = std::move(ConsumingInterface(std::format("{}_{}", *prevNodeName, i).c_str())); } } - // Create consumer facing interfaces - debug(std::format("Creating {} interfaces for communication with successors.", OStreamsSize)); - if (NodeIndex != TotalNodes - 1 && nodeName && CommunicatesWithSuccessor) { - for (std::size_t i = 0; i < OStreamsSize; ++i) { - toConsumerInterface[i] = std::make_unique(std::format("{}_{}", *nodeName, i).c_str()); - } - } debug("Finished initializing simulation.\n------------------------------\n"); } - private: + private: /// Communicate with predecessors and successors and update their values and our own [[gnu::hot]] void communicate() { - if constexpr(NodeIndex != TotalNodes - 1 && CommunicatesWithSuccessor) { + if constexpr (NodeIndex != TotalNodes - 1 && CommunicatesWithSuccessor) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - this->ostreams[i].ready(toConsumerInterface[i]->communicate(this->ostreams[i].is_valid())); + this->ostreams[i].ready(toConsumerInterface[i].communicate(this->ostreams[i].is_valid())); } } - if constexpr(NodeIndex != 0 && CommunicatesWithPredecessor) { + if constexpr (NodeIndex != 0 && CommunicatesWithPredecessor) { for (std::size_t i = 0; i < IStreamsSize; ++i) { - this->istreams[i].valid(fromProducerInterface[i]->communicate(this->istreams[i].is_ready())); + this->istreams[i].valid(fromProducerInterface[i].communicate(this->istreams[i].is_ready())); } } } - public: + public: /// Init streams according to nodeindex - void initStreams() requires (NodeIndex == 0) { for (auto&& s : this->istreams) { s.valid(); } } - void initStreams() requires (NodeIndex == TotalNodes - 1) { for (auto&& s : this->ostreams) { s.ready(); } } - void initStreams() requires (NodeIndex > 0 && NodeIndex < TotalNodes - 1) { } + void initStreams() { + if constexpr (NodeIndex == 0) { + for (auto&& s : this->istreams) { + s.valid(); + } + } else if constexpr (NodeIndex == TotalNodes - 1) { + for (auto&& s : this->ostreams) { + s.ready(); + } + } + // Middle nodes don't initialize any streams + } /// Reset simulation (stream and current FIFO depth) void reset() { - this->reset(); + Simulation::reset(); for (std::size_t i = 0; i < OStreamsSize; ++i) { - toConsumerInterface[i]->reset(); + toConsumerInterface[i].reset(); } for (std::size_t i = 0; i < IStreamsSize; ++i) { - fromProducerInterface[i]->reset(); + fromProducerInterface[i].reset(); } } [[gnu::hot]] void runSingleCycle() { this->clk.toggle_clk(); communicate(); - if constexpr(LoggingEnabled) { + if constexpr (LoggingEnabled) { ++cyclesRun; debug(std::format("Finished cycle {}\n\n", cyclesRun)); } // Log the signals that this simulations set (ready to predecessor, valid to successor) // TODO: Collect signals in vectors and only write to file after the sim for speedup - if constexpr(LoggingEnabled) { + if constexpr (LoggingEnabled) { for (S_AXIS_Control& stream : this->istreams) { this->readyLog << stream.is_ready() << " "; } @@ -351,7 +418,6 @@ class SingleNodeSimulation : public SimulationvalidLog << "\n"; } } - }; #endif /* SIMULATION */ From 5fc6c9400fafddfa48666b451a7bf60c89b504aa Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 28 Oct 2025 16:07:21 +0100 Subject: [PATCH 016/170] Restructure files --- finn_xsi/finn_xsi/include/Simulation.hpp | 209 +--------------- .../finn_xsi/include/SimulationInterface.hpp | 223 ++++++++++++++++++ finn_xsi/finn_xsi/include/helper.h | 7 + 3 files changed, 231 insertions(+), 208 deletions(-) create mode 100644 finn_xsi/finn_xsi/include/SimulationInterface.hpp diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index c91a7f0e91..39778ac754 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -17,32 +18,14 @@ #include #include #include -#include #include #include #include #include #include -#include -#include -#include -#include #include #include #include -#ifdef __cpp_lib_hardware_interference_size -using std::hardware_destructive_interference_size; -#else -constexpr std::size_t hardware_destructive_interference_size = 64; -#endif - -#ifdef NDEBUG -[[maybe_unused]] inline void debug([[maybe_unused]] std::string_view s) {} -#else -inline void debug(std::string_view s) { std::cout << "[DBG] " << s << "\n"; } -#endif - -namespace ipc = boost::interprocess; template @@ -115,196 +98,6 @@ class Simulation { } }; -enum class SimulationInterfaceType { PRODUCING, CONSUMING }; -constexpr std::string_view to_string(SimulationInterfaceType t) { - if (t == SimulationInterfaceType::CONSUMING) { - return "CONSUMING"; - } else if (t == SimulationInterfaceType::PRODUCING) { - return "PRODUCING"; - } - return "UNKNOWN SIMULATION INTERFACE TYPE"; -} - -template -class SimulationInterface { - private: - // Shared memory structure with proper cache-line alignment - struct SharedData { - alignas(hardware_destructive_interference_size) boost::ipc_atomic fifoOccupation; - alignas(hardware_destructive_interference_size) boost::ipc_atomic maxFifoDepth; - alignas(hardware_destructive_interference_size) boost::ipc_atomic iCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic oCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic iReady; - alignas(hardware_destructive_interference_size) boost::ipc_atomic oValid; - - SharedData() : fifoOccupation(0), maxFifoDepth(0), iCycle(0), oCycle(0), iReady(false), oValid(false) {} - SharedData(unsigned int fifoOcc, unsigned int maxDepth, unsigned int inCycle, unsigned int outCycle, bool inReady, bool outValid) - : fifoOccupation(fifoOcc), maxFifoDepth(maxDepth), iCycle(inCycle), oCycle(outCycle), iReady(inReady), oValid(outValid) {} - SharedData(const SharedData& other) - : fifoOccupation(other.fifoOccupation.load()), - maxFifoDepth(other.maxFifoDepth.load()), - iCycle(other.iCycle.load()), - oCycle(other.oCycle.load()), - iReady(other.iReady.load()), - oValid(other.oValid.load()) {} - SharedData& operator=(const SharedData& other) { - fifoOccupation.store(other.fifoOccupation.load()); - maxFifoDepth.store(other.maxFifoDepth.load()); - iCycle.store(other.iCycle.load()); - oCycle.store(other.oCycle.load()); - iReady.store(other.iReady.load()); - oValid.store(other.oValid.load()); - return *this; - } - }; - - SharedData* sharedData = nullptr; - boost::ipc_atomic* refCount = nullptr; - ipc::managed_shared_memory shmem; - const std::string shmIdentifier; - -#ifdef NDEBUG - [[maybe_unused]] void simInterfaceDebug([[maybe_unused]] std::string_view s) {} -#else - /// Log the given text with a header identifying the shared memory region and the interface type - void simInterfaceDebug(std::string_view s) { debug(std::format("{} ({}): {}", shmIdentifier, to_string(T), s)); } -#endif - - public: - // Default constructor needed for std::array - SimulationInterface() : shmIdentifier("") { - // Uninitialized - will be move-assigned later - } - - SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier) { - simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); - if (T == SimulationInterfaceType::PRODUCING) { - ipc::shared_memory_object::remove(_shmIdentifier); - simInterfaceDebug("Removed previous shared memory objects."); - shmem = ipc::managed_shared_memory(ipc::create_only, _shmIdentifier, ShmemSize); - } else { - while (true) { - try { - shmem = ipc::managed_shared_memory(ipc::open_only, _shmIdentifier); - break; - } catch (const ipc::interprocess_exception& e) { simInterfaceDebug("Producer shared memory not yet created. Waiting.."); } - } - } - simInterfaceDebug("Shared memory constructed or found."); - - // Construct or find the reference counter (separate from SharedData) - refCount = shmem.find_or_construct>("refCount")(0); - - // Increment reference count atomically - int currentRefCount = refCount->fetch_add(1, boost::memory_order_acq_rel) + 1; - simInterfaceDebug(std::format("Reference count incremented to {}", currentRefCount)); - - // Construct or find the entire SharedData struct in shared memory - sharedData = shmem.find_or_construct("data")(SharedData(0, initialMaxDepth, 0, 0, initialMaxDepth > 0, false)); - simInterfaceDebug("Shared data structure constructed or found."); - } - - // Delete copy operations - SimulationInterface(const SimulationInterface&) = delete; - SimulationInterface& operator=(const SimulationInterface&) = delete; - - // Move constructor - SimulationInterface(SimulationInterface&& other) noexcept - : sharedData(other.sharedData), refCount(other.refCount), shmem(std::move(other.shmem)), shmIdentifier(std::move(other.shmIdentifier)) { - // Mark other as moved-from - other.sharedData = nullptr; - other.refCount = nullptr; - } - - // Move assignment operator - SimulationInterface& operator=(SimulationInterface&& other) noexcept { - if (this != &other) { - sharedData = other.sharedData; - refCount = other.refCount; - // Note: managed_shared_memory has deleted assignment, use swap - shmem.swap(other.shmem); - const_cast(shmIdentifier) = std::move(other.shmIdentifier); - - // Mark other as moved-from - other.sharedData = nullptr; - other.refCount = nullptr; - } - return *this; - } - - ~SimulationInterface() { - // Skip cleanup if moved-from or default-constructed - if (!refCount || !sharedData) { - return; - } - - // Decrement reference count atomically - int remainingRefs = refCount->fetch_sub(1, boost::memory_order_acq_rel) - 1; - simInterfaceDebug(std::format("Reference count decremented to {}", remainingRefs)); - - // If we're the last process, clean up the shared memory - if (remainingRefs == 0) { - simInterfaceDebug("Last process exiting - cleaning up shared memory"); - shmem.destroy("data"); - shmem.destroy>("refCount"); - - // Remove the shared memory segment completely - ipc::shared_memory_object::remove(shmIdentifier.c_str()); - simInterfaceDebug("Shared memory cleaned up successfully"); - } else { - simInterfaceDebug("Other processes still using shared memory - detaching only"); - } - } - - /// Set the max fifo depth in this interface. - void setMaxFifoDepth(unsigned int depth) { - simInterfaceDebug(std::format("Setting max FIFO depth to {}", depth)); - sharedData->maxFifoDepth.store(depth, boost::memory_order_release); - } - - /// Reset all interface data fields to their defaults - void reset(unsigned int newMaxFifoDepth = 2) { - simInterfaceDebug(std::format("Resetting simulation interface (with max FIFO depth {})", newMaxFifoDepth)); - sharedData->fifoOccupation.store(0, boost::memory_order_release); - sharedData->maxFifoDepth.store(newMaxFifoDepth, boost::memory_order_release); - sharedData->iReady.store(true, boost::memory_order_release); - sharedData->iCycle.store(0, boost::memory_order_release); - sharedData->oValid.store(false, boost::memory_order_release); - sharedData->oCycle.store(0, boost::memory_order_release); - } - - /// Communicate with the interface from the consumer side. Pass in the consuming nodes' input_ready. - /// If the interface has valid data, it will do the exchange. - /// The function returns the interfaces (FIFOs) output_valid signal, which should be - /// read by the consumer and set on their simulation port. - bool communicate(bool consumerReady) - requires(T == SimulationInterfaceType::CONSUMING) - { - // The input side must always be one cycle ahead of the output side - // Wait until input catches up (and overtakes) - while (sharedData->iCycle <= sharedData->oCycle) {} - sharedData->oValid = sharedData->fifoOccupation > 0; - sharedData->fifoOccupation -= static_cast(sharedData->oValid && consumerReady); - ++(sharedData->oCycle); - return sharedData->oValid; - } - - /// Communicate with the interface from the producer side. Pass in the producing nodes' output_valid. - /// If the interface is ready to receive data, it will do the exchange. - /// The function returns the interfaces (FIFOs) input_ready signal, which should be - /// read by the producer and set on their simulation port. - bool communicate(bool producerValid) - requires(T == SimulationInterfaceType::PRODUCING) - { - // The input side must always be one cycle ahead of the output side - // Wait until output catches up - while (sharedData->oCycle != sharedData->iCycle) {} - sharedData->iReady = sharedData->fifoOccupation < sharedData->maxFifoDepth; - sharedData->fifoOccupation += static_cast(sharedData->iReady && producerValid); - ++(sharedData->iCycle); - return sharedData->iReady; - } -}; template diff --git a/finn_xsi/finn_xsi/include/SimulationInterface.hpp b/finn_xsi/finn_xsi/include/SimulationInterface.hpp new file mode 100644 index 0000000000..67b8d5a3b7 --- /dev/null +++ b/finn_xsi/finn_xsi/include/SimulationInterface.hpp @@ -0,0 +1,223 @@ +#ifndef SIMULATION_INTERFACE +#define SIMULATION_INTERFACE +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cpp_lib_hardware_interference_size +using std::hardware_destructive_interference_size; +#else +constexpr std::size_t hardware_destructive_interference_size = 64; +#endif + +namespace ipc = boost::interprocess; + +enum class SimulationInterfaceType { PRODUCING, CONSUMING }; +constexpr std::string_view to_string(SimulationInterfaceType t) { + if (t == SimulationInterfaceType::CONSUMING) { + return "CONSUMING"; + } else if (t == SimulationInterfaceType::PRODUCING) { + return "PRODUCING"; + } + return "UNKNOWN SIMULATION INTERFACE TYPE"; +} + +template +class SimulationInterface { + private: + // Shared memory structure with proper cache-line alignment + struct SharedData { + alignas(hardware_destructive_interference_size) boost::ipc_atomic fifoOccupation; + alignas(hardware_destructive_interference_size) boost::ipc_atomic maxFifoDepth; + alignas(hardware_destructive_interference_size) boost::ipc_atomic iCycle; + alignas(hardware_destructive_interference_size) boost::ipc_atomic oCycle; + alignas(hardware_destructive_interference_size) boost::ipc_atomic iReady; + alignas(hardware_destructive_interference_size) boost::ipc_atomic oValid; + + SharedData() : fifoOccupation(0), maxFifoDepth(0), iCycle(0), oCycle(0), iReady(false), oValid(false) {} + SharedData(unsigned int fifoOcc, unsigned int maxDepth, unsigned int inCycle, unsigned int outCycle, bool inReady, bool outValid) + : fifoOccupation(fifoOcc), maxFifoDepth(maxDepth), iCycle(inCycle), oCycle(outCycle), iReady(inReady), oValid(outValid) {} + SharedData(const SharedData& other) + : fifoOccupation(other.fifoOccupation.load()), + maxFifoDepth(other.maxFifoDepth.load()), + iCycle(other.iCycle.load()), + oCycle(other.oCycle.load()), + iReady(other.iReady.load()), + oValid(other.oValid.load()) {} + SharedData& operator=(const SharedData& other) { + fifoOccupation.store(other.fifoOccupation.load()); + maxFifoDepth.store(other.maxFifoDepth.load()); + iCycle.store(other.iCycle.load()); + oCycle.store(other.oCycle.load()); + iReady.store(other.iReady.load()); + oValid.store(other.oValid.load()); + return *this; + } + }; + + SharedData* sharedData = nullptr; + boost::ipc_atomic* refCount = nullptr; + ipc::managed_shared_memory shmem; + const std::string shmIdentifier; + +#ifdef NDEBUG + [[maybe_unused]] void simInterfaceDebug([[maybe_unused]] std::string_view s) {} +#else + /// Log the given text with a header identifying the shared memory region and the interface type + void simInterfaceDebug(std::string_view s) { debug(std::format("{} ({}): {}", shmIdentifier, to_string(T), s)); } +#endif + + public: + // Default constructor needed for std::array + SimulationInterface() : shmIdentifier("") { + // Uninitialized - will be move-assigned later + } + + SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier) { + simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); + if (T == SimulationInterfaceType::PRODUCING) { + ipc::shared_memory_object::remove(_shmIdentifier); + simInterfaceDebug("Removed previous shared memory objects."); + shmem = ipc::managed_shared_memory(ipc::create_only, _shmIdentifier, ShmemSize); + } else { + while (true) { + try { + shmem = ipc::managed_shared_memory(ipc::open_only, _shmIdentifier); + break; + } catch (const ipc::interprocess_exception& e) { simInterfaceDebug("Producer shared memory not yet created. Waiting.."); } + } + } + simInterfaceDebug("Shared memory constructed or found."); + + // Construct or find the reference counter (separate from SharedData) + refCount = shmem.find_or_construct>("refCount")(0); + + // Increment reference count atomically + int currentRefCount = refCount->fetch_add(1, boost::memory_order_acq_rel) + 1; + simInterfaceDebug(std::format("Reference count incremented to {}", currentRefCount)); + + // Construct or find the entire SharedData struct in shared memory + sharedData = shmem.find_or_construct("data")(SharedData(0, initialMaxDepth, 0, 0, initialMaxDepth > 0, false)); + simInterfaceDebug("Shared data structure constructed or found."); + } + + // Delete copy operations + SimulationInterface(const SimulationInterface&) = delete; + SimulationInterface& operator=(const SimulationInterface&) = delete; + + // Move constructor + SimulationInterface(SimulationInterface&& other) noexcept + : sharedData(other.sharedData), refCount(other.refCount), shmem(std::move(other.shmem)), shmIdentifier(std::move(other.shmIdentifier)) { + // Mark other as moved-from + other.sharedData = nullptr; + other.refCount = nullptr; + } + + // Move assignment operator + SimulationInterface& operator=(SimulationInterface&& other) noexcept { + if (this != &other) { + sharedData = other.sharedData; + refCount = other.refCount; + // Note: managed_shared_memory has deleted assignment, use swap + shmem.swap(other.shmem); + const_cast(shmIdentifier) = std::move(other.shmIdentifier); + + // Mark other as moved-from + other.sharedData = nullptr; + other.refCount = nullptr; + } + return *this; + } + + ~SimulationInterface() { + // Skip cleanup if moved-from or default-constructed + if (!refCount || !sharedData) { + return; + } + + // Decrement reference count atomically + int remainingRefs = refCount->fetch_sub(1, boost::memory_order_acq_rel) - 1; + simInterfaceDebug(std::format("Reference count decremented to {}", remainingRefs)); + + // If we're the last process, clean up the shared memory + if (remainingRefs == 0) { + simInterfaceDebug("Last process exiting - cleaning up shared memory"); + shmem.destroy("data"); + shmem.destroy>("refCount"); + + // Remove the shared memory segment completely + ipc::shared_memory_object::remove(shmIdentifier.c_str()); + simInterfaceDebug("Shared memory cleaned up successfully"); + } else { + simInterfaceDebug("Other processes still using shared memory - detaching only"); + } + } + + /// Set the max fifo depth in this interface. + void setMaxFifoDepth(unsigned int depth) { + simInterfaceDebug(std::format("Setting max FIFO depth to {}", depth)); + sharedData->maxFifoDepth.store(depth, boost::memory_order_release); + } + + /// Reset all interface data fields to their defaults + void reset(unsigned int newMaxFifoDepth = 2) { + simInterfaceDebug(std::format("Resetting simulation interface (with max FIFO depth {})", newMaxFifoDepth)); + sharedData->fifoOccupation.store(0, boost::memory_order_release); + sharedData->maxFifoDepth.store(newMaxFifoDepth, boost::memory_order_release); + sharedData->iReady.store(true, boost::memory_order_release); + sharedData->iCycle.store(0, boost::memory_order_release); + sharedData->oValid.store(false, boost::memory_order_release); + sharedData->oCycle.store(0, boost::memory_order_release); + } + + /// Communicate with the interface from the consumer side. Pass in the consuming nodes' input_ready. + /// If the interface has valid data, it will do the exchange. + /// The function returns the interfaces (FIFOs) output_valid signal, which should be + /// read by the consumer and set on their simulation port. + bool communicate(bool consumerReady) + requires(T == SimulationInterfaceType::CONSUMING) + { + // The input side must always be one cycle ahead of the output side + // Wait until input catches up (and overtakes) + while (sharedData->iCycle <= sharedData->oCycle) {} + sharedData->oValid = sharedData->fifoOccupation > 0; + sharedData->fifoOccupation -= static_cast(sharedData->oValid && consumerReady); + ++(sharedData->oCycle); + return sharedData->oValid; + } + + /// Communicate with the interface from the producer side. Pass in the producing nodes' output_valid. + /// If the interface is ready to receive data, it will do the exchange. + /// The function returns the interfaces (FIFOs) input_ready signal, which should be + /// read by the producer and set on their simulation port. + bool communicate(bool producerValid) + requires(T == SimulationInterfaceType::PRODUCING) + { + // The input side must always be one cycle ahead of the output side + // Wait until output catches up + while (sharedData->oCycle != sharedData->iCycle) {} + sharedData->iReady = sharedData->fifoOccupation < sharedData->maxFifoDepth; + sharedData->fifoOccupation += static_cast(sharedData->iReady && producerValid); + ++(sharedData->iCycle); + return sharedData->iReady; + } +}; +#endif diff --git a/finn_xsi/finn_xsi/include/helper.h b/finn_xsi/finn_xsi/include/helper.h index 49a896fcb3..827985940d 100644 --- a/finn_xsi/finn_xsi/include/helper.h +++ b/finn_xsi/finn_xsi/include/helper.h @@ -3,6 +3,7 @@ #include #include +#include constexpr std::array XZ10 = {'0', '1', 'Z', 'X'}; constexpr std::array HEX = {'0', '1', '2', '3', '4', '5', '6', '7', @@ -15,4 +16,10 @@ struct StreamDescriptor { std::size_t job_ticks; }; +#ifdef NDEBUG +[[maybe_unused]] inline void debug([[maybe_unused]] std::string_view s) {} +#else +inline void debug(std::string_view s) { std::cout << "[DBG] " << s << "\n"; } +#endif + #endif /* HELPER_H_ */ From 94e37245c1788ca965010d22ce38ec266090aaa0 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 31 Oct 2025 15:52:44 +0100 Subject: [PATCH 017/170] Added program options and JSON data output --- finn_xsi/finn_xsi/CMakeLists.txt | 14 +++-- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 53 +++++++++++----- finn_xsi/finn_xsi/include/Simulation.hpp | 21 ++++++- finn_xsi/finn_xsi/rtlsim_config.hpp.template | 60 +++++++++---------- .../transformation/fpgadataflow/simulation.py | 14 ++++- 5 files changed, 105 insertions(+), 57 deletions(-) diff --git a/finn_xsi/finn_xsi/CMakeLists.txt b/finn_xsi/finn_xsi/CMakeLists.txt index a901181b69..f2b1db354a 100644 --- a/finn_xsi/finn_xsi/CMakeLists.txt +++ b/finn_xsi/finn_xsi/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.11) project(LayerSimulationBackend) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake") @@ -72,15 +72,17 @@ list(APPEND CMAKE_MESSAGE_INDENT " ") #indent +1 check_include(FIFOSIM_IPO "InterproceduralOptimization" InterproceduralOptimization.cmake) list(POP_BACK CMAKE_MESSAGE_INDENT) #indent -1 -# Write configuration header -#configure_file("${CMAKE_BINARY_DIR}/simulation_config.hpp.in" "${CMAKE_BINARY_DIR}/simulation_config.hpp") - # Main file(GLOB_RECURSE CORE_SRC src/*.cpp) add_executable(LayerSimulationBackend LayerSimulationBackend.cpp ${CORE_SRC}) +# For JSON writing +include(FetchContent) +FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz) +FetchContent_MakeAvailable(json) + # Add boost for IPC -find_package(Boost REQUIRED) +find_package(Boost COMPONENTS program_options REQUIRED) target_include_directories(LayerSimulationBackend SYSTEM PUBLIC ${Boost_INCLUDE_DIRS}) # Include the rtlsim wrapper directory itself @@ -91,4 +93,4 @@ target_include_directories(LayerSimulationBackend PUBLIC "$ENV{XILINX_VIVADO}/da target_include_directories(LayerSimulationBackend PUBLIC "include") # Link libraries -target_link_libraries(LayerSimulationBackend fifosim::options Threads::Threads OpenMP::OpenMP_CXX -ldl -lrt) +target_link_libraries(LayerSimulationBackend fifosim::options nlohmann_json::nlohmann_json Threads::Threads OpenMP::OpenMP_CXX Boost::program_options -ldl -lrt) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 20a99ba904..13bc7ef762 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -5,37 +5,58 @@ #include #include #include +#include #include +#include #include #include - -#define NDEBUG #include -int main(){ - // TODO: Give proper names for previous and name - constexpr bool communicateWithPredecessor = (NodeIndex != 0); - constexpr bool communicateWithSuccessor = (NodeIndex != TotalNodes - 1); - SingleNodeSimulation<1, 1, false, NodeIndex, TotalNodes, communicateWithPredecessor, communicateWithSuccessor> sim( - kernel_libname, - design_libname, +namespace po = boost::program_options; + +constexpr bool CommunicateWithPredecessor = (RTLSimConfig::NodeIndex != 0); +constexpr bool CommunicateWithSuccessor = (RTLSimConfig::NodeIndex != RTLSimConfig::TotalNodes - 1); +constexpr std::size_t InstreamCount = RTLSimConfig::istream_descs.size(); +constexpr std::size_t OutstreamCount = RTLSimConfig::ostream_descs.size(); + +int main(int argc, const char* argv[]) { + // Parse CLI options + po::options_description desc{"Options"}; + desc.add_options() + ("depth,d", po::value()->required(), "FIFO Depth") + ("output,o", po::value()->default_value("simulation_data.json"), "Simulation Data Output"); + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + + // Construct simulation + SingleNodeSimulation sim( + RTLSimConfig::kernel_libname, + RTLSimConfig::design_libname, "xsim_log_file.txt", "trace_file.txt", - std::array{StreamDescriptor{istream_descs[0].name, istream_descs[0].job_size, istream_descs[0].job_ticks}}, - std::array{StreamDescriptor{ostream_descs[0].name, ostream_descs[0].job_size, ostream_descs[0].job_ticks}}, - previousNodeName, - currentNodeName + RTLSimConfig::istream_descs, + RTLSimConfig::ostream_descs, + RTLSimConfig::previousNodeName, + RTLSimConfig::currentNodeName, + vm["depth"].as() ); - // TODO: Run correct frames - + /** SECTION WIP */ auto start = std::chrono::high_resolution_clock::now(); for (std::size_t j = 0; j < 100000; ++j) { sim.runSingleCycle(); } auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); - if constexpr(NodeIndex == 0) { + if constexpr(RTLSimConfig::NodeIndex == 0) { std::cout << duration << " ms" << std::endl; } + /***********/ + + // Write results as JSON + auto outputPath = std::filesystem::path(vm["output"].as()); + sim.writeResults(outputPath); + return 0; } diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 39778ac754..9453051e3a 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -27,6 +28,9 @@ #include #include +#include +using json = nlohmann::json; + template class Simulation { @@ -111,7 +115,8 @@ class SingleNodeSimulation : public Simulation _istream_descs, - std::array _ostream_descs, std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt) + std::array _ostream_descs, std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, + unsigned int initialFIFODepth = 2) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { if (CommunicatesWithPredecessor && !prevNodeName) { throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); @@ -134,7 +139,7 @@ class SingleNodeSimulation : public SimulationvalidLog << "\n"; } } + + /// Write the results of the simulation as a JSON file + void writeResults(std::filesystem::path& path) { + json j; + j["maxOccupation"] = 0; + j["cyclesSimulated"] = 0; + std::ofstream file(path); + file << j; + file.close(); + } }; #endif /* SIMULATION */ diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index 87b6ed4e12..c676a09ba0 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -15,43 +15,43 @@ #include #include #include +#include -/**** General RTLSIM Configuration Parameters ****/ -const std::optional currentNodeName = "@NODE_NAME@"; -const std::optional previousNodeName = @PREVIOUS_NODE_NAME@; +namespace RTLSimConfig { + // Log during simulation. Turned off by default. Might increase runtime if used. + constexpr bool LoggingEnabled = false; -// Which index node this simulation executes -// In a complete design simulation this is 0 -constexpr size_t NodeIndex = @NODE_INDEX@; + /**** General RTLSIM Configuration Parameters ****/ + const std::optional currentNodeName = "@NODE_NAME@"; + const std::optional previousNodeName = @PREVIOUS_NODE_NAME@; -// Number of total nodes in the simulation (over all processes) -// In a complete design simulation this is 1 -constexpr size_t TotalNodes = @TOTAL_NODES@; + // Which index node this simulation executes + // In a complete design simulation this is 0 + constexpr size_t NodeIndex = @NODE_INDEX@; -struct stream_desc { - char const *name; - size_t job_size; - // Next job can only start this many clock ticks after start of predecessor. - size_t job_ticks; -}; + // Number of total nodes in the simulation (over all processes) + // In a complete design simulation this is 1 + constexpr size_t TotalNodes = @TOTAL_NODES@; -// sim kernel .so to use (depends on Vivado version) -static char const kernel_libname[] = "@SIMKERNEL_SO@"; + // sim kernel .so to use (depends on Vivado version) + static char const kernel_libname[] = "@SIMKERNEL_SO@"; -// design library .so to use (important to use this relative path here, -// due to how XSI looks for certain files) -static char const design_libname[] = "xsim.dir/@TOP_MODULE_NAME@/xsimk.so"; + // design library .so to use (important to use this relative path here, + // due to how XSI looks for certain files) + static char const design_libname[] = "xsim.dir/@TOP_MODULE_NAME@/xsimk.so"; -// AXI stream descriptors {stream_name, transactions_per_inference} -// input AXI stream descriptors -std::array istream_descs { @ISTREAM_DESC@ }; + // AXI stream descriptors {stream_name, transactions_per_inference} + // input AXI stream descriptors + std::array istream_descs { @ISTREAM_DESC@ }; -// output AXI stream descriptors -std::array ostream_descs { @OSTREAM_DESC@ }; + // output AXI stream descriptors + std::array ostream_descs { @OSTREAM_DESC@ }; -// max number of cycles to wait for output activity on any stream before timeout -constexpr unsigned max_iters = @TIMEOUT_CYCLES@; + // max number of cycles to wait for output activity on any stream before timeout + constexpr unsigned max_iters = @TIMEOUT_CYCLES@; -// filename for trace and debug, if enabled. This needs xelab -debug option too. -static const std::optional trace_filename = @TRACE_FILE@; -static const std::string xsim_log_filename = @XSIM_LOG_FILE@; + // filename for trace and debug, if enabled. This needs xelab -debug option too. + static const std::optional trace_filename = @TRACE_FILE@; + static const std::string xsim_log_filename = @XSIM_LOG_FILE@; + +} diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 86261ee134..69ada441e8 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -251,7 +251,7 @@ def _compile_simulation(self, sim_base: Path) -> Path: runsim = Path(sim_base) / "run_fifosim.sh" ld_library_path = get_vivado_root() + "/lib/lnx64.o" runsim.write_text( - f"LD_LIBRARY_PATH={ld_library_path}:" f"$LD_LIBRARY_PATH {simulation_executable}" + f"LD_LIBRARY_PATH={ld_library_path}:$LD_LIBRARY_PATH {simulation_executable} --depth 2" ) return runsim @@ -362,7 +362,7 @@ def _build_single_node_simulation( wrapper_filename = node_model.get_metadata_prop("wrapper_filename") if wrapper_filename is None or not Path(wrapper_filename).exists(): raise FINNUserError( - f"Call CreateStitchedIP prior to building " f"the simulation for {node_name}" + f"Call CreateStitchedIP prior to building the simulation for {node_name}" ) vivado_stitched_proj = node_model.get_metadata_prop("vivado_stitch_proj") @@ -460,6 +460,16 @@ def _run_simulation(binary: Path, cpu: int | None) -> None: for i, future in futures.items(): binaries[i] = future.result() + # Create a script to run the entire simulation again + run_simulation = make_build_dir("run:simulation") + run_all_simulations = Path(run_simulation) / "run.sh" + log.info(f"Storing run-all-simulations script in {run_all_simulations}") + with (run_all_simulations).open("w+") as f: + f.write("#!/bin/bash\n") + f.write('echo "Running simulation"') + for binary in binaries.values(): + f.write(f"bash {binary}\n") + # TODO: Change to info when done log.warning("RUNNING NODE SIMULATIONS") # TODO: Might be unnecessary. Remove later From 48dce96d4a89c14fc515d75334b8cfac8c120070 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 31 Oct 2025 18:09:42 +0100 Subject: [PATCH 018/170] Fixed data logging --- .gitignore | 4 ++++ finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 2 ++ finn_xsi/finn_xsi/include/Simulation.hpp | 13 +++++++------ .../finn_xsi/include/SimulationInterface.hpp | 12 +++++++++++- .../transformation/fpgadataflow/simulation.py | 18 +++++++++++++----- 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index 0115f7ac39..72e69ea17e 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,7 @@ tags poetry.lock *.code-workspace .env +*.vim # Cmake files **/CMakeFiles @@ -100,6 +101,9 @@ MANIFEST /data/ *.csv +# Mock templated simulation config +finn_xsi/finn_xsi/rtlsim_config.hpp + # Google Drive key for dashboard /gdrive-key/ diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 13bc7ef762..5635d8993d 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -9,6 +9,8 @@ #include #include #include + +#define NDEBUG #include #include diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 9453051e3a..c85021e7e8 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -198,10 +198,8 @@ class SingleNodeSimulation : public Simulationclk.toggle_clk(); communicate(); - if constexpr (LoggingEnabled) { - ++cyclesRun; - debug(std::format("Finished cycle {}\n\n", cyclesRun)); - } + debug(std::format("Finished cycle {}\n\n", cyclesRun)); + ++cyclesRun; // Log the signals that this simulations set (ready to predecessor, valid to successor) // TODO: Collect signals in vectors and only write to file after the sim for speedup @@ -220,12 +218,15 @@ class SingleNodeSimulation : public Simulation* refCount = nullptr; + std::atomic largestOccupation; ipc::managed_shared_memory shmem; const std::string shmIdentifier; @@ -91,7 +92,7 @@ class SimulationInterface { // Uninitialized - will be move-assigned later } - SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier) { + SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier), largestOccupation(0) { simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); if (T == SimulationInterfaceType::PRODUCING) { ipc::shared_memory_object::remove(_shmIdentifier); @@ -171,6 +172,11 @@ class SimulationInterface { } } + /// Return the largest occupation that this FIFO has had so far + std::size_t getLargestOccupation() { + return largestOccupation; + } + /// Set the max fifo depth in this interface. void setMaxFifoDepth(unsigned int depth) { simInterfaceDebug(std::format("Setting max FIFO depth to {}", depth)); @@ -180,6 +186,7 @@ class SimulationInterface { /// Reset all interface data fields to their defaults void reset(unsigned int newMaxFifoDepth = 2) { simInterfaceDebug(std::format("Resetting simulation interface (with max FIFO depth {})", newMaxFifoDepth)); + largestOccupation = 0; sharedData->fifoOccupation.store(0, boost::memory_order_release); sharedData->maxFifoDepth.store(newMaxFifoDepth, boost::memory_order_release); sharedData->iReady.store(true, boost::memory_order_release); @@ -216,6 +223,9 @@ class SimulationInterface { while (sharedData->oCycle != sharedData->iCycle) {} sharedData->iReady = sharedData->fifoOccupation < sharedData->maxFifoDepth; sharedData->fifoOccupation += static_cast(sharedData->iReady && producerValid); + if (sharedData->iReady && producerValid) { + ++largestOccupation; + } ++(sharedData->iCycle); return sharedData->iReady; } diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 69ada441e8..d0dde7dbb1 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -460,15 +460,23 @@ def _run_simulation(binary: Path, cpu: int | None) -> None: for i, future in futures.items(): binaries[i] = future.result() - # Create a script to run the entire simulation again - run_simulation = make_build_dir("run:simulation") + # Create a script to build and run the entire simulation again + run_simulation = make_build_dir("run_simulation") run_all_simulations = Path(run_simulation) / "run.sh" - log.info(f"Storing run-all-simulations script in {run_all_simulations}") + build_all_simulations = Path(run_simulation) / "build.sh" + log.info(f"Storing run-all-simulations script in {run_simulation}") with (run_all_simulations).open("w+") as f: f.write("#!/bin/bash\n") - f.write('echo "Running simulation"') + f.write('echo "Running simulation"\n') for binary in binaries.values(): - f.write(f"bash {binary}\n") + f.write(f"bash {binary} &\n") + f.write("wait\n") + with build_all_simulations.open("w+") as f: + f.write("#!/bin/bash\n") + for binary in binaries.values(): + # Build each binary new. Done in parallel in the background + f.write(f"{{ cd {binary.parent};cmake . && make; }} &\n") + f.write("wait\n") # TODO: Change to info when done log.warning("RUNNING NODE SIMULATIONS") From 28fc0e5f26f346fdae4c162f1a00930ccbaa88cf Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:27:00 +0100 Subject: [PATCH 019/170] Add dummy layer to remove data path --- .../removedatapath/hdl/dummy_template.v | 25 + finn_xsi/finn_xsi/adapter.py | 9 +- .../fpgadataflow/hls/checksum_hls.py | 2 +- .../custom_op/fpgadataflow/hls/iodma_hls.py | 2 +- .../fpgadataflow/hls/tlastmarker_hls.py | 2 +- src/finn/custom_op/fpgadataflow/hlsbackend.py | 25 +- src/finn/custom_op/fpgadataflow/hwcustomop.py | 49 +- .../custom_op/fpgadataflow/rtl/__init__.py | 2 + .../fpgadataflow/rtl/removedatapath_rtl.py | 230 +++++++ src/finn/custom_op/fpgadataflow/rtlbackend.py | 23 +- .../fpgadataflow/create_stitched_ip.py | 616 ++++++++++-------- .../transformation/fpgadataflow/simulation.py | 86 ++- src/finn/util/basic.py | 2 +- 13 files changed, 736 insertions(+), 337 deletions(-) create mode 100644 finn-rtllib/removedatapath/hdl/dummy_template.v create mode 100644 src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py diff --git a/finn-rtllib/removedatapath/hdl/dummy_template.v b/finn-rtllib/removedatapath/hdl/dummy_template.v new file mode 100644 index 0000000000..36dec63915 --- /dev/null +++ b/finn-rtllib/removedatapath/hdl/dummy_template.v @@ -0,0 +1,25 @@ +module $TOP_MODULE_NAME$( +//- Global Control ------------------ +(* X_INTERFACE_PARAMETER = "ASSOCIATED_BUSIF in0_V:out0_V, ASSOCIATED_RESET = ap_rst_n" *) +(* X_INTERFACE_INFO = "xilinx.com:signal:clock:1.0 ap_clk CLK" *) +input ap_clk, +(* X_INTERFACE_PARAMETER = "POLARITY ACTIVE_LOW" *) +input ap_rst_n, + +//- AXI Stream - Input -------------- +output in0_V_TREADY, +input in0_V_TVALID, +input [$WIDTH$-1:0] in0_V_TDATA, + +//- AXI Stream - Output -------------- +input out0_V_TREADY, +output out0_V_TVALID, +output [$WIDTH$-1:0] out0_V_TDATA +); + +assign in0_V_TREADY = out0_V_TREADY; +assign out0_V_TVALID = in0_V_TVALID; +assign out0_V_TDATA = 0; + + +endmodule diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 55707ccc52..5ad07a6ab1 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -78,15 +78,15 @@ def compile_sim_obj(top_module_name, source_list, sim_out_dir, debug=False): "floating_point_v7_1_18", "floating_point_v7_1_15", "floating_point_v7_1_19", + "work", ] cmd_xelab = [ "xelab", - "work." + top_module_name, + "work." + "finn_design_wrapper", "-relax", - "-prj", - "rtlsim.prj", "-dll", + "--O3", "-s", top_module_name, ] @@ -101,6 +101,9 @@ def compile_sim_obj(top_module_name, source_list, sim_out_dir, debug=False): if locate_glbl() is not None: cmd_xelab.insert(1, "work.glbl") + cmd_xvlog = "xvlog --incr --relax -prj rtlsim.prj".split() + + launch_process_helper(cmd_xvlog, cwd=sim_out_dir) launch_process_helper(cmd_xelab, cwd=sim_out_dir) out_so_relative_path = "xsim.dir/%s/xsimk.so" % top_module_name out_so_full_path = sim_out_dir + "/" + out_so_relative_path diff --git a/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py b/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py index 14ef567404..72bc3bd973 100644 --- a/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py @@ -35,7 +35,7 @@ from finn.util.logging import log -class CheckSum_hls(HWCustomOp, HLSBackend): +class CheckSum_hls(HLSBackend, HWCustomOp): """Class that corresponds to custom_hls checksum function.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py b/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py index bf239dd056..6fb54ddcff 100644 --- a/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py @@ -74,7 +74,7 @@ # -the folded shape is not defined -class IODMA_hls(HWCustomOp, HLSBackend): +class IODMA_hls(HLSBackend, HWCustomOp): """Class that corresponds to finn-hlslib DMA function(s).""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py index 610dd2f6ef..34f93705af 100644 --- a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py @@ -31,7 +31,7 @@ from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -class TLastMarker_hls(HWCustomOp, HLSBackend): +class TLastMarker_hls(HLSBackend, HWCustomOp): """Node that adds/removes AXI stream TLAST signals where needed. Its behavior is transparent in node-by-node execution, only visible in IP-stitched rtlsim or actual hardware. diff --git a/src/finn/custom_op/fpgadataflow/hlsbackend.py b/src/finn/custom_op/fpgadataflow/hlsbackend.py index 80604a09bd..43d2c9daff 100644 --- a/src/finn/custom_op/fpgadataflow/hlsbackend.py +++ b/src/finn/custom_op/fpgadataflow/hlsbackend.py @@ -40,6 +40,7 @@ from qonnx.core.datatype import DataType from finn.custom_op.fpgadataflow import templates +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.basic import CppBuilder, launch_process_helper, make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy from finn.util.deps import get_deps_path @@ -48,7 +49,7 @@ from finn.util.logging import log -class HLSBackend(ABC): +class HLSBackend(HWCustomOp, ABC): """HLSBackend class all custom ops that correspond to a finn-hlslib function are using functionality of. Contains different functions every HLS custom node should have. Some as abstract methods, these have to be filled @@ -56,15 +57,19 @@ class HLSBackend(ABC): def get_nodeattr_types(self): """Return dictionary of node attribute types and properties.""" - return { - "code_gen_dir_cppsim": ("s", False, ""), - "executable_path": ("s", False, ""), - "res_hls": ("s", False, ""), - # temporary node attribute to keep track of interface style of hls ops - "cpp_interface": ("s", False, "packed", {"packed", "hls_vector"}), - # temporary node attribute to keep track of execution style of hls ops - "hls_style": ("s", False, "ifm_aware", {"ifm_aware", "freerunning"}), - } + super_types = super().get_nodeattr_types() + super_types.update( + { + "code_gen_dir_cppsim": ("s", False, ""), + "executable_path": ("s", False, ""), + "res_hls": ("s", False, ""), + # temporary node attribute to keep track of interface style of hls ops + "cpp_interface": ("s", False, "packed", {"packed", "hls_vector"}), + # temporary node attribute to keep track of execution style of hls ops + "hls_style": ("s", False, "ifm_aware", {"ifm_aware", "freerunning"}), + } + ) + return super_types def get_all_verilog_paths(self): """Return list of all folders containing Verilog code for this node.""" diff --git a/src/finn/custom_op/fpgadataflow/hwcustomop.py b/src/finn/custom_op/fpgadataflow/hwcustomop.py index 6ea6bbab87..1cba406255 100644 --- a/src/finn/custom_op/fpgadataflow/hwcustomop.py +++ b/src/finn/custom_op/fpgadataflow/hwcustomop.py @@ -34,10 +34,14 @@ import numpy as np import os from abc import abstractmethod +from onnx import NodeProto +from qonnx.core.datatype import BaseDataType from qonnx.custom_op.base import CustomOp from qonnx.util.basic import roundup_to_integer_multiple +from typing import Optional, Sequence, Union from finn.util.basic import get_liveness_threshold_cycles, is_versal +from finn.util.exception import FINNInternalError from finn.util.logging import log @@ -47,11 +51,19 @@ class HWCustomOp(CustomOp): custom node should have. Some as abstract methods, these have to be filled when writing a new fpgadataflow custom op node.""" - def __init__(self, onnx_node, **kwargs): + def __init__(self, onnx_node, **kwargs) -> None: super().__init__(onnx_node, **kwargs) self.code_gen_dict = {} - def get_nodeattr_types(self): + def get_nodeattr_types( + self, + ) -> dict[ + str, + Union[ + tuple[str, bool, Union[int, float, str, bool, np.ndarray, list]], + tuple[str, bool, Union[int, float, str, bool, np.ndarray, list], Optional[set]], + ], + ]: return { "backend": ("s", True, "fpgadataflow"), "preferred_impl_style": ("s", False, "", {"", "hls", "rtl"}), @@ -97,12 +109,17 @@ def get_nodeattr_types(self): "io_chrc_pads_out": ("ints", False, []), } - def make_shape_compatible_op(self, model): + def make_shape_compatible_op(self, model) -> NodeProto: oshape = self.get_normal_output_shape() + if oshape is None: + raise FINNInternalError( + f"Cannot make shape compatible op for {self.onnx_node.name} " + "since normal output shape is not defined." + ) # implement tensor with correct shape return super().make_const_shape_op(oshape) - def get_verilog_top_module_name(self): + def get_verilog_top_module_name(self) -> str: "Return the Verilog top module name for this node." node = self.onnx_node @@ -155,11 +172,11 @@ def get_rtlsim(self): return sim - def close_rtlsim(self, sim): + def close_rtlsim(self, sim) -> None: "Close and free up resources for rtlsim." finnxsi.close_rtlsim(sim) - def node_res_estimation(self, fpgapart): + def node_res_estimation(self, fpgapart) -> dict[str, Union[int, float]]: """Returns summarized resource estimation of BRAMs and LUTs of the node as a dictionary.""" ret = dict() @@ -251,38 +268,38 @@ def get_number_output_values(self): return np.prod(self.get_folded_output_shape()[:-1]) @abstractmethod - def get_input_datatype(self, ind=0): + def get_input_datatype(self, ind=0) -> BaseDataType: """Returns FINN DataType of input stream ind.""" @abstractmethod - def get_output_datatype(self, ind=0): + def get_output_datatype(self, ind=0) -> BaseDataType: """Returns FINN DataType of output stream ind.""" @abstractmethod - def get_normal_input_shape(self, ind=0): + def get_normal_input_shape(self, ind=0) -> Sequence[int] | None: """Returns normal input shape if implemented.""" @abstractmethod - def get_normal_output_shape(self, ind=0): + def get_normal_output_shape(self, ind=0) -> Sequence[int] | None: """Returns folded output shape if implemented.""" @abstractmethod - def get_folded_input_shape(self, ind=0): + def get_folded_input_shape(self, ind=0) -> Sequence[int] | None: """Returns folded input shape (according to synapse folding), if implemented.""" @abstractmethod - def get_folded_output_shape(self, ind=0): + def get_folded_output_shape(self, ind=0) -> Sequence[int] | None: """Returns folded output shape (according to neuron folding), if implemented.""" @abstractmethod - def get_instream_width(self, ind=0): + def get_instream_width(self, ind=0) -> int: """Returns input stream width, if implemented.""" @abstractmethod - def get_outstream_width(self, ind=0): + def get_outstream_width(self, ind=0) -> int: """Returns output stream width, if implemented.""" - def get_instream_width_padded(self, ind=0): + def get_instream_width_padded(self, ind=0) -> int: """Returns input stream width padded to a multiple of 8. This is required by the AXI Stream spec.""" in_width = self.get_instream_width(ind=ind) @@ -291,7 +308,7 @@ def get_instream_width_padded(self, ind=0): else: return 0 - def get_outstream_width_padded(self, ind=0): + def get_outstream_width_padded(self, ind=0) -> int: """Returns output stream width padded to a multiple of 8. This is required by the AXI Stream spec.""" out_width = self.get_outstream_width(ind=ind) diff --git a/src/finn/custom_op/fpgadataflow/rtl/__init__.py b/src/finn/custom_op/fpgadataflow/rtl/__init__.py index 06067a4fca..8a6f530dc2 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/__init__.py +++ b/src/finn/custom_op/fpgadataflow/rtl/__init__.py @@ -31,6 +31,7 @@ ) from finn.custom_op.fpgadataflow.rtl.fmpadding_rtl import FMPadding_rtl from finn.custom_op.fpgadataflow.rtl.matrixvectoractivation_rtl import MVAU_rtl +from finn.custom_op.fpgadataflow.rtl.removedatapath_rtl import RemoveDataPath_rtl from finn.custom_op.fpgadataflow.rtl.streamingdatawidthconverter_rtl import ( StreamingDataWidthConverter_rtl, ) @@ -49,3 +50,4 @@ custom_op["MVAU_rtl"] = MVAU_rtl custom_op["VVAU_rtl"] = VVAU_rtl custom_op["Thresholding_rtl"] = Thresholding_rtl +custom_op["RemoveDataPath_rtl"] = RemoveDataPath_rtl diff --git a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py new file mode 100644 index 0000000000..4c715e55ea --- /dev/null +++ b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py @@ -0,0 +1,230 @@ +import os +from numpy import ndarray +from pathlib import Path +from qonnx.core.datatype import BaseDataType, DataType +from typing import Sequence, cast + +from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend +from finn.util.exception import FINNInternalError +from finn.util.logging import log + + +class RemoveDataPath_rtl(RTLBackend): + """RTL implementation for RemoveDataPath custom op.""" + + def __init__(self, onnx_node, **kwargs) -> None: + super().__init__(onnx_node, **kwargs) + + def get_nodeattr_types(self) -> dict: + my_attrs = super().get_nodeattr_types() + my_attrs.update( + { + # folded shape of input/output + "folded_shape": ("ints", True, []), + # normal shape of input/output + "normal_shape": ("ints", True, []), + # FINN DataTypes for inputs/outputs + "dataType": ("s", True, ""), + } + ) + return my_attrs + + def infer_node_datatype(self, model) -> None: + node = self.onnx_node + idt = model.get_tensor_datatype(node.input[0]) + if idt != self.get_input_datatype(): + log.warning( + f"inputDataType changing for {node.name}: {self.get_input_datatype()} -> {idt}" + ) + self.set_nodeattr("dataType", idt.name) + # data type stays the same + model.set_tensor_datatype(node.output[0], idt) + + def get_rtl_file_list(self, abspath=False) -> list[Path]: + if abspath: + code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + else: + code_gen_dir = "" + + top_name = self.get_nodeattr("gen_top_module") + if type(code_gen_dir) is not str: + raise FINNInternalError( + f"code_gen_dir_ipgen attribute not set in {self.onnx_node.name}, " + "cannot get RTL file list" + ) + if type(top_name) is not str or top_name == "": + raise FINNInternalError( + f"gen_top_module attribute not set in {self.onnx_node.name}, " + "cannot get RTL file list" + ) + + code_gen_dir_path = Path(code_gen_dir) + + verilog_files = [ + code_gen_dir_path / f"{top_name}.v", + ] + return verilog_files + + def generate_hdl(self, model, fpgapart, clk) -> None: + """Generates the RTL code for this custom op.""" + rtlsrc = Path(os.environ["FINN_RTLLIB"]) / "removedatapath" / "hdl" + template_path = rtlsrc / "dummy_template.v" + + # save top module name so we can refer to it after this node has been renamed + # (e.g. by GiveUniqueNodeNames(prefix) during MakeZynqProject) + topname = self.get_verilog_top_module_name() + self.set_nodeattr("gen_top_module", topname) + + # make instream width a multiple of 8 for axi interface + in_width = self.get_instream_width_padded() + + code_gen_dict = {"$TOP_MODULE_NAME$": topname, "$WIDTH$": str(in_width)} + + # apply code generation to templates + code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + if type(code_gen_dir) is not str or code_gen_dir == "": + raise FINNInternalError( + f"code_gen_dir_ipgen attribute not set in {topname}, cannot generate RTL code" + ) + with open(template_path, "r") as f: + template = f.read() + + for placeholder, value in code_gen_dict.items(): + template = template.replace(placeholder, value) + + output_path = Path(code_gen_dir) / f"{self.get_verilog_top_module_name()}.v" + with open(output_path, "w") as f: + f.write(template) + + # set ipgen_path and ip_path so that HLS-Synth transformation + # and stich_ip transformation do not complain + # i.e. during the HLSSynthIP() transformation + self.set_nodeattr("ipgen_path", code_gen_dir) + self.set_nodeattr("ip_path", code_gen_dir) + + def code_generation_ipi(self) -> list[str]: + """Code generation for IP integration.""" + sourcefiles = self.get_rtl_file_list(abspath=True) + + cmd = [] + for f in sourcefiles: + cmd += [f"add_files -norecurse {f}"] + cmd += [ + "create_bd_cell -type module -reference " + f"{self.get_nodeattr('gen_top_module')} {self.onnx_node.name}" + ] + return cmd + + def get_normal_input_shape(self, ind=0) -> Sequence[int]: + normal_shape = self.get_nodeattr("normal_shape") + if ( + type(normal_shape) is not list + and type(normal_shape) is not tuple + and not isinstance(normal_shape, ndarray) + ): + raise FINNInternalError( + f"normal_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get normal input shape" + ) + if len(normal_shape) == 0: + raise FINNInternalError( + f"normal_shape attribute is empty in {self.onnx_node.name}, " + "cannot get normal input shape" + ) + if type(normal_shape[0]) is not int: + raise FINNInternalError( + f"normal_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get normal input shape" + ) + return cast(Sequence[int], normal_shape) + + def get_normal_output_shape(self, ind=0) -> Sequence[int]: + return self.get_normal_input_shape() + + def get_folded_input_shape(self, ind=0) -> Sequence[int]: + folded_shape = self.get_nodeattr("folded_shape") + if ( + type(folded_shape) is not list + and type(folded_shape) is not tuple + and not isinstance(folded_shape, ndarray) + ): + raise FINNInternalError( + f"folded_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get folded input shape" + ) + if len(folded_shape) == 0: + raise FINNInternalError( + f"folded_shape attribute is empty in {self.onnx_node.name}, " + "cannot get folded input shape" + ) + if type(folded_shape[0]) is not int: + raise FINNInternalError( + f"folded_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get folded input shape" + ) + return cast(Sequence[int], folded_shape) + + def get_folded_output_shape(self, ind=0) -> Sequence[int]: + return self.get_folded_input_shape() + + def get_instream_width(self, ind=0) -> int: + dtype = self.get_nodeattr("dataType") + if type(dtype) is not str: + raise FINNInternalError( + f"dataType attribute not set correctly in {self.onnx_node.name}, " + "cannot get instream width" + ) + dtype = DataType[dtype] + folded_shape = self.get_nodeattr("folded_shape") + if ( + type(folded_shape) is not list + and type(folded_shape) is not tuple + and not isinstance(folded_shape, ndarray) + ): + raise FINNInternalError( + f"folded_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + in_width = folded_shape[-1] * dtype.bitwidth() + return in_width + + def get_outstream_width(self, ind=0) -> int: + dtype = self.get_nodeattr("dataType") + if type(dtype) is not str: + raise FINNInternalError( + f"dataType attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + dtype = DataType[dtype] + folded_shape = self.get_nodeattr("folded_shape") + if ( + type(folded_shape) is not list + and type(folded_shape) is not tuple + and not isinstance(folded_shape, ndarray) + ): + raise FINNInternalError( + f"folded_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + in_width = folded_shape[-1] * dtype.bitwidth() + return in_width + + def get_input_datatype(self, ind=0) -> BaseDataType: + dtype = self.get_nodeattr("dataType") + if type(dtype) is not str: + raise FINNInternalError( + f"dataType attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + dtype = DataType[dtype] + return dtype + + def get_output_datatype(self, ind=0) -> BaseDataType: + dtype = self.get_nodeattr("dataType") + if type(dtype) is not str: + raise FINNInternalError( + f"dataType attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + dtype = DataType[dtype] + return dtype diff --git a/src/finn/custom_op/fpgadataflow/rtlbackend.py b/src/finn/custom_op/fpgadataflow/rtlbackend.py index 31cd527678..c2c000b3e0 100644 --- a/src/finn/custom_op/fpgadataflow/rtlbackend.py +++ b/src/finn/custom_op/fpgadataflow/rtlbackend.py @@ -34,26 +34,33 @@ import numpy as np import os from abc import ABC, abstractmethod +from pathlib import Path +from typing import List +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.basic import make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy from finn.util.logging import log -class RTLBackend(ABC): +class RTLBackend(HWCustomOp, ABC): """RTLBackend class all custom ops that correspond to a module in finn-rtllib are using functionality of. Contains different functions every RTL custom node should have. Some as abstract methods, these have to be filled when writing a new RTL custom op node.""" def get_nodeattr_types(self): - return { - # attribute to save top module name - not user configurable - "gen_top_module": ("s", False, ""), - } + super_attrs = super().get_nodeattr_types() + super_attrs.update( + { + # attribute to save top module name - not user configurable + "gen_top_module": ("s", False, ""), + } + ) + return super_attrs @abstractmethod - def generate_hdl(self, model, fpgapart, clk): + def generate_hdl(self, model, fpgapart, clk) -> None: pass def prepare_rtlsim(self): @@ -77,12 +84,12 @@ def get_verilog_paths(self): return [code_gen_dir] @abstractmethod - def get_rtl_file_list(self, abspath=False): + def get_rtl_file_list(self, abspath=False) -> List[str] | List[Path]: """Returns list of rtl files. Needs to be filled by each node.""" pass @abstractmethod - def code_generation_ipi(self): + def code_generation_ipi(self) -> List[str]: pass def code_generation_ipgen(self, model, fpgapart, clk): diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py index eadd321a08..50aa88e84c 100644 --- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py +++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py @@ -27,8 +27,6 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Transformation to create stitched IP from dataflow graph components.""" - import json import multiprocessing as mp import os @@ -38,20 +36,20 @@ from shutil import copytree from subprocess import CalledProcessError +from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.transformation.fpgadataflow.replace_verilog_relpaths import ReplaceVerilogRelPaths from finn.util.basic import launch_process_helper, make_build_dir -from finn.util.exception import FINNError, FINNUserError +from finn.util.exception import FINNError, FINNInternalError, FINNUserError from finn.util.fpgadataflow import is_hls_node, is_rtl_node from finn.util.logging import log def is_external_input(model, node, i): - """ - Determine whether input i of node should be made external. - - True only if input is unconnected and has no initializer. - Only exception is second input of FC layers when mem_mode is external. - """ + # indicate whether input i of node should be made external + # True only if input is unconnected and has no initializer + # Only esception is second input of FC layers when mem_mode is external node_inst = getCustomOp(node) op_type = node.op_type producer = model.find_producer(node.input[i]) @@ -66,7 +64,8 @@ def is_external_input(model, node, i): def is_external_output(model, node, i): - """Determine whether output i of node should be made external.""" + # indicate whether output i of node should be made external + # True only if output is unconnected consumers = model.find_consumers(node.output[i]) if consumers == []: # TODO should ideally check if tensor is in top-level @@ -89,14 +88,22 @@ class CreateStitchedIP(Transformation): The packaged block design IP can be found under the ip subdirectory. """ - def __init__(self, fpgapart, clk_ns, ip_name="finn_design", vitis=False, signature=[]): - """Initialize CreateStitchedIP transformation with FPGA part and clock settings.""" + def __init__( + self, + fpgapart, + clk_ns, + ip_name="finn_design", + vitis=False, + signature=[], + functional_simulation=False, + ): super().__init__() self.fpgapart = fpgapart self.clk_ns = clk_ns self.ip_name = ip_name self.vitis = vitis self.signature = signature + self.functional_simulation = functional_simulation self.has_aximm = False self.has_m_axis = False self.m_axis_idx = 0 @@ -117,7 +124,6 @@ def __init__(self, fpgapart, clk_ns, ip_name="finn_design", vitis=False, signatu } def is_double_pumped(self, node): - """Check if node uses double pumped computation.""" if node.op_type.startswith("MVAU"): inst = getCustomOp(node) try: @@ -127,197 +133,203 @@ def is_double_pumped(self, node): return pumped_compute or inst.get_nodeattr("pumpedMemory") def connect_clk_rst(self, node): - """Connect clock and reset signals for the node.""" inst_name = node.name node_inst = getCustomOp(node) + if not isinstance(node_inst, HWCustomOp): + raise FINNInternalError( + f"Node {node.name} is not an HWCustomOp, cannot connect AXI interfaces." + ) clock_intf_name = node_inst.get_verilog_top_module_intf_names()["clk"][0] reset_intf_name = node_inst.get_verilog_top_module_intf_names()["rst"][0] + # make clock and reset external, if they aren't already if not self.clock_reset_are_external: - self.connect_cmds.append( - "make_bd_pins_external [get_bd_pins %s/%s]" % (inst_name, clock_intf_name) - ) - self.connect_cmds.append("set_property name ap_clk [get_bd_ports ap_clk_0]") - self.connect_cmds.append( - "make_bd_pins_external [get_bd_pins %s/%s]" % (inst_name, reset_intf_name) + self.connect_cmds.extend( + [ + f"make_bd_pins_external [get_bd_pins {inst_name}/{clock_intf_name}]", + "set_property name ap_clk [get_bd_ports ap_clk_0]", + f"make_bd_pins_external [get_bd_pins {inst_name}/{reset_intf_name}]", + "set_property name ap_rst_n [get_bd_ports ap_rst_n_0]", + ] ) - self.connect_cmds.append("set_property name ap_rst_n [get_bd_ports ap_rst_n_0]") self.clock_reset_are_external = True self.intf_names["clk"] = ["ap_clk"] self.intf_names["rst"] = ["ap_rst_n"] # otherwise connect clock and reset else: - self.connect_cmds.append( - "connect_bd_net [get_bd_ports ap_rst_n] [get_bd_pins %s/%s]" - % (inst_name, reset_intf_name) - ) - self.connect_cmds.append( - "connect_bd_net [get_bd_ports ap_clk] [get_bd_pins %s/%s]" - % (inst_name, clock_intf_name) + self.connect_cmds.extend( + [ + f"connect_bd_net [get_bd_ports ap_rst_n] " + f"[get_bd_pins {inst_name}/{reset_intf_name}]", + f"connect_bd_net [get_bd_ports ap_clk] " + f"[get_bd_pins {inst_name}/{clock_intf_name}]", + ] ) + # make clk2x external, if it isn't already and connect clk2x if self.is_double_pumped(node): clock2x_intf_name = node_inst.get_verilog_top_module_intf_names()["clk2x"][0] if not self.clock2x_is_external: - self.connect_cmds.append( - "make_bd_pins_external [get_bd_pins %s/%s]" % (inst_name, clock2x_intf_name) + self.connect_cmds.extend( + [ + f"make_bd_pins_external [get_bd_pins {inst_name}/{clock2x_intf_name}]", + "set_property name ap_clk2x [get_bd_ports ap_clk2x_0]", + ] ) - self.connect_cmds.append("set_property name ap_clk2x [get_bd_ports ap_clk2x_0]") self.clock2x_is_external = True self.intf_names["clk2x"] = ["ap_clk2x"] # otherwise connect clk2x else: if self.is_double_pumped(node): self.connect_cmds.append( - "connect_bd_net [get_bd_ports ap_clk2x] [get_bd_pins %s/%s]" - % (inst_name, clock2x_intf_name) + f"connect_bd_net [get_bd_ports ap_clk2x] " + f"[get_bd_pins {inst_name}/{clock2x_intf_name}]" ) def connect_axi(self, node): - """Connect AXI interfaces for the node.""" inst_name = node.name node_inst = getCustomOp(node) + if not isinstance(node_inst, HWCustomOp): + raise FINNInternalError( + f"Node {node.name} is not an HWCustomOp, cannot connect AXI interfaces." + ) axilite_intf_name = node_inst.get_verilog_top_module_intf_names()["axilite"] aximm_intf_name = node_inst.get_verilog_top_module_intf_names()["aximm"] + if len(axilite_intf_name) != 0: self.connect_cmds.append( - "make_bd_intf_pins_external " - "[get_bd_intf_pins %s/%s]" % (inst_name, axilite_intf_name[0]) - ) - ext_if_name = "%s_%d" % ( - axilite_intf_name[0], - len(self.intf_names["axilite"]), + f"make_bd_intf_pins_external " + f"[get_bd_intf_pins {inst_name}/{axilite_intf_name[0]}]" ) + ext_if_name = f"{axilite_intf_name[0]}_{len(self.intf_names['axilite'])}" self.intf_names["axilite"].append(ext_if_name) + if len(aximm_intf_name) != 0: - self.connect_cmds.append( - "make_bd_intf_pins_external [get_bd_intf_pins %s/%s]" - % (inst_name, aximm_intf_name[0][0]) - ) - ext_if_name = "m_axi_gmem%d" % (len(self.intf_names["aximm"])) - self.connect_cmds.append( - "set_property name %s [get_bd_intf_ports m_axi_gmem_0]" % ext_if_name + ext_if_name = f"m_axi_gmem{len(self.intf_names['aximm'])}" + seg_name = f"{inst_name}/Data_m_axi_gmem/SEG_{ext_if_name}_Reg" + + self.connect_cmds.extend( + [ + f"make_bd_intf_pins_external " + f"[get_bd_intf_pins {inst_name}/{aximm_intf_name[0][0]}]", + f"set_property name {ext_if_name} [get_bd_intf_ports m_axi_gmem_0]", + "assign_bd_address", + f"set_property offset 0 [get_bd_addr_segs {{{seg_name}}}]", + f"set_property range 4G [get_bd_addr_segs {{{seg_name}}}]", + ] ) - self.connect_cmds.append("assign_bd_address") - seg_name = "%s/Data_m_axi_gmem/SEG_%s_Reg" % (inst_name, ext_if_name) - self.connect_cmds.append("set_property offset 0 [get_bd_addr_segs {%s}]" % (seg_name)) - # TODO should propagate this information from the node instead of 4G - self.connect_cmds.append("set_property range 4G [get_bd_addr_segs {%s}]" % (seg_name)) + self.intf_names["aximm"] = [(ext_if_name, aximm_intf_name[0][1])] self.has_aximm = True def connect_m_axis_external(self, node, idx=None): - """Connect master AXI stream interfaces as external ports.""" inst_name = node.name node_inst = getCustomOp(node) + if not isinstance(node_inst, HWCustomOp): + raise FINNInternalError( + f"Node {node.name} is not an HWCustomOp, cannot connect AXI interfaces." + ) output_intf_names = node_inst.get_verilog_top_module_intf_names()["m_axis"] + # make output axis external for i in range(len(output_intf_names)): if idx is not None and idx != i: continue output_intf_name = output_intf_names[i][0] - self.connect_cmds.append( - "make_bd_intf_pins_external [get_bd_intf_pins %s/%s]" - % (inst_name, output_intf_name) - ) - self.connect_cmds.append( - "set_property name m_axis_%d [get_bd_intf_ports %s_0]" - % (self.m_axis_idx, output_intf_name) + + self.connect_cmds.extend( + [ + f"make_bd_intf_pins_external [get_bd_intf_pins {inst_name}/{output_intf_name}]", + f"set_property name m_axis_{self.m_axis_idx} " + f"[get_bd_intf_ports {output_intf_name}_0]", + ] ) + self.has_m_axis = True - self.intf_names["m_axis"].append( - ("m_axis_%d" % self.m_axis_idx, output_intf_names[i][1]) - ) + self.intf_names["m_axis"].append((f"m_axis_{self.m_axis_idx}", output_intf_names[i][1])) self.m_axis_idx += 1 def connect_s_axis_external(self, node, idx=None): - """Connect slave AXI stream interfaces as external ports.""" inst_name = node.name node_inst = getCustomOp(node) + if not isinstance(node_inst, HWCustomOp): + raise FINNInternalError( + f"Node {node.name} is not an HWCustomOp, cannot connect AXI interfaces." + ) input_intf_names = node_inst.get_verilog_top_module_intf_names()["s_axis"] + # make input axis external for i in range(len(input_intf_names)): if idx is not None and idx != i: continue input_intf_name = input_intf_names[i][0] - self.connect_cmds.append( - "make_bd_intf_pins_external [get_bd_intf_pins %s/%s]" % (inst_name, input_intf_name) - ) - self.connect_cmds.append( - "set_property name s_axis_%d [get_bd_intf_ports %s_0]" - % (self.s_axis_idx, input_intf_name) + + self.connect_cmds.extend( + [ + f"make_bd_intf_pins_external [get_bd_intf_pins {inst_name}/{input_intf_name}]", + f"set_property name s_axis_{self.s_axis_idx} " + f"[get_bd_intf_ports {input_intf_name}_0]", + ] ) + self.has_s_axis = True - self.intf_names["s_axis"].append( - ("s_axis_%d" % self.s_axis_idx, input_intf_names[i][1]) - ) + self.intf_names["s_axis"].append((f"s_axis_{self.s_axis_idx}", input_intf_names[i][1])) self.s_axis_idx += 1 def connect_ap_none_external(self, node): - """Connect ap_none interfaces as external ports.""" inst_name = node.name node_inst = getCustomOp(node) + if not isinstance(node_inst, HWCustomOp): + raise FINNInternalError( + f"Node {node.name} is not an HWCustomOp, cannot connect AXI interfaces." + ) input_intf_names = node_inst.get_verilog_top_module_intf_names()["ap_none"] + # make external for i in range(len(input_intf_names)): input_intf_name = input_intf_names[i] - self.connect_cmds.append( - "make_bd_pins_external [get_bd_pins %s/%s]" % (inst_name, input_intf_name) - ) - self.connect_cmds.append( - "set_property name %s [get_bd_ports %s_0]" % (input_intf_name, input_intf_name) + self.connect_cmds.extend( + [ + f"make_bd_pins_external [get_bd_pins {inst_name}/{input_intf_name}]", + f"set_property name {input_intf_name} [get_bd_ports {input_intf_name}_0]", + ] ) def insert_signature(self, checksum_count): - """Insert signature block for design identification.""" signature_vlnv = "AMD:user:axi_info_top:1.0" signature_name = "axi_info_top0" - self.create_cmds.append( - "create_bd_cell -type ip -vlnv %s %s" % (signature_vlnv, signature_name) - ) - self.create_cmds.append( - "set_property -dict [list " - "CONFIG.SIG_CUSTOMER {%s} " - "CONFIG.SIG_APPLICATION {%s} " - "CONFIG.VERSION {%s} " - "CONFIG.CHECKSUM_COUNT {%s} " - "] [get_bd_cells %s]" - % ( - self.signature[0], - self.signature[1], - self.signature[2], - checksum_count, - signature_name, - ) - ) - # set clk and reset - self.connect_cmds.append( - "connect_bd_net [get_bd_ports ap_clk] [get_bd_pins %s/ap_clk]" % signature_name - ) - self.connect_cmds.append( - "connect_bd_net [get_bd_ports ap_rst_n] [get_bd_pins %s/ap_rst_n]" % signature_name - ) fclk_mhz = 1 / (self.clk_ns * 0.001) fclk_hz = fclk_mhz * 1000000 - self.connect_cmds.append( - "set_property -dict [list " - "CONFIG.FREQ_HZ {%f} " - "CONFIG.CLK_DOMAIN {ap_clk} " - "] [get_bd_intf_pins %s/s_axi]" - % ( - fclk_hz, - signature_name, - ) + + # Create signature cell and configure properties + self.create_cmds.extend( + [ + f"create_bd_cell -type ip -vlnv {signature_vlnv} {signature_name}", + f"set_property -dict [list " + f"CONFIG.SIG_CUSTOMER {{{self.signature[0]}}} " + f"CONFIG.SIG_APPLICATION {{{self.signature[1]}}} " + f"CONFIG.VERSION {{{self.signature[2]}}} " + f"CONFIG.CHECKSUM_COUNT {{{checksum_count}}} " + f"] [get_bd_cells {signature_name}]", + ] ) - # make axilite interface external - self.connect_cmds.append( - "make_bd_intf_pins_external [get_bd_intf_pins %s/s_axi]" % signature_name + + # Connect clocks, resets and configure AXI interface + self.connect_cmds.extend( + [ + f"connect_bd_net [get_bd_ports ap_clk] [get_bd_pins {signature_name}/ap_clk]", + f"connect_bd_net [get_bd_ports ap_rst_n] [get_bd_pins {signature_name}/ap_rst_n]", + f"set_property -dict [list " + f"CONFIG.FREQ_HZ {{{fclk_hz}}} " + f"CONFIG.CLK_DOMAIN {{ap_clk}} " + f"] [get_bd_intf_pins {signature_name}/s_axi]", + f"make_bd_intf_pins_external [get_bd_intf_pins {signature_name}/s_axi]", + "set_property name s_axilite_info [get_bd_intf_ports s_axi_0]", + "assign_bd_address", + ] ) - self.connect_cmds.append("set_property name s_axilite_info [get_bd_intf_ports s_axi_0]") - self.connect_cmds.append("assign_bd_address") def apply(self, model): - """Apply the CreateStitchedIP transformation to the model.""" # ensure non-relative readmemh .dat files model = model.transform(ReplaceVerilogRelPaths()) ip_dirs = ["list"] @@ -342,12 +354,23 @@ def apply(self, model): ) for node in model.graph.node: # ensure that all nodes are fpgadataflow, and that IPs are generated - assert is_hls_node(node) or is_rtl_node( - node - ), "All nodes must be FINN fpgadataflow nodes." + if not is_hls_node(node) and not is_rtl_node(node): + raise FINNUserError( + f"{node.name} is not an fpgadataflow node. Aborting stitching IP." + ) node_inst = getCustomOp(node) + if not isinstance(node_inst, RTLBackend) and not isinstance(node_inst, HLSBackend): + raise FINNInternalError( + f"Node {node.name} is not an RTL Node or HLS Node, " + "cannot connect AXI interfaces." + ) ip_dir_value = node_inst.get_nodeattr("ip_path") - assert os.path.isdir(ip_dir_value), "IP generation directory doesn't exist." + if type(ip_dir_value) is not str or ip_dir_value == "": + raise FINNInternalError(f"ip_path has the wrong type in node {node.name}.") + if not os.path.isdir(ip_dir_value): + raise FINNInternalError( + f"IP generation directory doesn't exist in node {node.name}." + ) ip_dirs += [ip_dir_value] self.create_cmds += node_inst.code_generation_ipi() self.connect_clk_rst(node) @@ -359,22 +382,25 @@ def apply(self, model): if producer is None: continue j = list(producer.output).index(node.input[i]) - src_intf_name = getCustomOp(producer).get_verilog_top_module_intf_names()[ - "m_axis" - ][j][0] + prod = getCustomOp(producer) + if not isinstance(prod, HWCustomOp): + raise FINNInternalError( + f"Producer node {producer.name} is not an HWCustomOp, " + "cannot connect AXI interfaces." + ) + src_intf_name = prod.get_verilog_top_module_intf_names()["m_axis"][j][0] dst_intf_name = node_inst.get_verilog_top_module_intf_names()["s_axis"][i][0] self.connect_cmds.append( - "connect_bd_intf_net [get_bd_intf_pins %s/%s] " - "[get_bd_intf_pins %s/%s]" - % (producer.name, src_intf_name, node.name, dst_intf_name) + f"connect_bd_intf_net [get_bd_intf_pins {producer.name}/{src_intf_name}] " + f"[get_bd_intf_pins {node.name}/{dst_intf_name}]" ) # process external inputs and outputs in top-level graph input order for input in model.graph.input: inp_name = input.name inp_cons = model.find_consumers(inp_name) - assert inp_cons != [], "No consumer for input " + inp_name - assert len(inp_cons) == 1, "Multiple consumers for input " + inp_name + assert inp_cons != [], f"No consumer for input {inp_name}" + assert len(inp_cons) == 1, f"Multiple consumers for input {inp_name}" node = inp_cons[0] node_inst = getCustomOp(node) for i in range(len(node.input)): @@ -383,7 +409,7 @@ def apply(self, model): for output in model.graph.output: out_name = output.name node = model.find_producer(out_name) - assert node is not None, "No producer for output " + out_name + assert node is not None, f"No producer for output {out_name}" node_inst = getCustomOp(node) for i in range(len(node.output)): if node.output[i] == out_name: @@ -400,151 +426,175 @@ def apply(self, model): model.set_metadata_prop("vivado_stitch_proj", vivado_stitch_proj_dir) # start building the tcl script tcl = [] - # create vivado project - tcl.append( - "create_project %s %s -part %s" % (prjname, vivado_stitch_proj_dir, self.fpgapart) - ) - # no warnings on long module names - tcl.append("set_msg_config -id {[BD 41-1753]} -suppress") - # add all the generated IP dirs to ip_repo_paths + + # Project setup ip_dirs_str = " ".join(ip_dirs) - tcl.append("set_property ip_repo_paths [%s] [current_project]" % ip_dirs_str) - tcl.append("update_ip_catalog") - # create block design and instantiate all layers block_name = self.ip_name - tcl.append('create_bd_design "%s"' % block_name) + + tcl.extend( + [ + f"create_project {prjname} {vivado_stitch_proj_dir} -part {self.fpgapart}", + "set_msg_config -id {[BD 41-1753]} -suppress", + f"set_property ip_repo_paths [{ip_dirs_str}] [current_project]", + "update_ip_catalog", + f'create_bd_design "{block_name}"', + ] + ) + # Add commands and validate design tcl.extend(self.create_cmds) tcl.extend(self.connect_cmds) + fclk_mhz = 1 / (self.clk_ns * 0.001) fclk_hz = fclk_mhz * 1000000 - tcl.append("set_property CONFIG.FREQ_HZ %d [get_bd_ports /ap_clk]" % round(fclk_hz)) + + # Configure clocks and validate design + clock_config = [f"set_property CONFIG.FREQ_HZ {round(fclk_hz)} [get_bd_ports /ap_clk]"] if self.clock2x_is_external: - tcl.append( - "set_property CONFIG.FREQ_HZ %d [get_bd_ports /ap_clk2x]" % round(2 * fclk_hz) + clock_config.append( + f"set_property CONFIG.FREQ_HZ {round(2 * fclk_hz)} [get_bd_ports /ap_clk2x]" ) - tcl.append("validate_bd_design") - tcl.append("save_bd_design") - # create wrapper hdl (for rtlsim later on) - bd_base = "%s/%s.srcs/sources_1/bd/%s" % ( - vivado_stitch_proj_dir, - prjname, - block_name, + + clock_config.extend(["validate_bd_design", "save_bd_design"]) + + tcl.extend(clock_config) + + # Create wrapper HDL + bd_base = f"{vivado_stitch_proj_dir}/{prjname}.srcs/sources_1/bd/{block_name}" + bd_filename = f"{bd_base}/{block_name}.bd" + wrapper_filename = f"{bd_base}/hdl/{block_name}_wrapper.v" + + tcl.extend( + [ + f"make_wrapper -files [get_files {bd_filename}] -top", + f"add_files -norecurse {wrapper_filename}", + f"set_property top {block_name}_wrapper [current_fileset]", + ] ) - bd_filename = "%s/%s.bd" % (bd_base, block_name) - tcl.append("make_wrapper -files [get_files %s] -top" % bd_filename) - wrapper_filename = "%s/hdl/%s_wrapper.v" % (bd_base, block_name) - tcl.append("add_files -norecurse %s" % wrapper_filename) + model.set_metadata_prop("wrapper_filename", wrapper_filename) - tcl.append("set_property top %s_wrapper [current_fileset]" % block_name) - # synthesize to DCP and export stub, DCP and constraints + num_workers = get_num_default_workers() + assert num_workers >= 0, "Number of workers must be nonnegative." + if num_workers == 0: + num_workers = mp.cpu_count() + + fifosim_wrapper_filename = None + if self.functional_simulation: + bd_base_sim = f"{vivado_stitch_proj_dir}/{prjname}.sim/sim_1/synth/func/xsim/" + fifosim_wrapper_filename = f"{bd_base_sim}/fifosim_wrapper_func_synth.v" + + tcl.extend( + [ + f"launch_runs synth_1 -jobs {num_workers}", + "wait_on_run [get_runs synth_1]", + "open_run synth_1 -name synth_1", + "opt_design", + # "opt_design -muxf_remap -carry_remap -control_set_merge " + # "-merge_equivalent_drivers -mbufg_opt -dsp_register_opt " + # "-control_set_opt -remap -resynth_area -resynth_remap", + # "opt_design", + f"write_verilog -mode funcsim -force -file {fifosim_wrapper_filename}", + ] + ) + + model.set_metadata_prop("wrapper_filename", fifosim_wrapper_filename) + # Synthesize to DCP and export stub, DCP and constraints if self.vitis: - tcl.append( - "set_property SYNTH_CHECKPOINT_MODE Hierarchical [ get_files %s ]" % bd_filename - ) - tcl.append( - "set_property -name {STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS} " - "-value {-mode out_of_context} -objects [get_runs synth_1]" - ) - num_workers = get_num_default_workers() - assert num_workers >= 0, "Number of workers must be nonnegative." - if num_workers == 0: - num_workers = mp.cpu_count() - tcl.append("launch_runs synth_1 -jobs %s" % str(num_workers)) - tcl.append("wait_on_run [get_runs synth_1]") - tcl.append("open_run synth_1 -name synth_1") - tcl.append("write_verilog -force -mode synth_stub %s.v" % block_name) - tcl.append("write_checkpoint %s.dcp" % block_name) - tcl.append("write_xdc %s.xdc" % block_name) - tcl.append( - "report_utilization -hierarchical -hierarchical_depth 5 " - "-file %s_partition_util.rpt" % block_name - ) - # export block design itself as an IP core + tcl.extend( + [ + f"set_property SYNTH_CHECKPOINT_MODE Hierarchical [ get_files {bd_filename} ]", + "set_property -name {STEPS.SYNTH_DESIGN.ARGS.MORE OPTIONS} " + "-value {-mode out_of_context} -objects [get_runs synth_1]", + f"launch_runs synth_1 -jobs {num_workers}", + "wait_on_run [get_runs synth_1]", + "open_run synth_1 -name synth_1", + f"write_verilog -force -mode synth_stub {block_name}.v", + f"write_checkpoint {block_name}.dcp", + f"write_xdc {block_name}.xdc", + f"report_utilization -hierarchical -hierarchical_depth 5 " + f"-file {block_name}_partition_util.rpt", + ] + ) + # Export block design itself as an IP core block_vendor = "xilinx_finn" block_library = "finn" - block_vlnv = "%s:%s:%s:1.0" % (block_vendor, block_library, block_name) + block_vlnv = f"{block_vendor}:{block_library}:{block_name}:1.0" model.set_metadata_prop("vivado_stitch_vlnv", block_vlnv) model.set_metadata_prop("vivado_stitch_ifnames", json.dumps(self.intf_names)) - tcl.append( - ( - "ipx::package_project -root_dir %s/ip -vendor %s " - "-library %s -taxonomy /UserIP -module %s -import_files" - ) - % (vivado_stitch_proj_dir, block_vendor, block_library, block_name) - ) - # Allow user to customize clock in deployment of stitched IP - tcl.append("set_property ipi_drc {ignore_freq_hz true} [ipx::current_core]") - # in some cases, the IP packager seems to infer an aperture of 64K or 4G, - # preventing address assignment of the DDR_LOW and/or DDR_HIGH segments - # the following is a hotfix to remove this aperture during IODMA packaging - tcl.append( - "ipx::remove_segment -quiet m_axi_gmem0:APERTURE_0 " - "[ipx::get_address_spaces m_axi_gmem0 -of_objects [ipx::current_core]]" - ) - tcl.append("set_property core_revision 2 [ipx::find_open_core %s]" % block_vlnv) - tcl.append("ipx::create_xgui_files [ipx::find_open_core %s]" % block_vlnv) - # mark bus interface params as user-resolvable to avoid FREQ_MHZ mismatches - tcl.append( - "set_property value_resolve_type user [ipx::get_bus_parameters " - "-of [ipx::get_bus_interfaces -of [ipx::current_core ]]]" + + # Package IP and configure properties + tcl.extend( + [ + f"ipx::package_project -root_dir {vivado_stitch_proj_dir}/ip " + f"-vendor {block_vendor} -library {block_library} -taxonomy /UserIP " + f"-module {block_name} -import_files", + "set_property ipi_drc {ignore_freq_hz true} [ipx::current_core]", + "ipx::remove_segment -quiet m_axi_gmem0:APERTURE_0 " + "[ipx::get_address_spaces m_axi_gmem0 -of_objects [ipx::current_core]]", + f"set_property core_revision 2 [ipx::find_open_core {block_vlnv}]", + f"ipx::create_xgui_files [ipx::find_open_core {block_vlnv}]", + "set_property value_resolve_type user [ipx::get_bus_parameters " + "-of [ipx::get_bus_interfaces -of [ipx::current_core ]]]", + ] ) - # if targeting Vitis, add some properties to the IP + # If targeting Vitis, add some properties to the IP if self.vitis: - # replace source code with dcp - tcl.append("set_property sdx_kernel true [ipx::find_open_core %s]" % block_vlnv) - tcl.append("set_property sdx_kernel_type rtl [ipx::find_open_core %s]" % block_vlnv) - tcl.append("set_property supported_families { } [ipx::find_open_core %s]" % block_vlnv) - tcl.append( - "set_property xpm_libraries {XPM_CDC XPM_MEMORY XPM_FIFO} " - "[ipx::find_open_core %s]" % block_vlnv - ) - tcl.append( - "set_property auto_family_support_level level_2 " - "[ipx::find_open_core %s]" % block_vlnv - ) - # remove all files from synthesis and sim groups - # we'll replace with DCP, stub, and xdc - tcl.append( - "ipx::remove_all_file " - "[ipx::get_file_groups xilinx_anylanguagebehavioralsimulation]" - ) - tcl.append("ipx::remove_all_file " "[ipx::get_file_groups xilinx_anylanguagesynthesis]") - tcl.append( - "ipx::remove_file_group " - "xilinx_anylanguagebehavioralsimulation [ipx::current_core]" - ) - tcl.append("ipx::remove_file_group " "xilinx_anylanguagesynthesis [ipx::current_core]") - # remove sim and src folders - tcl.append("file delete -force %s/ip/sim" % vivado_stitch_proj_dir) - tcl.append("file delete -force %s/ip/src" % vivado_stitch_proj_dir) - # copy and add DCP, stub, and xdc - tcl.append("file mkdir %s/ip/dcp" % vivado_stitch_proj_dir) - tcl.append("file mkdir %s/ip/impl" % vivado_stitch_proj_dir) - tcl.append("file copy -force %s.dcp %s/ip/dcp" % (block_name, vivado_stitch_proj_dir)) - tcl.append("file copy -force %s.xdc %s/ip/impl" % (block_name, vivado_stitch_proj_dir)) - tcl.append("ipx::add_file_group xilinx_implementation [ipx::current_core]") - tcl.append( - "ipx::add_file impl/%s.xdc [ipx::get_file_groups xilinx_implementation]" - % block_name - ) - tcl.append( - "set_property used_in [list implementation] " - "[ipx::get_files impl/%s.xdc " - "-of_objects [ipx::get_file_groups xilinx_implementation]]" % block_name - ) - tcl.append("ipx::add_file_group " "xilinx_synthesischeckpoint [ipx::current_core]") - tcl.append( - "ipx::add_file dcp/%s.dcp " - "[ipx::get_file_groups xilinx_synthesischeckpoint]" % block_name - ) - tcl.append("ipx::add_file_group xilinx_simulationcheckpoint [ipx::current_core]") - tcl.append( - "ipx::add_file dcp/%s.dcp " - "[ipx::get_file_groups xilinx_simulationcheckpoint]" % block_name + # Configure Vitis kernel properties + tcl.extend( + [ + f"set_property sdx_kernel true [ipx::find_open_core {block_vlnv}]", + f"set_property sdx_kernel_type rtl [ipx::find_open_core {block_vlnv}]", + f"set_property supported_families {{}} [ipx::find_open_core {block_vlnv}]", + f"set_property xpm_libraries {{XPM_CDC XPM_MEMORY XPM_FIFO}} " + f"[ipx::find_open_core {block_vlnv}]", + f"set_property auto_family_support_level level_2 " + f"[ipx::find_open_core {block_vlnv}]", + ] + ) + + # Remove all files from synthesis and sim groups and replace with DCP + tcl.extend( + [ + "ipx::remove_all_file " + "[ipx::get_file_groups xilinx_anylanguagebehavioralsimulation]", + "ipx::remove_all_file [ipx::get_file_groups xilinx_anylanguagesynthesis]", + "ipx::remove_file_group " + "xilinx_anylanguagebehavioralsimulation [ipx::current_core]", + "ipx::remove_file_group xilinx_anylanguagesynthesis [ipx::current_core]", + ] + ) + + # Setup file structure for DCP-based IP + tcl.extend( + [ + f"file delete -force {vivado_stitch_proj_dir}/ip/sim", + f"file delete -force {vivado_stitch_proj_dir}/ip/src", + f"file mkdir {vivado_stitch_proj_dir}/ip/dcp", + f"file mkdir {vivado_stitch_proj_dir}/ip/impl", + f"file copy -force {block_name}.dcp {vivado_stitch_proj_dir}/ip/dcp", + f"file copy -force {block_name}.xdc {vivado_stitch_proj_dir}/ip/impl", + ] + ) + + # Add implementation and checkpoint file groups + tcl.extend( + [ + "ipx::add_file_group xilinx_implementation [ipx::current_core]", + f"ipx::add_file impl/{block_name}.xdc " + "[ipx::get_file_groups xilinx_implementation]", + f"set_property used_in [list implementation] " + f"[ipx::get_files impl/{block_name}.xdc " + f"-of_objects [ipx::get_file_groups xilinx_implementation]]", + "ipx::add_file_group xilinx_synthesischeckpoint [ipx::current_core]", + f"ipx::add_file dcp/{block_name}.dcp " + f"[ipx::get_file_groups xilinx_synthesischeckpoint]", + "ipx::add_file_group xilinx_simulationcheckpoint [ipx::current_core]", + f"ipx::add_file dcp/{block_name}.dcp " + f"[ipx::get_file_groups xilinx_simulationcheckpoint]", + ] ) # add a rudimentary driver mdd to get correct ranges in xparameters.h later on example_data_dir = os.path.join(os.environ["FINN_QNN_DATA"], "mdd-data") - copytree(example_data_dir, vivado_stitch_proj_dir + "/data") + copytree(example_data_dir, f"{vivado_stitch_proj_dir}/data") ##### # Core Cleanup Operations @@ -623,52 +673,54 @@ def apply(self, model): """ ) - # export list of used Verilog files (for rtlsim later on) - tcl.append( - "set all_v_files [get_files -filter {USED_IN_SYNTHESIS == 1 " - + "&& (FILE_TYPE == Verilog || FILE_TYPE == SystemVerilog " - + '|| FILE_TYPE =="Verilog Header")}]' + # Export list of used Verilog files (for rtlsim later on) + v_file_list = f"{vivado_stitch_proj_dir}/all_verilog_srcs.txt" + tcl.extend( + [ + "set all_v_files [get_files -filter {USED_IN_SYNTHESIS == 1 " + + "&& (FILE_TYPE == Verilog || FILE_TYPE == SystemVerilog " + + '|| FILE_TYPE =="Verilog Header")}]', + f"set fp [open {v_file_list} w]", + "foreach vf $all_v_files {puts $fp $vf}", + "close $fp", + ] ) - v_file_list = "%s/all_verilog_srcs.txt" % vivado_stitch_proj_dir - tcl.append("set fp [open %s w]" % v_file_list) - # write each verilog filename to all_verilog_srcs.txt - tcl.append("foreach vf $all_v_files {puts $fp $vf}") - tcl.append("close $fp") # write the project creator tcl script tcl_string = "\n".join(tcl) + "\n" - with open(vivado_stitch_proj_dir + "/make_project.tcl", "w") as f: + with open(f"{vivado_stitch_proj_dir}/make_project.tcl", "w") as f: f.write(tcl_string) # create a shell script and call Vivado - make_project_sh = vivado_stitch_proj_dir + "/make_project.sh" + make_project_sh = f"{vivado_stitch_proj_dir}/make_project.sh" working_dir = os.getcwd() with open(make_project_sh, "w") as f: f.write("#!/bin/bash \n") - f.write("cd {}\n".format(vivado_stitch_proj_dir)) - f.write("set -e\n") # Exit with non-zero if vivado fails. + f.write(f"cd {vivado_stitch_proj_dir}\n") f.write("vivado -mode batch -source make_project.tcl\n") - f.write("cd {}\n".format(working_dir)) + f.write(f"cd {working_dir}\n") bash_command = ["bash", make_project_sh] try: launch_process_helper(bash_command, print_stdout=False) - except CalledProcessError as e: - raise FINNUserError( - f"CreateStitchedIP: make_project.sh failed with a non-zero " - f"exit code. Check previous logs and logs in " - f"{vivado_stitch_proj_dir} to find out why it failed." - ) from e + except CalledProcessError: + # Check success manually by looking for wrapper HDL + pass + + if self.functional_simulation: + with open(v_file_list, "a") as f: + f.write(f"{fifosim_wrapper_filename}\n") # wrapper may be created in different location depending on Vivado version if not os.path.isfile(wrapper_filename): # check in alternative location (.gen instead of .srcs) wrapper_filename_alt = wrapper_filename.replace(".srcs", ".gen") if os.path.isfile(wrapper_filename_alt): - model.set_metadata_prop("wrapper_filename", wrapper_filename_alt) + if not self.functional_simulation: + model.set_metadata_prop("wrapper_filename", wrapper_filename_alt) else: raise FINNError( - """CreateStitchedIP failed, no wrapper HDL found under %s or %s. + f"""CreateStitchedIP failed, no wrapper HDL found \ + under {wrapper_filename} or {wrapper_filename_alt}. Please check logs under the parent directory.""" - % (wrapper_filename, wrapper_filename_alt) ) return (model, False) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 86261ee134..e7651c9fff 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -3,6 +3,7 @@ import numpy as np import onnx import os +import psutil import shlex import subprocess import sys @@ -13,11 +14,17 @@ from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation +from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames +from qonnx.transformation.infer_shapes import InferShapes from random import Random from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, cast from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP +from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP +from finn.transformation.fpgadataflow.insert_dwc import InsertDWC +from finn.transformation.fpgadataflow.prepare_ip import PrepareIP +from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.basic import get_vivado_root, launch_process_helper, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log @@ -85,23 +92,59 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: for i, node in enumerate(self.model.graph.node): if i != index: node_model.graph.node.remove(node) - target_op: HWCustomOp = getCustomOp(self.model.graph.node[0]) + target_op = getCustomOp(node_model.graph.node[0]) + if not isinstance(target_op, HWCustomOp): + raise FINNInternalError( + f"Node {node_model.graph.node[0].name} is not a HWCustomOp, cannot " + f"isolate for simulation." + ) inp = onnx.helper.make_tensor_value_info( "inp", TensorProto.FLOAT, target_op.get_folded_input_shape() ) - outp = onnx.helper.make_tensor_value_info( + inp_dummy_out = onnx.helper.make_tensor_value_info( # noqa + "inp_dummy_out", TensorProto.FLOAT, target_op.get_folded_input_shape() + ) + outp = onnx.helper.make_tensor_value_info( # noqa "outp", TensorProto.FLOAT, target_op.get_normal_output_shape() ) + outp_dummy_out = onnx.helper.make_tensor_value_info( + "outp_dummy_out", TensorProto.FLOAT, target_op.get_normal_output_shape() + ) + input_dummy_node = onnx.helper.make_node( + "RemoveDataPath_rtl", + inputs=["inp"], + outputs=["inp_dummy_out"], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + folded_shape=target_op.get_folded_input_shape(), + normal_shape=target_op.get_normal_input_shape(), + dataType=target_op.get_input_datatype().name, + name=node_model.graph.node[0].name + "_input_dummy", + ) + output_dummy_node = onnx.helper.make_node( + "RemoveDataPath_rtl", + inputs=["outp"], + outputs=["outp_dummy_out"], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + folded_shape=target_op.get_folded_output_shape(), + normal_shape=target_op.get_normal_output_shape(), + dataType=target_op.get_output_datatype().name, + name=node_model.graph.node[0].name + "_output_dummy", + ) + + node_model.graph.node.insert(0, input_dummy_node) + node_model.graph.node.append(output_dummy_node) # Remove old io - for _ in range(len(node_model.graph.node[0].input)): - node_model.graph.node[0].input.pop() - for _ in range(len(node_model.graph.node[0].output)): - node_model.graph.node[0].output.pop() + for _ in range(len(node_model.graph.node[1].input)): + node_model.graph.node[1].input.pop() + for _ in range(len(node_model.graph.node[1].output)): + node_model.graph.node[1].output.pop() # Set new io - node_model.graph.node[0].input.append("inp") - node_model.graph.node[0].output.append("outp") + node_model.graph.node[1].input.append("inp_dummy_out") + node_model.graph.node[1].output.append("outp") # Remove graph io for _ in range(len(node_model.graph.input)): @@ -111,7 +154,7 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: # Set new graph io node_model.graph.input.append(inp) - node_model.graph.output.append(outp) + node_model.graph.output.append(outp_dummy_out) return node_model @@ -350,8 +393,8 @@ def _build_single_node_simulation( # TODO: Check if something is an output node instead of checking the node index # TODO: Requires changes in the C++ code as well - # Sanity checks - if len(node_model.graph.node) > 1: + # Sanity checks (2 Dummy nodes + 1 target node) + if len(node_model.graph.node) != 3: raise FINNUserError( "Cannot create single-node simulation for a model with more than " "1 node. Make sure to pass the ModelWrapper containing only" @@ -362,7 +405,9 @@ def _build_single_node_simulation( wrapper_filename = node_model.get_metadata_prop("wrapper_filename") if wrapper_filename is None or not Path(wrapper_filename).exists(): raise FINNUserError( - f"Call CreateStitchedIP prior to building " f"the simulation for {node_name}" + f"Call CreateStitchedIP prior to building " + f"the simulation for {node_name}." + f"wrapper_filename is set to {wrapper_filename}!" ) vivado_stitched_proj = node_model.get_metadata_prop("vivado_stitch_proj") @@ -415,7 +460,11 @@ def _build_simulation( build_dir: Path, ) -> Any: nodemodel = self._isolated_node_model(node_index) - nodemodel = nodemodel.transform(CreateStitchedIP(self.fpgapart, self.clk_ns)) + nodemodel = nodemodel.transform(InferShapes()) + nodemodel = nodemodel.transform(PrepareIP(self.fpgapart, self.clk_ns)) + nodemodel = nodemodel.transform( + CreateStitchedIP(self.fpgapart, self.clk_ns, functional_simulation=True) + ) return self._build_single_node_simulation( node_name, nodemodel, node_index, total_nodes, prev_node_name, build_dir ) @@ -446,7 +495,16 @@ def _run_simulation(binary: Path, cpu: int | None) -> None: total_nodes = len(self.model.graph.node) futures: dict[int, Future] = {} binaries: dict[int, Path] = {} - with ThreadPoolExecutor(max_workers=workers) as pool: + self.model = self.model.transform(InsertDWC()) + self.model = self.model.transform(SpecializeLayers(self.fpgapart)) + self.model = self.model.transform(GiveUniqueNodeNames()) + self.model = self.model.transform(GiveReadableTensorNames()) + self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) + self.model = self.model.transform(HLSSynthIP()) + synth_workers = max( + 1, cast(int, (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 16) + ) # 16GB per synthesis + with ThreadPoolExecutor(max_workers=synth_workers) as pool: for i in range(total_nodes): futures[i] = pool.submit( _build_simulation, diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 86881585d8..2ba132af72 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -159,7 +159,7 @@ def get_liveness_threshold_cycles(): return int(os.getenv("LIVENESS_THRESHOLD", 1000000)) -def make_build_dir(prefix: str = "", return_as_path: bool = False) -> str | Path: +def make_build_dir(prefix: str = "", return_as_path: bool = False) -> str: """Creates a folder with given prefix to be used as a build dir. Use this function instead of tempfile.mkdtemp to ensure any generated files will survive on the host after the FINN Docker container exits.""" From 0eae0af95117d2ed3f43b7a93b1ab0b1bfa37422 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:56:37 +0100 Subject: [PATCH 020/170] Fix small problem --- finn_xsi/finn_xsi/include/SimulationInterface.hpp | 4 ++-- src/finn/transformation/fpgadataflow/simulation.py | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/finn_xsi/finn_xsi/include/SimulationInterface.hpp b/finn_xsi/finn_xsi/include/SimulationInterface.hpp index 61033d1646..12119173ae 100644 --- a/finn_xsi/finn_xsi/include/SimulationInterface.hpp +++ b/finn_xsi/finn_xsi/include/SimulationInterface.hpp @@ -75,9 +75,9 @@ class SimulationInterface { SharedData* sharedData = nullptr; boost::ipc_atomic* refCount = nullptr; + const std::string shmIdentifier; std::atomic largestOccupation; ipc::managed_shared_memory shmem; - const std::string shmIdentifier; #ifdef NDEBUG [[maybe_unused]] void simInterfaceDebug([[maybe_unused]] std::string_view s) {} @@ -126,7 +126,7 @@ class SimulationInterface { // Move constructor SimulationInterface(SimulationInterface&& other) noexcept - : sharedData(other.sharedData), refCount(other.refCount), shmem(std::move(other.shmem)), shmIdentifier(std::move(other.shmIdentifier)) { + : sharedData(other.sharedData), refCount(other.refCount), shmIdentifier(std::move(other.shmIdentifier)), shmem(std::move(other.shmem)) { // Mark other as moved-from other.sharedData = nullptr; other.refCount = nullptr; diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index bd5cdbb2dc..43b200ff40 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -18,8 +18,9 @@ from qonnx.transformation.infer_shapes import InferShapes from random import Random from subprocess import CalledProcessError -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.insert_dwc import InsertDWC @@ -35,10 +36,6 @@ finnxsi = None -if TYPE_CHECKING: - from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp - - class Simulation: """Manage simulations in FINN.""" From 5e77755d8ed7f457dc62735f20321513af303ecb Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:09:31 +0100 Subject: [PATCH 021/170] Add typing --- src/finn/custom_op/fpgadataflow/hwcustomop.py | 365 +++++++++++------- .../fpgadataflow/rtl/removedatapath_rtl.py | 185 +++++++-- src/finn/custom_op/fpgadataflow/rtlbackend.py | 31 +- 3 files changed, 420 insertions(+), 161 deletions(-) diff --git a/src/finn/custom_op/fpgadataflow/hwcustomop.py b/src/finn/custom_op/fpgadataflow/hwcustomop.py index 1cba406255..69f3f2ed4c 100644 --- a/src/finn/custom_op/fpgadataflow/hwcustomop.py +++ b/src/finn/custom_op/fpgadataflow/hwcustomop.py @@ -26,24 +26,37 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Base class for hardware custom operations in FINN dataflow architecture. + +This module provides the HWCustomOp base class for custom operations that can be +implemented using HLS or RTL backends in FPGA dataflow architectures. +""" + try: import finn_xsi.adapter as finnxsi except ModuleNotFoundError: finnxsi = None import numpy as np +import numpy.typing as npt import os from abc import abstractmethod +from collections.abc import Sequence +from finn_xsi.sim_engine import SimEngine from onnx import NodeProto +from pathlib import Path from qonnx.core.datatype import BaseDataType from qonnx.custom_op.base import CustomOp from qonnx.util.basic import roundup_to_integer_multiple -from typing import Optional, Sequence, Union +from typing import TYPE_CHECKING, Any, cast from finn.util.basic import get_liveness_threshold_cycles, is_versal from finn.util.exception import FINNInternalError from finn.util.logging import log +if TYPE_CHECKING: + from qonnx.core.modelwrapper import ModelWrapper + class HWCustomOp(CustomOp): """HWCustomOp class all custom ops that can be implemented with either @@ -51,7 +64,13 @@ class HWCustomOp(CustomOp): custom node should have. Some as abstract methods, these have to be filled when writing a new fpgadataflow custom op node.""" - def __init__(self, onnx_node, **kwargs) -> None: + def __init__(self, onnx_node: NodeProto, **kwargs: Any) -> None: + """Initialize HWCustomOp with an ONNX node. + + Args: + onnx_node: The ONNX node to wrap. + **kwargs: Additional keyword arguments passed to parent class. + """ super().__init__(onnx_node, **kwargs) self.code_gen_dict = {} @@ -59,11 +78,15 @@ def get_nodeattr_types( self, ) -> dict[ str, - Union[ - tuple[str, bool, Union[int, float, str, bool, np.ndarray, list]], - tuple[str, bool, Union[int, float, str, bool, np.ndarray, list], Optional[set]], - ], + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], ]: + """Return node attribute types for HWCustomOp. + + Returns: + Dictionary mapping attribute names to their type specifications. + + """ return { "backend": ("s", True, "fpgadataflow"), "preferred_impl_style": ("s", False, "", {"", "hls", "rtl"}), @@ -109,7 +132,15 @@ def get_nodeattr_types( "io_chrc_pads_out": ("ints", False, []), } - def make_shape_compatible_op(self, model) -> NodeProto: + def make_shape_compatible_op(self, model: "ModelWrapper") -> NodeProto: # noqa: ARG002 + """Make a shape compatible operation. + + Args: + model: The model wrapper containing this node. + + Returns: + The ONNX node for the shape compatible operation. + """ oshape = self.get_normal_output_shape() if oshape is None: raise FINNInternalError( @@ -120,14 +151,13 @@ def make_shape_compatible_op(self, model) -> NodeProto: return super().make_const_shape_op(oshape) def get_verilog_top_module_name(self) -> str: - "Return the Verilog top module name for this node." - + """Return the Verilog top module name for this node.""" node = self.onnx_node prefixed_top_name = node.name return prefixed_top_name - def get_verilog_top_module_intf_names(self): + def get_verilog_top_module_intf_names(self) -> dict[str, list[tuple[str, int]] | list[str]]: """Return a dict of names of input and output interfaces. The keys reflect the protocols each interface implements: 'clk', 'rst', 'm_axis', 's_axis', 'aximm', 'axilite'. @@ -146,21 +176,28 @@ def get_verilog_top_module_intf_names(self): # filter out inputs that have no stream width associated with them width = self.get_instream_width_padded(i) if width != 0: - intf_names["s_axis"].append(("in%d_V" % (i), self.get_instream_width_padded(i))) + intf_names["s_axis"].append((f"in{i}_V", self.get_instream_width_padded(i))) intf_names["m_axis"] = [] for i in range(len(node.output)): - intf_names["m_axis"].append(("out%d_V" % (i), self.get_outstream_width_padded(i))) + intf_names["m_axis"].append((f"out{i}_V", self.get_outstream_width_padded(i))) intf_names["aximm"] = [] intf_names["axilite"] = [] intf_names["ap_none"] = [] return intf_names - def get_rtlsim(self): - """Return a xsi wrapper for the emulation library - for this node.""" - + def get_rtlsim(self) -> SimEngine: + """Return a xsi wrapper for the emulation library for this node.""" rtlsim_so = self.get_nodeattr("rtlsim_so") - assert os.path.isfile(rtlsim_so), "Cannot find rtlsim library." + if type(rtlsim_so) is not str: + raise FINNInternalError( + f"rtlsim_so attribute not set correctly in {self.onnx_node.name}, " + "cannot get rtlsim" + ) + if not Path(rtlsim_so).is_file(): + raise FINNInternalError( + f"rtlsim_so attribute points to non-existent file in {self.onnx_node.name}, " + "cannot get rtlsim" + ) sim_base, sim_rel = rtlsim_so.split("xsim.dir") sim_rel = "xsim.dir" + sim_rel @@ -172,14 +209,18 @@ def get_rtlsim(self): return sim - def close_rtlsim(self, sim) -> None: - "Close and free up resources for rtlsim." + def close_rtlsim(self, sim: SimEngine) -> None: + """Close and free up resources for rtlsim. + + Args: + sim: The RTL simulation object to close. + + """ finnxsi.close_rtlsim(sim) - def node_res_estimation(self, fpgapart) -> dict[str, Union[int, float]]: - """Returns summarized resource estimation of BRAMs and LUTs - of the node as a dictionary.""" - ret = dict() + def node_res_estimation(self, fpgapart: str) -> dict[str, int | float]: + """Return summarized resource estimation of BRAMs and LUTs of the node as a dictionary.""" + ret = {} ret["BRAM_18K"] = self.bram_estimation() ret["BRAM_efficiency"] = self.bram_efficiency_estimation() ret["LUT"] = self.lut_estimation() @@ -188,56 +229,83 @@ def node_res_estimation(self, fpgapart) -> dict[str, Union[int, float]]: ret["DSP"] = self.dsp_estimation(fpgapart) return ret - def bram_efficiency_estimation(self): - """Function for BRAM efficiency estimation: actual parameter storage - needed divided by the allocated BRAM storage (from estimation)""" + def bram_efficiency_estimation(self) -> float: + """Estimate BRAM efficiency. + + Returns actual parameter storage needed divided by the allocated BRAM + storage (from estimation). + + """ return 1 - def uram_efficiency_estimation(self): - """Function for URAM efficiency estimation: actual parameter storage - needed divided by the allocated URAM storage (from estimation)""" + def uram_efficiency_estimation(self) -> float: + """Estimate URAM efficiency. + + Returns actual parameter storage needed divided by the allocated URAM + storage (from estimation). + + """ return 1 - def bram_estimation(self): - """Function for BRAM resource estimation, is member function of - HWCustomOp class but has to be filled by every node""" + def bram_estimation(self) -> int: + """Estimate BRAM resource usage. + + Member function of HWCustomOp class that must be implemented by every node. + + """ return 0 - def uram_estimation(self): - """Function for UltraRAM resource estimation, is member function of - HWCustomOp class but has to be filled by every node""" + def uram_estimation(self) -> int: + """Estimate UltraRAM resource usage. + + Member function of HWCustomOp class that must be implemented by every node. + + """ return 0 - def lut_estimation(self): - """Function for LUT resource estimation, is member function of - HWCustomOp class but has to be filled by every node""" + def lut_estimation(self) -> int: + """Estimate LUT resource usage. + + Member function of HWCustomOp class that must be implemented by every node. + + """ return 0 - def dsp_estimation(self, fpgapart): - """Function for DSP resource estimation, is member function of - HWCustomOp class but has to be filled by every node""" + def dsp_estimation(self, fpgapart: str) -> int: # noqa: ARG002 + """Estimate DSP resource usage. + + Member function of HWCustomOp class that must be implemented by every node. + + Args: + fpgapart: Target FPGA part string. + + """ return 0 - def get_exp_cycles(self): - """Function for estimation of expected cycles for set folding, - is member function of HWCustomOp class but has to be filled - by every node""" + def get_exp_cycles(self) -> int: + """Estimate expected cycles for set folding. + + Member function of HWCustomOp class that must be implemented by every node. + + """ return 0 - def get_op_and_param_counts(self): - """Return a dictionary with number of ops needed per inference for - this layer as well as parameter count (weights, thresholds, etc.). - Entries should be in the format: - {op_ : , param_: }.""" + def get_op_and_param_counts(self) -> dict[str, int]: + """Return a dictionary with number of ops needed per inference. + + Returns number of ops needed per inference for this layer as well as + parameter count (weights, thresholds, etc.). Entries should be in the + format: {op_ : , param_: }. + + """ return {} - def reset_rtlsim(self, sim): - """Sets reset input in finnxsi to zero, toggles the clock and set it - back to one""" + def reset_rtlsim(self, sim: SimEngine) -> None: + """Set reset input in finnxsi to zero, toggle the clock and set it back to one.""" finnxsi.reset_rtlsim(sim) - def rtlsim_multi_io(self, sim, io_dict, sname="_V"): - "Run rtlsim for this node, supports multiple i/o streams." + def rtlsim_multi_io(self, sim: SimEngine, io_dict: dict[str, Any], sname: str = "_V") -> None: + """Run rtlsim for this node, supports multiple i/o streams.""" num_out_values = self.get_number_output_values() total_cycle_count = finnxsi.rtlsim_multi_io( sim, @@ -249,78 +317,103 @@ def rtlsim_multi_io(self, sim, io_dict, sname="_V"): self.set_nodeattr("cycles_rtlsim", total_cycle_count) - def verify_node(self): + def verify_node(self) -> None: """Can be implemented to verify that all attributes the node needs are there and that particular attributes are set correctly. Can also check if the number of inputs is equal to the expected number.""" - pass - - def generate_params(self, model, path): - """Function to generate parameters (i.e. weights and thresholds), - is member function of HWCustomOp class but has to be filled - by every node that needs to generate parameters.""" - pass - - def get_number_output_values(self): - """Function to get the number of expected output values, - is member function of HWCustomOp class but has to be filled - by every node.""" + + def generate_params(self, model: Any, path: str) -> None: + """Generate parameters (i.e. weights and thresholds). + + Member function of HWCustomOp class that must be implemented by every node + that needs to generate parameters. + + Args: + model: The model wrapper containing this node. + path: Path where parameters should be generated. + + """ + + def get_number_output_values(self) -> int: + """Get the number of expected output values. + + Member function of HWCustomOp class that must be implemented by every node. + + """ return np.prod(self.get_folded_output_shape()[:-1]) @abstractmethod - def get_input_datatype(self, ind=0) -> BaseDataType: - """Returns FINN DataType of input stream ind.""" + def get_input_datatype(self, ind: int = 0) -> BaseDataType: + """Return FINN DataType of input stream ind.""" @abstractmethod - def get_output_datatype(self, ind=0) -> BaseDataType: - """Returns FINN DataType of output stream ind.""" + def get_output_datatype(self, ind: int = 0) -> BaseDataType: + """Return FINN DataType of output stream ind.""" @abstractmethod - def get_normal_input_shape(self, ind=0) -> Sequence[int] | None: - """Returns normal input shape if implemented.""" + def get_normal_input_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_] | None: + """Return normal input shape if implemented.""" @abstractmethod - def get_normal_output_shape(self, ind=0) -> Sequence[int] | None: - """Returns folded output shape if implemented.""" + def get_normal_output_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_] | None: + """Return folded output shape if implemented.""" @abstractmethod - def get_folded_input_shape(self, ind=0) -> Sequence[int] | None: - """Returns folded input shape (according to synapse folding), if implemented.""" + def get_folded_input_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_] | None: + """Return folded input shape (according to synapse folding), if implemented.""" @abstractmethod - def get_folded_output_shape(self, ind=0) -> Sequence[int] | None: - """Returns folded output shape (according to neuron folding), if implemented.""" + def get_folded_output_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_] | None: + """Return folded output shape (according to neuron folding), if implemented.""" @abstractmethod - def get_instream_width(self, ind=0) -> int: - """Returns input stream width, if implemented.""" + def get_instream_width(self, ind: int = 0) -> int: + """Return input stream width, if implemented.""" @abstractmethod - def get_outstream_width(self, ind=0) -> int: - """Returns output stream width, if implemented.""" + def get_outstream_width(self, ind: int = 0) -> int: + """Return output stream width, if implemented.""" + + def get_instream_width_padded(self, ind: int = 0) -> int: + """Return input stream width padded to a multiple of 8. + + This is required by the AXI Stream spec. + + Args: + ind: Input index (default: 0). - def get_instream_width_padded(self, ind=0) -> int: - """Returns input stream width padded to a multiple of 8. This is required - by the AXI Stream spec.""" + """ in_width = self.get_instream_width(ind=ind) if in_width != 0: return roundup_to_integer_multiple(in_width, 8) - else: - return 0 + return 0 + + def get_outstream_width_padded(self, ind: int = 0) -> int: + """Return output stream width padded to a multiple of 8. - def get_outstream_width_padded(self, ind=0) -> int: - """Returns output stream width padded to a multiple of 8. This is required - by the AXI Stream spec.""" + This is required by the AXI Stream spec. + + Args: + ind: Output index (default: 0). + + """ out_width = self.get_outstream_width(ind=ind) return roundup_to_integer_multiple(out_width, 8) - def generate_hdl_memstream(self, fpgapart, pumped_memory=0): - """Helper function to generate verilog code for memstream component. - Currently utilized by MVAU, VVAU and HLS Thresholding layer.""" + def generate_hdl_memstream(self, fpgapart: str, pumped_memory: int = 0) -> None: + """Generate verilog code for memstream component. + + Currently utilized by MVAU, VVAU and HLS Thresholding layer. + + Args: + fpgapart: Target FPGA part string. + pumped_memory: Whether to use pumped memory (default: 0). + + """ ops = ["MVAU_hls", "MVAU_rtl", "VVAU_hls", "VVAU_rtl", "Thresholding_hls"] if self.onnx_node.op_type in ops or self.onnx_node.op_type.startswith("Elementwise"): - template_path = os.path.join( - os.environ["FINN_RTLLIB"] + "/memstream/hdl/memstream_wrapper_template.v" + template_path = ( + Path(os.environ["FINN_RTLLIB"]) / "memstream/hdl/memstream_wrapper_template.v" ) mname = self.onnx_node.name if self.onnx_node.op_type.startswith("Thresholding"): @@ -328,10 +421,10 @@ def generate_hdl_memstream(self, fpgapart, pumped_memory=0): else: depth = self.calc_wmem() padded_width = self.get_instream_width_padded(1) - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) - ram_style = self.get_nodeattr("ram_style") - init_file = code_gen_dir + "/memblock.dat" + ram_style = cast("str", self.get_nodeattr("ram_style")) + init_file = str(Path(code_gen_dir) / "memblock.dat") if ram_style == "ultra" and not is_versal(fpgapart): init_file = "" code_gen_dict = { @@ -343,29 +436,34 @@ def generate_hdl_memstream(self, fpgapart, pumped_memory=0): "$PUMPED_MEMORY$": [str(pumped_memory)], } # apply code generation to template - with open(template_path, "r") as f: + with template_path.open() as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' code_gen_line = "\n".join(code_gen_dict[key]) template_wrapper = template_wrapper.replace(key, code_gen_line) - with open( - os.path.join(code_gen_dir, mname + "_memstream_wrapper.v"), - "w", - ) as f: + output_path = Path(code_gen_dir) / f"{mname}_memstream_wrapper.v" + with output_path.open("w") as f: f.write(template_wrapper) - else: - pass - def generate_hdl_dynload(self): - template_path = os.environ["FINN_RTLLIB"] + "/dynload/hdl/dynamic_load_wrapper_template.v" + def generate_hdl_dynload(self) -> None: + """Generate HDL for dynamic load wrapper.""" + template_path = ( + Path(os.environ["FINN_RTLLIB"]) / "dynload/hdl/dynamic_load_wrapper_template.v" + ) mname = self.onnx_node.name pe = self.get_nodeattr("PE") simd = self.get_nodeattr("SIMD") mh = self.get_nodeattr("MH") mw = self.get_nodeattr("MW") - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) + num_vectors = self.get_nodeattr("numInputVectors") + n_reps = ( + str(num_vectors[-1]) # type: ignore[index] + if isinstance(num_vectors, (list, np.ndarray)) + else str(num_vectors) + ) code_gen_dict = { "$MODULE_NAME$": [mname], "$PE$": [str(pe)], @@ -373,23 +471,32 @@ def generate_hdl_dynload(self): "$MH$": [str(mh)], "$MW$": [str(mw)], "$WEIGHT_WIDTH$": [str(self.get_input_datatype(1).bitwidth())], - "$N_REPS$": [str(self.get_nodeattr("numInputVectors")[-1])], + "$N_REPS$": [n_reps], } # apply code generation to template - with open(template_path, "r") as f: + with template_path.open() as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' code_gen_line = "\n".join(code_gen_dict[key]) template_wrapper = template_wrapper.replace(key, code_gen_line) - with open( - os.path.join(code_gen_dir, mname + "_dynamic_load_wrapper.v"), - "w", - ) as f: + output_path = Path(code_gen_dir) / f"{mname}_dynamic_load_wrapper.v" + with output_path.open("w") as f: f.write(template_wrapper) - def derive_characteristic_fxns(self, period, override_rtlsim_dict=None): - """Return the unconstrained characteristic functions for this node.""" + def derive_characteristic_fxns( + self, period: int, override_rtlsim_dict: dict | None = None + ) -> None: + """Return the unconstrained characteristic functions for this node. + + Args: + period: The characterization period. + override_rtlsim_dict: Optional dictionary to override rtlsim settings. + + Raises: + ValueError: If period is too short to characterize the node. + + """ # ensure rtlsim is ready assert self.get_nodeattr("rtlsim_so") != "", "rtlsim not ready for " + self.onnx_node.name if self.get_nodeattr("io_chrc_period") > 0: @@ -401,20 +508,18 @@ def derive_characteristic_fxns(self, period, override_rtlsim_dict=None): if exp_cycles == 0: # try to come up with an optimistic estimate exp_cycles = min(n_inps, n_outs) - assert ( - exp_cycles <= period - ), "Period %d too short to characterize %s : expects min %d cycles" % ( - period, - self.onnx_node.name, - exp_cycles, - ) + if exp_cycles < period: + raise ValueError( + f"Period {period} too short to characterize {self.onnx_node.name} : " + f"expects min {n_inps} cycles" + ) sim = self.get_rtlsim() if override_rtlsim_dict is not None: io_dict = override_rtlsim_dict else: io_dict = { "inputs": { - "in0": [i for i in range(n_inps)], + "in0": list(range(n_inps)), }, "outputs": {"out0": []}, } @@ -435,10 +540,8 @@ def derive_characteristic_fxns(self, period, override_rtlsim_dict=None): total_cycle_count = self.get_nodeattr("cycles_rtlsim") assert ( total_cycle_count <= period - ), """Total cycle count from rtl simulation is higher than - specified period, please set the period higher than {}""".format( - total_cycle_count - ) + ), f"""Total cycle count from rtl simulation is higher than + specified period, please set the period higher than {total_cycle_count}""" self.set_nodeattr("io_chrc_period", period) # call str() on stream tracers to get their outputs, and convert # to list of ints @@ -447,7 +550,7 @@ def derive_characteristic_fxns(self, period, override_rtlsim_dict=None): for k in txns_out.keys(): txns_out[k] = [int(c) for c in str(txns_out[k])] - def accumulate_char_fxn(chrc): + def accumulate_char_fxn(chrc: list) -> npt.NDArray[np.int32]: p = len(chrc) ret = [] for t in range(2 * p): diff --git a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py index 4c715e55ea..3ed7752a7f 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py @@ -1,8 +1,19 @@ +"""RTL implementation for RemoveDataPath custom operation. + +This module provides the RTL backend implementation for the RemoveDataPath +custom operation, which removes data from the datapath while maintaining +the control flow. +""" + +import numpy as np import os +from collections.abc import Sequence from numpy import ndarray +from numpy import typing as npt +from onnx import NodeProto from pathlib import Path from qonnx.core.datatype import BaseDataType, DataType -from typing import Sequence, cast +from typing import Any, cast from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.util.exception import FINNInternalError @@ -12,10 +23,23 @@ class RemoveDataPath_rtl(RTLBackend): """RTL implementation for RemoveDataPath custom op.""" - def __init__(self, onnx_node, **kwargs) -> None: + def __init__(self, onnx_node: NodeProto, **kwargs: Any) -> None: + """Initialize RemoveDataPath RTL backend. + + Args: + onnx_node: The ONNX node proto for this operation. + **kwargs: Additional keyword arguments passed to parent class. + + """ super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self) -> dict: + """Return node attribute types for this custom operation. + + Returns: + Dictionary mapping attribute names to their type specifications. + + """ my_attrs = super().get_nodeattr_types() my_attrs.update( { @@ -29,7 +53,13 @@ def get_nodeattr_types(self) -> dict: ) return my_attrs - def infer_node_datatype(self, model) -> None: + def infer_node_datatype(self, model: Any) -> None: + """Infer and set the output datatype based on input datatype. + + Args: + model: The model wrapper containing this node. + + """ node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -40,11 +70,20 @@ def infer_node_datatype(self, model) -> None: # data type stays the same model.set_tensor_datatype(node.output[0], idt) - def get_rtl_file_list(self, abspath=False) -> list[Path]: - if abspath: - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - else: - code_gen_dir = "" + def get_rtl_file_list(self, abspath: bool = False) -> list[Path]: + """Return list of RTL files required for this custom operation. + + Args: + abspath: Whether to return absolute paths (default: False). + + Returns: + List of Path objects pointing to required RTL files. + + Raises: + FINNInternalError: If code_gen_dir_ipgen or gen_top_module attributes are invalid. + + """ + code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") if abspath else "" top_name = self.get_nodeattr("gen_top_module") if type(code_gen_dir) is not str: @@ -65,8 +104,18 @@ def get_rtl_file_list(self, abspath=False) -> list[Path]: ] return verilog_files - def generate_hdl(self, model, fpgapart, clk) -> None: - """Generates the RTL code for this custom op.""" + def generate_hdl(self, model: Any, fpgapart: str, clk: str) -> None: # noqa: ARG002 + """Generate the RTL code for this custom op. + + Args: + model: The model wrapper containing this node (unused). + fpgapart: Target FPGA part string (unused). + clk: Clock period in nanoseconds (unused). + + Raises: + FINNInternalError: If code_gen_dir_ipgen attribute is invalid. + + """ rtlsrc = Path(os.environ["FINN_RTLLIB"]) / "removedatapath" / "hdl" template_path = rtlsrc / "dummy_template.v" @@ -86,14 +135,14 @@ def generate_hdl(self, model, fpgapart, clk) -> None: raise FINNInternalError( f"code_gen_dir_ipgen attribute not set in {topname}, cannot generate RTL code" ) - with open(template_path, "r") as f: + with Path.open(template_path) as f: template = f.read() for placeholder, value in code_gen_dict.items(): template = template.replace(placeholder, value) output_path = Path(code_gen_dir) / f"{self.get_verilog_top_module_name()}.v" - with open(output_path, "w") as f: + with Path.open(output_path, "w") as f: f.write(template) # set ipgen_path and ip_path so that HLS-Synth transformation @@ -115,7 +164,21 @@ def code_generation_ipi(self) -> list[str]: ] return cmd - def get_normal_input_shape(self, ind=0) -> Sequence[int]: + def get_normal_input_shape( + self, ind: int = 0 + ) -> Sequence[int] | npt.NDArray[np.int_] | None: # noqa: ARG002 + """Return the normal (unfolded) input shape. + + Args: + ind: Input index (unused, kept for interface compatibility). + + Returns: + The normal input shape dimensions. + + Raises: + FINNInternalError: If normal_shape attribute is invalid or empty. + + """ normal_shape = self.get_nodeattr("normal_shape") if ( type(normal_shape) is not list @@ -136,12 +199,34 @@ def get_normal_input_shape(self, ind=0) -> Sequence[int]: f"normal_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get normal input shape" ) - return cast(Sequence[int], normal_shape) + return normal_shape + + def get_normal_output_shape( + self, ind: int = 0 + ) -> Sequence[int] | npt.NDArray[np.int_] | None: # noqa: ARG002 + """Return the normal (unfolded) output shape. + + Args: + ind: Output index (unused, kept for interface compatibility). + + Returns: + Tuple containing the normal output shape dimensions. - def get_normal_output_shape(self, ind=0) -> Sequence[int]: + """ return self.get_normal_input_shape() - def get_folded_input_shape(self, ind=0) -> Sequence[int]: + def get_folded_input_shape( + self, ind: int = 0 + ) -> Sequence[int] | npt.NDArray[np.int_] | None: # noqa: ARG002 + """Return the folded input shape. + + Args: + ind: Input index (unused, kept for interface compatibility). + + Returns: + Tuple containing the folded input shape dimensions. + + """ folded_shape = self.get_nodeattr("folded_shape") if ( type(folded_shape) is not list @@ -162,12 +247,32 @@ def get_folded_input_shape(self, ind=0) -> Sequence[int]: f"folded_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get folded input shape" ) - return cast(Sequence[int], folded_shape) + return cast("Sequence[int]", folded_shape) + + def get_folded_output_shape( + self, ind: int = 0 + ) -> Sequence[int] | npt.NDArray[np.int_] | None: # noqa: ARG002 + """Return the folded output shape. + + Args: + ind: Output index (unused, kept for interface compatibility). + + Returns: + Tuple containing the folded output shape dimensions. - def get_folded_output_shape(self, ind=0) -> Sequence[int]: + """ return self.get_folded_input_shape() - def get_instream_width(self, ind=0) -> int: + def get_instream_width(self, ind: int = 0) -> int: # noqa: ARG002 + """Return the input stream width in bits. + + Args: + ind: Input index (unused, kept for interface compatibility). + + Returns: + Input stream width in bits. + + """ dtype = self.get_nodeattr("dataType") if type(dtype) is not str: raise FINNInternalError( @@ -188,7 +293,19 @@ def get_instream_width(self, ind=0) -> int: in_width = folded_shape[-1] * dtype.bitwidth() return in_width - def get_outstream_width(self, ind=0) -> int: + def get_outstream_width(self, ind: int = 0) -> int: # noqa: ARG002 + """Return the output stream width in bits. + + Args: + ind: Output index (unused, kept for interface compatibility). + + Returns: + Output stream width in bits. + + Raises: + FINNInternalError: If dataType or folded_shape attributes are invalid. + + """ dtype = self.get_nodeattr("dataType") if type(dtype) is not str: raise FINNInternalError( @@ -209,7 +326,19 @@ def get_outstream_width(self, ind=0) -> int: in_width = folded_shape[-1] * dtype.bitwidth() return in_width - def get_input_datatype(self, ind=0) -> BaseDataType: + def get_input_datatype(self, ind: int = 0) -> BaseDataType: # noqa: ARG002 + """Return the input data type. + + Args: + ind: Input index (unused, kept for interface compatibility). + + Returns: + The QONNX data type for the input. + + Raises: + FINNInternalError: If dataType attribute is invalid. + + """ dtype = self.get_nodeattr("dataType") if type(dtype) is not str: raise FINNInternalError( @@ -219,7 +348,19 @@ def get_input_datatype(self, ind=0) -> BaseDataType: dtype = DataType[dtype] return dtype - def get_output_datatype(self, ind=0) -> BaseDataType: + def get_output_datatype(self, ind: int = 0) -> BaseDataType: # noqa: ARG002 + """Return the output data type. + + Args: + ind: Output index (unused, kept for interface compatibility). + + Returns: + The QONNX data type for the output. + + Raises: + FINNInternalError: If dataType attribute is invalid. + + """ dtype = self.get_nodeattr("dataType") if type(dtype) is not str: raise FINNInternalError( diff --git a/src/finn/custom_op/fpgadataflow/rtlbackend.py b/src/finn/custom_op/fpgadataflow/rtlbackend.py index c2c000b3e0..0d9dffd72b 100644 --- a/src/finn/custom_op/fpgadataflow/rtlbackend.py +++ b/src/finn/custom_op/fpgadataflow/rtlbackend.py @@ -32,10 +32,11 @@ finnxsi = None import numpy as np +import numpy.typing as npt import os from abc import ABC, abstractmethod from pathlib import Path -from typing import List +from typing import cast from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.basic import make_build_dir @@ -49,7 +50,21 @@ class RTLBackend(HWCustomOp, ABC): custom node should have. Some as abstract methods, these have to be filled when writing a new RTL custom op node.""" - def get_nodeattr_types(self): + def get_nodeattr_types( + self, + ) -> dict[ + str, + tuple[str, bool, int | float | str | bool | npt.ArrayLike | list] + | tuple[str, bool, int | float | str | bool | npt.ArrayLike | list, set | None], + ]: + """Return 4-tuple (dtype, required, default_val, allowed_values) for attribute + with name. allowed_values will be None if not specified. + + Returns: + dict[ str, tuple[str, bool, int | float | str | bool | npt.ArrayLike | list] | tuple[ + str, bool, int | float | str | bool | npt.ArrayLike | list, set | None]]: + Dictionary of node attribute types + """ super_attrs = super().get_nodeattr_types() super_attrs.update( { @@ -77,25 +92,25 @@ def prepare_rtlsim(self): # save generated lib filename in attribute self.set_nodeattr("rtlsim_so", ret[0] + "/" + ret[1]) - def get_verilog_paths(self): + def get_verilog_paths(self) -> list[str]: """Returns path to code gen directory. Can be overwritten to return additional paths to relevant verilog files""" code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - return [code_gen_dir] + return [cast("str", code_gen_dir)] @abstractmethod - def get_rtl_file_list(self, abspath=False) -> List[str] | List[Path]: + def get_rtl_file_list(self, abspath=False) -> list[str] | list[Path]: """Returns list of rtl files. Needs to be filled by each node.""" pass @abstractmethod - def code_generation_ipi(self) -> List[str]: + def code_generation_ipi(self) -> list[str]: pass - def code_generation_ipgen(self, model, fpgapart, clk): + def code_generation_ipgen(self, model, fpgapart, clk) -> None: self.generate_hdl(model, fpgapart, clk) - def execute_node(self, context, graph): + def execute_node(self, context, graph) -> None: mode = self.get_nodeattr("exec_mode") code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") From 136264ce951e1337e9496884daeb7263737e636e Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:11:30 +0100 Subject: [PATCH 022/170] Fix noqa --- .../fpgadataflow/rtl/removedatapath_rtl.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py index 3ed7752a7f..14b5cb1dc1 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py @@ -165,8 +165,8 @@ def code_generation_ipi(self) -> list[str]: return cmd def get_normal_input_shape( - self, ind: int = 0 - ) -> Sequence[int] | npt.NDArray[np.int_] | None: # noqa: ARG002 + self, ind: int = 0 # noqa: ARG002 + ) -> Sequence[int] | npt.NDArray[np.int_] | None: """Return the normal (unfolded) input shape. Args: @@ -202,8 +202,8 @@ def get_normal_input_shape( return normal_shape def get_normal_output_shape( - self, ind: int = 0 - ) -> Sequence[int] | npt.NDArray[np.int_] | None: # noqa: ARG002 + self, ind: int = 0 # noqa: ARG002 + ) -> Sequence[int] | npt.NDArray[np.int_] | None: """Return the normal (unfolded) output shape. Args: @@ -216,8 +216,8 @@ def get_normal_output_shape( return self.get_normal_input_shape() def get_folded_input_shape( - self, ind: int = 0 - ) -> Sequence[int] | npt.NDArray[np.int_] | None: # noqa: ARG002 + self, ind: int = 0 # noqa: ARG002 + ) -> Sequence[int] | npt.NDArray[np.int_] | None: """Return the folded input shape. Args: @@ -250,8 +250,8 @@ def get_folded_input_shape( return cast("Sequence[int]", folded_shape) def get_folded_output_shape( - self, ind: int = 0 - ) -> Sequence[int] | npt.NDArray[np.int_] | None: # noqa: ARG002 + self, ind: int = 0 # noqa: ARG002 + ) -> Sequence[int] | npt.NDArray[np.int_] | None: """Return the folded output shape. Args: From b6e858e3d538001be49f47c795802df2e5ac6871 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:53:38 +0100 Subject: [PATCH 023/170] Fix type hinting and other problems --- .../custom_op/fpgadataflow/channelwise_op.py | 3 +- src/finn/custom_op/fpgadataflow/hwcustomop.py | 92 ++++------- .../fpgadataflow/matrixvectoractivation.py | 3 +- src/finn/custom_op/fpgadataflow/memstream.py | 67 ++++++++ .../fpgadataflow/rtl/removedatapath_rtl.py | 8 +- src/finn/custom_op/fpgadataflow/rtlbackend.py | 151 +++++++++++++----- .../custom_op/fpgadataflow/thresholding.py | 3 +- .../fpgadataflow/vectorvectoractivation.py | 3 +- src/finn/util/deprecated.py | 27 ++++ 9 files changed, 249 insertions(+), 108 deletions(-) create mode 100644 src/finn/custom_op/fpgadataflow/memstream.py create mode 100644 src/finn/util/deprecated.py diff --git a/src/finn/custom_op/fpgadataflow/channelwise_op.py b/src/finn/custom_op/fpgadataflow/channelwise_op.py index ac49a0ae75..ecb555ce3e 100644 --- a/src/finn/custom_op/fpgadataflow/channelwise_op.py +++ b/src/finn/custom_op/fpgadataflow/channelwise_op.py @@ -33,6 +33,7 @@ from qonnx.util.basic import qonnx_make_model from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.custom_op.fpgadataflow.memstream import MemStreamSupport from finn.util.logging import log # ONNX i/o tensor shape assumptions for channelwise ops: @@ -73,7 +74,7 @@ def get_smallest_possible(vals): return DataType["INT64"] -class ChannelwiseOp(HWCustomOp): +class ChannelwiseOp(MemStreamSupport, HWCustomOp): """Abstraction layer for HW implementation of ChannelwiseOp.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/custom_op/fpgadataflow/hwcustomop.py b/src/finn/custom_op/fpgadataflow/hwcustomop.py index 69f3f2ed4c..b7d57a3150 100644 --- a/src/finn/custom_op/fpgadataflow/hwcustomop.py +++ b/src/finn/custom_op/fpgadataflow/hwcustomop.py @@ -32,11 +32,6 @@ implemented using HLS or RTL backends in FPGA dataflow architectures. """ -try: - import finn_xsi.adapter as finnxsi -except ModuleNotFoundError: - finnxsi = None - import numpy as np import numpy.typing as npt import os @@ -50,7 +45,8 @@ from qonnx.util.basic import roundup_to_integer_multiple from typing import TYPE_CHECKING, Any, cast -from finn.util.basic import get_liveness_threshold_cycles, is_versal +from finn.util.basic import get_liveness_threshold_cycles +from finn.util.deprecated import deprecated from finn.util.exception import FINNInternalError from finn.util.logging import log @@ -187,6 +183,10 @@ def get_verilog_top_module_intf_names(self) -> dict[str, list[tuple[str, int]] | def get_rtlsim(self) -> SimEngine: """Return a xsi wrapper for the emulation library for this node.""" + import finn_xsi.adapter as finnxsi + + # without finnxsi dependency + rtlsim_so = self.get_nodeattr("rtlsim_so") if type(rtlsim_so) is not str: raise FINNInternalError( @@ -216,6 +216,9 @@ def close_rtlsim(self, sim: SimEngine) -> None: sim: The RTL simulation object to close. """ + import finn_xsi.adapter as finnxsi + + # without finnxsi dependency finnxsi.close_rtlsim(sim) def node_res_estimation(self, fpgapart: str) -> dict[str, int | float]: @@ -302,10 +305,16 @@ def get_op_and_param_counts(self) -> dict[str, int]: def reset_rtlsim(self, sim: SimEngine) -> None: """Set reset input in finnxsi to zero, toggle the clock and set it back to one.""" + import finn_xsi.adapter as finnxsi + + # without finnxsi dependency finnxsi.reset_rtlsim(sim) def rtlsim_multi_io(self, sim: SimEngine, io_dict: dict[str, Any], sname: str = "_V") -> None: """Run rtlsim for this node, supports multiple i/o streams.""" + import finn_xsi.adapter as finnxsi + + # without finnxsi dependency num_out_values = self.get_number_output_values() total_cycle_count = finnxsi.rtlsim_multi_io( sim, @@ -340,7 +349,13 @@ def get_number_output_values(self) -> int: Member function of HWCustomOp class that must be implemented by every node. """ - return np.prod(self.get_folded_output_shape()[:-1]) + folded_oshape = self.get_folded_output_shape() + if folded_oshape is None: + raise FINNInternalError( + f"Cannot get number of output values for {self.onnx_node.name} " + "since folded output shape is not defined." + ) + return int(np.prod(folded_oshape[:-1])) @abstractmethod def get_input_datatype(self, ind: int = 0) -> BaseDataType: @@ -351,19 +366,19 @@ def get_output_datatype(self, ind: int = 0) -> BaseDataType: """Return FINN DataType of output stream ind.""" @abstractmethod - def get_normal_input_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_] | None: + def get_normal_input_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_]: """Return normal input shape if implemented.""" @abstractmethod - def get_normal_output_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_] | None: + def get_normal_output_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_]: """Return folded output shape if implemented.""" @abstractmethod - def get_folded_input_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_] | None: + def get_folded_input_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_]: """Return folded input shape (according to synapse folding), if implemented.""" @abstractmethod - def get_folded_output_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_] | None: + def get_folded_output_shape(self, ind: int = 0) -> Sequence[int] | npt.NDArray[np.int_]: """Return folded output shape (according to neuron folding), if implemented.""" @abstractmethod @@ -400,52 +415,6 @@ def get_outstream_width_padded(self, ind: int = 0) -> int: out_width = self.get_outstream_width(ind=ind) return roundup_to_integer_multiple(out_width, 8) - def generate_hdl_memstream(self, fpgapart: str, pumped_memory: int = 0) -> None: - """Generate verilog code for memstream component. - - Currently utilized by MVAU, VVAU and HLS Thresholding layer. - - Args: - fpgapart: Target FPGA part string. - pumped_memory: Whether to use pumped memory (default: 0). - - """ - ops = ["MVAU_hls", "MVAU_rtl", "VVAU_hls", "VVAU_rtl", "Thresholding_hls"] - if self.onnx_node.op_type in ops or self.onnx_node.op_type.startswith("Elementwise"): - template_path = ( - Path(os.environ["FINN_RTLLIB"]) / "memstream/hdl/memstream_wrapper_template.v" - ) - mname = self.onnx_node.name - if self.onnx_node.op_type.startswith("Thresholding"): - depth = self.calc_tmem() - else: - depth = self.calc_wmem() - padded_width = self.get_instream_width_padded(1) - code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) - - ram_style = cast("str", self.get_nodeattr("ram_style")) - init_file = str(Path(code_gen_dir) / "memblock.dat") - if ram_style == "ultra" and not is_versal(fpgapart): - init_file = "" - code_gen_dict = { - "$MODULE_NAME$": [mname], - "$DEPTH$": [str(depth)], - "$WIDTH$": [str(padded_width)], - "$INIT_FILE$": [init_file], - "$RAM_STYLE$": [ram_style], - "$PUMPED_MEMORY$": [str(pumped_memory)], - } - # apply code generation to template - with template_path.open() as f: - template_wrapper = f.read() - for key in code_gen_dict: - # transform list into long string separated by '\n' - code_gen_line = "\n".join(code_gen_dict[key]) - template_wrapper = template_wrapper.replace(key, code_gen_line) - output_path = Path(code_gen_dir) / f"{mname}_memstream_wrapper.v" - with output_path.open("w") as f: - f.write(template_wrapper) - def generate_hdl_dynload(self) -> None: """Generate HDL for dynamic load wrapper.""" template_path = ( @@ -484,6 +453,7 @@ def generate_hdl_dynload(self) -> None: with output_path.open("w") as f: f.write(template_wrapper) + @deprecated def derive_characteristic_fxns( self, period: int, override_rtlsim_dict: dict | None = None ) -> None: @@ -499,7 +469,7 @@ def derive_characteristic_fxns( """ # ensure rtlsim is ready assert self.get_nodeattr("rtlsim_so") != "", "rtlsim not ready for " + self.onnx_node.name - if self.get_nodeattr("io_chrc_period") > 0: + if cast("int | float", self.get_nodeattr("io_chrc_period")) > 0: log.warning(f"Skipping node {self.onnx_node.name}: already has FIFO characteristic") return exp_cycles = self.get_exp_cycles() @@ -533,11 +503,11 @@ def derive_characteristic_fxns( self.reset_rtlsim(sim) # create stream tracers for all input and output streams for k in txns_in.keys(): - txns_in[k] = sim.trace_stream(k + sname) + txns_in[k] = sim.trace_stream(k + sname) # type: ignore for k in txns_out.keys(): - txns_out[k] = sim.trace_stream(k + sname) + txns_out[k] = sim.trace_stream(k + sname) # type: ignore self.rtlsim_multi_io(sim, io_dict) - total_cycle_count = self.get_nodeattr("cycles_rtlsim") + total_cycle_count = cast("int", self.get_nodeattr("cycles_rtlsim")) assert ( total_cycle_count <= period ), f"""Total cycle count from rtl simulation is higher than diff --git a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py index f274cecac5..bfa7ed5c61 100644 --- a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py @@ -40,6 +40,7 @@ ) from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.custom_op.fpgadataflow.memstream import MemStreamSupport from finn.util.data_packing import numpy_to_hls_code, pack_innermost_dim_as_hex_string from finn.util.logging import log @@ -51,7 +52,7 @@ # the ... here can be any shape (representing groups of vectors) -class MVAU(HWCustomOp): +class MVAU(MemStreamSupport, HWCustomOp): """Abstraction layer for HW implementation of MatrixVectorActivation layers.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/custom_op/fpgadataflow/memstream.py b/src/finn/custom_op/fpgadataflow/memstream.py new file mode 100644 index 0000000000..48aabf509c --- /dev/null +++ b/src/finn/custom_op/fpgadataflow/memstream.py @@ -0,0 +1,67 @@ +"""Support for memory stream operations in FPGA dataflow.""" + +import os +from abc import ABC, abstractmethod +from pathlib import Path +from typing import cast + +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.util.basic import is_versal + + +class MemStreamSupport(HWCustomOp, ABC): + """Custom Op for memory stream operations in FPGA dataflow.""" + + @abstractmethod + def calc_tmem(self) -> int: + """Abstract method to calculate threshold memory size.""" + + @abstractmethod + def calc_wmem(self) -> int: + """Abstract method to calculate weight memory size.""" + + def generate_hdl_memstream(self, fpgapart: str, pumped_memory: int = 0) -> None: + """Generate verilog code for memstream component. + + Currently utilized by MVAU, VVAU and HLS Thresholding layer. + + Args: + fpgapart: Target FPGA part string. + pumped_memory: Whether to use pumped memory (default: 0). + + """ + ops = ["MVAU_hls", "MVAU_rtl", "VVAU_hls", "VVAU_rtl", "Thresholding_hls"] + if self.onnx_node.op_type in ops or self.onnx_node.op_type.startswith("Elementwise"): + template_path = ( + Path(os.environ["FINN_RTLLIB"]) / "memstream/hdl/memstream_wrapper_template.v" + ) + mname = self.onnx_node.name + if self.onnx_node.op_type.startswith("Thresholding"): + depth = self.calc_tmem() + else: + depth = self.calc_wmem() + padded_width = self.get_instream_width_padded(1) + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) + + ram_style = cast("str", self.get_nodeattr("ram_style")) + init_file = str(Path(code_gen_dir) / "memblock.dat") + if ram_style == "ultra" and not is_versal(fpgapart): + init_file = "" + code_gen_dict = { + "$MODULE_NAME$": [mname], + "$DEPTH$": [str(depth)], + "$WIDTH$": [str(padded_width)], + "$INIT_FILE$": [init_file], + "$RAM_STYLE$": [ram_style], + "$PUMPED_MEMORY$": [str(pumped_memory)], + } + # apply code generation to template + with template_path.open() as f: + template_wrapper = f.read() + for key in code_gen_dict: + # transform list into long string separated by '\n' + code_gen_line = "\n".join(code_gen_dict[key]) + template_wrapper = template_wrapper.replace(key, code_gen_line) + output_path = Path(code_gen_dir) / f"{mname}_memstream_wrapper.v" + with output_path.open("w") as f: + f.write(template_wrapper) diff --git a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py index 14b5cb1dc1..aa7a7c0004 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py @@ -166,7 +166,7 @@ def code_generation_ipi(self) -> list[str]: def get_normal_input_shape( self, ind: int = 0 # noqa: ARG002 - ) -> Sequence[int] | npt.NDArray[np.int_] | None: + ) -> Sequence[int] | npt.NDArray[np.int_]: """Return the normal (unfolded) input shape. Args: @@ -203,7 +203,7 @@ def get_normal_input_shape( def get_normal_output_shape( self, ind: int = 0 # noqa: ARG002 - ) -> Sequence[int] | npt.NDArray[np.int_] | None: + ) -> Sequence[int] | npt.NDArray[np.int_]: """Return the normal (unfolded) output shape. Args: @@ -217,7 +217,7 @@ def get_normal_output_shape( def get_folded_input_shape( self, ind: int = 0 # noqa: ARG002 - ) -> Sequence[int] | npt.NDArray[np.int_] | None: + ) -> Sequence[int] | npt.NDArray[np.int_]: """Return the folded input shape. Args: @@ -251,7 +251,7 @@ def get_folded_input_shape( def get_folded_output_shape( self, ind: int = 0 # noqa: ARG002 - ) -> Sequence[int] | npt.NDArray[np.int_] | None: + ) -> Sequence[int] | npt.NDArray[np.int_]: """Return the folded output shape. Args: diff --git a/src/finn/custom_op/fpgadataflow/rtlbackend.py b/src/finn/custom_op/fpgadataflow/rtlbackend.py index 0d9dffd72b..873bacf399 100644 --- a/src/finn/custom_op/fpgadataflow/rtlbackend.py +++ b/src/finn/custom_op/fpgadataflow/rtlbackend.py @@ -26,21 +26,27 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -try: - import finn_xsi.adapter as finnxsi -except ModuleNotFoundError: - finnxsi = None +"""RTL backend support for FINN custom operations. + +This module provides the RTLBackend abstract base class that all RTL-based custom +operations in FINN inherit from. It includes functionality for HDL code generation, +RTL simulation, and integration with Vivado IP Integrator. +""" import numpy as np import numpy.typing as npt -import os from abc import ABC, abstractmethod from pathlib import Path -from typing import cast +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from onnx import GraphProto + from qonnx.core.modelwrapper import ModelWrapper from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.basic import make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy +from finn.util.exception import FINNInternalError from finn.util.logging import log @@ -54,15 +60,15 @@ def get_nodeattr_types( self, ) -> dict[ str, - tuple[str, bool, int | float | str | bool | npt.ArrayLike | list] - | tuple[str, bool, int | float | str | bool | npt.ArrayLike | list, set | None], + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], ]: """Return 4-tuple (dtype, required, default_val, allowed_values) for attribute with name. allowed_values will be None if not specified. Returns: - dict[ str, tuple[str, bool, int | float | str | bool | npt.ArrayLike | list] | tuple[ - str, bool, int | float | str | bool | npt.ArrayLike | list, set | None]]: + dict[ str, tuple[str, bool, int | float | str | bool | npt.NDArray | list] | tuple[ + str, bool, int | float | str | bool | npt.NDArray | list, set | None]]: Dictionary of node attribute types """ super_attrs = super().get_nodeattr_types() @@ -75,12 +81,26 @@ def get_nodeattr_types( return super_attrs @abstractmethod - def generate_hdl(self, model, fpgapart, clk) -> None: - pass + def generate_hdl(self, model: "ModelWrapper", fpgapart: str, clk: str) -> None: + """Generate HDL code for this node. + + Args: + model: The FINN model containing this node + fpgapart: Target FPGA part string + clk: Clock period specification - def prepare_rtlsim(self): - """Creates a xsi emulation library for the RTL code generated - for this node, sets the rtlsim_so attribute to its path.""" + Returns: + None + """ + + def prepare_rtlsim(self) -> None: + """Create a xsi emulation library for the RTL code generated for this node. + Sets the rtlsim_so attribute to the path of the generated library. + + Returns: + None + """ + import finn_xsi.adapter as finnxsi verilog_files = self.get_rtl_file_list(abspath=True) single_src_dir = make_build_dir("rtlsim_" + self.onnx_node.name + "_") @@ -93,33 +113,84 @@ def prepare_rtlsim(self): self.set_nodeattr("rtlsim_so", ret[0] + "/" + ret[1]) def get_verilog_paths(self) -> list[str]: - """Returns path to code gen directory. Can be overwritten to - return additional paths to relevant verilog files""" + """Return path to code gen directory. + Can be overwritten to return additional paths to relevant verilog files. + + Returns: + list[str]: List of paths to directories containing Verilog files + """ code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") return [cast("str", code_gen_dir)] @abstractmethod - def get_rtl_file_list(self, abspath=False) -> list[str] | list[Path]: - """Returns list of rtl files. Needs to be filled by each node.""" - pass + def get_rtl_file_list(self, abspath: bool = False) -> list[str] | list[Path]: + """Return list of RTL files. + Must be implemented by each subclass to provide the list of RTL files used by this node. + + Args: + abspath: If True, return absolute paths; if False, return relative paths + + Returns: + list[str] | list[Path]: List of paths to RTL files + """ @abstractmethod def code_generation_ipi(self) -> list[str]: - pass + """Generate TCL commands for IP Integrator. + Must be implemented by each subclass to provide the TCL commands needed + to integrate this node into Vivado IP Integrator. - def code_generation_ipgen(self, model, fpgapart, clk) -> None: + Returns: + list[str]: List of TCL commands for IP Integrator + """ + + def code_generation_ipgen(self, model: "ModelWrapper", fpgapart: str, clk: str) -> None: + """Generate HDL code for IP generation. + Wrapper method that calls generate_hdl to produce the HDL code for this node. + + Args: + model: The FINN model containing this node + fpgapart: Target FPGA part string + clk: Clock period specification + + Returns: + None + """ self.generate_hdl(model, fpgapart, clk) - def execute_node(self, context, graph) -> None: + def execute_node( + self, context: dict[str, npt.NDArray], graph: "GraphProto" + ) -> None: # noqa: ARG002 + """Execute this node's RTL simulation. + + Args: + context: Dictionary mapping tensor names to their numpy array values + graph: The ONNX graph containing this node + + Returns: + None + + Raises: + Exception: If exec_mode is not set to "rtlsim" + """ mode = self.get_nodeattr("exec_mode") - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) if mode == "rtlsim": node = self.onnx_node inputs = {} for i, inp in enumerate(node.input): - exp_ishape = tuple(self.get_normal_input_shape(i)) + shape = self.get_normal_input_shape(i) + if shape is None: + raise FINNInternalError( + f"Input shape for input {i} of node {node.name} is None." + ) + exp_ishape = tuple(shape) folded_ishape = self.get_folded_input_shape(i) + if folded_ishape is None: + raise FINNInternalError( + f"Folded input shape for input {i} of node {node.name} is None." + ) inp_val = context[inp] # Make sure the input has the right container datatype if inp_val.dtype != np.float32: @@ -136,15 +207,14 @@ def execute_node(self, context, graph) -> None: export_idt = self.get_input_datatype(i) reshaped_input = inp_val.reshape(folded_ishape) - np.save(os.path.join(code_gen_dir, "input_%s.npy" % i), reshaped_input) + input_path = Path(code_gen_dir) / f"input_{i}.npy" + np.save(input_path, reshaped_input) nbits = self.get_instream_width(i) - rtlsim_inp = npy_to_rtlsim_input( - "{}/input_{}.npy".format(code_gen_dir, i), export_idt, nbits - ) - inputs["in%s" % i] = rtlsim_inp + rtlsim_inp = npy_to_rtlsim_input(str(input_path), export_idt, nbits) + inputs[f"in{i}"] = rtlsim_inp outputs = {} - for o, outp in enumerate(node.output): - outputs["out%s" % o] = [] + for o, _ in enumerate(node.output): + outputs[f"out{o}"] = [] # assembled execution context io_dict = {"inputs": inputs, "outputs": outputs} @@ -153,17 +223,22 @@ def execute_node(self, context, graph) -> None: self.rtlsim_multi_io(sim, io_dict) self.close_rtlsim(sim) for o, outp in enumerate(node.output): - rtlsim_output = io_dict["outputs"]["out%s" % o] + rtlsim_output = io_dict["outputs"][f"out{o}"] odt = self.get_output_datatype(o) target_bits = odt.bitwidth() packed_bits = self.get_outstream_width(o) - out_npy_path = "{}/output.npy".format(code_gen_dir) + out_npy_path = f"{code_gen_dir}/output.npy" out_shape = self.get_folded_output_shape(o) rtlsim_output_to_npy( rtlsim_output, out_npy_path, odt, out_shape, packed_bits, target_bits ) # load and reshape output - exp_oshape = tuple(self.get_normal_output_shape(o)) + oshape = self.get_normal_output_shape(o) + if oshape is None: + raise FINNInternalError( + f"Output shape for output {o} of node {node.name} is None." + ) + exp_oshape = tuple(oshape) output = np.load(out_npy_path) output = np.asarray([output], dtype=np.float32).reshape(*exp_oshape) context[outp] = output @@ -174,8 +249,6 @@ def execute_node(self, context, graph) -> None: else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) diff --git a/src/finn/custom_op/fpgadataflow/thresholding.py b/src/finn/custom_op/fpgadataflow/thresholding.py index 80e1ddfda2..14e91ae0bd 100644 --- a/src/finn/custom_op/fpgadataflow/thresholding.py +++ b/src/finn/custom_op/fpgadataflow/thresholding.py @@ -32,10 +32,11 @@ from qonnx.util.basic import interleave_matrix_outer_dim_from_partitions from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.custom_op.fpgadataflow.memstream import MemStreamSupport from finn.util.logging import log -class Thresholding(HWCustomOp): +class Thresholding(MemStreamSupport, HWCustomOp): """Abstraction layer for HW implementation of Thresholding.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py b/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py index c72b8c8925..027b3a514d 100644 --- a/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py @@ -48,11 +48,12 @@ ) from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.custom_op.fpgadataflow.memstream import MemStreamSupport from finn.util.data_packing import numpy_to_hls_code, pack_innermost_dim_as_hex_string from finn.util.logging import log -class VVAU(HWCustomOp): +class VVAU(MemStreamSupport, HWCustomOp): """Abstraction layer for HW implementation of VectorVectorActivation layers.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/util/deprecated.py b/src/finn/util/deprecated.py new file mode 100644 index 0000000000..cd5e6e88f7 --- /dev/null +++ b/src/finn/util/deprecated.py @@ -0,0 +1,27 @@ +"""Implements a decorator to mark functions as deprecated.""" +import functools +import warnings +from collections.abc import Callable +from typing import ParamSpec, TypeVar + +rT = TypeVar("rT") # return type # noqa: N816 +pT = ParamSpec("pT") # parameters type # noqa: N816 + + +def deprecated(func: Callable[pT, rT]) -> Callable[pT, rT]: + """Use this decorator to mark functions as deprecated. + Every time the decorated function runs, it will emit + a "deprecation" warning.""" + + @functools.wraps(func) + def new_func(*args: pT.args, **kwargs: pT.kwargs) -> rT: + warnings.simplefilter("always", DeprecationWarning) # turn off filter + warnings.warn( + f"Call to a deprecated function {func.__name__}.", + category=DeprecationWarning, + stacklevel=2, + ) + warnings.simplefilter("default", DeprecationWarning) # reset filter + return func(*args, **kwargs) + + return new_func From 0a320044204c1ca501082f2b3507d089410e044d Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:53:50 +0100 Subject: [PATCH 024/170] Fix type hinting and other problems --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5b3511a4e9..a341f1da3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,4 +196,6 @@ ignore = [ "ANN401", # Don't use the "Any" type "D413", # Blank lines at docstring end "D205", # Blank line after summary (enables multiline summaries) + "N801", # Class name should use CapWords convention + "D209", # Multi-line docstring closing quotes should be on a separate line ] From d51e39deb7ddc88f2434a185cf34945c2abbec9f Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:18:07 +0100 Subject: [PATCH 025/170] New FIFO implementation & Bug fix --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 1 + finn_xsi/finn_xsi/include/FIFO.h | 26 ++++ finn_xsi/finn_xsi/include/Simulation.hpp | 93 +++++++++----- .../finn_xsi/include/SimulationInterface.hpp | 120 +++++++----------- finn_xsi/finn_xsi/src/FIFO.cpp | 32 +++++ .../custom_op/fpgadataflow/channelwise_op.py | 3 +- .../fpgadataflow/elementwise_binary.py | 3 +- .../transformation/fpgadataflow/simulation.py | 26 ++-- 8 files changed, 179 insertions(+), 125 deletions(-) create mode 100644 finn_xsi/finn_xsi/include/FIFO.h create mode 100644 finn_xsi/finn_xsi/src/FIFO.cpp diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 5635d8993d..f814efb8f5 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -45,6 +45,7 @@ int main(int argc, const char* argv[]) { vm["depth"].as() ); + /** SECTION WIP */ auto start = std::chrono::high_resolution_clock::now(); for (std::size_t j = 0; j < 100000; ++j) { diff --git a/finn_xsi/finn_xsi/include/FIFO.h b/finn_xsi/finn_xsi/include/FIFO.h new file mode 100644 index 0000000000..83a4f5bfa4 --- /dev/null +++ b/finn_xsi/finn_xsi/include/FIFO.h @@ -0,0 +1,26 @@ +#ifndef FIFO_H +#define FIFO_H + +#include +#include + +class FIFO { + std::size_t util = 0; + std::size_t max_util = 0; + std::size_t max_size = 0; + bool sucReady = false; + + public: + FIFO(std::size_t max_size = std::numeric_limits::max()); + ~FIFO(); + + // Add FIFO methods and members as needed + bool is_valid(); + void ready(bool ready); + bool is_ready() const; + void write(bool valid); + std::size_t get_largest_occupation() const; + void reset(); +}; + +#endif /* FIFO_H */ diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index c85021e7e8..1d72d90472 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -3,12 +3,13 @@ #include #include #include +#include #include #include #include #include -#include +#include #include #include #include @@ -24,11 +25,10 @@ #include #include #include +#include #include #include #include - -#include using json = nlohmann::json; @@ -41,19 +41,12 @@ class Simulation { public: xsi::Kernel kernel; xsi::Design top; + // S_AXIS_Control goes into the simulated layer std::array istreams; + // M_AXIS_Control comes from the simulated layer std::array ostreams; Clock clk; - /// Initialize streams to the correct valid and ready states. - void initStreams() { - for (auto&& s : istreams) { - s.valid(); - } - for (auto&& s : ostreams) { - s.ready(); - } - } Simulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, std::array _ostream_descs) @@ -79,7 +72,6 @@ class Simulation { // Find Global Control & Run Startup Sequence clearPorts(); reset(); - initStreams(); } void clearPorts() noexcept { @@ -102,8 +94,15 @@ class Simulation { } }; - - +// Communication Flow: +// +// valid ┌──────────────────────────────────────┐ valid valid +// SHM ─────────> │ valid valid │ ─────────> FIFO ─────> SHM +// (pred) <───────── istream ─────────> xsim ─────────> ostream <───────── <───── (succ) +// ready │ <───────── <───────── │ ready ready +// │ ready ready │ +// │ (sim) │ +// └──────────────────────────────────────┘ template class SingleNodeSimulation : public Simulation { private: @@ -111,12 +110,12 @@ class SingleNodeSimulation : public Simulation; std::array fromProducerInterface; std::array toConsumerInterface; + std::array fifo; std::size_t cyclesRun = 0; public: SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, - std::array _ostream_descs, std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, - unsigned int initialFIFODepth = 2) + std::array _ostream_descs, std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { if (CommunicatesWithPredecessor && !prevNodeName) { throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); @@ -128,18 +127,16 @@ class SingleNodeSimulation : public Simulationostreams[i].ready(toConsumerInterface[i].communicate(this->ostreams[i].is_valid())); + // Interface sim <-> FIFO + this->fifo[i].write(this->ostreams[i].is_valid()); + this->ostreams[i].ready(this->fifo[i].is_ready()); + // Interface FIFO <-> SHM + this->fifo[i].ready(toConsumerInterface[i].writeToNextNode(this->fifo[i].is_valid())); } } if constexpr (NodeIndex != 0 && CommunicatesWithPredecessor) { for (std::size_t i = 0; i < IStreamsSize; ++i) { - this->istreams[i].valid(fromProducerInterface[i].communicate(this->istreams[i].is_ready())); + // Interface SHM <-> sim + this->istreams[i].valid(fromProducerInterface[i].readFromLastNode(this->istreams[i].is_ready())); } } } public: - /// Init streams according to nodeindex + /** + * Initialize streams according to nodeindex + */ void initStreams() { - if constexpr (NodeIndex == 0) { - for (auto&& s : this->istreams) { + if constexpr (NodeIndex == 0) { // First Node; no predecessor + for (auto&& s : this->istreams) { // Input into sim valid s.valid(); } - } else if constexpr (NodeIndex == TotalNodes - 1) { - for (auto&& s : this->ostreams) { + } else if constexpr (NodeIndex == TotalNodes - 1) { // Last Node; no successor + for (auto&& s : this->ostreams) { // Output from sim ready s.ready(); } + for (std::size_t i = 0; i < IStreamsSize; ++i) { // Relay ready from sim to predecessor + fromProducerInterface[i].readFromLastNode(this->istreams[i].is_ready()); + } + } else { // Intermediate Node; has both predecessor and successor + for (std::size_t i = 0; i < OStreamsSize; ++i) { // Relay ready from FIFO to sim + this->ostreams[i].ready(this->fifo[i].is_ready()); + } + for (std::size_t i = 0; i < IStreamsSize; ++i) { // Relay valid from sim to predecessor + fromProducerInterface[i].readFromLastNode(this->istreams[i].is_ready()); + } } - // Middle nodes don't initialize any streams } /// Reset simulation (stream and current FIFO depth) @@ -189,6 +208,7 @@ class SingleNodeSimulation : public Simulation::reset(); for (std::size_t i = 0; i < OStreamsSize; ++i) { toConsumerInterface[i].reset(); + fifo[i].reset(); } for (std::size_t i = 0; i < IStreamsSize; ++i) { fromProducerInterface[i].reset(); @@ -196,6 +216,11 @@ class SingleNodeSimulation : public Simulationclk.toggle_clk(); communicate(); debug(std::format("Finished cycle {}\n\n", cyclesRun)); @@ -219,7 +244,7 @@ class SingleNodeSimulation : public Simulation fifoOccupation; - alignas(hardware_destructive_interference_size) boost::ipc_atomic maxFifoDepth; - alignas(hardware_destructive_interference_size) boost::ipc_atomic iCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic oCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic iReady; - alignas(hardware_destructive_interference_size) boost::ipc_atomic oValid; - - SharedData() : fifoOccupation(0), maxFifoDepth(0), iCycle(0), oCycle(0), iReady(false), oValid(false) {} - SharedData(unsigned int fifoOcc, unsigned int maxDepth, unsigned int inCycle, unsigned int outCycle, bool inReady, bool outValid) - : fifoOccupation(fifoOcc), maxFifoDepth(maxDepth), iCycle(inCycle), oCycle(outCycle), iReady(inReady), oValid(outValid) {} - SharedData(const SharedData& other) - : fifoOccupation(other.fifoOccupation.load()), - maxFifoDepth(other.maxFifoDepth.load()), - iCycle(other.iCycle.load()), - oCycle(other.oCycle.load()), - iReady(other.iReady.load()), - oValid(other.oValid.load()) {} + alignas(hardware_destructive_interference_size) boost::ipc_atomic predCycle; + alignas(hardware_destructive_interference_size) boost::ipc_atomic succCycle; + alignas(hardware_destructive_interference_size) boost::ipc_atomic ready; + alignas(hardware_destructive_interference_size) boost::ipc_atomic valid; + + SharedData() : predCycle(0), succCycle(0), ready(false), valid(false) {} + SharedData(unsigned int predecessorCycle, unsigned int successorCycle, bool inReady, bool outValid) : predCycle(predecessorCycle), succCycle(successorCycle), ready(inReady), valid(outValid) {} + SharedData(const SharedData& other) : predCycle(other.predCycle.load()), succCycle(other.succCycle.load()), ready(other.ready.load()), valid(other.valid.load()) {} SharedData& operator=(const SharedData& other) { - fifoOccupation.store(other.fifoOccupation.load()); - maxFifoDepth.store(other.maxFifoDepth.load()); - iCycle.store(other.iCycle.load()); - oCycle.store(other.oCycle.load()); - iReady.store(other.iReady.load()); - oValid.store(other.oValid.load()); + predCycle.store(other.predCycle.load()); + succCycle.store(other.succCycle.load()); + ready.store(other.ready.load()); + valid.store(other.valid.load()); return *this; } }; @@ -76,7 +65,6 @@ class SimulationInterface { SharedData* sharedData = nullptr; boost::ipc_atomic* refCount = nullptr; const std::string shmIdentifier; - std::atomic largestOccupation; ipc::managed_shared_memory shmem; #ifdef NDEBUG @@ -92,8 +80,8 @@ class SimulationInterface { // Uninitialized - will be move-assigned later } - SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier), largestOccupation(0) { - simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); + SimulationInterface(const char* _shmIdentifier) : shmIdentifier(_shmIdentifier) { + simInterfaceDebug("Creating simulation interface."); if (T == SimulationInterfaceType::PRODUCING) { ipc::shared_memory_object::remove(_shmIdentifier); simInterfaceDebug("Removed previous shared memory objects."); @@ -116,7 +104,7 @@ class SimulationInterface { simInterfaceDebug(std::format("Reference count incremented to {}", currentRefCount)); // Construct or find the entire SharedData struct in shared memory - sharedData = shmem.find_or_construct("data")(SharedData(0, initialMaxDepth, 0, 0, initialMaxDepth > 0, false)); + sharedData = shmem.find_or_construct("data")(SharedData(0, 0, true, false)); simInterfaceDebug("Shared data structure constructed or found."); } @@ -125,8 +113,7 @@ class SimulationInterface { SimulationInterface& operator=(const SimulationInterface&) = delete; // Move constructor - SimulationInterface(SimulationInterface&& other) noexcept - : sharedData(other.sharedData), refCount(other.refCount), shmIdentifier(std::move(other.shmIdentifier)), shmem(std::move(other.shmem)) { + SimulationInterface(SimulationInterface&& other) noexcept : sharedData(other.sharedData), refCount(other.refCount), shmIdentifier(std::move(other.shmIdentifier)), shmem(std::move(other.shmem)) { // Mark other as moved-from other.sharedData = nullptr; other.refCount = nullptr; @@ -172,62 +159,45 @@ class SimulationInterface { } } - /// Return the largest occupation that this FIFO has had so far - std::size_t getLargestOccupation() { - return largestOccupation; - } - - /// Set the max fifo depth in this interface. - void setMaxFifoDepth(unsigned int depth) { - simInterfaceDebug(std::format("Setting max FIFO depth to {}", depth)); - sharedData->maxFifoDepth.store(depth, boost::memory_order_release); - } - /// Reset all interface data fields to their defaults - void reset(unsigned int newMaxFifoDepth = 2) { - simInterfaceDebug(std::format("Resetting simulation interface (with max FIFO depth {})", newMaxFifoDepth)); - largestOccupation = 0; - sharedData->fifoOccupation.store(0, boost::memory_order_release); - sharedData->maxFifoDepth.store(newMaxFifoDepth, boost::memory_order_release); - sharedData->iReady.store(true, boost::memory_order_release); - sharedData->iCycle.store(0, boost::memory_order_release); - sharedData->oValid.store(false, boost::memory_order_release); - sharedData->oCycle.store(0, boost::memory_order_release); + void reset() { + simInterfaceDebug("Resetting simulation interface"); + sharedData->ready.store(true, boost::memory_order_release); + sharedData->predCycle.store(0, boost::memory_order_release); + sharedData->valid.store(false, boost::memory_order_release); + sharedData->succCycle.store(0, boost::memory_order_release); } - /// Communicate with the interface from the consumer side. Pass in the consuming nodes' input_ready. - /// If the interface has valid data, it will do the exchange. - /// The function returns the interfaces (FIFOs) output_valid signal, which should be - /// read by the consumer and set on their simulation port. - bool communicate(bool consumerReady) + /** + * Reads valid from shm and puts ready into shm. + * Returns the valid signal read from shm. + * Called on consumer side. + */ + bool readFromLastNode(bool consumerReady) requires(T == SimulationInterfaceType::CONSUMING) { - // The input side must always be one cycle ahead of the output side - // Wait until input catches up (and overtakes) - while (sharedData->iCycle <= sharedData->oCycle) {} - sharedData->oValid = sharedData->fifoOccupation > 0; - sharedData->fifoOccupation -= static_cast(sharedData->oValid && consumerReady); - ++(sharedData->oCycle); - return sharedData->oValid; + // The predecessor must always be one cycle ahead of the successor side + // Wait until predecessor catches up (and overtakes) + while (sharedData->predCycle <= sharedData->succCycle) {} + sharedData->ready = consumerReady; + ++(sharedData->succCycle); + return sharedData->valid; } - /// Communicate with the interface from the producer side. Pass in the producing nodes' output_valid. - /// If the interface is ready to receive data, it will do the exchange. - /// The function returns the interfaces (FIFOs) input_ready signal, which should be - /// read by the producer and set on their simulation port. - bool communicate(bool producerValid) + /** + * Reads ready from shm and puts valid into shm. + * Returns the ready signal read from shm. + * Called on producer side. + */ + bool writeToNextNode(bool producerValid) requires(T == SimulationInterfaceType::PRODUCING) { - // The input side must always be one cycle ahead of the output side + // The predecessor side must always be at least one cycle ahead of the output side // Wait until output catches up - while (sharedData->oCycle != sharedData->iCycle) {} - sharedData->iReady = sharedData->fifoOccupation < sharedData->maxFifoDepth; - sharedData->fifoOccupation += static_cast(sharedData->iReady && producerValid); - if (sharedData->iReady && producerValid) { - ++largestOccupation; - } - ++(sharedData->iCycle); - return sharedData->iReady; + while (sharedData->succCycle != sharedData->predCycle) {} + sharedData->valid = producerValid; + ++(sharedData->predCycle); + return sharedData->ready; } }; #endif diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp new file mode 100644 index 0000000000..eff415e3ef --- /dev/null +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -0,0 +1,32 @@ +#include +#include + +FIFO::FIFO(std::size_t max_size) : util(0), max_util(0), max_size(max_size) {} + +FIFO::~FIFO() {} + +bool FIFO::is_valid() { + if (sucReady && util > 0) { + --util; + return true; + } + return false; +} + +void FIFO::ready(bool ready) { sucReady = ready; } + +bool FIFO::is_ready() const { return util < max_size; } + +void FIFO::write(bool valid) { + if (valid && util < max_size) { + max_util = std::max(max_util, ++util); + } +} + +std::size_t FIFO::get_largest_occupation() const { return max_util; } + +void FIFO::reset() { + util = 0; + max_util = 0; + sucReady = false; +} diff --git a/src/finn/custom_op/fpgadataflow/channelwise_op.py b/src/finn/custom_op/fpgadataflow/channelwise_op.py index ecb555ce3e..ac49a0ae75 100644 --- a/src/finn/custom_op/fpgadataflow/channelwise_op.py +++ b/src/finn/custom_op/fpgadataflow/channelwise_op.py @@ -33,7 +33,6 @@ from qonnx.util.basic import qonnx_make_model from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.custom_op.fpgadataflow.memstream import MemStreamSupport from finn.util.logging import log # ONNX i/o tensor shape assumptions for channelwise ops: @@ -74,7 +73,7 @@ def get_smallest_possible(vals): return DataType["INT64"] -class ChannelwiseOp(MemStreamSupport, HWCustomOp): +class ChannelwiseOp(HWCustomOp): """Abstraction layer for HW implementation of ChannelwiseOp.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/custom_op/fpgadataflow/elementwise_binary.py b/src/finn/custom_op/fpgadataflow/elementwise_binary.py index 8bcf961386..3f953a971b 100644 --- a/src/finn/custom_op/fpgadataflow/elementwise_binary.py +++ b/src/finn/custom_op/fpgadataflow/elementwise_binary.py @@ -35,13 +35,14 @@ from finn.custom_op.fpgadataflow import register_custom_op from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.custom_op.fpgadataflow.memstream import MemStreamSupport # FINN logging from finn.util.logging import log # Generic implementation for elementwise binary operations -class ElementwiseBinaryOperation(HWCustomOp): +class ElementwiseBinaryOperation(MemStreamSupport, HWCustomOp): # Specifies the elementwise operation to be implemented # Format: (Identifier, Python, C++, RTL) _operation: tuple[str, np.ufunc, str, str] | None = None diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 43b200ff40..add5b1f47e 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,4 +1,5 @@ """Manage FINN simulation variants.""" +import finn_xsi.adapter as finnxsi import multiprocessing import numpy as np import onnx @@ -18,7 +19,7 @@ from qonnx.transformation.infer_shapes import InferShapes from random import Random from subprocess import CalledProcessError -from typing import Any, cast +from typing import Any, Sequence, cast from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -30,11 +31,6 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log -try: - import finn_xsi.adapter as finnxsi -except ModuleNotFoundError: - finnxsi = None - class Simulation: """Manage simulations in FINN.""" @@ -96,16 +92,20 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: f"isolate for simulation." ) inp = onnx.helper.make_tensor_value_info( - "inp", TensorProto.FLOAT, target_op.get_folded_input_shape() + "inp", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_folded_input_shape()) ) inp_dummy_out = onnx.helper.make_tensor_value_info( # noqa - "inp_dummy_out", TensorProto.FLOAT, target_op.get_folded_input_shape() + "inp_dummy_out", + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_folded_input_shape()), ) outp = onnx.helper.make_tensor_value_info( # noqa - "outp", TensorProto.FLOAT, target_op.get_normal_output_shape() + "outp", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_output_shape()) ) outp_dummy_out = onnx.helper.make_tensor_value_info( - "outp_dummy_out", TensorProto.FLOAT, target_op.get_normal_output_shape() + "outp_dummy_out", + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_output_shape()), ) input_dummy_node = onnx.helper.make_node( "RemoveDataPath_rtl", @@ -243,7 +243,7 @@ def _create_sim_so( ) sim_base, sim_rel = finnxsi.compile_sim_obj( top_module_name, all_verilog_srcs, str(sim_dir), debug=debug - ) # noqa # type: ignore + ) rtlsim_so = Path(sim_base) / Path(sim_rel) model.set_metadata_prop("rtlsim_so", str(rtlsim_so)) else: @@ -442,7 +442,7 @@ def _build_single_node_simulation( # Building the whole simulation return self._compile_simulation(sim_base).absolute() - def run_sim_node_parallel_isolated(self, inputs: int) -> None: + def run_sim_node_parallel_isolated(self, inputs: int) -> None: # noqa: ARG002 """Simulate the given number of inputs for every layer. Layers are completely isolated and simulated in parallel. """ @@ -499,7 +499,7 @@ def _run_simulation(binary: Path, cpu: int | None) -> None: self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) self.model = self.model.transform(HLSSynthIP()) synth_workers = max( - 1, cast(int, (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 16) + 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 16) ) # 16GB per synthesis with ThreadPoolExecutor(max_workers=synth_workers) as pool: for i in range(total_nodes): From a3ff9f8c40acf0f89d69633046d1ccfcf319a1b0 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 11 Nov 2025 13:46:49 +0100 Subject: [PATCH 026/170] Added thread communication of simulation with Python. Restructured simulation building --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 23 +- finn_xsi/finn_xsi/include/Simulation.hpp | 312 +++++++++++++++--- .../finn_xsi/include/SimulationInterface.hpp | 16 +- finn_xsi/finn_xsi/include/helper.h | 2 +- finn_xsi/finn_xsi/rtlsim_config.hpp.template | 2 +- src/finn/builder/build_dataflow.py | 12 +- .../transformation/fpgadataflow/simulation.py | 250 +++++++++----- .../fpgadataflow/simulation_controller.py | 156 +++++++++ src/finn/util/basic.py | 4 +- src/finn/util/logging.py | 79 +++++ 10 files changed, 680 insertions(+), 176 deletions(-) create mode 100644 src/finn/transformation/fpgadataflow/simulation_controller.py diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 5635d8993d..90a98a8532 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #define NDEBUG #include @@ -25,7 +26,6 @@ int main(int argc, const char* argv[]) { // Parse CLI options po::options_description desc{"Options"}; desc.add_options() - ("depth,d", po::value()->required(), "FIFO Depth") ("output,o", po::value()->default_value("simulation_data.json"), "Simulation Data Output"); po::variables_map vm; po::store(po::parse_command_line(argc, argv, desc), vm); @@ -42,23 +42,10 @@ int main(int argc, const char* argv[]) { RTLSimConfig::ostream_descs, RTLSimConfig::previousNodeName, RTLSimConfig::currentNodeName, - vm["depth"].as() + 2, + vm["output"].as() ); - - /** SECTION WIP */ - auto start = std::chrono::high_resolution_clock::now(); - for (std::size_t j = 0; j < 100000; ++j) { - sim.runSingleCycle(); - } - auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); - if constexpr(RTLSimConfig::NodeIndex == 0) { - std::cout << duration << " ms" << std::endl; - } - /***********/ - - // Write results as JSON - auto outputPath = std::filesystem::path(vm["output"].as()); - sim.writeResults(outputPath); - + std::this_thread::sleep_for(std::chrono::milliseconds(2000)); + sim.start(); return 0; } diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index c85021e7e8..50d31fd720 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -24,14 +24,19 @@ #include #include #include +#include +#include #include #include +#include #include #include using json = nlohmann::json; + + template class Simulation { protected: @@ -82,6 +87,12 @@ class Simulation { initStreams(); } + template + bool hasValidOutput() { + //static_assert(Index < ostreams.size(), "Cannot request valid status of unknown output stream index"); + return ostreams[Index].is_valid(); + } + void clearPorts() noexcept { // Clear all input ports for (xsi::Port& p : top.ports()) { @@ -105,28 +116,40 @@ class Simulation { template -class SingleNodeSimulation : public Simulation { +class _SingleNodeSimulation : public Simulation { private: - using ConsumingInterface = SimulationInterface; - using ProducingInterface = SimulationInterface; + // Interface members + using ConsumingInterface = SimulationInterface; + using ProducingInterface = SimulationInterface; std::array fromProducerInterface; std::array toConsumerInterface; - std::size_t cyclesRun = 0; public: - SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, - std::array _ostream_descs, std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, - unsigned int initialFIFODepth = 2) - : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { + _SingleNodeSimulation( + const std::string& kernel_lib, + const std::string& design_lib, + const char* xsim_log_file, + const char* trace_file, + std::array _istream_descs, + std::array _ostream_descs, + std::optional prevNodeName = std::nullopt, + std::optional nodeName = std::nullopt, + unsigned int initialFIFODepth = 2 + ) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { if (CommunicatesWithPredecessor && !prevNodeName) { throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); } else if (!CommunicatesWithPredecessor && prevNodeName) { - std::cout << "Simulation was passed the previous nodes name but is NOT marked for communication with predecessor node. No shared memory will be created." << std::endl; + std::cout << "log Simulation was passed the previous nodes name but is " + "NOT marked for communication with predecessor node. No " + "shared memory will be created." << std::endl; } if (CommunicatesWithSuccessor && !nodeName) { - throw std::runtime_error("Cannot communicate with successor because current node name was not given!"); + throw std::runtime_error("Cannot communicate with successor because " + "current node name was not given!"); } else if (!CommunicatesWithSuccessor && nodeName) { - std::cout << "Simulation was passed the current nodes name but is NOT marked for communication with successor node. No shared memory will be created." << std::endl; + std::cout << "log Simulation was passed the current nodes name but is NOT " + "marked for communication with successor node. No shared " + "memory will be created." << std::endl; } // Set valid and ready @@ -136,25 +159,40 @@ class SingleNodeSimulation : public Simulationistreams) { + s.valid(); + } + } else if constexpr (NodeIndex == TotalNodes - 1) { + for (auto&& s : this->ostreams) { + s.ready(); + } + } + // Middle nodes don't initialize any streams + } + + /// Communicate with predecessors and successors and update their values and our own [[gnu::hot]] void communicate() { if constexpr (NodeIndex != TotalNodes - 1 && CommunicatesWithSuccessor) { @@ -169,22 +207,32 @@ class SingleNodeSimulation : public Simulationistreams) { + this->readyLog << stream.is_ready() << " "; + } + this->readyLog << "\n"; + for (M_AXIS_Control& stream : this->ostreams) { + this->validLog << stream.is_valid() << " "; + } + this->validLog << "\n"; + } + + public: - /// Init streams according to nodeindex - void initStreams() { - if constexpr (NodeIndex == 0) { - for (auto&& s : this->istreams) { - s.valid(); - } - } else if constexpr (NodeIndex == TotalNodes - 1) { - for (auto&& s : this->ostreams) { - s.ready(); - } + /// Run a single cycle, increase the cyclesRun counter and, if enabled, log ready and valid signals + [[gnu::hot]] void runSingleCycle() { + this->clk.toggle_clk(); + communicate(); + if constexpr (LoggingEnabled) { + logReadyValidState(); } - // Middle nodes don't initialize any streams } - /// Reset simulation (stream and current FIFO depth) + /// Reset simulation (stream and current FIFO depth, as well as cycle counter) void reset() { Simulation::reset(); for (std::size_t i = 0; i < OStreamsSize; ++i) { @@ -195,37 +243,197 @@ class SingleNodeSimulation : public Simulationclk.toggle_clk(); - communicate(); - debug(std::format("Finished cycle {}\n\n", cyclesRun)); - ++cyclesRun; + /// Return the largest occupation the specified output stream / FIFO has seen + std::size_t getLargestOccupation(std::size_t outputIndex) { + return toConsumerInterface[outputIndex].getLargestOccupation(); + } - // Log the signals that this simulations set (ready to predecessor, valid to successor) - // TODO: Collect signals in vectors and only write to file after the sim for speedup - if constexpr (LoggingEnabled) { - for (S_AXIS_Control& stream : this->istreams) { - this->readyLog << stream.is_ready() << " "; - } - this->readyLog << "\n"; - for (M_AXIS_Control& stream : this->ostreams) { - this->validLog << stream.is_valid() << " "; - } - this->validLog << "\n"; + /// Set the max FIFO depth of all interfaces + void setMaxFIFODepth(unsigned int depth) { + for (ProducingInterface& prod : toConsumerInterface) { + prod.setMaxFifoDepth(depth); + } + for (ConsumingInterface& cons : fromProducerInterface) { + cons.setMaxFifoDepth(depth); } } + /// Get the job size of the specified output stream + std::size_t getOutputJobSize(std::size_t outputIndex = 0) { + return this->ostreams[outputIndex].job_size; + } +}; + + +/// Single Node Simulation, thread controlled +template +class SingleNodeSimulation { + private: + std::jthread simulator; + std::jthread communicator; + + // Only run cycles if True + // TODO: Atomic? + bool running; + + // Run until cyclesTarget are hit + std::size_t cyclesTarget; + + // Current run cycles counter + std::size_t cyclesRun; + + // Path on which to store simulation data after stopping + std::filesystem::path simulationDataPath; + + // The simulation itself + _SingleNodeSimulation sim; + + public: + SingleNodeSimulation( + const std::string& kernel_lib, + const std::string& design_lib, + const char* xsim_log_file, + const char* trace_file, + std::array _istream_descs, + std::array _ostream_descs, + std::optional prevNodeName = std::nullopt, + std::optional nodeName = std::nullopt, + unsigned int initialFIFODepth = 2, + std::string simulationDataFilename = "simulation_data.json" + ) : running(false), + cyclesTarget(0), + cyclesRun(0), + simulationDataPath(simulationDataFilename), + sim(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs, prevNodeName, nodeName, initialFIFODepth) {} + + + /// Stop and reset the simulation, reset cycle counter, target cycle counter, log queue. + /// Leaves the simulation in a paused and reset state. + void reset() { + running = false; + sim.reset(); + cyclesRun = 0; + cyclesTarget = 0; + } + /// Write the results of the simulation as a JSON file - void writeResults(std::filesystem::path& path) { + void writeResults() { json j; for (std::size_t i = 0; i < OStreamsSize; ++i) { - j["maxOccupation"][std::to_string(i)] = toConsumerInterface[i].getLargestOccupation(); + j["maxOccupation"][std::to_string(i)] = sim.getLargestOccupation(i); } j["cyclesRun"] = cyclesRun; - std::ofstream file(path); - file << j; + std::ofstream file(simulationDataPath); + file << j.dump(4); file.close(); } + + /// Split the given string in two, delimited by a space. Subsequent spaces are ignored. If no + /// space is found, the second element is empty. + std::tuple splitSpace(std::string& s) { + auto pos = s.find(" "); + if (s == "") { + return std::make_tuple("", ""); + } + return std::make_tuple(s.substr(0, pos), s.substr(pos)); + } + + /// Read from std::cin if possible, otherwise + /// return immediately. + void getlineIfAvailable(std::string& buffer) { + //std::cin.exceptions(std::istream::failbit | std::istream::badbit); + if (std::cin.eof() || std::cin.rdbuf()->in_avail() == -1) { + buffer = ""; + return; + } + std::getline(std::cin, buffer); + } + + /// Start both threads. Listen for commands on stdin. + void start() { + running = true; + simulator = std::jthread([this](std::stop_token stop) { + while (cyclesTarget == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::cout << "log Waiting.... (target: " << cyclesTarget << ")" << std::endl; + } + std::cout << "log Starting running with a target of " << cyclesTarget << " cycles!" << std::endl; + + // TODO: Move to communicator thread + std::cout << "started" << std::endl; + while (cyclesRun < cyclesTarget) { + if (!running) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + if (stop.stop_requested()) { + return; + } + std::cout << "cycles " << cyclesRun << " " << cyclesTarget << std::endl; + sim.runSingleCycle(); + ++cyclesRun; + } + // TODO: Move to communicator thread + std::cout << "stopped" << std::endl; + }); + communicator = std::jthread([this]() { + std::cout << "ready" << std::endl; + std::string input = ""; + while (true) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (cyclesTarget != 0 && cyclesRun == cyclesTarget) { + std::cout << "end" << std::endl; + return; + } + if constexpr(LoggingEnabled) { + if (running && cyclesRun % 5000 == 1) { + std::cout << "cycles " << cyclesRun << " " << cyclesTarget << std::endl; + } + } + // Parse incoming commands + getlineIfAvailable(input); + if (input == "") { + continue; + } + auto [command, argument] = splitSpace(input); + if (command == "stop") { + simulator.request_stop(); + writeResults(); + // Wait for the file to be fully written + std::this_thread::sleep_for(std::chrono::milliseconds(1000)); + // Signal python that we are done + std::cout << "end" << std::endl; + return; + } else if (command == "fifodepth") { + unsigned int newDepth = static_cast(std::stoul(argument)); + running = false; + sim.setMaxFIFODepth(newDepth); + running = true; + std::cout << "log Set FIFO depth to " << newDepth << std::endl; + } else if (command == "runCycles") { + cyclesTarget += static_cast(std::stoul(argument)); + running = true; + std::cout << "log Set running with " << cyclesTarget << std::endl; + } else if (command == "runSamples") { + cyclesTarget += static_cast(std::stoul(argument)) * sim.getOutputJobSize(0); + running = true; + std::cout << "log Set running with " << cyclesTarget << std::endl; + } else if (command == "pause") { + running = false; + } else if (command == "reset") { + reset(); + } else if (command == "resume") { + running = true; + } else if (command == "help") { + // TODO: Insert here or document separately + } else { + std::cout << "log Unknown command " << std::endl; + } + } + }); + simulator.join(); + communicator.join(); + } }; diff --git a/finn_xsi/finn_xsi/include/SimulationInterface.hpp b/finn_xsi/finn_xsi/include/SimulationInterface.hpp index 61033d1646..7a104a07cb 100644 --- a/finn_xsi/finn_xsi/include/SimulationInterface.hpp +++ b/finn_xsi/finn_xsi/include/SimulationInterface.hpp @@ -40,7 +40,7 @@ constexpr std::string_view to_string(SimulationInterfaceType t) { return "UNKNOWN SIMULATION INTERFACE TYPE"; } -template +template class SimulationInterface { private: // Shared memory structure with proper cache-line alignment @@ -52,9 +52,14 @@ class SimulationInterface { alignas(hardware_destructive_interference_size) boost::ipc_atomic iReady; alignas(hardware_destructive_interference_size) boost::ipc_atomic oValid; + SharedData() : fifoOccupation(0), maxFifoDepth(0), iCycle(0), oCycle(0), iReady(false), oValid(false) {} + + SharedData(unsigned int fifoOcc, unsigned int maxDepth, unsigned int inCycle, unsigned int outCycle, bool inReady, bool outValid) : fifoOccupation(fifoOcc), maxFifoDepth(maxDepth), iCycle(inCycle), oCycle(outCycle), iReady(inReady), oValid(outValid) {} + + SharedData(const SharedData& other) : fifoOccupation(other.fifoOccupation.load()), maxFifoDepth(other.maxFifoDepth.load()), @@ -62,6 +67,8 @@ class SimulationInterface { oCycle(other.oCycle.load()), iReady(other.iReady.load()), oValid(other.oValid.load()) {} + + SharedData& operator=(const SharedData& other) { fifoOccupation.store(other.fifoOccupation.load()); maxFifoDepth.store(other.maxFifoDepth.load()); @@ -75,15 +82,15 @@ class SimulationInterface { SharedData* sharedData = nullptr; boost::ipc_atomic* refCount = nullptr; - std::atomic largestOccupation; ipc::managed_shared_memory shmem; const std::string shmIdentifier; + std::atomic largestOccupation; #ifdef NDEBUG [[maybe_unused]] void simInterfaceDebug([[maybe_unused]] std::string_view s) {} #else /// Log the given text with a header identifying the shared memory region and the interface type - void simInterfaceDebug(std::string_view s) { debug(std::format("{} ({}): {}", shmIdentifier, to_string(T), s)); } + void simInterfaceDebug(std::string_view s) { debug(std::format("log {} ({}): {}", shmIdentifier, to_string(T), s)); } #endif public: @@ -94,7 +101,7 @@ class SimulationInterface { SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier), largestOccupation(0) { simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); - if (T == SimulationInterfaceType::PRODUCING) { + if (T == SimulationInterfaceType::PRODUCING || IsIOInterface) { ipc::shared_memory_object::remove(_shmIdentifier); simInterfaceDebug("Removed previous shared memory objects."); shmem = ipc::managed_shared_memory(ipc::create_only, _shmIdentifier, ShmemSize); @@ -179,7 +186,6 @@ class SimulationInterface { /// Set the max fifo depth in this interface. void setMaxFifoDepth(unsigned int depth) { - simInterfaceDebug(std::format("Setting max FIFO depth to {}", depth)); sharedData->maxFifoDepth.store(depth, boost::memory_order_release); } diff --git a/finn_xsi/finn_xsi/include/helper.h b/finn_xsi/finn_xsi/include/helper.h index 827985940d..2c681cf920 100644 --- a/finn_xsi/finn_xsi/include/helper.h +++ b/finn_xsi/finn_xsi/include/helper.h @@ -19,7 +19,7 @@ struct StreamDescriptor { #ifdef NDEBUG [[maybe_unused]] inline void debug([[maybe_unused]] std::string_view s) {} #else -inline void debug(std::string_view s) { std::cout << "[DBG] " << s << "\n"; } +inline void debug(std::string_view s) { std::cout << "log [DBG] " << s << "\n"; } #endif #endif /* HELPER_H_ */ diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index c676a09ba0..217de6c038 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -19,7 +19,7 @@ namespace RTLSimConfig { // Log during simulation. Turned off by default. Might increase runtime if used. - constexpr bool LoggingEnabled = false; + constexpr bool LoggingEnabled = true; /**** General RTLSIM Configuration Parameters ****/ const std::optional currentNodeName = "@NODE_NAME@"; diff --git a/src/finn/builder/build_dataflow.py b/src/finn/builder/build_dataflow.py index 2977539e79..30efb556f5 100644 --- a/src/finn/builder/build_dataflow.py +++ b/src/finn/builder/build_dataflow.py @@ -26,8 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" -FINN dataflow build system. +"""FINN dataflow build system. This module provides the main build infrastructure for converting ONNX models to FINN dataflow accelerators. It handles step resolution, logging, error handling, @@ -40,8 +39,8 @@ import json import logging import os +from collections.abc import Callable from pathlib import Path -from typing import Callable import pdb # isort: split import sys @@ -52,6 +51,7 @@ from rich.logging import RichHandler from rich.traceback import Traceback +import finn.util.logging from finn.builder.build_dataflow_config import DataflowBuildConfig, default_build_dataflow_steps from finn.builder.build_dataflow_steps import build_dataflow_step_lookup from finn.util.exception import ( @@ -285,8 +285,7 @@ def setup_logging(cfg: DataflowBuildConfig): elif cfg.console_log_level == "CRITICAL": consoleHandler.setLevel(logging.CRITICAL) logging.getLogger().addHandler(consoleHandler) - - return log + return log, console def exit_buildflow(cfg: DataflowBuildConfig, time_per_step: dict = None, exit_code: int = 0): @@ -344,7 +343,8 @@ def build_dataflow_cfg(model_filename, cfg: DataflowBuildConfig): # Create the output (report) dir if it doesn't exist os.makedirs(os.path.join(cfg.output_dir, "report"), exist_ok=True) - log = setup_logging(cfg) + log, console = setup_logging(cfg) + finn.util.logging._RICH_CONSOLE = console logfile = get_logfile_path(cfg) print(f"Intermediate outputs will be generated in {os.environ['FINN_BUILD_DIR']}") print(f"Final outputs will be generated in {cfg.output_dir}") diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index d0dde7dbb1..43d8b30504 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,13 +1,14 @@ """Manage FINN simulation variants.""" -import multiprocessing +import json import numpy as np import onnx import os import shlex -import subprocess import sys +import time from concurrent.futures import Future, ThreadPoolExecutor from copy import deepcopy +from enum import Enum from onnx import NodeProto, TensorProto from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper @@ -18,9 +19,10 @@ from typing import TYPE_CHECKING, Any, cast from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP -from finn.util.basic import get_vivado_root, launch_process_helper, make_build_dir +from finn.transformation.fpgadataflow.simulation_controller import NodeConnectedSimulationController +from finn.util.basic import launch_process_helper, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError -from finn.util.logging import log +from finn.util.logging import DisabledLoggingConsole, ThreadsafeProgressDisplay, log try: import finn_xsi.adapter as finnxsi @@ -32,14 +34,26 @@ from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -class Simulation: - """Manage simulations in FINN.""" +class SimulationType(str, Enum): + # Individual node simulations connected by IPC + NODE_BASED_CONNECTED = "NODE_BASED_CONNECTED" + + # Individual node simulations, isolated. E.g. for analysis purposes + NODE_BASED_ISOLATED = "NODE_BASED_ISOLATED" + + # Legacy method (deprecated) + COMPLETE_DESIGN = "COMPLETE_DESIGN" + + +class SimulationBuilder: + """Build simulations in FINN.""" def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: """Create a new simulation instance.""" self.model = model self.fpgapart = fpgapart self.clk_ns = clk_ns + self.progress_bar = ThreadsafeProgressDisplay([], [], []) def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: """Return a modelwrapper that has only the specified node. @@ -211,7 +225,7 @@ def _create_sim_so( sim_rel = "xsim.dir" + sim_rel return Path(sim_base), Path(sim_rel) - def _compile_simulation(self, sim_base: Path) -> Path: + def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: """Compile an existing RTLSIM directory. Requires _create_sim_so to be run before. Expects rtlsim_config.hpp to be templated already. @@ -226,18 +240,26 @@ def _compile_simulation(self, sim_base: Path) -> Path: launch_process_helper( shlex.split(cmake_call), cwd=finnxsi_dir, - print_stdout=True, + print_stdout=not silent, + print_stderr=not silent, proc_env=os.environ.copy(), ) except CalledProcessError as e: raise FINNUserError(f"Failed to run cmake in {sim_base}") from e + self.progress_bar.update("CMake") # Calling make to actually build the simulation makefile = Path(sim_base) / "Makefile" if not makefile.exists(): raise FINNUserError(f"Failed to create Makefile in {sim_base}!") try: - launch_process_helper(["make"], proc_env=os.environ.copy(), cwd=sim_base) + launch_process_helper( + ["make"], + proc_env=os.environ.copy(), + cwd=sim_base, + print_stdout=not silent, + print_stderr=not silent, + ) except CalledProcessError as e: raise FINNUserError(f"Failed to create executable in {sim_base}!") from e @@ -245,15 +267,8 @@ def _compile_simulation(self, sim_base: Path) -> Path: simulation_executable = Path(sim_base) / "LayerSimulationBackend" if not simulation_executable.exists(): raise FINNUserError(f"Make call in {sim_base} failed!") - - # Prepare the script to run the simulation - # (important to specify LD_LIBRARY_PATH here for XSI to work correctly) - runsim = Path(sim_base) / "run_fifosim.sh" - ld_library_path = get_vivado_root() + "/lib/lnx64.o" - runsim.write_text( - f"LD_LIBRARY_PATH={ld_library_path}:$LD_LIBRARY_PATH {simulation_executable} --depth 2" - ) - return runsim + self.progress_bar.update("Make") + return simulation_executable def _template_rtlsim_config( self, @@ -313,7 +328,7 @@ def _template_rtlsim_config( rtlsim_config.write_text(fsim_config) return rtlsim_config - def _build_single_node_simulation( + def build_single_node_simulation( self, node_name: str, node_model: ModelWrapper, @@ -322,6 +337,7 @@ def _build_single_node_simulation( previous_node_name: str | None, build_dir: Path | None, timeout_cycles: int = 0, + silent: bool = False, ) -> Path: """Build the simulation binary for a single node. @@ -343,6 +359,7 @@ def _build_single_node_simulation( created from the nodes name. timeout_cycles: Number of cycles until simulation timeout. When set to 0 (default), no timeout is given. + silent: If True, silences the Cmake and make output (including stderr) Returns: Path: The path to the simulation binary (shell script). @@ -398,16 +415,38 @@ def _build_single_node_simulation( ) # Building the whole simulation - return self._compile_simulation(sim_base).absolute() + return self._compile_simulation(sim_base, silent).absolute() + + def _get_randomized_names(self, model: ModelWrapper, suffix_length: int = 5) -> dict[int, str]: + """Add a randomized suffix to every name in the model. Used to avoid interference with + previous or parallel running IPC simulations.""" + rand = Random() + rand.seed() + return { + i: model.graph.node[i].name + + "".join( + rand.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(suffix_length) + ) + for i in range(len(model.graph.node)) + } + + def _build_simulation_node_connected( + self, workers: int, with_live_display: bool + ) -> dict[int, Path]: + """Build all nodes in the model in parallel, as isolated simulations, ready for usage in + an IPC connected simulation chain. - def run_sim_node_parallel_isolated(self, inputs: int) -> None: - """Simulate the given number of inputs for every layer. Layers are completely isolated - and simulated in parallel. + Args: + workers: Number of parallel workers to use. + with_live_display: If True, display the building progress in a rich progress bar. + + Returns: + Dict of executables that start the simulation of the given nodes, + indexed by the node-index. These are in their respective FINN_TMP + directories. """ - for i, node in enumerate(self.model.graph.node): - print(f"{i}: {node.name}") - def _build_simulation( + def _build( node_name: str, node_index: int, total_nodes: int, @@ -416,93 +455,117 @@ def _build_simulation( ) -> Any: nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(CreateStitchedIP(self.fpgapart, self.clk_ns)) - return self._build_single_node_simulation( - node_name, nodemodel, node_index, total_nodes, prev_node_name, build_dir - ) - - def _run_simulation(binary: Path, cpu: int | None) -> None: - command = "" - if cpu is not None: - command += f"taskset --cpu-list {cpu} " - # TODO: numactl - command += f"bash {binary}" - subprocess.run( - shlex.split(command), stdout=sys.stdout, stderr=sys.stderr, cwd=binary.parent + self.progress_bar.update("StitchedIP") + return self.build_single_node_simulation( + node_name, + nodemodel, + node_index, + total_nodes, + prev_node_name, + build_dir, + silent=with_live_display, ) # Create randomized names to avoid clashing with old IPC shared memory segments. - rand = Random() - rand.seed() - randomized_names = { - i: self.model.graph.node[i].name - + "".join(rand.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(5)) - for i in range(len(self.model.graph.node)) - } + randomized_names = self._get_randomized_names(self.model) # Build simulations in parallel - # TODO: Change to info when done - log.warning("BUILDING NODE SIMULATIONS") workers = int(os.environ["NUM_DEFAULT_WORKERS"]) total_nodes = len(self.model.graph.node) futures: dict[int, Future] = {} - binaries: dict[int, Path] = {} + if with_live_display: + log.disabled = True + self.progress_bar.start() with ThreadPoolExecutor(max_workers=workers) as pool: for i in range(total_nodes): futures[i] = pool.submit( - _build_simulation, + _build, randomized_names[i], i, total_nodes, randomized_names[i - 1] if i >= 1 else None, # type: ignore Path(make_build_dir(f"rtlsim_{randomized_names[i]}_")), ) - pool.shutdown(wait=True) - for i, future in futures.items(): - binaries[i] = future.result() + + binaries = {i: future.result() for i, future in futures.items()} + if with_live_display: + self.progress_bar.stop() + log.disabled = False # Create a script to build and run the entire simulation again - run_simulation = make_build_dir("run_simulation") - run_all_simulations = Path(run_simulation) / "run.sh" - build_all_simulations = Path(run_simulation) / "build.sh" - log.info(f"Storing run-all-simulations script in {run_simulation}") - with (run_all_simulations).open("w+") as f: - f.write("#!/bin/bash\n") - f.write('echo "Running simulation"\n') - for binary in binaries.values(): - f.write(f"bash {binary} &\n") - f.write("wait\n") - with build_all_simulations.open("w+") as f: - f.write("#!/bin/bash\n") - for binary in binaries.values(): - # Build each binary new. Done in parallel in the background - f.write(f"{{ cd {binary.parent};cmake . && make; }} &\n") - f.write("wait\n") - - # TODO: Change to info when done - log.warning("RUNNING NODE SIMULATIONS") - # TODO: Might be unnecessary. Remove later - sys.stdout = sys.stdout.console - sys.stderr = sys.stderr.console - with ThreadPoolExecutor(max_workers=workers) as pool: - for i, binary in binaries.items(): - print( - f"Submitting thread for running simulation {i} / {total_nodes} " - f"({self.model.graph.node[i].name})" + # TODO + return binaries + + def build_simulation( + self, simtype: SimulationType, workers: int, with_live_display: bool + ) -> dict[int, Path]: + """Build a simulation of the given type, return the resulting executable. + + Args: + simtype: Simulation type to build. + workers: Number of workers to use in parallel. + Normally set by the Simulation() class automatically. + with_live_display: If True, display a live progress-bar. + """ + match simtype: + case SimulationType.NODE_BASED_CONNECTED: + node_count = len(self.model.graph.node) + self.progress_bar = ThreadsafeProgressDisplay( + ["StitchedIP", "CMake", "Make"], + [node_count] * 3, + [ + "[bold blue](1)[/bold blue] Creating stitched IPs", + "[bold blue](2)[/bold blue] Configuring project with CMake", + "[bold blue](3)[/bold blue] Building simulation binaries", + ], ) - # TODO: If more processes than CPU cores, group processes to their adjacent nodes - pool.submit(_run_simulation, binary, i % multiprocessing.cpu_count()) - pool.shutdown(wait=True) + return self._build_simulation_node_connected(workers, with_live_display) + case SimulationType.NODE_BASED_ISOLATED: + raise NotImplementedError() + case SimulationType.COMPLETE_DESIGN: + raise FINNUserError(f"Simulation method {simtype} is deprecated!") - def run_sim_node_parallel_connected(self, inputs: int) -> Any: - """Simulate a whole model, with all layers simulated in parallel.""" - # TODO: Enable control through either Python or a seperate C++ driver - raise NotImplementedError() - def run_sim_complete(self) -> Any: - raise NotImplementedError() +class Simulation: + """Manage simulation (runs) in FINN. + + IMPORTANT: If the modelwrapper was somehow changed, create a NEW simulation object! + """ + + def __init__( + self, model: ModelWrapper, fpgapart: str, clk_ns: float, workers: int | None = None + ) -> None: + """Create a new simulation instance. If workers is None, NUM_DEFAULT_WORKERS are used.""" + self.model = model + self.builder = SimulationBuilder(model, fpgapart, clk_ns) + self.workers = int(os.environ["NUM_DEFAULT_WORKERS"]) if workers is None else workers - def run_sim_single_node(self, node: Any) -> Any: - raise NotImplementedError() + # TODO: Caching of existing simulations + + def simulate_node_connected(self, samples: int, depth: int) -> dict[int, dict]: + """Simulate the given number of samples for every layer. Layers are completely isolated + and simulated in parallel. Simulation data is returned as a dict (by node name as index). + """ + binaries = self.builder.build_simulation( + SimulationType.NODE_BASED_CONNECTED, self.workers, with_live_display=True + ) + names = [node.name for node in self.model.graph.node] + + # Run simulation + start = time.time() + with DisabledLoggingConsole() as console: + controller = NodeConnectedSimulationController( + len(binaries), names, list(binaries.values()), console, 0.1, False + ) + controller.run(depth, samples) + end = time.time() + log.warning(f"Simulation took {end-start} seconds!") + # Return the collected data + data = {} + for i, binary in binaries.items(): + with (binary.parent / "simulation_data.json").open() as f: + data[i] = json.load(f) + return data # TODO: Just a test transformation. Will be integrated properly later @@ -514,5 +577,10 @@ def __init__(self, fpgapart: str, clk_ns: float) -> None: # noqa def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: sim = Simulation(model, self.fpgapart, self.clk_ns) - sim.run_sim_node_parallel_isolated(1) + sys.stdout = sys.stdout.console + sys.stderr = sys.stderr.console + sim.simulate_node_connected(5, 1024) + sim.simulate_node_connected(1, 2) + sim.simulate_node_connected(1, 20000) + sim.simulate_node_connected(10, 20000) return model, False diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py new file mode 100644 index 0000000000..67ba906340 --- /dev/null +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -0,0 +1,156 @@ +"""Control (node based) simulations via stdio.""" +import multiprocessing +import subprocess +import sys +import time +import traceback +from concurrent.futures import Future, ThreadPoolExecutor +from pathlib import Path +from rich.console import Console +from subprocess import Popen +from threading import Lock + +from finn.util.basic import get_vivado_root +from finn.util.exception import FINNInternalError +from finn.util.logging import ThreadsafeProgressDisplay + + +class NodeConnectedSimulationController: + """Control a node-node IPC connected simulation in threads.""" + + def __init__( + self, + parallel_simulations: int, + names: list[str], + binaries: list[Path], + console: Console, + poll_interval: float = 0.1, + with_progressbar: bool = True, + ) -> None: + """Create a new controller, without starting the simulation. + + Args: + parallel_simulations: Number of simulations to run in parallel. + names: List of names for the simulations. + binaries: List of paths to the simulation binaries. + console: The rich.console.Console to print with. + poll_interval: How long the wait between checks of the processes stdout/stdin is. + with_progressbar: Whether or not to display a progressbar for the cycle count. + """ + if len(names) != len(binaries): + raise FINNInternalError( + f"Simulation controller received non-matching " + f"name and binary count: {len(names)} and {len(binaries)}" + ) + self.binaries = binaries + self.names = names + self.console = console + self.poll_interval = poll_interval + self.workers = parallel_simulations + self.progress = None + if with_progressbar: + self.progress = ThreadsafeProgressDisplay(names, [0] * len(names), names) + self.running_lock = Lock() + self.running = 0 + self.total = len(names) + + def run(self, depth: int, samples: int) -> None: + """Run the simulation entirely with the given depth and sample count.""" + futures: list[Future] = [] + if self.progress is not None: + self.progress.start() + with ThreadPoolExecutor(self.workers) as pool: + for i, (name, binary) in enumerate(zip(self.names, self.binaries, strict=True)): + futures.append( + pool.submit( + self._run_binary, + binary, + name, + i % multiprocessing.cpu_count(), + depth, + samples, + i == 0 or i == len(self.names) - 1, + ) + ) + pool.shutdown() + if self.progress is not None: + self.progress.stop() + + def _send(self, proc: Popen[bytes], cmd: str) -> None: + """Send a command to the given process stdin and flush the buffer.""" + if not cmd.endswith("\n"): + cmd += "\n" + proc.stdin.write(cmd.encode()) + proc.stdin.flush() + + def _run_binary( + self, + binary: Path, + name: str | None, + cpu: int | None, + depth: int, + samples: int, + is_end_node: bool = False, + ) -> None: + """Run the specified simulation binary in a new subprocess and communicate with it.""" + + def _print(msg: str, color: str = "green") -> None: + if self.progress is None: + if is_end_node: + color = "orange3" + self.console.log(f"[bold {color}]{name:<35}[/bold {color}] {msg:<35}") + + ld_library_path = "LD_LIBRARY_PATH=" + get_vivado_root() + "/lib/lnx64.o:$LD_LIBRARY_PATH" + cwd = binary.parent + if name is None: + name = cwd.name.replace("rtlsim_", "") + taskset = "" + if cpu is not None: + taskset += f"taskset --cpu-list {cpu}" # TODO: numactl? + command = f"{ld_library_path} {taskset} {binary}" + _print(f"Running command: {command}") + try: + proc = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=cwd, + shell=True, + ) + _send = lambda cmd: self._send(proc, cmd) # noqa: E731 + received = "" + while received != "end": + time.sleep(self.poll_interval) + received = proc.stdout.readline().decode("UTF-8").strip().split() # type: ignore + if len(received) == 0: + continue + if received[0] == "end": + _print("Ending simulation.") + return + if received[0] == "log": + _print(" ".join(received[1:])) + elif received[0] == "ready": + _print("Received ready signal from simulation") + _send(f"fifodepth {depth}") + _send(f"runSamples {samples}") + _print("Settings sent to simulation.") + elif received[0] == "cycles": + if self.progress is None: + _print(" ".join(received[1:])) + else: + self.progress.update(name, int(received[1]), int(received[2])) + elif received[0] == "started": + with self.running_lock: + self.running += 1 + _print(f"Running: {self.running} / {self.total}") + elif received[1] == "stopped": + with self.running_lock: + self.running -= 1 + _print(f"Running: {self.running} / {self.total}") + else: + raise FINNInternalError(f"Simulation {name}: Unrecognized command: {received}") + except Exception as e: + self.console.log(f"Exception caught during simulation execution ({name}): {e}") + self.console.log(traceback.format_exc()) + sys.exit(1) diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 86881585d8..c2bc01e1b3 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -180,7 +180,7 @@ def make_build_dir(prefix: str = "", return_as_path: bool = False) -> str | Path return str(tmpdir) -def launch_process_helper(args, proc_env=None, cwd=None, print_stdout=True): +def launch_process_helper(args, proc_env=None, cwd=None, print_stdout=True, print_stderr=True): """Helper function to launch a process in a way that facilitates logging stdout/stderr with Python loggers. Returns (cmd_out, cmd_err) if successful, raises CalledProcessError otherwise.""" @@ -199,7 +199,7 @@ def launch_process_helper(args, proc_env=None, cwd=None, print_stdout=True): # Handle stderr, depending on return code if process.returncode == 0: # Process completed successfully, log stderr only as WARNING - if cmd_err: + if cmd_err and print_stderr: log.warning(cmd_err) else: # Process failed, log stderr as ERROR diff --git a/src/finn/util/logging.py b/src/finn/util/logging.py index 1ae2dbfcfa..04806a663a 100644 --- a/src/finn/util/logging.py +++ b/src/finn/util/logging.py @@ -1,3 +1,82 @@ +"""Handle logging related functionality.""" import logging +from rich.console import Console +from rich.progress import Progress, TaskID +from threading import Lock log = logging.getLogger("finn_logger") + +_RICH_CONSOLE = Console() + + +class DisabledLoggingConsole: + """Contextmanager to use the current rich console without logging active.""" + + def __init__(self) -> None: + log.disabled = True + + def __enter__(self) -> Console: + return _RICH_CONSOLE + + def __exit__(self, tp, vl, tb) -> None: + log.disabled = False + + +def get_console() -> Console: + return _RICH_CONSOLE + + +class ThreadsafeProgressDisplay: + """Small helper to display multithreaded display bars. + Logging has to be disabled before usage. + """ + + def __init__( + self, tasks: list[str], totals: list[int | float], descriptions: list[str] + ) -> None: + """Create a new progress display.""" + self.lock = Lock() + self.state: dict[str, int | float] = dict.fromkeys(tasks, 0) + self.ptasks: dict[str, TaskID] = {} + self.totals_state = dict(zip(tasks, totals, strict=True)) + + self.tasks: list[str] = tasks + self.totals: list[float | int] = totals + self.descriptions: list[str] = descriptions + assert len(tasks) == len(totals) + assert len(totals) == len(descriptions) + + def start(self) -> None: + """Start the display.""" + self.progress = Progress(transient=True, redirect_stdout=False, redirect_stderr=False) + self.progress.start() + for task, desc, total in zip(self.tasks, self.descriptions, self.totals, strict=True): + self.ptasks[task] = self.progress.add_task(desc, total=total) + + def update(self, task: str, value: float | None = None, total: float | None = None) -> None: + """Update a value and the progress bar. If the task does not exist do nothing. + This is practical, because it means any method can update the progressbar + without any danger. Just the initially calling method must create a fitting display object. + + If value is None, the value is incremented once. + """ + if task in self.state and task in self.ptasks: + # NOTE: rich.progress at some point apparently became threadsafe, + # but just to be extra sure we add a lock here. + with self.lock: + if value is None: + self.state[task] += 1 + else: + self.state[task] = value + if total is not None: + self.totals_state[task] = total + self.progress.update( + self.ptasks[task], + completed=self.state[task], + refresh=True, + total=self.totals_state[task], + ) + + def stop(self) -> None: + """Stop the display.""" + self.progress.stop() From 4a09271f8e00fc1ed206b3b47c056c05ba44b91c Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 11 Nov 2025 15:49:46 +0100 Subject: [PATCH 027/170] Merge complete --- finn_xsi/finn_xsi/include/AXIS_Control.h | 12 +- finn_xsi/finn_xsi/include/AXI_Control.h | 16 +-- finn_xsi/finn_xsi/include/Clock.h | 2 +- finn_xsi/finn_xsi/include/FIFO.h | 15 +-- finn_xsi/finn_xsi/include/Simulation.hpp | 35 +++--- finn_xsi/finn_xsi/src/AXIS_Control.cpp | 10 +- finn_xsi/finn_xsi/src/AXI_Control.cpp | 70 +++++------ finn_xsi/finn_xsi/src/Clock.cpp | 2 +- finn_xsi/finn_xsi/src/FIFO.cpp | 24 ++-- src/finn/builder/build_dataflow_config.py | 4 + .../transformation/fpgadataflow/simulation.py | 114 +++++++++++------- src/finn/util/logging.py | 6 + 12 files changed, 173 insertions(+), 137 deletions(-) diff --git a/finn_xsi/finn_xsi/include/AXIS_Control.h b/finn_xsi/finn_xsi/include/AXIS_Control.h index 415ed48401..f2e936337f 100644 --- a/finn_xsi/finn_xsi/include/AXIS_Control.h +++ b/finn_xsi/finn_xsi/include/AXIS_Control.h @@ -26,13 +26,13 @@ class AXIS_Control { // Core functions - immediate writes void valid(bool value = true); - bool is_valid() const noexcept; + bool isValid() const noexcept; void ready(bool value = true); - bool is_ready() const noexcept; + bool isReady() const noexcept; // Deferred write functions - std::reference_wrapper set_valid(bool value = true); - std::reference_wrapper set_ready(bool value = true); + std::reference_wrapper setValid(bool value = true); + std::reference_wrapper setReady(bool value = true); // Job Size and Transaction Statistics size_t job_size; @@ -75,10 +75,10 @@ class M_AXIS_Control : public AXIS_Control { M_AXIS_Control(M_AXIS_Control&& other) = default; M_AXIS_Control& operator=(M_AXIS_Control&& other) = default; - size_t last_complete = 0; + size_t lastComplete = 0; size_t interval; size_t latency = 0; - size_t min_latency = std::numeric_limits::max(); // Minimum latency observed + size_t minLatency = std::numeric_limits::max(); // Minimum latency observed }; #endif /* AXIS_CONTROL */ diff --git a/finn_xsi/finn_xsi/include/AXI_Control.h b/finn_xsi/finn_xsi/include/AXI_Control.h index 64eaf3740f..24e0e11237 100644 --- a/finn_xsi/finn_xsi/include/AXI_Control.h +++ b/finn_xsi/finn_xsi/include/AXI_Control.h @@ -18,8 +18,8 @@ class AXI_Control { ~AXI_Control() noexcept = default; // // Core register access functions - void write_register(uint32_t addr, uint32_t data); - uint32_t read_register(uint32_t addr); + void writeRegister(uint32_t addr, uint32_t data); + uint32_t readRegister(uint32_t addr); private: // AXI interface prefix @@ -28,13 +28,13 @@ class AXI_Control { Clock& clk; // Helper functions for multi-bit signal handling - void write_addr(const std::string& signal, uint32_t addr); - void write_data(const std::string& signal, uint32_t data); - void write_strb(const std::string& signal, uint32_t strb); + void writeAddr(const std::string& signal, uint32_t addr); + void writeData(const std::string& signal, uint32_t data); + void writeStrb(const std::string& signal, uint32_t strb); uint32_t read(const std::string& signal); - void set_bool(const std::string& signal); - void clear_bool(const std::string& signal); - bool chk_bool(const std::string& signal); + void setBool(const std::string& signal); + void clearBool(const std::string& signal); + bool chkBool(const std::string& signal); }; #endif /* AXI_CONTROL */ diff --git a/finn_xsi/finn_xsi/include/Clock.h b/finn_xsi/finn_xsi/include/Clock.h index ad2ce73ac3..b9c6e1235b 100644 --- a/finn_xsi/finn_xsi/include/Clock.h +++ b/finn_xsi/finn_xsi/include/Clock.h @@ -25,7 +25,7 @@ class Clock { std::function cycle; - void toggle_clk() noexcept; + void toggleClk() noexcept; }; #endif /* CLOCK */ diff --git a/finn_xsi/finn_xsi/include/FIFO.h b/finn_xsi/finn_xsi/include/FIFO.h index 83a4f5bfa4..2a29c2e9a2 100644 --- a/finn_xsi/finn_xsi/include/FIFO.h +++ b/finn_xsi/finn_xsi/include/FIFO.h @@ -5,22 +5,23 @@ #include class FIFO { - std::size_t util = 0; - std::size_t max_util = 0; - std::size_t max_size = 0; + std::size_t currentUtil = 0; + std::size_t maxUtil = 0; + std::size_t maxSize = 0; bool sucReady = false; public: - FIFO(std::size_t max_size = std::numeric_limits::max()); + FIFO(std::size_t maxSize = std::numeric_limits::max()); ~FIFO(); // Add FIFO methods and members as needed - bool is_valid(); + bool isValid(); void ready(bool ready); - bool is_ready() const; + bool isReady() const; void write(bool valid); - std::size_t get_largest_occupation() const; + std::size_t getLargestOccupation() const; void reset(); + void setMaxSize(std::size_t newSize); }; #endif /* FIFO_H */ diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 9c37b42345..6ffa7c235e 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -97,7 +97,7 @@ class Simulation { // Reset all Inputs, Wait for Reset Period rst_n.set(0).write_back(); for (unsigned i = 0; i < 16; i++) { - clk.toggle_clk(); + clk.toggleClk(); } rst_n.set(1).write_back(); } @@ -173,16 +173,16 @@ class _SingleNodeSimulation : public Simulation FIFO - this->fifo[i].write(this->ostreams[i].is_valid()); - this->ostreams[i].ready(this->fifo[i].is_ready()); + this->fifo[i].write(this->ostreams[i].isValid()); + this->ostreams[i].ready(this->fifo[i].isReady()); // Interface FIFO <-> SHM - this->fifo[i].ready(toConsumerInterface[i].writeToNextNode(this->fifo[i].is_valid())); + this->fifo[i].ready(toConsumerInterface[i].writeToNextNode(this->fifo[i].isValid())); } } if constexpr (NodeIndex != 0 && CommunicatesWithPredecessor) { for (std::size_t i = 0; i < IStreamsSize; ++i) { // Interface SHM <-> sim - this->istreams[i].valid(fromProducerInterface[i].readFromLastNode(this->istreams[i].is_ready())); + this->istreams[i].valid(fromProducerInterface[i].readFromLastNode(this->istreams[i].isReady())); } } } @@ -201,14 +201,14 @@ class _SingleNodeSimulation : public Simulationistreams[i].is_ready()); + fromProducerInterface[i].readFromLastNode(this->istreams[i].isReady()); } } else { // Intermediate Node; has both predecessor and successor for (std::size_t i = 0; i < OStreamsSize; ++i) { // Relay ready from FIFO to sim - this->ostreams[i].ready(this->fifo[i].is_ready()); + this->ostreams[i].ready(this->fifo[i].isReady()); } for (std::size_t i = 0; i < IStreamsSize; ++i) { // Relay valid from sim to predecessor - fromProducerInterface[i].readFromLastNode(this->istreams[i].is_ready()); + fromProducerInterface[i].readFromLastNode(this->istreams[i].isReady()); } } } @@ -218,11 +218,11 @@ class _SingleNodeSimulation : public Simulationistreams) { - this->readyLog << stream.is_ready() << " "; + this->readyLog << stream.isReady() << " "; } this->readyLog << "\n"; for (M_AXIS_Control& stream : this->ostreams) { - this->validLog << stream.is_valid() << " "; + this->validLog << stream.isValid() << " "; } this->validLog << "\n"; } @@ -243,7 +243,7 @@ class _SingleNodeSimulation : public Simulationclk.toggle_clk(); + this->clk.toggleClk(); communicate(); if constexpr (LoggingEnabled) { logReadyValidState(); @@ -262,12 +262,9 @@ class _SingleNodeSimulation : public Simulationset(static_cast(value)).write_back(); } -bool AXIS_Control::is_valid() const noexcept { return port_vld->read().as_bool(); } +bool AXIS_Control::isValid() const noexcept { return port_vld->read().as_bool(); } void AXIS_Control::ready(bool value) { port_rdy->set(static_cast(value)).write_back(); } -bool AXIS_Control::is_ready() const noexcept { return port_rdy->read().as_bool(); } +bool AXIS_Control::isReady() const noexcept { return port_rdy->read().as_bool(); } // Deferred write functions -std::reference_wrapper AXIS_Control::set_valid(bool value) { return std::ref(port_vld->set(value ? 1 : 0)); } +std::reference_wrapper AXIS_Control::setValid(bool value) { return std::ref(port_vld->set(value ? 1 : 0)); } -std::reference_wrapper AXIS_Control::set_ready(bool value) { return std::ref(port_rdy->set(value ? 1 : 0)); } +std::reference_wrapper AXIS_Control::setReady(bool value) { return std::ref(port_rdy->set(value ? 1 : 0)); } S_AXIS_Control::S_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, size_t job_tks, const std::string& prefix) : AXIS_Control(des, clock, job_sz, prefix), job_ticks(job_tks), await_iter(job_tks) { if (job_sz < 1 || job_tks < 1) { @@ -44,7 +44,7 @@ S_AXIS_Control::S_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, si } } -M_AXIS_Control::M_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, const std::string& prefix) : AXIS_Control(des, clock, job_sz, prefix), last_complete(0), interval(0) { +M_AXIS_Control::M_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, const std::string& prefix) : AXIS_Control(des, clock, job_sz, prefix), lastComplete(0), interval(0) { if (job_sz < 1) { throw std::invalid_argument("Job size must be greater than 0."); } diff --git a/finn_xsi/finn_xsi/src/AXI_Control.cpp b/finn_xsi/finn_xsi/src/AXI_Control.cpp index 78717b8c27..fa3c8b6f35 100644 --- a/finn_xsi/finn_xsi/src/AXI_Control.cpp +++ b/finn_xsi/finn_xsi/src/AXI_Control.cpp @@ -24,7 +24,7 @@ AXI_Control::AXI_Control(xsi::Design& des, Clock& clock, const std::string& axi_ } // Helper functions for multi-bit signal handling -void AXI_Control::write_addr(const std::string& signal, uint32_t addr) { +void AXI_Control::writeAddr(const std::string& signal, uint32_t addr) { // Convert addr to binary string std::string addr_bin = std::bitset<32>(addr).to_string(); @@ -46,7 +46,7 @@ void AXI_Control::write_addr(const std::string& signal, uint32_t addr) { port.set_binstr(addr_bin).write_back(); } -void AXI_Control::write_data(const std::string& signal, uint32_t data) { +void AXI_Control::writeData(const std::string& signal, uint32_t data) { // Similar to write_addr std::string data_bin = std::bitset<32>(data).to_string(); @@ -63,7 +63,7 @@ void AXI_Control::write_data(const std::string& signal, uint32_t data) { port.set_binstr(data_bin).write_back(); } -void AXI_Control::write_strb(const std::string& signal, uint32_t strb) { +void AXI_Control::writeStrb(const std::string& signal, uint32_t strb) { // Similar to write_addr std::string strb_bin = std::bitset<4>(strb).to_string(); @@ -85,56 +85,56 @@ uint32_t AXI_Control::read(const std::string& signal) { return port.read().as_unsigned(); } -void AXI_Control::set_bool(const std::string& signal) { +void AXI_Control::setBool(const std::string& signal) { Port& port = design.getPort(signal); port.set(1).write_back(); } -void AXI_Control::clear_bool(const std::string& signal) { +void AXI_Control::clearBool(const std::string& signal) { Port& port = design.getPort(signal); port.set(0).write_back(); } -bool AXI_Control::chk_bool(const std::string& signal) { +bool AXI_Control::chkBool(const std::string& signal) { Port& port = design.getPort(signal); return port.read().as_bool(); } -void AXI_Control::write_register(uint32_t addr, uint32_t data) { +void AXI_Control::writeRegister(uint32_t addr, uint32_t data) { // Assert BREADY to receive response - set_bool(prefix + "bready"); + setBool(prefix + "bready"); // Set address - write_addr(prefix + "awaddr", addr); + writeAddr(prefix + "awaddr", addr); // Set data and strobe (full 32-bit word) - write_data(prefix + "wdata", data); - write_strb(prefix + "wstrb", 0xF); // All bytes enabled + writeData(prefix + "wdata", data); + writeStrb(prefix + "wstrb", 0xF); // All bytes enabled // Assert AWVALID - set_bool(prefix + "awvalid"); + setBool(prefix + "awvalid"); // Assert WVALID - set_bool(prefix + "wvalid"); + setBool(prefix + "wvalid"); // Wait for AWREADY - while (!chk_bool(prefix + "awready")) { - clk.toggle_clk(); + while (!chkBool(prefix + "awready")) { + clk.toggleClk(); } // Wait for WREADY - while (!chk_bool(prefix + "wready")) { - clk.toggle_clk(); + while (!chkBool(prefix + "wready")) { + clk.toggleClk(); } - clk.toggle_clk(); // Make sure that for at least one cycle the signals were set + clk.toggleClk(); // Make sure that for at least one cycle the signals were set // Deassert AWVALID and WVALID - clear_bool(prefix + "awvalid"); - clear_bool(prefix + "wvalid"); + clearBool(prefix + "awvalid"); + clearBool(prefix + "wvalid"); // Wait for BVALID - while (!chk_bool(prefix + "bvalid")) { - clk.toggle_clk(); + while (!chkBool(prefix + "bvalid")) { + clk.toggleClk(); } // Check BRESP (optional, could add error handling) @@ -144,32 +144,32 @@ void AXI_Control::write_register(uint32_t addr, uint32_t data) { } // Deassert BREADY - clear_bool(prefix + "bready"); + clearBool(prefix + "bready"); - clk.toggle_clk(); + clk.toggleClk(); } -uint32_t AXI_Control::read_register(uint32_t addr) { +uint32_t AXI_Control::readRegister(uint32_t addr) { // Assert RREADY to receive data - set_bool(prefix + "rready"); + setBool(prefix + "rready"); // Set address - write_addr(prefix + "araddr", addr); + writeAddr(prefix + "araddr", addr); // Assert ARVALID - set_bool(prefix + "arvalid"); + setBool(prefix + "arvalid"); // Wait for ARREADY - while (!chk_bool(prefix + "arready")) { - clk.toggle_clk(); + while (!chkBool(prefix + "arready")) { + clk.toggleClk(); } // Wait for RVALID - while (!chk_bool(prefix + "rvalid")) { - clk.toggle_clk(); + while (!chkBool(prefix + "rvalid")) { + clk.toggleClk(); } // Deassert ARVALID - clear_bool(prefix + "arvalid"); + clearBool(prefix + "arvalid"); // Read data uint32_t data = read(prefix + "rdata"); @@ -181,8 +181,8 @@ uint32_t AXI_Control::read_register(uint32_t addr) { } // Deassert RREADY - clear_bool(prefix + "rready"); - clk.toggle_clk(); + clearBool(prefix + "rready"); + clk.toggleClk(); return data; } diff --git a/finn_xsi/finn_xsi/src/Clock.cpp b/finn_xsi/finn_xsi/src/Clock.cpp index eb6d1dd77c..6814b11161 100644 --- a/finn_xsi/finn_xsi/src/Clock.cpp +++ b/finn_xsi/finn_xsi/src/Clock.cpp @@ -29,7 +29,7 @@ Clock::Clock(xsi::Design& des) : design(des) { }); } -void Clock::toggle_clk() noexcept { +void Clock::toggleClk() noexcept { cycle(1); cycle(0); } diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp index eff415e3ef..c4566fdc07 100644 --- a/finn_xsi/finn_xsi/src/FIFO.cpp +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -1,13 +1,13 @@ #include #include -FIFO::FIFO(std::size_t max_size) : util(0), max_util(0), max_size(max_size) {} +FIFO::FIFO(std::size_t max_size) : currentUtil(0), maxUtil(0), maxSize(max_size) {} FIFO::~FIFO() {} -bool FIFO::is_valid() { - if (sucReady && util > 0) { - --util; +bool FIFO::isValid() { + if (sucReady && currentUtil > 0) { + --currentUtil; return true; } return false; @@ -15,18 +15,22 @@ bool FIFO::is_valid() { void FIFO::ready(bool ready) { sucReady = ready; } -bool FIFO::is_ready() const { return util < max_size; } +bool FIFO::isReady() const { return currentUtil < maxSize; } void FIFO::write(bool valid) { - if (valid && util < max_size) { - max_util = std::max(max_util, ++util); + if (valid && currentUtil < maxSize) { + maxUtil = std::max(maxUtil, ++currentUtil); } } -std::size_t FIFO::get_largest_occupation() const { return max_util; } +std::size_t FIFO::getLargestOccupation() const { return maxUtil; } + +void FIFO::setMaxSize(size_t newSize) { + maxSize = newSize; +} void FIFO::reset() { - util = 0; - max_util = 0; + currentUtil = 0; + maxUtil = 0; sucReady = false; } diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index 968eed880b..0c71879ade 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -318,6 +318,10 @@ class DataflowBuildConfig(DataClassJSONMixin, DataClassYAMLMixin): #: Enables experimental live FIFO sizing on the FPGA. live_fifo_sizing: bool = False + #: Whether to use functional simulation when available. Takes some time + #: to synthesize, but results in much faster simulations. + functional_simulation: bool = True + #: Whether FIFO nodes with depth larger than 32768 will be split. #: Allow to configure very large FIFOs in the folding_config_file. split_large_fifos: bool = False diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 71b93fe679..aa8ff850ba 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -9,6 +9,7 @@ import sys import time from concurrent.futures import Future, ThreadPoolExecutor +from contextlib import nullcontext from copy import deepcopy from enum import Enum from onnx import NodeProto, TensorProto @@ -20,8 +21,9 @@ from qonnx.transformation.infer_shapes import InferShapes from random import Random from subprocess import CalledProcessError -from typing import Any, Sequence, cast +from typing import TYPE_CHECKING, Any, cast +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP @@ -33,6 +35,9 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import DisabledLoggingConsole, ThreadsafeProgressDisplay, log +if TYPE_CHECKING: + from collections.abc import Sequence + class SimulationType(str, Enum): # Individual node simulations connected by IPC @@ -473,7 +478,7 @@ def _get_randomized_names(self, model: ModelWrapper, suffix_length: int = 5) -> } def _build_simulation_node_connected( - self, workers: int, with_live_display: bool + self, workers: int, with_live_display: bool, functional_sim: bool ) -> dict[int, Path]: """Build all nodes in the model in parallel, as isolated simulations, ready for usage in an IPC connected simulation chain. @@ -499,7 +504,7 @@ def _build( nodemodel = nodemodel.transform(InferShapes()) nodemodel = nodemodel.transform(PrepareIP(self.fpgapart, self.clk_ns)) nodemodel = nodemodel.transform( - CreateStitchedIP(self.fpgapart, self.clk_ns, functional_simulation=False) + CreateStitchedIP(self.fpgapart, self.clk_ns, functional_simulation=functional_sim) ) self.progress_bar.update("StitchedIP") return self.build_single_node_simulation( @@ -512,17 +517,6 @@ def _build( silent=with_live_display, ) - # Prepare the model - self.model = self.model.transform(InsertDWC()) - self.model = self.model.transform(SpecializeLayers(self.fpgapart)) - self.model = self.model.transform(GiveUniqueNodeNames()) - self.model = self.model.transform(GiveReadableTensorNames()) - self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) - self.model = self.model.transform(HLSSynthIP()) - synth_workers = max( - 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 16) - ) # 16GB per synthesis - # Create randomized names to avoid clashes with old IPC shared memory randomized_names = self._get_randomized_names(self.model) @@ -530,32 +524,35 @@ def _build( total_nodes = len(self.model.graph.node) futures: dict[int, Future] = {} - # TODO: Add synthesis to status bars - if with_live_display: - log.disabled = True - self.progress_bar.start() - with ThreadPoolExecutor(max_workers=synth_workers) as pool: - for i in range(total_nodes): - futures[i] = pool.submit( - _build, - randomized_names[i], - i, - total_nodes, - randomized_names[i - 1] if i >= 1 else None, # type: ignore - Path(make_build_dir(f"rtlsim_{randomized_names[i]}_")), - ) - - binaries = {i: future.result() for i, future in futures.items()} - if with_live_display: - self.progress_bar.stop() - log.disabled = False - - # Create a script to build and run the entire simulation again - # TODO - return binaries + # Build sims in parallel + synth_workers = max( + 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 16) + ) # 16GB per synthesis + if not functional_sim: + # When not having to do synthesis, the build is not memory bottlenecked and + # can be executed as parallel as possible + synth_workers = int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))) + + # Build (stitched IP, cmake, make) all sims in parallel and return paths to + # the compiled executables + with DisabledLoggingConsole(), self.progress_bar if with_live_display else nullcontext(): + self.progress_bar.progress.console.log( + f"Building simulations " f"using {synth_workers} workers.." + ) + with ThreadPoolExecutor(max_workers=synth_workers) as pool: + for i in range(total_nodes): + futures[i] = pool.submit( + _build, + randomized_names[i], + i, + total_nodes, + randomized_names[i - 1] if i >= 1 else None, # type: ignore + Path(make_build_dir(f"rtlsim_{randomized_names[i]}_")), + ) + return {i: future.result() for i, future in futures.items()} def build_simulation( - self, simtype: SimulationType, workers: int, with_live_display: bool + self, simtype: SimulationType, workers: int, with_live_display: bool, functional_sim: bool ) -> dict[int, Path]: """Build a simulation of the given type, return the resulting executable. @@ -564,6 +561,7 @@ def build_simulation( workers: Number of workers to use in parallel. Normally set by the Simulation() class automatically. with_live_display: If True, display a live progress-bar. + functional_sim: If True, use functional simulation (faster but takes some time to build) """ match simtype: case SimulationType.NODE_BASED_CONNECTED: @@ -577,7 +575,9 @@ def build_simulation( "[bold blue](3)[/bold blue] Building simulation binaries", ], ) - return self._build_simulation_node_connected(workers, with_live_display) + return self._build_simulation_node_connected( + workers, with_live_display, functional_sim + ) case SimulationType.NODE_BASED_ISOLATED: raise NotImplementedError() case SimulationType.COMPLETE_DESIGN: @@ -591,21 +591,44 @@ class Simulation: """ def __init__( - self, model: ModelWrapper, fpgapart: str, clk_ns: float, workers: int | None = None + self, + model: ModelWrapper, + fpgapart: str, + clk_ns: float, + functional_sim: bool, + workers: int | None = None, ) -> None: """Create a new simulation instance. If workers is None, NUM_DEFAULT_WORKERS are used.""" self.model = model - self.builder = SimulationBuilder(model, fpgapart, clk_ns) self.workers = int(os.environ["NUM_DEFAULT_WORKERS"]) if workers is None else workers - + self.functional_sim = functional_sim + self.fpgapart = fpgapart + self.clk_ns = clk_ns # TODO: Caching of existing simulations + # Prepare the model for simulation + with DisabledLoggingConsole() as console: # noqa + with console.status("Preparing model for the simulation step..."): + self._prepare_model() + self.builder = SimulationBuilder(self.model, fpgapart, clk_ns) + + def _prepare_model(self) -> None: + """Execute some preparation transformations on the model.""" + self.model = self.model.transform(InsertDWC()) + self.model = self.model.transform(SpecializeLayers(self.fpgapart)) + self.model = self.model.transform(GiveUniqueNodeNames()) + self.model = self.model.transform(GiveReadableTensorNames()) + self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) + self.model = self.model.transform(HLSSynthIP()) def simulate_node_connected(self, samples: int, depth: int) -> dict[int, dict]: """Simulate the given number of samples for every layer. Layers are completely isolated and simulated in parallel. Simulation data is returned as a dict (by node name as index). """ binaries = self.builder.build_simulation( - SimulationType.NODE_BASED_CONNECTED, self.workers, with_live_display=True + SimulationType.NODE_BASED_CONNECTED, + self.workers, + with_live_display=True, + functional_sim=self.functional_sim, ) names = [node.name for node in self.model.graph.node] @@ -628,13 +651,14 @@ def simulate_node_connected(self, samples: int, depth: int) -> dict[int, dict]: # TODO: Just a test transformation. Will be integrated properly later class RunLayerParallelSimulation(Transformation): # noqa - def __init__(self, fpgapart: str, clk_ns: float) -> None: # noqa + def __init__(self, fpgapart: str, clk_ns: float, cfg: DataflowBuildConfig) -> None: # noqa super().__init__() self.fpgapart = fpgapart self.clk_ns = clk_ns + self.cfg = cfg def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: - sim = Simulation(model, self.fpgapart, self.clk_ns) + sim = Simulation(model, self.fpgapart, self.clk_ns, self.cfg.functional_simulation) sys.stdout = sys.stdout.console sys.stderr = sys.stderr.console sim.simulate_node_connected(5, 1024) diff --git a/src/finn/util/logging.py b/src/finn/util/logging.py index 04806a663a..83e21f7ea2 100644 --- a/src/finn/util/logging.py +++ b/src/finn/util/logging.py @@ -80,3 +80,9 @@ def update(self, task: str, value: float | None = None, total: float | None = No def stop(self) -> None: """Stop the display.""" self.progress.stop() + + def __enter__(self) -> None: + self.start() + + def __exit__(self, tp, vl, tb) -> None: + self.stop() From 7f77ce412497a8cc50c238dbe30a2782eb515844 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:59:03 +0100 Subject: [PATCH 028/170] Fixes, Linting and prep work --- finn_xsi/finn_xsi/CMakeLists.txt | 32 +++- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 2 + finn_xsi/finn_xsi/include/FIFO.h | 3 + finn_xsi/finn_xsi/include/Simulation.hpp | 27 ++-- .../finn_xsi/include/SimulationInterface.hpp | 46 ++++-- finn_xsi/finn_xsi/src/FIFO.cpp | 10 +- .../fpgadataflow/create_stitched_ip.py | 143 +++++++++++------- .../transformation/fpgadataflow/simulation.py | 2 +- 8 files changed, 183 insertions(+), 82 deletions(-) diff --git a/finn_xsi/finn_xsi/CMakeLists.txt b/finn_xsi/finn_xsi/CMakeLists.txt index f2b1db354a..7bdce26f04 100644 --- a/finn_xsi/finn_xsi/CMakeLists.txt +++ b/finn_xsi/finn_xsi/CMakeLists.txt @@ -33,7 +33,28 @@ if(${FIFOSIM_ENABLE_ALLOPT}) message(STATUS "All optimizations are enabled") target_compile_options( fifosim_options - INTERFACE -Ofast -ffast-math -march=native -mtune=native -fstack-protector-strong -fopenmp -ffunction-sections -fdata-sections -pipe -funroll-loops -shared -fPIC) + INTERFACE -Ofast -ffast-math -march=native -mtune=native -fstack-protector-strong -fopenmp -ffunction-sections -fdata-sections -pipe -funroll-loops -shared -fPIC -Wno-interference-size + # Additional performance options: + -flto=auto # Link-time optimization (auto-detect thread count) + -fno-plt # Avoid PLT for better performance with shared libs + -fno-semantic-interposition # Allow more aggressive optimization in shared libs + -ftree-vectorize # Enable auto-vectorization (usually on with -O3) + -fvect-cost-model=dynamic # Better vectorization cost model + -fprefetch-loop-arrays # Prefetch arrays in loops + -fno-math-errno # Don't set errno for math functions (covered by -ffast-math mostly) + -fno-trapping-math # Allow optimizations that may trap (part of -ffast-math) + -ffinite-math-only # Assume no NaN/Inf (part of -ffast-math) + -fassociative-math # Allow reassociation (part of -ffast-math) + ) + target_link_options( + fifosim_options + INTERFACE + -flto=auto # LTO at link time + -Wl,--gc-sections # Remove unused sections + -Wl,--as-needed # Only link needed libraries + -Wl,-O3 # Linker optimization level + -Wl,--hash-style=gnu # Faster symbol lookup +) #target_link_options(fifosim_options INTERFACE -fsanitize=undefined,address) endif() @@ -49,7 +70,14 @@ if (FIFOSIM_ENABLE_WARNINGS) "" "") endif (FIFOSIM_ENABLE_WARNINGS) -target_compile_options(fifosim_options INTERFACE -Wno-interference-size) + +# Use ccache if available +find_program(CCACHE_PROGRAM ccache) +if(CCACHE_PROGRAM) + message(STATUS "Using ccache for builds") + set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") + set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") +endif() # # Create options for including cmake files from the cmake folder with a bit of output. diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index f814efb8f5..df6c9907a2 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -45,6 +45,7 @@ int main(int argc, const char* argv[]) { vm["depth"].as() ); + sim.initializeCommunication(); /** SECTION WIP */ auto start = std::chrono::high_resolution_clock::now(); @@ -52,6 +53,7 @@ int main(int argc, const char* argv[]) { sim.runSingleCycle(); } auto duration = std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count(); + std::cout << "Ready Counter: " << sim.getReadyCtr() << std::endl; if constexpr(RTLSimConfig::NodeIndex == 0) { std::cout << duration << " ms" << std::endl; } diff --git a/finn_xsi/finn_xsi/include/FIFO.h b/finn_xsi/finn_xsi/include/FIFO.h index 83a4f5bfa4..1845b11f52 100644 --- a/finn_xsi/finn_xsi/include/FIFO.h +++ b/finn_xsi/finn_xsi/include/FIFO.h @@ -10,6 +10,8 @@ class FIFO { std::size_t max_size = 0; bool sucReady = false; + std::size_t readyCtr = 0; + public: FIFO(std::size_t max_size = std::numeric_limits::max()); ~FIFO(); @@ -20,6 +22,7 @@ class FIFO { bool is_ready() const; void write(bool valid); std::size_t get_largest_occupation() const; + std::size_t getReadyCtr() const { return readyCtr; } void reset(); }; diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 1d72d90472..2752d6b075 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -159,14 +159,15 @@ class SingleNodeSimulation : public Simulation FIFO this->fifo[i].write(this->ostreams[i].is_valid()); this->ostreams[i].ready(this->fifo[i].is_ready()); // Interface FIFO <-> SHM - this->fifo[i].ready(toConsumerInterface[i].writeToNextNode(this->fifo[i].is_valid())); + this->fifo[i].ready(toConsumerInterface[i].writeToNextNode(this->fifo[i].is_valid(), static_cast(cyclesRun))); + } } if constexpr (NodeIndex != 0 && CommunicatesWithPredecessor) { @@ -215,16 +216,24 @@ class SingleNodeSimulation : public Simulationclk.toggle_clk(); + return totalReadyCtr; + } + + void initializeCommunication() { communicate(); - debug(std::format("Finished cycle {}\n\n", cyclesRun)); + } + + [[gnu::hot, gnu::always_inline]] void runSingleCycle() { + this->clk.toggle_clk(); ++cyclesRun; + communicate(); + debug(std::format("Finished cycle {}\n\n", cyclesRun-1)); + // Log the signals that this simulations set (ready to predecessor, valid to successor) // TODO: Collect signals in vectors and only write to file after the sim for speedup diff --git a/finn_xsi/finn_xsi/include/SimulationInterface.hpp b/finn_xsi/finn_xsi/include/SimulationInterface.hpp index 5a46ae5b9f..8b88080e93 100644 --- a/finn_xsi/finn_xsi/include/SimulationInterface.hpp +++ b/finn_xsi/finn_xsi/include/SimulationInterface.hpp @@ -1,5 +1,5 @@ -#ifndef SIMULATION_INTERFACE -#define SIMULATION_INTERFACE +#ifndef SIMULATIONINTERFACE +#define SIMULATIONINTERFACE #include #include #include @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -176,12 +177,20 @@ class SimulationInterface { bool readFromLastNode(bool consumerReady) requires(T == SimulationInterfaceType::CONSUMING) { - // The predecessor must always be one cycle ahead of the successor side - // Wait until predecessor catches up (and overtakes) - while (sharedData->predCycle <= sharedData->succCycle) {} - sharedData->ready = consumerReady; - ++(sharedData->succCycle); - return sharedData->valid; + // Use relaxed loads in the spin loop, acquire only when exiting + while (sharedData->predCycle.load(boost::memory_order_relaxed) <= sharedData->succCycle.load(boost::memory_order_relaxed)) { +// CPU hint for spin-wait +#if defined(__x86_64__) || defined(_M_X64) + __builtin_ia32_pause(); +#elif defined(__aarch64__) + asm volatile("yield" ::: "memory"); +#endif + } + boost::atomic_thread_fence(boost::memory_order_acquire); + + sharedData->ready.store(consumerReady, boost::memory_order_release); + sharedData->succCycle.fetch_add(1, boost::memory_order_release); + return sharedData->valid.load(boost::memory_order_acquire); } /** @@ -189,15 +198,24 @@ class SimulationInterface { * Returns the ready signal read from shm. * Called on producer side. */ - bool writeToNextNode(bool producerValid) + bool writeToNextNode(bool producerValid, unsigned int cycle) requires(T == SimulationInterfaceType::PRODUCING) { // The predecessor side must always be at least one cycle ahead of the output side // Wait until output catches up - while (sharedData->succCycle != sharedData->predCycle) {} - sharedData->valid = producerValid; - ++(sharedData->predCycle); - return sharedData->ready; + while (sharedData->succCycle.load(boost::memory_order_relaxed) != sharedData->predCycle.load(boost::memory_order_relaxed)) { + // CPU hint for spin-wait +#if defined(__x86_64__) || defined(_M_X64) + __builtin_ia32_pause(); +#elif defined(__aarch64__) + asm volatile("yield" ::: "memory"); +#endif + } + boost::atomic_thread_fence(boost::memory_order_acquire); + + sharedData->valid.store(producerValid, boost::memory_order_release); + sharedData->predCycle.store(cycle, boost::memory_order_release); + return sharedData->ready.load(boost::memory_order_acquire); } }; -#endif +#endif /* SIMULATIONINTERFACE */ diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp index eff415e3ef..a9550a409a 100644 --- a/finn_xsi/finn_xsi/src/FIFO.cpp +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -1,7 +1,8 @@ #include + #include -FIFO::FIFO(std::size_t max_size) : util(0), max_util(0), max_size(max_size) {} +FIFO::FIFO(std::size_t p_max_size) : util(0), max_util(0), max_size(p_max_size) {} FIFO::~FIFO() {} @@ -10,10 +11,13 @@ bool FIFO::is_valid() { --util; return true; } - return false; + return util > 0; } -void FIFO::ready(bool ready) { sucReady = ready; } +void FIFO::ready(bool ready) { + sucReady = ready; + readyCtr = ready ? readyCtr + 1 : readyCtr; +} bool FIFO::is_ready() const { return util < max_size; } diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py index 50aa88e84c..4eb604a2c0 100644 --- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py +++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py @@ -1,3 +1,9 @@ +"""Create stitched IP from FINN dataflow graph. + +This module provides transformations to create a Vivado IP Block Design project +from generated IPs in a FINN dataflow graph. +""" + # Copyright (c) 2020, Xilinx, Inc. # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -30,48 +36,53 @@ import json import multiprocessing as mp import os +from pathlib import Path +from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.util.basic import get_num_default_workers from shutil import copytree from subprocess import CalledProcessError +from typing import TYPE_CHECKING, Literal, cast + +if TYPE_CHECKING: + from onnx import NodeProto from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.transformation.fpgadataflow.replace_verilog_relpaths import ReplaceVerilogRelPaths from finn.util.basic import launch_process_helper, make_build_dir -from finn.util.exception import FINNError, FINNInternalError, FINNUserError +from finn.util.exception import FINNInternalError, FINNUserError from finn.util.fpgadataflow import is_hls_node, is_rtl_node from finn.util.logging import log -def is_external_input(model, node, i): - # indicate whether input i of node should be made external - # True only if input is unconnected and has no initializer - # Only esception is second input of FC layers when mem_mode is external +def is_external_input(model: ModelWrapper, node: "NodeProto", i: int) -> bool: + """Check if input i of node should be made external. + + Returns True only if input is unconnected and has no initializer. + Exception: second input of FC layers when mem_mode is external. + """ node_inst = getCustomOp(node) op_type = node.op_type producer = model.find_producer(node.input[i]) if producer is None: if model.get_initializer(node.input[i]) is None: return True - else: - if op_type.startswith("MVAU"): - if node_inst.get_nodeattr("mem_mode") == "external": - return True + if op_type.startswith("MVAU") and node_inst.get_nodeattr("mem_mode") == "external": + return True return False -def is_external_output(model, node, i): - # indicate whether output i of node should be made external - # True only if output is unconnected +def is_external_output(model: ModelWrapper, node: "NodeProto", i: int) -> bool: + """Check if output i of node should be made external. + + Returns True only if output is unconnected. + """ + # TODO should ideally check if tensor is in top-level outputs consumers = model.find_consumers(node.output[i]) - if consumers == []: - # TODO should ideally check if tensor is in top-level - # outputs - return True - return False + return consumers == [] class CreateStitchedIP(Transformation): @@ -90,13 +101,25 @@ class CreateStitchedIP(Transformation): def __init__( self, - fpgapart, - clk_ns, - ip_name="finn_design", - vitis=False, - signature=[], - functional_simulation=False, - ): + fpgapart: str, + clk_ns: float, + ip_name: str = "finn_design", + vitis: bool = False, + signature: list | None = None, + functional_simulation: bool = False, + ) -> None: + """Initialize CreateStitchedIP transformation. + + Args: + fpgapart: FPGA part identifier + clk_ns: Clock period in nanoseconds + ip_name: Name for the IP design + vitis: Whether to target Vitis + signature: Optional signature list [customer, application, version] + functional_simulation: Whether to generate functional simulation wrapper + """ + if signature is None: + signature = [] super().__init__() self.fpgapart = fpgapart self.clk_ns = clk_ns @@ -123,16 +146,19 @@ def __init__( "axilite": [], } - def is_double_pumped(self, node): + def is_double_pumped(self, node: "NodeProto") -> bool: + """Check if node uses double-pumped compute or memory.""" if node.op_type.startswith("MVAU"): inst = getCustomOp(node) try: - pumped_compute = inst.get_nodeattr("pumpedCompute") + pumped_compute = cast("int", inst.get_nodeattr("pumpedCompute")) except AttributeError: pumped_compute = 0 - return pumped_compute or inst.get_nodeattr("pumpedMemory") + return bool(pumped_compute or cast("int", inst.get_nodeattr("pumpedMemory"))) + return False - def connect_clk_rst(self, node): + def connect_clk_rst(self, node: "NodeProto") -> None: + """Connect clock and reset signals for a node.""" inst_name = node.name node_inst = getCustomOp(node) if not isinstance(node_inst, HWCustomOp): @@ -186,7 +212,8 @@ def connect_clk_rst(self, node): f"[get_bd_pins {inst_name}/{clock2x_intf_name}]" ) - def connect_axi(self, node): + def connect_axi(self, node: "NodeProto") -> None: + """Connect AXI-Lite and AXI-MM interfaces for a node.""" inst_name = node.name node_inst = getCustomOp(node) if not isinstance(node_inst, HWCustomOp): @@ -198,8 +225,7 @@ def connect_axi(self, node): if len(axilite_intf_name) != 0: self.connect_cmds.append( - f"make_bd_intf_pins_external " - f"[get_bd_intf_pins {inst_name}/{axilite_intf_name[0]}]" + f"make_bd_intf_pins_external [get_bd_intf_pins {inst_name}/{axilite_intf_name[0]}]" ) ext_if_name = f"{axilite_intf_name[0]}_{len(self.intf_names['axilite'])}" self.intf_names["axilite"].append(ext_if_name) @@ -222,7 +248,8 @@ def connect_axi(self, node): self.intf_names["aximm"] = [(ext_if_name, aximm_intf_name[0][1])] self.has_aximm = True - def connect_m_axis_external(self, node, idx=None): + def connect_m_axis_external(self, node: "NodeProto", idx: int | None = None) -> None: + """Make AXI Stream master interface(s) external.""" inst_name = node.name node_inst = getCustomOp(node) if not isinstance(node_inst, HWCustomOp): @@ -249,7 +276,8 @@ def connect_m_axis_external(self, node, idx=None): self.intf_names["m_axis"].append((f"m_axis_{self.m_axis_idx}", output_intf_names[i][1])) self.m_axis_idx += 1 - def connect_s_axis_external(self, node, idx=None): + def connect_s_axis_external(self, node: "NodeProto", idx: int | None = None) -> None: + """Make AXI Stream slave interface(s) external.""" inst_name = node.name node_inst = getCustomOp(node) if not isinstance(node_inst, HWCustomOp): @@ -276,7 +304,8 @@ def connect_s_axis_external(self, node, idx=None): self.intf_names["s_axis"].append((f"s_axis_{self.s_axis_idx}", input_intf_names[i][1])) self.s_axis_idx += 1 - def connect_ap_none_external(self, node): + def connect_ap_none_external(self, node: "NodeProto") -> None: + """Make ap_none interfaces external.""" inst_name = node.name node_inst = getCustomOp(node) if not isinstance(node_inst, HWCustomOp): @@ -295,7 +324,8 @@ def connect_ap_none_external(self, node): ] ) - def insert_signature(self, checksum_count): + def insert_signature(self, checksum_count: int) -> None: + """Insert AXI info signature component into the design.""" signature_vlnv = "AMD:user:axi_info_top:1.0" signature_name = "axi_info_top0" fclk_mhz = 1 / (self.clk_ns * 0.001) @@ -329,7 +359,8 @@ def insert_signature(self, checksum_count): ] ) - def apply(self, model): + def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: + """Apply the CreateStitchedIP transformation to the model.""" # ensure non-relative readmemh .dat files model = model.transform(ReplaceVerilogRelPaths()) ip_dirs = ["list"] @@ -337,7 +368,10 @@ def apply(self, model): ip_dirs.append("$::env(FINN_RTLLIB)/memstream") if self.signature: ip_dirs.append("$::env(FINN_RTLLIB)/axi_info") - if model.graph.node[0].op_type not in ["StreamingFIFO_rtl", "IODMA_hls"]: + if ( + model.graph.node[0].op_type not in ["StreamingFIFO_rtl", "IODMA_hls"] + and self.functional_simulation is False + ): log.warning( """First node is not StreamingFIFO or IODMA. You may experience incorrect stitched-IP rtlsim or hardware @@ -367,7 +401,7 @@ def apply(self, model): ip_dir_value = node_inst.get_nodeattr("ip_path") if type(ip_dir_value) is not str or ip_dir_value == "": raise FINNInternalError(f"ip_path has the wrong type in node {node.name}.") - if not os.path.isdir(ip_dir_value): + if not Path(ip_dir_value).is_dir(): raise FINNInternalError( f"IP generation directory doesn't exist in node {node.name}." ) @@ -396,8 +430,8 @@ def apply(self, model): ) # process external inputs and outputs in top-level graph input order - for input in model.graph.input: - inp_name = input.name + for graph_input in model.graph.input: + inp_name = graph_input.name inp_cons = model.find_consumers(inp_name) assert inp_cons != [], f"No consumer for input {inp_name}" assert len(inp_cons) == 1, f"Multiple consumers for input {inp_name}" @@ -593,7 +627,7 @@ def apply(self, model): ] ) # add a rudimentary driver mdd to get correct ranges in xparameters.h later on - example_data_dir = os.path.join(os.environ["FINN_QNN_DATA"], "mdd-data") + example_data_dir = Path(os.environ["FINN_QNN_DATA"]) / "mdd-data" copytree(example_data_dir, f"{vivado_stitch_proj_dir}/data") ##### @@ -678,8 +712,8 @@ def apply(self, model): tcl.extend( [ "set all_v_files [get_files -filter {USED_IN_SYNTHESIS == 1 " - + "&& (FILE_TYPE == Verilog || FILE_TYPE == SystemVerilog " - + '|| FILE_TYPE =="Verilog Header")}]', + "&& (FILE_TYPE == Verilog || FILE_TYPE == SystemVerilog " + '|| FILE_TYPE =="Verilog Header")}]', f"set fp [open {v_file_list} w]", "foreach vf $all_v_files {puts $fp $vf}", "close $fp", @@ -687,12 +721,12 @@ def apply(self, model): ) # write the project creator tcl script tcl_string = "\n".join(tcl) + "\n" - with open(f"{vivado_stitch_proj_dir}/make_project.tcl", "w") as f: + with Path(f"{vivado_stitch_proj_dir}/make_project.tcl").open("w") as f: f.write(tcl_string) # create a shell script and call Vivado make_project_sh = f"{vivado_stitch_proj_dir}/make_project.sh" - working_dir = os.getcwd() - with open(make_project_sh, "w") as f: + working_dir = Path.cwd() + with Path(make_project_sh).open("w") as f: f.write("#!/bin/bash \n") f.write(f"cd {vivado_stitch_proj_dir}\n") f.write("vivado -mode batch -source make_project.tcl\n") @@ -701,23 +735,26 @@ def apply(self, model): try: launch_process_helper(bash_command, print_stdout=False) - except CalledProcessError: - # Check success manually by looking for wrapper HDL - pass + except CalledProcessError as e: + raise FINNUserError( + f"CreateStitchedIP: make_project.sh failed with a non-zero " + f"exit code. Check previous logs and logs in " + f"{vivado_stitch_proj_dir} to find out why it failed." + ) from e if self.functional_simulation: - with open(v_file_list, "a") as f: + with Path(v_file_list).open("a") as f: f.write(f"{fifosim_wrapper_filename}\n") # wrapper may be created in different location depending on Vivado version - if not os.path.isfile(wrapper_filename): + if not Path(wrapper_filename).is_file(): # check in alternative location (.gen instead of .srcs) wrapper_filename_alt = wrapper_filename.replace(".srcs", ".gen") - if os.path.isfile(wrapper_filename_alt): + if Path(wrapper_filename_alt).is_file(): if not self.functional_simulation: model.set_metadata_prop("wrapper_filename", wrapper_filename_alt) else: - raise FINNError( + raise FINNUserError( f"""CreateStitchedIP failed, no wrapper HDL found \ under {wrapper_filename} or {wrapper_filename_alt}. Please check logs under the parent directory.""" diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index add5b1f47e..7450812a6c 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -403,7 +403,7 @@ def _build_single_node_simulation( if wrapper_filename is None or not Path(wrapper_filename).exists(): raise FINNUserError( f"Call CreateStitchedIP prior to building " - f"the simulation for {node_name}." + f"the simulation for {node_name}. " f"wrapper_filename is set to {wrapper_filename}!" ) From 1873576f7869ea0a96d31e28839448bb311cabd1 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:39:21 +0100 Subject: [PATCH 029/170] New shared memory implementation --- .../include/InterSimulationInterface.hpp | 187 +++++++++++++++ finn_xsi/finn_xsi/include/Simulation.hpp | 19 +- .../finn_xsi/include/SimulationInterface.hpp | 221 ------------------ 3 files changed, 192 insertions(+), 235 deletions(-) create mode 100644 finn_xsi/finn_xsi/include/InterSimulationInterface.hpp delete mode 100644 finn_xsi/finn_xsi/include/SimulationInterface.hpp diff --git a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp new file mode 100644 index 0000000000..77be1d5a05 --- /dev/null +++ b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp @@ -0,0 +1,187 @@ +#ifndef INTERSIMULATIONINTERFACE +#define INTERSIMULATIONINTERFACE + +#include +#include +#include +#include +#include + +#ifdef __cpp_lib_hardware_interference_size +constexpr std::size_t CACHE_LINE_SIZE = std::hardware_destructive_interference_size; +#else +constexpr std::size_t CACHE_LINE_SIZE = 64; +#endif + +namespace bip = boost::interprocess; + +template +class InterSimulationInterface { + private: + // ===== SHARED MEMORY STRUCTURE ===== + // This goes into shared memory and is accessible from both processes + struct alignas(CACHE_LINE_SIZE) SharedHaloExchange { + struct alignas(CACHE_LINE_SIZE) BufferSlot { + std::atomic value; + std::atomic ready; // Ready flag for Halo Exchange NOT Simulation + + // Must explicitly initialize atomics in shared memory + BufferSlot() : value(false), ready(false) {} + }; + + // Linearized buffers: [process_id * 2 + buffer_id] + BufferSlot buffers[4]; + alignas(CACHE_LINE_SIZE) std::atomic current_buffer; + alignas(CACHE_LINE_SIZE) std::atomic flip_barrier; + + SharedHaloExchange() : current_buffer(0), flip_barrier(0) { + // Verify atomics are lock-free (required for shared memory) + static_assert(std::atomic::is_always_lock_free, "std::atomic must be lock-free for inter-process use"); + static_assert(std::atomic::is_always_lock_free, "std::atomic must be lock-free for inter-process use"); + } + + static constexpr int idx(int proc_id, int buf_id) { return proc_id * 2 + buf_id; } + }; + + // ===== PROCESS-LOCAL STATE ===== + // This is NOT in shared memory - each process has its own copy + template + struct ProcessLocalState { + int expected_buf; + bool first_call; + constexpr static int process_id = IsReceiver ? 1 : 0; // 0 or 1 + ProcessLocalState() : expected_buf(0), first_call(true) {} + }; + + SharedHaloExchange* halo = nullptr; + std::atomic* refCount = nullptr; + const std::string sharedMemoryName; + bip::managed_shared_memory shmem; + // Create process-local state + ProcessLocalState local; + + public: + // Default constructor needed for std::array + InterSimulationInterface() : sharedMemoryName("") { + // Uninitialized - will be move-assigned later + } + + InterSimulationInterface(const std::string& shmName) : sharedMemoryName(shmName) { + if constexpr (Receiver) { + bip::shared_memory_object::remove(sharedMemoryName.c_str()); + shmem = bip::managed_shared_memory(bip::create_only, sharedMemoryName.c_str(), SharedMemorySize); + } else { + while (true) { + try { + shmem = bip::managed_shared_memory(bip::open_only, sharedMemoryName.c_str()); + break; + } catch (const bip::interprocess_exception& e) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + } + } + + // Construct or find the reference counter (separate from SharedData) + refCount = shmem.find_or_construct>("refCount")(0); + + // Increment reference count atomically + int currentRefCount = refCount->fetch_add(1, std::memory_order_acq_rel) + 1; + + // Construct the halo exchange object in shared memory + halo = shmem.find_or_construct("HaloExchange")(); + } + + // Delete copy operations + InterSimulationInterface(const InterSimulationInterface&) = delete; + InterSimulationInterface& operator=(const InterSimulationInterface&) = delete; + + // Move constructor + InterSimulationInterface(InterSimulationInterface&& other) noexcept : halo(other.halo), refCount(other.refCount), sharedMemoryName(std::move(other.sharedMemoryName)), shmem(std::move(other.shmem)) { + // Mark other as moved-from + other.halo = nullptr; + other.refCount = nullptr; + } + + // Move assignment operator + InterSimulationInterface& operator=(InterSimulationInterface&& other) noexcept { + if (this != &other) { + halo = other.halo; + refCount = other.refCount; + // Note: managed_shared_memory has deleted assignment, use swap + shmem.swap(other.shmem); + const_cast(sharedMemoryName) = std::move(other.sharedMemoryName); + + // Mark other as moved-from + other.halo = nullptr; + other.refCount = nullptr; + } + return *this; + } + + ~InterSimulationInterface() { + // Skip cleanup if moved-from or default-constructed + if (!refCount || !halo) { + return; + } + + // Decrement reference count atomically + int remainingRefs = refCount->fetch_sub(1, std::memory_order_acq_rel) - 1; + + // If we're the last process, clean up the shared memory + if (remainingRefs == 0) { + shmem.destroy("HaloExchange"); + shmem.destroy>("refCount"); + + // Remove the shared memory segment completely + bip::shared_memory_object::remove(sharedMemoryName.c_str()); + } + } + + // ===== EXCHANGE FUNCTION ===== + // This function runs in each process + bool exchange(bool send_value) { + constexpr int neighbor_id = 1 - this->local.process_id; + + // Wait for previous buffer flip (latency hiding) + if (!local.first_call) { + while (this->halo->current_buffer.load(std::memory_order_acquire) % 2 == local.expected_buf) { +#if defined(__x86_64__) || defined(_M_X64) + __builtin_ia32_pause(); +#endif + } + } + local.first_call = false; + + int buf_id = this->halo->current_buffer.load(std::memory_order_acquire) % 2; + + // Write our data + int my_idx = SharedHaloExchange::idx(this->local.process_id, buf_id); + this->halo->buffers[my_idx].value.store(send_value, std::memory_order_release); + this->halo->buffers[my_idx].ready.store(true, std::memory_order_release); + + // Wait for neighbor + int neighbor_idx = SharedHaloExchange::idx(neighbor_id, buf_id); + bool neighbor_ready = this->halo->buffers[neighbor_idx].ready.load(std::memory_order_acquire); + if (!neighbor_ready) { + while (!this->halo->buffers[neighbor_idx].ready.load(std::memory_order_acquire)) { +#if defined(__x86_64__) || defined(_M_X64) + __builtin_ia32_pause(); +#endif + } + } + + bool received = this->halo->buffers[neighbor_idx].value.load(std::memory_order_acquire); + + // Flip barrier + if (this->halo->flip_barrier.fetch_add(1, std::memory_order_acq_rel) == 1) { + // First process: flip the buffer + int old_buf = buf_id; + this->halo->buffers[SharedHaloExchange::idx(0, old_buf)].ready.store(false, std::memory_order_relaxed); + this->halo->buffers[SharedHaloExchange::idx(1, old_buf)].ready.store(false, std::memory_order_relaxed); + this->halo->current_buffer.fetch_add(1, std::memory_order_release); + this->halo->flip_barrier.store(0, std::memory_order_release); + } + + local.expected_buf = buf_id; + return received; + } +}; +#endif /* INTERSIMULATIONINTERFACE */ diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 2752d6b075..621a39e235 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -9,16 +9,7 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include #include #include #include @@ -106,8 +97,8 @@ class Simulation { template class SingleNodeSimulation : public Simulation { private: - using ConsumingInterface = SimulationInterface; - using ProducingInterface = SimulationInterface; + using ConsumingInterface = InterSimulationInterface; + using ProducingInterface = InterSimulationInterface; std::array fromProducerInterface; std::array toConsumerInterface; std::array fifo; @@ -166,14 +157,14 @@ class SingleNodeSimulation : public Simulationfifo[i].write(this->ostreams[i].is_valid()); this->ostreams[i].ready(this->fifo[i].is_ready()); // Interface FIFO <-> SHM - this->fifo[i].ready(toConsumerInterface[i].writeToNextNode(this->fifo[i].is_valid(), static_cast(cyclesRun))); + this->fifo[i].ready(toConsumerInterface[i].exchange(this->fifo[i].is_valid())); } } if constexpr (NodeIndex != 0 && CommunicatesWithPredecessor) { for (std::size_t i = 0; i < IStreamsSize; ++i) { // Interface SHM <-> sim - this->istreams[i].valid(fromProducerInterface[i].readFromLastNode(this->istreams[i].is_ready())); + this->istreams[i].valid(fromProducerInterface[i].exchange(this->istreams[i].is_ready())); } } } diff --git a/finn_xsi/finn_xsi/include/SimulationInterface.hpp b/finn_xsi/finn_xsi/include/SimulationInterface.hpp deleted file mode 100644 index 8b88080e93..0000000000 --- a/finn_xsi/finn_xsi/include/SimulationInterface.hpp +++ /dev/null @@ -1,221 +0,0 @@ -#ifndef SIMULATIONINTERFACE -#define SIMULATIONINTERFACE -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __cpp_lib_hardware_interference_size -using std::hardware_destructive_interference_size; -#else -constexpr std::size_t hardware_destructive_interference_size = 64; -#endif - -namespace ipc = boost::interprocess; - -enum class SimulationInterfaceType { PRODUCING, CONSUMING }; -constexpr std::string_view to_string(SimulationInterfaceType t) { - if (t == SimulationInterfaceType::CONSUMING) { - return "CONSUMING"; - } else if (t == SimulationInterfaceType::PRODUCING) { - return "PRODUCING"; - } - return "UNKNOWN SIMULATION INTERFACE TYPE"; -} - -template -class SimulationInterface { - private: - // Shared memory structure with proper cache-line alignment - struct SharedData { - alignas(hardware_destructive_interference_size) boost::ipc_atomic predCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic succCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic ready; - alignas(hardware_destructive_interference_size) boost::ipc_atomic valid; - - SharedData() : predCycle(0), succCycle(0), ready(false), valid(false) {} - SharedData(unsigned int predecessorCycle, unsigned int successorCycle, bool inReady, bool outValid) : predCycle(predecessorCycle), succCycle(successorCycle), ready(inReady), valid(outValid) {} - SharedData(const SharedData& other) : predCycle(other.predCycle.load()), succCycle(other.succCycle.load()), ready(other.ready.load()), valid(other.valid.load()) {} - SharedData& operator=(const SharedData& other) { - predCycle.store(other.predCycle.load()); - succCycle.store(other.succCycle.load()); - ready.store(other.ready.load()); - valid.store(other.valid.load()); - return *this; - } - }; - - SharedData* sharedData = nullptr; - boost::ipc_atomic* refCount = nullptr; - const std::string shmIdentifier; - ipc::managed_shared_memory shmem; - -#ifdef NDEBUG - [[maybe_unused]] void simInterfaceDebug([[maybe_unused]] std::string_view s) {} -#else - /// Log the given text with a header identifying the shared memory region and the interface type - void simInterfaceDebug(std::string_view s) { debug(std::format("{} ({}): {}", shmIdentifier, to_string(T), s)); } -#endif - - public: - // Default constructor needed for std::array - SimulationInterface() : shmIdentifier("") { - // Uninitialized - will be move-assigned later - } - - SimulationInterface(const char* _shmIdentifier) : shmIdentifier(_shmIdentifier) { - simInterfaceDebug("Creating simulation interface."); - if (T == SimulationInterfaceType::PRODUCING) { - ipc::shared_memory_object::remove(_shmIdentifier); - simInterfaceDebug("Removed previous shared memory objects."); - shmem = ipc::managed_shared_memory(ipc::create_only, _shmIdentifier, ShmemSize); - } else { - while (true) { - try { - shmem = ipc::managed_shared_memory(ipc::open_only, _shmIdentifier); - break; - } catch (const ipc::interprocess_exception& e) { simInterfaceDebug("Producer shared memory not yet created. Waiting.."); } - } - } - simInterfaceDebug("Shared memory constructed or found."); - - // Construct or find the reference counter (separate from SharedData) - refCount = shmem.find_or_construct>("refCount")(0); - - // Increment reference count atomically - int currentRefCount = refCount->fetch_add(1, boost::memory_order_acq_rel) + 1; - simInterfaceDebug(std::format("Reference count incremented to {}", currentRefCount)); - - // Construct or find the entire SharedData struct in shared memory - sharedData = shmem.find_or_construct("data")(SharedData(0, 0, true, false)); - simInterfaceDebug("Shared data structure constructed or found."); - } - - // Delete copy operations - SimulationInterface(const SimulationInterface&) = delete; - SimulationInterface& operator=(const SimulationInterface&) = delete; - - // Move constructor - SimulationInterface(SimulationInterface&& other) noexcept : sharedData(other.sharedData), refCount(other.refCount), shmIdentifier(std::move(other.shmIdentifier)), shmem(std::move(other.shmem)) { - // Mark other as moved-from - other.sharedData = nullptr; - other.refCount = nullptr; - } - - // Move assignment operator - SimulationInterface& operator=(SimulationInterface&& other) noexcept { - if (this != &other) { - sharedData = other.sharedData; - refCount = other.refCount; - // Note: managed_shared_memory has deleted assignment, use swap - shmem.swap(other.shmem); - const_cast(shmIdentifier) = std::move(other.shmIdentifier); - - // Mark other as moved-from - other.sharedData = nullptr; - other.refCount = nullptr; - } - return *this; - } - - ~SimulationInterface() { - // Skip cleanup if moved-from or default-constructed - if (!refCount || !sharedData) { - return; - } - - // Decrement reference count atomically - int remainingRefs = refCount->fetch_sub(1, boost::memory_order_acq_rel) - 1; - simInterfaceDebug(std::format("Reference count decremented to {}", remainingRefs)); - - // If we're the last process, clean up the shared memory - if (remainingRefs == 0) { - simInterfaceDebug("Last process exiting - cleaning up shared memory"); - shmem.destroy("data"); - shmem.destroy>("refCount"); - - // Remove the shared memory segment completely - ipc::shared_memory_object::remove(shmIdentifier.c_str()); - simInterfaceDebug("Shared memory cleaned up successfully"); - } else { - simInterfaceDebug("Other processes still using shared memory - detaching only"); - } - } - - /// Reset all interface data fields to their defaults - void reset() { - simInterfaceDebug("Resetting simulation interface"); - sharedData->ready.store(true, boost::memory_order_release); - sharedData->predCycle.store(0, boost::memory_order_release); - sharedData->valid.store(false, boost::memory_order_release); - sharedData->succCycle.store(0, boost::memory_order_release); - } - - /** - * Reads valid from shm and puts ready into shm. - * Returns the valid signal read from shm. - * Called on consumer side. - */ - bool readFromLastNode(bool consumerReady) - requires(T == SimulationInterfaceType::CONSUMING) - { - // Use relaxed loads in the spin loop, acquire only when exiting - while (sharedData->predCycle.load(boost::memory_order_relaxed) <= sharedData->succCycle.load(boost::memory_order_relaxed)) { -// CPU hint for spin-wait -#if defined(__x86_64__) || defined(_M_X64) - __builtin_ia32_pause(); -#elif defined(__aarch64__) - asm volatile("yield" ::: "memory"); -#endif - } - boost::atomic_thread_fence(boost::memory_order_acquire); - - sharedData->ready.store(consumerReady, boost::memory_order_release); - sharedData->succCycle.fetch_add(1, boost::memory_order_release); - return sharedData->valid.load(boost::memory_order_acquire); - } - - /** - * Reads ready from shm and puts valid into shm. - * Returns the ready signal read from shm. - * Called on producer side. - */ - bool writeToNextNode(bool producerValid, unsigned int cycle) - requires(T == SimulationInterfaceType::PRODUCING) - { - // The predecessor side must always be at least one cycle ahead of the output side - // Wait until output catches up - while (sharedData->succCycle.load(boost::memory_order_relaxed) != sharedData->predCycle.load(boost::memory_order_relaxed)) { - // CPU hint for spin-wait -#if defined(__x86_64__) || defined(_M_X64) - __builtin_ia32_pause(); -#elif defined(__aarch64__) - asm volatile("yield" ::: "memory"); -#endif - } - boost::atomic_thread_fence(boost::memory_order_acquire); - - sharedData->valid.store(producerValid, boost::memory_order_release); - sharedData->predCycle.store(cycle, boost::memory_order_release); - return sharedData->ready.load(boost::memory_order_acquire); - } -}; -#endif /* SIMULATIONINTERFACE */ From 7dae9d9c6643586c5b16cda235a39a9562a38a00 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 18 Nov 2025 14:54:21 +0100 Subject: [PATCH 030/170] Remove unused include --- finn_xsi/finn_xsi/include/InterSimulationInterface.hpp | 1 - 1 file changed, 1 deletion(-) diff --git a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp index 77be1d5a05..90a392c0d3 100644 --- a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp +++ b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp @@ -3,7 +3,6 @@ #include #include -#include #include #include From a07de486233eab4c3f5c3451d1f9d3e78d353c0b Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 19 Nov 2025 08:58:17 +0100 Subject: [PATCH 031/170] Updated communicating node by node simulation --- finn_xsi/finn_xsi/include/FIFO.h | 38 ++- finn_xsi/finn_xsi/include/Simulation.hpp | 228 ++++++++++-------- .../finn_xsi/include/SimulationInterface.hpp | 4 +- finn_xsi/finn_xsi/src/FIFO.cpp | 75 +++++- .../transformation/fpgadataflow/simulation.py | 2 +- .../fpgadataflow/simulation_controller.py | 146 ++++++----- 6 files changed, 322 insertions(+), 171 deletions(-) diff --git a/finn_xsi/finn_xsi/include/FIFO.h b/finn_xsi/finn_xsi/include/FIFO.h index 2a29c2e9a2..42c6c31ca8 100644 --- a/finn_xsi/finn_xsi/include/FIFO.h +++ b/finn_xsi/finn_xsi/include/FIFO.h @@ -3,25 +3,49 @@ #include #include - +#include +#include +/* class FIFO { std::size_t currentUtil = 0; std::size_t maxUtil = 0; std::size_t maxSize = 0; - bool sucReady = false; public: + FIFO(std::size_t maxSize = std::numeric_limits::max()); ~FIFO(); // Add FIFO methods and members as needed - bool isValid(); - void ready(bool ready); - bool isReady() const; - void write(bool valid); - std::size_t getLargestOccupation() const; + bool isEmpty() const; + void tryPushPop(bool incomingValid, bool successorReady); + void tryPop(bool successorReady); + bool isOutputValid() const; + bool isInputReady() const; + void tryPush(bool incomingValid); void reset(); void setMaxSize(std::size_t newSize); +};*/ + +// TODO: switch int to std::size_t + +class FIFO { + int maxUtil = 0; + int currentUtil = 0; + int maxSize = 0; + int nextUtil = 0; + + public: + FIFO(int size = std::numeric_limits::max()); + ~FIFO(); + + void update(bool incomingValid, bool outgoingReady); + void toggleClock(); + bool isInputReady() const; + bool isOutputValid() const; + bool isEmpty() const; + void reset(std::optional size = std::nullopt); + void setMaxSize(int size); }; #endif /* FIFO_H */ diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 6ffa7c235e..37dcedb93f 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -120,10 +119,13 @@ class _SingleNodeSimulation : public Simulation; std::array fromProducerInterface; std::array toConsumerInterface; - std::array fifo; std::size_t cyclesRun = 0; + public: + // TODO: Move to private, currently only here for debugging purposes + std::array(OStreamsSize)> fifo; + _SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, std::array _ostream_descs, std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { @@ -149,20 +151,15 @@ class _SingleNodeSimulation : public Simulation FIFO - this->fifo[i].write(this->ostreams[i].isValid()); - this->ostreams[i].ready(this->fifo[i].isReady()); - // Interface FIFO <-> SHM - this->fifo[i].ready(toConsumerInterface[i].writeToNextNode(this->fifo[i].isValid())); - } - } - if constexpr (NodeIndex != 0 && CommunicatesWithPredecessor) { + if constexpr (NodeIndex != 0) { for (std::size_t i = 0; i < IStreamsSize; ++i) { // Interface SHM <-> sim this->istreams[i].valid(fromProducerInterface[i].readFromLastNode(this->istreams[i].isReady())); } } + if constexpr (NodeIndex != TotalNodes - 1) { + for (std::size_t i = 0; i < OStreamsSize; ++i) { + this->fifo[i].update( + this->ostreams[i].isValid(), + toConsumerInterface[i].writeToNextNode(this->fifo[i].isOutputValid()) + ); + this->fifo[i].toggleClock(); + } + } } public: @@ -194,41 +191,18 @@ class _SingleNodeSimulation : public Simulationistreams) { // Input into sim valid - s.valid(); + s.valid(true); } } else if constexpr (NodeIndex == TotalNodes - 1) { // Last Node; no successor for (auto&& s : this->ostreams) { // Output from sim ready - s.ready(); - } - for (std::size_t i = 0; i < IStreamsSize; ++i) { // Relay ready from sim to predecessor - fromProducerInterface[i].readFromLastNode(this->istreams[i].isReady()); - } - } else { // Intermediate Node; has both predecessor and successor - for (std::size_t i = 0; i < OStreamsSize; ++i) { // Relay ready from FIFO to sim - this->ostreams[i].ready(this->fifo[i].isReady()); - } - for (std::size_t i = 0; i < IStreamsSize; ++i) { // Relay valid from sim to predecessor - fromProducerInterface[i].readFromLastNode(this->istreams[i].isReady()); + s.ready(true); } } } /// Write the current ready and valid states into the filestreams - void logReadyValidState() requires LoggingEnabled { - // Log the signals that this simulations set (ready to predecessor, valid to successor) - // TODO: Collect signals in vectors and only write to file after the sim for speedup - for (S_AXIS_Control& stream : this->istreams) { - this->readyLog << stream.isReady() << " "; - } - this->readyLog << "\n"; - for (M_AXIS_Control& stream : this->ostreams) { - this->validLog << stream.isValid() << " "; - } - this->validLog << "\n"; - } - + void logReadyValidState() requires LoggingEnabled { /* TODO: Match the new extra FIFO implementation*/ } - public: /// Reset simulation (stream and current FIFO depth, as well as cycle counter) void reset() { Simulation::reset(); @@ -236,14 +210,27 @@ class _SingleNodeSimulation : public Simulationfifo[i].tryPop(toConsumerInterface[i].writeToNextNode(this->fifo[i].isOutputValid())); + // } + runSingleCycle(); + } } [[gnu::hot]] void runSingleCycle() { @@ -272,9 +259,15 @@ class _SingleNodeSimulation : public Simulationostreams[outputIndex].job_size; } + + /// Get the job size of the specified input stream + std::size_t getInputJobSize(std::size_t inputIndex = 0) { + return this->istreams[inputIndex].job_size; + } }; + /// Single Node Simulation, thread controlled template class SingleNodeSimulation { @@ -286,12 +279,13 @@ class SingleNodeSimulation { // TODO: Atomic? bool running; - // Run until cyclesTarget are hit + std::size_t samplesTarget; + std::size_t samplesProduced; + std::size_t validCyclesProduced; std::size_t cyclesTarget; - - // Current run cycles counter std::size_t cyclesRun; + // Path on which to store simulation data after stopping std::filesystem::path simulationDataPath; @@ -311,8 +305,11 @@ class SingleNodeSimulation { unsigned int initialFIFODepth = 2, std::string simulationDataFilename = "simulation_data.json" ) : running(false), - cyclesTarget(0), + samplesProduced(0), + samplesTarget(0), + validCyclesProduced(0), cyclesRun(0), + cyclesTarget(0), simulationDataPath(simulationDataFilename), sim(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs, prevNodeName, nodeName, initialFIFODepth) {} @@ -322,17 +319,18 @@ class SingleNodeSimulation { void reset() { running = false; sim.reset(); - cyclesRun = 0; - cyclesTarget = 0; + samplesTarget = 0; + samplesProduced = 0; } /// Write the results of the simulation as a JSON file void writeResults() { json j; - for (std::size_t i = 0; i < OStreamsSize; ++i) { - j["maxOccupation"][std::to_string(i)] = sim.getLargestOccupation(i); - } - j["cyclesRun"] = cyclesRun; + // TODO: Reintroduce + // for (std::size_t i = 0; i < OStreamsSize; ++i) { + // j["maxOccupation"][std::to_string(i)] = sim.getLargestOccupation(i); + // } + //j["cyclesRun"] = outValidCycles; std::ofstream file(simulationDataPath); file << j.dump(4); file.close(); @@ -359,75 +357,113 @@ class SingleNodeSimulation { std::getline(std::cin, buffer); } + + void sendLog(std::string message) { std::cout << "log " << message << std::endl; } + void sendError(std::string message) { std::cout << "error " << message << std::endl; } + void sendEnd() { std::cout << "end" << std::endl; } + void sendCycles() { std::cout << "cycles " << cyclesRun << " " << cyclesTarget << std::endl; } + void sendSamples() { std::cout << "samples " << samplesProduced << " " << samplesTarget << std::endl; } + void sendStarted() { std::cout << "started" << std::endl; } + void sendStopped() { std::cout << "stopped" << std::endl; } + void sendReady() { std::cout << "ready" << std::endl; } + + /// Start both threads. Listen for commands on stdin. void start() { - running = true; simulator = std::jthread([this](std::stop_token stop) { - while (cyclesTarget == 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - std::cout << "log Waiting.... (target: " << cyclesTarget << ")" << std::endl; + /// Prepare and wait for data from the controller + while (samplesTarget == 0 && cyclesTarget == 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } - std::cout << "log Starting running with a target of " << cyclesTarget << " cycles!" << std::endl; - // TODO: Move to communicator thread - std::cout << "started" << std::endl; - while (cyclesRun < cyclesTarget) { - if (!running) { + /// Run the simulation + sendStarted(); + if constexpr(NodeIndex == TotalNodes - 1) { + while ((samplesTarget != 0 && samplesProduced < samplesTarget) || (cyclesTarget != 0 && cyclesRun < cyclesTarget)) { + if (!running) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + sim.runSingleCycle(); + ++cyclesRun; + if (std::all_of(sim.ostreams.begin(), sim.ostreams.end(), [](M_AXIS_Control& s) { return s.isValid(); })) { + ++validCyclesProduced; + // TODO: For all streams + samplesProduced = validCyclesProduced / sim.ostreams[0].job_size; + sendSamples(); + } + } + sendStopped(); + } else { + bool validSeen = false; + while (!running) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); - continue; } - if (stop.stop_requested()) { - return; + while (running) { + sim.runSingleCycle(); + if (!validSeen && sim.ostreams[0].isValid()) { + validSeen = true; + sendLog("First valid sample seen"); + } } - std::cout << "cycles " << cyclesRun << " " << cyclesTarget << std::endl; - sim.runSingleCycle(); - ++cyclesRun; } - // TODO: Move to communicator thread - std::cout << "stopped" << std::endl; + // Finish communicating and sent an update to the controller + sim.finishCommunication(); + sendStopped(); + if constexpr (NodeIndex == TotalNodes - 1) { + sendEnd(); + } }); + communicator = std::jthread([this]() { - std::cout << "ready" << std::endl; + // Run initial sanity checks + if constexpr (NodeIndex == 0) { + if (!std::all_of(sim.istreams.begin(), sim.istreams.end(), [](S_AXIS_Control& stream) { return stream.isValid(); })) { + sendLog("ERROR: First node input is not set to valid!"); + // TODO: Stop + } + } else if constexpr (NodeIndex == TotalNodes - 1) { + if (!std::all_of(sim.ostreams.begin(), sim.ostreams.end(), [](M_AXIS_Control& stream) { return stream.isReady(); })) { + sendLog("ERROR: Last node output is not set to ready!"); + // TODO: Stop + } + } + sendReady(); std::string input = ""; while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); - if (cyclesTarget != 0 && cyclesRun == cyclesTarget) { - std::cout << "end" << std::endl; - return; - } - if constexpr(LoggingEnabled) { - if (running && cyclesRun % 5000 == 1) { - std::cout << "cycles " << cyclesRun << " " << cyclesTarget << std::endl; - } - } + // Parse incoming commands getlineIfAvailable(input); if (input == "") { continue; } auto [command, argument] = splitSpace(input); + + // React to command if (command == "stop") { simulator.request_stop(); writeResults(); // Wait for the file to be fully written std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // Signal python that we are done - std::cout << "end" << std::endl; + sendEnd(); return; } else if (command == "fifodepth") { - unsigned int newDepth = static_cast(std::stoul(argument)); + std::size_t newDepth = static_cast(std::stoul(argument)); running = false; sim.setMaxFIFODepth(newDepth); running = true; - std::cout << "log Set FIFO depth to " << newDepth << std::endl; + sendLog(std::format("Set FIFO depth to {}", newDepth)); } else if (command == "runCycles") { cyclesTarget += static_cast(std::stoul(argument)); running = true; - std::cout << "log Set running with " << cyclesTarget << std::endl; + sendLog(std::format("Set running with {}", cyclesTarget)); } else if (command == "runSamples") { - cyclesTarget += static_cast(std::stoul(argument)) * sim.getOutputJobSize(0); + // TODO: Make generic for any number of streams (max) + samplesTarget += static_cast(std::stoul(argument)); running = true; - std::cout << "log Set running with " << cyclesTarget << std::endl; + sendLog(std::format("Set running with a target of {} samples!", samplesTarget)); } else if (command == "pause") { running = false; } else if (command == "reset") { @@ -437,7 +473,7 @@ class SingleNodeSimulation { } else if (command == "help") { // TODO: Insert here or document separately } else { - std::cout << "log Unknown command " << std::endl; + sendLog("Unknown command."); } } }); diff --git a/finn_xsi/finn_xsi/include/SimulationInterface.hpp b/finn_xsi/finn_xsi/include/SimulationInterface.hpp index 3db882f626..54a19b23df 100644 --- a/finn_xsi/finn_xsi/include/SimulationInterface.hpp +++ b/finn_xsi/finn_xsi/include/SimulationInterface.hpp @@ -83,7 +83,7 @@ class SimulationInterface { SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier), largestOccupation(0) { simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); - if (T == SimulationInterfaceType::PRODUCING || IsIOInterface) { + if (T == SimulationInterfaceType::PRODUCING) { ipc::shared_memory_object::remove(_shmIdentifier); simInterfaceDebug("Removed previous shared memory objects."); shmem = ipc::managed_shared_memory(ipc::create_only, _shmIdentifier, ShmemSize); @@ -205,7 +205,7 @@ class SimulationInterface { { // The predecessor side must always be at least one cycle ahead of the output side // Wait until output catches up - while (sharedData->succCycle != sharedData->predCycle) {} + while (sharedData->succCycle < sharedData->predCycle) {} sharedData->valid = producerValid; ++(sharedData->predCycle); return sharedData->ready; diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp index c4566fdc07..fd2668b162 100644 --- a/finn_xsi/finn_xsi/src/FIFO.cpp +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -1,30 +1,43 @@ #include #include +#include +/* FIFO::FIFO(std::size_t max_size) : currentUtil(0), maxUtil(0), maxSize(max_size) {} FIFO::~FIFO() {} -bool FIFO::isValid() { - if (sucReady && currentUtil > 0) { +bool FIFO::isEmpty() const { + return currentUtil == 0; +} + +void FIFO::tryPushPop(bool incomingValid, bool successorReady) { + int diff = static_cast(incomingValid) - static_cast(successorReady); + if (diff == 1 && currentUtil < maxSize) { + ++currentUtil; + maxUtil = std::max(maxUtil, ++currentUtil); + } else if (diff == -1 && currentUtil > 0) { + --currentUtil; + } + // Otherwise do nothing +} + +void FIFO::tryPop(bool successorReady) { + if (successorReady && currentUtil > 0) { --currentUtil; - return true; } - return false; } -void FIFO::ready(bool ready) { sucReady = ready; } +bool FIFO::isOutputValid() const { return currentUtil > 0; } -bool FIFO::isReady() const { return currentUtil < maxSize; } +bool FIFO::isInputReady() const { return currentUtil < maxSize; } -void FIFO::write(bool valid) { - if (valid && currentUtil < maxSize) { +void FIFO::tryPush(bool incomingValid) { + if (incomingValid && currentUtil < maxSize) { maxUtil = std::max(maxUtil, ++currentUtil); } } -std::size_t FIFO::getLargestOccupation() const { return maxUtil; } - void FIFO::setMaxSize(size_t newSize) { maxSize = newSize; } @@ -32,5 +45,45 @@ void FIFO::setMaxSize(size_t newSize) { void FIFO::reset() { currentUtil = 0; maxUtil = 0; - sucReady = false; +} +*/ + +FIFO::FIFO(int size) : maxSize(size) {} +FIFO::~FIFO() {} + +/// Prepare update for the next clock cycle. +void FIFO::update(bool incomingValid, bool outgoingReady) { + int diff = static_cast(incomingValid) - static_cast(outgoingReady); + if ((currentUtil > 0 && currentUtil < maxSize) || (currentUtil == 0 && diff > 0) || (currentUtil == maxSize && diff < 0)) { + nextUtil = currentUtil += diff; + } +} + +/// Toggle the clock cycle, and update the previously set values. +void FIFO::toggleClock() { + currentUtil = nextUtil; +} + +/// Return whether the FIFO can accept inputs (for the current utilization) +bool FIFO::isInputReady() const { return currentUtil < maxSize; } + +/// Return whether the FIFO can output values (for the current utilization) +bool FIFO::isOutputValid() const { return currentUtil > 0; } + +/// Return whether the FIFO is empty (for the current utilization) +bool FIFO::isEmpty() const { return currentUtil == 0; } + +/// Reset the FIFOs internal state. If size is given, also set maxSize, otherwise keep it. +void FIFO::reset(std::optional size) { + currentUtil = 0; + maxUtil = 0; + if (size) { + maxSize = *size; + } + nextUtil = 0; +} + +/// Set the FIFOs max size +void FIFO::setMaxSize(int size) { + maxSize = size; } diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index aa8ff850ba..f47af43475 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -661,7 +661,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: sim = Simulation(model, self.fpgapart, self.clk_ns, self.cfg.functional_simulation) sys.stdout = sys.stdout.console sys.stderr = sys.stderr.console - sim.simulate_node_connected(5, 1024) + sim.simulate_node_connected(2, 65556) sim.simulate_node_connected(1, 2) sim.simulate_node_connected(1, 20000) sim.simulate_node_connected(10, 20000) diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index 67ba906340..3a12646a45 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -10,7 +10,7 @@ from subprocess import Popen from threading import Lock -from finn.util.basic import get_vivado_root +from finn.util.basic import get_vivado_root, make_build_dir from finn.util.exception import FINNInternalError from finn.util.logging import ThreadsafeProgressDisplay @@ -53,10 +53,13 @@ def __init__( self.running_lock = Lock() self.running = 0 self.total = len(names) + self.logdir = Path(make_build_dir("node_connected_simulation_logfiles_")) def run(self, depth: int, samples: int) -> None: """Run the simulation entirely with the given depth and sample count.""" futures: list[Future] = [] + for i, name in enumerate(self.names): + print(f"{i}: {name}") if self.progress is not None: self.progress.start() with ThreadPoolExecutor(self.workers) as pool: @@ -94,63 +97,98 @@ def _run_binary( ) -> None: """Run the specified simulation binary in a new subprocess and communicate with it.""" - def _print(msg: str, color: str = "green") -> None: - if self.progress is None: - if is_end_node: - color = "orange3" - self.console.log(f"[bold {color}]{name:<35}[/bold {color}] {msg:<35}") + # TODO: Seperate into multiple methods - ld_library_path = "LD_LIBRARY_PATH=" + get_vivado_root() + "/lib/lnx64.o:$LD_LIBRARY_PATH" cwd = binary.parent if name is None: name = cwd.name.replace("rtlsim_", "") - taskset = "" - if cpu is not None: - taskset += f"taskset --cpu-list {cpu}" # TODO: numactl? - command = f"{ld_library_path} {taskset} {binary}" - _print(f"Running command: {command}") - try: - proc = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=cwd, - shell=True, + with (self.logdir / f"{name}_{self.names.index(name)}_of_{self.total}.txt").open( + "w+" + ) as logfile: + + def _print(msg: str, color: str = "green") -> None: + if self.progress is None: + if is_end_node: + color = "orange3" + if "ERROR" in msg: + color = "red" + self.console.log( + f"[bold {color}]{name:<35}" + f"[/bold {color}][cornflower_blue]{self.names.index(name)} " # type:ignore + f"/ {len(self.names)-1}[/cornflower_blue] {msg:<35}" + ) + + ld_library_path = ( + "LD_LIBRARY_PATH=" + get_vivado_root() + "/lib/lnx64.o:$LD_LIBRARY_PATH" ) - _send = lambda cmd: self._send(proc, cmd) # noqa: E731 - received = "" - while received != "end": - time.sleep(self.poll_interval) - received = proc.stdout.readline().decode("UTF-8").strip().split() # type: ignore - if len(received) == 0: - continue - if received[0] == "end": - _print("Ending simulation.") - return - if received[0] == "log": - _print(" ".join(received[1:])) - elif received[0] == "ready": - _print("Received ready signal from simulation") - _send(f"fifodepth {depth}") - _send(f"runSamples {samples}") - _print("Settings sent to simulation.") - elif received[0] == "cycles": - if self.progress is None: + taskset = "" + if cpu is not None: + taskset += f"taskset --cpu-list {cpu}" # TODO: numactl? + command = f"{ld_library_path} {taskset} {binary}" + _print(f"Running command: {command}") + try: + proc = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=cwd, + shell=True, + ) + + def _send(cmd: str) -> None: + self._send(proc, cmd) + logfile.write(f"SIM: {cmd}") + logfile.flush() + + received = "" + while received != "end": + time.sleep(self.poll_interval) + received = ( + proc.stdout.readline().decode("UTF-8").strip().split() + ) # type: ignore + logfile.write(f"CTRL: {' '.join(received)}\n") + logfile.flush() + if len(received) == 0: + continue + if received[0] == "end": + _print("Ending simulation.") + return + if received[0] == "log": _print(" ".join(received[1:])) + elif received[0] == "ready": + _print("Received ready signal from simulation") + _send(f"fifodepth {depth}") + _send(f"runSamples {samples}") + # _send(f"runCycles 10000000") + _print("Settings sent to simulation.") + elif received[0] == "cycles": + if self.progress is None: + _print(" ".join(received[1:])) + else: + self.progress.update(name, int(received[1]), int(received[2])) + elif received[0] == "samples": + if self.progress is None: + _print(" ".join(received[1:])) + else: + self.progress.update(name, int(received[1]), int(received[2])) + elif received[0] == "started": + with self.running_lock: + self.running += 1 + _print(f"Running: {self.running} / {self.total}") + elif received[0] == "stopped": + with self.running_lock: + self.running -= 1 + _print(f"Running: {self.running} / {self.total}") + # elif received[0] == "error": + # _print("ERROR: " + " ".join(received[1:])) + # self.stop_flag = True + # return else: - self.progress.update(name, int(received[1]), int(received[2])) - elif received[0] == "started": - with self.running_lock: - self.running += 1 - _print(f"Running: {self.running} / {self.total}") - elif received[1] == "stopped": - with self.running_lock: - self.running -= 1 - _print(f"Running: {self.running} / {self.total}") - else: - raise FINNInternalError(f"Simulation {name}: Unrecognized command: {received}") - except Exception as e: - self.console.log(f"Exception caught during simulation execution ({name}): {e}") - self.console.log(traceback.format_exc()) - sys.exit(1) + raise FINNInternalError( + f"Simulation {name}: Unrecognized command: {received}" + ) + except Exception as e: + self.console.log(f"Exception caught during simulation execution ({name}): {e}") + self.console.log(traceback.format_exc()) + sys.exit(1) From 5b48ec05c3a608031ddeebbb0de8914aa572939e Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 21 Nov 2025 17:10:07 +0100 Subject: [PATCH 032/170] Add tests --- .gitignore | 1 + finn_xsi/finn_xsi/.clang-format | 2 +- finn_xsi/finn_xsi/CMakeLists.txt | 5 + finn_xsi/finn_xsi/include/FIFO.h | 30 +- .../include/InterSimulationInterface.hpp | 27 +- finn_xsi/finn_xsi/include/Simulation.hpp | 113 ++- .../finn_xsi/include/SimulationInterface.hpp | 214 ----- finn_xsi/finn_xsi/src/FIFO.cpp | 73 +- finn_xsi/finn_xsi/unittests/CMakeLists.txt | 40 + finn_xsi/finn_xsi/unittests/FIFO_test.cpp | 826 ++++++++++++++++++ .../finn_xsi/unittests/Integration_test.cpp | 624 +++++++++++++ .../InterSimulationInterface_test.cpp | 614 +++++++++++++ .../transformation/fpgadataflow/simulation.py | 6 +- 13 files changed, 2268 insertions(+), 307 deletions(-) delete mode 100644 finn_xsi/finn_xsi/include/SimulationInterface.hpp create mode 100644 finn_xsi/finn_xsi/unittests/CMakeLists.txt create mode 100644 finn_xsi/finn_xsi/unittests/FIFO_test.cpp create mode 100644 finn_xsi/finn_xsi/unittests/Integration_test.cpp create mode 100644 finn_xsi/finn_xsi/unittests/InterSimulationInterface_test.cpp diff --git a/.gitignore b/.gitignore index 72e69ea17e..f0a10e9194 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ poetry.lock **/CMakeCache.txt **/compile_commands.json **/.cache +**/build # Package files *.egg diff --git a/finn_xsi/finn_xsi/.clang-format b/finn_xsi/finn_xsi/.clang-format index d4573c3508..2df30c132e 100644 --- a/finn_xsi/finn_xsi/.clang-format +++ b/finn_xsi/finn_xsi/.clang-format @@ -12,7 +12,7 @@ BinPackParameters: 'true' BreakConstructorInitializers: BeforeColon BreakInheritanceList: BeforeColon BreakStringLiterals: true -ColumnLimit: '240' +ColumnLimit: '180' Cpp11BracedListStyle: 'true' FixNamespaceComments: 'true' IndentCaseLabels: 'true' diff --git a/finn_xsi/finn_xsi/CMakeLists.txt b/finn_xsi/finn_xsi/CMakeLists.txt index 7bdce26f04..3cc6736bbc 100644 --- a/finn_xsi/finn_xsi/CMakeLists.txt +++ b/finn_xsi/finn_xsi/CMakeLists.txt @@ -122,3 +122,8 @@ target_include_directories(LayerSimulationBackend PUBLIC "include") # Link libraries target_link_libraries(LayerSimulationBackend fifosim::options nlohmann_json::nlohmann_json Threads::Threads OpenMP::OpenMP_CXX Boost::program_options -ldl -lrt) + +OPTION(ENABLE_UNITTESTS "Enable unittests" OFF) +if(${ENABLE_UNITTESTS}) +add_subdirectory(unittests) +endif() diff --git a/finn_xsi/finn_xsi/include/FIFO.h b/finn_xsi/finn_xsi/include/FIFO.h index ed4767d83a..11bb85390d 100644 --- a/finn_xsi/finn_xsi/include/FIFO.h +++ b/finn_xsi/finn_xsi/include/FIFO.h @@ -1,28 +1,34 @@ #ifndef FIFO_H #define FIFO_H +#include #include -#include - -// TODO: switch int to std::size_t class FIFO { - int maxUtil = 0; - int currentUtil = 0; - int maxSize = 0; - int nextUtil = 0; + uint64_t maxUtil = 0; + uint64_t currentUtil = 0; + uint64_t maxSize = 0; + uint64_t nextUtil = 0; - public: - FIFO(int size = std::numeric_limits::max()); + public: + FIFO(uint64_t size = std::numeric_limits::max()); ~FIFO(); - void update(bool incomingValid, bool outgoingReady); + void update(bool incomingValid, bool incomingReady); void toggleClock(); bool isInputReady() const; bool isOutputValid() const; bool isEmpty() const; - void reset(std::optional size = std::nullopt); - void setMaxSize(int size); + void reset(uint64_t size = std::numeric_limits::max()); + void setMaxSize(const uint64_t size); + uint64_t getSpaceLeft() const; + uint64_t getMaxUtil() const; + void increaseCounter(const uint64_t count); + + // NOTE: User needs to ensure proper ordering. No runtime enforcement of order. + void tryPush(bool incomingValid); + void tryPop(bool incomingReady); + uint64_t size() const; }; #endif /* FIFO_H */ diff --git a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp index 90a392c0d3..3dc6939e05 100644 --- a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp +++ b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp @@ -74,7 +74,7 @@ class InterSimulationInterface { try { shmem = bip::managed_shared_memory(bip::open_only, sharedMemoryName.c_str()); break; - } catch (const bip::interprocess_exception& e) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); } + } catch (const bip::interprocess_exception& e) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } } @@ -82,7 +82,7 @@ class InterSimulationInterface { refCount = shmem.find_or_construct>("refCount")(0); // Increment reference count atomically - int currentRefCount = refCount->fetch_add(1, std::memory_order_acq_rel) + 1; + refCount->fetch_add(1, std::memory_order_acq_rel); // Construct the halo exchange object in shared memory halo = shmem.find_or_construct("HaloExchange")(); @@ -121,15 +121,32 @@ class InterSimulationInterface { return; } - // Decrement reference count atomically - int remainingRefs = refCount->fetch_sub(1, std::memory_order_acq_rel) - 1; + // Clear our local pointers before decrementing (safety) + halo = nullptr; + refCount = nullptr; + + // Get a raw pointer to refCount for the atomic operation + // (we need this because we just nulled our member pointer) + std::atomic* ref_ptr = shmem.find>("refCount").first; + if (!ref_ptr) { + return; // Already destroyed somehow + } + + // Decrement reference count atomically and get the value BEFORE decrement + int remainingRefs = ref_ptr->fetch_sub(1, std::memory_order_acq_rel) - 1; // If we're the last process, clean up the shared memory if (remainingRefs == 0) { + // Destroy all objects first shmem.destroy("HaloExchange"); shmem.destroy>("refCount"); - // Remove the shared memory segment completely + // Close our handle to the shared memory + // This doesn't delete it yet if other processes have it mapped + shmem = bip::managed_shared_memory(); + + // Now remove the shared memory segment completely + // This is safe even if other processes still have stale mappings bip::shared_memory_object::remove(sharedMemoryName.c_str()); } } diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 3feb2b3f95..498ae6b326 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -16,12 +16,12 @@ #include #include #include -#include #include #include #include #include #include +#include using json = nlohmann::json; @@ -41,8 +41,8 @@ class Simulation { Clock clk; - Simulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, - std::array _ostream_descs) + Simulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, + std::array _istream_descs, std::array _ostream_descs) : kernel(kernel_lib), top(kernel, design_lib, xsim_log_file, trace_file), clk(top) { if (trace_file) { top.trace_all(); @@ -69,7 +69,7 @@ class Simulation { template bool hasValidOutput() { - //static_assert(Index < ostreams.size(), "Cannot request valid status of unknown output stream index"); + // static_assert(Index < ostreams.size(), "Cannot request valid status of unknown output stream index"); return ostreams[Index].is_valid(); } @@ -116,23 +116,27 @@ class _SingleNodeSimulation : public Simulation(OStreamsSize)> fifo; - _SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, std::array _istream_descs, - std::array _ostream_descs, std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2) + _SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, + std::array _istream_descs, std::array _ostream_descs, + std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { if (CommunicatesWithPredecessor && !prevNodeName) { throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); } else if (!CommunicatesWithPredecessor && prevNodeName) { std::cout << "log Simulation was passed the previous nodes name but is " "NOT marked for communication with predecessor node. No " - "shared memory will be created." << std::endl; + "shared memory will be created." + << std::endl; } if (CommunicatesWithSuccessor && !nodeName) { - throw std::runtime_error("Cannot communicate with successor because " - "current node name was not given!"); + throw std::runtime_error( + "Cannot communicate with successor because " + "current node name was not given!"); } else if (!CommunicatesWithSuccessor && nodeName) { std::cout << "log Simulation was passed the current nodes name but is NOT " "marked for communication with successor node. No shared " - "memory will be created." << std::endl; + "memory will be created." + << std::endl; } // Create FIFO buffer for (std::size_t i = 0; i < OStreamsSize; ++i) { @@ -165,10 +169,11 @@ class _SingleNodeSimulation : public Simulationfifo[i].update( - this->ostreams[i].isValid(), - toConsumerInterface[i].exchange(this->fifo[i].isOutputValid()) - ); + // Interface sim -valid-> FIFO <-> SHM + this->fifo[i].update(this->ostreams[i].isValid(), toConsumerInterface[i].exchange(this->fifo[i].isOutputValid())); + // FIFO -ready-> sim + this->ostreams[i].ready(this->fifo[i].isInputReady()); + // Toggle FIFO clock this->fifo[i].toggleClock(); } } @@ -191,20 +196,22 @@ class _SingleNodeSimulation : public Simulation::reset(); - for (std::size_t i = 0; i < OStreamsSize; ++i) { - toConsumerInterface[i].reset(); - fifo[i].reset(); - } - if constexpr (NodeIndex != 0) { - for (std::size_t i = 0; i < IStreamsSize; ++i) { - fromProducerInterface[i].reset(); - } - } + // for (std::size_t i = 0; i < OStreamsSize; ++i) { + // toConsumerInterface[i].reset(); + // fifo[i].reset(); + // } + // if constexpr (NodeIndex != 0) { + // for (std::size_t i = 0; i < IStreamsSize; ++i) { + // fromProducerInterface[i].reset(); + // } + // } } /// Return whether all connected FIFOs are empty @@ -215,20 +222,20 @@ class _SingleNodeSimulation : public Simulationfifo[i].tryPop(toConsumerInterface[i].writeToNextNode(this->fifo[i].isOutputValid())); // } runSingleCycle(); - } + } } [[gnu::hot, gnu::always_inline]] void runSingleCycle() { ++cyclesRun; - this->clk.toggleClk(); communicate(); + this->clk.toggleClk(); if constexpr (LoggingEnabled) { - logReadyValidState(); + logReadyValidState(); } debug(std::format("Finished cycle {}\n\n", cyclesRun)); } @@ -241,22 +248,17 @@ class _SingleNodeSimulation : public Simulationostreams[outputIndex].job_size; - } + std::size_t getOutputJobSize(std::size_t outputIndex = 0) { return this->ostreams[outputIndex].job_size; } /// Get the job size of the specified input stream - std::size_t getInputJobSize(std::size_t inputIndex = 0) { - return this->istreams[inputIndex].job_size; - } + std::size_t getInputJobSize(std::size_t inputIndex = 0) { return this->istreams[inputIndex].job_size; } }; - /// Single Node Simulation, thread controlled template class SingleNodeSimulation { - private: + private: std::jthread simulator; std::jthread communicator; @@ -277,26 +279,19 @@ class SingleNodeSimulation { // The simulation itself _SingleNodeSimulation sim; - public: - SingleNodeSimulation( - const std::string& kernel_lib, - const std::string& design_lib, - const char* xsim_log_file, - const char* trace_file, - std::array _istream_descs, - std::array _ostream_descs, - std::optional prevNodeName = std::nullopt, - std::optional nodeName = std::nullopt, - unsigned int initialFIFODepth = 2, - std::string simulationDataFilename = "simulation_data.json" - ) : running(false), - samplesProduced(0), - samplesTarget(0), - validCyclesProduced(0), - cyclesRun(0), - cyclesTarget(0), - simulationDataPath(simulationDataFilename), - sim(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs, prevNodeName, nodeName, initialFIFODepth) {} + public: + SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, + std::array _istream_descs, std::array _ostream_descs, + std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2, + std::string simulationDataFilename = "simulation_data.json") + : running(false), + samplesProduced(0), + samplesTarget(0), + validCyclesProduced(0), + cyclesRun(0), + cyclesTarget(0), + simulationDataPath(simulationDataFilename), + sim(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs, prevNodeName, nodeName, initialFIFODepth) {} /// Stop and reset the simulation, reset cycle counter, target cycle counter, log queue. @@ -315,7 +310,7 @@ class SingleNodeSimulation { // for (std::size_t i = 0; i < OStreamsSize; ++i) { // j["maxOccupation"][std::to_string(i)] = sim.getLargestOccupation(i); // } - //j["cyclesRun"] = outValidCycles; + // j["cyclesRun"] = outValidCycles; std::ofstream file(simulationDataPath); file << j.dump(4); file.close(); @@ -334,7 +329,7 @@ class SingleNodeSimulation { /// Read from std::cin if possible, otherwise /// return immediately. void getlineIfAvailable(std::string& buffer) { - //std::cin.exceptions(std::istream::failbit | std::istream::badbit); + // std::cin.exceptions(std::istream::failbit | std::istream::badbit); if (std::cin.eof() || std::cin.rdbuf()->in_avail() == -1) { buffer = ""; return; @@ -363,7 +358,7 @@ class SingleNodeSimulation { /// Run the simulation sendStarted(); - if constexpr(NodeIndex == TotalNodes - 1) { + if constexpr (NodeIndex == TotalNodes - 1) { while ((samplesTarget != 0 && samplesProduced < samplesTarget) || (cyclesTarget != 0 && cyclesRun < cyclesTarget)) { if (!running) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); diff --git a/finn_xsi/finn_xsi/include/SimulationInterface.hpp b/finn_xsi/finn_xsi/include/SimulationInterface.hpp deleted file mode 100644 index 54a19b23df..0000000000 --- a/finn_xsi/finn_xsi/include/SimulationInterface.hpp +++ /dev/null @@ -1,214 +0,0 @@ -#ifndef SIMULATION_INTERFACE -#define SIMULATION_INTERFACE -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef __cpp_lib_hardware_interference_size -using std::hardware_destructive_interference_size; -#else -constexpr std::size_t hardware_destructive_interference_size = 64; -#endif - -namespace ipc = boost::interprocess; - -enum class SimulationInterfaceType { PRODUCING, CONSUMING }; -constexpr std::string_view to_string(SimulationInterfaceType t) { - if (t == SimulationInterfaceType::CONSUMING) { - return "CONSUMING"; - } else if (t == SimulationInterfaceType::PRODUCING) { - return "PRODUCING"; - } - return "UNKNOWN SIMULATION INTERFACE TYPE"; -} - -template -class SimulationInterface { - private: - // Shared memory structure with proper cache-line alignment - struct SharedData { - alignas(hardware_destructive_interference_size) boost::ipc_atomic predCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic succCycle; - alignas(hardware_destructive_interference_size) boost::ipc_atomic ready; - alignas(hardware_destructive_interference_size) boost::ipc_atomic valid; - - SharedData() : predCycle(0), succCycle(0), ready(false), valid(false) {} - SharedData(unsigned int predecessorCycle, unsigned int successorCycle, bool inReady, bool outValid) : predCycle(predecessorCycle), succCycle(successorCycle), ready(inReady), valid(outValid) {} - SharedData(const SharedData& other) : predCycle(other.predCycle.load()), succCycle(other.succCycle.load()), ready(other.ready.load()), valid(other.valid.load()) {} - SharedData& operator=(const SharedData& other) { - predCycle.store(other.predCycle.load()); - succCycle.store(other.succCycle.load()); - ready.store(other.ready.load()); - valid.store(other.valid.load()); - return *this; - } - }; - - SharedData* sharedData = nullptr; - boost::ipc_atomic* refCount = nullptr; - ipc::managed_shared_memory shmem; - const std::string shmIdentifier; - std::atomic largestOccupation; - -#ifdef NDEBUG - [[maybe_unused]] void simInterfaceDebug([[maybe_unused]] std::string_view s) {} -#else - /// Log the given text with a header identifying the shared memory region and the interface type - void simInterfaceDebug(std::string_view s) { debug(std::format("log {} ({}): {}", shmIdentifier, to_string(T), s)); } -#endif - - public: - // Default constructor needed for std::array - SimulationInterface() : shmIdentifier("") { - // Uninitialized - will be move-assigned later - } - - SimulationInterface(const char* _shmIdentifier, unsigned int initialMaxDepth = 2) : shmIdentifier(_shmIdentifier), largestOccupation(0) { - simInterfaceDebug(std::format("Creating simulation interface with {} depth.", initialMaxDepth)); - if (T == SimulationInterfaceType::PRODUCING) { - ipc::shared_memory_object::remove(_shmIdentifier); - simInterfaceDebug("Removed previous shared memory objects."); - shmem = ipc::managed_shared_memory(ipc::create_only, _shmIdentifier, ShmemSize); - } else { - while (true) { - try { - shmem = ipc::managed_shared_memory(ipc::open_only, _shmIdentifier); - break; - } catch (const ipc::interprocess_exception& e) { simInterfaceDebug("Producer shared memory not yet created. Waiting.."); } - } - } - simInterfaceDebug("Shared memory constructed or found."); - - // Construct or find the reference counter (separate from SharedData) - refCount = shmem.find_or_construct>("refCount")(0); - - // Increment reference count atomically - int currentRefCount = refCount->fetch_add(1, boost::memory_order_acq_rel) + 1; - simInterfaceDebug(std::format("Reference count incremented to {}", currentRefCount)); - - // Construct or find the entire SharedData struct in shared memory - sharedData = shmem.find_or_construct("data")(SharedData(0, 0, true, false)); - simInterfaceDebug("Shared data structure constructed or found."); - } - - // Delete copy operations - SimulationInterface(const SimulationInterface&) = delete; - SimulationInterface& operator=(const SimulationInterface&) = delete; - - // Move constructor - SimulationInterface(SimulationInterface&& other) noexcept : sharedData(other.sharedData), refCount(other.refCount), shmIdentifier(std::move(other.shmIdentifier)), shmem(std::move(other.shmem)) { - // Mark other as moved-from - other.sharedData = nullptr; - other.refCount = nullptr; - } - - // Move assignment operator - SimulationInterface& operator=(SimulationInterface&& other) noexcept { - if (this != &other) { - sharedData = other.sharedData; - refCount = other.refCount; - // Note: managed_shared_memory has deleted assignment, use swap - shmem.swap(other.shmem); - const_cast(shmIdentifier) = std::move(other.shmIdentifier); - - // Mark other as moved-from - other.sharedData = nullptr; - other.refCount = nullptr; - } - return *this; - } - - ~SimulationInterface() { - // Skip cleanup if moved-from or default-constructed - if (!refCount || !sharedData) { - return; - } - - // Decrement reference count atomically - int remainingRefs = refCount->fetch_sub(1, boost::memory_order_acq_rel) - 1; - simInterfaceDebug(std::format("Reference count decremented to {}", remainingRefs)); - - // If we're the last process, clean up the shared memory - if (remainingRefs == 0) { - simInterfaceDebug("Last process exiting - cleaning up shared memory"); - shmem.destroy("data"); - shmem.destroy>("refCount"); - - // Remove the shared memory segment completely - ipc::shared_memory_object::remove(shmIdentifier.c_str()); - simInterfaceDebug("Shared memory cleaned up successfully"); - } else { - simInterfaceDebug("Other processes still using shared memory - detaching only"); - } - } - - /// Return the largest occupation that this FIFO has had so far - std::size_t getLargestOccupation() { - return largestOccupation; - } - - /// Set the max fifo depth in this interface. - void setMaxFifoDepth(unsigned int depth) { - sharedData->maxFifoDepth.store(depth, boost::memory_order_release); - } - - /// Reset all interface data fields to their defaults - void reset() { - simInterfaceDebug("Resetting simulation interface"); - sharedData->ready.store(true, boost::memory_order_release); - sharedData->predCycle.store(0, boost::memory_order_release); - sharedData->valid.store(false, boost::memory_order_release); - sharedData->succCycle.store(0, boost::memory_order_release); - } - - /** - * Reads valid from shm and puts ready into shm. - * Returns the valid signal read from shm. - * Called on consumer side. - */ - bool readFromLastNode(bool consumerReady) - requires(T == SimulationInterfaceType::CONSUMING) - { - // The predecessor must always be one cycle ahead of the successor side - // Wait until predecessor catches up (and overtakes) - while (sharedData->predCycle <= sharedData->succCycle) {} - sharedData->ready = consumerReady; - ++(sharedData->succCycle); - return sharedData->valid; - } - - /** - * Reads ready from shm and puts valid into shm. - * Returns the ready signal read from shm. - * Called on producer side. - */ - bool writeToNextNode(bool producerValid) - requires(T == SimulationInterfaceType::PRODUCING) - { - // The predecessor side must always be at least one cycle ahead of the output side - // Wait until output catches up - while (sharedData->succCycle < sharedData->predCycle) {} - sharedData->valid = producerValid; - ++(sharedData->predCycle); - return sharedData->ready; - } -}; -#endif diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp index 53ac2a6977..97c8f4d046 100644 --- a/finn_xsi/finn_xsi/src/FIFO.cpp +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -1,20 +1,34 @@ #include -#include -FIFO::FIFO(int size) : maxSize(size) {} +#include + +FIFO::FIFO(uint64_t size) : maxSize(size) {} FIFO::~FIFO() {} /// Prepare update for the next clock cycle. -void FIFO::update(bool incomingValid, bool outgoingReady) { - int diff = static_cast(incomingValid) - static_cast(outgoingReady); - if ((currentUtil > 0 && currentUtil < maxSize) || (currentUtil == 0 && diff > 0) || (currentUtil == maxSize && diff < 0)) { - nextUtil = currentUtil += diff; - } +/// This models Q_srl behavior where: +/// - When empty: only accepts input (ignores output ready), transitions to size 1 +/// - When non-empty: can consume, produce, or both +/// With bounded maxSize, this models a real FIFO with backpressure. +void FIFO::update(bool incomingValid, bool incomingReady) { + + // When empty: only push if valid (ignoring ready) + // When non-empty: push if valid AND space available + uint64_t canPush = incomingValid & (currentUtil < maxSize); + + // Q_srl behavior: when empty, only check input valid (ignore output ready) + // Only pop if was non-empty at start AND output ready + uint64_t canPop = incomingReady & (currentUtil != 0); + + nextUtil = nextUtil + canPush - canPop; } /// Toggle the clock cycle, and update the previously set values. +/// nextUtil is guaranteed to be in [0, maxSize] by all operations. void FIFO::toggleClock() { currentUtil = nextUtil; + maxUtil = std::max(maxUtil, currentUtil); + nextUtil = currentUtil; } /// Return whether the FIFO can accept inputs (for the current utilization) @@ -26,17 +40,48 @@ bool FIFO::isOutputValid() const { return currentUtil > 0; } /// Return whether the FIFO is empty (for the current utilization) bool FIFO::isEmpty() const { return currentUtil == 0; } -/// Reset the FIFOs internal state. If size is given, also set maxSize, otherwise keep it. -void FIFO::reset(std::optional size) { +/// Reset the FIFOs internal state. If size is given, also set maxSize, +/// otherwise keep it. +void FIFO::reset(uint64_t size) { currentUtil = 0; maxUtil = 0; - if (size) { - maxSize = *size; - } + maxSize = size; nextUtil = 0; } /// Set the FIFOs max size -void FIFO::setMaxSize(int size) { - maxSize = size; +void FIFO::setMaxSize(const uint64_t size) { maxSize = size; } + +uint64_t FIFO::getSpaceLeft() const { return maxSize - currentUtil; } + +uint64_t FIFO::getMaxUtil() const { return maxUtil; } + +void FIFO::increaseCounter(const uint64_t count) { + // Branchless: compute new value and saturate at maxSize + uint64_t newUtil = nextUtil + count; + uint64_t overflow = newUtil > maxSize; + nextUtil = overflow ? maxSize : newUtil; +} + +/// If incomingValid is true and FIFO has space, increment nextUtil +/// Matches Q_srl: when empty, always accepts input +/// When using tryPush/tryPop separately, ALWAYS call tryPush BEFORE tryPop! +void FIFO::tryPush(bool incomingValid) { + // When empty: accept input unconditionally (like Q_srl state_empty) + // When non-empty: accept if space available + nextUtil += incomingValid & (nextUtil < maxSize); } + +/// If incomingReady is true and FIFO has data, decrement nextUtil +/// Matches Q_srl: only pops if data available +/// When using tryPush/tryPop separately, ALWAYS call tryPush BEFORE tryPop! +/// Note: If FIFO was empty and tryPush just added data, tryPop will NOT pop it +/// (matching Q_srl where state_empty ignores output ready) +void FIFO::tryPop(bool incomingReady) { + // Check currentUtil (state at cycle start) not nextUtil (after tryPush) + // This ensures empty->tryPush->tryPop results in size=1, matching Q_srl + nextUtil -= incomingReady & (currentUtil > 0); +} + +/// Return the current number of elements in the FIFO +uint64_t FIFO::size() const { return currentUtil; } diff --git a/finn_xsi/finn_xsi/unittests/CMakeLists.txt b/finn_xsi/finn_xsi/unittests/CMakeLists.txt new file mode 100644 index 0000000000..a9189d01bb --- /dev/null +++ b/finn_xsi/finn_xsi/unittests/CMakeLists.txt @@ -0,0 +1,40 @@ +# Enable testing +enable_testing() + +# Fetch Google Test +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.14.0 +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +FetchContent_MakeAvailable(googletest) + +# Add FIFO unit tests +add_executable(FIFO_test FIFO_test.cpp ${CORE_SRC}) +target_link_libraries(FIFO_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt) +target_include_directories(FIFO_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) +target_include_directories(FIFO_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") + +# Add InterSimulationInterface unit tests +add_executable(InterSimulationInterface_test InterSimulationInterface_test.cpp) +target_link_libraries(InterSimulationInterface_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt) +target_include_directories(InterSimulationInterface_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ${Boost_INCLUDE_DIRS}) + +# Add Integration tests (FIFO + InterSimulationInterface) +add_executable(Integration_test Integration_test.cpp ${CORE_SRC}) +target_link_libraries(Integration_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt) +target_include_directories(Integration_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ${Boost_INCLUDE_DIRS}) +target_include_directories(Integration_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") + +# Register tests with CTest +include(GoogleTest) +gtest_discover_tests(FIFO_test) +gtest_discover_tests(InterSimulationInterface_test) +gtest_discover_tests(Integration_test) + +# Create a target to build all unittests at once +add_custom_target(all_unittests) +add_dependencies(all_unittests FIFO_test InterSimulationInterface_test Integration_test) diff --git a/finn_xsi/finn_xsi/unittests/FIFO_test.cpp b/finn_xsi/finn_xsi/unittests/FIFO_test.cpp new file mode 100644 index 0000000000..6f728f5319 --- /dev/null +++ b/finn_xsi/finn_xsi/unittests/FIFO_test.cpp @@ -0,0 +1,826 @@ +#include "FIFO.h" +#include + +// Test fixture for FIFO tests +class FIFOTest : public ::testing::Test { +protected: + void SetUp() override { + // Setup code if needed + } + + void TearDown() override { + // Cleanup code if needed + } +}; + +// ===== Constructor and Initialization Tests ===== + +TEST_F(FIFOTest, ConstructorWithDefaultSize) { + FIFO fifo; + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isInputReady()); + EXPECT_FALSE(fifo.isOutputValid()); +} + +TEST_F(FIFOTest, ConstructorWithSpecificSize) { + FIFO fifo(10); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isInputReady()); + EXPECT_FALSE(fifo.isOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); +} + +TEST_F(FIFOTest, ConstructorWithZeroSize) { + FIFO fifo(0); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_FALSE(fifo.isInputReady()); + EXPECT_FALSE(fifo.isOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 0); +} + +// ===== Reset Tests ===== + +TEST_F(FIFOTest, ResetClearsState) { + FIFO fifo(10); + fifo.update(true, false); // Add one element + fifo.toggleClock(); + EXPECT_FALSE(fifo.isEmpty()); + + fifo.reset(10); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); +} + +TEST_F(FIFOTest, ResetChangesSize) { + FIFO fifo(10); + fifo.reset(20); + EXPECT_EQ(fifo.getSpaceLeft(), 20); +} + +TEST_F(FIFOTest, SetMaxSize) { + FIFO fifo(10); + fifo.setMaxSize(15); + EXPECT_EQ(fifo.getSpaceLeft(), 15); +} + +// ===== Basic Update and Toggle Tests ===== + +TEST_F(FIFOTest, PushOneElement) { + FIFO fifo(10); + fifo.update(true, false); // Push (valid=true, ready=false) + fifo.toggleClock(); + + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isOutputValid()); + EXPECT_TRUE(fifo.isInputReady()); + EXPECT_EQ(fifo.getSpaceLeft(), 9); +} + +TEST_F(FIFOTest, PopOneElement) { + FIFO fifo(10); + // First push an element + fifo.update(true, false); + fifo.toggleClock(); + + // Then pop it + fifo.update(false, true); // Pop (valid=false, ready=true) + fifo.toggleClock(); + + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_FALSE(fifo.isOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); +} + +TEST_F(FIFOTest, PushAndPopSimultaneously) { + FIFO fifo(10); + // First push an element + fifo.update(true, false); + fifo.toggleClock(); + + // Now push and pop simultaneously (FIFO size should stay the same) + fifo.update(true, true); + fifo.toggleClock(); + + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 9); +} + +// ===== Boundary Condition Tests ===== + +TEST_F(FIFOTest, FillToCapacity) { + FIFO fifo(3); + + for (int i = 0; i < 3; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } + + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isOutputValid()); + EXPECT_FALSE(fifo.isInputReady()); + EXPECT_EQ(fifo.getSpaceLeft(), 0); +} + +TEST_F(FIFOTest, CannotPushWhenFull) { + FIFO fifo(2); + + // Fill the FIFO + fifo.update(true, false); + fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); + + EXPECT_FALSE(fifo.isInputReady()); + + // Try to push when full (should have no effect) + fifo.update(true, false); + fifo.toggleClock(); + + EXPECT_EQ(fifo.getSpaceLeft(), 0); +} + +TEST_F(FIFOTest, CanPushAndPullWhenFull) { + FIFO fifo(2); + + // Fill the FIFO + fifo.update(true, false); + fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); + + EXPECT_FALSE(fifo.isInputReady()); + + // Try to push and pull when full (should have no effect) + fifo.update(true, true); + fifo.toggleClock(); + + EXPECT_EQ(fifo.getSpaceLeft(), 1); + + fifo.reset(2); + + // Fill the FIFO + fifo.update(true, false); + fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); + + EXPECT_FALSE(fifo.isInputReady()); + + // Try to push and pull when full (should have no effect) + fifo.update(false, true); + fifo.toggleClock(); + + EXPECT_EQ(fifo.getSpaceLeft(), 1); +} + +TEST_F(FIFOTest, CannotPopWhenEmpty) { + FIFO fifo(10); + + EXPECT_TRUE(fifo.isEmpty()); + + // Try to pop when empty (should have no effect) + fifo.update(false, true); + fifo.toggleClock(); + + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); +} + +TEST_F(FIFOTest, CanPushAndPopWhenEmpty) { + FIFO fifo(10); + + EXPECT_TRUE(fifo.isEmpty()); + + // Try to pop when empty (should have no effect) + fifo.update(true, true); + fifo.toggleClock(); + + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 9); +} + +TEST_F(FIFOTest, PopWhenFullMakesSpaceAvailable) { + FIFO fifo(2); + + // Fill the FIFO + fifo.update(true, false); + fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); + + EXPECT_FALSE(fifo.isInputReady()); + + // Pop one element + fifo.update(false, true); + fifo.toggleClock(); + + EXPECT_TRUE(fifo.isInputReady()); + EXPECT_EQ(fifo.getSpaceLeft(), 1); +} + +// ===== Sequential Operation Tests ===== + +TEST_F(FIFOTest, SequentialPushAndPop) { + FIFO fifo(5); + + // Push 3 elements + for (int i = 0; i < 3; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.getSpaceLeft(), 2); + + // Pop 2 elements + for (int i = 0; i < 2; ++i) { + fifo.update(false, true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.getSpaceLeft(), 4); + + // Pop 1 more + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_TRUE(fifo.isEmpty()); +} + +TEST_F(FIFOTest, AlternatingPushPop) { + FIFO fifo(10); + + for (int i = 0; i < 5; ++i) { + // Push + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_FALSE(fifo.isEmpty()); + + // Pop + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_TRUE(fifo.isEmpty()); + } +} + +TEST_F(FIFOTest, StreamingOperation) { + FIFO fifo(10); + + // Push one element first + fifo.update(true, false); + fifo.toggleClock(); + + // Now stream: push and pop simultaneously for multiple cycles + for (int i = 0; i < 100; ++i) { + fifo.update(true, true); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 9); // Size should remain constant + } +} + +// ===== State Query Tests ===== + +TEST_F(FIFOTest, IsEmptyCorrectly) { + FIFO fifo(5); + EXPECT_TRUE(fifo.isEmpty()); + + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_FALSE(fifo.isEmpty()); + + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_TRUE(fifo.isEmpty()); +} + +TEST_F(FIFOTest, IsInputReadyCorrectly) { + FIFO fifo(2); + EXPECT_TRUE(fifo.isInputReady()); + + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_TRUE(fifo.isInputReady()); + + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_FALSE(fifo.isInputReady()); +} + +TEST_F(FIFOTest, IsOutputValidCorrectly) { + FIFO fifo(5); + EXPECT_FALSE(fifo.isOutputValid()); + + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_TRUE(fifo.isOutputValid()); + + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_FALSE(fifo.isOutputValid()); +} + +TEST_F(FIFOTest, GetSpaceLeftCorrectly) { + FIFO fifo(10); + EXPECT_EQ(fifo.getSpaceLeft(), 10); + + for (int i = 0; i < 3; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 10 - i - 1); + } + + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 8); +} + +// ===== Edge Case Tests ===== + +TEST_F(FIFOTest, NoUpdateBeforeToggle) { + FIFO fifo(10); + fifo.toggleClock(); // Toggle without update + + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); +} + +TEST_F(FIFOTest, LargeCapacity) { + FIFO fifo(1000000); + EXPECT_EQ(fifo.getSpaceLeft(), 1000000); + + for (int i = 0; i < 100; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } + + EXPECT_EQ(fifo.getSpaceLeft(), 999900); +} + +// ===== IncreaseCounter Tests ===== + +TEST_F(FIFOTest, IncreaseCounterBasic) { + FIFO fifo(100); + + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 99); + + fifo.increaseCounter(5); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 94); +} + +TEST_F(FIFOTest, IncreaseCounterOnEmptyFIFO) { + FIFO fifo(100); + + fifo.increaseCounter(10); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 90); +} + +TEST_F(FIFOTest, IncreaseCounterZero) { + FIFO fifo(100); + + fifo.update(true, false); + fifo.toggleClock(); + + fifo.increaseCounter(0); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 99); +} + +// ===== Complex Scenarios ===== + +TEST_F(FIFOTest, BurstTrafficPattern) { + FIFO fifo(20); + + // Burst of 10 pushes + for (int i = 0; i < 10; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.getSpaceLeft(), 10); + + // Burst of 10 pops + for (int i = 0; i < 10; ++i) { + fifo.update(false, true); + fifo.toggleClock(); + } + EXPECT_TRUE(fifo.isEmpty()); +} + +TEST_F(FIFOTest, StressTestManyOperations) { + FIFO fifo(100); + + // Perform 1000 operations + for (int i = 0; i < 500; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } + + for (int i = 0; i < 500; ++i) { + fifo.update(false, true); + fifo.toggleClock(); + } + + EXPECT_TRUE(fifo.isEmpty()); +} + +// ===== Multiple FIFO Instances ===== + +TEST_F(FIFOTest, MultipleFIFOsIndependent) { + FIFO fifo1(10); + FIFO fifo2(20); + + fifo1.update(true, false); + fifo1.toggleClock(); + + EXPECT_EQ(fifo1.getSpaceLeft(), 9); + EXPECT_EQ(fifo2.getSpaceLeft(), 20); + + fifo2.update(true, false); + fifo2.update(true, false); + fifo2.toggleClock(); + fifo2.toggleClock(); + + // fifo2 should have 2 elements (last update takes effect) + EXPECT_EQ(fifo1.getSpaceLeft(), 9); + EXPECT_TRUE(fifo2.getSpaceLeft() < 20); +} + +// ===== Individual Method Tests ===== + +TEST_F(FIFOTest, TryPushBasic) { + FIFO fifo(10); + EXPECT_EQ(fifo.size(), 0); + + fifo.tryPush(true); + fifo.toggleClock(); + + EXPECT_EQ(fifo.size(), 1); + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isOutputValid()); +} + +TEST_F(FIFOTest, TryPushFalseDoesNothing) { + FIFO fifo(10); + + fifo.tryPush(false); + fifo.toggleClock(); + + EXPECT_EQ(fifo.size(), 0); + EXPECT_TRUE(fifo.isEmpty()); +} + +TEST_F(FIFOTest, TryPushMultiple) { + FIFO fifo(10); + + for (int i = 0; i < 5; ++i) { + fifo.tryPush(true); + fifo.toggleClock(); + } + + EXPECT_EQ(fifo.size(), 5); + EXPECT_EQ(fifo.getSpaceLeft(), 5); +} + +TEST_F(FIFOTest, TryPushWhenFull) { + FIFO fifo(3); + + // Fill the FIFO + for (int i = 0; i < 3; ++i) { + fifo.tryPush(true); + fifo.toggleClock(); + } + + EXPECT_EQ(fifo.size(), 3); + EXPECT_FALSE(fifo.isInputReady()); + + // Try to push when full (should have no effect) + fifo.tryPush(true); + fifo.toggleClock(); + + EXPECT_EQ(fifo.size(), 3); +} + +TEST_F(FIFOTest, TryPopBasic) { + FIFO fifo(10); + + // First push an element + fifo.tryPush(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); + + // Then pop it + fifo.tryPop(true); + fifo.toggleClock(); + + EXPECT_EQ(fifo.size(), 0); + EXPECT_TRUE(fifo.isEmpty()); +} + +TEST_F(FIFOTest, TryPopFalseDoesNothing) { + FIFO fifo(10); + + fifo.tryPush(true); + fifo.toggleClock(); + + fifo.tryPop(false); + fifo.toggleClock(); + + EXPECT_EQ(fifo.size(), 1); +} + +TEST_F(FIFOTest, TryPopWhenEmpty) { + FIFO fifo(10); + + EXPECT_TRUE(fifo.isEmpty()); + + // Try to pop when empty (should have no effect) + fifo.tryPop(true); + fifo.toggleClock(); + + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.size(), 0); +} + +TEST_F(FIFOTest, TryPushAndTryPopSameCycle) { + FIFO fifo(10); + + // Push first element + fifo.tryPush(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); + + // Push and pop in same cycle (order: push then pop) + fifo.tryPush(true); + fifo.tryPop(true); + fifo.toggleClock(); + + // Should still have 1 element (pushed 1, popped 1) + EXPECT_EQ(fifo.size(), 1); + + // Push and pop in same cycle (order: push then pop) + fifo.tryPop(true); + fifo.tryPush(true); + fifo.toggleClock(); + + // Should still have 1 element (pushed 1, popped 1) + EXPECT_EQ(fifo.size(), 1); +} + +TEST_F(FIFOTest, TryPushAndTryPopSameCycleEmptyFIFO) { + FIFO fifo(10); + + // Push and pop in same cycle (order: push then pop) + fifo.tryPush(true); + fifo.tryPop(true); + fifo.toggleClock(); + + // Should still have 1 element (pushed 1, popped 0, because was empty) + EXPECT_EQ(fifo.size(), 1); + + fifo.reset(10); + // Push and pop in same cycle (order: push then pop) + fifo.tryPush(true); + fifo.tryPop(false); + fifo.toggleClock(); + + // Should still have 0 element (pushed 1, popped 0) + EXPECT_EQ(fifo.size(), 1); +} + +TEST_F(FIFOTest, TryPushAndTryPopSameCycleFullFIFO) { + FIFO fifo(1); + + // Push first element + fifo.tryPush(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); + EXPECT_FALSE(fifo.isInputReady()); + + // Push and pop in same cycle (order: push then pop) + fifo.tryPush(true); + fifo.tryPop(true); + fifo.toggleClock(); + + // Should still have 1 element (pushed 1, popped 1) + EXPECT_EQ(fifo.size(), 0); + + fifo.reset(1); + + // Push first element + fifo.tryPush(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); + EXPECT_FALSE(fifo.isInputReady()); + + // Push and pop in same cycle (order: push then pop) + fifo.tryPush(false); + fifo.tryPop(true); + fifo.toggleClock(); + + // Should still have 1 element (pushed 1, popped 1) + EXPECT_EQ(fifo.size(), 0); +} + +TEST_F(FIFOTest, TryPushAndTryPopSequence) { + FIFO fifo(10); + + // Push 3 + for (int i = 0; i < 3; ++i) { + fifo.tryPush(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 3); + + // Pop 2 + for (int i = 0; i < 2; ++i) { + fifo.tryPop(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 1); + + // Push 1 more + fifo.tryPush(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 2); +} + +TEST_F(FIFOTest, TryPushAndTryPopStreaming) { + FIFO fifo(10); + + // Initialize with one element + fifo.tryPush(true); + fifo.toggleClock(); + + // Stream: push and pop simultaneously for many cycles + for (int i = 0; i < 100; ++i) { + fifo.tryPush(true); + fifo.tryPop(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); // Size should remain constant + } +} + +TEST_F(FIFOTest, TryPushAlternatingValid) { + FIFO fifo(10); + + for (int i = 0; i < 10; ++i) { + fifo.tryPush(i % 2 == 0); // Push only on even iterations + fifo.toggleClock(); + } + + EXPECT_EQ(fifo.size(), 5); // Should have 5 elements +} + +TEST_F(FIFOTest, TryPopAlternatingReady) { + FIFO fifo(10); + + // Fill with 6 elements + for (int i = 0; i < 6; ++i) { + fifo.tryPush(true); + fifo.toggleClock(); + } + + // Pop alternating + for (int i = 0; i < 10; ++i) { + fifo.tryPop(i % 2 == 0); // Pop only on even iterations + fifo.toggleClock(); + } + + EXPECT_EQ(fifo.size(), 1); // 6 - 5 pops = 1 +} + +TEST_F(FIFOTest, SizeMethodCorrectness) { + FIFO fifo(20); + + EXPECT_EQ(fifo.size(), 0); + + for (int i = 1; i <= 10; ++i) { + fifo.tryPush(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), i); + } + + for (int i = 9; i >= 0; --i) { + fifo.tryPop(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), i); + } +} + +TEST_F(FIFOTest, TryMethodsVsUpdateEquivalence) { + FIFO fifo1(10); + FIFO fifo2(10); + + // Use update() on fifo1 + fifo1.update(true, false); // Push + fifo1.toggleClock(); + fifo1.update(true, false); // Push + fifo1.toggleClock(); + fifo1.update(false, true); // Pop + fifo1.toggleClock(); + + // Use tryPush/tryPop on fifo2 + fifo2.tryPush(true); + fifo2.toggleClock(); + fifo2.tryPush(true); + fifo2.toggleClock(); + fifo2.tryPop(true); + fifo2.toggleClock(); + + // Should have same result + EXPECT_EQ(fifo1.size(), fifo2.size()); + EXPECT_EQ(fifo1.isEmpty(), fifo2.isEmpty()); + EXPECT_EQ(fifo1.isOutputValid(), fifo2.isOutputValid()); +} + +TEST_F(FIFOTest, TryMethodsBurstPattern) { + FIFO fifo(50); + + // Burst of pushes + for (int i = 0; i < 30; ++i) { + fifo.tryPush(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 30); + + // Burst of pops + for (int i = 0; i < 20; ++i) { + fifo.tryPop(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 10); + + // Mixed burst + for (int i = 0; i < 15; ++i) { + fifo.tryPush(true); + fifo.tryPop(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 10); // Should remain constant +} + +TEST_F(FIFOTest, TryMethodsStressTest) { + FIFO fifo(1000); + + // Complex pattern + for (int i = 0; i < 500; ++i) { + fifo.tryPush(i % 3 != 0); // Push 2 out of 3 times + if (i > 100) { + fifo.tryPop(i % 2 == 0); // Pop every other time after 100 + } + fifo.toggleClock(); + } + + // Verify FIFO is in valid state + EXPECT_LE(fifo.size(), 1000); + EXPECT_EQ(fifo.size() == 0, fifo.isEmpty()); + EXPECT_EQ(fifo.size() > 0, fifo.isOutputValid()); +} + +TEST_F(FIFOTest, TryMethodsEdgeCaseFullToEmpty) { + FIFO fifo(5); + + // Fill completely + for (int i = 0; i < 5; ++i) { + fifo.tryPush(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 5); + EXPECT_FALSE(fifo.isInputReady()); + + // Empty completely + for (int i = 0; i < 5; ++i) { + fifo.tryPop(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 0); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_FALSE(fifo.isOutputValid()); +} + +TEST_F(FIFOTest, TryMethodsWithReset) { + FIFO fifo(10); + + // Add some elements + for (int i = 0; i < 5; ++i) { + fifo.tryPush(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 5); + + // Reset + fifo.reset(10); + EXPECT_EQ(fifo.size(), 0); + + // Should work normally after reset + fifo.tryPush(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); +} + +// Main function to run all tests +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/finn_xsi/finn_xsi/unittests/Integration_test.cpp b/finn_xsi/finn_xsi/unittests/Integration_test.cpp new file mode 100644 index 0000000000..350a0a21c9 --- /dev/null +++ b/finn_xsi/finn_xsi/unittests/Integration_test.cpp @@ -0,0 +1,624 @@ +#include +#include +#include + +#include + +#include "FIFO.h" +#include "InterSimulationInterface.hpp" + +// Test fixture for integration tests +class IntegrationTest : public ::testing::Test { + protected: + std::string shmName; + + void SetUp() override { + // Generate unique shared memory name for each test + shmName = "test_shm_integration_" + std::to_string(getpid()); + + // Clean up any leftover shared memory from previous runs + boost::interprocess::shared_memory_object::remove(shmName.c_str()); + } + + void TearDown() override { + // Clean up shared memory after test + boost::interprocess::shared_memory_object::remove(shmName.c_str()); + } +}; + +class SimDummy { + bool currentValid = false; + bool currentReady = true; + bool nextValid = false; + bool nextReady = true; + + public: + bool isOutputValid() const { return currentValid; } + void toggleClock() { + currentValid = nextValid; + currentReady = nextReady; + } + bool isInputReady() const { return currentReady; } + void setNextValid(bool v) { nextValid = v; } + void setNextReady(bool r) { nextReady = r; } +}; + +// ===== Basic Integration Tests ===== + +TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { + // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver with FIFO output validation + int receivedCount = 0; + { + InterSimulationInterface receiver(shmName); + SimDummy simDummy; + + simDummy.setNextReady(false); + bool readySignal = simDummy.isInputReady(); + bool validSignal = receiver.exchange(readySignal); + if (validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + } // Destructor called here + exit(0); + } + + // Parent process: Sender with FIFO input + { + InterSimulationInterface sender(shmName); + FIFO inputFifo(15); + + bool validSignal = false; + EXPECT_TRUE(inputFifo.isInputReady()); + bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==false for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + + } // Destructor called here + + // Wait for child + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + +TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { + // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver with FIFO output validation + int receivedCount = 0; + { + InterSimulationInterface receiver(shmName); + SimDummy simDummy; + + simDummy.setNextReady(true); + bool readySignal = simDummy.isInputReady(); + bool validSignal = receiver.exchange(readySignal); + if (validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + } // Destructor called here + exit(0); + } + + // Parent process: Sender with FIFO input + { + InterSimulationInterface sender(shmName); + FIFO inputFifo(15); + + bool validSignal = false; + EXPECT_TRUE(inputFifo.isInputReady()); + bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + + } // Destructor called here + + // Wait for child + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + +TEST_F(IntegrationTest, OneCycleReadyFalseValidTrue) { + // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver with FIFO output validation + int receivedCount = 0; + { + InterSimulationInterface receiver(shmName); + SimDummy simDummy; + + simDummy.setNextReady(false); + bool readySignal = simDummy.isInputReady(); + bool validSignal = receiver.exchange(readySignal); + if (validSignal) { // It is correct that valid is false here, because we only have a single cycle and the fifo input is set to valid in cycle 0. Therefore, the FIFO + // output is valid in cycle 1 and we should receive a valid in cycle 1. + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + } // Destructor called here + exit(0); + } + + // Parent process: Sender with FIFO input + { + InterSimulationInterface sender(shmName); + FIFO inputFifo(15); + + bool validSignal = true; + EXPECT_TRUE(inputFifo.isInputReady()); + bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 14); + EXPECT_TRUE(inputFifo.isInputReady()); + + } // Destructor called here + + // Wait for child + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + +TEST_F(IntegrationTest, OneCycleReadyTrueValidTrue) { + // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver with FIFO output validation + int receivedCount = 0; + { + InterSimulationInterface receiver(shmName); + SimDummy simDummy; + + simDummy.setNextReady(true); + bool readySignal = simDummy.isInputReady(); + bool validSignal = receiver.exchange(readySignal); + if (validSignal) { // It is correct that valid is false here, because we only have a single cycle and the fifo input is set to valid in cycle 0. Therefore, the FIFO + // output is valid in cycle 1 and we should receive a valid in cycle 1. + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + } // Destructor called here + exit(0); + } + + // Parent process: Sender with FIFO input + { + InterSimulationInterface sender(shmName); + FIFO inputFifo(15); + + bool validSignal = true; + EXPECT_TRUE(inputFifo.isInputReady()); + bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 14); + EXPECT_TRUE(inputFifo.isInputReady()); + + } // Destructor called here + + // Wait for child + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + +// ===== Multicycle Integration Tests ===== + +TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { + // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver with FIFO output validation + int receivedCount = 0; + { + InterSimulationInterface receiver(shmName); + SimDummy simDummy; + + simDummy.setNextReady(false); + bool readySignal = simDummy.isInputReady(); + bool validSignal = receiver.exchange(readySignal); + if (validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + + simDummy.setNextReady(false); + readySignal = simDummy.isInputReady(); // Should be false now + validSignal = receiver.exchange(readySignal); + if (validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 2 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + + } // Destructor called here + exit(0); + } + + // Parent process: Sender with FIFO input + { + InterSimulationInterface sender(shmName); + FIFO inputFifo(15); + + bool validSignal = false; + EXPECT_TRUE(inputFifo.isInputReady()); + bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + EXPECT_TRUE(inputFifo.isInputReady()); + incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_FALSE(incomingReady); // We are in cycle 1; expect ready==false for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + + } // Destructor called here + + // Wait for child + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + +TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { + // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver with FIFO output validation + int receivedCount = 0; + { + InterSimulationInterface receiver(shmName); + SimDummy simDummy; + + simDummy.setNextReady(true); + bool readySignal = simDummy.isInputReady(); + bool validSignal = receiver.exchange(readySignal); + if (validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + + simDummy.setNextReady(true); + readySignal = simDummy.isInputReady(); // Should be true now + validSignal = receiver.exchange(readySignal); + if (validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 2 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + + } // Destructor called here + exit(0); + } + + // Parent process: Sender with FIFO input + { + InterSimulationInterface sender(shmName); + FIFO inputFifo(15); + + bool validSignal = false; + EXPECT_TRUE(inputFifo.isInputReady()); + bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + EXPECT_TRUE(inputFifo.isInputReady()); + incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + + } // Destructor called here + + // Wait for child + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + +TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { + // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver with FIFO output validation + int receivedCount = 0; + { + InterSimulationInterface receiver(shmName); + SimDummy simDummy; + + simDummy.setNextReady(false); + bool readySignal = simDummy.isInputReady(); + bool validSignal = receiver.exchange(readySignal); + if (validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + + simDummy.setNextReady(false); + readySignal = simDummy.isInputReady(); // Should be false now + validSignal = receiver.exchange(readySignal); + if (!validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 2 STARTS + + // Verify we received all data + if (!simDummy.isOutputValid()) { + exit(1); + } + + } // Destructor called here + exit(0); + } + + // Parent process: Sender with FIFO input + { + InterSimulationInterface sender(shmName); + FIFO inputFifo(15); + + bool validSignal = true; + EXPECT_TRUE(inputFifo.isInputReady()); + bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 14); + EXPECT_TRUE(inputFifo.isInputReady()); + EXPECT_TRUE(inputFifo.isOutputValid()); + incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_FALSE(incomingReady); // We are in cycle 1; expect ready==false for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 14); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 13); + EXPECT_TRUE(inputFifo.isInputReady()); + + } // Destructor called here + + // Wait for child + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + + +TEST_F(IntegrationTest, TwoCycleReadyTrueValidTrue) { + // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver with FIFO output validation + int receivedCount = 0; + { + InterSimulationInterface receiver(shmName); + SimDummy simDummy; + + simDummy.setNextReady(true); + bool readySignal = simDummy.isInputReady(); + bool validSignal = receiver.exchange(readySignal); + if (validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + + // Verify we received all data + if (simDummy.isOutputValid()) { + exit(1); + } + + simDummy.setNextReady(true); + readySignal = simDummy.isInputReady(); // Should be true now + validSignal = receiver.exchange(readySignal); + if (!validSignal) { + exit(2); + } + simDummy.setNextValid(validSignal); + simDummy.toggleClock(); // BELOW HERE CYCLE 2 STARTS + + // Verify we received all data + if (!simDummy.isOutputValid()) { + exit(1); + } + + } // Destructor called here + exit(0); + } + + // Parent process: Sender with FIFO input + { + InterSimulationInterface sender(shmName); + FIFO inputFifo(15); + + bool validSignal = true; + EXPECT_TRUE(inputFifo.isInputReady()); + bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 15); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 14); + EXPECT_TRUE(inputFifo.isInputReady()); + EXPECT_TRUE(inputFifo.isOutputValid()); + incomingReady = sender.exchange(inputFifo.isOutputValid()); + EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 1 + inputFifo.update(validSignal, incomingReady); + EXPECT_EQ(inputFifo.getSpaceLeft(), 14); + EXPECT_TRUE(inputFifo.isInputReady()); + inputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS + EXPECT_EQ(inputFifo.getSpaceLeft(), 14); + EXPECT_TRUE(inputFifo.isInputReady()); + + } // Destructor called here + + // Wait for child + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); +} + + +// ===== Sender Side Integration Tests ===== + +TEST_F(IntegrationTest, SimToFIFO) { + // Architecture: SimDummy -> FIFO + + SimDummy sim; + FIFO fifo(15); + + //Propagate valid through SimDummy + sim.setNextValid(true); + fifo.update(sim.isOutputValid(), false); + EXPECT_TRUE(fifo.isInputReady()); + sim.setNextReady(fifo.isInputReady()); + fifo.toggleClock(); + sim.toggleClock(); + EXPECT_EQ(fifo.size(), 0); + EXPECT_TRUE(sim.isInputReady()); + + //Fill FIFO to capacity + for (std::size_t i = 0; i < 15; ++i) { + sim.setNextValid(true); + + fifo.update(sim.isOutputValid(), false); + EXPECT_TRUE(fifo.isInputReady()); + sim.setNextReady(fifo.isInputReady()); + EXPECT_EQ(fifo.size(), i); + fifo.toggleClock(); + sim.toggleClock(); + EXPECT_EQ(fifo.size(), i+1); + EXPECT_TRUE(sim.isInputReady()); + } + + EXPECT_FALSE(fifo.isInputReady()); // FIFO changed to not ready on this cycle; Sim is still ready + sim.setNextValid(true); + fifo.update(sim.isOutputValid(), false); + EXPECT_FALSE(fifo.isInputReady()); + sim.setNextReady(fifo.isInputReady()); + fifo.toggleClock(); + sim.toggleClock(); //Propagate ready false through sim + + EXPECT_EQ(fifo.size(), 15); + EXPECT_FALSE(sim.isInputReady()); +} diff --git a/finn_xsi/finn_xsi/unittests/InterSimulationInterface_test.cpp b/finn_xsi/finn_xsi/unittests/InterSimulationInterface_test.cpp new file mode 100644 index 0000000000..f5b9730e2e --- /dev/null +++ b/finn_xsi/finn_xsi/unittests/InterSimulationInterface_test.cpp @@ -0,0 +1,614 @@ +#include "InterSimulationInterface.hpp" + +#include +#include +#include + +#include +#include + +// Test fixture for InterSimulationInterface tests +class InterSimulationInterfaceTest : public ::testing::Test { + protected: + void SetUp() override { + // Generate unique shared memory name for each test + shmName = "test_shm_" + std::to_string(getpid()) + "_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + } + + void TearDown() override { + // Cleanup: ensure shared memory is removed + boost::interprocess::shared_memory_object::remove(shmName.c_str()); + } + + std::string shmName; +}; + +// ===== Constructor and Initialization Tests ===== + +TEST_F(InterSimulationInterfaceTest, ReceiverConstructorCreatesSharedMemory) { + InterSimulationInterface receiver(shmName); + + // Verify that shared memory exists + bool shmExists = false; + try { + boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); + shmExists = true; + } catch (...) { shmExists = false; } + + EXPECT_TRUE(shmExists); +} + +TEST_F(InterSimulationInterfaceTest, SenderWaitsForReceiverToCreateSharedMemory) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender (waits for receiver) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + InterSimulationInterface sender(shmName); + exit(0); + } else { + // Parent process: Receiver (creates shared memory) + InterSimulationInterface receiver(shmName); + + // Wait for child to complete + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, DefaultConstructorCreatesUninitializedObject) { + InterSimulationInterface interface; + // Should not crash - object is in moved-from state + // Destructor should handle this gracefully +} + +TEST_F(InterSimulationInterfaceTest, MoveConstructorTransfersOwnership) { + InterSimulationInterface receiver1(shmName); + InterSimulationInterface receiver2(std::move(receiver1)); + + // receiver2 should now own the shared memory + // receiver1 should be in moved-from state (destructor shouldn't crash) +} + +TEST_F(InterSimulationInterfaceTest, MoveAssignmentTransfersOwnership) { + InterSimulationInterface receiver1(shmName); + InterSimulationInterface receiver2; + + receiver2 = std::move(receiver1); + + // receiver2 should now own the shared memory + // receiver1 should be in moved-from state +} + +// ===== Single Exchange Tests ===== + +TEST_F(InterSimulationInterfaceTest, SingleExchangeBothProcesses) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterSimulationInterface sender(shmName); + bool received = sender.exchange(true); + + // Sender sends true, should receive false from receiver + exit(received ? 1 : 0); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + + // Small delay to ensure both processes are ready + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + bool received = receiver.exchange(false); + + // Receiver sends false, should receive true from sender + EXPECT_TRUE(received); + + // Wait for child and check result + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, ExchangeBothSendTrue) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterSimulationInterface sender(shmName); + bool received = sender.exchange(true); + exit(received ? 0 : 1); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + bool received = receiver.exchange(true); + EXPECT_TRUE(received); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, ExchangeBothSendFalse) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterSimulationInterface sender(shmName); + bool received = sender.exchange(false); + exit(received ? 1 : 0); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + bool received = receiver.exchange(false); + EXPECT_FALSE(received); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Multiple Exchange Tests ===== + +TEST_F(InterSimulationInterfaceTest, MultipleExchangesSequential) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterSimulationInterface sender(shmName); + + for (int i = 0; i < 10; ++i) { + bool send_val = (i % 2 == 0); + bool received = sender.exchange(send_val); + + // Sender alternates true/false, receiver sends opposite + bool expected = !send_val; + if (received != expected) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 10; ++i) { + bool send_val = (i % 2 != 0); // Opposite of sender + bool received = receiver.exchange(send_val); + + bool expected = !send_val; + EXPECT_EQ(received, expected); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, ManyExchanges) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterSimulationInterface sender(shmName); + + for (int i = 0; i < 1000; ++i) { + bool send_val = (i % 3 == 0); + sender.exchange(send_val); + } + exit(0); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 1000; ++i) { + bool send_val = (i % 5 == 0); + bool received = receiver.exchange(send_val); + + // Just verify exchange completes without deadlock + bool expected_from_sender = (i % 3 == 0); + EXPECT_EQ(received, expected_from_sender); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, AlternatingPattern) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender sends alternating true/false + InterSimulationInterface sender(shmName); + + for (int i = 0; i < 100; ++i) { + bool send_val = (i % 2 == 0); + bool received = sender.exchange(send_val); + + // Receiver also alternates, but starts with false + bool expected = (i % 2 != 0); + if (received != expected) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver sends alternating false/true + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 100; ++i) { + bool send_val = (i % 2 != 0); + bool received = receiver.exchange(send_val); + + bool expected = (i % 2 == 0); + EXPECT_EQ(received, expected); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Buffer Flipping Tests ===== + +TEST_F(InterSimulationInterfaceTest, BufferFlipsCorrectly) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterSimulationInterface sender(shmName); + + // Perform multiple exchanges to trigger buffer flips + for (int i = 0; i < 20; ++i) { + sender.exchange(true); + } + exit(0); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Perform multiple exchanges - buffer should flip multiple times + for (int i = 0; i < 20; ++i) { + bool received = receiver.exchange(false); + EXPECT_TRUE(received); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Stress Tests ===== + +TEST_F(InterSimulationInterfaceTest, HighFrequencyExchanges) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender - rapid exchanges + InterSimulationInterface sender(shmName); + + for (int i = 0; i < 10000; ++i) { + sender.exchange(i & 1); // Alternate between true/false + } + exit(0); + } else { + // Parent process: Receiver - rapid exchanges + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 10000; ++i) { + receiver.exchange(!(i & 1)); // Opposite pattern + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, StressTestWithComplexPattern) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with complex pattern + InterSimulationInterface sender(shmName); + + for (int i = 0; i < 5000; ++i) { + bool val = ((i * 7) % 11) < 5; // Pseudo-random pattern + sender.exchange(val); + } + exit(0); + } else { + // Parent process: Receiver with different complex pattern + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 5000; ++i) { + bool val = ((i * 13) % 17) < 8; // Different pseudo-random pattern + receiver.exchange(val); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Reference Counting Tests ===== + +TEST_F(InterSimulationInterfaceTest, ReferenceCountingTwoProcesses) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Create sender and let it go out of scope + { + InterSimulationInterface sender(shmName); + sender.exchange(true); + } + + // Shared memory should still exist because parent still holds reference + bool shmExists = false; + try { + boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); + shmExists = true; + } catch (...) { shmExists = false; } + + exit(shmExists ? 0 : 1); + } else { + // Parent process: Keep receiver alive + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + receiver.exchange(false); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, SharedMemoryCleanupAfterBothProcessesExit) { + // This test verifies that shared memory is properly cleaned up + // when both processes exit. We need to test from a third process + // that was never part of the shared memory to avoid race conditions. + + pid_t verifier_pid = fork(); + + if (verifier_pid == 0) { + // Verifier process: spawns two children and then checks cleanup + pid_t receiver_pid = fork(); + + if (receiver_pid == 0) { + // First child: Receiver + // Use block scope so destructor is called before exit + { + InterSimulationInterface receiver(shmName); + receiver.exchange(true); + } // Destructor called here + exit(0); + } + + // Small delay to ensure receiver creates shared memory + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + pid_t sender_pid = fork(); + if (sender_pid == 0) { + // Second child: Sender + // Use block scope so destructor is called before exit + { + InterSimulationInterface sender(shmName); + sender.exchange(false); + } // Destructor called here + exit(0); + } + + // Wait for both children to complete + int receiver_status, sender_status; + waitpid(receiver_pid, &receiver_status, 0); + waitpid(sender_pid, &sender_status, 0); + + // Give time for cleanup to complete + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Verify shared memory is cleaned up + bool shmExists = false; + try { + boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); + shmExists = true; + } catch (...) { + shmExists = false; + } + + // Exit with 0 if cleanup succeeded (shmExists == false) + exit(shmExists ? 1 : 0); + } else { + // Parent: Wait for verifier process + int status; + waitpid(verifier_pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Move Semantics Tests ===== + +TEST_F(InterSimulationInterfaceTest, MoveConstructorMaintainsConnection) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with move + InterSimulationInterface sender1(shmName); + InterSimulationInterface sender2(std::move(sender1)); + + bool received = sender2.exchange(true); + exit(received ? 1 : 0); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + bool received = receiver.exchange(false); + EXPECT_TRUE(received); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, MoveAssignmentMaintainsConnection) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with move assignment + InterSimulationInterface sender1(shmName); + InterSimulationInterface sender2; + sender2 = std::move(sender1); + + bool received = sender2.exchange(true); + exit(received ? 1 : 0); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + bool received = receiver.exchange(false); + EXPECT_TRUE(received); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Edge Cases ===== + +TEST_F(InterSimulationInterfaceTest, FirstCallBehavior) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender - first call should not wait for buffer flip + InterSimulationInterface sender(shmName); + + auto start = std::chrono::steady_clock::now(); + sender.exchange(true); + auto end = std::chrono::steady_clock::now(); + + // First call should complete quickly (not waiting for previous flip) + auto duration = std::chrono::duration_cast(end - start); + exit(duration.count() < 100 ? 0 : 1); + } else { + // Parent process: Receiver + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + receiver.exchange(false); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterSimulationInterfaceTest, ConsecutiveExchangesSameValue) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Send same value repeatedly + InterSimulationInterface sender(shmName); + + for (int i = 0; i < 50; ++i) { + sender.exchange(true); // Always true + } + exit(0); + } else { + // Parent process: Verify same value received repeatedly + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 50; ++i) { + bool received = receiver.exchange(false); + EXPECT_TRUE(received); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Timing and Synchronization Tests ===== + +TEST_F(InterSimulationInterfaceTest, SynchronizationBetweenProcesses) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender - delayed start + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + InterSimulationInterface sender(shmName); + + for (int i = 0; i < 10; ++i) { + sender.exchange(true); + } + exit(0); + } else { + // Parent process: Receiver - starts immediately + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Should wait for sender to be ready + for (int i = 0; i < 10; ++i) { + receiver.exchange(false); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Custom Shared Memory Size Tests ===== + +TEST_F(InterSimulationInterfaceTest, CustomSharedMemorySize) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with larger shared memory + InterSimulationInterface sender(shmName); + sender.exchange(true); + exit(0); + } else { + // Parent process: Receiver with larger shared memory + InterSimulationInterface receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + bool received = receiver.exchange(false); + EXPECT_TRUE(received); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// Main function to run all tests +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index df1a5d8456..9a8813ff3e 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -192,14 +192,14 @@ def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, assert first_node is not None, "Failed to find consumer for " + iname top_ind = list(first_node.input).index(iname) ishape_folded = getCustomOp(first_node).get_folded_input_shape(ind=top_ind) - instream_iters.append(np.prod(ishape_folded[:-1])) + instream_iters.append(int(np.prod(ishape_folded[:-1]))) for top_out in model.graph.output: oname = top_out.name last_node = model.find_producer(oname) assert last_node is not None, "Failed to find producer for " + oname top_ind = list(last_node.output).index(oname) oshape_folded = getCustomOp(last_node).get_folded_output_shape(ind=top_ind) - outstream_iters.append(np.prod(oshape_folded[:-1])) + outstream_iters.append(int(np.prod(oshape_folded[:-1]))) interface_names = model.get_metadata_prop("vivado_stitch_ifnames") if interface_names is None: raise FINNUserError( @@ -290,6 +290,8 @@ def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: proc_env=os.environ.copy(), ) except CalledProcessError as e: + print(e.stdout) + print(e.stderr) raise FINNUserError(f"Failed to run cmake in {sim_base}") from e self.progress_bar.update("CMake") From efc49c920d899e04577df346fab5094c9a5b95c6 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:04:38 +0100 Subject: [PATCH 033/170] Fix fifosim --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 235 +++++++- finn_xsi/finn_xsi/include/AXIS_Control.h | 6 +- .../include/InterSimulationInterface.hpp | 15 +- finn_xsi/finn_xsi/include/Simulation.hpp | 412 ++++---------- finn_xsi/finn_xsi/include/SocketServer.h | 40 ++ .../finn_xsi/include/StableStateTracker.hpp | 84 +++ finn_xsi/finn_xsi/src/SocketServer.cpp | 144 +++++ .../transformation/fpgadataflow/simulation.py | 35 +- .../fpgadataflow/simulation_controller.py | 521 ++++++++++++++---- 9 files changed, 1044 insertions(+), 448 deletions(-) create mode 100644 finn_xsi/finn_xsi/include/SocketServer.h create mode 100644 finn_xsi/finn_xsi/include/StableStateTracker.hpp create mode 100644 finn_xsi/finn_xsi/src/SocketServer.cpp diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 90a98a8532..82853cc0c4 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -5,47 +5,236 @@ #include #include #include +#include + +#include +#include #include #include -#include -#include -#include +#include +#include #define NDEBUG -#include #include +#include namespace po = boost::program_options; -constexpr bool CommunicateWithPredecessor = (RTLSimConfig::NodeIndex != 0); -constexpr bool CommunicateWithSuccessor = (RTLSimConfig::NodeIndex != RTLSimConfig::TotalNodes - 1); constexpr std::size_t InstreamCount = RTLSimConfig::istream_descs.size(); constexpr std::size_t OutstreamCount = RTLSimConfig::ostream_descs.size(); +// Simulation state management +enum class SimulationState { IDLE, CONFIGURED, RUNNING, FINISHED, ERROR }; + +class SimulationController { + private: + SingleNodeSimulation& sim; + std::atomic state{SimulationState::IDLE}; + std::atomic current_cycles{0}; + std::atomic current_samples{0}; + std::atomic target_samples{0}; + std::mutex state_mutex; + std::string error_message; + std::jthread sim_thread; + std::size_t fifo_depth{2}; + + public: + explicit SimulationController(SingleNodeSimulation& simulation) + : sim(simulation) {} + + void configure(std::size_t depth, uint64_t samples) { + std::lock_guard lock(state_mutex); + if (state != SimulationState::IDLE && state != SimulationState::FINISHED) { + throw std::runtime_error("Cannot configure while simulation is running"); + } + fifo_depth = depth; + target_samples = samples; + current_cycles = 0; + current_samples = 0; + state = SimulationState::CONFIGURED; + } + + void start() { + std::lock_guard lock(state_mutex); + if (state != SimulationState::CONFIGURED) { + throw std::runtime_error("Simulation must be configured before starting"); + } + + state = SimulationState::RUNNING; + + // Start simulation in a separate thread + sim_thread = std::jthread([this](std::stop_token stoken) { + try { + // Configure simulation + sim.setMaxFIFODepth(fifo_depth); + sim.reset(); + + // Run the simulation + sim.runToStableState(stoken); + + // Update state based on completion + if (!stoken.stop_requested()) { + current_samples.store(target_samples.load()); + state = SimulationState::FINISHED; + } + } catch (const std::exception& e) { + std::lock_guard error_lock(state_mutex); + error_message = e.what(); + state = SimulationState::ERROR; + } + }); + } + + void stop() { + if (sim_thread.joinable()) { + sim_thread.request_stop(); + sim_thread.join(); + } + if (state == SimulationState::RUNNING) { + state = SimulationState::FINISHED; + } + } + + json get_status() const { + json status; + status["status"] = "success"; + + SimulationState current_state = state.load(); + switch (current_state) { + case SimulationState::IDLE: + status["state"] = "idle"; + break; + case SimulationState::CONFIGURED: + status["state"] = "configured"; + break; + case SimulationState::RUNNING: + status["state"] = "running"; + status["cycles"] = sim.getCyclesRun(); + status["samples"] = sim.getCompletedMaps(); + break; + case SimulationState::FINISHED: + status["state"] = "finished"; + status["cycles"] = sim.getCyclesRun(); + status["samples"] = sim.getCompletedMaps(); + // Add FIFO utilization data + { + auto utilizations = sim.getFIFOUtilization(); + json fifo_util = json::array(); + for (size_t i = 0; i < utilizations.size(); ++i) { + fifo_util.push_back(utilizations[i]); + } + if (!fifo_util.empty()) { + status["fifo_utilization"] = fifo_util; + } + } + break; + case SimulationState::ERROR: + status["state"] = "error"; + status["message"] = error_message; + break; + } + + return status; + } + + ~SimulationController() { stop(); } +}; + +void process_command(const json& request, json& response, SimulationController& controller) { + const std::string command = request["command"]; + const json& payload = request["payload"]; + + try { + if (command == "configure") { + std::size_t fifo_depth = payload.value("fifo_depth", 2ULL); + uint64_t samples = payload.value("samples", 1ULL); + controller.configure(fifo_depth, samples); + response["status"] = "success"; + response["message"] = "Configuration successful"; + } else if (command == "start") { + controller.start(); + response["status"] = "success"; + response["message"] = "Simulation started"; + } else if (command == "status") { + response = controller.get_status(); + } else if (command == "stop") { + controller.stop(); + response["status"] = "success"; + response["message"] = "Simulation stopped"; + // Include final status with FIFO utilization + json final_status = controller.get_status(); + if (final_status.contains("fifo_utilization")) { + response["fifo_utilization"] = final_status["fifo_utilization"]; + } + if (final_status.contains("cycles")) { + response["cycles"] = final_status["cycles"]; + } + if (final_status.contains("samples")) { + response["samples"] = final_status["samples"]; + } + } else { + response["status"] = "error"; + response["message"] = "Unknown command: " + command; + } + } catch (const std::exception& e) { + response["status"] = "error"; + response["message"] = std::string("Error: ") + e.what(); + } +} + int main(int argc, const char* argv[]) { // Parse CLI options po::options_description desc{"Options"}; - desc.add_options() - ("output,o", po::value()->default_value("simulation_data.json"), "Simulation Data Output"); + desc.add_options()("socket,s", po::value(), + "Unix domain socket path for IPC"); po::variables_map vm; po::store(po::parse_command_line(argc, argv, desc), vm); po::notify(vm); // Construct simulation - SingleNodeSimulation sim( - RTLSimConfig::kernel_libname, - RTLSimConfig::design_libname, - "xsim_log_file.txt", - "trace_file.txt", - RTLSimConfig::istream_descs, - RTLSimConfig::ostream_descs, - RTLSimConfig::previousNodeName, - RTLSimConfig::currentNodeName, - 2, - vm["output"].as() - ); - std::this_thread::sleep_for(std::chrono::milliseconds(2000)); - sim.start(); + SingleNodeSimulation sim( + RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, "xsim_log_file.txt", "trace_file.txt", RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs, + RTLSimConfig::previousNodeName, RTLSimConfig::currentNodeName, 2); + + // Create simulation controller + SimulationController controller(sim); + + // Check if socket communication is enabled + if (vm.count("socket")) { + const std::string socket_path = vm["socket"].as(); + std::cout << "Initializing socket server at: " << socket_path << std::endl; + std::cout.flush(); + + SocketServer server(socket_path); + if (auto error = server.initialize(); error.has_value()) { + std::cerr << "Failed to initialize socket server: " << *error << std::endl; + std::cerr.flush(); + return 1; + } + + std::cout << "Socket server initialized, waiting for commands..." << std::endl; + std::cout.flush(); + + // Command processing loop + while (true) { + auto request = server.receive_message(); + if (!request.has_value()) { + std::cout << "Connection closed or error occurred" << std::endl; + break; + } + + json response; + process_command(*request, response, controller); + server.send_message(response); + + // Exit if stop command received + if ((*request)["command"] == "stop") { + break; + } + } + } else { + throw std::runtime_error("Socket path not provided. Socket communication is required."); + } + return 0; } diff --git a/finn_xsi/finn_xsi/include/AXIS_Control.h b/finn_xsi/finn_xsi/include/AXIS_Control.h index f2e936337f..d357b9303d 100644 --- a/finn_xsi/finn_xsi/include/AXIS_Control.h +++ b/finn_xsi/finn_xsi/include/AXIS_Control.h @@ -2,9 +2,10 @@ #define AXIS_CONTROL #include -#include #include +#include + // Fwd declarations namespace xsi { class Design; @@ -77,8 +78,7 @@ class M_AXIS_Control : public AXIS_Control { size_t lastComplete = 0; size_t interval; - size_t latency = 0; - size_t minLatency = std::numeric_limits::max(); // Minimum latency observed + StableStateTracker<> stableState; }; #endif /* AXIS_CONTROL */ diff --git a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp index 3dc6939e05..08e0a350fa 100644 --- a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp +++ b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp @@ -93,7 +93,8 @@ class InterSimulationInterface { InterSimulationInterface& operator=(const InterSimulationInterface&) = delete; // Move constructor - InterSimulationInterface(InterSimulationInterface&& other) noexcept : halo(other.halo), refCount(other.refCount), sharedMemoryName(std::move(other.sharedMemoryName)), shmem(std::move(other.shmem)) { + InterSimulationInterface(InterSimulationInterface&& other) noexcept + : halo(other.halo), refCount(other.refCount), sharedMemoryName(std::move(other.sharedMemoryName)), shmem(std::move(other.shmem)) { // Mark other as moved-from other.halo = nullptr; other.refCount = nullptr; @@ -153,16 +154,19 @@ class InterSimulationInterface { // ===== EXCHANGE FUNCTION ===== // This function runs in each process - bool exchange(bool send_value) { + bool exchange(bool send_value, std::stop_token stoken = {}) { constexpr int neighbor_id = 1 - this->local.process_id; // Wait for previous buffer flip (latency hiding) if (!local.first_call) { - while (this->halo->current_buffer.load(std::memory_order_acquire) % 2 == local.expected_buf) { + while (this->halo->current_buffer.load(std::memory_order_acquire) % 2 == local.expected_buf && !stoken.stop_requested()) { #if defined(__x86_64__) || defined(_M_X64) __builtin_ia32_pause(); #endif } + if (stoken.stop_requested()) { + return false; // Early termination + } } local.first_call = false; @@ -177,11 +181,14 @@ class InterSimulationInterface { int neighbor_idx = SharedHaloExchange::idx(neighbor_id, buf_id); bool neighbor_ready = this->halo->buffers[neighbor_idx].ready.load(std::memory_order_acquire); if (!neighbor_ready) { - while (!this->halo->buffers[neighbor_idx].ready.load(std::memory_order_acquire)) { + while (!this->halo->buffers[neighbor_idx].ready.load(std::memory_order_acquire) && !stoken.stop_requested()) { #if defined(__x86_64__) || defined(_M_X64) __builtin_ia32_pause(); #endif } + if (stoken.stop_requested()) { + return false; // Early termination + } } bool received = this->halo->buffers[neighbor_idx].value.load(std::memory_order_acquire); diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 498ae6b326..12c58c0ad5 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -10,19 +10,17 @@ #include #include +#include #include #include -#include #include #include #include -#include #include #include +#include #include #include -#include -using json = nlohmann::json; template @@ -102,142 +100,148 @@ class Simulation { // │ ready ready │ // │ (sim) │ // └──────────────────────────────────────┘ -template -class _SingleNodeSimulation : public Simulation { - private: +template +class SingleNodeSimulation : public Simulation { using ConsumingInterface = InterSimulationInterface; using ProducingInterface = InterSimulationInterface; + constexpr static bool FirstNode = NodeIndex == 0; + constexpr static bool LastNode = NodeIndex == (TotalNodes - 1); std::array fromProducerInterface; std::array toConsumerInterface; std::size_t cyclesRun = 0; + std::size_t completedMaps = 0; + std::array fifo; - - public: - // TODO: Move to private, currently only here for debugging purposes - std::array(OStreamsSize)> fifo; - - _SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, - std::array _istream_descs, std::array _ostream_descs, - std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2) - : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { - if (CommunicatesWithPredecessor && !prevNodeName) { - throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); - } else if (!CommunicatesWithPredecessor && prevNodeName) { - std::cout << "log Simulation was passed the previous nodes name but is " - "NOT marked for communication with predecessor node. No " - "shared memory will be created." - << std::endl; - } - if (CommunicatesWithSuccessor && !nodeName) { - throw std::runtime_error( - "Cannot communicate with successor because " - "current node name was not given!"); - } else if (!CommunicatesWithSuccessor && nodeName) { - std::cout << "log Simulation was passed the current nodes name but is NOT " - "marked for communication with successor node. No shared " - "memory will be created." - << std::endl; - } - // Create FIFO buffer - for (std::size_t i = 0; i < OStreamsSize; ++i) { - fifo[i] = FIFO(initialFIFODepth); - } - - // Create consumer facing interfaces - for (std::size_t i = 0; i < OStreamsSize; ++i) { - toConsumerInterface[i] = std::move(ProducingInterface(std::format("{}_{}", *nodeName, i))); - } - if constexpr (NodeIndex != 0) { - for (std::size_t i = 0; i < IStreamsSize; ++i) { - fromProducerInterface[i] = std::move(ConsumingInterface(std::format("{}_{}", *prevNodeName, i))); - } - } - - initStreams(); - debug("log Finished initializing simulation.\nlog ------------------------------\n"); - } - - - private: /// Communicate with predecessors and successors and update their values and our own - [[gnu::hot, gnu::flatten, gnu::always_inline]] void communicate() { - if constexpr (NodeIndex != 0) { + [[gnu::hot, gnu::flatten, gnu::always_inline]] void communicate(std::stop_token stoken = {}) { + if constexpr (!FirstNode) { for (std::size_t i = 0; i < IStreamsSize; ++i) { // Interface SHM <-> sim - this->istreams[i].valid(fromProducerInterface[i].exchange(this->istreams[i].isReady())); + this->istreams[i].valid(fromProducerInterface[i].exchange(this->istreams[i].isReady(), stoken)); } } - if constexpr (NodeIndex != TotalNodes - 1) { + if constexpr (!LastNode) { for (std::size_t i = 0; i < OStreamsSize; ++i) { // Interface sim -valid-> FIFO <-> SHM - this->fifo[i].update(this->ostreams[i].isValid(), toConsumerInterface[i].exchange(this->fifo[i].isOutputValid())); + this->fifo[i].update(this->ostreams[i].isValid(), toConsumerInterface[i].exchange(this->fifo[i].isOutputValid(), stoken)); // FIFO -ready-> sim this->ostreams[i].ready(this->fifo[i].isInputReady()); // Toggle FIFO clock this->fifo[i].toggleClock(); } } + if constexpr (LastNode) { + for (auto&& stream : this->ostreams) { + if (stream.isValid() && ++stream.job_txns == stream.job_size) { + static std::vector debug_intervals; + // Track job completion and intervals + std::size_t lastComplete = stream.lastComplete; + stream.interval = cyclesRun - lastComplete; + stream.lastComplete = cyclesRun; + stream.job_txns = 0; + ++completedMaps; + if (lastComplete != 0){ + // Update stable state tracker + stream.stableState.update(stream.interval); + } + } + } + } } - public: /** * Initialize streams according to nodeindex */ void initStreams() { - if constexpr (NodeIndex == 0) { // First Node; no predecessor + if constexpr (FirstNode) { // First Node; no predecessor for (auto&& s : this->istreams) { // Input into sim valid s.valid(true); } - } else if constexpr (NodeIndex == TotalNodes - 1) { // Last Node; no successor - for (auto&& s : this->ostreams) { // Output from sim ready + } else if constexpr (LastNode) { // Last Node; no successor + for (auto&& s : this->ostreams) { // Output from sim ready s.ready(true); } } } - /// Write the current ready and valid states into the filestreams - void logReadyValidState() - requires LoggingEnabled - { /* TODO: Match the new extra FIFO implementation*/ } + [[gnu::hot, gnu::always_inline]] void runSingleCycle(std::stop_token stoken = {}) { + ++cyclesRun; + communicate(stoken); + this->clk.toggleClk(); + } + + public: + SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, + std::array _istream_descs, std::array _ostream_descs, + std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2) + : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { + if (!FirstNode && !prevNodeName) { + throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); + } else if (FirstNode && prevNodeName) { + std::cout << "Simulation was passed the previous nodes name but is " + "NOT marked for communication with predecessor node. No " + "shared memory will be created." + << std::endl; + } + if (!LastNode && !nodeName) { + throw std::runtime_error( + "Cannot communicate with successor because " + "current node name was not given!"); + } else if (LastNode && nodeName) { + std::cout << "Simulation was passed the current nodes name but is NOT " + "marked for communication with successor node. No shared " + "memory will be created." + << std::endl; + } + if constexpr (!LastNode) { + // Create FIFO buffer + for (std::size_t i = 0; i < OStreamsSize; ++i) { + fifo[i] = FIFO(initialFIFODepth); + } + } + + + if constexpr (!FirstNode) { + for (std::size_t i = 0; i < IStreamsSize; ++i) { + fromProducerInterface[i] = std::move(ConsumingInterface(std::format("{}_{}", *prevNodeName, i))); + } + } + + if constexpr (!LastNode) { + // Create consumer facing interfaces + for (std::size_t i = 0; i < OStreamsSize; ++i) { + toConsumerInterface[i] = std::move(ProducingInterface(std::format("{}_{}", *nodeName, i))); + } + } + + initStreams(); + debug("Finished initializing simulation.\nlog ------------------------------\n"); + } /// Reset simulation (stream and current FIFO depth, as well as cycle counter) void reset() { Simulation::reset(); - // for (std::size_t i = 0; i < OStreamsSize; ++i) { - // toConsumerInterface[i].reset(); - // fifo[i].reset(); - // } - // if constexpr (NodeIndex != 0) { - // for (std::size_t i = 0; i < IStreamsSize; ++i) { - // fromProducerInterface[i].reset(); - // } - // } - } - - /// Return whether all connected FIFOs are empty - bool allFIFOsEmpty() const { - return std::all_of(fifo.begin(), fifo.end(), [](const FIFO& f) { return f.isEmpty(); }); + if constexpr (!LastNode) { + // Reset FIFOs + for (std::size_t i = 0; i < OStreamsSize; ++i) { + fifo[i].reset(); + } + } } - /// Finish communication by exchanging FIFO contents with the successor, but not - /// interacting with the predecessor anymore. Runs until the FIFO is empty - [[gnu::hot]] void finishCommunication() { - while (!allFIFOsEmpty()) { - // for (std::size_t i = 0; i < toConsumerInterface.size(); ++i) { - // this->fifo[i].tryPop(toConsumerInterface[i].writeToNextNode(this->fifo[i].isOutputValid())); - // } - runSingleCycle(); + [[gnu::hot, gnu::always_inline]] void runFeatureMaps(std::size_t featureMaps, std::stop_token stoken = {}) { + completedMaps = 0; + while (completedMaps < featureMaps && !stoken.stop_requested()) { + runSingleCycle(stoken); } } - [[gnu::hot, gnu::always_inline]] void runSingleCycle() { - ++cyclesRun; - communicate(); - this->clk.toggleClk(); - if constexpr (LoggingEnabled) { - logReadyValidState(); + [[gnu::hot, gnu::always_inline]] void runToStableState(std::stop_token stoken = {}) { + while (std::all_of(this->ostreams.begin(), this->ostreams.end(), + [](const M_AXIS_Control& stream) { return stream.stableState.is_stable(); }) == false && + !stoken.stop_requested()) { + runSingleCycle(stoken); } - debug(std::format("Finished cycle {}\n\n", cyclesRun)); } /// Set the max FIFO depth of all interfaces @@ -252,213 +256,23 @@ class _SingleNodeSimulation : public Simulationistreams[inputIndex].job_size; } -}; - -/// Single Node Simulation, thread controlled -template -class SingleNodeSimulation { - private: - std::jthread simulator; - std::jthread communicator; - - // Only run cycles if True - // TODO: Atomic? - bool running; - - std::size_t samplesTarget; - std::size_t samplesProduced; - std::size_t validCyclesProduced; - std::size_t cyclesTarget; - std::size_t cyclesRun; - - - // Path on which to store simulation data after stopping - std::filesystem::path simulationDataPath; - - // The simulation itself - _SingleNodeSimulation sim; - - public: - SingleNodeSimulation(const std::string& kernel_lib, const std::string& design_lib, const char* xsim_log_file, const char* trace_file, - std::array _istream_descs, std::array _ostream_descs, - std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2, - std::string simulationDataFilename = "simulation_data.json") - : running(false), - samplesProduced(0), - samplesTarget(0), - validCyclesProduced(0), - cyclesRun(0), - cyclesTarget(0), - simulationDataPath(simulationDataFilename), - sim(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs, prevNodeName, nodeName, initialFIFODepth) {} - - - /// Stop and reset the simulation, reset cycle counter, target cycle counter, log queue. - /// Leaves the simulation in a paused and reset state. - void reset() { - running = false; - sim.reset(); - samplesTarget = 0; - samplesProduced = 0; - } + /// Get the number of cycles the simulation has run + std::size_t getCyclesRun() const noexcept { return cyclesRun; } - /// Write the results of the simulation as a JSON file - void writeResults() { - json j; - // TODO: Reintroduce - // for (std::size_t i = 0; i < OStreamsSize; ++i) { - // j["maxOccupation"][std::to_string(i)] = sim.getLargestOccupation(i); - // } - // j["cyclesRun"] = outValidCycles; - std::ofstream file(simulationDataPath); - file << j.dump(4); - file.close(); - } + /// Get the number of completed feature maps + std::size_t getCompletedMaps() const noexcept { return completedMaps; } - /// Split the given string in two, delimited by a space. Subsequent spaces are ignored. If no - /// space is found, the second element is empty. - std::tuple splitSpace(std::string& s) { - auto pos = s.find(" "); - if (s == "") { - return std::make_tuple("", ""); + /// Get the maximum FIFO utilization for each output stream + std::array getFIFOUtilization() const noexcept { + if constexpr (LastNode) { + return {}; } - return std::make_tuple(s.substr(0, pos), s.substr(pos)); - } - - /// Read from std::cin if possible, otherwise - /// return immediately. - void getlineIfAvailable(std::string& buffer) { - // std::cin.exceptions(std::istream::failbit | std::istream::badbit); - if (std::cin.eof() || std::cin.rdbuf()->in_avail() == -1) { - buffer = ""; - return; + std::array utilizations{}; + for (std::size_t i = 0; i < OStreamsSize; ++i) { + utilizations[i] = fifo[i].getMaxUtil(); } - std::getline(std::cin, buffer); - } - - - void sendLog(std::string message) { std::cout << "log " << message << std::endl; } - void sendError(std::string message) { std::cout << "error " << message << std::endl; } - void sendEnd() { std::cout << "end" << std::endl; } - void sendCycles() { std::cout << "cycles " << cyclesRun << " " << cyclesTarget << std::endl; } - void sendSamples() { std::cout << "samples " << samplesProduced << " " << samplesTarget << std::endl; } - void sendStarted() { std::cout << "started" << std::endl; } - void sendStopped() { std::cout << "stopped" << std::endl; } - void sendReady() { std::cout << "ready" << std::endl; } - - - /// Start both threads. Listen for commands on stdin. - void start() { - simulator = std::jthread([this](std::stop_token stop) { - /// Prepare and wait for data from the controller - while (samplesTarget == 0 && cyclesTarget == 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - /// Run the simulation - sendStarted(); - if constexpr (NodeIndex == TotalNodes - 1) { - while ((samplesTarget != 0 && samplesProduced < samplesTarget) || (cyclesTarget != 0 && cyclesRun < cyclesTarget)) { - if (!running) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - continue; - } - sim.runSingleCycle(); - ++cyclesRun; - if (std::all_of(sim.ostreams.begin(), sim.ostreams.end(), [](M_AXIS_Control& s) { return s.isValid(); })) { - ++validCyclesProduced; - // TODO: For all streams - samplesProduced = validCyclesProduced / sim.ostreams[0].job_size; - sendSamples(); - } - } - sendStopped(); - } else { - bool validSeen = false; - while (!running) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - while (running) { - sim.runSingleCycle(); - if (!validSeen && sim.ostreams[0].isValid()) { - validSeen = true; - sendLog("First valid sample seen"); - } - } - } - // Finish communicating and sent an update to the controller - sim.finishCommunication(); - sendStopped(); - if constexpr (NodeIndex == TotalNodes - 1) { - sendEnd(); - } - }); - - communicator = std::jthread([this]() { - // Run initial sanity checks - if constexpr (NodeIndex == 0) { - if (!std::all_of(sim.istreams.begin(), sim.istreams.end(), [](S_AXIS_Control& stream) { return stream.isValid(); })) { - sendLog("ERROR: First node input is not set to valid!"); - // TODO: Stop - } - } else if constexpr (NodeIndex == TotalNodes - 1) { - if (!std::all_of(sim.ostreams.begin(), sim.ostreams.end(), [](M_AXIS_Control& stream) { return stream.isReady(); })) { - sendLog("ERROR: Last node output is not set to ready!"); - // TODO: Stop - } - } - sendReady(); - std::string input = ""; - while (true) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Parse incoming commands - getlineIfAvailable(input); - if (input == "") { - continue; - } - auto [command, argument] = splitSpace(input); - - // React to command - if (command == "stop") { - simulator.request_stop(); - writeResults(); - // Wait for the file to be fully written - std::this_thread::sleep_for(std::chrono::milliseconds(1000)); - // Signal python that we are done - sendEnd(); - return; - } else if (command == "fifodepth") { - std::size_t newDepth = static_cast(std::stoul(argument)); - running = false; - sim.setMaxFIFODepth(newDepth); - running = true; - sendLog(std::format("Set FIFO depth to {}", newDepth)); - } else if (command == "runCycles") { - cyclesTarget += static_cast(std::stoul(argument)); - running = true; - sendLog(std::format("Set running with {}", cyclesTarget)); - } else if (command == "runSamples") { - // TODO: Make generic for any number of streams (max) - samplesTarget += static_cast(std::stoul(argument)); - running = true; - sendLog(std::format("Set running with a target of {} samples!", samplesTarget)); - } else if (command == "pause") { - running = false; - } else if (command == "reset") { - reset(); - } else if (command == "resume") { - running = true; - } else if (command == "help") { - // TODO: Insert here or document separately - } else { - sendLog("Unknown command."); - } - } - }); - simulator.join(); - communicator.join(); + return utilizations; } }; diff --git a/finn_xsi/finn_xsi/include/SocketServer.h b/finn_xsi/finn_xsi/include/SocketServer.h new file mode 100644 index 0000000000..3b9778d50c --- /dev/null +++ b/finn_xsi/finn_xsi/include/SocketServer.h @@ -0,0 +1,40 @@ +#ifndef SOCKET_SERVER_H +#define SOCKET_SERVER_H + +#include +#include +#include +#include + +using json = nlohmann::json; + +class SocketServer { + private: + int server_fd{-1}; + int client_fd{-1}; + std::string socket_path; + + void close_fd(int& fd) noexcept; + + public: + explicit SocketServer(std::string_view path); + ~SocketServer(); + + // Disable copy construction and assignment + SocketServer(const SocketServer&) = delete; + SocketServer& operator=(const SocketServer&) = delete; + + // Enable move semantics + SocketServer(SocketServer&& other) noexcept; + SocketServer& operator=(SocketServer&& other) noexcept; + + // Returns std::nullopt on success, error message on failure + [[nodiscard]] std::optional initialize(); + [[nodiscard]] std::optional receive_message(); + void send_message(const json& message); + void close_connection() noexcept; + + [[nodiscard]] bool is_connected() const noexcept { return client_fd >= 0; } +}; + +#endif // SOCKET_SERVER_H diff --git a/finn_xsi/finn_xsi/include/StableStateTracker.hpp b/finn_xsi/finn_xsi/include/StableStateTracker.hpp new file mode 100644 index 0000000000..e4e614f18f --- /dev/null +++ b/finn_xsi/finn_xsi/include/StableStateTracker.hpp @@ -0,0 +1,84 @@ +#ifndef STABLESTATETRACKER_HPP +#define STABLESTATETRACKER_HPP + +#include +#include + +/** + * Implements an Exponential Moving Average (EMA) tracker with stability detection. + * The tracker updates its EMA with new unsigned integral values and checks for stability + * based on relative changes over consecutive updates. + */ +template + requires (Alpha > 0 && Alpha <= 1) && + (StabilityThreshold > 0 && StabilityThreshold < 1) && + (RequiredStableCount > 0) +class StableStateTracker { +private: + static constexpr double InvAlpha = 1.0 - Alpha; + static constexpr double SquaredStabilityThreshold = StabilityThreshold * StabilityThreshold; + + double ema; + uint8_t stableCount; + +public: + constexpr StableStateTracker() noexcept + : ema{0.0} + , stableCount{0} + { + } + + /** + * Update with new interval value + * Concepts ensure only unsigned integral types are accepted + */ + inline void update(std::unsigned_integral auto value) noexcept { + // First update initializes directly + if (ema == 0.0) [[unlikely]] { + ema = static_cast(value); + stableCount = 0; + return; + } + + const double oldEma = ema; + const double valDouble = static_cast(value); + + // EMA calculation: ema = value + (1-alpha) * (oldEma - value) + ema = valDouble + InvAlpha * (oldEma - valDouble); + + // Stability check: |change|² / oldEma² < threshold² + // Avoids sqrt and abs operations + const double diff = ema - oldEma; + const double squaredRelativeChange = (diff * diff) / (oldEma * oldEma); + + // Branchless increment/reset using arithmetic + const bool is_change_small = squaredRelativeChange < SquaredStabilityThreshold; + stableCount = is_change_small * (stableCount + (stableCount < RequiredStableCount)); + } + + [[nodiscard]] constexpr double get_ema() const noexcept { + return ema; + } + + [[nodiscard]] constexpr bool is_stable() const noexcept { + return stableCount >= RequiredStableCount; + } + + [[nodiscard]] constexpr uint8_t get_stable_count() const noexcept { + return stableCount; + } + + constexpr void reset() noexcept { + ema = 0.0; + stableCount = 0; + } + + // Get compile-time parameters + static consteval double get_alpha() { return Alpha; } + static consteval double get_stability_threshold() { return StabilityThreshold; } + static consteval uint8_t get_required_stable_count() { return RequiredStableCount; } +}; + +#endif // STABLESTATETRACKER_HPP diff --git a/finn_xsi/finn_xsi/src/SocketServer.cpp b/finn_xsi/finn_xsi/src/SocketServer.cpp new file mode 100644 index 0000000000..9aa73970a2 --- /dev/null +++ b/finn_xsi/finn_xsi/src/SocketServer.cpp @@ -0,0 +1,144 @@ +#include +#include +#include +#include + +#include +#include +#include +#include + +SocketServer::SocketServer(std::string_view path) : socket_path(path) {} + +SocketServer::~SocketServer() { close_connection(); } + +SocketServer::SocketServer(SocketServer&& other) noexcept + : server_fd(std::exchange(other.server_fd, -1)), client_fd(std::exchange(other.client_fd, -1)), socket_path(std::move(other.socket_path)) {} + +SocketServer& SocketServer::operator=(SocketServer&& other) noexcept { + if (this != &other) { + close_connection(); + server_fd = std::exchange(other.server_fd, -1); + client_fd = std::exchange(other.client_fd, -1); + socket_path = std::move(other.socket_path); + } + return *this; +} + +void SocketServer::close_fd(int& fd) noexcept { + if (fd >= 0) { + ::close(fd); + fd = -1; + } +} + +std::optional SocketServer::initialize() { + // Create socket + server_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (server_fd < 0) { + return std::format("Failed to create socket: {}", strerror(errno)); + } + + // Remove existing socket file + unlink(socket_path.c_str()); + + // Bind socket + sockaddr_un addr{}; + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1); + + if (bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { + auto error = std::format("Failed to bind socket: {}", strerror(errno)); + close_fd(server_fd); + return error; + } + + // Listen + if (listen(server_fd, 1) < 0) { + auto error = std::format("Failed to listen on socket: {}", strerror(errno)); + close_fd(server_fd); + return error; + } + + // Accept connection + client_fd = accept(server_fd, nullptr, nullptr); + if (client_fd < 0) { + auto error = std::format("Failed to accept connection: {}", strerror(errno)); + close_fd(server_fd); + return error; + } + + return std::nullopt; // Success +} + +std::optional SocketServer::receive_message() { + if (client_fd < 0) { + std::cerr << "Socket not connected" << std::endl; + return std::nullopt; + } + + // Read length prefix + uint32_t length{}; + const ssize_t bytes_read = read(client_fd, &length, sizeof(length)); + if (bytes_read != sizeof(length)) { + if (bytes_read == 0) { + std::cerr << "Connection closed by client" << std::endl; + } else { + std::cerr << std::format("Failed to read message length: {}", strerror(errno)) << std::endl; + } + return std::nullopt; + } + + // Read message + std::string buffer(length, '\0'); + size_t total_read = 0; + while (total_read < length) { + const ssize_t n = read(client_fd, buffer.data() + total_read, length - total_read); + if (n <= 0) { + std::cerr << std::format("Failed to read message data: {}", strerror(errno)) << std::endl; + return std::nullopt; + } + total_read += static_cast(n); + } + + try { + return json::parse(buffer); + } catch (const json::exception& e) { + std::cerr << std::format("Failed to parse JSON: {}", e.what()) << std::endl; + return std::nullopt; + } +} + +void SocketServer::send_message(const json& message) { + if (client_fd < 0) { + std::cerr << "Socket not connected" << std::endl; + return; + } + + const std::string msg_str = message.dump(); + const uint32_t length = static_cast(msg_str.size()); + + // Send length prefix + const ssize_t bytes_written = write(client_fd, &length, sizeof(length)); + if (bytes_written != sizeof(length)) { + std::cerr << std::format("Failed to write message length: {}", strerror(errno)) << std::endl; + return; + } + + // Send message + size_t total_written = 0; + while (total_written < length) { + const ssize_t n = write(client_fd, msg_str.data() + total_written, length - total_written); + if (n <= 0) { + std::cerr << std::format("Failed to write message data: {}", strerror(errno)) << std::endl; + return; + } + total_written += static_cast(n); + } +} + +void SocketServer::close_connection() noexcept { + close_fd(client_fd); + close_fd(server_fd); + unlink(socket_path.c_str()); +} diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 9a8813ff3e..09fc380c5f 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,4 +1,5 @@ """Manage FINN simulation variants.""" + import finn_xsi.adapter as finnxsi import json import numpy as np @@ -290,8 +291,6 @@ def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: proc_env=os.environ.copy(), ) except CalledProcessError as e: - print(e.stdout) - print(e.stderr) raise FINNUserError(f"Failed to run cmake in {sim_base}") from e self.progress_bar.update("CMake") @@ -539,7 +538,7 @@ def _build( # the compiled executables with DisabledLoggingConsole(), self.progress_bar if with_live_display else nullcontext(): self.progress_bar.progress.console.log( - f"Building simulations " f"using {synth_workers} workers.." + f"Building simulations using {int(synth_workers)} workers.." ) with ThreadPoolExecutor(max_workers=synth_workers) as pool: for i in range(total_nodes): @@ -636,18 +635,28 @@ def simulate_node_connected(self, samples: int, depth: int) -> dict[int, dict]: # Run simulation start = time.time() + output_json = Path(make_build_dir("simulation_results_")) / "simulation_data.json" with DisabledLoggingConsole() as console: controller = NodeConnectedSimulationController( len(binaries), names, list(binaries.values()), console, 0.1, False ) - controller.run(depth, samples) + controller.run(depth, samples, output_json) end = time.time() - log.warning(f"Simulation took {end-start} seconds!") - # Return the collected data + log.info(f"Simulation took {end - start} seconds!") + + # Load the merged data from JSON + merged_data = json.loads(output_json.read_text()) + + # Return the collected data indexed by node index data = {} - for i, binary in binaries.items(): - with (binary.parent / "simulation_data.json").open() as f: - data[i] = json.load(f) + for i, sim_entry in enumerate(merged_data["simulations"]): + data[i] = { + "name": sim_entry["name"], + "fifo_utilization": sim_entry["fifo_utilization"], + "cycles": sim_entry["cycles"], + "samples": sim_entry["samples"], + } + json.dump(data, output_json.open("w"), indent=4) return data @@ -663,8 +672,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: sim = Simulation(model, self.fpgapart, self.clk_ns, self.cfg.functional_simulation) sys.stdout = sys.stdout.console sys.stderr = sys.stderr.console - sim.simulate_node_connected(2, 65556) - sim.simulate_node_connected(1, 2) - sim.simulate_node_connected(1, 20000) - sim.simulate_node_connected(10, 20000) + sim.simulate_node_connected(3, 100000000) + # sim.simulate_node_connected(1, 2) + # sim.simulate_node_connected(1, 20000) + # sim.simulate_node_connected(10, 20000) return model, False diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index 3a12646a45..b04dd42342 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -1,16 +1,18 @@ -"""Control (node based) simulations via stdio.""" +"""Control (node based) simulations via unix sockets.""" + +import json import multiprocessing +import socket import subprocess -import sys import time import traceback from concurrent.futures import Future, ThreadPoolExecutor from pathlib import Path from rich.console import Console -from subprocess import Popen from threading import Lock +from typing import Any -from finn.util.basic import get_vivado_root, make_build_dir +from finn.util.basic import make_build_dir from finn.util.exception import FINNInternalError from finn.util.logging import ThreadsafeProgressDisplay @@ -24,7 +26,7 @@ def __init__( names: list[str], binaries: list[Path], console: Console, - poll_interval: float = 0.1, + poll_interval: float = 1.0, with_progressbar: bool = True, ) -> None: """Create a new controller, without starting the simulation. @@ -55,56 +57,331 @@ def __init__( self.total = len(names) self.logdir = Path(make_build_dir("node_connected_simulation_logfiles_")) - def run(self, depth: int, samples: int) -> None: - """Run the simulation entirely with the given depth and sample count.""" + # Socket communication management + self.processes: list[tuple[subprocess.Popen, Any, Any]] = [] + self.sockets: list[tuple[socket.socket, str]] = [] + + # Early termination flag + self.should_stop = False + self.stop_lock = Lock() + + def _start_process(self, binary: Path, process_id: int) -> int: + """Start a single C++ simulation process with its own Unix socket. + + Args: + binary: Path to the simulation executable + process_id: Unique identifier for this process + + Returns: + Index of the started process + """ + socket_path = Path(f"/tmp/sim_socket_{process_id}.sock") + + # Remove socket if it exists + if socket_path.exists(): + socket_path.unlink() + + # Build command arguments + cmd = [str(binary), "--socket", socket_path] + + # Create log files for stdout and stderr + stdout_log = self.logdir / f"{process_id}_stdout.log" + stderr_log = self.logdir / f"{process_id}_stderr.log" + + stdout_file = stdout_log.open("w") + stderr_file = stderr_log.open("w") + + # Start C++ process - redirect stdout/stderr to files + cwd = binary.parent + proc = subprocess.Popen(cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd) + + # Check if process started successfully + time.sleep(0.2) # Give process time to fail if there's an immediate error + if proc.poll() is not None: + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + stdout_file.close() + stderr_file.close() + msg = ( + f"C++ process exited immediately with code {proc.returncode}\n" + f"Stderr: {stderr_output}\nStdout: {stdout_output}" + ) + raise RuntimeError(msg) + + # Create Unix socket and connect + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + + # Wait for C++ process to create socket (with timeout) + max_retries = 100 # 20 seconds total + connected = False + for i in range(max_retries): + # Check if process is still alive + if proc.poll() is not None: + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + stdout_file.close() + stderr_file.close() + msg = ( + f"C++ process died during socket wait with code {proc.returncode}\n" + f"Stderr: {stderr_output}\nStdout: {stdout_output}" + ) + raise RuntimeError(msg) + + try: + sock.connect(str(socket_path)) + connected = True + break + except (FileNotFoundError, ConnectionRefusedError) as e: + if i == max_retries - 1: + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + stdout_file.close() + stderr_file.close() + msg = ( + f"Failed to connect to socket after {max_retries} retries\n" + f"Stderr: {stderr_output}\nStdout: {stdout_output}" + ) + raise RuntimeError(msg) from e + time.sleep(0.2) + + if not connected: + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + stdout_file.close() + stderr_file.close() + msg = ( + f"Failed to connect to socket {socket_path}\n" + f"Stderr: {stderr_output}\nStdout: {stdout_output}" + ) + raise RuntimeError(msg) + + self.processes.append((proc, stdout_file, stderr_file)) + self.sockets.append((sock, str(socket_path))) + return len(self.processes) - 1 + + def _send_command(self, process_idx: int, command: str, payload: dict[str, Any]) -> None: + """Send command and payload to a specific process. + + Args: + process_idx: Index of the process to send to + command: Command string (e.g., "start", "status", "stop") + payload: Dictionary containing command-specific data + """ + sock, _ = self.sockets[process_idx] + + message = {"command": command, "payload": payload} + + # Send length-prefixed message + msg_str = json.dumps(message) + msg_bytes = msg_str.encode("utf-8") + length = len(msg_bytes) + + # Send 4-byte length prefix (little-endian) + sock.sendall(length.to_bytes(4, byteorder="little")) + # Send actual message + sock.sendall(msg_bytes) + + def _receive_response(self, process_idx: int) -> dict[str, Any] | None: + """Receive response from a specific process. + + Args: + process_idx: Index of the process to receive from + + Returns: + Dictionary containing the response, or None if error + """ + sock, _ = self.sockets[process_idx] + + # Read 4-byte length prefix + length_bytes = sock.recv(4) + if not length_bytes: + return None + + length = int.from_bytes(length_bytes, byteorder="little") + + # Read message data + msg_bytes = b"" + while len(msg_bytes) < length: + chunk = sock.recv(length - len(msg_bytes)) + if not chunk: + break + msg_bytes += chunk + + return json.loads(msg_bytes.decode("utf-8")) + + def _send_and_receive( + self, process_idx: int, command: str, payload: dict[str, Any] + ) -> dict[str, Any] | None: + """Send command and wait for response (convenience method). + + Args: + process_idx: Index of the process + command: Command string + payload: Command payload + + Returns: + Response dictionary + """ + self._send_command(process_idx, command, payload) + return self._receive_response(process_idx) + + def _cleanup_sockets(self) -> None: + """Close all sockets and terminate all processes.""" + # Send stop command to all processes + errors = [] + for i in range(len(self.processes)): + try: + self._send_command(i, "stop", {}) + self._receive_response(i) + except Exception as e: # noqa + errors.append((i, e)) + + # Close sockets + for sock, socket_path in self.sockets: + sock.close() + socket_path_obj = Path(socket_path) + if socket_path_obj.exists(): + socket_path_obj.unlink() + + # Terminate processes and close file handles + for proc, stdout_file, stderr_file in self.processes: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + finally: + stdout_file.close() + stderr_file.close() + + def run( + self, depth: int, samples: int, output_json: Path | None = None + ) -> dict[str, list[int]]: + """Run the simulation entirely with the given depth and sample count. + + Args: + depth: FIFO depth to configure for simulations. + samples: Number of samples to simulate. + output_json: Optional path to write merged simulation data as JSON. + + Returns: + Dictionary mapping simulation names to their FIFO utilization arrays. + """ futures: list[Future] = [] - for i, name in enumerate(self.names): - print(f"{i}: {name}") + fifo_results: dict[str, list[int]] = {} + cycles_results: dict[str, int] = {} + samples_results: dict[str, int] = {} + if self.progress is not None: self.progress.start() - with ThreadPoolExecutor(self.workers) as pool: - for i, (name, binary) in enumerate(zip(self.names, self.binaries, strict=True)): - futures.append( - pool.submit( - self._run_binary, - binary, - name, - i % multiprocessing.cpu_count(), - depth, - samples, - i == 0 or i == len(self.names) - 1, + try: + with ThreadPoolExecutor(self.workers) as pool: + for i, (name, binary) in enumerate(zip(self.names, self.binaries, strict=True)): + futures.append( + pool.submit( + self._run_binary, + binary, + name, + i % multiprocessing.cpu_count(), + depth, + samples, + i == 0 or i == len(self.names) - 1, + ) ) - ) - pool.shutdown() - if self.progress is not None: - self.progress.stop() - def _send(self, proc: Popen[bytes], cmd: str) -> None: - """Send a command to the given process stdin and flush the buffer.""" - if not cmd.endswith("\n"): - cmd += "\n" - proc.stdin.write(cmd.encode()) - proc.stdin.flush() + # Wait for first completion or error + from concurrent.futures import FIRST_COMPLETED, wait + + all_futures = list(futures) # Keep track of all futures + while futures: + done, futures = wait(futures, return_when=FIRST_COMPLETED) + + # Check if any completed task indicates we should stop + for future in done: + try: + result = future.result() # This will raise if there was an exception + if result is not None: + sim_name, fifo_util, cycles, samps = result + fifo_results[sim_name] = fifo_util + cycles_results[sim_name] = cycles + samples_results[sim_name] = samps + except Exception as e: # noqa + self.console.log(f"Simulation failed: {e}") + # Set stop flag and break + with self.stop_lock: + self.should_stop = True + break + + # If we should stop, signal all remaining simulations + with self.stop_lock: + if self.should_stop: + self.console.log("Stopping all remaining simulations...") + # Don't cancel - let them finish with early stop + break + + # Wait for all futures to complete and collect their results + pool.shutdown(wait=True) + for future in all_futures: + if not future.done(): + continue + try: + result = future.result() + if result is not None: + sim_name, fifo_util, cycles, samps = result + # Only update if not already collected + if sim_name not in fifo_results: + fifo_results[sim_name] = fifo_util + cycles_results[sim_name] = cycles + samples_results[sim_name] = samps + except Exception as e: + self.console.log(f"Error collecting result: {e}") + finally: + if self.progress is not None: + self.progress.stop() + self._cleanup_sockets() + + # Merge all simulation data + if output_json is not None: + merged_data = { + "simulations": [ + { + "name": name, + "fifo_utilization": fifo_results.get(name, []), + "cycles": cycles_results.get(name, 0), + "samples": samples_results.get(name, 0), + } + for name in self.names + ], + "depth_configured": depth, + "samples_requested": samples, + } + output_json.write_text(json.dumps(merged_data, indent=2)) + + return fifo_results def _run_binary( self, binary: Path, name: str | None, - cpu: int | None, + _cpu: int | None, depth: int, samples: int, is_end_node: bool = False, - ) -> None: - """Run the specified simulation binary in a new subprocess and communicate with it.""" - - # TODO: Seperate into multiple methods + ) -> tuple[str, list[int], int, int] | None: + """Run the specified simulation binary in a new subprocess and communicate with it. + Returns: + Tuple of (simulation_name, fifo_utilization, cycles, samples) on success, + None on failure. + """ cwd = binary.parent if name is None: name = cwd.name.replace("rtlsim_", "") - with (self.logdir / f"{name}_{self.names.index(name)}_of_{self.total}.txt").open( - "w+" - ) as logfile: + + process_index = self.names.index(name) + + with (self.logdir / f"{name}_{process_index}_of_{self.total}.txt").open("w+") as logfile: def _print(msg: str, color: str = "green") -> None: if self.progress is None: @@ -114,81 +391,113 @@ def _print(msg: str, color: str = "green") -> None: color = "red" self.console.log( f"[bold {color}]{name:<35}" - f"[/bold {color}][cornflower_blue]{self.names.index(name)} " # type:ignore - f"/ {len(self.names)-1}[/cornflower_blue] {msg:<35}" + f"[/bold {color}][cornflower_blue]{process_index} " + f"/ {len(self.names) - 1}[/cornflower_blue] {msg:<35}" ) + logfile.write(f"{msg}\n") + logfile.flush() - ld_library_path = ( - "LD_LIBRARY_PATH=" + get_vivado_root() + "/lib/lnx64.o:$LD_LIBRARY_PATH" - ) - taskset = "" - if cpu is not None: - taskset += f"taskset --cpu-list {cpu}" # TODO: numactl? - command = f"{ld_library_path} {taskset} {binary}" - _print(f"Running command: {command}") try: - proc = subprocess.Popen( - command, - stdout=subprocess.PIPE, - stdin=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=cwd, - shell=True, + # Start the simulation process with socket communication + proc_idx = self._start_process(binary, process_index) + + # Send configuration commands + response = self._send_and_receive( + proc_idx, "configure", {"fifo_depth": depth, "samples": samples} ) - def _send(cmd: str) -> None: - self._send(proc, cmd) - logfile.write(f"SIM: {cmd}") - logfile.flush() + if not response or response.get("status") != "success": + error_msg = ( + response.get("message", "Unknown error") if response else "No response" + ) + _print(f"Configuration failed: {error_msg}", "red") + return None + + # Start the simulation + response = self._send_and_receive(proc_idx, "start", {}) + + if not response or response.get("status") != "success": + error_msg = ( + response.get("message", "Unknown error") if response else "No response" + ) + _print(f"Failed to start simulation: {error_msg}", "red") + return None + + # Poll for status updates + while True: + # Check if we should stop early + with self.stop_lock: + if self.should_stop: + stop_response = self._send_and_receive(proc_idx, "stop", {}) + fifo_util = [] + cycles = 0 + samps = 0 + if stop_response: + fifo_util = stop_response.get("fifo_utilization", []) + cycles = stop_response.get("cycles", 0) + samps = stop_response.get("samples", 0) + if fifo_util: + logfile.write(f"Final FIFO utilization: {fifo_util}\n") + return (name, fifo_util, cycles, samps) - received = "" - while received != "end": time.sleep(self.poll_interval) - received = ( - proc.stdout.readline().decode("UTF-8").strip().split() - ) # type: ignore - logfile.write(f"CTRL: {' '.join(received)}\n") - logfile.flush() - if len(received) == 0: - continue - if received[0] == "end": - _print("Ending simulation.") - return - if received[0] == "log": - _print(" ".join(received[1:])) - elif received[0] == "ready": - _print("Received ready signal from simulation") - _send(f"fifodepth {depth}") - _send(f"runSamples {samples}") - # _send(f"runCycles 10000000") - _print("Settings sent to simulation.") - elif received[0] == "cycles": - if self.progress is None: - _print(" ".join(received[1:])) - else: - self.progress.update(name, int(received[1]), int(received[2])) - elif received[0] == "samples": - if self.progress is None: - _print(" ".join(received[1:])) - else: - self.progress.update(name, int(received[1]), int(received[2])) - elif received[0] == "started": - with self.running_lock: - self.running += 1 - _print(f"Running: {self.running} / {self.total}") - elif received[0] == "stopped": - with self.running_lock: - self.running -= 1 - _print(f"Running: {self.running} / {self.total}") - # elif received[0] == "error": - # _print("ERROR: " + " ".join(received[1:])) - # self.stop_flag = True - # return - else: - raise FINNInternalError( - f"Simulation {name}: Unrecognized command: {received}" - ) + + response = self._send_and_receive(proc_idx, "status", {}) + + if not response: + _print("Lost connection to simulation", "red") + with self.stop_lock: + self.should_stop = True + raise RuntimeError("Lost connection to simulation") + + state = response.get("state", "unknown") + + if state == "finished": + _print("Simulation completed successfully") + cycles = response.get("cycles", 0) + samples_done = response.get("samples", 0) + fifo_util = response.get("fifo_utilization", []) + if self.progress is not None: + self.progress.update(name, samples_done, samples) + # Signal other simulations to stop + with self.stop_lock: + self.should_stop = True + break + + if state == "running": + # Update progress if available + cycles = response.get("cycles", 0) + samples_done = response.get("samples", 0) + if self.progress is not None and samples_done > 0: + self.progress.update(name, samples_done, samples) + + if state == "error": + error_msg = response.get("message", "Unknown error") + _print(f"Simulation error: {error_msg}", "red") + # Signal other simulations to stop + with self.stop_lock: + self.should_stop = True + raise RuntimeError(f"Simulation error: {error_msg}") + + # Stop the simulation + stop_response = self._send_and_receive(proc_idx, "stop", {}) + fifo_util = [] + cycles = 0 + samps = 0 + if stop_response: + fifo_util = stop_response.get("fifo_utilization", []) + cycles = stop_response.get("cycles", 0) + samps = stop_response.get("samples", 0) + if fifo_util: + logfile.write(f"Final FIFO utilization: {fifo_util}\n") + + return (name, fifo_util, cycles, samps) + except Exception as e: self.console.log(f"Exception caught during simulation execution ({name}): {e}") self.console.log(traceback.format_exc()) - sys.exit(1) + logfile.write(f"Exception: {e}\n") + logfile.write(traceback.format_exc()) + with self.stop_lock: + self.should_stop = True + return None From ba6ec0fd6c8b12491f828dbde9a2f2718769a8a6 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 19 Dec 2025 15:55:24 +0100 Subject: [PATCH 034/170] Implement full minimization --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 97 ++- finn_xsi/finn_xsi/include/FIFO.h | 1 + finn_xsi/finn_xsi/include/Simulation.hpp | 60 +- finn_xsi/finn_xsi/src/FIFO.cpp | 3 + .../transformation/fpgadataflow/simulation.py | 656 +++++++++++++++++- .../fpgadataflow/simulation_controller.py | 129 +++- 6 files changed, 863 insertions(+), 83 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 82853cc0c4..7cbdd60202 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -10,7 +10,9 @@ #include #include #include +#include #include +#include #include #include @@ -32,25 +34,26 @@ class SimulationController { std::atomic state{SimulationState::IDLE}; std::atomic current_cycles{0}; std::atomic current_samples{0}; - std::atomic target_samples{0}; std::mutex state_mutex; std::string error_message; std::jthread sim_thread; - std::size_t fifo_depth{2}; + std::vector fifo_depths{2}; + std::size_t max_cycles{std::numeric_limits::max()}; + bool timeout_occurred{false}; public: explicit SimulationController(SingleNodeSimulation& simulation) : sim(simulation) {} - void configure(std::size_t depth, uint64_t samples) { + void configure(const std::vector& depths, std::size_t maxCycles) { std::lock_guard lock(state_mutex); if (state != SimulationState::IDLE && state != SimulationState::FINISHED) { throw std::runtime_error("Cannot configure while simulation is running"); } - fifo_depth = depth; - target_samples = samples; + fifo_depths = depths; current_cycles = 0; current_samples = 0; + max_cycles = maxCycles; state = SimulationState::CONFIGURED; } @@ -65,16 +68,33 @@ class SimulationController { // Start simulation in a separate thread sim_thread = std::jthread([this](std::stop_token stoken) { try { - // Configure simulation - sim.setMaxFIFODepth(fifo_depth); + // Reset simulation first sim.reset(); + // Configure FIFO depths AFTER reset + std::size_t num_fifos = sim.getFIFOCount(); + + if (fifo_depths.empty()) { + throw std::runtime_error("FIFO depths not configured"); + } + + // Apply depths: if list is shorter, use last value for remaining FIFOs + for (std::size_t i = 0; i < num_fifos; ++i) { + std::size_t depth_idx = std::min(i, fifo_depths.size() - 1); + sim.setFIFODepth(i, fifo_depths[depth_idx]); + } + // Run the simulation - sim.runToStableState(stoken); + bool timeout = sim.runToStableState(stoken, max_cycles); + + if (timeout) { + state = SimulationState::FINISHED; + timeout_occurred = true; + } // Update state based on completion if (!stoken.stop_requested()) { - current_samples.store(target_samples.load()); + current_samples.store(sim.getCompletedMaps()); state = SimulationState::FINISHED; } } catch (const std::exception& e) { @@ -114,8 +134,24 @@ class SimulationController { break; case SimulationState::FINISHED: status["state"] = "finished"; + status["timeout"] = timeout_occurred; + if (timeout_occurred) { + status["state"] = "timeout"; + } status["cycles"] = sim.getCyclesRun(); status["samples"] = sim.getCompletedMaps(); + status["intervals"] = sim.getOStreamStableStateIntervals(); + // Add FIFO depth data + { + auto depths = sim.getFIFODepth(); + json fifo_depth = json::array(); + for (size_t i = 0; i < depths.size(); ++i) { + fifo_depth.push_back(depths[i]); + } + if (!fifo_depth.empty()) { + status["fifo_depth"] = fifo_depth; + } + } // Add FIFO utilization data { auto utilizations = sim.getFIFOUtilization(); @@ -146,9 +182,32 @@ void process_command(const json& request, json& response, SimulationController& try { if (command == "configure") { - std::size_t fifo_depth = payload.value("fifo_depth", 2ULL); - uint64_t samples = payload.value("samples", 1ULL); - controller.configure(fifo_depth, samples); + std::vector fifo_depths; + + // Handle fifo_depth as either a single value or an array + if (payload.contains("fifo_depth")) { + const auto& depth_value = payload["fifo_depth"]; + if (depth_value.is_array()) { + for (const auto& val : depth_value) { + fifo_depths.push_back(val.get()); + } + } else { + fifo_depths.push_back(depth_value.get()); + } + } else { + fifo_depths.push_back(std::numeric_limits::max()); // Default value + } + + if (fifo_depths.empty()) { + throw std::runtime_error("FIFO depth list cannot be empty"); + } + + std::size_t max_cycles = std::numeric_limits::max(); + if (payload.contains("max_cycles")) { + max_cycles = payload["max_cycles"].get(); + } + + controller.configure(fifo_depths, max_cycles); response["status"] = "success"; response["message"] = "Configuration successful"; } else if (command == "start") { @@ -161,17 +220,26 @@ void process_command(const json& request, json& response, SimulationController& controller.stop(); response["status"] = "success"; response["message"] = "Simulation stopped"; - // Include final status with FIFO utilization + // Include final status with FIFO utilization and depth json final_status = controller.get_status(); if (final_status.contains("fifo_utilization")) { response["fifo_utilization"] = final_status["fifo_utilization"]; } + if (final_status.contains("fifo_depth")) { + response["fifo_depth"] = final_status["fifo_depth"]; + } if (final_status.contains("cycles")) { response["cycles"] = final_status["cycles"]; } if (final_status.contains("samples")) { response["samples"] = final_status["samples"]; } + if (final_status.contains("intervals")) { + response["intervals"] = final_status["intervals"]; + } + if (final_status.contains("timeout")) { + response["timeout"] = final_status["timeout"]; + } } else { response["status"] = "error"; response["message"] = "Unknown command: " + command; @@ -185,8 +253,7 @@ void process_command(const json& request, json& response, SimulationController& int main(int argc, const char* argv[]) { // Parse CLI options po::options_description desc{"Options"}; - desc.add_options()("socket,s", po::value(), - "Unix domain socket path for IPC"); + desc.add_options()("socket,s", po::value(), "Unix domain socket path for IPC"); po::variables_map vm; po::store(po::parse_command_line(argc, argv, desc), vm); po::notify(vm); diff --git a/finn_xsi/finn_xsi/include/FIFO.h b/finn_xsi/finn_xsi/include/FIFO.h index 11bb85390d..531c0fb099 100644 --- a/finn_xsi/finn_xsi/include/FIFO.h +++ b/finn_xsi/finn_xsi/include/FIFO.h @@ -21,6 +21,7 @@ class FIFO { bool isEmpty() const; void reset(uint64_t size = std::numeric_limits::max()); void setMaxSize(const uint64_t size); + uint64_t getMaxSize() const; uint64_t getSpaceLeft() const; uint64_t getMaxUtil() const; void increaseCounter(const uint64_t count); diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 12c58c0ad5..72cb6ff4b8 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -140,7 +140,7 @@ class SingleNodeSimulation : public Simulationostreams.begin(), this->ostreams.end(), - [](const M_AXIS_Control& stream) { return stream.stableState.is_stable(); }) == false && - !stoken.stop_requested()) { + [[gnu::hot, gnu::always_inline]] bool runToStableState(std::stop_token stoken = {}, std::size_t max_cycles = std::numeric_limits::max()) { + while (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](const M_AXIS_Control& stream) { return stream.stableState.is_stable(); }) & + !stoken.stop_requested() & cyclesRun <= max_cycles) { runSingleCycle(stoken); + runSingleCycle(stoken); + runSingleCycle(stoken); + runSingleCycle(stoken); + } + return cyclesRun > max_cycles; + } + + /// Get the number of FIFOs + std::size_t getFIFOCount() const noexcept { + if constexpr (LastNode) { + return 0; } + return OStreamsSize; + } + + /// Set the depth of a specific FIFO + void setFIFODepth(std::size_t index, std::size_t depth) { + if constexpr (LastNode) { + throw std::runtime_error("Cannot set FIFO depth on last node (no FIFOs present)"); + } + if (index >= OStreamsSize) { + throw std::out_of_range(std::format("FIFO index {} out of range (max: {})", index, OStreamsSize - 1)); + } + fifo[index].setMaxSize(depth); } /// Set the max FIFO depth of all interfaces void setMaxFIFODepth(std::size_t depth) { - for (FIFO& f : fifo) { - f.setMaxSize(depth); + if constexpr (!LastNode) { + for (FIFO& f : fifo) { + f.setMaxSize(depth); + } } } + std::array getFIFODepth() const noexcept { + if constexpr (LastNode) { + return {}; + } + std::array utilizations{}; + for (std::size_t i = 0; i < OStreamsSize; ++i) { + utilizations[i] = fifo[i].getMaxSize(); + } + return utilizations; + } + /// Get the job size of the specified output stream std::size_t getOutputJobSize(std::size_t outputIndex = 0) { return this->ostreams[outputIndex].job_size; } @@ -274,6 +309,17 @@ class SingleNodeSimulation : public Simulation getOStreamStableStateIntervals() const noexcept { + std::array intervals{}; + if constexpr (LastNode) { + for (std::size_t i = 0; i < OStreamsSize; ++i) { + intervals[i] = this->ostreams[i].interval; + } + } + return intervals; + } }; diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp index 97c8f4d046..1b0547d3eb 100644 --- a/finn_xsi/finn_xsi/src/FIFO.cpp +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -1,6 +1,7 @@ #include #include +#include FIFO::FIFO(uint64_t size) : maxSize(size) {} FIFO::~FIFO() {} @@ -52,6 +53,8 @@ void FIFO::reset(uint64_t size) { /// Set the FIFOs max size void FIFO::setMaxSize(const uint64_t size) { maxSize = size; } +uint64_t FIFO::getMaxSize() const { return maxSize; } + uint64_t FIFO::getSpaceLeft() const { return maxSize - currentUtil; } uint64_t FIFO::getMaxUtil() const { return maxUtil; } diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 09fc380c5f..f0745ddd5c 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -2,6 +2,7 @@ import finn_xsi.adapter as finnxsi import json +import math import numpy as np import onnx import os @@ -203,7 +204,7 @@ def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, outstream_iters.append(int(np.prod(oshape_folded[:-1]))) interface_names = model.get_metadata_prop("vivado_stitch_ifnames") if interface_names is None: - raise FINNUserError( + raise FINNInternalError( f"{model}: Could not find stitched-IP interface names. " f"Did you run IP Stitching first?" ) @@ -211,7 +212,7 @@ def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, # TODO: Copied from rtlsim_exec_cppxsi. Remove eval(). interface_names = eval(interface_names) if "aximm" in interface_names.keys() and interface_names["aximm"] != []: - raise FINNUserError( + raise FINNInternalError( f"{model}: CPP XSI Sim does not know how to handle full " f"AXI MM interfaces: {interface_names['aximm']}" ) @@ -291,13 +292,13 @@ def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: proc_env=os.environ.copy(), ) except CalledProcessError as e: - raise FINNUserError(f"Failed to run cmake in {sim_base}") from e + raise FINNInternalError(f"Failed to run cmake in {sim_base}") from e self.progress_bar.update("CMake") # Calling make to actually build the simulation makefile = Path(sim_base) / "Makefile" if not makefile.exists(): - raise FINNUserError(f"Failed to create Makefile in {sim_base}!") + raise FINNInternalError(f"Failed to create Makefile in {sim_base}!") try: launch_process_helper( ["make"], @@ -307,12 +308,12 @@ def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: print_stderr=not silent, ) except CalledProcessError as e: - raise FINNUserError(f"Failed to create executable in {sim_base}!") from e + raise FINNInternalError(f"Failed to create executable in {sim_base}!") from e # TODO: Fix name for general rtlsim simulation_executable = Path(sim_base) / "LayerSimulationBackend" if not simulation_executable.exists(): - raise FINNUserError(f"Make call in {sim_base} failed!") + raise FINNInternalError(f"Make call in {sim_base} failed!") self.progress_bar.update("Make") return simulation_executable @@ -527,8 +528,8 @@ def _build( # Build sims in parallel synth_workers = max( - 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 16) - ) # 16GB per synthesis + 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 20) + ) # 20GB per synthesis if not functional_sim: # When not having to do synthesis, the build is not memory bottlenecked and # can be executed as parallel as possible @@ -612,6 +613,16 @@ def __init__( self._prepare_model() self.builder = SimulationBuilder(self.model, fpgapart, clk_ns) + sys.stdout = sys.stdout.console + sys.stderr = sys.stderr.console + + self.binaries = self.builder.build_simulation( + SimulationType.NODE_BASED_CONNECTED, + self.workers, + with_live_display=True, + functional_sim=self.functional_sim, + ) + def _prepare_model(self) -> None: """Execute some preparation transformations on the model.""" self.model = self.model.transform(InsertDWC()) @@ -621,26 +632,23 @@ def _prepare_model(self) -> None: self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) self.model = self.model.transform(HLSSynthIP()) - def simulate_node_connected(self, samples: int, depth: int) -> dict[int, dict]: + def simulate_node_connected( + self, depth: int | list[list[int]] | None = None, max_cycles: int | None = None + ) -> tuple[dict[int, dict[str, list[int]]], bool]: """Simulate the given number of samples for every layer. Layers are completely isolated and simulated in parallel. Simulation data is returned as a dict (by node name as index). """ - binaries = self.builder.build_simulation( - SimulationType.NODE_BASED_CONNECTED, - self.workers, - with_live_display=True, - functional_sim=self.functional_sim, - ) names = [node.name for node in self.model.graph.node] + initial_depth = [[depth]] * len(self.binaries) if isinstance(depth, int) else depth # Run simulation start = time.time() output_json = Path(make_build_dir("simulation_results_")) / "simulation_data.json" with DisabledLoggingConsole() as console: controller = NodeConnectedSimulationController( - len(binaries), names, list(binaries.values()), console, 0.1, False + len(self.binaries), names, list(self.binaries.values()), console, 0.1, False ) - controller.run(depth, samples, output_json) + controller.run(initial_depth, output_json, max_cycles) end = time.time() log.info(f"Simulation took {end - start} seconds!") @@ -653,27 +661,625 @@ def simulate_node_connected(self, samples: int, depth: int) -> dict[int, dict]: data[i] = { "name": sim_entry["name"], "fifo_utilization": sim_entry["fifo_utilization"], + "fifo_depth": sim_entry["fifo_depth"], "cycles": sim_entry["cycles"], "samples": sim_entry["samples"], + "intervals": sim_entry["intervals"], } json.dump(data, output_json.open("w"), indent=4) - return data + return data, merged_data.get("timeout_occurred", False) # TODO: Just a test transformation. Will be integrated properly later class RunLayerParallelSimulation(Transformation): # noqa - def __init__(self, fpgapart: str, clk_ns: float, cfg: DataflowBuildConfig) -> None: # noqa + def __init__( + self, + fpgapart: str, + clk_ns: float, + cfg: DataflowBuildConfig, + max_qsrl_depth: int = 256, + vivado_ram_style: str = "auto", + quality_of_results: str = "default", + ) -> None: super().__init__() self.fpgapart = fpgapart self.clk_ns = clk_ns self.cfg = cfg + self.max_qsrl_depth = max_qsrl_depth + self.vivado_ram_style = vivado_ram_style + self.quality_of_results = quality_of_results def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: sim = Simulation(model, self.fpgapart, self.clk_ns, self.cfg.functional_simulation) - sys.stdout = sys.stdout.console - sys.stderr = sys.stderr.console - sim.simulate_node_connected(3, 100000000) - # sim.simulate_node_connected(1, 2) - # sim.simulate_node_connected(1, 20000) - # sim.simulate_node_connected(10, 20000) + model = sim.model # TODO:clean up + + initial_fifo_depths, _ = sim.simulate_node_connected() + + fifo_depths = [] # Each entry is a list of fifo sizes for that node + for val in initial_fifo_depths.values(): + fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) + + # Max cycles for any simulation + sim_cycles = max([val["cycles"] for val in initial_fifo_depths.values()]) + + bit_widths = [] + for i in range(len(fifo_depths)): + bit_widths.append([]) + hw_node = getCustomOp(model.graph.node[i]) + if isinstance(hw_node, HWCustomOp): + for j in range(len(fifo_depths[i])): + bit_widths[i].append(hw_node.get_outstream_width(j)) + else: + raise FINNInternalError("Non-HW node found in dataflow graph during simulation") + + needs_minimization = [] + for i in range(len(fifo_depths)): + needs_minimization.append([True] * len(fifo_depths[i])) + for i in range(len(fifo_depths)): + for j in range(len(fifo_depths[i])): + # Check if we can reduce the fifo size + + used_size = fifo_depths[i][j] + bw = bit_widths[i][j] + + needs_minimization[i][j] = self.needs_minimization(used_size, bw) + + # Preserve original baseline depths for testing (deep copy) + original_fifo_depths = [row[:] for row in fifo_depths] + + # Minimize FIFO depths using binary search over BRAM block counts + for i in range(len(fifo_depths)): + for j in range(len(fifo_depths[i])): + if not needs_minimization[i][j]: + continue + + minimized_depth = self._minimize_fifo_depth( + i, + j, + fifo_depths, + original_fifo_depths, + bit_widths, + initial_fifo_depths, + sim, + sim_cycles, + ) + fifo_depths[i][j] = minimized_depth + + print("Final FIFO depths:") + for i in range(len(fifo_depths)): + print(f"{i}: {fifo_depths[i]}") + log.info(f"{i}: {fifo_depths[i]}") + return model, False + + def _check_performance(self, new_data: dict, initial_fifo_depths: dict) -> bool: + """Check if performance has degraded compared to baseline. + + Args: + new_data: Simulation results to check + initial_fifo_depths: Baseline performance data + + Returns: + True if performance degraded, False otherwise + """ + for k, v in new_data.items(): + for idx in range(len(v["intervals"])): + if v["intervals"][idx] > initial_fifo_depths[k]["intervals"][idx]: + return True + return False + + def _test_depth( + self, + test_depth: int, + node_idx: int, + fifo_idx: int, + baseline_depths: list, + initial_fifo_depths: dict, + sim, + sim_cycles: float, + ) -> tuple[bool, bool]: + """Test a specific FIFO depth. + + Args: + test_depth: Depth to test + node_idx: Node index + fifo_idx: FIFO index within node + baseline_depths: Original baseline FIFO depths (unchanged during minimization) + initial_fifo_depths: Baseline performance data + sim: Simulation controller + sim_cycles: Maximum simulation cycles + + Returns: + Tuple of (success, timeout) where success means depth works without degradation + """ + test_depths = [row[:] for row in baseline_depths] # Deep copy from baseline + test_depths[node_idx][fifo_idx] = test_depth + + new_data, timeout = sim.simulate_node_connected(test_depths, max_cycles=sim_cycles * 1.1) + + if timeout: + return False, True + + performance_degraded = self._check_performance(new_data, initial_fifo_depths) + return not performance_degraded, False + + def _find_valid_block_count( + self, target_blocks: int, bitwidth: int, lower_bound: int = 1 + ) -> tuple[int, int, int]: + """Find a valid block count and corresponding depth range. + + Args: + target_blocks: Desired number of BRAM blocks + bitwidth: Data bitwidth + lower_bound: Minimum acceptable block count + + Returns: + Tuple of (valid_blocks, min_depth, max_depth) + """ + blocks = target_blocks + while blocks >= lower_bound: + min_d, max_d = calculate_bram_depth_range(blocks, bitwidth) + if max_d > 0: + return blocks, min_d, max_d + blocks -= 1 + return 0, 0, 0 + + def _minimize_fifo_depth( + self, + node_idx: int, + fifo_idx: int, + current_depths: list, + baseline_depths: list, + bit_widths: list, + initial_fifo_depths: dict, + sim, + sim_cycles: int, + ) -> int: + """Minimize a single FIFO depth using binary search. + + Args: + node_idx: Node index + fifo_idx: FIFO index within node + current_depths: Current working FIFO depth configuration + (may have already-minimized values) + baseline_depths: Original baseline FIFO depths (unchanged during minimization) + bit_widths: Bitwidths for all FIFOs + initial_fifo_depths: Baseline performance data + sim: Simulation controller + sim_cycles: Maximum simulation cycles + + Returns: + Minimized FIFO depth + """ + original_size = baseline_depths[node_idx][fifo_idx] + bw = bit_widths[node_idx][fifo_idx] + + print(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") + + # Try FIFO depth of 32 first (fits into bitwidth LUTs) + success, timeout = self._test_depth( + 32, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + + if success: + # If FIFO depth of 2 works, we dont need FIFOs at all, because AXI buffers some values + success, timeout = self._test_depth( + 2, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + if success: + return 2 + return 32 + + # Try one BRAM block less than current + upper_blocks = calculate_bram_blocks(original_size, bw) + blocks, min_d, max_d = self._find_valid_block_count(upper_blocks - 1, bw) + + if max_d == 0: + return original_size + + success, timeout = self._test_depth( + max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + + if timeout or not success: + return original_size + + best_working_depth = max_d + + # Binary search if there's room to search + if blocks > 2 and min_d - 1 > 32: + best_working_depth = self._binary_search_depth( + node_idx, + fifo_idx, + baseline_depths, + bw, + initial_fifo_depths, + sim, + sim_cycles, + lower_blocks=1, + upper_blocks=blocks, + ) + + # If we are within reach of the max qsrl depth, + # test that as well, so that we can maybe move to LUTRAM + if ( + best_working_depth < self.max_qsrl_depth * 1.1 + and best_working_depth > self.max_qsrl_depth + ): + success, timeout = self._test_depth( + self.max_qsrl_depth, + node_idx, + fifo_idx, + baseline_depths, + initial_fifo_depths, + sim, + sim_cycles, + ) + if success: + best_working_depth = self.max_qsrl_depth + + return best_working_depth + + def _binary_search_depth( + self, + node_idx: int, + fifo_idx: int, + baseline_depths: list, + bitwidth: int, + initial_fifo_depths: dict, + sim, + sim_cycles: float, + lower_blocks: int, + upper_blocks: int, + ) -> int: + """Perform binary search to find minimal working FIFO depth. + + Args: + node_idx: Node index + fifo_idx: FIFO index within node + baseline_depths: Original baseline FIFO depths (unchanged during minimization) + bitwidth: Data bitwidth + initial_fifo_depths: Baseline performance data + sim: Simulation controller + sim_cycles: Maximum simulation cycles + lower_blocks: Lower bound for block count + upper_blocks: Upper bound for block count (known to work) + + Returns: + Best working depth found + """ + _, _, max_d = self._find_valid_block_count(upper_blocks, bitwidth) + best_working_depth = max_d + + while lower_blocks < upper_blocks: + mid_blocks = (lower_blocks + upper_blocks) // 2 + + # Prevent infinite loop + if mid_blocks == upper_blocks: + mid_blocks = upper_blocks - 1 + if mid_blocks < lower_blocks: + break + + # Find valid depth for this block count + valid_blocks, _, max_d = self._find_valid_block_count( + mid_blocks, bitwidth, lower_blocks + ) + + if max_d == 0: + # No valid configuration, try more blocks + lower_blocks = mid_blocks + 1 + continue + + success, _ = self._test_depth( + max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + + if success: + # This depth works, try smaller + best_working_depth = max_d + upper_blocks = valid_blocks + else: + # This depth doesn't work, need larger + lower_blocks = valid_blocks + 1 + + return best_working_depth + + def needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: + """Determine whether a FIFO can be minimized further. + + Args: + fifo_depth: Current FIFO depth + bitwidth: Data bitwidth + + Returns: + True if the FIFO can be minimized further, False otherwise. + """ + # TODO: Make sure that the FIFOs are correctly instantiated afterwards. + # Everything <= max_qsrl_depth should use rtl fifos. + # TODO: Rewrite this method. We should set the FIFO style instead of the user, + # also QoR should not be used + + # Qsrl FIFO Formula: LUTs = ⌈depth/32⌉ x ⌈bitwidth/2⌉ + if fifo_depth <= self.max_qsrl_depth and self.quality_of_results != "best": + return False + if fifo_depth <= 32: # FIFOs of depth <=32 fit into bitwidth/2 LUTs + return False + # possible RAM styles: auto, block, distributed, ultra + if self.vivado_ram_style == "block" and calculate_bram_blocks(fifo_depth, bitwidth) == 1: + return False + if self.vivado_ram_style == "ultra" and calculate_uram_blocks(fifo_depth, bitwidth) == 1: + return False + if self.vivado_ram_style == "auto": + if ( + self.quality_of_results == "fast" + and calculate_uram_blocks(fifo_depth, bitwidth) == 1 + ): + return False + if ( + calculate_bram_blocks(fifo_depth, bitwidth) == 1 + and fifo_depth > self.max_qsrl_depth * 1.1 + ): + return False + + # Larger FIFOs with style distributed are always optimized further + # If more than 1 RAM block is used, we can try to reduce it further + + return True + + +def calculate_bram_blocks(depth: int, bitwidth: int) -> int: + """Calculate the number of BRAM blocks required for a BRAM FIFO. + + Args: + depth: FIFO depth + bitwidth: Data bitwidth + """ + if bitwidth == 1: + return math.ceil(depth / 16384) + if bitwidth == 2: + return math.ceil(depth / 8192) + if bitwidth <= 4: + return (math.ceil(depth / 4096)) * (math.ceil(bitwidth / 4)) + if bitwidth <= 9: + return (math.ceil(depth / 2048)) * (math.ceil(bitwidth / 9)) + if bitwidth <= 18 or depth > 512: + return (math.ceil(depth / 1024)) * (math.ceil(bitwidth / 18)) + return (math.ceil(depth / 512)) * (math.ceil(bitwidth / 36)) + + +def calculate_bram_depth_range(blocks: int, bitwidth: int) -> tuple[int, int]: + """Calculate the range of FIFO depths that use exactly the given number of BRAM blocks. + + Args: + blocks: Number of BRAM blocks + bitwidth: Data bitwidth + + Returns: + Tuple of (min_depth, max_depth) that uses exactly 'blocks' BRAM blocks. + """ + if blocks < 1: + raise FINNInternalError("Number of BRAM blocks must be at least 1") + + # Invert the formula from calculate_bram_blocks based on bitwidth + if bitwidth == 1: + # blocks = ⌈depth/16384⌉ + # Inversion: (blocks-1)*16384 < depth ≤ blocks*16384 + min_depth = (blocks - 1) * 16384 + 1 if blocks > 1 else 1 + max_depth = blocks * 16384 + elif bitwidth == 2: + # blocks = ⌈depth/8192⌉ + # Inversion: (blocks-1)*8192 < depth ≤ blocks*8192 + min_depth = (blocks - 1) * 8192 + 1 if blocks > 1 else 1 + max_depth = blocks * 8192 + elif bitwidth <= 4: + # blocks = ⌈depth/4096⌉ * ⌈bitwidth/4⌉ + bitwidth_factor = math.ceil(bitwidth / 4) + depth_blocks = math.ceil(blocks / bitwidth_factor) + min_depth = (depth_blocks - 1) * 4096 + 1 if depth_blocks > 1 else 1 + max_depth = depth_blocks * 4096 + elif bitwidth <= 9: + # blocks = ⌈depth/2048⌉ * ⌈bitwidth/9⌉ + bitwidth_factor = math.ceil(bitwidth / 9) + depth_blocks = math.ceil(blocks / bitwidth_factor) + min_depth = (depth_blocks - 1) * 2048 + 1 if depth_blocks > 1 else 1 + max_depth = depth_blocks * 2048 + elif bitwidth <= 18: + # blocks = ⌈depth/1024⌉ * ⌈bitwidth/18⌉ + bitwidth_factor = math.ceil(bitwidth / 18) + depth_blocks = math.ceil(blocks / bitwidth_factor) + min_depth = (depth_blocks - 1) * 1024 + 1 + max_depth = depth_blocks * 1024 + else: + # bitwidth > 18, split into two cases from original function + # Case 1: depth > 512 uses ⌈depth/1024⌉ * ⌈bitwidth/18⌉ + # Case 2: depth ≤ 512 uses ⌈depth/512⌉ * ⌈bitwidth/36⌉ + + # Try the depth > 512 case first (⌈depth/1024⌉ * ⌈bitwidth/18⌉) + bitwidth_factor = math.ceil(bitwidth / 18) + depth_blocks = math.ceil(blocks / bitwidth_factor) + if depth_blocks <= 1 and bitwidth <= 18: + return (1, 1024) + min_depth = max((depth_blocks - 1) * 1024 + 1, 513) # Must be > 512 + max_depth = depth_blocks * 1024 + # Check if this range is valid (entirely > 512) + if min_depth > 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: + return (min_depth, max_depth) + + # Try the depth ≤ 512 case (⌈depth/512⌉ * ⌈bitwidth/36⌉) + bitwidth_factor = math.ceil(bitwidth / 36) + depth_blocks = math.ceil(blocks / bitwidth_factor) + if depth_blocks <= 1 and bitwidth > 18: + return (1, 512) + min_depth = (depth_blocks - 1) * 512 + 1 + max_depth = min(depth_blocks * 512, 512) # Must be ≤ 512 + # Check if this range is valid (entirely ≤ 512) + if max_depth <= 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: + return (min_depth, max_depth) + + return (0, 0) # No valid range found + + # Verify the range is valid + if calculate_bram_blocks(min_depth, bitwidth) != blocks: + raise FINNInternalError("Calculated BRAM depth range is invalid!") + return (min_depth, max_depth) + + +def calculate_uram_blocks(depth: int, bitwidth: int) -> int: + """Calculate the number of URAM blocks required for a URAM FIFO. + + Args: + depth: FIFO depth + bitwidth: Data bitwidth + """ + return (math.ceil(depth / 4096)) * (math.ceil(bitwidth / 72)) + + +def calculate_uram_depth_range(blocks: int, bitwidth: int) -> tuple[int, int]: + """Calculate the range of FIFO depths that use exactly the given number of URAM blocks. + + Args: + blocks: Number of URAM blocks + bitwidth: Data bitwidth + + Returns: + Tuple of (min_depth, max_depth) that uses exactly 'blocks' URAM blocks. + Returns (0, 0) if no valid range exists. + """ + if blocks < 1: + return (0, 0) + + # URAM formula: blocks = ⌈depth/4096⌉ * ⌈bitwidth/72⌉ + bitwidth_factor = math.ceil(bitwidth / 72) + + # Calculate depth range + # Minimum depth: (blocks / bitwidth_factor - 1) * 4096 + 1 + # Maximum depth: (blocks / bitwidth_factor) * 4096 + + if blocks % bitwidth_factor != 0: + return (0, 0) # Invalid block count for this bitwidth + + depth_blocks = blocks // bitwidth_factor + min_depth = (depth_blocks - 1) * 4096 + 1 if depth_blocks > 1 else 1 + max_depth = depth_blocks * 4096 + + # Verify + if calculate_uram_blocks(min_depth, bitwidth) != blocks: + return (0, 0) + + return (min_depth, max_depth) + + +def calculate_smaller_uram_blocks(depth: int, bitwidth: int, dif: int) -> int: + """Calculate the biggest FIFO depth that uses fewer URAM blocks. + + Args: + depth: Current FIFO depth + bitwidth: Data bitwidth + dif: Number of URAM blocks to reduce by (will be clamped to available blocks) + + Returns: + Maximum depth that uses at least 'dif' fewer blocks, or uses half the current blocks + if dif is too large. + """ + current_uram_blocks = calculate_uram_blocks(depth, bitwidth) + if current_uram_blocks <= 1: + return depth # Cannot reduce further + + # If requested reduction is larger than what we have, reduce by half instead + target_blocks = current_uram_blocks - dif + if target_blocks < 1: + target_blocks = max(1, current_uram_blocks // 2) + + # Use the range function to find the maximum depth for target blocks + min_d, max_d = calculate_uram_depth_range(target_blocks, bitwidth) + if max_d > 0: + return max_d + + # Fallback to linear search if range calculation fails + for i in range(depth, 0, -1): + uram_blocks = calculate_uram_blocks(i, bitwidth) + if uram_blocks <= target_blocks: + return i + return depth - 1 # Fallback, should not happen + + +def calculate_srl16e_luts(depth: int, bitwidth: int) -> int: + """Calculate the number of SRL16E LUTs required for a FIFO. + + Args: + depth: FIFO depth (must be >= 2) + bitwidth: Data bitwidth + + Returns: + Number of SRL16E LUTs required without adress LUTs. + + Formula: LUTs = ⌈depth/32⌉ x ⌈bitwidth/2⌉ + """ + ram_luts = (math.ceil(depth / 32)) * (math.ceil(bitwidth / 2)) + return ram_luts + + +def calculate_srl16e_depth_range(luts: int, bitwidth: int) -> tuple[int, int]: + """Calculate the range of FIFO depths that use exactly the given number of SRL16E LUTs. + + Args: + luts: Number of SRL16E LUTs + bitwidth: Data bitwidth + + Returns: + Tuple of (min_depth, max_depth) that uses exactly 'luts' LUTs. + Returns (0, 0) if no valid range exists. + """ + if luts < 1: + return (0, 0) + + # SRL16E formula: luts = ⌈depth/32⌉ * ⌈bitwidth/2⌉ + bitwidth_factor = math.ceil(bitwidth / 2) + + # Calculate depth range + if luts % bitwidth_factor != 0: + return (0, 0) # Invalid LUT count for this bitwidth + + depth_blocks = luts // bitwidth_factor + min_depth = (depth_blocks - 1) * 32 + 1 if depth_blocks > 1 else 2 + max_depth = depth_blocks * 32 + + # Verify + if calculate_srl16e_luts(min_depth, bitwidth) != luts: + return (0, 0) + + return (min_depth, max_depth) + + +def calculate_smaller_srl16e_luts(depth: int, bitwidth: int, dif: int) -> int: + """Calculate the biggest FIFO depth that uses fewer SRL16E LUTs. + + Args: + depth: Current FIFO depth + bitwidth: Data bitwidth + dif: Number of LUTs to reduce by (will be clamped to available LUTs) + + Returns: + Maximum depth that uses at least 'dif' fewer LUTs, or uses half the current LUTs + if dif is too large. + """ + current_luts = calculate_srl16e_luts(depth, bitwidth) + if current_luts <= 1: + return depth # Cannot reduce further + + # If requested reduction is larger than what we have, reduce by half instead + target_luts = current_luts - dif + if target_luts < 1: + target_luts = max(1, current_luts // 2) + + # Use the range function to find the maximum depth for target LUTs + min_d, max_d = calculate_srl16e_depth_range(target_luts, bitwidth) + if max_d > 0: + return max_d + + # Fallback to linear search if range calculation fails + for i in range(depth, 1, -1): + luts = calculate_srl16e_luts(i, bitwidth) + if luts <= target_luts: + return i + return depth - 1 # Fallback, should not happen diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index b04dd42342..d69a5c2f27 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -4,6 +4,7 @@ import multiprocessing import socket import subprocess +import threading import time import traceback from concurrent.futures import Future, ThreadPoolExecutor @@ -75,7 +76,13 @@ def _start_process(self, binary: Path, process_id: int) -> int: Returns: Index of the started process """ - socket_path = Path(f"/tmp/sim_socket_{process_id}.sock") + thread_id = threading.get_ident() + + # Create unique socket path which includes thread ID to avoid conflicts + # with multiple threads + socket_path = Path(f"/tmp/{thread_id}/") + socket_path.mkdir(parents=True, exist_ok=True) + socket_path = socket_path / f"sim_socket_{process_id}.sock" # Remove socket if it exists if socket_path.exists(): @@ -256,7 +263,10 @@ def _cleanup_sockets(self) -> None: stderr_file.close() def run( - self, depth: int, samples: int, output_json: Path | None = None + self, + depth: list[list[int]] | None = None, + output_json: Path | None = None, + max_cycles: int | None = None, ) -> dict[str, list[int]]: """Run the simulation entirely with the given depth and sample count. @@ -272,21 +282,27 @@ def run( fifo_results: dict[str, list[int]] = {} cycles_results: dict[str, int] = {} samples_results: dict[str, int] = {} + intervals_results: dict[str, list[int]] = {} + timeout_result = False + fifo_depths: dict[str, list[int]] = {} if self.progress is not None: self.progress.start() try: with ThreadPoolExecutor(self.workers) as pool: for i, (name, binary) in enumerate(zip(self.names, self.binaries, strict=True)): + is_last_node = i == len(self.names) - 1 + is_special_for_display = i == 0 or is_last_node futures.append( pool.submit( self._run_binary, binary, name, i % multiprocessing.cpu_count(), - depth, - samples, - i == 0 or i == len(self.names) - 1, + depth[i] if depth is not None else None, + is_last_node, # Only last node has no output FIFOs + is_special_for_display, # First and last get special coloring + max_cycles, ) ) @@ -302,10 +318,21 @@ def run( try: result = future.result() # This will raise if there was an exception if result is not None: - sim_name, fifo_util, cycles, samps = result + ( + sim_name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + ) = result + fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles samples_results[sim_name] = samps + intervals_results[sim_name] = intervals + timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") # Set stop flag and break @@ -316,7 +343,6 @@ def run( # If we should stop, signal all remaining simulations with self.stop_lock: if self.should_stop: - self.console.log("Stopping all remaining simulations...") # Don't cancel - let them finish with early stop break @@ -328,12 +354,23 @@ def run( try: result = future.result() if result is not None: - sim_name, fifo_util, cycles, samps = result + ( + sim_name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + ) = result # Only update if not already collected if sim_name not in fifo_results: + fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles samples_results[sim_name] = samps + intervals_results[sim_name] = intervals + timeout_result = timeout_result or timeout except Exception as e: self.console.log(f"Error collecting result: {e}") finally: @@ -348,13 +385,15 @@ def run( { "name": name, "fifo_utilization": fifo_results.get(name, []), + "fifo_depth": fifo_depths.get(name, []), "cycles": cycles_results.get(name, 0), "samples": samples_results.get(name, 0), + "intervals": intervals_results.get(name, []), } for name in self.names ], "depth_configured": depth, - "samples_requested": samples, + "timeout_occurred": timeout_result, } output_json.write_text(json.dumps(merged_data, indent=2)) @@ -365,14 +404,25 @@ def _run_binary( binary: Path, name: str | None, _cpu: int | None, - depth: int, - samples: int, - is_end_node: bool = False, - ) -> tuple[str, list[int], int, int] | None: + depth: list[int] | None = None, + is_last_node: bool = False, + is_special_for_display: bool = False, + max_cycles: int | None = None, + ) -> tuple[str, list[int], int, int, list[int], bool, list[int]] | None: """Run the specified simulation binary in a new subprocess and communicate with it. + Args: + binary: Path to simulation binary + name: Name of simulation node + _cpu: CPU affinity (unused) + depth: List of FIFO depths for this node's output FIFOs + is_last_node: True if this is the last node (no output FIFOs to configure) + is_special_for_display: True if this node should get special color in logs + max_cycles: Maximum cycles to simulate + Returns: - Tuple of (simulation_name, fifo_utilization, cycles, samples) on success, + Tuple of (simulation_name, fifo_utilization, cycles, samples, intervals, timeout, + fifo_depth) on success, None on failure. """ cwd = binary.parent @@ -385,7 +435,7 @@ def _run_binary( def _print(msg: str, color: str = "green") -> None: if self.progress is None: - if is_end_node: + if is_special_for_display: color = "orange3" if "ERROR" in msg: color = "red" @@ -402,9 +452,14 @@ def _print(msg: str, color: str = "green") -> None: proc_idx = self._start_process(binary, process_index) # Send configuration commands - response = self._send_and_receive( - proc_idx, "configure", {"fifo_depth": depth, "samples": samples} - ) + # Last node has no output FIFOs, so don't configure FIFO depths + config_payload: dict[str, list[int] | int] = {} + if not is_last_node and depth is not None: + config_payload["fifo_depth"] = depth + if max_cycles is not None: + config_payload["max_cycles"] = max_cycles + + response = self._send_and_receive(proc_idx, "configure", config_payload) if not response or response.get("status") != "success": error_msg = ( @@ -423,23 +478,29 @@ def _print(msg: str, color: str = "green") -> None: _print(f"Failed to start simulation: {error_msg}", "red") return None + cycles = 0 + samps = 0 + intervals: list[int] = [] + timeout = False + fifo_util: list[int] = [] + fifo_depth: list[int] = [] + # Poll for status updates while True: # Check if we should stop early with self.stop_lock: if self.should_stop: stop_response = self._send_and_receive(proc_idx, "stop", {}) - fifo_util = [] - cycles = 0 - samps = 0 if stop_response: - fifo_util = stop_response.get("fifo_utilization", []) cycles = stop_response.get("cycles", 0) samps = stop_response.get("samples", 0) + fifo_util = stop_response.get("fifo_utilization", []) + intervals = stop_response.get("intervals", []) + fifo_depth = stop_response.get("fifo_depth", []) + timeout = stop_response.get("timeout", False) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") - return (name, fifo_util, cycles, samps) - + return (name, fifo_util, cycles, samps, intervals, timeout, fifo_depth) time.sleep(self.poll_interval) response = self._send_and_receive(proc_idx, "status", {}) @@ -452,14 +513,13 @@ def _print(msg: str, color: str = "green") -> None: state = response.get("state", "unknown") - if state == "finished": - _print("Simulation completed successfully") + if state == "finished" or state == "timeout": cycles = response.get("cycles", 0) - samples_done = response.get("samples", 0) + samps = response.get("samples", 0) fifo_util = response.get("fifo_utilization", []) - if self.progress is not None: - self.progress.update(name, samples_done, samples) - # Signal other simulations to stop + fifo_depth = response.get("fifo_depth", []) + intervals = response.get("intervals", []) + timeout = response.get("timeout", False) with self.stop_lock: self.should_stop = True break @@ -467,9 +527,6 @@ def _print(msg: str, color: str = "green") -> None: if state == "running": # Update progress if available cycles = response.get("cycles", 0) - samples_done = response.get("samples", 0) - if self.progress is not None and samples_done > 0: - self.progress.update(name, samples_done, samples) if state == "error": error_msg = response.get("message", "Unknown error") @@ -482,16 +539,16 @@ def _print(msg: str, color: str = "green") -> None: # Stop the simulation stop_response = self._send_and_receive(proc_idx, "stop", {}) fifo_util = [] - cycles = 0 - samps = 0 + if stop_response: fifo_util = stop_response.get("fifo_utilization", []) + fifo_depth = stop_response.get("fifo_depth", []) cycles = stop_response.get("cycles", 0) samps = stop_response.get("samples", 0) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") - return (name, fifo_util, cycles, samps) + return (name, fifo_util, cycles, samps, intervals, timeout, fifo_depth) except Exception as e: self.console.log(f"Exception caught during simulation execution ({name}): {e}") From 01fd709f211ec09535a5e96370d636dd50b023f3 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:34:32 +0100 Subject: [PATCH 035/170] Clean up a bit --- .../transformation/fpgadataflow/simulation.py | 105 ++---------------- 1 file changed, 10 insertions(+), 95 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index f0745ddd5c..93907f1415 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -917,6 +917,7 @@ def _minimize_fifo_depth( ) if success: best_working_depth = self.max_qsrl_depth + # TODO: If depth 256 works, try minimize in LUTRAM range return best_working_depth @@ -994,37 +995,19 @@ def needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: Returns: True if the FIFO can be minimized further, False otherwise. """ - # TODO: Make sure that the FIFOs are correctly instantiated afterwards. - # Everything <= max_qsrl_depth should use rtl fifos. - # TODO: Rewrite this method. We should set the FIFO style instead of the user, - # also QoR should not be used - # Qsrl FIFO Formula: LUTs = ⌈depth/32⌉ x ⌈bitwidth/2⌉ - if fifo_depth <= self.max_qsrl_depth and self.quality_of_results != "best": + if ( + fifo_depth <= self.max_qsrl_depth + ): # TODO: Later this should not be needed. Binary search over LUTRAM sizes. return False if fifo_depth <= 32: # FIFOs of depth <=32 fit into bitwidth/2 LUTs return False - # possible RAM styles: auto, block, distributed, ultra - if self.vivado_ram_style == "block" and calculate_bram_blocks(fifo_depth, bitwidth) == 1: - return False - if self.vivado_ram_style == "ultra" and calculate_uram_blocks(fifo_depth, bitwidth) == 1: - return False - if self.vivado_ram_style == "auto": - if ( - self.quality_of_results == "fast" - and calculate_uram_blocks(fifo_depth, bitwidth) == 1 - ): - return False - if ( - calculate_bram_blocks(fifo_depth, bitwidth) == 1 - and fifo_depth > self.max_qsrl_depth * 1.1 - ): - return False - - # Larger FIFOs with style distributed are always optimized further - # If more than 1 RAM block is used, we can try to reduce it further - - return True + # Return False if exactly 1 BRAM block is used and depth is sufficiently large + # that further optimization is unlikely to succeed + return not ( + calculate_bram_blocks(fifo_depth, bitwidth) == 1 + and fifo_depth > self.max_qsrl_depth * 1.1 + ) def calculate_bram_blocks(depth: int, bitwidth: int) -> int: @@ -1169,40 +1152,6 @@ def calculate_uram_depth_range(blocks: int, bitwidth: int) -> tuple[int, int]: return (min_depth, max_depth) -def calculate_smaller_uram_blocks(depth: int, bitwidth: int, dif: int) -> int: - """Calculate the biggest FIFO depth that uses fewer URAM blocks. - - Args: - depth: Current FIFO depth - bitwidth: Data bitwidth - dif: Number of URAM blocks to reduce by (will be clamped to available blocks) - - Returns: - Maximum depth that uses at least 'dif' fewer blocks, or uses half the current blocks - if dif is too large. - """ - current_uram_blocks = calculate_uram_blocks(depth, bitwidth) - if current_uram_blocks <= 1: - return depth # Cannot reduce further - - # If requested reduction is larger than what we have, reduce by half instead - target_blocks = current_uram_blocks - dif - if target_blocks < 1: - target_blocks = max(1, current_uram_blocks // 2) - - # Use the range function to find the maximum depth for target blocks - min_d, max_d = calculate_uram_depth_range(target_blocks, bitwidth) - if max_d > 0: - return max_d - - # Fallback to linear search if range calculation fails - for i in range(depth, 0, -1): - uram_blocks = calculate_uram_blocks(i, bitwidth) - if uram_blocks <= target_blocks: - return i - return depth - 1 # Fallback, should not happen - - def calculate_srl16e_luts(depth: int, bitwidth: int) -> int: """Calculate the number of SRL16E LUTs required for a FIFO. @@ -1249,37 +1198,3 @@ def calculate_srl16e_depth_range(luts: int, bitwidth: int) -> tuple[int, int]: return (0, 0) return (min_depth, max_depth) - - -def calculate_smaller_srl16e_luts(depth: int, bitwidth: int, dif: int) -> int: - """Calculate the biggest FIFO depth that uses fewer SRL16E LUTs. - - Args: - depth: Current FIFO depth - bitwidth: Data bitwidth - dif: Number of LUTs to reduce by (will be clamped to available LUTs) - - Returns: - Maximum depth that uses at least 'dif' fewer LUTs, or uses half the current LUTs - if dif is too large. - """ - current_luts = calculate_srl16e_luts(depth, bitwidth) - if current_luts <= 1: - return depth # Cannot reduce further - - # If requested reduction is larger than what we have, reduce by half instead - target_luts = current_luts - dif - if target_luts < 1: - target_luts = max(1, current_luts // 2) - - # Use the range function to find the maximum depth for target LUTs - min_d, max_d = calculate_srl16e_depth_range(target_luts, bitwidth) - if max_d > 0: - return max_d - - # Fallback to linear search if range calculation fails - for i in range(depth, 1, -1): - luts = calculate_srl16e_luts(i, bitwidth) - if luts <= target_luts: - return i - return depth - 1 # Fallback, should not happen From e3d08199f5c234859e5181c2304ff0eb0dbe35ba Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 8 Jan 2026 16:55:45 +0100 Subject: [PATCH 036/170] Fix bugs, optimize search and swap sim to InterprocessCommunicationChannel --- .../InterprocessCommunicationChannel.hpp | 207 ++++ finn_xsi/finn_xsi/include/Simulation.hpp | 19 +- finn_xsi/finn_xsi/unittests/CMakeLists.txt | 12 +- .../finn_xsi/unittests/Integration_test.cpp | 396 +++---- .../InterprocessCommunicationChannel_test.cpp | 1037 +++++++++++++++++ .../transformation/fpgadataflow/simulation.py | 287 +++-- .../fpgadataflow/simulation_controller.py | 2 +- tests/fpgadataflow/test_bram_block_search.py | 465 ++++++++ 8 files changed, 2134 insertions(+), 291 deletions(-) create mode 100644 finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp create mode 100644 finn_xsi/finn_xsi/unittests/InterprocessCommunicationChannel_test.cpp create mode 100644 tests/fpgadataflow/test_bram_block_search.py diff --git a/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp b/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp new file mode 100644 index 0000000000..59bc4cb140 --- /dev/null +++ b/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp @@ -0,0 +1,207 @@ +#ifndef INTERPROCESSCOMMUNICATIONCHANNEL +#define INTERPROCESSCOMMUNICATIONCHANNEL + +#include +#include +#include +#include + +#ifndef CACHE_LINE_SIZE + #ifdef __cpp_lib_hardware_interference_size +constexpr std::size_t CACHE_LINE_SIZE = std::hardware_destructive_interference_size; + #else +constexpr std::size_t CACHE_LINE_SIZE = 64; + #endif +#endif + +namespace bip = boost::interprocess; + +// ===== INTERPROCESS ASYMMETRIC REQUEST-RESPONSE EXCHANGE ===== +// Concepts for constraining methods based on role +template +concept Sender = IsSender; + +template +class InterprocessCommunicationChannel { + private: + // ===== SHARED MEMORY STRUCTURE ===== + struct alignas(CACHE_LINE_SIZE) SharedChannelData { + struct alignas(CACHE_LINE_SIZE) RequestSlot { + Request data; + std::atomic valid; + + RequestSlot() : data(), valid(false) {} + }; + + struct alignas(CACHE_LINE_SIZE) ResponseSlot { + Response data; + std::atomic valid; + + ResponseSlot() : data(), valid(false) {} + }; + + // Double-buffered requests and responses + RequestSlot requests[2]; + ResponseSlot responses[2]; + + alignas(CACHE_LINE_SIZE) std::atomic request_write_idx; + alignas(CACHE_LINE_SIZE) std::atomic request_read_idx; + alignas(CACHE_LINE_SIZE) std::atomic response_write_idx; + alignas(CACHE_LINE_SIZE) std::atomic response_read_idx; + + SharedChannelData() : request_write_idx(0), request_read_idx(0), response_write_idx(0), response_read_idx(0) { + // Verify atomics are lock-free (required for shared memory) + static_assert(std::atomic::is_always_lock_free, "std::atomic must be lock-free for inter-process use"); + static_assert(std::atomic::is_always_lock_free, "std::atomic must be lock-free for inter-process use"); + } + }; + + // ===== PROCESS-LOCAL STATE ===== + SharedChannelData* channel = nullptr; + std::atomic* refCount = nullptr; + const std::string sharedMemoryName; + bip::managed_shared_memory shmem; + + public: + // Default constructor + InterprocessCommunicationChannel() : sharedMemoryName("") {} + + // Constructor with shared memory name + InterprocessCommunicationChannel(const std::string& shmName) : sharedMemoryName(shmName) { + if constexpr (IsSender) { + // Sender creates shared memory + bip::shared_memory_object::remove(sharedMemoryName.c_str()); + shmem = bip::managed_shared_memory(bip::create_only, sharedMemoryName.c_str(), SharedMemorySize); + } else { + // Receiver opens existing shared memory + while (true) { + try { + shmem = bip::managed_shared_memory(bip::open_only, sharedMemoryName.c_str()); + break; + } catch (const bip::interprocess_exception& e) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } + } + } + + // Construct or find the reference counter + refCount = shmem.find_or_construct>("refCount")(0); + refCount->fetch_add(1, std::memory_order_acq_rel); + + // Construct the channel data in shared memory + channel = shmem.find_or_construct("ChannelData")(); + } + + // Delete copy operations + InterprocessCommunicationChannel(const InterprocessCommunicationChannel&) = delete; + InterprocessCommunicationChannel& operator=(const InterprocessCommunicationChannel&) = delete; + + // Move constructor + InterprocessCommunicationChannel(InterprocessCommunicationChannel&& other) noexcept + : channel(other.channel), refCount(other.refCount), sharedMemoryName(std::move(other.sharedMemoryName)), shmem(std::move(other.shmem)) { + other.channel = nullptr; + other.refCount = nullptr; + } + + // Move assignment operator + InterprocessCommunicationChannel& operator=(InterprocessCommunicationChannel&& other) noexcept { + if (this != &other) { + channel = other.channel; + refCount = other.refCount; + shmem.swap(other.shmem); + const_cast(sharedMemoryName) = std::move(other.sharedMemoryName); + + other.channel = nullptr; + other.refCount = nullptr; + } + return *this; + } + + ~InterprocessCommunicationChannel() { + if (!refCount || !channel) { + return; + } + + channel = nullptr; + refCount = nullptr; + + std::atomic* ref_ptr = shmem.find>("refCount").first; + if (!ref_ptr) { + return; + } + + int remainingRefs = ref_ptr->fetch_sub(1, std::memory_order_acq_rel) - 1; + + if (remainingRefs == 0) { + shmem.destroy("ChannelData"); + shmem.destroy>("refCount"); + shmem = bip::managed_shared_memory(); + bip::shared_memory_object::remove(sharedMemoryName.c_str()); + } + } + + // SENDER SIDE: Send request, wait for response + Response send_request(const Request& req, std::stop_token stoken = {}) + requires Sender + { + // Write request + int write_slot = channel->request_write_idx.load(std::memory_order_acquire) % 2; + channel->requests[write_slot].data = req; + channel->requests[write_slot].valid.store(true, std::memory_order_release); + channel->request_write_idx.fetch_add(1, std::memory_order_release); + + // Wait for response in corresponding slot + int read_slot = channel->response_read_idx.load(std::memory_order_acquire) % 2; + while (!channel->responses[read_slot].valid.load(std::memory_order_acquire) && !stoken.stop_requested()) { +#if defined(__x86_64__) || defined(_M_X64) + __builtin_ia32_pause(); +#elif defined(__aarch64__) + asm volatile("yield" ::: "memory"); +#endif + } + + if (stoken.stop_requested()) { + return Response{}; // Return default-constructed response on cancellation + } + + Response resp = channel->responses[read_slot].data; + channel->responses[read_slot].valid.store(false, std::memory_order_release); + channel->response_read_idx.fetch_add(1, std::memory_order_release); + + return resp; + } + + // RECEIVER SIDE: Wait for request, send response + Request receive_request(std::stop_token stoken = {}) + requires(!Sender) + { + int read_slot = channel->request_read_idx.load(std::memory_order_acquire) % 2; + + while (!channel->requests[read_slot].valid.load(std::memory_order_acquire) && !stoken.stop_requested()) { +#if defined(__x86_64__) || defined(_M_X64) + __builtin_ia32_pause(); +#elif defined(__aarch64__) + asm volatile("yield" ::: "memory"); +#endif + } + + if (stoken.stop_requested()) { + return Request{}; // Return default-constructed request on cancellation + } + + Request req = channel->requests[read_slot].data; + channel->requests[read_slot].valid.store(false, std::memory_order_release); + channel->request_read_idx.fetch_add(1, std::memory_order_release); + + return req; + } + + void send_response(const Response& resp) + requires(!Sender) + { + int write_slot = channel->response_write_idx.load(std::memory_order_acquire) % 2; + channel->responses[write_slot].data = resp; + channel->responses[write_slot].valid.store(true, std::memory_order_release); + channel->response_write_idx.fetch_add(1, std::memory_order_release); + } +}; + +#endif /* INTERPROCESSCOMMUNICATIONCHANNEL */ diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 72cb6ff4b8..fd85766fce 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -9,7 +9,8 @@ #include #include -#include +//#include +#include #include #include #include @@ -91,6 +92,11 @@ class Simulation { } }; +//Small struct used for exange. Will be changed later to more complex data structure. +struct CommData{ + bool data; +}; + // Communication Flow: // // valid ┌──────────────────────────────────────┐ valid valid @@ -102,8 +108,8 @@ class Simulation { // └──────────────────────────────────────┘ template class SingleNodeSimulation : public Simulation { - using ConsumingInterface = InterSimulationInterface; - using ProducingInterface = InterSimulationInterface; + using ConsumingInterface = InterprocessCommunicationChannel; + using ProducingInterface = InterprocessCommunicationChannel; constexpr static bool FirstNode = NodeIndex == 0; constexpr static bool LastNode = NodeIndex == (TotalNodes - 1); std::array fromProducerInterface; @@ -117,13 +123,14 @@ class SingleNodeSimulation : public Simulation sim - this->istreams[i].valid(fromProducerInterface[i].exchange(this->istreams[i].isReady(), stoken)); + this->istreams[i].valid(fromProducerInterface[i].receive_request(stoken).data); + fromProducerInterface[i].send_response(CommData{this->istreams[i].isReady()}); } } if constexpr (!LastNode) { for (std::size_t i = 0; i < OStreamsSize; ++i) { // Interface sim -valid-> FIFO <-> SHM - this->fifo[i].update(this->ostreams[i].isValid(), toConsumerInterface[i].exchange(this->fifo[i].isOutputValid(), stoken)); + this->fifo[i].update(this->ostreams[i].isValid(), toConsumerInterface[i].send_request(CommData{this->fifo[i].isOutputValid()}, stoken).data); // FIFO -ready-> sim this->ostreams[i].ready(this->fifo[i].isInputReady()); // Toggle FIFO clock @@ -238,7 +245,7 @@ class SingleNodeSimulation : public Simulation::max()) { while (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](const M_AXIS_Control& stream) { return stream.stableState.is_stable(); }) & - !stoken.stop_requested() & cyclesRun <= max_cycles) { + !stoken.stop_requested() & (cyclesRun <= max_cycles)) { runSingleCycle(stoken); runSingleCycle(stoken); runSingleCycle(stoken); diff --git a/finn_xsi/finn_xsi/unittests/CMakeLists.txt b/finn_xsi/finn_xsi/unittests/CMakeLists.txt index a9189d01bb..e42702a99c 100644 --- a/finn_xsi/finn_xsi/unittests/CMakeLists.txt +++ b/finn_xsi/finn_xsi/unittests/CMakeLists.txt @@ -20,12 +20,17 @@ target_include_directories(FIFO_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/inclu # Add InterSimulationInterface unit tests add_executable(InterSimulationInterface_test InterSimulationInterface_test.cpp) -target_link_libraries(InterSimulationInterface_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt) +target_link_libraries(InterSimulationInterface_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt nlohmann_json::nlohmann_json) target_include_directories(InterSimulationInterface_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ${Boost_INCLUDE_DIRS}) +# Add InterprocessCommunicationChannel unit tests +add_executable(InterprocessCommunicationChannel_test InterprocessCommunicationChannel_test.cpp) +target_link_libraries(InterprocessCommunicationChannel_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt nlohmann_json::nlohmann_json) +target_include_directories(InterprocessCommunicationChannel_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ${Boost_INCLUDE_DIRS}) + # Add Integration tests (FIFO + InterSimulationInterface) add_executable(Integration_test Integration_test.cpp ${CORE_SRC}) -target_link_libraries(Integration_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt) +target_link_libraries(Integration_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt nlohmann_json::nlohmann_json) target_include_directories(Integration_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ${Boost_INCLUDE_DIRS}) target_include_directories(Integration_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") @@ -33,8 +38,9 @@ target_include_directories(Integration_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsi include(GoogleTest) gtest_discover_tests(FIFO_test) gtest_discover_tests(InterSimulationInterface_test) +gtest_discover_tests(InterprocessCommunicationChannel_test) gtest_discover_tests(Integration_test) # Create a target to build all unittests at once add_custom_target(all_unittests) -add_dependencies(all_unittests FIFO_test InterSimulationInterface_test Integration_test) +add_dependencies(all_unittests FIFO_test InterSimulationInterface_test InterprocessCommunicationChannel_test Integration_test) diff --git a/finn_xsi/finn_xsi/unittests/Integration_test.cpp b/finn_xsi/finn_xsi/unittests/Integration_test.cpp index 350a0a21c9..90425bdc85 100644 --- a/finn_xsi/finn_xsi/unittests/Integration_test.cpp +++ b/finn_xsi/finn_xsi/unittests/Integration_test.cpp @@ -47,27 +47,36 @@ class SimDummy { TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair - // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); if (pid == 0) { - // Child process: Receiver with FIFO output validation + // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { InterSimulationInterface receiver(shmName); + FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(false); - bool readySignal = simDummy.isInputReady(); + bool readySignal = outputFifo.isInputReady(); bool validSignal = receiver.exchange(readySignal); if (validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - // Verify we received all data + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 15) { + exit(3); + } + if (outputFifo.isInputReady() != true) { + exit(4); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); } @@ -75,21 +84,13 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { exit(0); } - // Parent process: Sender with FIFO input + // Parent process: Sender { InterSimulationInterface sender(shmName); - FIFO inputFifo(15); bool validSignal = false; - EXPECT_TRUE(inputFifo.isInputReady()); - bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + bool incomingReady = sender.exchange(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==false for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); } // Destructor called here @@ -101,27 +102,36 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair - // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); if (pid == 0) { - // Child process: Receiver with FIFO output validation + // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { InterSimulationInterface receiver(shmName); + FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(true); - bool readySignal = simDummy.isInputReady(); + bool readySignal = outputFifo.isInputReady(); bool validSignal = receiver.exchange(readySignal); if (validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - // Verify we received all data + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 15) { + exit(3); + } + if (outputFifo.isInputReady() != true) { + exit(4); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); } @@ -129,21 +139,13 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { exit(0); } - // Parent process: Sender with FIFO input + // Parent process: Sender { InterSimulationInterface sender(shmName); - FIFO inputFifo(15); bool validSignal = false; - EXPECT_TRUE(inputFifo.isInputReady()); - bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + bool incomingReady = sender.exchange(validSignal); EXPECT_TRUE(incomingReady); - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); } // Destructor called here @@ -155,50 +157,54 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { TEST_F(IntegrationTest, OneCycleReadyFalseValidTrue) { // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair - // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); if (pid == 0) { - // Child process: Receiver with FIFO output validation + // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { InterSimulationInterface receiver(shmName); + FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(false); - bool readySignal = simDummy.isInputReady(); + bool readySignal = outputFifo.isInputReady(); bool validSignal = receiver.exchange(readySignal); - if (validSignal) { // It is correct that valid is false here, because we only have a single cycle and the fifo input is set to valid in cycle 0. Therefore, the FIFO - // output is valid in cycle 1 and we should receive a valid in cycle 1. + if (!validSignal) { // It is correct that valid is true here, because we only have a single cycle and the sender input is set to valid in cycle 0. Therefore, we should + // receive a valid in cycle 0. exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - // Verify we received all data - if (simDummy.isOutputValid()) { + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 14) { + exit(3); + } + if (outputFifo.isInputReady() != true) { + exit(4); + } + if (!outputFifo.isOutputValid()) { + exit(5); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); + if (!simDummy.isOutputValid()) { exit(1); } } // Destructor called here exit(0); } - // Parent process: Sender with FIFO input + // Parent process: Sender { InterSimulationInterface sender(shmName); - FIFO inputFifo(15); bool validSignal = true; - EXPECT_TRUE(inputFifo.isInputReady()); - bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + bool incomingReady = sender.exchange(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 14); - EXPECT_TRUE(inputFifo.isInputReady()); } // Destructor called here @@ -210,50 +216,54 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidTrue) { TEST_F(IntegrationTest, OneCycleReadyTrueValidTrue) { // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair - // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); if (pid == 0) { - // Child process: Receiver with FIFO output validation + // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { InterSimulationInterface receiver(shmName); + FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(true); - bool readySignal = simDummy.isInputReady(); + bool readySignal = outputFifo.isInputReady(); bool validSignal = receiver.exchange(readySignal); - if (validSignal) { // It is correct that valid is false here, because we only have a single cycle and the fifo input is set to valid in cycle 0. Therefore, the FIFO - // output is valid in cycle 1 and we should receive a valid in cycle 1. + if (!validSignal) { // It is correct that valid is true here, because we only have a single cycle and the sender input is set to valid in cycle 0. Therefore, we should + // receive a valid in cycle 0. exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - // Verify we received all data - if (simDummy.isOutputValid()) { + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 14) { + exit(3); + } + if (outputFifo.isInputReady() != true) { + exit(4); + } + if (!outputFifo.isOutputValid()) { + exit(5); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); + if (!simDummy.isOutputValid()) { exit(1); } } // Destructor called here exit(0); } - // Parent process: Sender with FIFO input + // Parent process: Sender { InterSimulationInterface sender(shmName); - FIFO inputFifo(15); bool validSignal = true; - EXPECT_TRUE(inputFifo.isInputReady()); - bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + bool incomingReady = sender.exchange(validSignal); EXPECT_TRUE(incomingReady); - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 14); - EXPECT_TRUE(inputFifo.isInputReady()); } // Destructor called here @@ -267,41 +277,52 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidTrue) { TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair - // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); if (pid == 0) { - // Child process: Receiver with FIFO output validation + // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { InterSimulationInterface receiver(shmName); + FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(false); - bool readySignal = simDummy.isInputReady(); + bool readySignal = outputFifo.isInputReady(); bool validSignal = receiver.exchange(readySignal); if (validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - // Verify we received all data + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 15) { + exit(3); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); } simDummy.setNextReady(false); - readySignal = simDummy.isInputReady(); // Should be false now + readySignal = outputFifo.isInputReady(); // Should be true validSignal = receiver.exchange(readySignal); if (validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 2 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS - // Verify we received all data + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 15) { + exit(3); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); } @@ -310,30 +331,15 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { exit(0); } - // Parent process: Sender with FIFO input + // Parent process: Sender { InterSimulationInterface sender(shmName); - FIFO inputFifo(15); bool validSignal = false; - EXPECT_TRUE(inputFifo.isInputReady()); - bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + bool incomingReady = sender.exchange(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - EXPECT_TRUE(inputFifo.isInputReady()); - incomingReady = sender.exchange(inputFifo.isOutputValid()); - EXPECT_FALSE(incomingReady); // We are in cycle 1; expect ready==false for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); + incomingReady = sender.exchange(validSignal); + EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 2 } // Destructor called here @@ -345,41 +351,52 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair - // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); if (pid == 0) { - // Child process: Receiver with FIFO output validation + // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { InterSimulationInterface receiver(shmName); + FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(true); - bool readySignal = simDummy.isInputReady(); + bool readySignal = outputFifo.isInputReady(); bool validSignal = receiver.exchange(readySignal); if (validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - // Verify we received all data + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 15) { + exit(3); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); } simDummy.setNextReady(true); - readySignal = simDummy.isInputReady(); // Should be true now + readySignal = outputFifo.isInputReady(); // Should be true now validSignal = receiver.exchange(readySignal); if (validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 2 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS - // Verify we received all data + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 15) { + exit(3); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); } @@ -388,30 +405,15 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { exit(0); } - // Parent process: Sender with FIFO input + // Parent process: Sender { InterSimulationInterface sender(shmName); - FIFO inputFifo(15); bool validSignal = false; - EXPECT_TRUE(inputFifo.isInputReady()); - bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + bool incomingReady = sender.exchange(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - EXPECT_TRUE(inputFifo.isInputReady()); - incomingReady = sender.exchange(inputFifo.isOutputValid()); - EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); + incomingReady = sender.exchange(validSignal); + EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 2 } // Destructor called here @@ -423,41 +425,58 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair - // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); if (pid == 0) { - // Child process: Receiver with FIFO output validation + // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { InterSimulationInterface receiver(shmName); + FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(false); - bool readySignal = simDummy.isInputReady(); + bool readySignal = outputFifo.isInputReady(); bool validSignal = receiver.exchange(readySignal); - if (validSignal) { + if (!validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - // Verify we received all data - if (simDummy.isOutputValid()) { + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 14) { + exit(3); + } + if (!outputFifo.isOutputValid()) { + exit(4); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); + if (!simDummy.isOutputValid()) { exit(1); } simDummy.setNextReady(false); - readySignal = simDummy.isInputReady(); // Should be false now + readySignal = outputFifo.isInputReady(); // Should be true now (FIFO not full) validSignal = receiver.exchange(readySignal); if (!validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 2 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS - // Verify we received all data + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 13) { + exit(3); + } + if (!outputFifo.isOutputValid()) { + exit(4); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); if (!simDummy.isOutputValid()) { exit(1); } @@ -466,30 +485,15 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { exit(0); } - // Parent process: Sender with FIFO input + // Parent process: Sender { InterSimulationInterface sender(shmName); - FIFO inputFifo(15); bool validSignal = true; - EXPECT_TRUE(inputFifo.isInputReady()); - bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + bool incomingReady = sender.exchange(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 14); - EXPECT_TRUE(inputFifo.isInputReady()); - EXPECT_TRUE(inputFifo.isOutputValid()); - incomingReady = sender.exchange(inputFifo.isOutputValid()); - EXPECT_FALSE(incomingReady); // We are in cycle 1; expect ready==false for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 14); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 13); - EXPECT_TRUE(inputFifo.isInputReady()); + incomingReady = sender.exchange(validSignal); + EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 2 } // Destructor called here @@ -502,41 +506,58 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { TEST_F(IntegrationTest, TwoCycleReadyTrueValidTrue) { // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair - // Architecture: FIFO (process A) -> Sender -> Receiver (process B) -> SimDummy -> validation + // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); if (pid == 0) { - // Child process: Receiver with FIFO output validation + // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { InterSimulationInterface receiver(shmName); + FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(true); - bool readySignal = simDummy.isInputReady(); + bool readySignal = outputFifo.isInputReady(); bool validSignal = receiver.exchange(readySignal); - if (validSignal) { + if (!validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 1 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - // Verify we received all data - if (simDummy.isOutputValid()) { + // Verify FIFO state and SimDummy + if (outputFifo.getSpaceLeft() != 14) { + exit(3); + } + if (!outputFifo.isOutputValid()) { + exit(4); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); + if (!simDummy.isOutputValid()) { exit(1); } simDummy.setNextReady(true); - readySignal = simDummy.isInputReady(); // Should be true now + readySignal = outputFifo.isInputReady(); // Should be true now validSignal = receiver.exchange(readySignal); if (!validSignal) { exit(2); } - simDummy.setNextValid(validSignal); - simDummy.toggleClock(); // BELOW HERE CYCLE 2 STARTS + outputFifo.update(validSignal, simDummy.isInputReady()); + outputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS - // Verify we received all data + // Verify FIFO state and SimDummy - FIFO consumes data because SimDummy is ready + if (outputFifo.getSpaceLeft() != 14) { + exit(3); + } + if (!outputFifo.isOutputValid()) { + exit(4); + } + simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.toggleClock(); if (!simDummy.isOutputValid()) { exit(1); } @@ -545,30 +566,15 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidTrue) { exit(0); } - // Parent process: Sender with FIFO input + // Parent process: Sender { InterSimulationInterface sender(shmName); - FIFO inputFifo(15); bool validSignal = true; - EXPECT_TRUE(inputFifo.isInputReady()); - bool incomingReady = sender.exchange(inputFifo.isOutputValid()); + bool incomingReady = sender.exchange(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 15); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 14); - EXPECT_TRUE(inputFifo.isInputReady()); - EXPECT_TRUE(inputFifo.isOutputValid()); - incomingReady = sender.exchange(inputFifo.isOutputValid()); - EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 1 - inputFifo.update(validSignal, incomingReady); - EXPECT_EQ(inputFifo.getSpaceLeft(), 14); - EXPECT_TRUE(inputFifo.isInputReady()); - inputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS - EXPECT_EQ(inputFifo.getSpaceLeft(), 14); - EXPECT_TRUE(inputFifo.isInputReady()); + incomingReady = sender.exchange(validSignal); + EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 2 } // Destructor called here @@ -587,7 +593,7 @@ TEST_F(IntegrationTest, SimToFIFO) { SimDummy sim; FIFO fifo(15); - //Propagate valid through SimDummy + // Propagate valid through SimDummy sim.setNextValid(true); fifo.update(sim.isOutputValid(), false); EXPECT_TRUE(fifo.isInputReady()); @@ -597,7 +603,7 @@ TEST_F(IntegrationTest, SimToFIFO) { EXPECT_EQ(fifo.size(), 0); EXPECT_TRUE(sim.isInputReady()); - //Fill FIFO to capacity + // Fill FIFO to capacity for (std::size_t i = 0; i < 15; ++i) { sim.setNextValid(true); @@ -607,17 +613,17 @@ TEST_F(IntegrationTest, SimToFIFO) { EXPECT_EQ(fifo.size(), i); fifo.toggleClock(); sim.toggleClock(); - EXPECT_EQ(fifo.size(), i+1); + EXPECT_EQ(fifo.size(), i + 1); EXPECT_TRUE(sim.isInputReady()); } - EXPECT_FALSE(fifo.isInputReady()); // FIFO changed to not ready on this cycle; Sim is still ready + EXPECT_FALSE(fifo.isInputReady()); // FIFO changed to not ready on this cycle; Sim is still ready sim.setNextValid(true); fifo.update(sim.isOutputValid(), false); EXPECT_FALSE(fifo.isInputReady()); sim.setNextReady(fifo.isInputReady()); fifo.toggleClock(); - sim.toggleClock(); //Propagate ready false through sim + sim.toggleClock(); // Propagate ready false through sim EXPECT_EQ(fifo.size(), 15); EXPECT_FALSE(sim.isInputReady()); diff --git a/finn_xsi/finn_xsi/unittests/InterprocessCommunicationChannel_test.cpp b/finn_xsi/finn_xsi/unittests/InterprocessCommunicationChannel_test.cpp new file mode 100644 index 0000000000..45db5496e7 --- /dev/null +++ b/finn_xsi/finn_xsi/unittests/InterprocessCommunicationChannel_test.cpp @@ -0,0 +1,1037 @@ +#include "InterprocessCommunicationChannel.hpp" + +#include +#include +#include + +#include +#include + +// Simple request/response types for testing +struct TestRequest { + int value; + bool flag; + + TestRequest() : value(0), flag(false) {} + TestRequest(int v, bool f) : value(v), flag(f) {} + + bool operator==(const TestRequest& other) const { return value == other.value && flag == other.flag; } +}; + +struct TestResponse { + int result; + bool success; + + TestResponse() : result(0), success(false) {} + TestResponse(int r, bool s) : result(r), success(s) {} + + bool operator==(const TestResponse& other) const { return result == other.result && success == other.success; } +}; + +// Test fixture for InterprocessCommunicationChannel tests +class InterprocessCommunicationChannelTest : public ::testing::Test { + protected: + void SetUp() override { + // Generate unique shared memory name for each test + shmName = "test_ipc_" + std::to_string(getpid()) + "_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + } + + void TearDown() override { + // Cleanup: ensure shared memory is removed + boost::interprocess::shared_memory_object::remove(shmName.c_str()); + } + + std::string shmName; +}; + +// ===== Constructor and Initialization Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, SenderConstructorCreatesSharedMemory) { + InterprocessCommunicationChannel sender(shmName); + + // Verify that shared memory exists + bool shmExists = false; + try { + boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); + shmExists = true; + } catch (...) { shmExists = false; } + + EXPECT_TRUE(shmExists); +} + +TEST_F(InterprocessCommunicationChannelTest, ReceiverWaitsForSenderToCreateSharedMemory) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver (waits for sender) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + InterprocessCommunicationChannel receiver(shmName); + exit(0); + } else { + // Parent process: Sender (creates shared memory) + InterprocessCommunicationChannel sender(shmName); + + // Wait for child to complete + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, DefaultConstructorCreatesUninitializedObject) { + InterprocessCommunicationChannel channel; + // Should not crash - object is in moved-from state + // Destructor should handle this gracefully +} + +TEST_F(InterprocessCommunicationChannelTest, MoveConstructorTransfersOwnership) { + InterprocessCommunicationChannel sender1(shmName); + InterprocessCommunicationChannel sender2(std::move(sender1)); + + // sender2 should now own the shared memory + // sender1 should be in moved-from state (destructor shouldn't crash) +} + +TEST_F(InterprocessCommunicationChannelTest, MoveAssignmentTransfersOwnership) { + InterprocessCommunicationChannel sender1(shmName); + InterprocessCommunicationChannel sender2; + + sender2 = std::move(sender1); + + // sender2 should now own the shared memory + // sender1 should be in moved-from state +} + +// ===== Single Request-Response Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, SingleRequestResponseExchange) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender sends request, waits for response + InterprocessCommunicationChannel sender(shmName); + + TestRequest req(42, true); + TestResponse resp = sender.send_request(req); + + // Verify response + exit((resp.result == 84 && resp.success) ? 0 : 1); + } else { + // Parent process: Receiver waits for request, sends response + InterprocessCommunicationChannel receiver(shmName); + + // Small delay to ensure both processes are ready + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, 42); + EXPECT_TRUE(req.flag); + + // Send response (double the request value) + TestResponse resp(req.value * 2, true); + receiver.send_response(resp); + + // Wait for child and check result + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, RequestResponseWithDifferentValues) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterprocessCommunicationChannel sender(shmName); + + TestRequest req(100, false); + TestResponse resp = sender.send_request(req); + + exit((resp.result == 200 && !resp.success) ? 0 : 1); + } else { + // Parent process: Receiver + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, 100); + EXPECT_FALSE(req.flag); + + TestResponse resp(req.value * 2, req.flag); + receiver.send_response(resp); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Multiple Request-Response Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, MultipleRequestResponseSequential) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender sends multiple requests + InterprocessCommunicationChannel sender(shmName); + + for (int i = 0; i < 10; ++i) { + TestRequest req(i, i % 2 == 0); + TestResponse resp = sender.send_request(req); + + // Verify response matches expected calculation + if (resp.result != i * 3 || resp.success != (i % 2 == 0)) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver processes multiple requests + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 10; ++i) { + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, i); + EXPECT_EQ(req.flag, i % 2 == 0); + + // Send calculated response + TestResponse resp(req.value * 3, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, ManyRequestResponseExchanges) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterprocessCommunicationChannel sender(shmName); + + for (int i = 0; i < 1000; ++i) { + TestRequest req(i % 100, i % 3 == 0); + TestResponse resp = sender.send_request(req); + + // Verify response + int expected = (i % 100) + 10; + if (resp.result != expected) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 1000; ++i) { + TestRequest req = receiver.receive_request(); + + // Just verify exchange completes without deadlock + int expected_val = i % 100; + EXPECT_EQ(req.value, expected_val); + + TestResponse resp(req.value + 10, true); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, AlternatingRequestPattern) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with alternating pattern + InterprocessCommunicationChannel sender(shmName); + + for (int i = 0; i < 100; ++i) { + bool flag = (i % 2 == 0); + TestRequest req(i, flag); + TestResponse resp = sender.send_request(req); + + // Verify response + if (resp.result != i * 2 || resp.success != flag) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 100; ++i) { + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, i); + EXPECT_EQ(req.flag, i % 2 == 0); + + TestResponse resp(req.value * 2, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Buffer Flipping Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, BufferFlipsCorrectly) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender + InterprocessCommunicationChannel sender(shmName); + + // Perform multiple exchanges to trigger buffer flips + for (int i = 0; i < 20; ++i) { + TestRequest req(i, true); + TestResponse resp = sender.send_request(req); + + if (resp.result != i + 1) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Perform multiple exchanges - buffer should flip multiple times + for (int i = 0; i < 20; ++i) { + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, i); + + TestResponse resp(req.value + 1, true); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Stress Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, HighFrequencyExchanges) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender - rapid exchanges + InterprocessCommunicationChannel sender(shmName); + + for (int i = 0; i < 10000; ++i) { + TestRequest req(i & 0xFF, i & 1); + TestResponse resp = sender.send_request(req); + + if (resp.result != (i & 0xFF) * 2) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver - rapid exchanges + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 10000; ++i) { + TestRequest req = receiver.receive_request(); + + TestResponse resp(req.value * 2, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, StressTestWithComplexPattern) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with complex pattern + InterprocessCommunicationChannel sender(shmName); + + for (int i = 0; i < 5000; ++i) { + int val = (i * 7) % 127; + bool flag = ((i * 11) % 13) < 6; + TestRequest req(val, flag); + TestResponse resp = sender.send_request(req); + + if (resp.result != val + 5) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver with response calculation + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 5000; ++i) { + TestRequest req = receiver.receive_request(); + + TestResponse resp(req.value + 5, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Reference Counting Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, ReferenceCountingTwoProcesses) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Create sender and let it go out of scope + { + InterprocessCommunicationChannel sender(shmName); + TestRequest req(1, true); + sender.send_request(req); + } + + // Shared memory should still exist because parent still holds reference + bool shmExists = false; + try { + boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); + shmExists = true; + } catch (...) { shmExists = false; } + + exit(shmExists ? 0 : 1); + } else { + // Parent process: Keep receiver alive + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + TestRequest req = receiver.receive_request(); + TestResponse resp(req.value, req.flag); + receiver.send_response(resp); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, SharedMemoryCleanupAfterBothProcessesExit) { + // This test verifies that shared memory is properly cleaned up + // when both processes exit. + + pid_t verifier_pid = fork(); + + if (verifier_pid == 0) { + // Verifier process: spawns two children and then checks cleanup + pid_t sender_pid = fork(); + + if (sender_pid == 0) { + // First child: Sender + // Use block scope so destructor is called before exit + { + InterprocessCommunicationChannel sender(shmName); + TestRequest req(42, true); + sender.send_request(req); + } // Destructor called here + exit(0); + } + + // Small delay to ensure sender creates shared memory + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + pid_t receiver_pid = fork(); + if (receiver_pid == 0) { + // Second child: Receiver + // Use block scope so destructor is called before exit + { + InterprocessCommunicationChannel receiver(shmName); + TestRequest req = receiver.receive_request(); + TestResponse resp(req.value, req.flag); + receiver.send_response(resp); + } // Destructor called here + exit(0); + } + + // Wait for both children to complete + int sender_status, receiver_status; + waitpid(sender_pid, &sender_status, 0); + waitpid(receiver_pid, &receiver_status, 0); + + // Give time for cleanup to complete + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Verify shared memory is cleaned up + bool shmExists = false; + try { + boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); + shmExists = true; + } catch (...) { shmExists = false; } + + // Exit with 0 if cleanup succeeded (shmExists == false) + exit(shmExists ? 1 : 0); + } else { + // Parent: Wait for verifier process + int status; + waitpid(verifier_pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Move Semantics Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, MoveConstructorMaintainsConnection) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with move + InterprocessCommunicationChannel sender1(shmName); + InterprocessCommunicationChannel sender2(std::move(sender1)); + + TestRequest req(99, false); + TestResponse resp = sender2.send_request(req); + + exit((resp.result == 99 && !resp.success) ? 0 : 1); + } else { + // Parent process: Receiver + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, 99); + EXPECT_FALSE(req.flag); + + TestResponse resp(req.value, req.flag); + receiver.send_response(resp); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, MoveAssignmentMaintainsConnection) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with move assignment + InterprocessCommunicationChannel sender1(shmName); + InterprocessCommunicationChannel sender2; + sender2 = std::move(sender1); + + TestRequest req(77, true); + TestResponse resp = sender2.send_request(req); + + exit((resp.result == 77 && resp.success) ? 0 : 1); + } else { + // Parent process: Receiver + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, 77); + EXPECT_TRUE(req.flag); + + TestResponse resp(req.value, req.flag); + receiver.send_response(resp); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Edge Cases ===== + +TEST_F(InterprocessCommunicationChannelTest, FirstCallBehavior) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender - first call should not wait for buffer flip + InterprocessCommunicationChannel sender(shmName); + + auto start = std::chrono::steady_clock::now(); + TestRequest req(1, true); + sender.send_request(req); + auto end = std::chrono::steady_clock::now(); + + // First call should complete quickly (not waiting for previous flip) + auto duration = std::chrono::duration_cast(end - start); + exit(duration.count() < 100 ? 0 : 1); + } else { + // Parent process: Receiver + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + TestRequest req = receiver.receive_request(); + TestResponse resp(req.value, req.flag); + receiver.send_response(resp); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, ConsecutiveRequestsSameValue) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Send same request repeatedly + InterprocessCommunicationChannel sender(shmName); + + for (int i = 0; i < 50; ++i) { + TestRequest req(123, true); + TestResponse resp = sender.send_request(req); + + if (resp.result != 123 || !resp.success) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Verify same request received repeatedly + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + for (int i = 0; i < 50; ++i) { + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, 123); + EXPECT_TRUE(req.flag); + + TestResponse resp(req.value, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Timing and Synchronization Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, SynchronizationBetweenProcesses) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender - delayed start + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + InterprocessCommunicationChannel sender(shmName); + + for (int i = 0; i < 10; ++i) { + TestRequest req(i, true); + sender.send_request(req); + } + exit(0); + } else { + // Parent process: Receiver - starts immediately + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + // Should wait for sender to be ready + for (int i = 0; i < 10; ++i) { + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, i); + + TestResponse resp(req.value, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Custom Shared Memory Size Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, CustomSharedMemorySize) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with larger shared memory + InterprocessCommunicationChannel sender(shmName); + + TestRequest req(55, false); + TestResponse resp = sender.send_request(req); + + exit((resp.result == 55) ? 0 : 1); + } else { + // Parent process: Receiver with larger shared memory + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + TestRequest req = receiver.receive_request(); + EXPECT_EQ(req.value, 55); + + TestResponse resp(req.value, req.flag); + receiver.send_response(resp); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// ===== Stop Token Tests ===== + +TEST_F(InterprocessCommunicationChannelTest, SenderCancellationViaStopToken) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender that gets cancelled while waiting for response + InterprocessCommunicationChannel sender(shmName); + + std::stop_source stop_src; + std::jthread canceller([&stop_src]() { + // Cancel after 100ms + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + stop_src.request_stop(); + }); + + TestRequest req(42, true); + auto start = std::chrono::steady_clock::now(); + TestResponse resp = sender.send_request(req, stop_src.get_token()); + auto end = std::chrono::steady_clock::now(); + + auto duration = std::chrono::duration_cast(end - start); + + // Should return default response quickly (within 200ms, accounting for scheduling) + // and not wait indefinitely for the receiver that never responds + exit((duration.count() < 200 && resp.result == 0 && !resp.success) ? 0 : 1); + } else { + // Parent process: Sender creates shared memory but receiver never responds + InterprocessCommunicationChannel dummy_sender(shmName); + + // Wait for child to complete + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, ReceiverCancellationViaStopToken) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver that gets cancelled while waiting for request + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + InterprocessCommunicationChannel receiver(shmName); + + std::stop_source stop_src; + std::jthread canceller([&stop_src]() { + // Cancel after 100ms + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + stop_src.request_stop(); + }); + + auto start = std::chrono::steady_clock::now(); + TestRequest req = receiver.receive_request(stop_src.get_token()); + auto end = std::chrono::steady_clock::now(); + + auto duration = std::chrono::duration_cast(end - start); + + // Should return default request quickly (within 200ms) + // and not wait indefinitely for a request that never comes + exit((duration.count() < 200 && req.value == 0 && !req.flag) ? 0 : 1); + } else { + // Parent process: Sender creates shared memory but never sends request + InterprocessCommunicationChannel sender(shmName); + + // Wait for child to complete + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, StopTokenDoesNotInterruptNormalOperation) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with stop token that is never triggered + InterprocessCommunicationChannel sender(shmName); + + std::stop_source stop_src; + + TestRequest req(99, true); + TestResponse resp = sender.send_request(req, stop_src.get_token()); + + // Should complete normally and receive proper response + exit((resp.result == 198 && resp.success) ? 0 : 1); + } else { + // Parent process: Receiver responds normally + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + std::stop_source stop_src; + TestRequest req = receiver.receive_request(stop_src.get_token()); + EXPECT_EQ(req.value, 99); + EXPECT_TRUE(req.flag); + + TestResponse resp(req.value * 2, req.flag); + receiver.send_response(resp); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, MultipleExchangesWithStopToken) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender performs multiple exchanges with stop token + InterprocessCommunicationChannel sender(shmName); + + std::stop_source stop_src; + + for (int i = 0; i < 50; ++i) { + TestRequest req(i, i % 2 == 0); + TestResponse resp = sender.send_request(req, stop_src.get_token()); + + if (resp.result != i * 2 || resp.success != (i % 2 == 0)) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver with stop token + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + std::stop_source stop_src; + + for (int i = 0; i < 50; ++i) { + TestRequest req = receiver.receive_request(stop_src.get_token()); + EXPECT_EQ(req.value, i); + + TestResponse resp(req.value * 2, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, SenderCancellationMidExchange) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender that gets cancelled after some exchanges + InterprocessCommunicationChannel sender(shmName); + + std::stop_source stop_src; + + // Perform a few successful exchanges + for (int i = 0; i < 5; ++i) { + TestRequest req(i, true); + TestResponse resp = sender.send_request(req, stop_src.get_token()); + + if (resp.result != i * 2) { + exit(1); + } + } + + // Now trigger cancellation for next exchange + std::jthread canceller([&stop_src]() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + stop_src.request_stop(); + }); + + // This should be cancelled (receiver won't respond in time) + TestRequest req(100, false); + auto start = std::chrono::steady_clock::now(); + TestResponse resp = sender.send_request(req, stop_src.get_token()); + auto end = std::chrono::steady_clock::now(); + + auto duration = std::chrono::duration_cast(end - start); + + // Should return default response due to cancellation + exit((duration.count() < 200 && resp.result == 0) ? 0 : 1); + } else { + // Parent process: Receiver responds to first 5 requests, then delays + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + std::stop_source stop_src; + + // Respond to first 5 requests normally + for (int i = 0; i < 5; ++i) { + TestRequest req = receiver.receive_request(stop_src.get_token()); + TestResponse resp(req.value * 2, req.flag); + receiver.send_response(resp); + } + + // Delay before processing the 6th request (which will be cancelled) + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + // Try to receive next request (might be cancelled) + TestRequest req = receiver.receive_request(stop_src.get_token()); + if (req.value == 100) { + TestResponse resp(req.value * 2, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, ReceiverCancellationMidExchange) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Receiver that gets cancelled after some exchanges + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + InterprocessCommunicationChannel receiver(shmName); + + std::stop_source stop_src; + + // Perform a few successful exchanges + for (int i = 0; i < 5; ++i) { + TestRequest req = receiver.receive_request(stop_src.get_token()); + + if (req.value != i) { + exit(1); + } + + TestResponse resp(req.value * 2, req.flag); + receiver.send_response(resp); + } + + // Now trigger cancellation for next receive + std::jthread canceller([&stop_src]() { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + stop_src.request_stop(); + }); + + // This should be cancelled (sender will delay) + auto start = std::chrono::steady_clock::now(); + TestRequest req = receiver.receive_request(stop_src.get_token()); + auto end = std::chrono::steady_clock::now(); + + auto duration = std::chrono::duration_cast(end - start); + + // Should return default request due to cancellation + exit((duration.count() < 200 && req.value == 0) ? 0 : 1); + } else { + // Parent process: Sender sends first 5 requests, then delays + InterprocessCommunicationChannel sender(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + std::stop_source stop_src; + + // Send first 5 requests normally + for (int i = 0; i < 5; ++i) { + TestRequest req(i, true); + TestResponse resp = sender.send_request(req, stop_src.get_token()); + + if (resp.result != i * 2) { + // Unexpected response + break; + } + } + + // Delay before sending the 6th request (receiver will be cancelled) + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, ImmediateCancellation) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with immediately stopped token + InterprocessCommunicationChannel sender(shmName); + + std::stop_source stop_src; + stop_src.request_stop(); // Stop immediately + + TestRequest req(42, true); + auto start = std::chrono::steady_clock::now(); + TestResponse resp = sender.send_request(req, stop_src.get_token()); + auto end = std::chrono::steady_clock::now(); + + auto duration = std::chrono::duration_cast(end - start); + + // Should return immediately with default response + exit((duration.count() < 50 && resp.result == 0 && !resp.success) ? 0 : 1); + } else { + // Parent process: Just creates sender + InterprocessCommunicationChannel dummy_sender(shmName); + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +TEST_F(InterprocessCommunicationChannelTest, StopTokenWithHighFrequencyExchanges) { + pid_t pid = fork(); + + if (pid == 0) { + // Child process: Sender with many rapid exchanges using stop token + InterprocessCommunicationChannel sender(shmName); + + std::stop_source stop_src; + + for (int i = 0; i < 100; ++i) { + TestRequest req(i % 10, i % 2 == 0); + TestResponse resp = sender.send_request(req, stop_src.get_token()); + + if (resp.result != (i % 10) * 3) { + exit(1); + } + } + exit(0); + } else { + // Parent process: Receiver with stop token + InterprocessCommunicationChannel receiver(shmName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + std::stop_source stop_src; + + for (int i = 0; i < 100; ++i) { + TestRequest req = receiver.receive_request(stop_src.get_token()); + + TestResponse resp(req.value * 3, req.flag); + receiver.send_response(resp); + } + + int status; + waitpid(pid, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } +} + +// Main function to run all tests +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 93907f1415..deb7726d12 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -803,26 +803,26 @@ def _test_depth( performance_degraded = self._check_performance(new_data, initial_fifo_depths) return not performance_degraded, False - def _find_valid_block_count( - self, target_blocks: int, bitwidth: int, lower_bound: int = 1 - ) -> tuple[int, int, int]: - """Find a valid block count and corresponding depth range. + def _get_valid_block_counts(self, min_blocks: int, max_blocks: int, bitwidth: int) -> list[int]: + """Get all valid BRAM block counts in the specified range. + + Some block counts are invalid for certain bitwidths due to quantization. + This method returns only the valid configurations. Args: - target_blocks: Desired number of BRAM blocks + min_blocks: Minimum block count (inclusive) + max_blocks: Maximum block count (inclusive) bitwidth: Data bitwidth - lower_bound: Minimum acceptable block count Returns: - Tuple of (valid_blocks, min_depth, max_depth) + Sorted list of valid block counts """ - blocks = target_blocks - while blocks >= lower_bound: - min_d, max_d = calculate_bram_depth_range(blocks, bitwidth) - if max_d > 0: - return blocks, min_d, max_d - blocks -= 1 - return 0, 0, 0 + valid_blocks = [] + for blocks in range(min_blocks, max_blocks + 1): + _, max_d = calculate_bram_depth_range(blocks, bitwidth) + if max_d > 0: # Valid configuration + valid_blocks.append(blocks) + return valid_blocks def _minimize_fifo_depth( self, @@ -856,26 +856,76 @@ def _minimize_fifo_depth( print(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") - # Try FIFO depth of 32 first (fits into bitwidth LUTs) + # If FIFO depth of 2 works, we dont need FIFOs at all, because AXI buffers some values success, timeout = self._test_depth( - 32, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + 2, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles ) + if success: + return 2 + + if original_size <= self.max_qsrl_depth: + upper_luts = calculate_srl16e_luts(original_size, bw) + # Smallest depth that is reasonable is 32 (Fits into bw LUTRAMs) + lower_luts = calculate_srl16e_luts(32, bw) + + # Binary search if there's room to search + if upper_luts > lower_luts: + best_working_depth = self._binary_search_srl_depth( + node_idx, + fifo_idx, + baseline_depths, + bw, + initial_fifo_depths, + sim, + sim_cycles, + lower_luts=lower_luts, + upper_luts=upper_luts, + ) + return best_working_depth + return original_size + # Try FIFO depth of 256 next (fits into LUTRAM) + success, timeout = self._test_depth( + self.max_qsrl_depth, + node_idx, + fifo_idx, + baseline_depths, + initial_fifo_depths, + sim, + sim_cycles, + ) if success: - # If FIFO depth of 2 works, we dont need FIFOs at all, because AXI buffers some values - success, timeout = self._test_depth( - 2, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles - ) - if success: - return 2 - return 32 + upper_luts = calculate_srl16e_luts(original_size, bw) + # Smallest depth that is reasonable is 32 (Fits into bw LUTRAMs) + lower_luts = calculate_srl16e_luts(32, bw) + + # Binary search if there's room to search + if upper_luts > lower_luts: + best_working_depth = self._binary_search_srl_depth( + node_idx, + fifo_idx, + baseline_depths, + bw, + initial_fifo_depths, + sim, + sim_cycles, + lower_luts=lower_luts, + upper_luts=upper_luts, + ) + return best_working_depth + return self.max_qsrl_depth + # We know 256 doesn't work, so we have to use BRAMs # Try one BRAM block less than current upper_blocks = calculate_bram_blocks(original_size, bw) - blocks, min_d, max_d = self._find_valid_block_count(upper_blocks - 1, bw) - - if max_d == 0: + # Get all valid block counts in the range + valid_blocks = self._get_valid_block_counts(1, upper_blocks - 1, bw) + if not valid_blocks: + # No valid configurations exist return original_size + # Test the maximum valid block count first (smallest depth) + max_valid_blocks = valid_blocks[-1] + _, max_d = calculate_bram_depth_range(max_valid_blocks, bw) success, timeout = self._test_depth( max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles @@ -886,9 +936,9 @@ def _minimize_fifo_depth( best_working_depth = max_d - # Binary search if there's room to search - if blocks > 2 and min_d - 1 > 32: - best_working_depth = self._binary_search_depth( + # Binary search if there's room to search and multiple valid configs + if len(valid_blocks) > 1: + best_working_depth = self._exponential_binary_search_depth( node_idx, fifo_idx, baseline_depths, @@ -896,32 +946,94 @@ def _minimize_fifo_depth( initial_fifo_depths, sim, sim_cycles, - lower_blocks=1, - upper_blocks=blocks, + valid_blocks=valid_blocks, ) - # If we are within reach of the max qsrl depth, - # test that as well, so that we can maybe move to LUTRAM - if ( - best_working_depth < self.max_qsrl_depth * 1.1 - and best_working_depth > self.max_qsrl_depth - ): - success, timeout = self._test_depth( - self.max_qsrl_depth, - node_idx, - fifo_idx, - baseline_depths, - initial_fifo_depths, - sim, - sim_cycles, + return best_working_depth + + def _exponential_binary_search_depth( + self, + node_idx: int, + fifo_idx: int, + baseline_depths: list, + bitwidth: int, + initial_fifo_depths: dict, + sim, + sim_cycles: float, + valid_blocks: list[int], + ) -> int: + """Perform exponential + binary search over valid block configurations. + + Uses exponential search to quickly find the range, then binary search within it. + This is more efficient when smaller block counts are more likely. + Only searches over pre-validated block counts. + + Args: + node_idx: Node index + fifo_idx: FIFO index within node + baseline_depths: Original baseline FIFO depths (unchanged during minimization) + bitwidth: Data bitwidth + initial_fifo_depths: Baseline performance data + sim: Simulation controller + sim_cycles: Maximum simulation cycles + valid_blocks: Sorted list of valid block counts to search over + + Returns: + Best working depth found + """ + if not valid_blocks: + raise FINNInternalError("valid_blocks list cannot be empty") + + # Start with the largest valid block count (known to work from caller) + _, max_d = calculate_bram_depth_range(valid_blocks[-1], bitwidth) + best_working_depth = max_d + + # Exponential search phase: find range where solution exists + # Check positions: 0, 1, 2, 4, 8, ... indices in valid_blocks list + lower_idx = 0 + upper_idx = len(valid_blocks) - 1 + exp_idx = 0 + last_failed_idx = -1 + + while exp_idx < upper_idx: + blocks = valid_blocks[exp_idx] + _, max_d = calculate_bram_depth_range(blocks, bitwidth) + + success, _ = self._test_depth( + max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles ) + if success: - best_working_depth = self.max_qsrl_depth - # TODO: If depth 256 works, try minimize in LUTRAM range + # Found a working depth, now binary search in [last_failed_idx+1, exp_idx] + best_working_depth = max_d + lower_idx = last_failed_idx + 1 + upper_idx = exp_idx + break + # This doesn't work, try exponentially larger index + last_failed_idx = exp_idx + exp_idx = min(exp_idx * 2 if exp_idx > 0 else 1, upper_idx) + + # Binary search phase: refine the range + while lower_idx < upper_idx: + mid_idx = (lower_idx + upper_idx) // 2 + blocks = valid_blocks[mid_idx] + _, max_d = calculate_bram_depth_range(blocks, bitwidth) + + success, _ = self._test_depth( + max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + + if success: + # This depth works, try smaller (lower indices) + best_working_depth = max_d + upper_idx = mid_idx + else: + # This depth doesn't work, need larger (higher indices) + lower_idx = mid_idx + 1 return best_working_depth - def _binary_search_depth( + def _binary_search_srl_depth( self, node_idx: int, fifo_idx: int, @@ -930,10 +1042,10 @@ def _binary_search_depth( initial_fifo_depths: dict, sim, sim_cycles: float, - lower_blocks: int, - upper_blocks: int, + lower_luts: int, + upper_luts: int, ) -> int: - """Perform binary search to find minimal working FIFO depth. + """Perform binary search to find minimal working FIFO depth in LUTRAM range. Args: node_idx: Node index @@ -943,32 +1055,30 @@ def _binary_search_depth( initial_fifo_depths: Baseline performance data sim: Simulation controller sim_cycles: Maximum simulation cycles - lower_blocks: Lower bound for block count - upper_blocks: Upper bound for block count (known to work) + lower_luts: Lower bound for LUT count + upper_luts: Upper bound for LUT count (known to work) Returns: Best working depth found """ - _, _, max_d = self._find_valid_block_count(upper_blocks, bitwidth) + _, max_d = calculate_srl16e_depth_range(upper_luts, bitwidth) best_working_depth = max_d - while lower_blocks < upper_blocks: - mid_blocks = (lower_blocks + upper_blocks) // 2 + while lower_luts < upper_luts: + mid_luts = (lower_luts + upper_luts) // 2 # Prevent infinite loop - if mid_blocks == upper_blocks: - mid_blocks = upper_blocks - 1 - if mid_blocks < lower_blocks: + if mid_luts == upper_luts: + mid_luts = upper_luts - 1 + if mid_luts < lower_luts: break - # Find valid depth for this block count - valid_blocks, _, max_d = self._find_valid_block_count( - mid_blocks, bitwidth, lower_blocks - ) + # Find valid depth for this LUT count + _, max_d = calculate_srl16e_depth_range(mid_luts, bitwidth) if max_d == 0: - # No valid configuration, try more blocks - lower_blocks = mid_blocks + 1 + # No valid configuration, try more LUTs + lower_luts = mid_luts + 1 continue success, _ = self._test_depth( @@ -978,10 +1088,10 @@ def _binary_search_depth( if success: # This depth works, try smaller best_working_depth = max_d - upper_blocks = valid_blocks + upper_luts = mid_luts else: # This depth doesn't work, need larger - lower_blocks = valid_blocks + 1 + lower_luts = mid_luts + 1 return best_working_depth @@ -996,17 +1106,14 @@ def needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: True if the FIFO can be minimized further, False otherwise. """ # Qsrl FIFO Formula: LUTs = ⌈depth/32⌉ x ⌈bitwidth/2⌉ - if ( - fifo_depth <= self.max_qsrl_depth - ): # TODO: Later this should not be needed. Binary search over LUTRAM sizes. - return False if fifo_depth <= 32: # FIFOs of depth <=32 fit into bitwidth/2 LUTs return False - # Return False if exactly 1 BRAM block is used and depth is sufficiently large - # that further optimization is unlikely to succeed + # Return False if exactly the minimum number of possible BRAM blocks is used for this + # bitwidth and depth is sufficiently large that further optimization is unlikely to succeed return not ( - calculate_bram_blocks(fifo_depth, bitwidth) == 1 - and fifo_depth > self.max_qsrl_depth * 1.1 + calculate_bram_blocks(fifo_depth, bitwidth) + <= self._get_valid_block_counts(1, bitwidth, bitwidth)[0] + and fifo_depth > math.floor(self.max_qsrl_depth * 1.1) ) @@ -1080,22 +1187,30 @@ def calculate_bram_depth_range(blocks: int, bitwidth: int) -> tuple[int, int]: # Try the depth > 512 case first (⌈depth/1024⌉ * ⌈bitwidth/18⌉) bitwidth_factor = math.ceil(bitwidth / 18) depth_blocks = math.ceil(blocks / bitwidth_factor) - if depth_blocks <= 1 and bitwidth <= 18: - return (1, 1024) - min_depth = max((depth_blocks - 1) * 1024 + 1, 513) # Must be > 512 - max_depth = depth_blocks * 1024 - # Check if this range is valid (entirely > 512) - if min_depth > 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: - return (min_depth, max_depth) + + # Check if blocks is achievable with this bitwidth factor + if blocks % bitwidth_factor != 0 or depth_blocks < 1: + # Try the depth ≤ 512 case instead + pass + else: + min_depth = max((depth_blocks - 1) * 1024 + 1, 513) # Must be > 512 + max_depth = depth_blocks * 1024 + # Check if this range is valid (entirely > 512) + if min_depth > 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: + return (min_depth, max_depth) # Try the depth ≤ 512 case (⌈depth/512⌉ * ⌈bitwidth/36⌉) bitwidth_factor = math.ceil(bitwidth / 36) depth_blocks = math.ceil(blocks / bitwidth_factor) - if depth_blocks <= 1 and bitwidth > 18: - return (1, 512) - min_depth = (depth_blocks - 1) * 512 + 1 + + # Check if blocks is achievable with this bitwidth factor + if blocks % bitwidth_factor != 0 or depth_blocks < 1: + return (0, 0) # Invalid block count for this bitwidth + + min_depth = (depth_blocks - 1) * 512 + 1 if depth_blocks > 1 else 1 max_depth = min(depth_blocks * 512, 512) # Must be ≤ 512 - # Check if this range is valid (entirely ≤ 512) + + # Verify the range is valid (entirely ≤ 512 and produces correct block count) if max_depth <= 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: return (min_depth, max_depth) diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index d69a5c2f27..4daa0f8366 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -248,7 +248,7 @@ def _cleanup_sockets(self) -> None: sock.close() socket_path_obj = Path(socket_path) if socket_path_obj.exists(): - socket_path_obj.unlink() + socket_path_obj.unlink(True) # Terminate processes and close file handles for proc, stdout_file, stderr_file in self.processes: diff --git a/tests/fpgadataflow/test_bram_block_search.py b/tests/fpgadataflow/test_bram_block_search.py new file mode 100644 index 0000000000..fb18206991 --- /dev/null +++ b/tests/fpgadataflow/test_bram_block_search.py @@ -0,0 +1,465 @@ +"""Test BRAM block calculations and search algorithms.""" +# ruff: noqa: ANN201, SLF001 + +import pytest + +import math + +from finn.transformation.fpgadataflow.simulation import ( + calculate_bram_blocks, + calculate_bram_depth_range, +) + + +class TestBRAMBlockCalculations: + """Test BRAM block calculation functions.""" + + def test_calculate_bram_blocks_bitwidth_1(self) -> None: + """Test BRAM block calculation for 1-bit data.""" + assert calculate_bram_blocks(1, 1) == 1 + assert calculate_bram_blocks(16384, 1) == 1 + assert calculate_bram_blocks(16385, 1) == 2 + assert calculate_bram_blocks(32768, 1) == 2 + + def test_calculate_bram_blocks_bitwidth_2(self) -> None: + """Test BRAM block calculation for 2-bit data.""" + assert calculate_bram_blocks(1, 2) == 1 + assert calculate_bram_blocks(8192, 2) == 1 + assert calculate_bram_blocks(8193, 2) == 2 + assert calculate_bram_blocks(16384, 2) == 2 + + def test_calculate_bram_blocks_bitwidth_4(self) -> None: + """Test BRAM block calculation for 4-bit data.""" + assert calculate_bram_blocks(1, 4) == 1 + assert calculate_bram_blocks(4096, 4) == 1 + assert calculate_bram_blocks(4097, 4) == 2 + assert calculate_bram_blocks(8192, 4) == 2 + + def test_calculate_bram_blocks_bitwidth_9(self) -> None: + """Test BRAM block calculation for 9-bit data.""" + assert calculate_bram_blocks(1, 9) == 1 + assert calculate_bram_blocks(2048, 9) == 1 + assert calculate_bram_blocks(2049, 9) == 2 + + def test_calculate_bram_blocks_bitwidth_18(self) -> None: + """Test BRAM block calculation for 18-bit data.""" + assert calculate_bram_blocks(1, 18) == 1 + assert calculate_bram_blocks(1024, 18) == 1 + assert calculate_bram_blocks(1025, 18) == 2 + + def test_calculate_bram_blocks_wide_bitwidth_deep(self) -> None: + """Test BRAM block calculation for wide bitwidth with depth > 512.""" + # bitwidth = 40, depth = 1024 > 512 + # Uses formula: ⌈1024/1024⌉ * ⌈40/18⌉ = 1 * 3 = 3 + assert calculate_bram_blocks(1024, 40) == 3 + + def test_calculate_bram_blocks_wide_bitwidth_shallow(self) -> None: + """Test BRAM block calculation for wide bitwidth with depth <= 512.""" + # bitwidth = 40, depth = 512 <= 512 + # Uses formula: ⌈512/512⌉ * ⌈40/36⌉ = 1 * 2 = 2 + assert calculate_bram_blocks(512, 40) == 2 + + +class TestBRAMDepthRange: + """Test BRAM depth range inversion function.""" + + def test_depth_range_bitwidth_1(self) -> None: + """Test depth range calculation for 1-bit data.""" + min_d, max_d = calculate_bram_depth_range(1, 1) + assert min_d == 1 + assert max_d == 16384 + assert calculate_bram_blocks(min_d, 1) == 1 + assert calculate_bram_blocks(max_d, 1) == 1 + + min_d, max_d = calculate_bram_depth_range(2, 1) + assert min_d == 16385 + assert max_d == 32768 + assert calculate_bram_blocks(min_d, 1) == 2 + assert calculate_bram_blocks(max_d, 1) == 2 + + def test_depth_range_bitwidth_4(self) -> None: + """Test depth range calculation for 4-bit data.""" + min_d, max_d = calculate_bram_depth_range(1, 4) + assert min_d == 1 + assert max_d == 4096 + assert calculate_bram_blocks(min_d, 4) == 1 + assert calculate_bram_blocks(max_d, 4) == 1 + + def test_depth_range_bitwidth_5_valid_blocks(self) -> None: + """Test block count validation for bitwidth=5.""" + # bitwidth=5 uses ⌈5/9⌉=1 bitwidth factor (falls in <=9 range) + # So all blocks should be valid + min_d, max_d = calculate_bram_depth_range(1, 5) + assert max_d > 0, "1 block should be valid for bitwidth=5" + assert calculate_bram_blocks(min_d, 5) == 1 + assert calculate_bram_blocks(max_d, 5) == 1 + + min_d, max_d = calculate_bram_depth_range(2, 5) + assert max_d > 0, "2 blocks should be valid for bitwidth=5" + assert calculate_bram_blocks(min_d, 5) == 2 + assert calculate_bram_blocks(max_d, 5) == 2 + + def test_depth_range_bitwidth_10_valid_blocks(self) -> None: + """Test block count validation for bitwidth=10.""" + # bitwidth=10 uses ⌈10/18⌉=1 bitwidth factor (falls in <=18 range) + min_d, max_d = calculate_bram_depth_range(1, 10) + assert max_d > 0 + assert calculate_bram_blocks(min_d, 10) == 1 + + min_d, max_d = calculate_bram_depth_range(2, 10) + assert max_d > 0 + assert calculate_bram_blocks(min_d, 10) == 2 + + def test_depth_range_wide_bitwidth(self) -> None: + """Test depth range for wide bitwidths > 18.""" + # bitwidth=40 has two modes depending on depth + min_d, max_d = calculate_bram_depth_range(2, 40) + # Should use depth ≤ 512 mode: ⌈depth/512⌉ * ⌈40/36⌉ + # 2 blocks / 2 = 1 depth_blocks → (1, 512) + if max_d > 0: + assert max_d <= 512 + assert calculate_bram_blocks(min_d, 40) == 2 + + def test_depth_range_consistency_all_bitwidths(self) -> None: + """Test that all valid ranges actually produce the correct block count.""" + for bitwidth in range(1, 8192): + for blocks in range(1, 1024): + min_d, max_d = calculate_bram_depth_range(blocks, bitwidth) + if max_d > 0: # Valid configuration + # Verify both endpoints produce correct block count + assert calculate_bram_blocks(min_d, bitwidth) == blocks, ( + f"Min depth {min_d} for {blocks} blocks, " + f"bitwidth {bitwidth} produces wrong count" + ) + assert calculate_bram_blocks(max_d, bitwidth) == blocks, ( + f"Max depth {max_d} for {blocks} blocks, " + f"bitwidth {bitwidth} produces wrong count" + ) + + # Verify just outside the range produces different counts + if min_d > 1: + assert calculate_bram_blocks(min_d - 1, bitwidth) < blocks + assert calculate_bram_blocks(max_d + 1, bitwidth) > blocks + + +class TestGetValidBlockCounts: + """Test the _get_valid_block_counts helper method.""" + + def test_all_valid_bitwidth_1(self) -> None: + """Test that all block counts are valid for bitwidth=1.""" + from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + + # Create dummy instance just to test the method + sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) + + valid_blocks = sim._get_valid_block_counts(1, 10, 1) + assert valid_blocks == list(range(1, 11)) + + def test_wide_bitwidth_filtering(self) -> None: + """Test that some block counts may be invalid for wide bitwidths.""" + from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + + sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) + + # For bitwidth > 18, some block counts may be invalid + valid_blocks = sim._get_valid_block_counts(1, 20, 40) + # Verify all returned blocks produce valid ranges + for b in valid_blocks: + _, max_d = calculate_bram_depth_range(b, 40) + assert max_d > 0, f"Block {b} should produce valid range" + + def test_range_respects_bounds(self) -> None: + """Test that valid blocks respect min/max bounds.""" + from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + + sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) + + valid_blocks = sim._get_valid_block_counts(5, 15, 1) + assert min(valid_blocks) >= 5 + assert max(valid_blocks) <= 15 + assert len(valid_blocks) == 11 + + def test_empty_when_no_valid_in_range(self) -> None: + """Test that empty list is returned when no valid configs exist in range.""" + from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + + sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) + + # Test a scenario where the range might have no valid blocks + # (this is rare but the method should handle it) + valid_blocks = sim._get_valid_block_counts(100, 99, 5) # Invalid range + assert valid_blocks == [] + + +class TestExponentialBinarySearchLogic: + """Test the exponential + binary search algorithm logic (without actual simulation).""" + + def test_exponential_indices_progression(self) -> None: + """Test that exponential search correctly progresses through indices.""" + # Simulate the exponential index progression + valid_blocks = list(range(1, 101)) # 100 valid blocks + + # Exponential progression should be: 0, 1, 2, 4, 8, 16, 32, 64... + exp_idx = 0 + indices_checked = [] + + while exp_idx < len(valid_blocks) - 1: + indices_checked.append(exp_idx) + exp_idx = min(exp_idx * 2 if exp_idx > 0 else 1, len(valid_blocks) - 1) + + assert indices_checked == [0, 1, 2, 4, 8, 16, 32, 64] + + def test_binary_search_reduces_range(self) -> None: + """Test that binary search correctly narrows the range.""" + lower_idx = 0 + upper_idx = 99 + + iterations = 0 + while lower_idx < upper_idx: + mid_idx = (lower_idx + upper_idx) // 2 + # Simulate "success" for indices < 50 + if mid_idx < 50: + upper_idx = mid_idx + else: + lower_idx = mid_idx + 1 + iterations += 1 + + # Prevent infinite loop in test + if iterations > 20: + break + + assert lower_idx == upper_idx + assert iterations <= 7 # log2(100) ≈ 6.6 + + +class TestSRL16ELUTCalculations: + """Test SRL16E LUT calculation functions.""" + + def test_calculate_srl16e_luts_basic(self): + """Test basic SRL16E LUT calculations.""" + from finn.transformation.fpgadataflow.simulation import calculate_srl16e_luts + + # Formula: LUTs = ⌈depth/32⌉ * ⌈bitwidth/2⌉ + # depth=32, bitwidth=2: ⌈32/32⌉ * ⌈2/2⌉ = 1 * 1 = 1 + assert calculate_srl16e_luts(32, 2) == 1 + + # depth=64, bitwidth=2: ⌈64/32⌉ * ⌈2/2⌉ = 2 * 1 = 2 + assert calculate_srl16e_luts(64, 2) == 2 + + # depth=32, bitwidth=4: ⌈32/32⌉ * ⌈4/2⌉ = 1 * 2 = 2 + assert calculate_srl16e_luts(32, 4) == 2 + + # depth=33, bitwidth=2: ⌈33/32⌉ * ⌈2/2⌉ = 2 * 1 = 2 + assert calculate_srl16e_luts(33, 2) == 2 + + def test_calculate_srl16e_luts_various_bitwidths(self): + """Test SRL16E LUT calculations for various bitwidths.""" + from finn.transformation.fpgadataflow.simulation import calculate_srl16e_luts + + # Bitwidth 1: ⌈1/2⌉ = 1 + assert calculate_srl16e_luts(32, 1) == 1 + assert calculate_srl16e_luts(64, 1) == 2 + + # Bitwidth 3: ⌈3/2⌉ = 2 + assert calculate_srl16e_luts(32, 3) == 2 + assert calculate_srl16e_luts(64, 3) == 4 + + # Bitwidth 8: ⌈8/2⌉ = 4 + assert calculate_srl16e_luts(32, 8) == 4 + assert calculate_srl16e_luts(64, 8) == 8 + + def test_calculate_srl16e_luts_small_depths(self): + """Test SRL16E LUT calculations for small depths.""" + from finn.transformation.fpgadataflow.simulation import calculate_srl16e_luts + + # Small depths still use at least 1 LUT per bitwidth factor + assert calculate_srl16e_luts(2, 2) == 1 + assert calculate_srl16e_luts(16, 2) == 1 + assert calculate_srl16e_luts(31, 2) == 1 + + +class TestSRL16EDepthRange: + """Test SRL16E depth range inversion function.""" + + def test_depth_range_basic(self): + """Test basic depth range calculation for SRL16E.""" + from finn.transformation.fpgadataflow.simulation import ( + calculate_srl16e_depth_range, + calculate_srl16e_luts, + ) + + # 1 LUT, bitwidth=2 + min_d, max_d = calculate_srl16e_depth_range(1, 2) + assert min_d == 2 + assert max_d == 32 + assert calculate_srl16e_luts(min_d, 2) == 1 + assert calculate_srl16e_luts(max_d, 2) == 1 + + def test_depth_range_bitwidth_1(self): + """Test depth range for 1-bit data.""" + from finn.transformation.fpgadataflow.simulation import ( + calculate_srl16e_depth_range, + calculate_srl16e_luts, + ) + + min_d, max_d = calculate_srl16e_depth_range(1, 1) + assert min_d == 2 + assert max_d == 32 + assert calculate_srl16e_luts(min_d, 1) == 1 + assert calculate_srl16e_luts(max_d, 1) == 1 + + min_d, max_d = calculate_srl16e_depth_range(2, 1) + assert min_d == 33 + assert max_d == 64 + assert calculate_srl16e_luts(min_d, 1) == 2 + assert calculate_srl16e_luts(max_d, 1) == 2 + + def test_depth_range_invalid_odd_luts(self): + """Test that odd LUT counts are invalid for certain bitwidths.""" + from finn.transformation.fpgadataflow.simulation import calculate_srl16e_depth_range + + # Bitwidth=4: ⌈4/2⌉ = 2, so only even LUT counts are valid + _, max_d = calculate_srl16e_depth_range(1, 4) + assert max_d == 0, "1 LUT should be invalid for bitwidth=4" + + _, max_d = calculate_srl16e_depth_range(2, 4) + assert max_d > 0, "2 LUTs should be valid for bitwidth=4" + + def test_depth_range_consistency(self): + """Test that all valid ranges produce the correct LUT count.""" + from finn.transformation.fpgadataflow.simulation import ( + calculate_srl16e_depth_range, + calculate_srl16e_luts, + ) + + for bitwidth in [1, 2, 3, 4, 8, 16]: + for luts in range(1, 20): + min_d, max_d = calculate_srl16e_depth_range(luts, bitwidth) + if max_d > 0: # Valid configuration + # Verify both endpoints produce correct LUT count + assert calculate_srl16e_luts(min_d, bitwidth) == luts, ( + f"Min depth {min_d} for {luts} LUTs, " + f"bitwidth {bitwidth} produces wrong count" + ) + assert calculate_srl16e_luts(max_d, bitwidth) == luts, ( + f"Max depth {max_d} for {luts} LUTs, " + f"bitwidth {bitwidth} produces wrong count" + ) + + # Verify just outside the range produces different counts + if min_d > 2: + assert calculate_srl16e_luts(min_d - 1, bitwidth) < luts + assert calculate_srl16e_luts(max_d + 1, bitwidth) > luts + + +class TestNeedsMinimization: + """Test the needs_minimization method.""" + + # TODO: Maybe remove this behavior + def test_small_depths_no_minimization(self): + """Test that small depths don't need minimization.""" + from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + + sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) + sim.max_qsrl_depth = 256 + + # Depths <= 32 don't need minimization (fit in bitwidth/2 LUTs) + assert not sim.needs_minimization(32, 8) + assert not sim.needs_minimization(16, 8) + assert not sim.needs_minimization(2, 8) + + # TODO: Maybe remove this behavior + def test_qsrl_range_no_minimization(self): + """Test that depths within QSRL range don't need minimization.""" + from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + + sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) + sim.max_qsrl_depth = 256 + + # Depths within max_qsrl_depth don't need minimization + assert not sim.needs_minimization(128, 8) + assert not sim.needs_minimization(256, 8) + + def test_large_depths_need_minimization(self): + """Test that large depths with multiple BRAM blocks need minimization.""" + from finn.transformation.fpgadataflow.simulation import ( + RunLayerParallelSimulation, + calculate_bram_blocks, + calculate_bram_depth_range, + ) + + sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) + sim.max_qsrl_depth = 256 + + # Test with specific known cases first + # bitwidth=8: 1 BRAM range is (1, 2048) + # Use depth > 2048 to get multiple blocks + depth = 5000 + bitwidth = 8 + blocks = calculate_bram_blocks(depth, bitwidth) + assert blocks > 1, f"depth={depth}, bitwidth={bitwidth} should use >1 BRAM" + assert sim.needs_minimization(depth, bitwidth) + + # bitwidth=18: 1 BRAM range is (1, 1024) + # Use depth > 1024 to get multiple blocks + depth = 3000 + bitwidth = 18 + blocks = calculate_bram_blocks(depth, bitwidth) + assert blocks > 1, f"depth={depth}, bitwidth={bitwidth} should use >1 BRAM" + assert sim.needs_minimization(depth, bitwidth) + + # Verify that depth with 1 BRAM doesn't need minimization + # when it's at minimum block count + depth = 1000 + bitwidth = 8 + blocks = calculate_bram_blocks(depth, bitwidth) + assert blocks == 1 + assert not sim.needs_minimization(depth, bitwidth) + + # Exhaustive test: check that depths with MORE than minimum BRAM blocks + # need minimization (unless very close to QSRL threshold) + for bw in range(1, 64): + # Find the minimum achievable block count for this bitwidth + min_blocks = None + max_d = 0 + test_blocks = 1 + while max_d == 0: + _, max_d = calculate_bram_depth_range(test_blocks, bw) + if max_d > 0: + min_blocks = test_blocks + break + test_blocks += 1 + + if min_blocks is None: + continue # Skip if no valid config found + + # Test depths that use more blocks than minimum + for depth in range(1, 8192): + blocks = calculate_bram_blocks(depth, bw) + + # Only expect minimization if blocks > minimum achievable + if blocks > min_blocks and depth > math.floor(sim.max_qsrl_depth * 1.1): + assert sim.needs_minimization(depth, bw), ( + f"depth={depth}, bw={bw}, blocks={blocks}, min_blocks={min_blocks} " + f"should need minimization" + ) + + def test_minimum_bram_edge_case(self): + """Test edge case at minimum BRAM blocks.""" + from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + + sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) + sim.max_qsrl_depth = 256 + + # A depth that's just slightly above max_qsrl_depth with minimum BRAM blocks + # The behavior depends on whether it's deemed too close to optimize + depth = 300 + bitwidth = 1 + + # Verify the method executes without error + result = sim.needs_minimization(depth, bitwidth) + assert isinstance(result, bool) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 41411669528046bea3bcfcb25a5a026837cd93fd Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 9 Jan 2026 15:00:58 +0100 Subject: [PATCH 037/170] Add FIFO simulation tests to CI --- .gitlab-ci.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 634f0c01dc..94720088c8 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -147,10 +147,17 @@ FINN Test Suite 2022.2: script: # Launch additional monitoring - $JOB_MONITORING_DIR/monitor.sh $JOB_MONITORING_DIR/$CI_PIPELINE_ID/$HOSTNAME.log & - # Launch FINN via test command, includes preparation of (cached) dependencies + # Run simulation tests - | source finn-plus-venv/bin/activate - finn test --variant $TEST_SUITE --dependency-path ./deps --build-path $FINN_BUILD_DIR --num-workers 1 --num-test-workers $PYTEST_PARALLEL + cd "$(python -m pip show finn_plus | grep Location | sed 's/Location: //g')/finn_xsi" + cmake -DENABLE_UNITTESTS=1 -B build -S . + cd build + make all_unittests + cd unittests + ctest --report-on-failure + # Launch FINN via test command, includes preparation of (cached) dependencies + - finn test --variant $TEST_SUITE --dependency-path ./deps --build-path $FINN_BUILD_DIR --num-workers 1 --num-test-workers $PYTEST_PARALLEL artifacts: name: "test_reports" when: always From 2b201bc1992de5da6a95dcee49ef88a588bc05ae Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 9 Jan 2026 22:47:30 +0100 Subject: [PATCH 038/170] Move FIFOs --- finn_xsi/finn_xsi/include/AXIS_Control.h | 15 +- .../finn_xsi/include/CommunicationChannel.hpp | 73 ++ finn_xsi/finn_xsi/include/FIFO.h | 12 +- .../include/InterSimulationInterface.hpp | 210 ---- ...erprocessCommunicationChannelInterface.hpp | 72 ++ finn_xsi/finn_xsi/include/Simulation.hpp | 108 +- finn_xsi/finn_xsi/src/AXIS_Control.cpp | 22 +- finn_xsi/finn_xsi/src/FIFO.cpp | 9 +- finn_xsi/finn_xsi/unittests/FIFO_test.cpp | 1049 +++++++++-------- .../finn_xsi/unittests/Integration_test.cpp | 192 +-- .../InterSimulationInterface_test.cpp | 614 ---------- .../transformation/fpgadataflow/simulation.py | 12 +- .../fpgadataflow/simulation_controller.py | 56 +- 13 files changed, 914 insertions(+), 1530 deletions(-) create mode 100644 finn_xsi/finn_xsi/include/CommunicationChannel.hpp delete mode 100644 finn_xsi/finn_xsi/include/InterSimulationInterface.hpp create mode 100644 finn_xsi/finn_xsi/include/InterprocessCommunicationChannelInterface.hpp delete mode 100644 finn_xsi/finn_xsi/unittests/InterSimulationInterface_test.cpp diff --git a/finn_xsi/finn_xsi/include/AXIS_Control.h b/finn_xsi/finn_xsi/include/AXIS_Control.h index d357b9303d..45cdb43336 100644 --- a/finn_xsi/finn_xsi/include/AXIS_Control.h +++ b/finn_xsi/finn_xsi/include/AXIS_Control.h @@ -1,10 +1,11 @@ #ifndef AXIS_CONTROL #define AXIS_CONTROL +#include +#include #include #include - -#include +#include // Fwd declarations namespace xsi { @@ -13,7 +14,7 @@ namespace xsi { } // namespace xsi class Clock; -class AXIS_Control { +class AXIS_Control : public CommunicationChannel { public: // Constructor/destructor AXIS_Control(xsi::Design& design, Clock& clock, size_t job_size, const std::string& prefix = "s_axis_"); @@ -26,10 +27,10 @@ class AXIS_Control { void inititialized_or_throw(); // Core functions - immediate writes - void valid(bool value = true); - bool isValid() const noexcept; - void ready(bool value = true); - bool isReady() const noexcept; + virtual void setInputValid(bool value = true, std::stop_token stoken = {}) override; + virtual bool getOutputValid(std::stop_token stoken = {}) noexcept override; + virtual void setOutputReady(bool value = true, std::stop_token stoken = {}) override; + virtual bool getInputReady(std::stop_token stoken = {}) noexcept override; // Deferred write functions std::reference_wrapper setValid(bool value = true); diff --git a/finn_xsi/finn_xsi/include/CommunicationChannel.hpp b/finn_xsi/finn_xsi/include/CommunicationChannel.hpp new file mode 100644 index 0000000000..871b909667 --- /dev/null +++ b/finn_xsi/finn_xsi/include/CommunicationChannel.hpp @@ -0,0 +1,73 @@ +#ifndef COMMUNICATIONCHANNEL +#define COMMUNICATIONCHANNEL + +#include +#include + +template +concept ChannelInterface = requires(T t, bool b, std::stop_token stoken) { + { t.getOutputValid(stoken) } -> std::same_as; + { t.setInputValid(b, stoken) } -> std::same_as; + { t.getInputReady(stoken) } -> std::same_as; + { t.setOutputReady(b, stoken) } -> std::same_as; +}; + +class CommunicationChannel { + // Function pointers for downstream object methods + bool (*downstreamGetInputReadyFn)(void*, std::stop_token) = nullptr; + void (*downstreamSetInputValidFn)(void*, bool, std::stop_token) = nullptr; + + void* downstreamObj = nullptr; + + protected: + // Derived classes call this to register their own methods + template + void registerSelfAs() { + // This is intentionally empty - we call methods directly on 'this' + // The template just ensures Derived implements ChannelInterface + } + + public: + template + void connectDownstream(Derived& downstreamPartner) { + this->downstreamObj = &downstreamPartner; + + // Store function pointers for calling the DOWNSTREAM object's methods + downstreamGetInputReadyFn = [](void* obj, std::stop_token stoken) -> bool { return static_cast(obj)->getInputReady(stoken); }; + downstreamSetInputValidFn = [](void* obj, bool v, std::stop_token stoken) { static_cast(obj)->setInputValid(v, stoken); }; + } + + // Mark as inline and noexcept for better optimization + inline void exchangeDataDownstream(std::stop_token stoken = {}) noexcept { + // Call methods on THIS object directly (non-virtual, resolved at compile time) + bool valid = this->getOutputValid(stoken); + // Call downstream object's methods via function pointers + downstreamSetInputValidFn(downstreamObj, valid, stoken); + bool ready = downstreamGetInputReadyFn(downstreamObj, stoken); + // Call method on THIS object directly + this->setOutputReady(ready, stoken); + } + + virtual bool getOutputValid([[maybe_unused]] std::stop_token stoken = {}) { return false; } + virtual void setInputValid([[maybe_unused]] bool v, [[maybe_unused]] std::stop_token stoken = {}) {} + virtual bool getInputReady([[maybe_unused]] std::stop_token stoken = {}) { return false; } + virtual void setOutputReady([[maybe_unused]] bool r, [[maybe_unused]] std::stop_token stoken = {}) {} + + virtual ~CommunicationChannel() = default; +}; + +// Example usage: +// class LayerA : public CommunicationChannel { +// public: +// bool getOutputValid(std::stop_token stoken = {}) { /* ... */ } +// void setInputValid(bool v, std::stop_token stoken = {}) { /* ... */ } +// bool getInputReady(std::stop_token stoken = {}) { /* ... */ } +// void setOutputReady(bool r, std::stop_token stoken = {}) { /* ... */ } +// }; +// +// LayerA a; +// LayerB b; +// a.connectDownstream(b); +// a.exchangeDataDownstream(); // or with stop_token: a.exchangeDataDownstream(stoken); + +#endif /* COMMUNICATIONCHANNEL */ diff --git a/finn_xsi/finn_xsi/include/FIFO.h b/finn_xsi/finn_xsi/include/FIFO.h index 531c0fb099..803e827fac 100644 --- a/finn_xsi/finn_xsi/include/FIFO.h +++ b/finn_xsi/finn_xsi/include/FIFO.h @@ -1,10 +1,12 @@ #ifndef FIFO_H #define FIFO_H +#include #include #include +#include -class FIFO { +class FIFO : public CommunicationChannel { uint64_t maxUtil = 0; uint64_t currentUtil = 0; uint64_t maxSize = 0; @@ -16,8 +18,8 @@ class FIFO { void update(bool incomingValid, bool incomingReady); void toggleClock(); - bool isInputReady() const; - bool isOutputValid() const; + virtual bool getInputReady(std::stop_token stoken = {}) noexcept override; + virtual bool getOutputValid(std::stop_token stoken = {}) noexcept override; bool isEmpty() const; void reset(uint64_t size = std::numeric_limits::max()); void setMaxSize(const uint64_t size); @@ -27,8 +29,8 @@ class FIFO { void increaseCounter(const uint64_t count); // NOTE: User needs to ensure proper ordering. No runtime enforcement of order. - void tryPush(bool incomingValid); - void tryPop(bool incomingReady); + virtual void setInputValid(bool incomingValid, std::stop_token stoken = {}) override; + virtual void setOutputReady(bool incomingReady, std::stop_token stoken = {}) override; uint64_t size() const; }; diff --git a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp b/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp deleted file mode 100644 index 08e0a350fa..0000000000 --- a/finn_xsi/finn_xsi/include/InterSimulationInterface.hpp +++ /dev/null @@ -1,210 +0,0 @@ -#ifndef INTERSIMULATIONINTERFACE -#define INTERSIMULATIONINTERFACE - -#include -#include -#include -#include - -#ifdef __cpp_lib_hardware_interference_size -constexpr std::size_t CACHE_LINE_SIZE = std::hardware_destructive_interference_size; -#else -constexpr std::size_t CACHE_LINE_SIZE = 64; -#endif - -namespace bip = boost::interprocess; - -template -class InterSimulationInterface { - private: - // ===== SHARED MEMORY STRUCTURE ===== - // This goes into shared memory and is accessible from both processes - struct alignas(CACHE_LINE_SIZE) SharedHaloExchange { - struct alignas(CACHE_LINE_SIZE) BufferSlot { - std::atomic value; - std::atomic ready; // Ready flag for Halo Exchange NOT Simulation - - // Must explicitly initialize atomics in shared memory - BufferSlot() : value(false), ready(false) {} - }; - - // Linearized buffers: [process_id * 2 + buffer_id] - BufferSlot buffers[4]; - alignas(CACHE_LINE_SIZE) std::atomic current_buffer; - alignas(CACHE_LINE_SIZE) std::atomic flip_barrier; - - SharedHaloExchange() : current_buffer(0), flip_barrier(0) { - // Verify atomics are lock-free (required for shared memory) - static_assert(std::atomic::is_always_lock_free, "std::atomic must be lock-free for inter-process use"); - static_assert(std::atomic::is_always_lock_free, "std::atomic must be lock-free for inter-process use"); - } - - static constexpr int idx(int proc_id, int buf_id) { return proc_id * 2 + buf_id; } - }; - - // ===== PROCESS-LOCAL STATE ===== - // This is NOT in shared memory - each process has its own copy - template - struct ProcessLocalState { - int expected_buf; - bool first_call; - constexpr static int process_id = IsReceiver ? 1 : 0; // 0 or 1 - ProcessLocalState() : expected_buf(0), first_call(true) {} - }; - - SharedHaloExchange* halo = nullptr; - std::atomic* refCount = nullptr; - const std::string sharedMemoryName; - bip::managed_shared_memory shmem; - // Create process-local state - ProcessLocalState local; - - public: - // Default constructor needed for std::array - InterSimulationInterface() : sharedMemoryName("") { - // Uninitialized - will be move-assigned later - } - - InterSimulationInterface(const std::string& shmName) : sharedMemoryName(shmName) { - if constexpr (Receiver) { - bip::shared_memory_object::remove(sharedMemoryName.c_str()); - shmem = bip::managed_shared_memory(bip::create_only, sharedMemoryName.c_str(), SharedMemorySize); - } else { - while (true) { - try { - shmem = bip::managed_shared_memory(bip::open_only, sharedMemoryName.c_str()); - break; - } catch (const bip::interprocess_exception& e) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - } - } - - // Construct or find the reference counter (separate from SharedData) - refCount = shmem.find_or_construct>("refCount")(0); - - // Increment reference count atomically - refCount->fetch_add(1, std::memory_order_acq_rel); - - // Construct the halo exchange object in shared memory - halo = shmem.find_or_construct("HaloExchange")(); - } - - // Delete copy operations - InterSimulationInterface(const InterSimulationInterface&) = delete; - InterSimulationInterface& operator=(const InterSimulationInterface&) = delete; - - // Move constructor - InterSimulationInterface(InterSimulationInterface&& other) noexcept - : halo(other.halo), refCount(other.refCount), sharedMemoryName(std::move(other.sharedMemoryName)), shmem(std::move(other.shmem)) { - // Mark other as moved-from - other.halo = nullptr; - other.refCount = nullptr; - } - - // Move assignment operator - InterSimulationInterface& operator=(InterSimulationInterface&& other) noexcept { - if (this != &other) { - halo = other.halo; - refCount = other.refCount; - // Note: managed_shared_memory has deleted assignment, use swap - shmem.swap(other.shmem); - const_cast(sharedMemoryName) = std::move(other.sharedMemoryName); - - // Mark other as moved-from - other.halo = nullptr; - other.refCount = nullptr; - } - return *this; - } - - ~InterSimulationInterface() { - // Skip cleanup if moved-from or default-constructed - if (!refCount || !halo) { - return; - } - - // Clear our local pointers before decrementing (safety) - halo = nullptr; - refCount = nullptr; - - // Get a raw pointer to refCount for the atomic operation - // (we need this because we just nulled our member pointer) - std::atomic* ref_ptr = shmem.find>("refCount").first; - if (!ref_ptr) { - return; // Already destroyed somehow - } - - // Decrement reference count atomically and get the value BEFORE decrement - int remainingRefs = ref_ptr->fetch_sub(1, std::memory_order_acq_rel) - 1; - - // If we're the last process, clean up the shared memory - if (remainingRefs == 0) { - // Destroy all objects first - shmem.destroy("HaloExchange"); - shmem.destroy>("refCount"); - - // Close our handle to the shared memory - // This doesn't delete it yet if other processes have it mapped - shmem = bip::managed_shared_memory(); - - // Now remove the shared memory segment completely - // This is safe even if other processes still have stale mappings - bip::shared_memory_object::remove(sharedMemoryName.c_str()); - } - } - - // ===== EXCHANGE FUNCTION ===== - // This function runs in each process - bool exchange(bool send_value, std::stop_token stoken = {}) { - constexpr int neighbor_id = 1 - this->local.process_id; - - // Wait for previous buffer flip (latency hiding) - if (!local.first_call) { - while (this->halo->current_buffer.load(std::memory_order_acquire) % 2 == local.expected_buf && !stoken.stop_requested()) { -#if defined(__x86_64__) || defined(_M_X64) - __builtin_ia32_pause(); -#endif - } - if (stoken.stop_requested()) { - return false; // Early termination - } - } - local.first_call = false; - - int buf_id = this->halo->current_buffer.load(std::memory_order_acquire) % 2; - - // Write our data - int my_idx = SharedHaloExchange::idx(this->local.process_id, buf_id); - this->halo->buffers[my_idx].value.store(send_value, std::memory_order_release); - this->halo->buffers[my_idx].ready.store(true, std::memory_order_release); - - // Wait for neighbor - int neighbor_idx = SharedHaloExchange::idx(neighbor_id, buf_id); - bool neighbor_ready = this->halo->buffers[neighbor_idx].ready.load(std::memory_order_acquire); - if (!neighbor_ready) { - while (!this->halo->buffers[neighbor_idx].ready.load(std::memory_order_acquire) && !stoken.stop_requested()) { -#if defined(__x86_64__) || defined(_M_X64) - __builtin_ia32_pause(); -#endif - } - if (stoken.stop_requested()) { - return false; // Early termination - } - } - - bool received = this->halo->buffers[neighbor_idx].value.load(std::memory_order_acquire); - - // Flip barrier - if (this->halo->flip_barrier.fetch_add(1, std::memory_order_acq_rel) == 1) { - // First process: flip the buffer - int old_buf = buf_id; - this->halo->buffers[SharedHaloExchange::idx(0, old_buf)].ready.store(false, std::memory_order_relaxed); - this->halo->buffers[SharedHaloExchange::idx(1, old_buf)].ready.store(false, std::memory_order_relaxed); - this->halo->current_buffer.fetch_add(1, std::memory_order_release); - this->halo->flip_barrier.store(0, std::memory_order_release); - } - - local.expected_buf = buf_id; - return received; - } -}; -#endif /* INTERSIMULATIONINTERFACE */ diff --git a/finn_xsi/finn_xsi/include/InterprocessCommunicationChannelInterface.hpp b/finn_xsi/finn_xsi/include/InterprocessCommunicationChannelInterface.hpp new file mode 100644 index 0000000000..1ae409c0af --- /dev/null +++ b/finn_xsi/finn_xsi/include/InterprocessCommunicationChannelInterface.hpp @@ -0,0 +1,72 @@ +#ifndef INTERPROCESSCOMMUNICATIONCHANNELINTERFACE +#define INTERPROCESSCOMMUNICATIONCHANNELINTERFACE + +#include +#include +#include + +template +class InterprocessCommunicationChannelInterface : public CommunicationChannel { + struct Forward { + bool valid; + }; + + struct Backward { + bool ready; + }; + + InterprocessCommunicationChannel channel; + Backward lastResponse; + + public: + // Default constructor + InterprocessCommunicationChannelInterface() = default; + + // Constructor with shared memory name + explicit InterprocessCommunicationChannelInterface(const std::string& shmName) : channel(shmName), lastResponse{false} {} + + // Delete copy operations + InterprocessCommunicationChannelInterface(const InterprocessCommunicationChannelInterface&) = delete; + InterprocessCommunicationChannelInterface& operator=(const InterprocessCommunicationChannelInterface&) = delete; + + // Move constructor + InterprocessCommunicationChannelInterface(InterprocessCommunicationChannelInterface&& other) noexcept = default; + + // Move assignment operator + InterprocessCommunicationChannelInterface& operator=(InterprocessCommunicationChannelInterface&& other) noexcept = default; + + virtual bool getInputReady([[maybe_unused]] std::stop_token stoken = {}) override { + if constexpr (!IsSender) { + throw std::runtime_error("getInputReady can only be called on sender instances."); + } else { + return lastResponse.ready; + } + + } + virtual bool getOutputValid(std::stop_token stoken = {}) override { + if constexpr (IsSender) { + throw std::runtime_error("getOutputValid can only be called on receiver instances."); + } else { + return channel.receive_request(stoken).valid; + } + } + + virtual void setInputValid(bool incomingValid, std::stop_token stoken = {}) override { + if constexpr (!IsSender) { + throw std::runtime_error("setInputValid can only be called on sender instances."); + } else { + lastResponse = channel.send_request(Forward{incomingValid}, stoken); + } + } + virtual void setOutputReady(bool incomingReady, [[maybe_unused]] std::stop_token stoken = {}) override { + if constexpr (IsSender) { + throw std::runtime_error("setOutputReady can only be called on receiver instances."); + } else { + channel.send_response(Backward{incomingReady}); + } + } + + virtual ~InterprocessCommunicationChannelInterface() = default; +}; + +#endif /* INTERPROCESSCOMMUNICATIONCHANNELINTERFACE */ diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index fd85766fce..dd4f4a710a 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -9,8 +9,7 @@ #include #include -//#include -#include +#include #include #include #include @@ -92,55 +91,49 @@ class Simulation { } }; -//Small struct used for exange. Will be changed later to more complex data structure. -struct CommData{ - bool data; -}; - // Communication Flow: // -// valid ┌──────────────────────────────────────┐ valid valid -// SHM ─────────> │ valid valid │ ─────────> FIFO ─────> SHM -// (pred) <───────── istream ─────────> xsim ─────────> ostream <───────── <───── (succ) -// ready │ <───────── <───────── │ ready ready -// │ ready ready │ -// │ (sim) │ -// └──────────────────────────────────────┘ +// valid valid ┌──────────────────────────────────────┐ valid +// SHM ─────────> FIFO ─────────> │ valid valid │ ─────────> SHM +// (pred) <───────── <───────── istream ─────────> xsim ─────────> ostream <───── (succ) +// ready ready │ <───────── <───────── │ ready +// │ ready ready │ +// │ (sim) │ +// └──────────────────────────────────────┘ template class SingleNodeSimulation : public Simulation { - using ConsumingInterface = InterprocessCommunicationChannel; - using ProducingInterface = InterprocessCommunicationChannel; + using ConsumingInterface = InterprocessCommunicationChannelInterface; + using ProducingInterface = InterprocessCommunicationChannelInterface; constexpr static bool FirstNode = NodeIndex == 0; constexpr static bool LastNode = NodeIndex == (TotalNodes - 1); std::array fromProducerInterface; std::array toConsumerInterface; std::size_t cyclesRun = 0; std::size_t completedMaps = 0; - std::array fifo; + std::array fifo; /// Communicate with predecessors and successors and update their values and our own [[gnu::hot, gnu::flatten, gnu::always_inline]] void communicate(std::stop_token stoken = {}) { if constexpr (!FirstNode) { for (std::size_t i = 0; i < IStreamsSize; ++i) { - // Interface SHM <-> sim - this->istreams[i].valid(fromProducerInterface[i].receive_request(stoken).data); - fromProducerInterface[i].send_response(CommData{this->istreams[i].isReady()}); + // Interface SHM <-> FIFO + fromProducerInterface[i].exchangeDataDownstream(stoken); + // FIFO <-> sim + this->fifo[i].exchangeDataDownstream(stoken); + // Toggle FIFO clock + this->fifo[i].toggleClock(); } } if constexpr (!LastNode) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - // Interface sim -valid-> FIFO <-> SHM - this->fifo[i].update(this->ostreams[i].isValid(), toConsumerInterface[i].send_request(CommData{this->fifo[i].isOutputValid()}, stoken).data); - // FIFO -ready-> sim - this->ostreams[i].ready(this->fifo[i].isInputReady()); - // Toggle FIFO clock - this->fifo[i].toggleClock(); + // Interface sim <-> SHM + this->ostreams[i].exchangeDataDownstream(stoken); + } } if constexpr (LastNode) { for (auto&& stream : this->ostreams) { - if (stream.isValid() && ++stream.job_txns == stream.job_size) { - static std::vector debug_intervals; + if (stream.getOutputValid() && ++stream.job_txns == stream.job_size) { // Track job completion and intervals std::size_t lastComplete = stream.lastComplete; stream.interval = cyclesRun - lastComplete; @@ -162,15 +155,25 @@ class SingleNodeSimulation : public Simulationistreams) { // Input into sim valid - s.valid(true); + s.setInputValid(true); } } else if constexpr (LastNode) { // Last Node; no successor for (auto&& s : this->ostreams) { // Output from sim ready - s.ready(true); + s.setOutputReady(true); } } } + void connectLayers(){ + for (std::size_t i = 0; i < IStreamsSize; ++i) { + fromProducerInterface[i].connectDownstream(fifo[i]); + fifo[i].connectDownstream(this->istreams[i]); + } + for (std::size_t i = 0; i < OStreamsSize; ++i) { + this->ostreams[i].connectDownstream(toConsumerInterface[i]); + } + } + [[gnu::hot, gnu::always_inline]] void runSingleCycle(std::stop_token stoken = {}) { ++cyclesRun; communicate(stoken); @@ -200,16 +203,12 @@ class SingleNodeSimulation : public Simulation::reset(); - if constexpr (!LastNode) { + if constexpr (!FirstNode) { // Reset FIFOs - for (std::size_t i = 0; i < OStreamsSize; ++i) { + for (std::size_t i = 0; i < IStreamsSize; ++i) { fifo[i].reset(); } } @@ -256,38 +256,38 @@ class SingleNodeSimulation : public Simulation= OStreamsSize) { - throw std::out_of_range(std::format("FIFO index {} out of range (max: {})", index, OStreamsSize - 1)); + if (index >= IStreamsSize) { + throw std::out_of_range(std::format("FIFO index {} out of range (max: {})", index, IStreamsSize - 1)); } fifo[index].setMaxSize(depth); } /// Set the max FIFO depth of all interfaces void setMaxFIFODepth(std::size_t depth) { - if constexpr (!LastNode) { + if constexpr (!FirstNode) { for (FIFO& f : fifo) { f.setMaxSize(depth); } } } - std::array getFIFODepth() const noexcept { - if constexpr (LastNode) { + std::array getFIFODepth() const noexcept { + if constexpr (FirstNode) { return {}; } - std::array utilizations{}; - for (std::size_t i = 0; i < OStreamsSize; ++i) { + std::array utilizations{}; + for (std::size_t i = 0; i < IStreamsSize; ++i) { utilizations[i] = fifo[i].getMaxSize(); } return utilizations; @@ -306,12 +306,12 @@ class SingleNodeSimulation : public Simulation getFIFOUtilization() const noexcept { - if constexpr (LastNode) { + std::array getFIFOUtilization() const noexcept { + if constexpr (FirstNode) { return {}; } - std::array utilizations{}; - for (std::size_t i = 0; i < OStreamsSize; ++i) { + std::array utilizations{}; + for (std::size_t i = 0; i < IStreamsSize; ++i) { utilizations[i] = fifo[i].getMaxUtil(); } return utilizations; diff --git a/finn_xsi/finn_xsi/src/AXIS_Control.cpp b/finn_xsi/finn_xsi/src/AXIS_Control.cpp index f6657afb15..758c4ca037 100644 --- a/finn_xsi/finn_xsi/src/AXIS_Control.cpp +++ b/finn_xsi/finn_xsi/src/AXIS_Control.cpp @@ -17,7 +17,15 @@ std::string sanitize_prefix(const std::string& prefix) { } AXIS_Control::AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, const std::string& prefix) - : job_size(job_sz), job_txns(0), total_txns(0), first_complete(0), name(sanitize_prefix(prefix)), design(&des), clk(&clock), port_vld(&des.getPort(name + "tvalid")), port_rdy(&des.getPort(name + "tready")) {} + : job_size(job_sz), + job_txns(0), + total_txns(0), + first_complete(0), + name(sanitize_prefix(prefix)), + design(&des), + clk(&clock), + port_vld(&des.getPort(name + "tvalid")), + port_rdy(&des.getPort(name + "tready")) {} void AXIS_Control::inititialized_or_throw() { if (!design || !clk || !port_rdy || !port_vld) { @@ -25,20 +33,20 @@ void AXIS_Control::inititialized_or_throw() { } } -void AXIS_Control::valid(bool value) { port_vld->set(static_cast(value)).write_back(); } +void AXIS_Control::setInputValid(bool value, [[maybe_unused]] std::stop_token stoken) { port_vld->set(static_cast(value)).write_back(); } -bool AXIS_Control::isValid() const noexcept { return port_vld->read().as_bool(); } +bool AXIS_Control::getOutputValid([[maybe_unused]] std::stop_token stoken) noexcept { return port_vld->read().as_bool(); } -void AXIS_Control::ready(bool value) { port_rdy->set(static_cast(value)).write_back(); } - -bool AXIS_Control::isReady() const noexcept { return port_rdy->read().as_bool(); } +void AXIS_Control::setOutputReady(bool value, [[maybe_unused]] std::stop_token stoken) { port_rdy->set(static_cast(value)).write_back(); } +bool AXIS_Control::getInputReady([[maybe_unused]] std::stop_token stoken) noexcept { return port_rdy->read().as_bool(); } // Deferred write functions std::reference_wrapper AXIS_Control::setValid(bool value) { return std::ref(port_vld->set(value ? 1 : 0)); } std::reference_wrapper AXIS_Control::setReady(bool value) { return std::ref(port_rdy->set(value ? 1 : 0)); } -S_AXIS_Control::S_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, size_t job_tks, const std::string& prefix) : AXIS_Control(des, clock, job_sz, prefix), job_ticks(job_tks), await_iter(job_tks) { +S_AXIS_Control::S_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, size_t job_tks, const std::string& prefix) + : AXIS_Control(des, clock, job_sz, prefix), job_ticks(job_tks), await_iter(job_tks) { if (job_sz < 1 || job_tks < 1) { throw std::invalid_argument("Job size and ticks must be greater than 0."); } diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp index 1b0547d3eb..abfe9f6a43 100644 --- a/finn_xsi/finn_xsi/src/FIFO.cpp +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -12,7 +12,6 @@ FIFO::~FIFO() {} /// - When non-empty: can consume, produce, or both /// With bounded maxSize, this models a real FIFO with backpressure. void FIFO::update(bool incomingValid, bool incomingReady) { - // When empty: only push if valid (ignoring ready) // When non-empty: push if valid AND space available uint64_t canPush = incomingValid & (currentUtil < maxSize); @@ -33,10 +32,10 @@ void FIFO::toggleClock() { } /// Return whether the FIFO can accept inputs (for the current utilization) -bool FIFO::isInputReady() const { return currentUtil < maxSize; } +bool FIFO::getInputReady([[maybe_unused]] std::stop_token stoken) noexcept { return currentUtil < maxSize; } /// Return whether the FIFO can output values (for the current utilization) -bool FIFO::isOutputValid() const { return currentUtil > 0; } +bool FIFO::getOutputValid([[maybe_unused]] std::stop_token stoken) noexcept { return currentUtil > 0; } /// Return whether the FIFO is empty (for the current utilization) bool FIFO::isEmpty() const { return currentUtil == 0; } @@ -69,7 +68,7 @@ void FIFO::increaseCounter(const uint64_t count) { /// If incomingValid is true and FIFO has space, increment nextUtil /// Matches Q_srl: when empty, always accepts input /// When using tryPush/tryPop separately, ALWAYS call tryPush BEFORE tryPop! -void FIFO::tryPush(bool incomingValid) { +void FIFO::setInputValid(bool incomingValid, [[maybe_unused]] std::stop_token stoken) { // When empty: accept input unconditionally (like Q_srl state_empty) // When non-empty: accept if space available nextUtil += incomingValid & (nextUtil < maxSize); @@ -80,7 +79,7 @@ void FIFO::tryPush(bool incomingValid) { /// When using tryPush/tryPop separately, ALWAYS call tryPush BEFORE tryPop! /// Note: If FIFO was empty and tryPush just added data, tryPop will NOT pop it /// (matching Q_srl where state_empty ignores output ready) -void FIFO::tryPop(bool incomingReady) { +void FIFO::setOutputReady(bool incomingReady, [[maybe_unused]] std::stop_token stoken) { // Check currentUtil (state at cycle start) not nextUtil (after tryPush) // This ensures empty->tryPush->tryPop results in size=1, matching Q_srl nextUtil -= incomingReady & (currentUtil > 0); diff --git a/finn_xsi/finn_xsi/unittests/FIFO_test.cpp b/finn_xsi/finn_xsi/unittests/FIFO_test.cpp index 6f728f5319..2fff65192e 100644 --- a/finn_xsi/finn_xsi/unittests/FIFO_test.cpp +++ b/finn_xsi/finn_xsi/unittests/FIFO_test.cpp @@ -1,826 +1,827 @@ #include "FIFO.h" + #include // Test fixture for FIFO tests class FIFOTest : public ::testing::Test { -protected: - void SetUp() override { - // Setup code if needed - } - - void TearDown() override { - // Cleanup code if needed - } + protected: + void SetUp() override { + // Setup code if needed + } + + void TearDown() override { + // Cleanup code if needed + } }; // ===== Constructor and Initialization Tests ===== TEST_F(FIFOTest, ConstructorWithDefaultSize) { - FIFO fifo; - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_TRUE(fifo.isInputReady()); - EXPECT_FALSE(fifo.isOutputValid()); + FIFO fifo; + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.getInputReady()); + EXPECT_FALSE(fifo.getOutputValid()); } TEST_F(FIFOTest, ConstructorWithSpecificSize) { - FIFO fifo(10); - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_TRUE(fifo.isInputReady()); - EXPECT_FALSE(fifo.isOutputValid()); - EXPECT_EQ(fifo.getSpaceLeft(), 10); + FIFO fifo(10); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.getInputReady()); + EXPECT_FALSE(fifo.getOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); } TEST_F(FIFOTest, ConstructorWithZeroSize) { - FIFO fifo(0); - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_FALSE(fifo.isInputReady()); - EXPECT_FALSE(fifo.isOutputValid()); - EXPECT_EQ(fifo.getSpaceLeft(), 0); + FIFO fifo(0); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_FALSE(fifo.getInputReady()); + EXPECT_FALSE(fifo.getOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 0); } // ===== Reset Tests ===== TEST_F(FIFOTest, ResetClearsState) { - FIFO fifo(10); - fifo.update(true, false); // Add one element - fifo.toggleClock(); - EXPECT_FALSE(fifo.isEmpty()); + FIFO fifo(10); + fifo.update(true, false); // Add one element + fifo.toggleClock(); + EXPECT_FALSE(fifo.isEmpty()); - fifo.reset(10); - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_EQ(fifo.getSpaceLeft(), 10); + fifo.reset(10); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); } TEST_F(FIFOTest, ResetChangesSize) { - FIFO fifo(10); - fifo.reset(20); - EXPECT_EQ(fifo.getSpaceLeft(), 20); + FIFO fifo(10); + fifo.reset(20); + EXPECT_EQ(fifo.getSpaceLeft(), 20); } TEST_F(FIFOTest, SetMaxSize) { - FIFO fifo(10); - fifo.setMaxSize(15); - EXPECT_EQ(fifo.getSpaceLeft(), 15); + FIFO fifo(10); + fifo.setMaxSize(15); + EXPECT_EQ(fifo.getSpaceLeft(), 15); } // ===== Basic Update and Toggle Tests ===== TEST_F(FIFOTest, PushOneElement) { - FIFO fifo(10); - fifo.update(true, false); // Push (valid=true, ready=false) - fifo.toggleClock(); + FIFO fifo(10); + fifo.update(true, false); // Push (valid=true, ready=false) + fifo.toggleClock(); - EXPECT_FALSE(fifo.isEmpty()); - EXPECT_TRUE(fifo.isOutputValid()); - EXPECT_TRUE(fifo.isInputReady()); - EXPECT_EQ(fifo.getSpaceLeft(), 9); + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.getOutputValid()); + EXPECT_TRUE(fifo.getInputReady()); + EXPECT_EQ(fifo.getSpaceLeft(), 9); } TEST_F(FIFOTest, PopOneElement) { - FIFO fifo(10); - // First push an element - fifo.update(true, false); - fifo.toggleClock(); + FIFO fifo(10); + // First push an element + fifo.update(true, false); + fifo.toggleClock(); - // Then pop it - fifo.update(false, true); // Pop (valid=false, ready=true) - fifo.toggleClock(); + // Then pop it + fifo.update(false, true); // Pop (valid=false, ready=true) + fifo.toggleClock(); - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_FALSE(fifo.isOutputValid()); - EXPECT_EQ(fifo.getSpaceLeft(), 10); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_FALSE(fifo.getOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); } TEST_F(FIFOTest, PushAndPopSimultaneously) { - FIFO fifo(10); - // First push an element - fifo.update(true, false); - fifo.toggleClock(); + FIFO fifo(10); + // First push an element + fifo.update(true, false); + fifo.toggleClock(); - // Now push and pop simultaneously (FIFO size should stay the same) - fifo.update(true, true); - fifo.toggleClock(); + // Now push and pop simultaneously (FIFO size should stay the same) + fifo.update(true, true); + fifo.toggleClock(); - EXPECT_FALSE(fifo.isEmpty()); - EXPECT_TRUE(fifo.isOutputValid()); - EXPECT_EQ(fifo.getSpaceLeft(), 9); + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.getOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 9); } // ===== Boundary Condition Tests ===== TEST_F(FIFOTest, FillToCapacity) { - FIFO fifo(3); + FIFO fifo(3); - for (int i = 0; i < 3; ++i) { - fifo.update(true, false); - fifo.toggleClock(); - } + for (int i = 0; i < 3; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } - EXPECT_FALSE(fifo.isEmpty()); - EXPECT_TRUE(fifo.isOutputValid()); - EXPECT_FALSE(fifo.isInputReady()); - EXPECT_EQ(fifo.getSpaceLeft(), 0); + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.getOutputValid()); + EXPECT_FALSE(fifo.getInputReady()); + EXPECT_EQ(fifo.getSpaceLeft(), 0); } TEST_F(FIFOTest, CannotPushWhenFull) { - FIFO fifo(2); + FIFO fifo(2); - // Fill the FIFO - fifo.update(true, false); - fifo.toggleClock(); - fifo.update(true, false); - fifo.toggleClock(); + // Fill the FIFO + fifo.update(true, false); + fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); - EXPECT_FALSE(fifo.isInputReady()); + EXPECT_FALSE(fifo.getInputReady()); - // Try to push when full (should have no effect) - fifo.update(true, false); - fifo.toggleClock(); + // Try to push when full (should have no effect) + fifo.update(true, false); + fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 0); + EXPECT_EQ(fifo.getSpaceLeft(), 0); } TEST_F(FIFOTest, CanPushAndPullWhenFull) { - FIFO fifo(2); + FIFO fifo(2); - // Fill the FIFO - fifo.update(true, false); - fifo.toggleClock(); - fifo.update(true, false); - fifo.toggleClock(); + // Fill the FIFO + fifo.update(true, false); + fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); - EXPECT_FALSE(fifo.isInputReady()); + EXPECT_FALSE(fifo.getInputReady()); - // Try to push and pull when full (should have no effect) - fifo.update(true, true); - fifo.toggleClock(); + // Try to push and pull when full (should have no effect) + fifo.update(true, true); + fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 1); + EXPECT_EQ(fifo.getSpaceLeft(), 1); - fifo.reset(2); + fifo.reset(2); - // Fill the FIFO - fifo.update(true, false); - fifo.toggleClock(); - fifo.update(true, false); - fifo.toggleClock(); + // Fill the FIFO + fifo.update(true, false); + fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); - EXPECT_FALSE(fifo.isInputReady()); + EXPECT_FALSE(fifo.getInputReady()); - // Try to push and pull when full (should have no effect) - fifo.update(false, true); - fifo.toggleClock(); + // Try to push and pull when full (should have no effect) + fifo.update(false, true); + fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 1); + EXPECT_EQ(fifo.getSpaceLeft(), 1); } TEST_F(FIFOTest, CannotPopWhenEmpty) { - FIFO fifo(10); + FIFO fifo(10); - EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isEmpty()); - // Try to pop when empty (should have no effect) - fifo.update(false, true); - fifo.toggleClock(); + // Try to pop when empty (should have no effect) + fifo.update(false, true); + fifo.toggleClock(); - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_EQ(fifo.getSpaceLeft(), 10); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); } TEST_F(FIFOTest, CanPushAndPopWhenEmpty) { - FIFO fifo(10); + FIFO fifo(10); - EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isEmpty()); - // Try to pop when empty (should have no effect) - fifo.update(true, true); - fifo.toggleClock(); + // Try to pop when empty (should have no effect) + fifo.update(true, true); + fifo.toggleClock(); - EXPECT_FALSE(fifo.isEmpty()); - EXPECT_TRUE(fifo.isOutputValid()); - EXPECT_EQ(fifo.getSpaceLeft(), 9); + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.getOutputValid()); + EXPECT_EQ(fifo.getSpaceLeft(), 9); } TEST_F(FIFOTest, PopWhenFullMakesSpaceAvailable) { - FIFO fifo(2); + FIFO fifo(2); - // Fill the FIFO - fifo.update(true, false); - fifo.toggleClock(); - fifo.update(true, false); - fifo.toggleClock(); + // Fill the FIFO + fifo.update(true, false); + fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); - EXPECT_FALSE(fifo.isInputReady()); + EXPECT_FALSE(fifo.getInputReady()); - // Pop one element - fifo.update(false, true); - fifo.toggleClock(); + // Pop one element + fifo.update(false, true); + fifo.toggleClock(); - EXPECT_TRUE(fifo.isInputReady()); - EXPECT_EQ(fifo.getSpaceLeft(), 1); + EXPECT_TRUE(fifo.getInputReady()); + EXPECT_EQ(fifo.getSpaceLeft(), 1); } // ===== Sequential Operation Tests ===== TEST_F(FIFOTest, SequentialPushAndPop) { - FIFO fifo(5); + FIFO fifo(5); - // Push 3 elements - for (int i = 0; i < 3; ++i) { - fifo.update(true, false); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.getSpaceLeft(), 2); + // Push 3 elements + for (int i = 0; i < 3; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.getSpaceLeft(), 2); + + // Pop 2 elements + for (int i = 0; i < 2; ++i) { + fifo.update(false, true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.getSpaceLeft(), 4); - // Pop 2 elements - for (int i = 0; i < 2; ++i) { + // Pop 1 more fifo.update(false, true); fifo.toggleClock(); - } - EXPECT_EQ(fifo.getSpaceLeft(), 4); - - // Pop 1 more - fifo.update(false, true); - fifo.toggleClock(); - EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isEmpty()); } TEST_F(FIFOTest, AlternatingPushPop) { - FIFO fifo(10); - - for (int i = 0; i < 5; ++i) { - // Push - fifo.update(true, false); - fifo.toggleClock(); - EXPECT_FALSE(fifo.isEmpty()); - - // Pop - fifo.update(false, true); - fifo.toggleClock(); - EXPECT_TRUE(fifo.isEmpty()); - } + FIFO fifo(10); + + for (int i = 0; i < 5; ++i) { + // Push + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_FALSE(fifo.isEmpty()); + + // Pop + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_TRUE(fifo.isEmpty()); + } } TEST_F(FIFOTest, StreamingOperation) { - FIFO fifo(10); - - // Push one element first - fifo.update(true, false); - fifo.toggleClock(); + FIFO fifo(10); - // Now stream: push and pop simultaneously for multiple cycles - for (int i = 0; i < 100; ++i) { - fifo.update(true, true); + // Push one element first + fifo.update(true, false); fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 9); // Size should remain constant - } + + // Now stream: push and pop simultaneously for multiple cycles + for (int i = 0; i < 100; ++i) { + fifo.update(true, true); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 9); // Size should remain constant + } } // ===== State Query Tests ===== TEST_F(FIFOTest, IsEmptyCorrectly) { - FIFO fifo(5); - EXPECT_TRUE(fifo.isEmpty()); + FIFO fifo(5); + EXPECT_TRUE(fifo.isEmpty()); - fifo.update(true, false); - fifo.toggleClock(); - EXPECT_FALSE(fifo.isEmpty()); + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_FALSE(fifo.isEmpty()); - fifo.update(false, true); - fifo.toggleClock(); - EXPECT_TRUE(fifo.isEmpty()); + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_TRUE(fifo.isEmpty()); } TEST_F(FIFOTest, IsInputReadyCorrectly) { - FIFO fifo(2); - EXPECT_TRUE(fifo.isInputReady()); + FIFO fifo(2); + EXPECT_TRUE(fifo.getInputReady()); - fifo.update(true, false); - fifo.toggleClock(); - EXPECT_TRUE(fifo.isInputReady()); + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_TRUE(fifo.getInputReady()); - fifo.update(true, false); - fifo.toggleClock(); - EXPECT_FALSE(fifo.isInputReady()); + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_FALSE(fifo.getInputReady()); } TEST_F(FIFOTest, IsOutputValidCorrectly) { - FIFO fifo(5); - EXPECT_FALSE(fifo.isOutputValid()); + FIFO fifo(5); + EXPECT_FALSE(fifo.getOutputValid()); - fifo.update(true, false); - fifo.toggleClock(); - EXPECT_TRUE(fifo.isOutputValid()); + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_TRUE(fifo.getOutputValid()); - fifo.update(false, true); - fifo.toggleClock(); - EXPECT_FALSE(fifo.isOutputValid()); + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_FALSE(fifo.getOutputValid()); } TEST_F(FIFOTest, GetSpaceLeftCorrectly) { - FIFO fifo(10); - EXPECT_EQ(fifo.getSpaceLeft(), 10); + FIFO fifo(10); + EXPECT_EQ(fifo.getSpaceLeft(), 10); - for (int i = 0; i < 3; ++i) { - fifo.update(true, false); - fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 10 - i - 1); - } + for (int i = 0; i < 3; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 10 - i - 1); + } - fifo.update(false, true); - fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 8); + fifo.update(false, true); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 8); } // ===== Edge Case Tests ===== TEST_F(FIFOTest, NoUpdateBeforeToggle) { - FIFO fifo(10); - fifo.toggleClock(); // Toggle without update + FIFO fifo(10); + fifo.toggleClock(); // Toggle without update - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_EQ(fifo.getSpaceLeft(), 10); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.getSpaceLeft(), 10); } TEST_F(FIFOTest, LargeCapacity) { - FIFO fifo(1000000); - EXPECT_EQ(fifo.getSpaceLeft(), 1000000); + FIFO fifo(1000000); + EXPECT_EQ(fifo.getSpaceLeft(), 1000000); - for (int i = 0; i < 100; ++i) { - fifo.update(true, false); - fifo.toggleClock(); - } + for (int i = 0; i < 100; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } - EXPECT_EQ(fifo.getSpaceLeft(), 999900); + EXPECT_EQ(fifo.getSpaceLeft(), 999900); } // ===== IncreaseCounter Tests ===== TEST_F(FIFOTest, IncreaseCounterBasic) { - FIFO fifo(100); + FIFO fifo(100); - fifo.update(true, false); - fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 99); + fifo.update(true, false); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 99); - fifo.increaseCounter(5); - fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 94); + fifo.increaseCounter(5); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 94); } TEST_F(FIFOTest, IncreaseCounterOnEmptyFIFO) { - FIFO fifo(100); + FIFO fifo(100); - fifo.increaseCounter(10); - fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 90); + fifo.increaseCounter(10); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 90); } TEST_F(FIFOTest, IncreaseCounterZero) { - FIFO fifo(100); + FIFO fifo(100); - fifo.update(true, false); - fifo.toggleClock(); + fifo.update(true, false); + fifo.toggleClock(); - fifo.increaseCounter(0); - fifo.toggleClock(); - EXPECT_EQ(fifo.getSpaceLeft(), 99); + fifo.increaseCounter(0); + fifo.toggleClock(); + EXPECT_EQ(fifo.getSpaceLeft(), 99); } // ===== Complex Scenarios ===== TEST_F(FIFOTest, BurstTrafficPattern) { - FIFO fifo(20); + FIFO fifo(20); - // Burst of 10 pushes - for (int i = 0; i < 10; ++i) { - fifo.update(true, false); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.getSpaceLeft(), 10); + // Burst of 10 pushes + for (int i = 0; i < 10; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.getSpaceLeft(), 10); - // Burst of 10 pops - for (int i = 0; i < 10; ++i) { - fifo.update(false, true); - fifo.toggleClock(); - } - EXPECT_TRUE(fifo.isEmpty()); + // Burst of 10 pops + for (int i = 0; i < 10; ++i) { + fifo.update(false, true); + fifo.toggleClock(); + } + EXPECT_TRUE(fifo.isEmpty()); } TEST_F(FIFOTest, StressTestManyOperations) { - FIFO fifo(100); + FIFO fifo(100); - // Perform 1000 operations - for (int i = 0; i < 500; ++i) { - fifo.update(true, false); - fifo.toggleClock(); - } + // Perform 1000 operations + for (int i = 0; i < 500; ++i) { + fifo.update(true, false); + fifo.toggleClock(); + } - for (int i = 0; i < 500; ++i) { - fifo.update(false, true); - fifo.toggleClock(); - } + for (int i = 0; i < 500; ++i) { + fifo.update(false, true); + fifo.toggleClock(); + } - EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isEmpty()); } // ===== Multiple FIFO Instances ===== TEST_F(FIFOTest, MultipleFIFOsIndependent) { - FIFO fifo1(10); - FIFO fifo2(20); + FIFO fifo1(10); + FIFO fifo2(20); - fifo1.update(true, false); - fifo1.toggleClock(); + fifo1.update(true, false); + fifo1.toggleClock(); - EXPECT_EQ(fifo1.getSpaceLeft(), 9); - EXPECT_EQ(fifo2.getSpaceLeft(), 20); + EXPECT_EQ(fifo1.getSpaceLeft(), 9); + EXPECT_EQ(fifo2.getSpaceLeft(), 20); - fifo2.update(true, false); - fifo2.update(true, false); - fifo2.toggleClock(); - fifo2.toggleClock(); + fifo2.update(true, false); + fifo2.update(true, false); + fifo2.toggleClock(); + fifo2.toggleClock(); - // fifo2 should have 2 elements (last update takes effect) - EXPECT_EQ(fifo1.getSpaceLeft(), 9); - EXPECT_TRUE(fifo2.getSpaceLeft() < 20); + // fifo2 should have 2 elements (last update takes effect) + EXPECT_EQ(fifo1.getSpaceLeft(), 9); + EXPECT_TRUE(fifo2.getSpaceLeft() < 20); } // ===== Individual Method Tests ===== TEST_F(FIFOTest, TryPushBasic) { - FIFO fifo(10); - EXPECT_EQ(fifo.size(), 0); + FIFO fifo(10); + EXPECT_EQ(fifo.size(), 0); - fifo.tryPush(true); - fifo.toggleClock(); + fifo.setInputValid(true); + fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 1); - EXPECT_FALSE(fifo.isEmpty()); - EXPECT_TRUE(fifo.isOutputValid()); + EXPECT_EQ(fifo.size(), 1); + EXPECT_FALSE(fifo.isEmpty()); + EXPECT_TRUE(fifo.getOutputValid()); } TEST_F(FIFOTest, TryPushFalseDoesNothing) { - FIFO fifo(10); + FIFO fifo(10); - fifo.tryPush(false); - fifo.toggleClock(); + fifo.setInputValid(false); + fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 0); - EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.size(), 0); + EXPECT_TRUE(fifo.isEmpty()); } TEST_F(FIFOTest, TryPushMultiple) { - FIFO fifo(10); + FIFO fifo(10); - for (int i = 0; i < 5; ++i) { - fifo.tryPush(true); - fifo.toggleClock(); - } + for (int i = 0; i < 5; ++i) { + fifo.setInputValid(true); + fifo.toggleClock(); + } - EXPECT_EQ(fifo.size(), 5); - EXPECT_EQ(fifo.getSpaceLeft(), 5); + EXPECT_EQ(fifo.size(), 5); + EXPECT_EQ(fifo.getSpaceLeft(), 5); } TEST_F(FIFOTest, TryPushWhenFull) { - FIFO fifo(3); + FIFO fifo(3); - // Fill the FIFO - for (int i = 0; i < 3; ++i) { - fifo.tryPush(true); - fifo.toggleClock(); - } + // Fill the FIFO + for (int i = 0; i < 3; ++i) { + fifo.setInputValid(true); + fifo.toggleClock(); + } - EXPECT_EQ(fifo.size(), 3); - EXPECT_FALSE(fifo.isInputReady()); + EXPECT_EQ(fifo.size(), 3); + EXPECT_FALSE(fifo.getInputReady()); - // Try to push when full (should have no effect) - fifo.tryPush(true); - fifo.toggleClock(); + // Try to push when full (should have no effect) + fifo.setInputValid(true); + fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 3); + EXPECT_EQ(fifo.size(), 3); } TEST_F(FIFOTest, TryPopBasic) { - FIFO fifo(10); + FIFO fifo(10); - // First push an element - fifo.tryPush(true); - fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 1); + // First push an element + fifo.setInputValid(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); - // Then pop it - fifo.tryPop(true); - fifo.toggleClock(); + // Then pop it + fifo.setOutputReady(true); + fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 0); - EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.size(), 0); + EXPECT_TRUE(fifo.isEmpty()); } TEST_F(FIFOTest, TryPopFalseDoesNothing) { - FIFO fifo(10); + FIFO fifo(10); - fifo.tryPush(true); - fifo.toggleClock(); + fifo.setInputValid(true); + fifo.toggleClock(); - fifo.tryPop(false); - fifo.toggleClock(); + fifo.setOutputReady(false); + fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 1); + EXPECT_EQ(fifo.size(), 1); } TEST_F(FIFOTest, TryPopWhenEmpty) { - FIFO fifo(10); + FIFO fifo(10); - EXPECT_TRUE(fifo.isEmpty()); + EXPECT_TRUE(fifo.isEmpty()); - // Try to pop when empty (should have no effect) - fifo.tryPop(true); - fifo.toggleClock(); + // Try to pop when empty (should have no effect) + fifo.setOutputReady(true); + fifo.toggleClock(); - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_EQ(fifo.size(), 0); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_EQ(fifo.size(), 0); } TEST_F(FIFOTest, TryPushAndTryPopSameCycle) { - FIFO fifo(10); + FIFO fifo(10); - // Push first element - fifo.tryPush(true); - fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 1); + // Push first element + fifo.setInputValid(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); - // Push and pop in same cycle (order: push then pop) - fifo.tryPush(true); - fifo.tryPop(true); - fifo.toggleClock(); + // Push and pop in same cycle (order: push then pop) + fifo.setInputValid(true); + fifo.setOutputReady(true); + fifo.toggleClock(); - // Should still have 1 element (pushed 1, popped 1) - EXPECT_EQ(fifo.size(), 1); + // Should still have 1 element (pushed 1, popped 1) + EXPECT_EQ(fifo.size(), 1); - // Push and pop in same cycle (order: push then pop) - fifo.tryPop(true); - fifo.tryPush(true); - fifo.toggleClock(); + // Push and pop in same cycle (order: push then pop) + fifo.setOutputReady(true); + fifo.setInputValid(true); + fifo.toggleClock(); - // Should still have 1 element (pushed 1, popped 1) - EXPECT_EQ(fifo.size(), 1); + // Should still have 1 element (pushed 1, popped 1) + EXPECT_EQ(fifo.size(), 1); } TEST_F(FIFOTest, TryPushAndTryPopSameCycleEmptyFIFO) { - FIFO fifo(10); + FIFO fifo(10); - // Push and pop in same cycle (order: push then pop) - fifo.tryPush(true); - fifo.tryPop(true); - fifo.toggleClock(); + // Push and pop in same cycle (order: push then pop) + fifo.setInputValid(true); + fifo.setOutputReady(true); + fifo.toggleClock(); - // Should still have 1 element (pushed 1, popped 0, because was empty) - EXPECT_EQ(fifo.size(), 1); + // Should still have 1 element (pushed 1, popped 0, because was empty) + EXPECT_EQ(fifo.size(), 1); - fifo.reset(10); + fifo.reset(10); // Push and pop in same cycle (order: push then pop) - fifo.tryPush(true); - fifo.tryPop(false); - fifo.toggleClock(); + fifo.setInputValid(true); + fifo.setOutputReady(false); + fifo.toggleClock(); - // Should still have 0 element (pushed 1, popped 0) - EXPECT_EQ(fifo.size(), 1); + // Should still have 0 element (pushed 1, popped 0) + EXPECT_EQ(fifo.size(), 1); } TEST_F(FIFOTest, TryPushAndTryPopSameCycleFullFIFO) { - FIFO fifo(1); + FIFO fifo(1); - // Push first element - fifo.tryPush(true); - fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 1); - EXPECT_FALSE(fifo.isInputReady()); + // Push first element + fifo.setInputValid(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); + EXPECT_FALSE(fifo.getInputReady()); - // Push and pop in same cycle (order: push then pop) - fifo.tryPush(true); - fifo.tryPop(true); - fifo.toggleClock(); + // Push and pop in same cycle (order: push then pop) + fifo.setInputValid(true); + fifo.setOutputReady(true); + fifo.toggleClock(); - // Should still have 1 element (pushed 1, popped 1) - EXPECT_EQ(fifo.size(), 0); + // Should still have 1 element (pushed 1, popped 1) + EXPECT_EQ(fifo.size(), 0); - fifo.reset(1); + fifo.reset(1); - // Push first element - fifo.tryPush(true); - fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 1); - EXPECT_FALSE(fifo.isInputReady()); + // Push first element + fifo.setInputValid(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); + EXPECT_FALSE(fifo.getInputReady()); - // Push and pop in same cycle (order: push then pop) - fifo.tryPush(false); - fifo.tryPop(true); - fifo.toggleClock(); + // Push and pop in same cycle (order: push then pop) + fifo.setInputValid(false); + fifo.setOutputReady(true); + fifo.toggleClock(); - // Should still have 1 element (pushed 1, popped 1) - EXPECT_EQ(fifo.size(), 0); + // Should still have 1 element (pushed 1, popped 1) + EXPECT_EQ(fifo.size(), 0); } TEST_F(FIFOTest, TryPushAndTryPopSequence) { - FIFO fifo(10); + FIFO fifo(10); - // Push 3 - for (int i = 0; i < 3; ++i) { - fifo.tryPush(true); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.size(), 3); + // Push 3 + for (int i = 0; i < 3; ++i) { + fifo.setInputValid(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 3); - // Pop 2 - for (int i = 0; i < 2; ++i) { - fifo.tryPop(true); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.size(), 1); + // Pop 2 + for (int i = 0; i < 2; ++i) { + fifo.setOutputReady(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 1); - // Push 1 more - fifo.tryPush(true); - fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 2); + // Push 1 more + fifo.setInputValid(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 2); } TEST_F(FIFOTest, TryPushAndTryPopStreaming) { - FIFO fifo(10); + FIFO fifo(10); - // Initialize with one element - fifo.tryPush(true); - fifo.toggleClock(); - - // Stream: push and pop simultaneously for many cycles - for (int i = 0; i < 100; ++i) { - fifo.tryPush(true); - fifo.tryPop(true); + // Initialize with one element + fifo.setInputValid(true); fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 1); // Size should remain constant - } + + // Stream: push and pop simultaneously for many cycles + for (int i = 0; i < 100; ++i) { + fifo.setInputValid(true); + fifo.setOutputReady(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); // Size should remain constant + } } TEST_F(FIFOTest, TryPushAlternatingValid) { - FIFO fifo(10); + FIFO fifo(10); - for (int i = 0; i < 10; ++i) { - fifo.tryPush(i % 2 == 0); // Push only on even iterations - fifo.toggleClock(); - } + for (int i = 0; i < 10; ++i) { + fifo.setInputValid(i % 2 == 0); // Push only on even iterations + fifo.toggleClock(); + } - EXPECT_EQ(fifo.size(), 5); // Should have 5 elements + EXPECT_EQ(fifo.size(), 5); // Should have 5 elements } TEST_F(FIFOTest, TryPopAlternatingReady) { - FIFO fifo(10); + FIFO fifo(10); - // Fill with 6 elements - for (int i = 0; i < 6; ++i) { - fifo.tryPush(true); - fifo.toggleClock(); - } + // Fill with 6 elements + for (int i = 0; i < 6; ++i) { + fifo.setInputValid(true); + fifo.toggleClock(); + } - // Pop alternating - for (int i = 0; i < 10; ++i) { - fifo.tryPop(i % 2 == 0); // Pop only on even iterations - fifo.toggleClock(); - } + // Pop alternating + for (int i = 0; i < 10; ++i) { + fifo.setOutputReady(i % 2 == 0); // Pop only on even iterations + fifo.toggleClock(); + } - EXPECT_EQ(fifo.size(), 1); // 6 - 5 pops = 1 + EXPECT_EQ(fifo.size(), 1); // 6 - 5 pops = 1 } TEST_F(FIFOTest, SizeMethodCorrectness) { - FIFO fifo(20); + FIFO fifo(20); - EXPECT_EQ(fifo.size(), 0); + EXPECT_EQ(fifo.size(), 0); - for (int i = 1; i <= 10; ++i) { - fifo.tryPush(true); - fifo.toggleClock(); - EXPECT_EQ(fifo.size(), i); - } + for (int i = 1; i <= 10; ++i) { + fifo.setInputValid(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), i); + } - for (int i = 9; i >= 0; --i) { - fifo.tryPop(true); - fifo.toggleClock(); - EXPECT_EQ(fifo.size(), i); - } + for (int i = 9; i >= 0; --i) { + fifo.setOutputReady(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), i); + } } TEST_F(FIFOTest, TryMethodsVsUpdateEquivalence) { - FIFO fifo1(10); - FIFO fifo2(10); - - // Use update() on fifo1 - fifo1.update(true, false); // Push - fifo1.toggleClock(); - fifo1.update(true, false); // Push - fifo1.toggleClock(); - fifo1.update(false, true); // Pop - fifo1.toggleClock(); - - // Use tryPush/tryPop on fifo2 - fifo2.tryPush(true); - fifo2.toggleClock(); - fifo2.tryPush(true); - fifo2.toggleClock(); - fifo2.tryPop(true); - fifo2.toggleClock(); - - // Should have same result - EXPECT_EQ(fifo1.size(), fifo2.size()); - EXPECT_EQ(fifo1.isEmpty(), fifo2.isEmpty()); - EXPECT_EQ(fifo1.isOutputValid(), fifo2.isOutputValid()); + FIFO fifo1(10); + FIFO fifo2(10); + + // Use update() on fifo1 + fifo1.update(true, false); // Push + fifo1.toggleClock(); + fifo1.update(true, false); // Push + fifo1.toggleClock(); + fifo1.update(false, true); // Pop + fifo1.toggleClock(); + + // Use tryPush/tryPop on fifo2 + fifo2.setInputValid(true); + fifo2.toggleClock(); + fifo2.setInputValid(true); + fifo2.toggleClock(); + fifo2.setOutputReady(true); + fifo2.toggleClock(); + + // Should have same result + EXPECT_EQ(fifo1.size(), fifo2.size()); + EXPECT_EQ(fifo1.isEmpty(), fifo2.isEmpty()); + EXPECT_EQ(fifo1.getOutputValid(), fifo2.getOutputValid()); } TEST_F(FIFOTest, TryMethodsBurstPattern) { - FIFO fifo(50); + FIFO fifo(50); - // Burst of pushes - for (int i = 0; i < 30; ++i) { - fifo.tryPush(true); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.size(), 30); + // Burst of pushes + for (int i = 0; i < 30; ++i) { + fifo.setInputValid(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 30); - // Burst of pops - for (int i = 0; i < 20; ++i) { - fifo.tryPop(true); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.size(), 10); + // Burst of pops + for (int i = 0; i < 20; ++i) { + fifo.setOutputReady(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 10); - // Mixed burst - for (int i = 0; i < 15; ++i) { - fifo.tryPush(true); - fifo.tryPop(true); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.size(), 10); // Should remain constant + // Mixed burst + for (int i = 0; i < 15; ++i) { + fifo.setInputValid(true); + fifo.setOutputReady(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 10); // Should remain constant } TEST_F(FIFOTest, TryMethodsStressTest) { - FIFO fifo(1000); - - // Complex pattern - for (int i = 0; i < 500; ++i) { - fifo.tryPush(i % 3 != 0); // Push 2 out of 3 times - if (i > 100) { - fifo.tryPop(i % 2 == 0); // Pop every other time after 100 + FIFO fifo(1000); + + // Complex pattern + for (int i = 0; i < 500; ++i) { + fifo.setInputValid(i % 3 != 0); // Push 2 out of 3 times + if (i > 100) { + fifo.setOutputReady(i % 2 == 0); // Pop every other time after 100 + } + fifo.toggleClock(); } - fifo.toggleClock(); - } - // Verify FIFO is in valid state - EXPECT_LE(fifo.size(), 1000); - EXPECT_EQ(fifo.size() == 0, fifo.isEmpty()); - EXPECT_EQ(fifo.size() > 0, fifo.isOutputValid()); + // Verify FIFO is in valid state + EXPECT_LE(fifo.size(), 1000); + EXPECT_EQ(fifo.size() == 0, fifo.isEmpty()); + EXPECT_EQ(fifo.size() > 0, fifo.getOutputValid()); } TEST_F(FIFOTest, TryMethodsEdgeCaseFullToEmpty) { - FIFO fifo(5); + FIFO fifo(5); - // Fill completely - for (int i = 0; i < 5; ++i) { - fifo.tryPush(true); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.size(), 5); - EXPECT_FALSE(fifo.isInputReady()); + // Fill completely + for (int i = 0; i < 5; ++i) { + fifo.setInputValid(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 5); + EXPECT_FALSE(fifo.getInputReady()); - // Empty completely - for (int i = 0; i < 5; ++i) { - fifo.tryPop(true); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.size(), 0); - EXPECT_TRUE(fifo.isEmpty()); - EXPECT_FALSE(fifo.isOutputValid()); + // Empty completely + for (int i = 0; i < 5; ++i) { + fifo.setOutputReady(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 0); + EXPECT_TRUE(fifo.isEmpty()); + EXPECT_FALSE(fifo.getOutputValid()); } TEST_F(FIFOTest, TryMethodsWithReset) { - FIFO fifo(10); + FIFO fifo(10); - // Add some elements - for (int i = 0; i < 5; ++i) { - fifo.tryPush(true); - fifo.toggleClock(); - } - EXPECT_EQ(fifo.size(), 5); + // Add some elements + for (int i = 0; i < 5; ++i) { + fifo.setInputValid(true); + fifo.toggleClock(); + } + EXPECT_EQ(fifo.size(), 5); - // Reset - fifo.reset(10); - EXPECT_EQ(fifo.size(), 0); + // Reset + fifo.reset(10); + EXPECT_EQ(fifo.size(), 0); - // Should work normally after reset - fifo.tryPush(true); - fifo.toggleClock(); - EXPECT_EQ(fifo.size(), 1); + // Should work normally after reset + fifo.setInputValid(true); + fifo.toggleClock(); + EXPECT_EQ(fifo.size(), 1); } // Main function to run all tests -int main(int argc, char **argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/finn_xsi/finn_xsi/unittests/Integration_test.cpp b/finn_xsi/finn_xsi/unittests/Integration_test.cpp index 90425bdc85..2edb9d38da 100644 --- a/finn_xsi/finn_xsi/unittests/Integration_test.cpp +++ b/finn_xsi/finn_xsi/unittests/Integration_test.cpp @@ -5,7 +5,7 @@ #include #include "FIFO.h" -#include "InterSimulationInterface.hpp" +#include "InterprocessCommunicationChannel.hpp" // Test fixture for integration tests class IntegrationTest : public ::testing::Test { @@ -46,7 +46,7 @@ class SimDummy { // ===== Basic Integration Tests ===== TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { - // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Test: FIFO feeds data to InterprocessCommunicationChannel sender/receiver pair // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); @@ -55,16 +55,17 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { - InterSimulationInterface receiver(shmName); + InterprocessCommunicationChannel receiver(shmName); FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(false); - bool readySignal = outputFifo.isInputReady(); - bool validSignal = receiver.exchange(readySignal); + bool readySignal = outputFifo.getInputReady(); + bool validSignal = receiver.receive_request(); if (validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS @@ -72,10 +73,10 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { if (outputFifo.getSpaceLeft() != 15) { exit(3); } - if (outputFifo.isInputReady() != true) { + if (outputFifo.getInputReady() != true) { exit(4); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); @@ -86,10 +87,10 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { // Parent process: Sender { - InterSimulationInterface sender(shmName); + InterprocessCommunicationChannel sender(shmName); bool validSignal = false; - bool incomingReady = sender.exchange(validSignal); + bool incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==false for cycle 1 } // Destructor called here @@ -101,7 +102,7 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidFalse) { } TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { - // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Test: FIFO feeds data to InterprocessCommunicationChannel sender/receiver pair // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); @@ -110,16 +111,17 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { - InterSimulationInterface receiver(shmName); + InterprocessCommunicationChannel receiver(shmName); FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(true); - bool readySignal = outputFifo.isInputReady(); - bool validSignal = receiver.exchange(readySignal); + bool readySignal = outputFifo.getInputReady(); + bool validSignal = receiver.receive_request(); if (validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS @@ -127,10 +129,10 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { if (outputFifo.getSpaceLeft() != 15) { exit(3); } - if (outputFifo.isInputReady() != true) { + if (outputFifo.getInputReady() != true) { exit(4); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); @@ -141,10 +143,10 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { // Parent process: Sender { - InterSimulationInterface sender(shmName); + InterprocessCommunicationChannel sender(shmName); bool validSignal = false; - bool incomingReady = sender.exchange(validSignal); + bool incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); } // Destructor called here @@ -156,7 +158,7 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidFalse) { } TEST_F(IntegrationTest, OneCycleReadyFalseValidTrue) { - // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Test: FIFO feeds data to InterprocessCommunicationChannel sender/receiver pair // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); @@ -165,17 +167,18 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidTrue) { // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { - InterSimulationInterface receiver(shmName); + InterprocessCommunicationChannel receiver(shmName); FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(false); - bool readySignal = outputFifo.isInputReady(); - bool validSignal = receiver.exchange(readySignal); + bool readySignal = outputFifo.getInputReady(); + bool validSignal = receiver.receive_request(); if (!validSignal) { // It is correct that valid is true here, because we only have a single cycle and the sender input is set to valid in cycle 0. Therefore, we should // receive a valid in cycle 0. exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS @@ -183,13 +186,13 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidTrue) { if (outputFifo.getSpaceLeft() != 14) { exit(3); } - if (outputFifo.isInputReady() != true) { + if (outputFifo.getInputReady() != true) { exit(4); } - if (!outputFifo.isOutputValid()) { + if (!outputFifo.getOutputValid()) { exit(5); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (!simDummy.isOutputValid()) { exit(1); @@ -200,10 +203,10 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidTrue) { // Parent process: Sender { - InterSimulationInterface sender(shmName); + InterprocessCommunicationChannel sender(shmName); bool validSignal = true; - bool incomingReady = sender.exchange(validSignal); + bool incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 } // Destructor called here @@ -215,7 +218,7 @@ TEST_F(IntegrationTest, OneCycleReadyFalseValidTrue) { } TEST_F(IntegrationTest, OneCycleReadyTrueValidTrue) { - // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Test: FIFO feeds data to InterprocessCommunicationChannel sender/receiver pair // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); @@ -224,17 +227,18 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidTrue) { // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { - InterSimulationInterface receiver(shmName); + InterprocessCommunicationChannel receiver(shmName); FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(true); - bool readySignal = outputFifo.isInputReady(); - bool validSignal = receiver.exchange(readySignal); + bool readySignal = outputFifo.getInputReady(); + bool validSignal = receiver.receive_request(); if (!validSignal) { // It is correct that valid is true here, because we only have a single cycle and the sender input is set to valid in cycle 0. Therefore, we should // receive a valid in cycle 0. exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS @@ -242,13 +246,13 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidTrue) { if (outputFifo.getSpaceLeft() != 14) { exit(3); } - if (outputFifo.isInputReady() != true) { + if (outputFifo.getInputReady() != true) { exit(4); } - if (!outputFifo.isOutputValid()) { + if (!outputFifo.getOutputValid()) { exit(5); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (!simDummy.isOutputValid()) { exit(1); @@ -259,10 +263,10 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidTrue) { // Parent process: Sender { - InterSimulationInterface sender(shmName); + InterprocessCommunicationChannel sender(shmName); bool validSignal = true; - bool incomingReady = sender.exchange(validSignal); + bool incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); } // Destructor called here @@ -276,7 +280,7 @@ TEST_F(IntegrationTest, OneCycleReadyTrueValidTrue) { // ===== Multicycle Integration Tests ===== TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { - // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Test: FIFO feeds data to InterprocessCommunicationChannel sender/receiver pair // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); @@ -285,16 +289,17 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { - InterSimulationInterface receiver(shmName); + InterprocessCommunicationChannel receiver(shmName); FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(false); - bool readySignal = outputFifo.isInputReady(); - bool validSignal = receiver.exchange(readySignal); + bool readySignal = outputFifo.getInputReady(); + bool validSignal = receiver.receive_request(); if (validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS @@ -302,18 +307,19 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { if (outputFifo.getSpaceLeft() != 15) { exit(3); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); } simDummy.setNextReady(false); - readySignal = outputFifo.isInputReady(); // Should be true - validSignal = receiver.exchange(readySignal); + readySignal = outputFifo.getInputReady(); // Should be true + validSignal = receiver.receive_request(); if (validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS @@ -321,7 +327,7 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { if (outputFifo.getSpaceLeft() != 15) { exit(3); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); @@ -333,12 +339,12 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { // Parent process: Sender { - InterSimulationInterface sender(shmName); + InterprocessCommunicationChannel sender(shmName); bool validSignal = false; - bool incomingReady = sender.exchange(validSignal); + bool incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - incomingReady = sender.exchange(validSignal); + incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 2 } // Destructor called here @@ -350,7 +356,7 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidFalse) { } TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { - // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Test: FIFO feeds data to InterprocessCommunicationChannel sender/receiver pair // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); @@ -359,16 +365,17 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { - InterSimulationInterface receiver(shmName); + InterprocessCommunicationChannel receiver(shmName); FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(true); - bool readySignal = outputFifo.isInputReady(); - bool validSignal = receiver.exchange(readySignal); + bool readySignal = outputFifo.getInputReady(); + bool validSignal = receiver.receive_request(); if (validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS @@ -376,18 +383,19 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { if (outputFifo.getSpaceLeft() != 15) { exit(3); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); } simDummy.setNextReady(true); - readySignal = outputFifo.isInputReady(); // Should be true now - validSignal = receiver.exchange(readySignal); + readySignal = outputFifo.getInputReady(); // Should be true now + validSignal = receiver.receive_request(); if (validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS @@ -395,7 +403,7 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { if (outputFifo.getSpaceLeft() != 15) { exit(3); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (simDummy.isOutputValid()) { exit(1); @@ -407,12 +415,12 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { // Parent process: Sender { - InterSimulationInterface sender(shmName); + InterprocessCommunicationChannel sender(shmName); bool validSignal = false; - bool incomingReady = sender.exchange(validSignal); + bool incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - incomingReady = sender.exchange(validSignal); + incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 2 } // Destructor called here @@ -424,7 +432,7 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidFalse) { } TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { - // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Test: FIFO feeds data to InterprocessCommunicationChannel sender/receiver pair // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); @@ -433,16 +441,17 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { - InterSimulationInterface receiver(shmName); + InterprocessCommunicationChannel receiver(shmName); FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(false); - bool readySignal = outputFifo.isInputReady(); - bool validSignal = receiver.exchange(readySignal); + bool readySignal = outputFifo.getInputReady(); + bool validSignal = receiver.receive_request(); if (!validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS @@ -450,21 +459,22 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { if (outputFifo.getSpaceLeft() != 14) { exit(3); } - if (!outputFifo.isOutputValid()) { + if (!outputFifo.getOutputValid()) { exit(4); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (!simDummy.isOutputValid()) { exit(1); } simDummy.setNextReady(false); - readySignal = outputFifo.isInputReady(); // Should be true now (FIFO not full) - validSignal = receiver.exchange(readySignal); + readySignal = outputFifo.getInputReady(); // Should be true now (FIFO not full) + validSignal = receiver.receive_request(); if (!validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS @@ -472,10 +482,10 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { if (outputFifo.getSpaceLeft() != 13) { exit(3); } - if (!outputFifo.isOutputValid()) { + if (!outputFifo.getOutputValid()) { exit(4); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (!simDummy.isOutputValid()) { exit(1); @@ -487,12 +497,12 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { // Parent process: Sender { - InterSimulationInterface sender(shmName); + InterprocessCommunicationChannel sender(shmName); bool validSignal = true; - bool incomingReady = sender.exchange(validSignal); + bool incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - incomingReady = sender.exchange(validSignal); + incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 2 } // Destructor called here @@ -505,7 +515,7 @@ TEST_F(IntegrationTest, TwoCycleReadyFalseValidTrue) { TEST_F(IntegrationTest, TwoCycleReadyTrueValidTrue) { - // Test: FIFO feeds data to InterSimulationInterface sender/receiver pair + // Test: FIFO feeds data to InterprocessCommunicationChannel sender/receiver pair // Architecture: Sender (process A) -> Receiver -> FIFO (process B) -> SimDummy -> validation pid_t pid = fork(); @@ -514,16 +524,17 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidTrue) { // Child process: Receiver with FIFO output to SimDummy int receivedCount = 0; { - InterSimulationInterface receiver(shmName); + InterprocessCommunicationChannel receiver(shmName); FIFO outputFifo(15); SimDummy simDummy; simDummy.setNextReady(true); - bool readySignal = outputFifo.isInputReady(); - bool validSignal = receiver.exchange(readySignal); + bool readySignal = outputFifo.getInputReady(); + bool validSignal = receiver.receive_request(); if (!validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 1 STARTS @@ -531,21 +542,22 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidTrue) { if (outputFifo.getSpaceLeft() != 14) { exit(3); } - if (!outputFifo.isOutputValid()) { + if (!outputFifo.getOutputValid()) { exit(4); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (!simDummy.isOutputValid()) { exit(1); } simDummy.setNextReady(true); - readySignal = outputFifo.isInputReady(); // Should be true now - validSignal = receiver.exchange(readySignal); + readySignal = outputFifo.getInputReady(); // Should be true now + validSignal = receiver.receive_request(); if (!validSignal) { exit(2); } + receiver.send_response(readySignal); outputFifo.update(validSignal, simDummy.isInputReady()); outputFifo.toggleClock(); // BELOW HERE CYCLE 2 STARTS @@ -553,10 +565,10 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidTrue) { if (outputFifo.getSpaceLeft() != 14) { exit(3); } - if (!outputFifo.isOutputValid()) { + if (!outputFifo.getOutputValid()) { exit(4); } - simDummy.setNextValid(outputFifo.isOutputValid()); + simDummy.setNextValid(outputFifo.getOutputValid()); simDummy.toggleClock(); if (!simDummy.isOutputValid()) { exit(1); @@ -568,12 +580,12 @@ TEST_F(IntegrationTest, TwoCycleReadyTrueValidTrue) { // Parent process: Sender { - InterSimulationInterface sender(shmName); + InterprocessCommunicationChannel sender(shmName); bool validSignal = true; - bool incomingReady = sender.exchange(validSignal); + bool incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 0; expect ready==true for cycle 1 - incomingReady = sender.exchange(validSignal); + incomingReady = sender.send_request(validSignal); EXPECT_TRUE(incomingReady); // We are in cycle 1; expect ready==true for cycle 2 } // Destructor called here @@ -596,8 +608,8 @@ TEST_F(IntegrationTest, SimToFIFO) { // Propagate valid through SimDummy sim.setNextValid(true); fifo.update(sim.isOutputValid(), false); - EXPECT_TRUE(fifo.isInputReady()); - sim.setNextReady(fifo.isInputReady()); + EXPECT_TRUE(fifo.getInputReady()); + sim.setNextReady(fifo.getInputReady()); fifo.toggleClock(); sim.toggleClock(); EXPECT_EQ(fifo.size(), 0); @@ -608,8 +620,8 @@ TEST_F(IntegrationTest, SimToFIFO) { sim.setNextValid(true); fifo.update(sim.isOutputValid(), false); - EXPECT_TRUE(fifo.isInputReady()); - sim.setNextReady(fifo.isInputReady()); + EXPECT_TRUE(fifo.getInputReady()); + sim.setNextReady(fifo.getInputReady()); EXPECT_EQ(fifo.size(), i); fifo.toggleClock(); sim.toggleClock(); @@ -617,11 +629,11 @@ TEST_F(IntegrationTest, SimToFIFO) { EXPECT_TRUE(sim.isInputReady()); } - EXPECT_FALSE(fifo.isInputReady()); // FIFO changed to not ready on this cycle; Sim is still ready + EXPECT_FALSE(fifo.getInputReady()); // FIFO changed to not ready on this cycle; Sim is still ready sim.setNextValid(true); fifo.update(sim.isOutputValid(), false); - EXPECT_FALSE(fifo.isInputReady()); - sim.setNextReady(fifo.isInputReady()); + EXPECT_FALSE(fifo.getInputReady()); + sim.setNextReady(fifo.getInputReady()); fifo.toggleClock(); sim.toggleClock(); // Propagate ready false through sim diff --git a/finn_xsi/finn_xsi/unittests/InterSimulationInterface_test.cpp b/finn_xsi/finn_xsi/unittests/InterSimulationInterface_test.cpp deleted file mode 100644 index f5b9730e2e..0000000000 --- a/finn_xsi/finn_xsi/unittests/InterSimulationInterface_test.cpp +++ /dev/null @@ -1,614 +0,0 @@ -#include "InterSimulationInterface.hpp" - -#include -#include -#include - -#include -#include - -// Test fixture for InterSimulationInterface tests -class InterSimulationInterfaceTest : public ::testing::Test { - protected: - void SetUp() override { - // Generate unique shared memory name for each test - shmName = "test_shm_" + std::to_string(getpid()) + "_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); - } - - void TearDown() override { - // Cleanup: ensure shared memory is removed - boost::interprocess::shared_memory_object::remove(shmName.c_str()); - } - - std::string shmName; -}; - -// ===== Constructor and Initialization Tests ===== - -TEST_F(InterSimulationInterfaceTest, ReceiverConstructorCreatesSharedMemory) { - InterSimulationInterface receiver(shmName); - - // Verify that shared memory exists - bool shmExists = false; - try { - boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); - shmExists = true; - } catch (...) { shmExists = false; } - - EXPECT_TRUE(shmExists); -} - -TEST_F(InterSimulationInterfaceTest, SenderWaitsForReceiverToCreateSharedMemory) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender (waits for receiver) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - InterSimulationInterface sender(shmName); - exit(0); - } else { - // Parent process: Receiver (creates shared memory) - InterSimulationInterface receiver(shmName); - - // Wait for child to complete - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, DefaultConstructorCreatesUninitializedObject) { - InterSimulationInterface interface; - // Should not crash - object is in moved-from state - // Destructor should handle this gracefully -} - -TEST_F(InterSimulationInterfaceTest, MoveConstructorTransfersOwnership) { - InterSimulationInterface receiver1(shmName); - InterSimulationInterface receiver2(std::move(receiver1)); - - // receiver2 should now own the shared memory - // receiver1 should be in moved-from state (destructor shouldn't crash) -} - -TEST_F(InterSimulationInterfaceTest, MoveAssignmentTransfersOwnership) { - InterSimulationInterface receiver1(shmName); - InterSimulationInterface receiver2; - - receiver2 = std::move(receiver1); - - // receiver2 should now own the shared memory - // receiver1 should be in moved-from state -} - -// ===== Single Exchange Tests ===== - -TEST_F(InterSimulationInterfaceTest, SingleExchangeBothProcesses) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - InterSimulationInterface sender(shmName); - bool received = sender.exchange(true); - - // Sender sends true, should receive false from receiver - exit(received ? 1 : 0); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - - // Small delay to ensure both processes are ready - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - bool received = receiver.exchange(false); - - // Receiver sends false, should receive true from sender - EXPECT_TRUE(received); - - // Wait for child and check result - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, ExchangeBothSendTrue) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - InterSimulationInterface sender(shmName); - bool received = sender.exchange(true); - exit(received ? 0 : 1); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - bool received = receiver.exchange(true); - EXPECT_TRUE(received); - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, ExchangeBothSendFalse) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - InterSimulationInterface sender(shmName); - bool received = sender.exchange(false); - exit(received ? 1 : 0); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - bool received = receiver.exchange(false); - EXPECT_FALSE(received); - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// ===== Multiple Exchange Tests ===== - -TEST_F(InterSimulationInterfaceTest, MultipleExchangesSequential) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - InterSimulationInterface sender(shmName); - - for (int i = 0; i < 10; ++i) { - bool send_val = (i % 2 == 0); - bool received = sender.exchange(send_val); - - // Sender alternates true/false, receiver sends opposite - bool expected = !send_val; - if (received != expected) { - exit(1); - } - } - exit(0); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - for (int i = 0; i < 10; ++i) { - bool send_val = (i % 2 != 0); // Opposite of sender - bool received = receiver.exchange(send_val); - - bool expected = !send_val; - EXPECT_EQ(received, expected); - } - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, ManyExchanges) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - InterSimulationInterface sender(shmName); - - for (int i = 0; i < 1000; ++i) { - bool send_val = (i % 3 == 0); - sender.exchange(send_val); - } - exit(0); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - for (int i = 0; i < 1000; ++i) { - bool send_val = (i % 5 == 0); - bool received = receiver.exchange(send_val); - - // Just verify exchange completes without deadlock - bool expected_from_sender = (i % 3 == 0); - EXPECT_EQ(received, expected_from_sender); - } - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, AlternatingPattern) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender sends alternating true/false - InterSimulationInterface sender(shmName); - - for (int i = 0; i < 100; ++i) { - bool send_val = (i % 2 == 0); - bool received = sender.exchange(send_val); - - // Receiver also alternates, but starts with false - bool expected = (i % 2 != 0); - if (received != expected) { - exit(1); - } - } - exit(0); - } else { - // Parent process: Receiver sends alternating false/true - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - for (int i = 0; i < 100; ++i) { - bool send_val = (i % 2 != 0); - bool received = receiver.exchange(send_val); - - bool expected = (i % 2 == 0); - EXPECT_EQ(received, expected); - } - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// ===== Buffer Flipping Tests ===== - -TEST_F(InterSimulationInterfaceTest, BufferFlipsCorrectly) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - InterSimulationInterface sender(shmName); - - // Perform multiple exchanges to trigger buffer flips - for (int i = 0; i < 20; ++i) { - sender.exchange(true); - } - exit(0); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - // Perform multiple exchanges - buffer should flip multiple times - for (int i = 0; i < 20; ++i) { - bool received = receiver.exchange(false); - EXPECT_TRUE(received); - } - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// ===== Stress Tests ===== - -TEST_F(InterSimulationInterfaceTest, HighFrequencyExchanges) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - rapid exchanges - InterSimulationInterface sender(shmName); - - for (int i = 0; i < 10000; ++i) { - sender.exchange(i & 1); // Alternate between true/false - } - exit(0); - } else { - // Parent process: Receiver - rapid exchanges - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - for (int i = 0; i < 10000; ++i) { - receiver.exchange(!(i & 1)); // Opposite pattern - } - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, StressTestWithComplexPattern) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender with complex pattern - InterSimulationInterface sender(shmName); - - for (int i = 0; i < 5000; ++i) { - bool val = ((i * 7) % 11) < 5; // Pseudo-random pattern - sender.exchange(val); - } - exit(0); - } else { - // Parent process: Receiver with different complex pattern - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - for (int i = 0; i < 5000; ++i) { - bool val = ((i * 13) % 17) < 8; // Different pseudo-random pattern - receiver.exchange(val); - } - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// ===== Reference Counting Tests ===== - -TEST_F(InterSimulationInterfaceTest, ReferenceCountingTwoProcesses) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Create sender and let it go out of scope - { - InterSimulationInterface sender(shmName); - sender.exchange(true); - } - - // Shared memory should still exist because parent still holds reference - bool shmExists = false; - try { - boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); - shmExists = true; - } catch (...) { shmExists = false; } - - exit(shmExists ? 0 : 1); - } else { - // Parent process: Keep receiver alive - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - receiver.exchange(false); - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, SharedMemoryCleanupAfterBothProcessesExit) { - // This test verifies that shared memory is properly cleaned up - // when both processes exit. We need to test from a third process - // that was never part of the shared memory to avoid race conditions. - - pid_t verifier_pid = fork(); - - if (verifier_pid == 0) { - // Verifier process: spawns two children and then checks cleanup - pid_t receiver_pid = fork(); - - if (receiver_pid == 0) { - // First child: Receiver - // Use block scope so destructor is called before exit - { - InterSimulationInterface receiver(shmName); - receiver.exchange(true); - } // Destructor called here - exit(0); - } - - // Small delay to ensure receiver creates shared memory - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - - pid_t sender_pid = fork(); - if (sender_pid == 0) { - // Second child: Sender - // Use block scope so destructor is called before exit - { - InterSimulationInterface sender(shmName); - sender.exchange(false); - } // Destructor called here - exit(0); - } - - // Wait for both children to complete - int receiver_status, sender_status; - waitpid(receiver_pid, &receiver_status, 0); - waitpid(sender_pid, &sender_status, 0); - - // Give time for cleanup to complete - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - // Verify shared memory is cleaned up - bool shmExists = false; - try { - boost::interprocess::managed_shared_memory shmem(boost::interprocess::open_only, shmName.c_str()); - shmExists = true; - } catch (...) { - shmExists = false; - } - - // Exit with 0 if cleanup succeeded (shmExists == false) - exit(shmExists ? 1 : 0); - } else { - // Parent: Wait for verifier process - int status; - waitpid(verifier_pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// ===== Move Semantics Tests ===== - -TEST_F(InterSimulationInterfaceTest, MoveConstructorMaintainsConnection) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender with move - InterSimulationInterface sender1(shmName); - InterSimulationInterface sender2(std::move(sender1)); - - bool received = sender2.exchange(true); - exit(received ? 1 : 0); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - bool received = receiver.exchange(false); - EXPECT_TRUE(received); - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, MoveAssignmentMaintainsConnection) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender with move assignment - InterSimulationInterface sender1(shmName); - InterSimulationInterface sender2; - sender2 = std::move(sender1); - - bool received = sender2.exchange(true); - exit(received ? 1 : 0); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - bool received = receiver.exchange(false); - EXPECT_TRUE(received); - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// ===== Edge Cases ===== - -TEST_F(InterSimulationInterfaceTest, FirstCallBehavior) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - first call should not wait for buffer flip - InterSimulationInterface sender(shmName); - - auto start = std::chrono::steady_clock::now(); - sender.exchange(true); - auto end = std::chrono::steady_clock::now(); - - // First call should complete quickly (not waiting for previous flip) - auto duration = std::chrono::duration_cast(end - start); - exit(duration.count() < 100 ? 0 : 1); - } else { - // Parent process: Receiver - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - receiver.exchange(false); - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -TEST_F(InterSimulationInterfaceTest, ConsecutiveExchangesSameValue) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Send same value repeatedly - InterSimulationInterface sender(shmName); - - for (int i = 0; i < 50; ++i) { - sender.exchange(true); // Always true - } - exit(0); - } else { - // Parent process: Verify same value received repeatedly - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - for (int i = 0; i < 50; ++i) { - bool received = receiver.exchange(false); - EXPECT_TRUE(received); - } - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// ===== Timing and Synchronization Tests ===== - -TEST_F(InterSimulationInterfaceTest, SynchronizationBetweenProcesses) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender - delayed start - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - InterSimulationInterface sender(shmName); - - for (int i = 0; i < 10; ++i) { - sender.exchange(true); - } - exit(0); - } else { - // Parent process: Receiver - starts immediately - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - // Should wait for sender to be ready - for (int i = 0; i < 10; ++i) { - receiver.exchange(false); - } - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// ===== Custom Shared Memory Size Tests ===== - -TEST_F(InterSimulationInterfaceTest, CustomSharedMemorySize) { - pid_t pid = fork(); - - if (pid == 0) { - // Child process: Sender with larger shared memory - InterSimulationInterface sender(shmName); - sender.exchange(true); - exit(0); - } else { - // Parent process: Receiver with larger shared memory - InterSimulationInterface receiver(shmName); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - - bool received = receiver.exchange(false); - EXPECT_TRUE(received); - - int status; - waitpid(pid, &status, 0); - EXPECT_EQ(WEXITSTATUS(status), 0); - } -} - -// Main function to run all tests -int main(int argc, char** argv) { - ::testing::InitGoogleTest(&argc, argv); - return RUN_ALL_TESTS(); -} diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index deb7726d12..72418a6d41 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -775,7 +775,7 @@ def _test_depth( fifo_idx: int, baseline_depths: list, initial_fifo_depths: dict, - sim, + sim: Simulation, sim_cycles: float, ) -> tuple[bool, bool]: """Test a specific FIFO depth. @@ -795,7 +795,9 @@ def _test_depth( test_depths = [row[:] for row in baseline_depths] # Deep copy from baseline test_depths[node_idx][fifo_idx] = test_depth - new_data, timeout = sim.simulate_node_connected(test_depths, max_cycles=sim_cycles * 1.1) + new_data, timeout = sim.simulate_node_connected( + test_depths, max_cycles=math.ceil(sim_cycles * 1.1) + ) if timeout: return False, True @@ -832,7 +834,7 @@ def _minimize_fifo_depth( baseline_depths: list, bit_widths: list, initial_fifo_depths: dict, - sim, + sim: Simulation, sim_cycles: int, ) -> int: """Minimize a single FIFO depth using binary search. @@ -958,7 +960,7 @@ def _exponential_binary_search_depth( baseline_depths: list, bitwidth: int, initial_fifo_depths: dict, - sim, + sim: Simulation, sim_cycles: float, valid_blocks: list[int], ) -> int: @@ -1040,7 +1042,7 @@ def _binary_search_srl_depth( baseline_depths: list, bitwidth: int, initial_fifo_depths: dict, - sim, + sim: Simulation, sim_cycles: float, lower_luts: int, upper_luts: int, diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index 4daa0f8366..d625917d72 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -228,9 +228,43 @@ def _send_and_receive( Returns: Response dictionary + + Raises: + RuntimeError: If the subprocess has terminated with an error """ - self._send_command(process_idx, command, payload) - return self._receive_response(process_idx) + try: + self._send_command(process_idx, command, payload) + return self._receive_response(process_idx) + except (BrokenPipeError, ConnectionResetError): + # Connection error means the subprocess has died + # Check if it exited with an error and raise that instead + proc, stdout_file, stderr_file = self.processes[process_idx] + returncode = proc.poll() + + if returncode is not None and returncode != 0: + # Process has terminated with an error + # Flush and read error logs + stdout_file.flush() + stderr_file.flush() + + stdout_log = self.logdir / f"{process_idx}_stdout.log" + stderr_log = self.logdir / f"{process_idx}_stderr.log" + + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + + # Raise the actual error from the subprocess + msg = ( + f"Subprocess (process_idx={process_idx}) terminated with" + " exit code {returncode}.\n" + f"Stderr:\n{stderr_output}\n" + f"Stdout:\n{stdout_output}" + ) + raise RuntimeError(msg) from None + + # If process exited cleanly (returncode == 0) or hasn't exited yet, + # this is an unexpected connection error + return None def _cleanup_sockets(self) -> None: """Close all sockets and terminate all processes.""" @@ -291,8 +325,8 @@ def run( try: with ThreadPoolExecutor(self.workers) as pool: for i, (name, binary) in enumerate(zip(self.names, self.binaries, strict=True)): - is_last_node = i == len(self.names) - 1 - is_special_for_display = i == 0 or is_last_node + is_first_node = i == 0 + is_special_for_display = is_first_node or i == len(self.names) - 1 futures.append( pool.submit( self._run_binary, @@ -300,7 +334,7 @@ def run( name, i % multiprocessing.cpu_count(), depth[i] if depth is not None else None, - is_last_node, # Only last node has no output FIFOs + is_first_node, # Only first node has no input FIFOs is_special_for_display, # First and last get special coloring max_cycles, ) @@ -405,7 +439,7 @@ def _run_binary( name: str | None, _cpu: int | None, depth: list[int] | None = None, - is_last_node: bool = False, + is_first_node: bool = False, is_special_for_display: bool = False, max_cycles: int | None = None, ) -> tuple[str, list[int], int, int, list[int], bool, list[int]] | None: @@ -416,7 +450,7 @@ def _run_binary( name: Name of simulation node _cpu: CPU affinity (unused) depth: List of FIFO depths for this node's output FIFOs - is_last_node: True if this is the last node (no output FIFOs to configure) + is_first_node: True if this is the first node (no input FIFOs to configure) is_special_for_display: True if this node should get special color in logs max_cycles: Maximum cycles to simulate @@ -454,7 +488,7 @@ def _print(msg: str, color: str = "green") -> None: # Send configuration commands # Last node has no output FIFOs, so don't configure FIFO depths config_payload: dict[str, list[int] | int] = {} - if not is_last_node and depth is not None: + if not is_first_node and depth is not None: config_payload["fifo_depth"] = depth if max_cycles is not None: config_payload["max_cycles"] = max_cycles @@ -490,7 +524,11 @@ def _print(msg: str, color: str = "green") -> None: # Check if we should stop early with self.stop_lock: if self.should_stop: - stop_response = self._send_and_receive(proc_idx, "stop", {}) + try: + stop_response = self._send_and_receive(proc_idx, "stop", {}) + except (BrokenPipeError, ConnectionResetError, RuntimeError): + # Process may have already exited - that's ok during shutdown + stop_response = None if stop_response: cycles = stop_response.get("cycles", 0) samps = stop_response.get("samples", 0) From 8ef0d32ffd55edea83866450cdb816d8ff96d2cc Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 14 Jan 2026 16:00:14 +0100 Subject: [PATCH 039/170] FIFO Depth application step added. C++ driver fix --- .gitignore | 2 + .../fpgadataflow/make_driver.py | 2 +- .../transformation/fpgadataflow/simulation.py | 250 +++++++++++++++++- 3 files changed, 248 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index f0a10e9194..2854862d7b 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,8 @@ poetry.lock **/compile_commands.json **/.cache **/build +**/_deps + # Package files *.egg diff --git a/src/finn/transformation/fpgadataflow/make_driver.py b/src/finn/transformation/fpgadataflow/make_driver.py index deac676743..568406f69d 100644 --- a/src/finn/transformation/fpgadataflow/make_driver.py +++ b/src/finn/transformation/fpgadataflow/make_driver.py @@ -97,7 +97,7 @@ def resolve_dt_name(s: str) -> str: if s in ["BINARY", "TERNARY", "BIPOLAR"]: return "Datatype" + s[0] + s[1:].lower() elif s.startswith("U"): - return "DatatypeUint<" + s.replace("UINT", "") + ">" + return "DatatypeUInt<" + s.replace("UINT", "") + ">" elif s.startswith("I"): return "DatatypeInt<" + s.replace("INT", "") + ">" elif "FLOAT" in s: diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 93907f1415..b34c48bf66 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -30,6 +30,7 @@ from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.insert_dwc import InsertDWC +from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.simulation_controller import NodeConnectedSimulationController from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers @@ -41,6 +42,20 @@ from collections.abc import Sequence +""" +Classes in this module: + SimulationType: Determines the type of simulation to be executed. + SimulationBuilder: Performs multiple steps to build a simulation binary. + Simulation: Builds a simulation of the given type and interacts with + the binary, returning the results. + + RunLayerParallelSimulation: Create a Simulation object for parallel simulation. + ApplyFIFOSizes: Read the output JSON from the simulation and apply the + found sizes to the FIFOs in the model. + +""" + + class SimulationType(str, Enum): # Individual node simulations connected by IPC NODE_BASED_CONNECTED = "NODE_BASED_CONNECTED" @@ -587,7 +602,7 @@ def build_simulation( class Simulation: - """Manage simulation (runs) in FINN. + """Manage simulation (runs) in FINN. Upon instance creation, the simulation will be built. IMPORTANT: If the modelwrapper was somehow changed, create a NEW simulation object! """ @@ -598,6 +613,7 @@ def __init__( fpgapart: str, clk_ns: float, functional_sim: bool, + simulation_type: SimulationType, workers: int | None = None, ) -> None: """Create a new simulation instance. If workers is None, NUM_DEFAULT_WORKERS are used.""" @@ -616,8 +632,9 @@ def __init__( sys.stdout = sys.stdout.console sys.stderr = sys.stderr.console + self.simulation_type = simulation_type self.binaries = self.builder.build_simulation( - SimulationType.NODE_BASED_CONNECTED, + simulation_type, self.workers, with_live_display=True, functional_sim=self.functional_sim, @@ -632,12 +649,30 @@ def _prepare_model(self) -> None: self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) self.model = self.model.transform(HLSSynthIP()) + def simulate(self, *args: Any, **kwargs: Any) -> Any: + """Run the built simulation and return its results. This function can always be called + and will lookup the correct function to use, but consequently + cannot provide typing information.""" + match self.simulation_type: + case SimulationType.NODE_BASED_CONNECTED: + return self.simulate_node_connected(*args, **kwargs) + case SimulationType.NODE_BASED_ISOLATED: + return self.simulate_node_isolated(*args, **kwargs) + case _: + raise FINNUserError(f"Unsupported simulation type {self.simulation_type}") + def simulate_node_connected( self, depth: int | list[list[int]] | None = None, max_cycles: int | None = None ) -> tuple[dict[int, dict[str, list[int]]], bool]: """Simulate the given number of samples for every layer. Layers are completely isolated and simulated in parallel. Simulation data is returned as a dict (by node name as index). """ + if self.simulation_type != SimulationType.NODE_BASED_CONNECTED: + raise FINNInternalError( + f"Called simulation function 'simulate_node_connected' " + f"does not match provided simulation type " + f"{self.simulation_type}" + ) names = [node.name for node in self.model.graph.node] initial_depth = [[depth]] * len(self.binaries) if isinstance(depth, int) else depth @@ -669,8 +704,16 @@ def simulate_node_connected( json.dump(data, output_json.open("w"), indent=4) return data, merged_data.get("timeout_occurred", False) + def simulate_node_isolated(self) -> None: + if self.simulation_type != SimulationType.NODE_BASED_ISOLATED: + raise FINNInternalError( + f"Called simulation function 'simulate_node_isolated' " + f"does not match provided simulation type " + f"{self.simulation_type}" + ) + raise NotImplementedError() + -# TODO: Just a test transformation. Will be integrated properly later class RunLayerParallelSimulation(Transformation): # noqa def __init__( self, @@ -690,10 +733,16 @@ def __init__( self.quality_of_results = quality_of_results def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: - sim = Simulation(model, self.fpgapart, self.clk_ns, self.cfg.functional_simulation) + sim = Simulation( + model, + self.fpgapart, + self.clk_ns, + self.cfg.functional_simulation, + SimulationType.NODE_BASED_CONNECTED, + ) model = sim.model # TODO:clean up - initial_fifo_depths, _ = sim.simulate_node_connected() + initial_fifo_depths, _ = sim.simulate() fifo_depths = [] # Each entry is a list of fifo sizes for that node for val in initial_fifo_depths.values(): @@ -750,6 +799,14 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: print(f"{i}: {fifo_depths[i]}") log.info(f"{i}: {fifo_depths[i]}") + # Write back results. By default write to output_dir / "fifo_config.json" + assert len(fifo_depths) == len(model.graph.node) + json_results = {} + for i in range(len(fifo_depths)): + json_results[i] = {"node": model.graph.node[i].name, "depths": fifo_depths[i]} + with (Path(self.cfg.output_dir) / "fifo_config.json").open("w") as f: + json.dump(json_results, f) + return model, False def _check_performance(self, new_data: dict, initial_fifo_depths: dict) -> bool: @@ -1198,3 +1255,186 @@ def calculate_srl16e_depth_range(luts: int, bitwidth: int) -> tuple[int, int]: return (0, 0) return (min_depth, max_depth) + + +class ApplyFIFOSizes(Transformation): + """Apply a FIFO sizing configuration to the model. If not existing, inserts FIFOs beforehand.""" + + def __init__( + self, + cfg: DataflowBuildConfig, + fifo_config: Path | None = None, + max_qsrl_depth: int = 256, + vivado_ram_style: str = "auto", + ) -> None: + """If given read the config json from the given path. + Otherwise check in the output directory. + """ + self.cfg = cfg + self.max_qsrl_depth = max_qsrl_depth + self.vivado_ram_style = vivado_ram_style + if fifo_config is None: + self.path = Path(cfg.output_dir) / "fifo_config.json" + else: + self.path = fifo_config + + FIFODepthConfig = dict[int, dict[str, str | list[int]]] # noqa + self.depth: FIFODepthConfig = {} + with self.path.open() as f: + self.depth = cast("FIFODepthConfig", json.load(f)) + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Apply FIFO Simulation Depths to the model.""" + # TODO: Better way to check for fifos (op_type for example) + if len(list(filter(lambda node: "StreamingFIFO" in node.op_type, model.graph.node))) > 0: + log.warning( + "It seems that StreamingFIFOs have already " + "been inserted into the graph. Skipping insertion of FIFOs." + ) + else: + if len(model.graph.node) != len(self.depth): + raise FINNUserError( + "There are no StreamingFIFOs in the graph, yet the number " + "of nodes and number of FIFO settings differ. There may be " + "unaccounted for nodes that have not been part of the FIFO " + "simulation. Consider re-running simulation directly before " + "applying the FIFO sizes. It might also be that your model " + "or config is outdated, in which case it is recommended to " + "re-run the entire flow from start to finish." + ) + + # Inser the FIFOs into the model + model = model.transform(InsertFIFO(True, self.max_qsrl_depth, self.vivado_ram_style)) + + # Synthesize the nodes (TODO: Remove) + model = model.transform(GiveUniqueNodeNames()) + model: ModelWrapper = model.transform(GiveReadableTensorNames()) + model = model.transform(SpecializeLayers(self.cfg._resolve_fpga_part())) # noqa + model = model.transform(GiveUniqueNodeNames()) + model: ModelWrapper = model.transform(GiveReadableTensorNames()) + model = model.transform( + PrepareIP( + fpgapart=self.cfg._resolve_fpga_part(), clk=self.cfg.synth_clk_period_ns # noqa + ) + ) + model = model.transform(HLSSynthIP()) + + # Sanity check to make sure fifos were inserted + inserted_fifo_count = sum( + [int("StreamingFIFO" in node.op_type) for node in model.graph.node] + ) + if inserted_fifo_count == 0: + raise FINNInternalError( + "No FIFOs were inserted. This may be due to " + "wrong network configuration, step order or " + "a number of other things." + ) + if inserted_fifo_count < int(0.1 * float(len(model.graph.node))): + log.warning( + "The number of inserted FIFOs makes up less than 10%" + " of the total number of nodes in the model. This could " + "point to a potential error." + ) + + # Assign data based on the names of the nodes. Since no FIFOs were in the graph + # before, the names should stay the same. + # TODO: This currently assumes that the FIFO to be sized comes AFTER the actual node. + # TODO: If the simulation code is changed, this needs to be changed as well + for i in range(len(model.graph.node)): + node: NodeProto = model.graph.node[i] + node_inst: HWCustomOp = getCustomOp(model.graph.node[i]) + if node.op_type.startswith("StreamingFIFO"): + # FIFOs can only have one producer, so this must + # be the node whoose simulated depth we have to get + predecessors: list[NodeProto] | None = model.find_direct_predecessors(node) + if predecessors is not None and len(predecessors) > 1: + raise FINNInternalError(f"FIFO node {node.name} has multiple producers!") + if predecessors is None: + continue + predecessor: NodeProto = predecessors[0] + + # Check which of the predecessors outputs this FIFO is connected to + # and use the depth at that index from the simulation + for sim_node_name, sim_depths in self.depth.values(): + if sim_node_name == predecessor.name: + depth_index = predecessor.output.index(node.input[0]) + depth = int(sim_depths[depth_index]) + node_inst.set_nodeattr("depth", depth) + + # TODO: Code copied from old FIFO sizing + # exception for top-level IO FIFOs which cause a bug in simulation + # (top-level IOs should not have impl_style=vivado) + toplevel_in = node.input[0] in [x.name for x in model.graph.input] + toplevel_out = node.output[0] in [x.name for x in model.graph.output] + toplevel_style_exception = toplevel_in or toplevel_out + # Set FIFO implementation/ram styles + if (depth > self.max_qsrl_depth) and (not toplevel_style_exception): + node_inst.set_nodeattr("impl_style", "vivado") + node_inst.set_nodeattr("ram_style", self.vivado_ram_style) + else: + node_inst.set_nodeattr("impl_style", "rtl") + + # TODO: Following code is copied from the old FIFO sizing. Might be shortenable + for node in model.graph.node: + if not node.op_type.startswith("StreamingFIFO"): + node_inst = getCustomOp(node) + fifodepth_in = [] + for node_inp in node.input: + prod = model.find_producer(node_inp) + if prod is None: + # no producer for this input + if node_inp in [x.name for x in model.graph.input]: + # top-level input with no FIFO + fifodepth_in.append(0) + else: + # FIFO depth attr applies only to dynamic attributes + pass + else: + # there is a producer for this input + if prod.op_type.startswith("StreamingFIFO"): + prod_inst = getCustomOp(prod) + fifodepth_in.append(prod_inst.get_nodeattr("depth")) + else: + # explicitly no FIFO on this dynamic input + fifodepth_in.append(0) + fifodepth_out = [] + for node_out in node.output: + cons = model.find_consumer(node_out) + if cons is None: + # no consumer for this output + if node_out in [x.name for x in model.graph.output]: + # top-level output with no FIFO + fifodepth_out.append(0) + else: + # FIFO depth attr applies only to dynamic attributes + pass + else: + # there is a consumer for this input + if cons.op_type.startswith("StreamingFIFO"): + cons_inst = getCustomOp(cons) + fifodepth_out.append(cons_inst.get_nodeattr("depth")) + else: + # explicitly no FIFO on this dynamic output + fifodepth_out.append(0) + node_inst.set_nodeattr("inFIFODepths", fifodepth_in) + node_inst.set_nodeattr("outFIFODepths", fifodepth_out) + + # Synthesize with the proper sizes set + model = model.transform(SpecializeLayers(self.cfg._resolve_fpga_part())) # noqa + model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveReadableTensorNames()) + model = model.transform( + PrepareIP( + fpgapart=self.cfg._resolve_fpga_part(), clk=self.cfg.synth_clk_period_ns # noqa + ) + ) + model = model.transform(HLSSynthIP()) + # model.set_metadata_prop("rtlsim_trace", "") + # model.set_metadata_prop("rtlsim_so", "") + # model.set_metadata_prop("vivado_stitch_proj", "") + # model.set_metadata_prop("wrapper_filename", "") + # model.set_metadata_prop("vivado_stitch_vlnv", "") + # model.set_metadata_prop("vivado_stitch_ifnames", "") + # model.set_metadata_prop("exec_mode", "") + + return model, False From c190059deb5f3187f52f31e95d3e458171b0dfb0 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 14 Jan 2026 16:29:40 +0100 Subject: [PATCH 040/170] Removed old unused FIFO simulation files --- finn_xsi/finn_xsi/sim_engine.py | 409 ------------------ finn_xsi/testcase/StreamingEltwise_hls_0.v | 349 --------------- ..._hls_0_flow_control_loop_pipe_no_ap_cont.v | 103 ----- .../StreamingEltwise_hls_0_regslice_both.v | 110 ----- 4 files changed, 971 deletions(-) delete mode 100644 finn_xsi/finn_xsi/sim_engine.py delete mode 100644 finn_xsi/testcase/StreamingEltwise_hls_0.v delete mode 100644 finn_xsi/testcase/StreamingEltwise_hls_0_flow_control_loop_pipe_no_ap_cont.v delete mode 100644 finn_xsi/testcase/StreamingEltwise_hls_0_regslice_both.v diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py deleted file mode 100644 index 1627809932..0000000000 --- a/finn_xsi/finn_xsi/sim_engine.py +++ /dev/null @@ -1,409 +0,0 @@ -############################################################################# -# Copyright (C) 2025, Advanced Micro Devices, Inc. -# All rights reserved. -# -# SPDX-License-Identifier: BSD-3-Clause -# -# @brief SimEngine abstraction for running FINN task in simulated hardware. -# @author Thomas B. Preußer -# @author Yaman Umuroglu -############################################################################# - -import xsi - - -class SimEngine: - # ------------------------------------------------------------------------ - # Life Cycle - def __init__(self, kernel, design, log=None, wdb=None): - top = xsi.Design(xsi.Kernel(kernel), design, log, wdb) - clk = top.getPort("ap_clk") - clk2x = top.getPort("ap_clk2x") - for p in top.ports(): - if p.isInput(): - p.clear().write_back() - - def cycle(updates): - # Rising Edge - clk.set(1).write_back() - if clk2x is not None: - clk2x.set(1).write_back() - # Updates after Active Edge - top.run(1) - for port, update in updates.items(): - port.set_hexstr(update).write_back() - - # Edges inactive on interface & finish Cycle - if clk2x is None: - top.run(4999) - clk.set(0).write_back() - top.run(5000) - else: - top.run(2499) - clk2x.set(0).write_back() - top.run(2500) - clk.set(0).write_back() - clk2x.set(1).write_back() - top.run(2500) - clk2x.set(0).write_back() - top.run(2500) - - self.top = top - self.cycle = cycle - self.ticks = 0 - self.tasks = [] - self.watchdogs = [] - - # ------------------------------------------------------------------------ - # Utility - def get_bus_port(self, bus, suffix): - port = self.top.getPort(bus + "_" + suffix.lower()) - return port if port is not None else self.top.getPort(bus + "_" + suffix.upper()) - - # ------------------------------------------------------------------------ - # Simulation Setup - - # Task Scheduling - def enlist(self, task): - self.tasks.append(task) - - # Watchdog Generation - def create_watchdog(self, name, timeout): - class Watchdog: - def __init__(self, name, timeout): - self.name = name - self.ticks = 0 - self.timeout = timeout - - def __bool__(self): - return self.ticks < self.timeout - - def __repr__(self): - return self.name - - def __call__(self): - self.ticks += 1 - - def reset(self): - self.ticks = 0 - - ret = Watchdog(name, timeout) - self.watchdogs.append(ret) - return ret - - def remove_watchdog(self, watchdog): - self.watchdogs.remove(watchdog) - - # ------------------------------------------------------------------------ - # Execution - def run(self, cycles=None): - "Run all tasks to completion or until a watchdog triggers." - timeout = None if cycles is None else self.create_watchdog("Run Timeout", cycles) - - woken = [] - while len(self.tasks) > 0 and len(woken := [w for w in self.watchdogs if not w]) == 0: - # Process Tasks and Collect Updates to Write Back - tasks = [] - updates = {} - - # Execute Cycle - self.ticks += 1 - strong = False - for task in self.tasks: - # Tasks read signals and derive updates to schedule for after the clock cycle - ret = task(self) - if ret is not None: - updates.update(ret) - tasks.append(task) - strong |= bool(task) - self.cycle(updates) - - # Step Watchdogs - for watchdog in self.watchdogs: - watchdog() - - # Update to Unfinished Tasks - self.tasks = tasks if strong else [] - - # Return List of Woken Watchdogs - if timeout is not None: - self.remove_watchdog(timeout) - return woken - - # ------------------------------------------------------------------------ - # Standard Tasks - def do_reset(self): - "Schedule a reset sequence." - - class Reset: - def __init__(self, top): - self.cnt = 0 - self.rst_n = top.getPort("ap_rst_n") - - def __call__(self, sim): - cnt = self.cnt - self.cnt = cnt + 1 - - if cnt == 0: - return {self.rst_n: "0"} - if cnt < 16: - return {} - if cnt == 16: - return {self.rst_n: "1"} - return None - - self.enlist(Reset(self.top)) - - def stream_input(self, istream, values, throttle=(float("inf"), 0)): - "Stream all values from the passed iterator into the specified stream." - - class InputStreamer: - def __init__(self, top, istream, values, throttle): - self.vld = top.get_bus_port(istream, "tvalid") - self.rdy = top.get_bus_port(istream, "tready") - self.dat = top.get_bus_port(istream, "tdata") - self.values = values - - self.throttle = throttle - self.await_tick = 0 - self.count_txns = throttle[0] - - def __call__(self, sim): - vld = self.vld.as_bool() - if vld and not self.rdy.read().as_bool(): - return {} - - # Track Transaction Count - if vld: - self.count_txns += 1 - - # Proceed according to Throttling Rate - if self.count_txns < self.throttle[0] or not sim.ticks < self.await_tick: - # Try Feed - val = next(self.values, None) - if val is None: - # Unset vld, then exit - return {self.vld: "0", self.dat: "0"} if vld else None - - # Feed next Value - ret = {self.dat: val} - if not vld: - ret[self.vld] = "1" - if self.count_txns == self.throttle[0]: - self.count_txns = 0 - self.await_tick = sim.ticks + self.throttle[1] - return ret - - # Stall Feed - return {self.vld: "0", self.dat: "0"} if vld else {} - - self.enlist(InputStreamer(self, istream, values, throttle)) - - def collect_output(self, ostream, size, watchdog=None): - "Collect size outputs from the specified stream into the returned iterable buffer." - - class OutputCollector: - def __init__(self, top, ostream, size, watchdog): - self.size = size - self.vld = top.get_bus_port(ostream, "tvalid") - self.rdy = top.get_bus_port(ostream, "tready") - self.dat = top.get_bus_port(ostream, "tdata") - self.buf = [] - self.watchdog = watchdog - - def __iter__(self): - return iter(self.buf) - - def __call__(self, sim): - if self.rdy.as_bool(): - if self.vld.read().as_bool(): - # Have a n Output Transaction - if self.watchdog is not None: - self.watchdog.reset() - val = self.dat.read().as_hexstr() - self.buf.append(val) - if len(self.buf) == size: - return {self.rdy: "0"} - return {} - - if len(self.buf) < size: - return {self.rdy: "1"} - return None - - ret = OutputCollector(self, ostream, size, watchdog) - self.enlist(ret) - return ret - - def trace_stream(self, stream): - "Monitor an AXI-Stream and trace its transaction activity" - - class StreamTracer: - def __init__(self, sim, stream): - self.vld = sim.get_bus_port(stream, "tvalid") - self.rdy = sim.get_bus_port(stream, "tready") - self.trace = "" - - def __call__(self, sim): - self.trace += ( - "1" if self.vld.read().as_bool() and self.rdy.read().as_bool() else "0" - ) - return {} - - def __bool__(self): - return False - - def __str__(self): - return self.trace - - ret = StreamTracer(self, stream) - self.enlist(ret) - return ret - - def write_axilite(self, m_axilite, writes): - "Execute writes specified as a list of (addr, val)-tuples to AXI-lite interface" - - class AxiLiteWriter: - INIT = 0 - FEED = 1 - COOL = 2 - - def __init__(self, top, m_axilite, writes): - self.awready = top.get_bus_port(m_axilite, "awready") - self.awvalid = top.get_bus_port(m_axilite, "awvalid") - self.awaddr = top.get_bus_port(m_axilite, "awaddr") - self.wready = top.get_bus_port(m_axilite, "wready") - self.wvalid = top.get_bus_port(m_axilite, "wvalid") - self.wdata = top.get_bus_port(m_axilite, "wdata") - wstrb = top.get_bus_port(m_axilite, "wstrb") - wstrb.set_binstr("1" * wstrb.width()).write_back() - self.bready = top.get_bus_port(m_axilite, "bready") - self.bvalid = top.get_bus_port(m_axilite, "bvalid") - self.bresp = top.get_bus_port(m_axilite, "bresp") - self.writes = writes - self.state = self.INIT - self.pending = 0 - - def __call__(self, sim): - # Termination - if self.state == self.COOL and not self.bready.as_bool(): - return None - - ret = {} - - # Always Monitor Completions - if self.state == self.INIT: - ret[self.bready] = "1" - self.state = self.FEED - - if self.bvalid.read().as_bool(): - if self.pending < 1: - print("Received spurious completion on", self.bresp.name()) - else: - self.pending -= 1 - if self.pending == 0 and self.state == self.COOL: - ret[self.bready] = "0" - - if self.bresp.read().as_unsigned() != 0: - print("Received error indication on", self.bresp.name()) - - # Transaction Feed - if self.state == self.FEED: - step = True - - # Check for busy address feed - avld = self.awvalid.as_bool() - aclr = False - if avld: - if self.awready.read().as_bool(): - aclr = True - else: - step = False - - # Check for busy data feed - wvld = self.wvalid.as_bool() - wclr = False - if wvld: - if self.wready.read().as_bool(): - wclr = True - else: - step = False - - # Proceed with next Write - if step: - addr, val = next(self.writes, (None, None)) - if addr is not None: - ret[self.awaddr] = f"{addr:x}" - ret[self.wdata] = val - if not avld: - ret[self.awvalid] = "1" - if not wvld: - ret[self.wvalid] = "1" - self.pending += 1 - return ret - if not self.pending: - ret[self.bready] = "0" - self.state = self.COOL - - # Deassert completed feed - if aclr: - ret[self.awvalid] = "0" - if wclr: - ret[self.wvalid] = "0" - - return ret - - self.enlist(AxiLiteWriter(self, m_axilite, writes)) - - def read_axilite(self, m_axilite, reads): - class AxiLiteReader: - def __init__(self, top, m_axilite, reads): - self.arready = top.get_bus_port(m_axilite, "arready") - self.arvalid = top.get_bus_port(m_axilite, "arvalid") - self.araddr = top.get_bus_port(m_axilite, "araddr") - self.rready = top.get_bus_port(m_axilite, "rready") - self.rvalid = top.get_bus_port(m_axilite, "rvalid") - self.rdata = top.get_bus_port(m_axilite, "rdata") - self.reads = reads - self.pending = [] - self.draining = False - self.replies = {} - - def __call__(self, sim): - ret = {} - - # Address Stream Feed: assert self.draining when done - if not self.draining: - if self.arready.read().as_bool() or not self.arvalid.as_bool(): - addr = next(self.reads, None) - if addr is None: - ret[self.arvalid] = "0" - self.draining = True - else: - ret[self.arvalid] = "1" - ret[self.araddr] = f"{addr:x}" - self.pending.append(addr) - - # Reply Collection - if not self.rready.as_bool(): - # Termination - if self.draining: - return None - # Activation - ret[self.rready] = "1" - elif self.rvalid.read().as_bool(): - assert len(self.pending) > 0, "Spurious reply." - self.replies[self.pending.pop(0)] = self.rdata.read().as_hexstr() - if self.draining and len(self.pending) == 0: - ret[self.rready] = "0" - - return ret - - def __iter__(self): - return iter(self.replies) - - def __getitem__(self, addr): - return self.replies[addr] - - ret = AxiLiteReader(self, m_axilite, reads) - self.enlist(ret) - return ret diff --git a/finn_xsi/testcase/StreamingEltwise_hls_0.v b/finn_xsi/testcase/StreamingEltwise_hls_0.v deleted file mode 100644 index f5207e0548..0000000000 --- a/finn_xsi/testcase/StreamingEltwise_hls_0.v +++ /dev/null @@ -1,349 +0,0 @@ -// ============================================================== -// Generated by Vitis HLS v2024.2 -// Copyright 1986-2022 Xilinx, Inc. All Rights Reserved. -// Copyright 2022-2024 Advanced Micro Devices, Inc. All Rights Reserved. -// ============================================================== - -`timescale 1 ns / 1 ps - -(* CORE_GENERATION_INFO="StreamingEltwise_hls_0_StreamingEltwise_hls_0,hls_ip_2024_2,{HLS_INPUT_TYPE=cxx,HLS_INPUT_FLOAT=0,HLS_INPUT_FIXED=0,HLS_INPUT_PART=xc7z020-clg400-1,HLS_INPUT_CLOCK=5.000000,HLS_INPUT_ARCH=others,HLS_SYN_CLOCK=4.826000,HLS_SYN_LAT=10,HLS_SYN_TPT=none,HLS_SYN_MEM=0,HLS_SYN_DSP=0,HLS_SYN_FF=7,HLS_SYN_LUT=101,HLS_VERSION=2024_2}" *) - -module StreamingEltwise_hls_0 ( - ap_clk, - ap_rst_n, - in0_V_TVALID, - in1_V_TVALID, - out_V_TREADY, - in0_V_TDATA, - in0_V_TREADY, - in1_V_TDATA, - in1_V_TREADY, - out_V_TDATA, - out_V_TVALID -); - -parameter ap_ST_iter0_fsm_state1 = 1'd1; -parameter ap_ST_iter1_fsm_state2 = 2'd2; -parameter ap_ST_iter1_fsm_state0 = 2'd1; - -input ap_clk; -input ap_rst_n; -input in0_V_TVALID; -input in1_V_TVALID; -input out_V_TREADY; -input [7:0] in0_V_TDATA; -output in0_V_TREADY; -input [7:0] in1_V_TDATA; -output in1_V_TREADY; -output [15:0] out_V_TDATA; -output out_V_TVALID; - - reg ap_rst_n_inv; -reg [0:0] ap_CS_iter0_fsm; -wire ap_CS_iter0_fsm_state1; -reg ap_block_state1_pp0_stage0_iter0; -reg [1:0] ap_CS_iter1_fsm; -wire regslice_both_out_V_U_apdone_blk; -reg ap_block_state2_pp0_stage0_iter1; -wire ap_CS_iter1_fsm_state2; -wire [0:0] icmp_ln82_fu_110_p2; -reg ap_condition_exit_pp0_iter0_stage0; -reg ap_ready_int; -reg in0_V_TDATA_blk_n; -reg in1_V_TDATA_blk_n; -reg out_V_TDATA_blk_n; -reg [0:0] icmp_ln82_reg_133; -wire [0:0] icmp_ln82_reg_133_pp0_iter0_reg; -reg [2:0] i1_fu_50; -wire [2:0] i_fu_104_p2; -wire ap_loop_init; -reg [2:0] ap_sig_allocacmp_i1_load; -wire [3:0] in0_slice_channels_fu_81_p1; -wire [8:0] zext_ln20_fu_85_p1; -wire [8:0] zext_ln20_1_fu_89_p1; -wire [8:0] outElem_fu_93_p2; -reg [0:0] ap_NS_iter0_fsm; -reg [1:0] ap_NS_iter1_fsm; -reg ap_ST_iter0_fsm_state1_blk; -reg ap_ST_iter1_fsm_state2_blk; -wire ap_start_int; -wire ap_ready_sig; -wire ap_done_sig; -wire ap_continue_int; -wire regslice_both_in0_V_U_apdone_blk; -wire [7:0] in0_V_TDATA_int_regslice; -wire in0_V_TVALID_int_regslice; -reg in0_V_TREADY_int_regslice; -wire regslice_both_in0_V_U_ack_in; -wire regslice_both_in1_V_U_apdone_blk; -wire [7:0] in1_V_TDATA_int_regslice; -wire in1_V_TVALID_int_regslice; -reg in1_V_TREADY_int_regslice; -wire regslice_both_in1_V_U_ack_in; -wire [15:0] out_V_TDATA_int_regslice; -reg out_V_TVALID_int_regslice; -wire out_V_TREADY_int_regslice; -wire regslice_both_out_V_U_vld_out; -reg ap_condition_50; -wire ap_ce_reg; - -// power-on initialization -initial begin -#0 ap_CS_iter0_fsm = 1'd1; -#0 ap_CS_iter1_fsm = 2'd1; -#0 i1_fu_50 = 3'd0; -end - -StreamingEltwise_hls_0_flow_control_loop_pipe_no_ap_cont flow_control_loop_pipe_no_ap_cont_U( - .ap_clk(ap_clk), - .ap_rst(ap_rst_n_inv), - .ap_start(1'b1), - .ap_ready(ap_ready_sig), - .ap_done(ap_done_sig), - .ap_start_int(ap_start_int), - .ap_loop_init(ap_loop_init), - .ap_ready_int(ap_ready_int), - .ap_loop_exit_ready(ap_condition_exit_pp0_iter0_stage0), - .ap_loop_exit_done(1'b0), - .ap_continue_int(ap_continue_int), - .ap_done_int(1'b0) -); - -StreamingEltwise_hls_0_regslice_both #( - .DataWidth( 8 )) -regslice_both_in0_V_U( - .ap_clk(ap_clk), - .ap_rst(ap_rst_n_inv), - .data_in(in0_V_TDATA), - .vld_in(in0_V_TVALID), - .ack_in(regslice_both_in0_V_U_ack_in), - .data_out(in0_V_TDATA_int_regslice), - .vld_out(in0_V_TVALID_int_regslice), - .ack_out(in0_V_TREADY_int_regslice), - .apdone_blk(regslice_both_in0_V_U_apdone_blk) -); - -StreamingEltwise_hls_0_regslice_both #( - .DataWidth( 8 )) -regslice_both_in1_V_U( - .ap_clk(ap_clk), - .ap_rst(ap_rst_n_inv), - .data_in(in1_V_TDATA), - .vld_in(in1_V_TVALID), - .ack_in(regslice_both_in1_V_U_ack_in), - .data_out(in1_V_TDATA_int_regslice), - .vld_out(in1_V_TVALID_int_regslice), - .ack_out(in1_V_TREADY_int_regslice), - .apdone_blk(regslice_both_in1_V_U_apdone_blk) -); - -StreamingEltwise_hls_0_regslice_both #( - .DataWidth( 16 )) -regslice_both_out_V_U( - .ap_clk(ap_clk), - .ap_rst(ap_rst_n_inv), - .data_in(out_V_TDATA_int_regslice), - .vld_in(out_V_TVALID_int_regslice), - .ack_in(out_V_TREADY_int_regslice), - .data_out(out_V_TDATA), - .vld_out(regslice_both_out_V_U_vld_out), - .ack_out(out_V_TREADY), - .apdone_blk(regslice_both_out_V_U_apdone_blk) -); - -always @ (posedge ap_clk) begin - if (ap_rst_n_inv == 1'b1) begin - ap_CS_iter0_fsm <= ap_ST_iter0_fsm_state1; - end else begin - ap_CS_iter0_fsm <= ap_NS_iter0_fsm; - end -end - -always @ (posedge ap_clk) begin - if (ap_rst_n_inv == 1'b1) begin - ap_CS_iter1_fsm <= ap_ST_iter1_fsm_state0; - end else begin - ap_CS_iter1_fsm <= ap_NS_iter1_fsm; - end -end - -always @ (posedge ap_clk) begin - if ((1'b1 == ap_condition_50)) begin - i1_fu_50 <= i_fu_104_p2; - end -end - -always @ (posedge ap_clk) begin - if ((~((1'b1 == ap_block_state1_pp0_stage0_iter0) | ((1'b1 == ap_CS_iter1_fsm_state2) & (1'b1 == ap_block_state2_pp0_stage0_iter1))) & (1'b1 == ap_CS_iter0_fsm_state1))) begin - icmp_ln82_reg_133 <= icmp_ln82_fu_110_p2; - end -end - -always @ (*) begin - if ((1'b1 == ap_block_state1_pp0_stage0_iter0)) begin - ap_ST_iter0_fsm_state1_blk = 1'b1; - end else begin - ap_ST_iter0_fsm_state1_blk = 1'b0; - end -end - -always @ (*) begin - if ((1'b1 == ap_block_state2_pp0_stage0_iter1)) begin - ap_ST_iter1_fsm_state2_blk = 1'b1; - end else begin - ap_ST_iter1_fsm_state2_blk = 1'b0; - end -end - -always @ (*) begin - if ((~((1'b1 == ap_block_state1_pp0_stage0_iter0) | ((1'b1 == ap_CS_iter1_fsm_state2) & (1'b1 == ap_block_state2_pp0_stage0_iter1))) & (icmp_ln82_fu_110_p2 == 1'd1) & (1'b1 == ap_CS_iter0_fsm_state1))) begin - ap_condition_exit_pp0_iter0_stage0 = 1'b1; - end else begin - ap_condition_exit_pp0_iter0_stage0 = 1'b0; - end -end - -always @ (*) begin - if ((~((1'b1 == ap_block_state1_pp0_stage0_iter0) | ((1'b1 == ap_CS_iter1_fsm_state2) & (1'b1 == ap_block_state2_pp0_stage0_iter1))) & (1'b1 == ap_CS_iter0_fsm_state1))) begin - ap_ready_int = 1'b1; - end else begin - ap_ready_int = 1'b0; - end -end - -always @ (*) begin - if (((ap_loop_init == 1'b1) & (1'b1 == ap_CS_iter0_fsm_state1))) begin - ap_sig_allocacmp_i1_load = 3'd0; - end else begin - ap_sig_allocacmp_i1_load = i1_fu_50; - end -end - -always @ (*) begin - if ((1'b1 == ap_CS_iter0_fsm_state1)) begin - in0_V_TDATA_blk_n = in0_V_TVALID_int_regslice; - end else begin - in0_V_TDATA_blk_n = 1'b1; - end -end - -always @ (*) begin - if ((~((1'b1 == ap_block_state1_pp0_stage0_iter0) | ((1'b1 == ap_CS_iter1_fsm_state2) & (1'b1 == ap_block_state2_pp0_stage0_iter1))) & (1'b1 == ap_CS_iter0_fsm_state1))) begin - in0_V_TREADY_int_regslice = 1'b1; - end else begin - in0_V_TREADY_int_regslice = 1'b0; - end -end - -always @ (*) begin - if ((1'b1 == ap_CS_iter0_fsm_state1)) begin - in1_V_TDATA_blk_n = in1_V_TVALID_int_regslice; - end else begin - in1_V_TDATA_blk_n = 1'b1; - end -end - -always @ (*) begin - if ((~((1'b1 == ap_block_state1_pp0_stage0_iter0) | ((1'b1 == ap_CS_iter1_fsm_state2) & (1'b1 == ap_block_state2_pp0_stage0_iter1))) & (1'b1 == ap_CS_iter0_fsm_state1))) begin - in1_V_TREADY_int_regslice = 1'b1; - end else begin - in1_V_TREADY_int_regslice = 1'b0; - end -end - -always @ (*) begin - if (((1'b1 == ap_CS_iter1_fsm_state2) | (1'b1 == ap_CS_iter0_fsm_state1))) begin - out_V_TDATA_blk_n = out_V_TREADY_int_regslice; - end else begin - out_V_TDATA_blk_n = 1'b1; - end -end - -always @ (*) begin - if ((~((1'b1 == ap_block_state1_pp0_stage0_iter0) | ((1'b1 == ap_CS_iter1_fsm_state2) & (1'b1 == ap_block_state2_pp0_stage0_iter1))) & (1'b1 == ap_CS_iter0_fsm_state1))) begin - out_V_TVALID_int_regslice = 1'b1; - end else begin - out_V_TVALID_int_regslice = 1'b0; - end -end - -always @ (*) begin - case (ap_CS_iter0_fsm) - ap_ST_iter0_fsm_state1 : begin - ap_NS_iter0_fsm = ap_ST_iter0_fsm_state1; - end - default : begin - ap_NS_iter0_fsm = 'bx; - end - endcase -end - -always @ (*) begin - case (ap_CS_iter1_fsm) - ap_ST_iter1_fsm_state2 : begin - if (((1'b1 == ap_CS_iter0_fsm_state1) & (1'b0 == ap_block_state2_pp0_stage0_iter1) & (1'b0 == ap_block_state1_pp0_stage0_iter0))) begin - ap_NS_iter1_fsm = ap_ST_iter1_fsm_state2; - end else if (((1'b0 == ap_block_state2_pp0_stage0_iter1) & ((1'b0 == ap_CS_iter0_fsm_state1) | ((1'b1 == ap_CS_iter0_fsm_state1) & (1'b1 == ap_block_state1_pp0_stage0_iter0))))) begin - ap_NS_iter1_fsm = ap_ST_iter1_fsm_state0; - end else if (((icmp_ln82_reg_133_pp0_iter0_reg == 1'd1) & (1'b1 == ap_CS_iter1_fsm_state2) & (1'b0 == ap_block_state2_pp0_stage0_iter1))) begin - ap_NS_iter1_fsm = ap_ST_iter0_fsm_state1; - end else begin - ap_NS_iter1_fsm = ap_ST_iter1_fsm_state2; - end - end - ap_ST_iter1_fsm_state0 : begin - if ((~((1'b1 == ap_block_state1_pp0_stage0_iter0) | ((1'b1 == ap_CS_iter1_fsm_state2) & (1'b1 == ap_block_state2_pp0_stage0_iter1))) & (1'b1 == ap_CS_iter0_fsm_state1))) begin - ap_NS_iter1_fsm = ap_ST_iter1_fsm_state2; - end else begin - ap_NS_iter1_fsm = ap_ST_iter1_fsm_state0; - end - end - default : begin - ap_NS_iter1_fsm = 'bx; - end - endcase -end - -assign ap_CS_iter0_fsm_state1 = ap_CS_iter0_fsm[32'd0]; - -assign ap_CS_iter1_fsm_state2 = ap_CS_iter1_fsm[32'd1]; - -always @ (*) begin - ap_block_state1_pp0_stage0_iter0 = ((out_V_TREADY_int_regslice == 1'b0) | (in1_V_TVALID_int_regslice == 1'b0) | (in0_V_TVALID_int_regslice == 1'b0)); -end - -always @ (*) begin - ap_block_state2_pp0_stage0_iter1 = ((regslice_both_out_V_U_apdone_blk == 1'b1) | (out_V_TREADY_int_regslice == 1'b0)); -end - -always @ (*) begin - ap_condition_50 = (~((1'b1 == ap_block_state1_pp0_stage0_iter0) | ((1'b1 == ap_CS_iter1_fsm_state2) & (1'b1 == ap_block_state2_pp0_stage0_iter1))) & (1'b1 == ap_CS_iter0_fsm_state1)); -end - -always @ (*) begin - ap_rst_n_inv = ~ap_rst_n; -end - -assign i_fu_104_p2 = (ap_sig_allocacmp_i1_load + 3'd1); - -assign icmp_ln82_fu_110_p2 = ((ap_sig_allocacmp_i1_load == 3'd7) ? 1'b1 : 1'b0); - -assign icmp_ln82_reg_133_pp0_iter0_reg = icmp_ln82_reg_133; - -assign in0_V_TREADY = regslice_both_in0_V_U_ack_in; - -assign in0_slice_channels_fu_81_p1 = in0_V_TDATA_int_regslice[3:0]; - -assign in1_V_TREADY = regslice_both_in1_V_U_ack_in; - -assign outElem_fu_93_p2 = (zext_ln20_fu_85_p1 - zext_ln20_1_fu_89_p1); - -assign out_V_TDATA_int_regslice = outElem_fu_93_p2; - -assign out_V_TVALID = regslice_both_out_V_U_vld_out; - -assign zext_ln20_1_fu_89_p1 = in1_V_TDATA_int_regslice; - -assign zext_ln20_fu_85_p1 = in0_slice_channels_fu_81_p1; - -endmodule //StreamingEltwise_hls_0 diff --git a/finn_xsi/testcase/StreamingEltwise_hls_0_flow_control_loop_pipe_no_ap_cont.v b/finn_xsi/testcase/StreamingEltwise_hls_0_flow_control_loop_pipe_no_ap_cont.v deleted file mode 100644 index e3ff4d1e48..0000000000 --- a/finn_xsi/testcase/StreamingEltwise_hls_0_flow_control_loop_pipe_no_ap_cont.v +++ /dev/null @@ -1,103 +0,0 @@ -// ============================================================== -// Vitis HLS - High-Level Synthesis from C, C++ and OpenCL v2024.2 (64-bit) -// Tool Version Limit: 2024.11 -// Copyright 1986-2022 Xilinx, Inc. All Rights Reserved. -// Copyright 2022-2024 Advanced Micro Devices, Inc. All Rights Reserved. -// -// ============================================================== - -`timescale 1 ns / 1 ps - -module StreamingEltwise_hls_0_flow_control_loop_pipe_no_ap_cont( - ap_clk, - ap_rst, - ap_start, - ap_ready, - ap_done, - ap_start_int, - ap_ready_int, - ap_done_int, - ap_continue_int, - ap_loop_init, - ap_loop_exit_ready, - ap_loop_exit_done -); - -input ap_clk; -input ap_rst; - -//Block level handshake with outside loop -input ap_start; -output ap_ready; -output ap_done; - -//Block level handshake with loop body -output ap_start_int; -input ap_ready_int; -input ap_done_int; -output ap_continue_int; - -//Init live in variables -output ap_loop_init; -reg ap_loop_init; -reg ap_done; -reg ap_done_cache; - -//Exit signal from loop body -input ap_loop_exit_ready; -input ap_loop_exit_done; - -// power-on initialization -initial begin -#0 ap_loop_init = 1'b1; -#0 ap_done_cache = 1'b0; -end - -assign ap_start_int = ap_start; - -assign ap_continue_int = 1'b1; - -assign ap_ready = ap_loop_exit_ready; - -//ap_loop_init is valid for the first II -//of the first loop run so as to enable -//the init block ops which are pushed into -//the first state of the pipeline region -always @ (posedge ap_clk) -begin - if (ap_rst == 1'b1) begin - ap_loop_init <= 1'b1; - end else if(ap_loop_exit_ready == 1'b1) begin - ap_loop_init <= 1'b1; - end else if(ap_ready_int == 1'b1) begin - ap_loop_init <= 1'b0; - end -end - -// if no ap_continue port and current module is not top module, -// ap_done handshakes with ap_start. Internally, flow control sends out -// ap_conintue_int = 1'b1 so the ap_done_int is asserted high for 1 clock cycle. -// ap_done_cache is used to record ap_done_int, and de-assert if ap_start_int -// is asserted, so DUT can start the next run -always @(posedge ap_clk) -begin - if (ap_rst == 1'b1) begin - ap_done_cache <= 1'b0; - end else if (ap_done_int == 1'b1) begin - ap_done_cache <= 1'b1; - end else if (ap_start_int == 1'b1) begin - ap_done_cache <= 1'b0; - end -end - -// if no ap_continue port and current module is not top module, ap_done handshakes with ap_start -always @(*) -begin - if ((ap_done_int == 1'b1) || ((ap_done_cache == 1'b1) && (ap_start_int == 1'b0))) begin - ap_done = 1'b1; - end else begin - ap_done = 1'b0; - end -end - -endmodule diff --git a/finn_xsi/testcase/StreamingEltwise_hls_0_regslice_both.v b/finn_xsi/testcase/StreamingEltwise_hls_0_regslice_both.v deleted file mode 100644 index c2e16007cc..0000000000 --- a/finn_xsi/testcase/StreamingEltwise_hls_0_regslice_both.v +++ /dev/null @@ -1,110 +0,0 @@ -// ============================================================== -// Generated by Vitis HLS v2024.2 -// Copyright 1986-2022 Xilinx, Inc. All Rights Reserved. -// Copyright 2022-2024 Advanced Micro Devices, Inc. All Rights Reserved. -// ============================================================== -`timescale 1ns/1ps - -module StreamingEltwise_hls_0_regslice_both -#(parameter - DataWidth = 8 -) ( - // system signals - input wire ap_clk, - input wire ap_rst, - // slave side - input wire [DataWidth-1:0] data_in, - input wire vld_in, - output wire ack_in, - // master side - output wire [DataWidth-1:0] data_out, - output wire vld_out, - input wire ack_out, - output wire apdone_blk); - //------------------------Parameter---------------------- - // state - localparam [1:0] - ZERO = 2'b10, - ONE = 2'b11, - TWO = 2'b01; - //------------------------Local signal------------------- - reg [DataWidth-1:0] data_p1 = {DataWidth{1'b0}}; - reg [DataWidth-1:0] data_p2 = {DataWidth{1'b0}}; - wire load_p1; - wire load_p2; - wire load_p1_from_p2; - reg ack_in_t = 1'b0; - reg [1:0] state = 2'b00; - reg [1:0] next; - //------------------------Body--------------------------- - assign ack_in = ack_in_t; - assign data_out = data_p1; - assign vld_out = state[0]; - assign apdone_blk = (state == ONE && ~ack_out) || (state == TWO); - - assign load_p1 = (state == ZERO && vld_in) || - (state == ONE && vld_in && ack_out) || - (state == TWO && ack_out); - assign load_p2 = vld_in & ack_in; - assign load_p1_from_p2 = (state == TWO); - - // data_p1 - always @(posedge ap_clk) begin - if (load_p1) begin - if (load_p1_from_p2) - data_p1 <= data_p2; - else - data_p1 <= data_in; - end - end - - // data_p2 - always @(posedge ap_clk) begin - if (load_p2) data_p2 <= data_in; - end - - // ack_in_t - always @(posedge ap_clk) begin - if (ap_rst) - ack_in_t <= 1'b0; - else if (state == ZERO) - ack_in_t <= 1'b1; - else if (state == ONE && next == TWO) - ack_in_t <= 1'b0; - else if (state == TWO && next == ONE) - ack_in_t <= 1'b1; - end - - // state - always @(posedge ap_clk) begin - if (ap_rst) - state <= ZERO; - else - state <= next; - end - - // next - always @(*) begin - case (state) - ZERO: - if (vld_in & ack_in) - next = ONE; - else - next = ZERO; - ONE: - if (~vld_in & ack_out) - next = ZERO; - else if (vld_in & ~ack_out) - next = TWO; - else - next = ONE; - TWO: - if (ack_out) - next = ONE; - else - next = TWO; - default: - next = ZERO; - endcase - end -endmodule From dc46c9c521a5bc70b29f23994b850c5fc4840105 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 14 Jan 2026 17:20:06 +0100 Subject: [PATCH 041/170] Revert previous deleted module --- finn_xsi/finn_xsi/sim_engine.py | 409 ++++++++++++++++++++++++++++++++ 1 file changed, 409 insertions(+) create mode 100644 finn_xsi/finn_xsi/sim_engine.py diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py new file mode 100644 index 0000000000..1627809932 --- /dev/null +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -0,0 +1,409 @@ +############################################################################# +# Copyright (C) 2025, Advanced Micro Devices, Inc. +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# +# @brief SimEngine abstraction for running FINN task in simulated hardware. +# @author Thomas B. Preußer +# @author Yaman Umuroglu +############################################################################# + +import xsi + + +class SimEngine: + # ------------------------------------------------------------------------ + # Life Cycle + def __init__(self, kernel, design, log=None, wdb=None): + top = xsi.Design(xsi.Kernel(kernel), design, log, wdb) + clk = top.getPort("ap_clk") + clk2x = top.getPort("ap_clk2x") + for p in top.ports(): + if p.isInput(): + p.clear().write_back() + + def cycle(updates): + # Rising Edge + clk.set(1).write_back() + if clk2x is not None: + clk2x.set(1).write_back() + # Updates after Active Edge + top.run(1) + for port, update in updates.items(): + port.set_hexstr(update).write_back() + + # Edges inactive on interface & finish Cycle + if clk2x is None: + top.run(4999) + clk.set(0).write_back() + top.run(5000) + else: + top.run(2499) + clk2x.set(0).write_back() + top.run(2500) + clk.set(0).write_back() + clk2x.set(1).write_back() + top.run(2500) + clk2x.set(0).write_back() + top.run(2500) + + self.top = top + self.cycle = cycle + self.ticks = 0 + self.tasks = [] + self.watchdogs = [] + + # ------------------------------------------------------------------------ + # Utility + def get_bus_port(self, bus, suffix): + port = self.top.getPort(bus + "_" + suffix.lower()) + return port if port is not None else self.top.getPort(bus + "_" + suffix.upper()) + + # ------------------------------------------------------------------------ + # Simulation Setup + + # Task Scheduling + def enlist(self, task): + self.tasks.append(task) + + # Watchdog Generation + def create_watchdog(self, name, timeout): + class Watchdog: + def __init__(self, name, timeout): + self.name = name + self.ticks = 0 + self.timeout = timeout + + def __bool__(self): + return self.ticks < self.timeout + + def __repr__(self): + return self.name + + def __call__(self): + self.ticks += 1 + + def reset(self): + self.ticks = 0 + + ret = Watchdog(name, timeout) + self.watchdogs.append(ret) + return ret + + def remove_watchdog(self, watchdog): + self.watchdogs.remove(watchdog) + + # ------------------------------------------------------------------------ + # Execution + def run(self, cycles=None): + "Run all tasks to completion or until a watchdog triggers." + timeout = None if cycles is None else self.create_watchdog("Run Timeout", cycles) + + woken = [] + while len(self.tasks) > 0 and len(woken := [w for w in self.watchdogs if not w]) == 0: + # Process Tasks and Collect Updates to Write Back + tasks = [] + updates = {} + + # Execute Cycle + self.ticks += 1 + strong = False + for task in self.tasks: + # Tasks read signals and derive updates to schedule for after the clock cycle + ret = task(self) + if ret is not None: + updates.update(ret) + tasks.append(task) + strong |= bool(task) + self.cycle(updates) + + # Step Watchdogs + for watchdog in self.watchdogs: + watchdog() + + # Update to Unfinished Tasks + self.tasks = tasks if strong else [] + + # Return List of Woken Watchdogs + if timeout is not None: + self.remove_watchdog(timeout) + return woken + + # ------------------------------------------------------------------------ + # Standard Tasks + def do_reset(self): + "Schedule a reset sequence." + + class Reset: + def __init__(self, top): + self.cnt = 0 + self.rst_n = top.getPort("ap_rst_n") + + def __call__(self, sim): + cnt = self.cnt + self.cnt = cnt + 1 + + if cnt == 0: + return {self.rst_n: "0"} + if cnt < 16: + return {} + if cnt == 16: + return {self.rst_n: "1"} + return None + + self.enlist(Reset(self.top)) + + def stream_input(self, istream, values, throttle=(float("inf"), 0)): + "Stream all values from the passed iterator into the specified stream." + + class InputStreamer: + def __init__(self, top, istream, values, throttle): + self.vld = top.get_bus_port(istream, "tvalid") + self.rdy = top.get_bus_port(istream, "tready") + self.dat = top.get_bus_port(istream, "tdata") + self.values = values + + self.throttle = throttle + self.await_tick = 0 + self.count_txns = throttle[0] + + def __call__(self, sim): + vld = self.vld.as_bool() + if vld and not self.rdy.read().as_bool(): + return {} + + # Track Transaction Count + if vld: + self.count_txns += 1 + + # Proceed according to Throttling Rate + if self.count_txns < self.throttle[0] or not sim.ticks < self.await_tick: + # Try Feed + val = next(self.values, None) + if val is None: + # Unset vld, then exit + return {self.vld: "0", self.dat: "0"} if vld else None + + # Feed next Value + ret = {self.dat: val} + if not vld: + ret[self.vld] = "1" + if self.count_txns == self.throttle[0]: + self.count_txns = 0 + self.await_tick = sim.ticks + self.throttle[1] + return ret + + # Stall Feed + return {self.vld: "0", self.dat: "0"} if vld else {} + + self.enlist(InputStreamer(self, istream, values, throttle)) + + def collect_output(self, ostream, size, watchdog=None): + "Collect size outputs from the specified stream into the returned iterable buffer." + + class OutputCollector: + def __init__(self, top, ostream, size, watchdog): + self.size = size + self.vld = top.get_bus_port(ostream, "tvalid") + self.rdy = top.get_bus_port(ostream, "tready") + self.dat = top.get_bus_port(ostream, "tdata") + self.buf = [] + self.watchdog = watchdog + + def __iter__(self): + return iter(self.buf) + + def __call__(self, sim): + if self.rdy.as_bool(): + if self.vld.read().as_bool(): + # Have a n Output Transaction + if self.watchdog is not None: + self.watchdog.reset() + val = self.dat.read().as_hexstr() + self.buf.append(val) + if len(self.buf) == size: + return {self.rdy: "0"} + return {} + + if len(self.buf) < size: + return {self.rdy: "1"} + return None + + ret = OutputCollector(self, ostream, size, watchdog) + self.enlist(ret) + return ret + + def trace_stream(self, stream): + "Monitor an AXI-Stream and trace its transaction activity" + + class StreamTracer: + def __init__(self, sim, stream): + self.vld = sim.get_bus_port(stream, "tvalid") + self.rdy = sim.get_bus_port(stream, "tready") + self.trace = "" + + def __call__(self, sim): + self.trace += ( + "1" if self.vld.read().as_bool() and self.rdy.read().as_bool() else "0" + ) + return {} + + def __bool__(self): + return False + + def __str__(self): + return self.trace + + ret = StreamTracer(self, stream) + self.enlist(ret) + return ret + + def write_axilite(self, m_axilite, writes): + "Execute writes specified as a list of (addr, val)-tuples to AXI-lite interface" + + class AxiLiteWriter: + INIT = 0 + FEED = 1 + COOL = 2 + + def __init__(self, top, m_axilite, writes): + self.awready = top.get_bus_port(m_axilite, "awready") + self.awvalid = top.get_bus_port(m_axilite, "awvalid") + self.awaddr = top.get_bus_port(m_axilite, "awaddr") + self.wready = top.get_bus_port(m_axilite, "wready") + self.wvalid = top.get_bus_port(m_axilite, "wvalid") + self.wdata = top.get_bus_port(m_axilite, "wdata") + wstrb = top.get_bus_port(m_axilite, "wstrb") + wstrb.set_binstr("1" * wstrb.width()).write_back() + self.bready = top.get_bus_port(m_axilite, "bready") + self.bvalid = top.get_bus_port(m_axilite, "bvalid") + self.bresp = top.get_bus_port(m_axilite, "bresp") + self.writes = writes + self.state = self.INIT + self.pending = 0 + + def __call__(self, sim): + # Termination + if self.state == self.COOL and not self.bready.as_bool(): + return None + + ret = {} + + # Always Monitor Completions + if self.state == self.INIT: + ret[self.bready] = "1" + self.state = self.FEED + + if self.bvalid.read().as_bool(): + if self.pending < 1: + print("Received spurious completion on", self.bresp.name()) + else: + self.pending -= 1 + if self.pending == 0 and self.state == self.COOL: + ret[self.bready] = "0" + + if self.bresp.read().as_unsigned() != 0: + print("Received error indication on", self.bresp.name()) + + # Transaction Feed + if self.state == self.FEED: + step = True + + # Check for busy address feed + avld = self.awvalid.as_bool() + aclr = False + if avld: + if self.awready.read().as_bool(): + aclr = True + else: + step = False + + # Check for busy data feed + wvld = self.wvalid.as_bool() + wclr = False + if wvld: + if self.wready.read().as_bool(): + wclr = True + else: + step = False + + # Proceed with next Write + if step: + addr, val = next(self.writes, (None, None)) + if addr is not None: + ret[self.awaddr] = f"{addr:x}" + ret[self.wdata] = val + if not avld: + ret[self.awvalid] = "1" + if not wvld: + ret[self.wvalid] = "1" + self.pending += 1 + return ret + if not self.pending: + ret[self.bready] = "0" + self.state = self.COOL + + # Deassert completed feed + if aclr: + ret[self.awvalid] = "0" + if wclr: + ret[self.wvalid] = "0" + + return ret + + self.enlist(AxiLiteWriter(self, m_axilite, writes)) + + def read_axilite(self, m_axilite, reads): + class AxiLiteReader: + def __init__(self, top, m_axilite, reads): + self.arready = top.get_bus_port(m_axilite, "arready") + self.arvalid = top.get_bus_port(m_axilite, "arvalid") + self.araddr = top.get_bus_port(m_axilite, "araddr") + self.rready = top.get_bus_port(m_axilite, "rready") + self.rvalid = top.get_bus_port(m_axilite, "rvalid") + self.rdata = top.get_bus_port(m_axilite, "rdata") + self.reads = reads + self.pending = [] + self.draining = False + self.replies = {} + + def __call__(self, sim): + ret = {} + + # Address Stream Feed: assert self.draining when done + if not self.draining: + if self.arready.read().as_bool() or not self.arvalid.as_bool(): + addr = next(self.reads, None) + if addr is None: + ret[self.arvalid] = "0" + self.draining = True + else: + ret[self.arvalid] = "1" + ret[self.araddr] = f"{addr:x}" + self.pending.append(addr) + + # Reply Collection + if not self.rready.as_bool(): + # Termination + if self.draining: + return None + # Activation + ret[self.rready] = "1" + elif self.rvalid.read().as_bool(): + assert len(self.pending) > 0, "Spurious reply." + self.replies[self.pending.pop(0)] = self.rdata.read().as_hexstr() + if self.draining and len(self.pending) == 0: + ret[self.rready] = "0" + + return ret + + def __iter__(self): + return iter(self.replies) + + def __getitem__(self, addr): + return self.replies[addr] + + ret = AxiLiteReader(self, m_axilite, reads) + self.enlist(ret) + return ret From 77a745175d938e16480e391eabc0fa20012a7691 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 15 Jan 2026 11:50:28 +0100 Subject: [PATCH 042/170] Fixed issue with MemStreamSupport.calc_wmem() --- src/finn/custom_op/fpgadataflow/memstream.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/finn/custom_op/fpgadataflow/memstream.py b/src/finn/custom_op/fpgadataflow/memstream.py index 48aabf509c..9fead1a0e7 100644 --- a/src/finn/custom_op/fpgadataflow/memstream.py +++ b/src/finn/custom_op/fpgadataflow/memstream.py @@ -16,9 +16,11 @@ class MemStreamSupport(HWCustomOp, ABC): def calc_tmem(self) -> int: """Abstract method to calculate threshold memory size.""" - @abstractmethod def calc_wmem(self) -> int: - """Abstract method to calculate weight memory size.""" + """Abstract method to calculate weight memory size. + The default implementation raises NotImplementedError because + some subclasses dont implement calc_wmem.""" + raise NotImplementedError() def generate_hdl_memstream(self, fpgapart: str, pumped_memory: int = 0) -> None: """Generate verilog code for memstream component. From e4ce540f4f95c9045df8b3b2e2482d2a4bf5b789 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 16 Jan 2026 15:13:03 +0100 Subject: [PATCH 043/170] Added isolated sim target, fixed std::format version incompatability. --- finn_xsi/finn_xsi/CMakeLists.txt | 27 +++-- .../finn_xsi/IsolatedSimulationBackend.cpp | 5 + .../finn_xsi/include/IsolatedSimulation.hpp | 107 ++++++++++++++++++ finn_xsi/finn_xsi/include/Simulation.hpp | 13 ++- finn_xsi/finn_xsi/src/SocketServer.cpp | 19 ++-- finn_xsi/finn_xsi/unittests/CMakeLists.txt | 4 +- 6 files changed, 148 insertions(+), 27 deletions(-) create mode 100644 finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp create mode 100644 finn_xsi/finn_xsi/include/IsolatedSimulation.hpp diff --git a/finn_xsi/finn_xsi/CMakeLists.txt b/finn_xsi/finn_xsi/CMakeLists.txt index 3cc6736bbc..5f4daa4c12 100644 --- a/finn_xsi/finn_xsi/CMakeLists.txt +++ b/finn_xsi/finn_xsi/CMakeLists.txt @@ -100,28 +100,33 @@ list(APPEND CMAKE_MESSAGE_INDENT " ") #indent +1 check_include(FIFOSIM_IPO "InterproceduralOptimization" InterproceduralOptimization.cmake) list(POP_BACK CMAKE_MESSAGE_INDENT) #indent -1 -# Main +# Collect source files file(GLOB_RECURSE CORE_SRC src/*.cpp) -add_executable(LayerSimulationBackend LayerSimulationBackend.cpp ${CORE_SRC}) # For JSON writing include(FetchContent) FetchContent_Declare(json URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz) FetchContent_MakeAvailable(json) -# Add boost for IPC +# Add boost for PO find_package(Boost COMPONENTS program_options REQUIRED) -target_include_directories(LayerSimulationBackend SYSTEM PUBLIC ${Boost_INCLUDE_DIRS}) -# Include the rtlsim wrapper directory itself -target_include_directories(LayerSimulationBackend PUBLIC "${CMAKE_BINARY_DIR}") +# Build the simulation library +add_library(SimulationBackendLib SHARED ${CORE_SRC}) +target_include_directories(SimulationBackendLib PUBLIC "${CMAKE_BINARY_DIR}") # Include the rtlsim wrapper directory itself +target_include_directories(SimulationBackendLib PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") # Add xsim includes +target_include_directories(SimulationBackendLib PUBLIC "include") +target_link_libraries(SimulationBackendLib PUBLIC fifosim::options nlohmann_json::nlohmann_json Threads::Threads OpenMP::OpenMP_CXX -ldl -lrt) -# Add xsim includes -target_include_directories(LayerSimulationBackend PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") -target_include_directories(LayerSimulationBackend PUBLIC "include") +# Build the executable for connected simulations +add_executable(LayerSimulationBackend LayerSimulationBackend.cpp) +target_include_directories(LayerSimulationBackend SYSTEM PUBLIC ${Boost_INCLUDE_DIRS}) +target_link_libraries(LayerSimulationBackend SimulationBackendLib Boost::program_options) -# Link libraries -target_link_libraries(LayerSimulationBackend fifosim::options nlohmann_json::nlohmann_json Threads::Threads OpenMP::OpenMP_CXX Boost::program_options -ldl -lrt) +# Build the executable for isolated simulations +add_executable(IsolatedSimulationBackend IsolatedSimulationBackend.cpp) +target_include_directories(IsolatedSimulationBackend SYSTEM PUBLIC ${Boost_INCLUDE_DIRS}) +target_link_libraries(IsolatedSimulationBackend SimulationBackendLib Boost::program_options) OPTION(ENABLE_UNITTESTS "Enable unittests" OFF) if(${ENABLE_UNITTESTS}) diff --git a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp new file mode 100644 index 0000000000..848c2797d4 --- /dev/null +++ b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp @@ -0,0 +1,5 @@ +#include + +int main() { + return 0; +} diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp new file mode 100644 index 0000000000..f2603a3302 --- /dev/null +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -0,0 +1,107 @@ +#include + + +template +class IsolatedSimulation : public Simulation { + enum class LogType {READY, VALID}; + std::ofstream readyLog; // Input side + std::ofstream validLog; // Output side + + /** + * Write CSV style headers to the files + **/ + void writeLogHeaders() { + readyLog << "totalCycles,inputCycles,targetInputCycles"; + for (S_AXIS_Control& s: this->istreams) { + readyLog << "," << s.name; + } + readyLog << std::endl; + validLog << "totalCycles,outputCycles,targetOutputCycles" << std::endl; + for (M_AXIS_Control& s : this->ostreams) { + validLog << "," << s.name; + } + validLog << std::endl; + } + + inline void writeLogEntryReady (size_t cyclesTotal, size_t cycles, + size_t targetCycles, std::span axis) { + readyLog << cyclesTotal << "," << cycles << "," << targetCycles; + for (S_AXIS_Control& s : axis) { readyLog << "," << s.isReady(); } + } + + inline void writeLogEntryValid(size_t cyclesTotal, size_t cycles, + size_t targetCycles, std::span axis) { + validLog << cyclesTotal << "," << cycles << "," << targetCycles; + for (M_AXIS_Control& s : axis) { validLog << "," << s.isValid(); } + } + + /** + * For the given streams check which has the largest job size, and return a tuple + * (stream_index, job_size) for that stream. + **/ + std::tuple getLargestTxnsStream(std::span axis) { + size_t l = 0; + size_t idx = 0; + for (size_t i = 0; i < axis.size(); i++) { + if (axis[i].job_size > l) { + l = axis[i].job_size; + idx = i; + } + } + return std::make_tuple(idx, l); + } + + public: + IsolatedSimulation( + const std::string& kernel_lib, + const std::string& design_lib, + const char* xsim_log_file, + const char* trace_file, + std::array _istream_descs, + std::array _ostream_descs, + const std::string readyLogPath, + const std::string validLogPath + ) : Simulation( + kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs + ), readyLog(readyLogPath), validLog(validLogPath) {} + + void simulate() { + this->clearPorts(); + this->reset(); + + // For split branches calculate the max of all incoming/outgoing stream job sizes, and which stream it is + auto [inputCyclesTarget, inputLargestStreamIndex] = getLargestTxnsStream(this->istreams); + auto [outputCyclesTarget, outputLargestStreamIndex] = getLargestTxnsStream(this->ostreams); + size_t inputCycles = 0; + size_t outputCycles = 0; + + // Set ports + for (S_AXIS_Control& s : this->istreams) { + s.setValid(true); + } + for (M_AXIS_Control& s : this->ostreams) { + s.setReady(true); + } + + // TODO: Check that components dont behave differently when 0 data is sent through them + // (only relevant for behavioural simulation.) + for (size_t cycles = 0; inputCycles < inputCyclesTarget && outputCycles < outputCyclesTarget; ++cycles) { + // Ready log ends when the input is completely consumed + if (inputCycles < inputCyclesTarget) { + writeLogEntryReady(cycles, inputCycles, inputCyclesTarget, this->istreams); + } + + // Valid log until the end + writeLogEntryValid(cycles, outputCycles, outputCyclesTarget, this->ostreams); + + // Only if the largest stream transaction is done, is the input/output complete + if (inputCycles < inputCyclesTarget && this->istreams[inputLargestStreamIndex].isReady()) { + ++inputCycles; + } + if (outputCycles < outputCyclesTarget && this->ostreams[outputLargestStreamIndex].isValid()) { + ++outputCycles; + } + this->clk.toggleClk(); + } + } +}; diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 72cb6ff4b8..7a97819b11 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -203,14 +202,16 @@ class SingleNodeSimulation : public Simulation= OStreamsSize) { - throw std::out_of_range(std::format("FIFO index {} out of range (max: {})", index, OStreamsSize - 1)); + auto error = "FIFO index " + + std::to_string(index) + + " out of range (max: " + + std::to_string(OStreamsSize - 1) + ")"; + throw std::out_of_range(error); } fifo[index].setMaxSize(depth); } diff --git a/finn_xsi/finn_xsi/src/SocketServer.cpp b/finn_xsi/finn_xsi/src/SocketServer.cpp index 9aa73970a2..e5fafe3997 100644 --- a/finn_xsi/finn_xsi/src/SocketServer.cpp +++ b/finn_xsi/finn_xsi/src/SocketServer.cpp @@ -4,7 +4,6 @@ #include #include -#include #include #include @@ -36,7 +35,7 @@ std::optional SocketServer::initialize() { // Create socket server_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (server_fd < 0) { - return std::format("Failed to create socket: {}", strerror(errno)); + return "Failed to create socket: " + std::string(strerror(errno)); } // Remove existing socket file @@ -48,14 +47,14 @@ std::optional SocketServer::initialize() { strncpy(addr.sun_path, socket_path.c_str(), sizeof(addr.sun_path) - 1); if (bind(server_fd, reinterpret_cast(&addr), sizeof(addr)) < 0) { - auto error = std::format("Failed to bind socket: {}", strerror(errno)); + std::string error = "Failed to bind socket: " + std::string(strerror(errno)); close_fd(server_fd); return error; } // Listen if (listen(server_fd, 1) < 0) { - auto error = std::format("Failed to listen on socket: {}", strerror(errno)); + std::string error = "Failed to listen on socket: " + std::string(strerror(errno)); close_fd(server_fd); return error; } @@ -63,7 +62,7 @@ std::optional SocketServer::initialize() { // Accept connection client_fd = accept(server_fd, nullptr, nullptr); if (client_fd < 0) { - auto error = std::format("Failed to accept connection: {}", strerror(errno)); + std::string error = "Failed to accept connection: " + std::string(strerror(errno)); close_fd(server_fd); return error; } @@ -84,7 +83,7 @@ std::optional SocketServer::receive_message() { if (bytes_read == 0) { std::cerr << "Connection closed by client" << std::endl; } else { - std::cerr << std::format("Failed to read message length: {}", strerror(errno)) << std::endl; + std::cerr << "Failed to read message length: " << strerror(errno) << std::endl; } return std::nullopt; } @@ -95,7 +94,7 @@ std::optional SocketServer::receive_message() { while (total_read < length) { const ssize_t n = read(client_fd, buffer.data() + total_read, length - total_read); if (n <= 0) { - std::cerr << std::format("Failed to read message data: {}", strerror(errno)) << std::endl; + std::cerr << "Failed to read message data: " << strerror(errno) << std::endl; return std::nullopt; } total_read += static_cast(n); @@ -104,7 +103,7 @@ std::optional SocketServer::receive_message() { try { return json::parse(buffer); } catch (const json::exception& e) { - std::cerr << std::format("Failed to parse JSON: {}", e.what()) << std::endl; + std::cerr << "Failed to parse JSON: " << e.what() << std::endl; return std::nullopt; } } @@ -121,7 +120,7 @@ void SocketServer::send_message(const json& message) { // Send length prefix const ssize_t bytes_written = write(client_fd, &length, sizeof(length)); if (bytes_written != sizeof(length)) { - std::cerr << std::format("Failed to write message length: {}", strerror(errno)) << std::endl; + std::cerr << "Failed to write message length: " << strerror(errno) << std::endl; return; } @@ -130,7 +129,7 @@ void SocketServer::send_message(const json& message) { while (total_written < length) { const ssize_t n = write(client_fd, msg_str.data() + total_written, length - total_written); if (n <= 0) { - std::cerr << std::format("Failed to write message data: {}", strerror(errno)) << std::endl; + std::cerr << "Failed to write message data: " << strerror(errno) << std::endl; return; } total_written += static_cast(n); diff --git a/finn_xsi/finn_xsi/unittests/CMakeLists.txt b/finn_xsi/finn_xsi/unittests/CMakeLists.txt index a9189d01bb..1392b9d203 100644 --- a/finn_xsi/finn_xsi/unittests/CMakeLists.txt +++ b/finn_xsi/finn_xsi/unittests/CMakeLists.txt @@ -14,7 +14,7 @@ FetchContent_MakeAvailable(googletest) # Add FIFO unit tests add_executable(FIFO_test FIFO_test.cpp ${CORE_SRC}) -target_link_libraries(FIFO_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt) +target_link_libraries(FIFO_test PRIVATE nlohmann_json::nlohmann_json GTest::gtest_main Threads::Threads -ldl -lrt) target_include_directories(FIFO_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) target_include_directories(FIFO_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") @@ -25,7 +25,7 @@ target_include_directories(InterSimulationInterface_test PRIVATE ${CMAKE_CURRENT # Add Integration tests (FIFO + InterSimulationInterface) add_executable(Integration_test Integration_test.cpp ${CORE_SRC}) -target_link_libraries(Integration_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt) +target_link_libraries(Integration_test PRIVATE nlohmann_json::nlohmann_json GTest::gtest_main Threads::Threads -ldl -lrt) target_include_directories(Integration_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ${Boost_INCLUDE_DIRS}) target_include_directories(Integration_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") From ab2cfc4d90d6006667a38fb8f29df99a81361abb Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 21 Jan 2026 18:25:57 +0100 Subject: [PATCH 044/170] Isolated node simulation runs; added simulation_build.py; bugfixes --- .gitignore | 2 + .../finn_xsi/IsolatedSimulationBackend.cpp | 123 ++- .../finn_xsi/include/IsolatedSimulation.hpp | 183 +++-- .../transformation/fpgadataflow/simulation.py | 691 ++--------------- .../fpgadataflow/simulation_build.py | 704 ++++++++++++++++++ .../fpgadataflow/simulation_controller.py | 143 +++- 6 files changed, 1173 insertions(+), 673 deletions(-) create mode 100644 src/finn/transformation/fpgadataflow/simulation_build.py diff --git a/.gitignore b/.gitignore index 2854862d7b..c3e086d43a 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,8 @@ poetry.lock **/.cache **/build **/_deps +finn_xsi/finn_xsi/unittests/*.cmake +finn_xsi/finn_xsi/unittests/Makefile # Package files diff --git a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp index 848c2797d4..999fdde7c2 100644 --- a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp @@ -1,5 +1,126 @@ #include +#include +#include +#include +#include -int main() { +namespace po = boost::program_options; + + + +int main(int argc, const char* argv[]) { + // Parse CLI options + po::options_description desc{"Options"}; + desc.add_options()("socket,s", po::value(), "Unix domain socket path for IPC"); + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + po::notify(vm); + + // Create simulation + IsolatedSimulation sim( + RTLSimConfig::kernel_libname, + RTLSimConfig::design_libname, + "xsim_log_file.txt", + "trace_file.txt", + RTLSimConfig::istream_descs, + RTLSimConfig::ostream_descs, + "readylog.txt", + "validlog.txt" + ); + + + + // Create controller + if (vm.count("socket")) { + const std::string socket_path = vm["socket"].as(); + std::cout << "Initializing socket server at: " << socket_path << std::endl; + std::cout.flush(); + + SocketServer server(socket_path); + if (auto error = server.initialize(); error.has_value()) { + std::cerr << "Failed to initialize socket server: " << *error << std::endl; + std::cerr.flush(); + return 1; + } + + std::cout << "Socket server initialized, waiting for commands..." << std::endl; + std::cout.flush(); + + // Preparing thread variable + std::optional simThread = std::nullopt; + std::mutex simMutex; + + // Command processing loop + while (true) { + // Read message + auto request = server.receive_message(); + if (!request.has_value()) { + std::cout << "Connection closed or error occurred" << std::endl; + break; + } + + // Process message + std::size_t cycles = 0; + std::string command = (*request)["command"]; + if (command == "start") { + std::cout << "Starting simulation" << std::endl; + if (!simThread.has_value()) { + simThread = std::jthread([&sim, &simMutex, &cycles](std::stop_token stop) { + { + std::lock_guard guard(simMutex); + sim.simulate(true); + } + std::cout << "Simulation initialized. Going into main loop." << std::endl; + { + std::lock_guard guard(simMutex); + sim.simulate(false); + std::cout << "Executed first cycle." << std::endl; + std::cout << "Status: " << sim.getStatus() << std::endl; + std::cout << "Is running: " << sim.isRunning() << std::endl; + } + while (!stop.stop_requested()) { + std::lock_guard guard(simMutex); + if (cycles % 1000 == 0) { + std::cout << cycles << " " << sim.getStatus() << std::endl; + } + sim.simulate(false); + ++cycles; + } + }); + simThread->join(); + } else { + std::lock_guard guard(simMutex); + sim.resume(); + } + } else if (command == "stop") { + std::cout << "Stopping simulation." << std::endl; + std::lock_guard guard(simMutex); + sim.halt(); + if (simThread.has_value()) { + simThread->request_stop(); + } + } else if (command == "pause") { + std::cout << "Pausing simulation." << std::endl; + std::lock_guard guard(simMutex); + if (simThread.has_value()) { + simThread->request_stop(); + } + } else if (command == "status") { + std::cout << "Sending status update." << std::endl; + std::lock_guard guard(simMutex); + server.send_message(sim.getStatus()); + } else { + std::cout << "Unknown command " << command << std::endl; + std::cerr << "Unknown command " << command << std::endl; + } + + // Exit if stop command received + if ((*request)["command"] == "stop") { + break; + } + } + } else { + throw std::runtime_error("Socket path not provided. Socket communication is required."); + } return 0; } diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index f2603a3302..70c3733494 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -1,4 +1,6 @@ #include +#include +#include "SocketServer.h" template @@ -6,51 +8,110 @@ class IsolatedSimulation : public Simulation enum class LogType {READY, VALID}; std::ofstream readyLog; // Input side std::ofstream validLog; // Output side + std::vector inJobSizes; + std::vector outJobSizes; + /** * Write CSV style headers to the files **/ void writeLogHeaders() { - readyLog << "totalCycles,inputCycles,targetInputCycles"; + readyLog << "totalCycles,inputCycles,doubled_targetInputCycles"; for (S_AXIS_Control& s: this->istreams) { readyLog << "," << s.name; } readyLog << std::endl; - validLog << "totalCycles,outputCycles,targetOutputCycles" << std::endl; + validLog << "totalCycles,outputCycles,doubled_targetOutputCycles" << std::endl; for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.name; } validLog << std::endl; } - inline void writeLogEntryReady (size_t cyclesTotal, size_t cycles, - size_t targetCycles, std::span axis) { - readyLog << cyclesTotal << "," << cycles << "," << targetCycles; - for (S_AXIS_Control& s : axis) { readyLog << "," << s.isReady(); } - } - - inline void writeLogEntryValid(size_t cyclesTotal, size_t cycles, - size_t targetCycles, std::span axis) { - validLog << cyclesTotal << "," << cycles << "," << targetCycles; - for (M_AXIS_Control& s : axis) { validLog << "," << s.isValid(); } - } /** * For the given streams check which has the largest job size, and return a tuple * (stream_index, job_size) for that stream. **/ - std::tuple getLargestTxnsStream(std::span axis) { + std::tuple getLargestTxnsStream(std::vector& jobSizes) { size_t l = 0; size_t idx = 0; - for (size_t i = 0; i < axis.size(); i++) { - if (axis[i].job_size > l) { - l = axis[i].job_size; + for (size_t i = 0; i < jobSizes.size(); i++) { + if (jobSizes[i] > l) { + l = jobSizes[i]; idx = i; } } return std::make_tuple(idx, l); } + class SimState { + public: + bool running; + size_t inputCyclesDone; + size_t inputCyclesTarget; + size_t inputLargestStreamIndex; + size_t outputCyclesDone; + size_t outputCyclesTarget; + size_t outputLargestStreamIndex; + size_t totalCycles; + + SimState(IsolatedSimulation& sim) { + reset(sim); + } + void reset(IsolatedSimulation& sim) { + totalCycles = 0; + inputCyclesDone = 0; + outputCyclesDone = 0; + running = false; + auto largestIn = sim.getLargestTxnsStream(sim.inJobSizes); + auto largestOut = sim.getLargestTxnsStream(sim.outJobSizes); + inputCyclesTarget = std::get<1>(largestIn) * 2; + inputLargestStreamIndex = std::get<0>(largestIn); + outputCyclesTarget = std::get<1>(largestOut) * 2; + outputLargestStreamIndex = std::get<0>(largestOut); + std::cout << "In Job Sizes: "; + for (auto js : sim.inJobSizes) { + std::cout << js << " "; + } + std::cout << std::endl; + std::cout << "IO cycle targets: " << inputCyclesTarget << ", " << outputCyclesTarget << std::endl; + } + inline bool inputCyclesProcessed() { return inputCyclesDone >= inputCyclesTarget; } + inline bool outputCyclesProcessed() { return outputCyclesDone >= outputCyclesTarget; } + inline bool allCyclesProcessed() { return inputCyclesProcessed() && outputCyclesProcessed(); } + inline bool isRunning() { return running; } + void setRunning(bool v) { running = v; } + std::string getCycleStateInput() { return std::to_string(totalCycles) + "," + std::to_string(inputCyclesDone) + "," + std::to_string(inputCyclesTarget); } + std::string getCycleStateOutput() { return std::to_string(totalCycles) + "," + std::to_string(outputCyclesDone) + "," + std::to_string(outputCyclesTarget); } + json getStatus() { + json j; + if (!running && allCyclesProcessed()) { + j["state"] = "done"; + } else { + j["state"] = running ? "running" : "halted"; + } + j["totalCycles"] = totalCycles; + j["inputCyclesDone"] = inputCyclesDone; + j["inputCyclesTarget"] = inputCyclesTarget; + j["outputCyclesDone"] = outputCyclesDone; + j["outputCyclesTarget"] = outputCyclesTarget; + return j; + } + }; + + SimState simState; + + inline void writeLogEntryReady () { + readyLog << simState.getCycleStateInput(); + for (S_AXIS_Control& s : this->istreams) { readyLog << "," << s.isReady(); } + } + + inline void writeLogEntryValid() { + validLog << simState.getCycleStateOutput(); + for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.isValid(); } + } + public: IsolatedSimulation( const std::string& kernel_lib, @@ -63,45 +124,73 @@ class IsolatedSimulation : public Simulation const std::string validLogPath ) : Simulation( kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs - ), readyLog(readyLogPath), validLog(validLogPath) {} + ), readyLog(readyLogPath), validLog(validLogPath), simState(*this) { + inJobSizes.resize(_istream_descs.size()); + outJobSizes.resize(_ostream_descs.size()); + std::transform( + _istream_descs.begin(), + _istream_descs.end(), + inJobSizes.begin(), + [](StreamDescriptor& s) { return s.job_size; } + ); + std::transform( + _ostream_descs.begin(), + _ostream_descs.end(), + outJobSizes.begin(), + [](StreamDescriptor& s) { return s.job_size; } + ); + } - void simulate() { - this->clearPorts(); - this->reset(); + json getStatus() { + return simState.getStatus(); + } - // For split branches calculate the max of all incoming/outgoing stream job sizes, and which stream it is - auto [inputCyclesTarget, inputLargestStreamIndex] = getLargestTxnsStream(this->istreams); - auto [outputCyclesTarget, outputLargestStreamIndex] = getLargestTxnsStream(this->ostreams); - size_t inputCycles = 0; - size_t outputCycles = 0; + void halt() { + simState.setRunning(false); + } - // Set ports - for (S_AXIS_Control& s : this->istreams) { - s.setValid(true); - } - for (M_AXIS_Control& s : this->ostreams) { - s.setReady(true); - } + void resume() { + simState.setRunning(true); + } - // TODO: Check that components dont behave differently when 0 data is sent through them - // (only relevant for behavioural simulation.) - for (size_t cycles = 0; inputCycles < inputCyclesTarget && outputCycles < outputCyclesTarget; ++cycles) { - // Ready log ends when the input is completely consumed - if (inputCycles < inputCyclesTarget) { - writeLogEntryReady(cycles, inputCycles, inputCyclesTarget, this->istreams); - } + bool isRunning() { return simState.isRunning(); } - // Valid log until the end - writeLogEntryValid(cycles, outputCycles, outputCyclesTarget, this->ostreams); + /*** + * Simulate a single cycle + ***/ + void simulate(bool restart = false) { + if (restart) { + simState.reset(*this); + simState.setRunning(true); + std::cout << "Sim set to running: " << simState.isRunning() << std::endl; + std::cout << "Target input/output cycles: " << simState.inputCyclesTarget << ", " << simState.outputCyclesTarget << std::endl; + this->clearPorts(); + this->reset(); + for (S_AXIS_Control& s : this->istreams) { + s.setValid(true); + } + for (M_AXIS_Control& s : this->ostreams) { + s.setReady(true); + } + } + if (!simState.isRunning()) { + std::cout << "Simulation not running! Send \"start\" command first." << std::endl; + return; + } + if (!simState.allCyclesProcessed()) { + writeLogEntryReady(); + writeLogEntryValid(); - // Only if the largest stream transaction is done, is the input/output complete - if (inputCycles < inputCyclesTarget && this->istreams[inputLargestStreamIndex].isReady()) { - ++inputCycles; + if (!simState.inputCyclesProcessed() && this->istreams[simState.inputLargestStreamIndex].isReady()) { + ++simState.inputCyclesDone; } - if (outputCycles < outputCyclesTarget && this->ostreams[outputLargestStreamIndex].isValid()) { - ++outputCycles; + if (!simState.outputCyclesProcessed() && this->ostreams[simState.outputLargestStreamIndex].isValid()) { + ++simState.outputCyclesDone; } this->clk.toggleClk(); + ++simState.totalCycles; + } else { + simState.setRunning(false); } } }; diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index b34c48bf66..ebc2afee38 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,604 +1,29 @@ """Manage FINN simulation variants.""" - -import finn_xsi.adapter as finnxsi import json import math -import numpy as np -import onnx -import os -import psutil -import shlex -import sys import time -from concurrent.futures import Future, ThreadPoolExecutor -from contextlib import nullcontext -from copy import deepcopy -from enum import Enum -from onnx import NodeProto, TensorProto +from onnx.onnx_ml_pb2 import NodeProto from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames -from qonnx.transformation.infer_shapes import InferShapes -from random import Random -from subprocess import CalledProcessError -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP -from finn.transformation.fpgadataflow.insert_dwc import InsertDWC from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO from finn.transformation.fpgadataflow.prepare_ip import PrepareIP -from finn.transformation.fpgadataflow.simulation_controller import NodeConnectedSimulationController +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType +from finn.transformation.fpgadataflow.simulation_controller import ( + NodeConnectedSimulationController, + NodeIsolatedSimulationController, +) from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -from finn.util.basic import launch_process_helper, make_build_dir +from finn.util.basic import make_build_dir from finn.util.exception import FINNInternalError, FINNUserError -from finn.util.logging import DisabledLoggingConsole, ThreadsafeProgressDisplay, log - -if TYPE_CHECKING: - from collections.abc import Sequence - - -""" -Classes in this module: - SimulationType: Determines the type of simulation to be executed. - SimulationBuilder: Performs multiple steps to build a simulation binary. - Simulation: Builds a simulation of the given type and interacts with - the binary, returning the results. - - RunLayerParallelSimulation: Create a Simulation object for parallel simulation. - ApplyFIFOSizes: Read the output JSON from the simulation and apply the - found sizes to the FIFOs in the model. - -""" - - -class SimulationType(str, Enum): - # Individual node simulations connected by IPC - NODE_BASED_CONNECTED = "NODE_BASED_CONNECTED" - - # Individual node simulations, isolated. E.g. for analysis purposes - NODE_BASED_ISOLATED = "NODE_BASED_ISOLATED" - - # Legacy method (deprecated) - COMPLETE_DESIGN = "COMPLETE_DESIGN" - - -class SimulationBuilder: - """Build simulations in FINN.""" - - def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: - """Create a new simulation instance.""" - self.model = model - self.fpgapart = fpgapart - self.clk_ns = clk_ns - self.progress_bar = ThreadsafeProgressDisplay([], [], []) - - def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: - """Return a modelwrapper that has only the specified node. - - Args: - by_node: If int, used as the index of the specified node. If string, assumed to be - the name of the node. - - Returns: - ModelWrapper: The isolated-node modelwrapper. - """ - # Find the node - index = 0 - if type(by_node) is int: - if by_node < 0 or by_node >= len(self.model.graph.node): - raise FINNInternalError( - f"Cannot isolate node index {by_node}. Model has" - f"{len(self.model.graph.node)} nodes." - ) - index = by_node - elif type(by_node) is str: - node_name = self.model.get_node_from_name(by_node) - if node_name is None: - raise FINNInternalError(f"Cannot isolate node {by_node}. No such node found.") - index = [n.name for n in self.model.graph.node].index(cast("str", node_name)) - elif type(by_node) is NodeProto: - try: - index = self.model.graph.node.index(by_node) - except Exception as e: - raise FINNInternalError(f"Node {by_node.name} not found in the model.") from e - else: - raise FINNInternalError( - f"Cannot find node to isolate: {by_node}. Specify either " - f"the index (int), node name (str) or the object itself " - f"(NodeProto)." - ) - - # Copy model to modify - node_model = deepcopy(self.model) - - # Remove any other node - # TODO: Refactor this following section - for i, node in enumerate(self.model.graph.node): - if i != index: - node_model.graph.node.remove(node) - target_op = getCustomOp(node_model.graph.node[0]) - if not isinstance(target_op, HWCustomOp): - raise FINNInternalError( - f"Node {node_model.graph.node[0].name} is not a HWCustomOp, cannot " - f"isolate for simulation." - ) - inp = onnx.helper.make_tensor_value_info( - "inp", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_folded_input_shape()) - ) - inp_dummy_out = onnx.helper.make_tensor_value_info( # noqa - "inp_dummy_out", - TensorProto.FLOAT, - cast("Sequence[int]", target_op.get_folded_input_shape()), - ) - outp = onnx.helper.make_tensor_value_info( # noqa - "outp", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_output_shape()) - ) - outp_dummy_out = onnx.helper.make_tensor_value_info( - "outp_dummy_out", - TensorProto.FLOAT, - cast("Sequence[int]", target_op.get_normal_output_shape()), - ) - input_dummy_node = onnx.helper.make_node( - "RemoveDataPath_rtl", - inputs=["inp"], - outputs=["inp_dummy_out"], - domain="finn.custom_op.fpgadataflow.rtl", - backend="fpgadataflow", - folded_shape=target_op.get_folded_input_shape(), - normal_shape=target_op.get_normal_input_shape(), - dataType=target_op.get_input_datatype().name, - name=node_model.graph.node[0].name + "_input_dummy", - ) - output_dummy_node = onnx.helper.make_node( - "RemoveDataPath_rtl", - inputs=["outp"], - outputs=["outp_dummy_out"], - domain="finn.custom_op.fpgadataflow.rtl", - backend="fpgadataflow", - folded_shape=target_op.get_folded_output_shape(), - normal_shape=target_op.get_normal_output_shape(), - dataType=target_op.get_output_datatype().name, - name=node_model.graph.node[0].name + "_output_dummy", - ) - - node_model.graph.node.insert(0, input_dummy_node) - node_model.graph.node.append(output_dummy_node) - - # Remove old io - for _ in range(len(node_model.graph.node[1].input)): - node_model.graph.node[1].input.pop() - for _ in range(len(node_model.graph.node[1].output)): - node_model.graph.node[1].output.pop() - - # Set new io - node_model.graph.node[1].input.append("inp_dummy_out") - node_model.graph.node[1].output.append("outp") - - # Remove graph io - for _ in range(len(node_model.graph.input)): - node_model.graph.input.pop() - for _ in range(len(node_model.graph.output)): - node_model.graph.output.pop() - - # Set new graph io - node_model.graph.input.append(inp) - node_model.graph.output.append(outp_dummy_out) - - return node_model - - def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, int]: - """Return the stream descriptions for the given model for the C++ sim config header. - - Used by for example _build_single_node_simulation(). - - Returns: - tuple[str, int, str, int]: Strings of stream descriptions together with - their count (in, out) - """ - # Get IO iterations required - instream_iters = [] - outstream_iters = [] - for top_inp in model.graph.input: - iname = top_inp.name - first_node = model.find_consumer(iname) - assert first_node is not None, "Failed to find consumer for " + iname - top_ind = list(first_node.input).index(iname) - ishape_folded = getCustomOp(first_node).get_folded_input_shape(ind=top_ind) - instream_iters.append(int(np.prod(ishape_folded[:-1]))) - for top_out in model.graph.output: - oname = top_out.name - last_node = model.find_producer(oname) - assert last_node is not None, "Failed to find producer for " + oname - top_ind = list(last_node.output).index(oname) - oshape_folded = getCustomOp(last_node).get_folded_output_shape(ind=top_ind) - outstream_iters.append(int(np.prod(oshape_folded[:-1]))) - interface_names = model.get_metadata_prop("vivado_stitch_ifnames") - if interface_names is None: - raise FINNInternalError( - f"{model}: Could not find stitched-IP interface names. " - f"Did you run IP Stitching first?" - ) - - # TODO: Copied from rtlsim_exec_cppxsi. Remove eval(). - interface_names = eval(interface_names) - if "aximm" in interface_names.keys() and interface_names["aximm"] != []: - raise FINNInternalError( - f"{model}: CPP XSI Sim does not know how to handle full " - f"AXI MM interfaces: {interface_names['aximm']}" - ) - instream_names = [x[0] for x in interface_names["s_axis"]] - outstream_names = [x[0] for x in interface_names["m_axis"]] - - # Format stream descriptions - def _format_descr_name(s: str) -> str: - for old, new in [("[", ""), ("]", ""), ("(", "{"), (")", "}"), ("'", '"')]: - s = s.replace(old, new) - return s - - # TODO: Change this since we don't have throttling - instream_descrs = [ - (instream_names[i], instream_iters[i], instream_iters[i]) - for i in range(len(instream_names)) - ] - instream_descrs_str = _format_descr_name(str(instream_descrs)) - - outstream_descrs = [ - (outstream_names[i], outstream_iters[i], outstream_iters[i]) - for i in range(len(outstream_names)) - ] - outstream_descrs_str = _format_descr_name(str(outstream_descrs)) - return instream_descrs_str, len(instream_names), outstream_descrs_str, len(outstream_names) - - def _create_sim_so( - self, - model: ModelWrapper, - top_module_name: str, - vivado_stitched_proj: Path, - build_dir: Path | None, - debug: bool, - ) -> tuple[Path, Path]: - """Create a new RTLSim .so file. If one exists already it is used. - - Returns: - tuple[Path, Path]: Return sim_base and sim_rel. - """ - rtlsim_so_str = model.get_metadata_prop("rtlsim_so") - if (rtlsim_so_str is None) or not Path(rtlsim_so_str).exists(): - all_verilog_srcs = ( - (Path(vivado_stitched_proj) / "all_verilog_srcs.txt").read_text().split() - ) - sim_dir = ( - make_build_dir(f"rtlsim_{model.graph.node[0].name}_") - if build_dir is None - else build_dir - ) - sim_base, sim_rel = finnxsi.compile_sim_obj( - top_module_name, all_verilog_srcs, str(sim_dir), debug=debug - ) - rtlsim_so = Path(sim_base) / Path(sim_rel) - model.set_metadata_prop("rtlsim_so", str(rtlsim_so)) - else: - sim_base, sim_rel = cast("str", rtlsim_so_str.split("xsim.dir")) - sim_rel = "xsim.dir" + sim_rel - return Path(sim_base), Path(sim_rel) - - def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: - """Compile an existing RTLSIM directory. Requires _create_sim_so to be run before. Expects - rtlsim_config.hpp to be templated already. - - Returns: - Path: Path to the executable shell script to run the binary - """ - finnxsi_dir = os.environ["FINN_XSI"] - # Running CMake first - cmake_call = f"{sys.executable} -m cmake -S {finnxsi_dir} -B {sim_base}" - log.info(f"Running cmake on RTLSIM Wrapper in {sim_base}") - try: - launch_process_helper( - shlex.split(cmake_call), - cwd=finnxsi_dir, - print_stdout=not silent, - print_stderr=not silent, - proc_env=os.environ.copy(), - ) - except CalledProcessError as e: - raise FINNInternalError(f"Failed to run cmake in {sim_base}") from e - self.progress_bar.update("CMake") - - # Calling make to actually build the simulation - makefile = Path(sim_base) / "Makefile" - if not makefile.exists(): - raise FINNInternalError(f"Failed to create Makefile in {sim_base}!") - try: - launch_process_helper( - ["make"], - proc_env=os.environ.copy(), - cwd=sim_base, - print_stdout=not silent, - print_stderr=not silent, - ) - except CalledProcessError as e: - raise FINNInternalError(f"Failed to create executable in {sim_base}!") from e - - # TODO: Fix name for general rtlsim - simulation_executable = Path(sim_base) / "LayerSimulationBackend" - if not simulation_executable.exists(): - raise FINNInternalError(f"Make call in {sim_base} failed!") - self.progress_bar.update("Make") - return simulation_executable - - def _template_rtlsim_config( - self, - model: ModelWrapper, - sim_base: Path, - node_name: str, - previous_node_name: str | None, - node_index: int, - total_nodes: int, - timeout_cycles: int, - top_module_name: str, - trace_file: str | None, - ) -> Path: - """Template finn_xsi/finn_xsi/rtlsim_config.hpp.template with the correct values and - return the templated file. - """ - finnxsi_dir = os.environ["FINN_XSI"] - # Prepare the C++ driver config template - ( - instream_descrs_str, - len_instreams, - outstream_descrs_str, - len_outstreams, - ) = self._get_stream_descriptions(model) - template_dict = { - "TIMEOUT_CYCLES": timeout_cycles, - # name of the top-level HDL module - "TOP_MODULE_NAME": top_module_name, - # top-level AXI stream descriptors - "ISTREAM_DESC": instream_descrs_str, - "ISTREAM_LEN": len_instreams, - "OSTREAM_DESC": outstream_descrs_str, - "OSTREAM_LEN": len_outstreams, - # control tracing and trace filename - "TRACE_FILE": "std::nullopt" if trace_file is None else f'"{trace_file}"', - # sim kernel .so to use (depends on Vivado version) - "SIMKERNEL_SO": finnxsi.get_simkernel_so(), - # log file for xsi (not the sim driver) - "XSIM_LOG_FILE": '"xsi.log"', - # Node name in case of single-node simulation - "NODE_NAME": node_name, - # Previous node name (for single node simulation) - "PREVIOUS_NODE_NAME": ( - "std::nullopt" if previous_node_name is None else f'"{previous_node_name}"' - ), - "NODE_INDEX": node_index, - "TOTAL_NODES": total_nodes, - } - - fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" - fsim_config = fifosim_config_fname.read_text() - for key, val in template_dict.items(): - fsim_config = fsim_config.replace(f"@{key}@", str(val)) - - # Write the config to the simulation directory - rtlsim_config = Path(sim_base) / "rtlsim_config.hpp" - rtlsim_config.write_text(fsim_config) - return rtlsim_config - - def build_single_node_simulation( - self, - node_name: str, - node_model: ModelWrapper, - node_index: int, - total_nodes: int, - previous_node_name: str | None, - build_dir: Path | None, - timeout_cycles: int = 0, - silent: bool = False, - ) -> Path: - """Build the simulation binary for a single node. - - This can be used both by the connected node-by-node sim and the isolated node sim. - - Much of this is from the rtlsim_exec.py in core/ - - Args: - node_name: Despite the fact that we receive an isolated node model, we can still - manually pass a node name. This is useful to give unique names (e.g. for IPC) - node_model: The single node ModelWrapper to build the simulation from. - node_index: The index of the simulated node. Used to determine whether a node shares IO - with successors or predecessors. - total_nodes: The total number of nodes in the complete design. - previous_node_name: Required by the connected simulation. In the simulation binary this - is used to get access to the correct shared memory segment between - this node and the previous one. - build_dir: If given, use this directory for building the simulation. Otherwise one is - created from the nodes name. - timeout_cycles: Number of cycles until simulation timeout. When set to 0 (default), no - timeout is given. - silent: If True, silences the Cmake and make output (including stderr) - - Returns: - Path: The path to the simulation binary (shell script). - """ - # TODO: Check if something is an output node instead of checking the node index - # TODO: Requires changes in the C++ code as well - - # Sanity checks (2 Dummy nodes + 1 target node) - if len(node_model.graph.node) != 3: - raise FINNUserError( - "Cannot create single-node simulation for a model with more than " - "1 node. Make sure to pass the ModelWrapper containing only" - "the relevant node." - ) - - # Check that the relevant data exists - wrapper_filename = node_model.get_metadata_prop("wrapper_filename") - if wrapper_filename is None or not Path(wrapper_filename).exists(): - raise FINNUserError( - f"Call CreateStitchedIP prior to building " - f"the simulation for {node_name}. " - f"wrapper_filename is set to {wrapper_filename}!" - ) - - vivado_stitched_proj = node_model.get_metadata_prop("vivado_stitch_proj") - if vivado_stitched_proj is None or not Path(vivado_stitched_proj).exists(): - raise FINNUserError( - f"Call CreateStitchedIP prior to building " - f"the simulation for {node_name}. (vivado_stitch_proj not set!)" - ) - - trace_file = cast("str | None", node_model.get_metadata_prop("rtlsim_trace")) - debug = not (trace_file is None or trace_file == "") - - # Get the module name and path - top_module_file = Path(wrapper_filename).resolve().absolute() - top_module_name = top_module_file.name.strip(".v") - - # Build the simulation .so and save it in the "rtlsim_so" metadata prop - sim_base, _ = self._create_sim_so( - node_model, top_module_name, Path(vivado_stitched_proj), build_dir, debug - ) - - # Fill out the simulation config header - _ = self._template_rtlsim_config( - node_model, - sim_base, - node_name, - previous_node_name, - node_index, - total_nodes, - timeout_cycles, - top_module_name, - trace_file, - ) - - # Building the whole simulation - return self._compile_simulation(sim_base, silent).absolute() - - def _get_randomized_names(self, model: ModelWrapper, suffix_length: int = 5) -> dict[int, str]: - """Add a randomized suffix to every name in the model. Used to avoid interference with - previous or parallel running IPC simulations.""" - rand = Random() - rand.seed() - return { - i: model.graph.node[i].name - + "".join( - rand.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(suffix_length) - ) - for i in range(len(model.graph.node)) - } - - def _build_simulation_node_connected( - self, workers: int, with_live_display: bool, functional_sim: bool - ) -> dict[int, Path]: - """Build all nodes in the model in parallel, as isolated simulations, ready for usage in - an IPC connected simulation chain. - - Args: - workers: Number of parallel workers to use. - with_live_display: If True, display the building progress in a rich progress bar. - - Returns: - Dict of executables that start the simulation of the given nodes, - indexed by the node-index. These are in their respective FINN_TMP - directories. - """ - - def _build( - node_name: str, - node_index: int, - total_nodes: int, - prev_node_name: str | None, - build_dir: Path, - ) -> Any: - nodemodel = self._isolated_node_model(node_index) - nodemodel = nodemodel.transform(InferShapes()) - nodemodel = nodemodel.transform(PrepareIP(self.fpgapart, self.clk_ns)) - nodemodel = nodemodel.transform( - CreateStitchedIP(self.fpgapart, self.clk_ns, functional_simulation=functional_sim) - ) - self.progress_bar.update("StitchedIP") - return self.build_single_node_simulation( - node_name, - nodemodel, - node_index, - total_nodes, - prev_node_name, - build_dir, - silent=with_live_display, - ) - - # Create randomized names to avoid clashes with old IPC shared memory - randomized_names = self._get_randomized_names(self.model) - - # TODO: Currently ignores workers argument - total_nodes = len(self.model.graph.node) - futures: dict[int, Future] = {} - - # Build sims in parallel - synth_workers = max( - 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 20) - ) # 20GB per synthesis - if not functional_sim: - # When not having to do synthesis, the build is not memory bottlenecked and - # can be executed as parallel as possible - synth_workers = int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))) - - # Build (stitched IP, cmake, make) all sims in parallel and return paths to - # the compiled executables - with DisabledLoggingConsole(), self.progress_bar if with_live_display else nullcontext(): - self.progress_bar.progress.console.log( - f"Building simulations using {int(synth_workers)} workers.." - ) - with ThreadPoolExecutor(max_workers=synth_workers) as pool: - for i in range(total_nodes): - futures[i] = pool.submit( - _build, - randomized_names[i], - i, - total_nodes, - randomized_names[i - 1] if i >= 1 else None, # type: ignore - Path(make_build_dir(f"rtlsim_{randomized_names[i]}_")), - ) - return {i: future.result() for i, future in futures.items()} - - def build_simulation( - self, simtype: SimulationType, workers: int, with_live_display: bool, functional_sim: bool - ) -> dict[int, Path]: - """Build a simulation of the given type, return the resulting executable. - - Args: - simtype: Simulation type to build. - workers: Number of workers to use in parallel. - Normally set by the Simulation() class automatically. - with_live_display: If True, display a live progress-bar. - functional_sim: If True, use functional simulation (faster but takes some time to build) - """ - match simtype: - case SimulationType.NODE_BASED_CONNECTED: - node_count = len(self.model.graph.node) - self.progress_bar = ThreadsafeProgressDisplay( - ["StitchedIP", "CMake", "Make"], - [node_count] * 3, - [ - "[bold blue](1)[/bold blue] Creating stitched IPs", - "[bold blue](2)[/bold blue] Configuring project with CMake", - "[bold blue](3)[/bold blue] Building simulation binaries", - ], - ) - return self._build_simulation_node_connected( - workers, with_live_display, functional_sim - ) - case SimulationType.NODE_BASED_ISOLATED: - raise NotImplementedError() - case SimulationType.COMPLETE_DESIGN: - raise FINNUserError(f"Simulation method {simtype} is deprecated!") +from finn.util.logging import DisabledLoggingConsole, log class Simulation: @@ -610,44 +35,41 @@ class Simulation: def __init__( self, model: ModelWrapper, + simulation_type: SimulationType, fpgapart: str, clk_ns: float, functional_sim: bool, - simulation_type: SimulationType, workers: int | None = None, ) -> None: - """Create a new simulation instance. If workers is None, NUM_DEFAULT_WORKERS are used.""" + """Create a new simulation instance. Read simulation binary paths + from the simulation_binaries metadata prop field.""" + self.simulation_type = simulation_type self.model = model - self.workers = int(os.environ["NUM_DEFAULT_WORKERS"]) if workers is None else workers - self.functional_sim = functional_sim - self.fpgapart = fpgapart - self.clk_ns = clk_ns - # TODO: Caching of existing simulations - # Prepare the model for simulation - with DisabledLoggingConsole() as console: # noqa - with console.status("Preparing model for the simulation step..."): - self._prepare_model() - self.builder = SimulationBuilder(self.model, fpgapart, clk_ns) + sim_binaries = self.model.get_metadata_prop("simulation_binaries") - sys.stdout = sys.stdout.console - sys.stderr = sys.stderr.console - - self.simulation_type = simulation_type - self.binaries = self.builder.build_simulation( - simulation_type, - self.workers, - with_live_display=True, - functional_sim=self.functional_sim, + if sim_binaries is None: + raise FINNUserError( + "No field simulation_binaries found in the model. Make " + "sure to run the BuildSimulation transformation beforehand." + ) + sim_binaries: list[Path] = [Path(p) for p in str(sim_binaries).split("\n")] + if len(sim_binaries) != len(self.model.graph.node): + raise FINNUserError( + "The number of found simulation binaries does not match the number " + "of nodes in the graph. Make sure to run BuildSimulation just " + "before." + ) + if any(not p.exists() for p in sim_binaries): + raise FINNUserError( + "Simulation binary data points to invalid paths. " "Please rerun BuildSimulation." + ) + # TODO: Currently we have to recompile even if we just + # TODO: called BuildSimulation in the step before + # (However this only compiles, it should NOT stitch the IPs again) + self.model = self.model.transform( + BuildSimulation(fpgapart, clk_ns, functional_sim, simulation_type, workers) ) - - def _prepare_model(self) -> None: - """Execute some preparation transformations on the model.""" - self.model = self.model.transform(InsertDWC()) - self.model = self.model.transform(SpecializeLayers(self.fpgapart)) - self.model = self.model.transform(GiveUniqueNodeNames()) - self.model = self.model.transform(GiveReadableTensorNames()) - self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) - self.model = self.model.transform(HLSSynthIP()) + self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} def simulate(self, *args: Any, **kwargs: Any) -> Any: """Run the built simulation and return its results. This function can always be called @@ -674,7 +96,7 @@ def simulate_node_connected( f"{self.simulation_type}" ) names = [node.name for node in self.model.graph.node] - initial_depth = [[depth]] * len(self.binaries) if isinstance(depth, int) else depth + initial_depth: Any = [[depth]] * len(self.binaries) if isinstance(depth, int) else depth # Run simulation start = time.time() @@ -705,13 +127,22 @@ def simulate_node_connected( return data, merged_data.get("timeout_occurred", False) def simulate_node_isolated(self) -> None: + """Simulate isolated nodes.""" if self.simulation_type != SimulationType.NODE_BASED_ISOLATED: raise FINNInternalError( f"Called simulation function 'simulate_node_isolated' " f"does not match provided simulation type " f"{self.simulation_type}" ) - raise NotImplementedError() + names = [node.name for node in self.model.graph.node] + with DisabledLoggingConsole() as console: + controller = NodeIsolatedSimulationController( + len(self.binaries), names, list(self.binaries.values()), console, 0.1, False + ) + _ = controller.run() + + # TODO: Implement algorithm + raise NotImplementedError() class RunLayerParallelSimulation(Transformation): # noqa @@ -724,6 +155,7 @@ def __init__( vivado_ram_style: str = "auto", quality_of_results: str = "default", ) -> None: + """Run layer parallel simulations.""" super().__init__() self.fpgapart = fpgapart self.clk_ns = clk_ns @@ -733,12 +165,13 @@ def __init__( self.quality_of_results = quality_of_results def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Run layer parallel simulations.""" sim = Simulation( model, + SimulationType.NODE_BASED_CONNECTED, self.fpgapart, self.clk_ns, - self.cfg.functional_simulation, - SimulationType.NODE_BASED_CONNECTED, + self.cfg.functional_sim, ) model = sim.model # TODO:clean up @@ -1257,6 +690,28 @@ def calculate_srl16e_depth_range(luts: int, bitwidth: int) -> tuple[int, int]: return (min_depth, max_depth) +class RunLayerIsolatedSimulation(Transformation): # noqa + def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: + """Run isolated layer simulations.""" + super().__init__() + self.fpgapart = fpgapart + self.clk_ns = clk_ns + self.functional_sim = functional_sim + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Run isolated layer simulations.""" + sim = Simulation( + model, + SimulationType.NODE_BASED_ISOLATED, + self.fpgapart, + self.clk_ns, + self.functional_sim, + ) + # TODO + _ = sim.simulate_node_isolated() + return model, False + + class ApplyFIFOSizes(Transformation): """Apply a FIFO sizing configuration to the model. If not existing, inserts FIFOs beforehand.""" diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py new file mode 100644 index 0000000000..5129fd2319 --- /dev/null +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -0,0 +1,704 @@ +"""Build FINN Simulations.""" +import finn_xsi.adapter as finnxsi +import numpy as np +import onnx +import os +import psutil +import shlex +import subprocess +import sys +from concurrent.futures import Future, ThreadPoolExecutor +from contextlib import nullcontext +from copy import deepcopy +from enum import Enum +from onnx import NodeProto, TensorProto +from pathlib import Path +from qonnx.core.modelwrapper import ModelWrapper +from qonnx.custom_op.registry import getCustomOp +from qonnx.transformation.base import Transformation +from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames +from qonnx.transformation.infer_shapes import InferShapes +from random import Random +from subprocess import CalledProcessError +from typing import TYPE_CHECKING, Any, cast + +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP +from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP +from finn.transformation.fpgadataflow.insert_dwc import InsertDWC +from finn.transformation.fpgadataflow.prepare_ip import PrepareIP +from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers +from finn.util.basic import launch_process_helper, make_build_dir +from finn.util.exception import FINNInternalError, FINNUserError +from finn.util.logging import DisabledLoggingConsole, ThreadsafeProgressDisplay, log + +if TYPE_CHECKING: + from collections.abc import Sequence + + +class SimulationType(str, Enum): + """Type of simulation.""" + + # Individual node simulations connected by IPC + NODE_BASED_CONNECTED = "NODE_BASED_CONNECTED" + + # Individual node simulations, isolated. E.g. for analysis purposes + NODE_BASED_ISOLATED = "NODE_BASED_ISOLATED" + + # Legacy method (deprecated) + COMPLETE_DESIGN = "COMPLETE_DESIGN" + + +class SimulationBuilder: + """Build simulations in FINN.""" + + def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: + """Create a new simulation instance.""" + self.model = model + self.fpgapart = fpgapart + self.clk_ns = clk_ns + self.progress_bar = ThreadsafeProgressDisplay([], [], []) + + def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: + """Return a modelwrapper that has only the specified node. + + Args: + by_node: If int, used as the index of the specified node. If string, assumed to be + the name of the node. + + Returns: + ModelWrapper: The isolated-node modelwrapper. + """ + # Find the node + index = 0 + if type(by_node) is int: + if by_node < 0 or by_node >= len(self.model.graph.node): + raise FINNInternalError( + f"Cannot isolate node index {by_node}. Model has" + f"{len(self.model.graph.node)} nodes." + ) + index = by_node + elif type(by_node) is str: + node_name = self.model.get_node_from_name(by_node) + if node_name is None: + raise FINNInternalError(f"Cannot isolate node {by_node}. No such node found.") + index = [n.name for n in self.model.graph.node].index(cast("str", node_name)) + elif type(by_node) is NodeProto: + try: + index = self.model.graph.node.index(by_node) + except Exception as e: + raise FINNInternalError(f"Node {by_node.name} not found in the model.") from e + else: + raise FINNInternalError( + f"Cannot find node to isolate: {by_node}. Specify either " + f"the index (int), node name (str) or the object itself " + f"(NodeProto)." + ) + + # Copy model to modify + node_model = deepcopy(self.model) + + # Remove any other node + # TODO: Refactor this following section + for i, node in enumerate(self.model.graph.node): + if i != index: + node_model.graph.node.remove(node) + target_op = getCustomOp(node_model.graph.node[0]) + if not isinstance(target_op, HWCustomOp): + raise FINNInternalError( + f"Node {node_model.graph.node[0].name} is not a HWCustomOp, cannot " + f"isolate for simulation." + ) + inp = onnx.helper.make_tensor_value_info( + "inp", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_folded_input_shape()) + ) + inp_dummy_out = onnx.helper.make_tensor_value_info( # noqa + "inp_dummy_out", + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_folded_input_shape()), + ) + outp = onnx.helper.make_tensor_value_info( # noqa + "outp", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_output_shape()) + ) + outp_dummy_out = onnx.helper.make_tensor_value_info( + "outp_dummy_out", + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_output_shape()), + ) + input_dummy_node = onnx.helper.make_node( + "RemoveDataPath_rtl", + inputs=["inp"], + outputs=["inp_dummy_out"], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + folded_shape=target_op.get_folded_input_shape(), + normal_shape=target_op.get_normal_input_shape(), + dataType=target_op.get_input_datatype().name, + name=node_model.graph.node[0].name + "_input_dummy", + ) + output_dummy_node = onnx.helper.make_node( + "RemoveDataPath_rtl", + inputs=["outp"], + outputs=["outp_dummy_out"], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + folded_shape=target_op.get_folded_output_shape(), + normal_shape=target_op.get_normal_output_shape(), + dataType=target_op.get_output_datatype().name, + name=node_model.graph.node[0].name + "_output_dummy", + ) + + node_model.graph.node.insert(0, input_dummy_node) + node_model.graph.node.append(output_dummy_node) + + # Remove old io + for _ in range(len(node_model.graph.node[1].input)): + node_model.graph.node[1].input.pop() + for _ in range(len(node_model.graph.node[1].output)): + node_model.graph.node[1].output.pop() + + # Set new io + node_model.graph.node[1].input.append("inp_dummy_out") + node_model.graph.node[1].output.append("outp") + + # Remove graph io + for _ in range(len(node_model.graph.input)): + node_model.graph.input.pop() + for _ in range(len(node_model.graph.output)): + node_model.graph.output.pop() + + # Set new graph io + node_model.graph.input.append(inp) + node_model.graph.output.append(outp_dummy_out) + + return node_model + + def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, int]: + """Return the stream descriptions for the given model for the C++ sim config header. + + Used by for example _build_single_node_simulation(). + + Returns: + tuple[str, int, str, int]: Strings of stream descriptions together with + their count (in, out) + """ + # Get IO iterations required + instream_iters = [] + outstream_iters = [] + for top_inp in model.graph.input: + iname = top_inp.name + first_node = model.find_consumer(iname) + assert first_node is not None, "Failed to find consumer for " + iname + top_ind = list(first_node.input).index(iname) + ishape_folded = getCustomOp(first_node).get_folded_input_shape(ind=top_ind) + instream_iters.append(int(np.prod(ishape_folded[:-1]))) + for top_out in model.graph.output: + oname = top_out.name + last_node = model.find_producer(oname) + assert last_node is not None, "Failed to find producer for " + oname + top_ind = list(last_node.output).index(oname) + oshape_folded = getCustomOp(last_node).get_folded_output_shape(ind=top_ind) + outstream_iters.append(int(np.prod(oshape_folded[:-1]))) + interface_names = model.get_metadata_prop("vivado_stitch_ifnames") + if interface_names is None: + raise FINNInternalError( + f"{model}: Could not find stitched-IP interface names. " + f"Did you run IP Stitching first?" + ) + + # TODO: Copied from rtlsim_exec_cppxsi. Remove eval(). + interface_names = eval(interface_names) + if "aximm" in interface_names.keys() and interface_names["aximm"] != []: + raise FINNInternalError( + f"{model}: CPP XSI Sim does not know how to handle full " + f"AXI MM interfaces: {interface_names['aximm']}" + ) + instream_names = [x[0] for x in interface_names["s_axis"]] + outstream_names = [x[0] for x in interface_names["m_axis"]] + + # Format stream descriptions + def _format_descr_name(s: str) -> str: + for old, new in [("[", ""), ("]", ""), ("(", "{"), (")", "}"), ("'", '"')]: + s = s.replace(old, new) + return s + + # TODO: Change this since we don't have throttling + instream_descrs = [ + (instream_names[i], instream_iters[i], instream_iters[i]) + for i in range(len(instream_names)) + ] + instream_descrs_str = _format_descr_name(str(instream_descrs)) + + outstream_descrs = [ + (outstream_names[i], outstream_iters[i], outstream_iters[i]) + for i in range(len(outstream_names)) + ] + outstream_descrs_str = _format_descr_name(str(outstream_descrs)) + return instream_descrs_str, len(instream_names), outstream_descrs_str, len(outstream_names) + + def _create_sim_so( + self, + model: ModelWrapper, + top_module_name: str, + vivado_stitched_proj: Path, + build_dir: Path | None, + debug: bool, + ) -> tuple[Path, Path]: + """Create a new RTLSim .so file. If one exists already it is used. + + Returns: + tuple[Path, Path]: Return sim_base and sim_rel. + """ + rtlsim_so_str = model.get_metadata_prop("rtlsim_so") + if (rtlsim_so_str is None) or not Path(rtlsim_so_str).exists(): + all_verilog_srcs = ( + (Path(vivado_stitched_proj) / "all_verilog_srcs.txt").read_text().split() + ) + sim_dir = ( + make_build_dir(f"rtlsim_{model.graph.node[0].name}_") + if build_dir is None + else build_dir + ) + sim_base, sim_rel = finnxsi.compile_sim_obj( + top_module_name, all_verilog_srcs, str(sim_dir), debug=debug + ) + rtlsim_so = Path(sim_base) / Path(sim_rel) + model.set_metadata_prop("rtlsim_so", str(rtlsim_so)) + else: + sim_base, sim_rel = cast("str", rtlsim_so_str.split("xsim.dir")) + sim_rel = "xsim.dir" + sim_rel + return Path(sim_base), Path(sim_rel) + + def _compile_simulation( + self, sim_base: Path, sim_type: SimulationType, silent: bool = False + ) -> Path: + """Compile an existing RTLSIM directory. Requires _create_sim_so to be run before. Expects + rtlsim_config.hpp to be templated already. + + Returns: + Path: Path to the executable shell script to run the binary + """ + # Determine executable name + execname = "" + match sim_type: + case SimulationType.NODE_BASED_CONNECTED: + execname = "LayerSimulationBackend" + case SimulationType.NODE_BASED_ISOLATED: + execname = "IsolatedSimulationBackend" + case _: + raise FINNInternalError(f"Unknown simulation type: {sim_type}") + simulation_executable = Path(sim_base) / execname + if simulation_executable.exists(): + # Simulation was already compiled, we can return early + self.progress_bar.update("Make") + return simulation_executable + + # Check where FINNXSI is + finnxsi_dir = os.environ["FINN_XSI"] + + # Running CMake first + cmake_call = f"{sys.executable} -m cmake -S {finnxsi_dir} -B {sim_base}" + log.info(f"Running cmake on RTLSIM Wrapper in {sim_base}") + try: + launch_process_helper( + shlex.split(cmake_call), + cwd=finnxsi_dir, + print_stdout=not silent, + print_stderr=not silent, + proc_env=os.environ.copy(), + ) + except CalledProcessError as e: + raise FINNInternalError(f"Failed to run cmake in {sim_base}") from e + self.progress_bar.update("CMake") + + # Calling make to actually build the simulation + makefile = Path(sim_base) / "Makefile" + if not makefile.exists(): + raise FINNInternalError(f"Failed to create Makefile in {sim_base}!") + try: + launch_process_helper( + ["make"], + proc_env=os.environ.copy(), + cwd=sim_base, + print_stdout=not silent, + print_stderr=not silent, + ) + except CalledProcessError as e: + raise FINNInternalError(f"Failed to create executable in {sim_base}!") from e + + if not simulation_executable.exists(): + raise FINNInternalError(f"Make call in {sim_base} failed!") + self.progress_bar.update("Make") + return simulation_executable + + def _template_rtlsim_config( + self, + model: ModelWrapper, + sim_base: Path, + node_name: str, + previous_node_name: str | None, + node_index: int, + total_nodes: int, + timeout_cycles: int, + top_module_name: str, + trace_file: str | None, + ) -> Path: + """Template finn_xsi/finn_xsi/rtlsim_config.hpp.template with the correct values and + return the templated file. + """ + finnxsi_dir = os.environ["FINN_XSI"] + # Prepare the C++ driver config template + ( + instream_descrs_str, + len_instreams, + outstream_descrs_str, + len_outstreams, + ) = self._get_stream_descriptions(model) + template_dict = { + "TIMEOUT_CYCLES": timeout_cycles, + # name of the top-level HDL module + "TOP_MODULE_NAME": top_module_name, + # top-level AXI stream descriptors + "ISTREAM_DESC": instream_descrs_str, + "ISTREAM_LEN": len_instreams, + "OSTREAM_DESC": outstream_descrs_str, + "OSTREAM_LEN": len_outstreams, + # control tracing and trace filename + "TRACE_FILE": "std::nullopt" if trace_file is None else f'"{trace_file}"', + # sim kernel .so to use (depends on Vivado version) + "SIMKERNEL_SO": finnxsi.get_simkernel_so(), + # log file for xsi (not the sim driver) + "XSIM_LOG_FILE": '"xsi.log"', + # Node name in case of single-node simulation + "NODE_NAME": node_name, + # Previous node name (for single node simulation) + "PREVIOUS_NODE_NAME": ( + "std::nullopt" if previous_node_name is None else f'"{previous_node_name}"' + ), + "NODE_INDEX": node_index, + "TOTAL_NODES": total_nodes, + } + + fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" + fsim_config = fifosim_config_fname.read_text() + for key, val in template_dict.items(): + fsim_config = fsim_config.replace(f"@{key}@", str(val)) + + # Write the config to the simulation directory + rtlsim_config = Path(sim_base) / "rtlsim_config.hpp" + rtlsim_config.write_text(fsim_config) + return rtlsim_config + + def build_single_node_simulation( + self, + node_name: str, + node_model: ModelWrapper, + node_index: int, + total_nodes: int, + previous_node_name: str | None, + build_dir: Path | None, + sim_type: SimulationType, + timeout_cycles: int = 0, + silent: bool = False, + ) -> Path: + """Build the simulation binary for a single node. + + This can be used both by the connected node-by-node sim and the isolated node sim. + + Much of this is from the rtlsim_exec.py in core/ + + Args: + node_name: Despite the fact that we receive an isolated node model, we can still + manually pass a node name. This is useful to give unique names (e.g. for IPC) + node_model: The single node ModelWrapper to build the simulation from. + node_index: The index of the simulated node. Used to determine whether a node shares IO + with successors or predecessors. + total_nodes: The total number of nodes in the complete design. + previous_node_name: Required by the connected simulation. In the simulation binary this + is used to get access to the correct shared memory segment between + this node and the previous one. + build_dir: If given, use this directory for building the simulation. Otherwise one is + created from the nodes name. + sim_type: Simulation Type - determines the name of the executable that will be built + timeout_cycles: Number of cycles until simulation timeout. When set to 0 (default), no + timeout is given. + silent: If True, silences the Cmake and make output (including stderr) + + Returns: + Path: The path to the simulation binary (shell script). + """ + # TODO: Check if something is an output node instead of checking the node index + # TODO: Requires changes in the C++ code as well + + # Sanity checks (2 Dummy nodes + 1 target node) + if len(node_model.graph.node) != 3: + raise FINNUserError( + "Cannot create single-node simulation for a model with more than " + "1 node. Make sure to pass the ModelWrapper containing only" + "the relevant node." + ) + + # Check that the relevant data exists + wrapper_filename = node_model.get_metadata_prop("wrapper_filename") + if wrapper_filename is None or not Path(wrapper_filename).exists(): + raise FINNUserError( + f"Call CreateStitchedIP prior to building " + f"the simulation for {node_name}. " + f"wrapper_filename is set to {wrapper_filename}!" + ) + + vivado_stitched_proj = node_model.get_metadata_prop("vivado_stitch_proj") + if vivado_stitched_proj is None or not Path(vivado_stitched_proj).exists(): + raise FINNUserError( + f"Call CreateStitchedIP prior to building " + f"the simulation for {node_name}. (vivado_stitch_proj not set!)" + ) + + trace_file = cast("str | None", node_model.get_metadata_prop("rtlsim_trace")) + debug = not (trace_file is None or trace_file == "") + + # Get the module name and path + top_module_file = Path(wrapper_filename).resolve().absolute() + top_module_name = top_module_file.name.strip(".v") + + # Build the simulation .so and save it in the "rtlsim_so" metadata prop + sim_base, _ = self._create_sim_so( + node_model, top_module_name, Path(vivado_stitched_proj), build_dir, debug + ) + + # Fill out the simulation config header + _ = self._template_rtlsim_config( + node_model, + sim_base, + node_name, + previous_node_name, + node_index, + total_nodes, + timeout_cycles, + top_module_name, + trace_file, + ) + + # Building the whole simulation + return self._compile_simulation(sim_base, sim_type=sim_type, silent=silent).absolute() + + def _get_randomized_names(self, model: ModelWrapper, suffix_length: int = 5) -> dict[int, str]: + """Add a randomized suffix to every name in the model. Used to avoid interference with + previous or parallel running IPC simulations.""" + rand = Random() + rand.seed() + return { + i: model.graph.node[i].name + + "".join( + rand.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(suffix_length) + ) + for i in range(len(model.graph.node)) + } + + def _build_simulations_parallel( + self, workers: int, with_live_display: bool, functional_sim: bool, sim_type: SimulationType + ) -> dict[int, Path]: + """Build all nodes in the model in parallel, as isolated simulations, ready for usage in + an IPC connected simulation chain. + + Args: + workers: Number of parallel workers to use. + with_live_display: If True, display the building progress in a rich progress bar. + functional_sim: Use a functional simulation (faster but takes time to build) + sim_type: Type of simulation + + Returns: + Dict of executables that start the simulation of the given nodes, + indexed by the node-index. These are in their respective FINN_TMP + directories. + """ + + def _build( + node_name: str, + node_index: int, + total_nodes: int, + prev_node_name: str | None, + build_dir: Path, + ) -> Any: + nodemodel = self._isolated_node_model(node_index) + nodemodel = nodemodel.transform(InferShapes()) + nodemodel = nodemodel.transform(PrepareIP(self.fpgapart, self.clk_ns)) + nodemodel = nodemodel.transform( + CreateStitchedIP(self.fpgapart, self.clk_ns, functional_simulation=functional_sim) + ) + self.progress_bar.update("StitchedIP") + return self.build_single_node_simulation( + node_name, + nodemodel, + node_index, + total_nodes, + prev_node_name, + build_dir, + sim_type, + silent=with_live_display, + ) + + # Create randomized names to avoid clashes with old IPC shared memory + randomized_names = self._get_randomized_names(self.model) + + # TODO: Currently ignores workers argument + total_nodes = len(self.model.graph.node) + futures: dict[int, Future] = {} + + # Build sims in parallel + synth_workers = max( + 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 20) + ) # 20GB per synthesis + if not functional_sim: + # When not having to do synthesis, the build is not memory bottlenecked and + # can be executed as parallel as possible + synth_workers = int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))) + + # Build (stitched IP, cmake, make) all sims in parallel and return paths to + # the compiled executables + with DisabledLoggingConsole(), self.progress_bar if with_live_display else nullcontext(): + self.progress_bar.progress.console.log( + f"Building simulations using {int(synth_workers)} workers.." + ) + with ThreadPoolExecutor(max_workers=synth_workers) as pool: + for i in range(total_nodes): + futures[i] = pool.submit( + _build, + randomized_names[i], + i, + total_nodes, + randomized_names[i - 1] if i >= 1 else None, # type: ignore + Path(make_build_dir(f"rtlsim_{randomized_names[i]}_")), + ) + return {i: future.result() for i, future in futures.items()} + + def build_simulation( + self, simtype: SimulationType, workers: int, with_live_display: bool, functional_sim: bool + ) -> dict[int, Path]: + """Build a simulation of the given type, return the resulting executable (indexed by the + corresponding node index in the graph). + + Args: + simtype: Simulation type to build. + workers: Number of workers to use in parallel. + Normally set by the Simulation() class automatically. + with_live_display: If True, display a live progress-bar. + functional_sim: If True, use functional simulation (faster but takes some time to build) + """ + match simtype: + case SimulationType.NODE_BASED_CONNECTED | SimulationType.NODE_BASED_ISOLATED: + node_count = len(self.model.graph.node) + self.progress_bar = ThreadsafeProgressDisplay( + ["StitchedIP", "CMake", "Make"], + [node_count] * 3, + [ + "[bold blue](1)[/bold blue] Creating stitched IPs", + "[bold blue](2)[/bold blue] Configuring project with CMake", + "[bold blue](3)[/bold blue] Building simulation binaries", + ], + ) + return self._build_simulations_parallel( + workers, with_live_display, functional_sim, simtype + ) + case SimulationType.COMPLETE_DESIGN: + raise FINNUserError(f"Simulation method {simtype} is deprecated!") + + +class BuildSimulation(Transformation): + """Build a simulation of the given type for the model. + Puts the model into a prepared state (changes the graph). + If simulation binaries already exist, enter their directory and only re-compile.""" + + def __init__( + self, + fpgapart: str, + clk_ns: float, + functional_sim: bool, + simulation_type: SimulationType, + workers: int | None = None, + ) -> None: + """Create a new BuildSimulation transform.""" + self.workers = int(os.environ["NUM_DEFAULT_WORKERS"]) if workers is None else workers + self.functional_sim = functional_sim + self.fpgapart = fpgapart + self.clk_ns = clk_ns + self.sim_type = simulation_type + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Build / compile the model. Modifies the model.""" + self.model = model + with DisabledLoggingConsole() as console: # noqa + with console.status("Preparing model for the simulation step..."): + self._prepare_model() + + # Check if we already have stitched IPs and built simulations. If so, rerun cmake/make + needs_rebuild = True + sim_binaries = self.model.get_metadata_prop("simulation_binaries") + if sim_binaries is not None: + sim_binaries = sim_binaries.split("\n") + if len(sim_binaries) != len(self.model.graph.node): + log.info( + f"Found existing binaries, but number ({len(sim_binaries)}) " + f"does not match number of nodes in the graph " + f"({len(self.model.graph.node)}). Rebuilding..." + ) + else: + log.info("Existing simulations found. Re-running only CMake/Make..") + needs_rebuild = False + else: + log.info("No simulation binaries found, building now.") + + if needs_rebuild: + self.builder = SimulationBuilder(self.model, self.fpgapart, self.clk_ns) + sys.stdout = sys.stdout.console # type: ignore + sys.stderr = sys.stderr.console # type: ignore + self.binaries = self.builder.build_simulation( + self.sim_type, + self.workers, + with_live_display=True, + functional_sim=self.functional_sim, + ) + self.model.set_metadata_prop( + "simulation_binaries", "\n".join([str(p) for p in self.binaries.values()]) + ) + else: + + def _compile(binary: Path, progress: ThreadsafeProgressDisplay) -> None: + result = subprocess.run( + "cmake .;make", + shell=True, + cwd=str(binary.parent), + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise FINNUserError(f"Failed compilation in {binary.parent}: {result.stderr}") + progress.update("Compilation") + + sim_binaries = [Path(p) for p in sim_binaries] + sys.stdout = sys.stdout.console # type: ignore + sys.stderr = sys.stderr.console # type: ignore + with DisabledLoggingConsole() as cons: # noqa + progress = ThreadsafeProgressDisplay( + ["Compilation"], [len(sim_binaries)], ["Compilation"] + ) + progress.start() + futures = [] + with ThreadPoolExecutor(self.workers) as tpe: + for binary in sim_binaries: + futures.append(tpe.submit(_compile, binary, progress)) + tpe.shutdown() + progress.stop() + for future in futures: + future.result() + log.info("Compilation done.") + return self.model, False + + def _prepare_model(self) -> None: + """Execute some preparation transformations on the model.""" + self.model = self.model.transform(InsertDWC()) + self.model = self.model.transform(SpecializeLayers(self.fpgapart)) + self.model = self.model.transform(GiveUniqueNodeNames()) + self.model = self.model.transform(GiveReadableTensorNames()) + self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) + self.model = self.model.transform(HLSSynthIP()) diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index d69a5c2f27..6683134fbe 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -11,14 +11,14 @@ from pathlib import Path from rich.console import Console from threading import Lock -from typing import Any +from typing import Any, Literal from finn.util.basic import make_build_dir -from finn.util.exception import FINNInternalError +from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import ThreadsafeProgressDisplay -class NodeConnectedSimulationController: +class SimulationController: """Control a node-node IPC connected simulation in threads.""" def __init__( @@ -56,7 +56,7 @@ def __init__( self.running_lock = Lock() self.running = 0 self.total = len(names) - self.logdir = Path(make_build_dir("node_connected_simulation_logfiles_")) + self.logdir = Path(make_build_dir("simulation_logfiles_")) # Socket communication management self.processes: list[tuple[subprocess.Popen, Any, Any]] = [] @@ -80,7 +80,7 @@ def _start_process(self, binary: Path, process_id: int) -> int: # Create unique socket path which includes thread ID to avoid conflicts # with multiple threads - socket_path = Path(f"/tmp/{thread_id}/") + socket_path = Path(f"/tmp/fifosim_sockets/{thread_id}/") socket_path.mkdir(parents=True, exist_ok=True) socket_path = socket_path / f"sim_socket_{process_id}.sock" @@ -92,8 +92,8 @@ def _start_process(self, binary: Path, process_id: int) -> int: cmd = [str(binary), "--socket", socket_path] # Create log files for stdout and stderr - stdout_log = self.logdir / f"{process_id}_stdout.log" - stderr_log = self.logdir / f"{process_id}_stderr.log" + stdout_log = self.logdir / f"{process_id}_stdout_cpp.log" + stderr_log = self.logdir / f"{process_id}_stderr_cpp.log" stdout_file = stdout_log.open("w") stderr_file = stderr_log.open("w") @@ -113,6 +113,7 @@ def _start_process(self, binary: Path, process_id: int) -> int: f"C++ process exited immediately with code {proc.returncode}\n" f"Stderr: {stderr_output}\nStdout: {stdout_output}" ) + self.console.log(str(process_id) + ": " + msg) raise RuntimeError(msg) # Create Unix socket and connect @@ -132,6 +133,7 @@ def _start_process(self, binary: Path, process_id: int) -> int: f"C++ process died during socket wait with code {proc.returncode}\n" f"Stderr: {stderr_output}\nStdout: {stdout_output}" ) + self.console.log(str(process_id) + ": " + msg) raise RuntimeError(msg) try: @@ -148,6 +150,7 @@ def _start_process(self, binary: Path, process_id: int) -> int: f"Failed to connect to socket after {max_retries} retries\n" f"Stderr: {stderr_output}\nStdout: {stdout_output}" ) + self.console.log(str(process_id) + ": " + msg) raise RuntimeError(msg) from e time.sleep(0.2) @@ -160,6 +163,7 @@ def _start_process(self, binary: Path, process_id: int) -> int: f"Failed to connect to socket {socket_path}\n" f"Stderr: {stderr_output}\nStdout: {stdout_output}" ) + self.console.log(str(process_id) + ": " + msg) raise RuntimeError(msg) self.processes.append((proc, stdout_file, stderr_file)) @@ -262,6 +266,130 @@ def _cleanup_sockets(self) -> None: stdout_file.close() stderr_file.close() + +class NodeIsolatedSimulationController(SimulationController): + """Run simulations for node isolated cases.""" + + def __init__( + self, + parallel_simulations: int, + names: list[str], + binaries: list[Path], + console: Console, + poll_interval: float = 1.0, + with_progressbar: bool = False, + ) -> None: + """Set up node isolated simulation.""" + super().__init__( + parallel_simulations, names, binaries, console, poll_interval, with_progressbar + ) + self.console.log("Started simulation controller") + + def _postprocess_logs( + self, d: Path, readylog_name: str = "readylog.txt", validlog_name: str = "validlog.txt" + ) -> dict[Literal["valid", "ready"], dict[int, tuple[int, int, list[int]]]]: + """Recieve the directory containing a binary and the simulation logs. + If no logs are found raises an error, otherwise return the postprocessed logs: + {: (, , [, ...]), ...} + """ # noqa + readylog = d / readylog_name + validlog = d / validlog_name + if not readylog.exists() or not validlog.exists(): + raise FINNInternalError(f"Could not find simulation logs at {readylog} and {validlog}") + readydata = [ + [int(elem) for elem in line.split(",")] for line in readylog.read_text().split("\n")[1:] + ] + validdata = [ + [int(elem) for elem in line.split(",")] for line in validlog.read_text().split("\n")[1:] + ] + return { + "ready": {line[0]: (line[1], line[2], line[3:]) for line in readydata}, + "valid": {line[0]: (line[1], line[2], line[3:]) for line in validdata}, + } + + def run(self) -> None: + futures: list[Future] = [] + with self.console.status(f"Running simulation on every node. Log directory: {self.logdir}"): + with ThreadPoolExecutor(len(self.binaries)) as tpe: + for binary in self.binaries: + futures.append(tpe.submit(self._run_binary, binary)) + tpe.shutdown(wait=True) + self._cleanup_sockets() + + # Read data + data = {} + invalid = [] + for i, future in enumerate(futures): + data[self.names[i]] = future.result() + if data[self.names[i]] is None: + invalid.append(self.names[i]) + if len(invalid) > 0: + raise FINNInternalError( + f"Lost connection / malformed response " f"from nodes: {', '.join(invalid)}" + ) + + # TODO: Algorithm + raise NotImplementedError() + + IsolatedSimReturnType = dict[Literal["valid", "ready"], dict[int, tuple[int, int, list[int]]]] + + def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: + """Run simulation. Returning None if connection is lost.""" + process_index = self.binaries.index(binary) + with ( + self.logdir / f"{process_index}_log_isolated_{self.names[process_index]}_python.txt" + ).open("w+") as logfile: + # Initialize + logfile.write("Initializing simulation.\n") + proc_idx = self._start_process(binary, process_index) + response = self._send_and_receive(proc_idx, "start", {}) + + # Main loop + logfile.write("Beginning main loop\n") + logfile.write( + "totalCycles,inputCyclesDone,inputCyclesTarget," + "outputCyclesDone,outputCyclesTarget\n" + ) + logfile.flush() + while True: + time.sleep(self.poll_interval) + logfile.write("Sending status request") + response = self._send_and_receive(proc_idx, "status", {}) + if response is None: + return None + state = response["state"] + if state == "done": + return self._postprocess_logs(binary.parent) + logfile.write( + f"{response['totalCycles']}," + f"{response['inputCyclesDone']}," + f"{response['inputCyclesTarget']}" + f"{response['outputCyclesDone']}," + f"{response['outputCyclesTarget']}\n" + ) + + +class NodeConnectedSimulationController(SimulationController): + """Run simulations for node connected cases.""" + + def __init__( + self, + parallel_simulations: int, + names: list[str], + binaries: list[Path], + console: Console, + poll_interval: float = 1.0, + with_progressbar: bool = True, + ) -> None: + """Set up node connected simulation.""" + super().__init__( + parallel_simulations, names, binaries, console, poll_interval, with_progressbar + ) + for binary in binaries: + if not binary.exists(): + console.log(f"Binary {binary} does not exist!") + raise FINNUserError(f"Binary {binary} does not exist!") + def run( self, depth: list[list[int]] | None = None, @@ -274,6 +402,7 @@ def run( depth: FIFO depth to configure for simulations. samples: Number of samples to simulate. output_json: Optional path to write merged simulation data as JSON. + max_cycles: Max cycles Returns: Dictionary mapping simulation names to their FIFO utilization arrays. From 77ecc6b53b780b84cb8655144abf6cd48a2a43ee Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 22 Jan 2026 09:41:23 +0100 Subject: [PATCH 045/170] Revert to Sim without Look-Ahead --- finn_xsi/finn_xsi/include/Simulation.hpp | 91 ++++++++++--------- .../fpgadataflow/simulation_controller.py | 12 +-- 2 files changed, 55 insertions(+), 48 deletions(-) diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index dd4f4a710a..926aec70b6 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include #include @@ -91,44 +91,48 @@ class Simulation { } }; +//Small struct used for exange. Will be changed later to more complex data structure. +struct CommData{ + bool data; +}; + // Communication Flow: // -// valid valid ┌──────────────────────────────────────┐ valid -// SHM ─────────> FIFO ─────────> │ valid valid │ ─────────> SHM -// (pred) <───────── <───────── istream ─────────> xsim ─────────> ostream <───── (succ) -// ready ready │ <───────── <───────── │ ready -// │ ready ready │ -// │ (sim) │ -// └──────────────────────────────────────┘ +// valid ┌──────────────────────────────────────┐ valid valid +// SHM ─────────> │ valid valid │ ─────────> FIFO ─────> SHM +// (pred) <───────── istream ─────────> xsim ─────────> ostream <───────── <───── (succ) +// ready │ <───────── <───────── │ ready ready +// │ ready ready │ +// │ (sim) │ +// └──────────────────────────────────────┘ template class SingleNodeSimulation : public Simulation { - using ConsumingInterface = InterprocessCommunicationChannelInterface; - using ProducingInterface = InterprocessCommunicationChannelInterface; + using ConsumingInterface = InterprocessCommunicationChannel; + using ProducingInterface = InterprocessCommunicationChannel; constexpr static bool FirstNode = NodeIndex == 0; constexpr static bool LastNode = NodeIndex == (TotalNodes - 1); std::array fromProducerInterface; std::array toConsumerInterface; std::size_t cyclesRun = 0; std::size_t completedMaps = 0; - std::array fifo; + std::array fifo; /// Communicate with predecessors and successors and update their values and our own [[gnu::hot, gnu::flatten, gnu::always_inline]] void communicate(std::stop_token stoken = {}) { if constexpr (!FirstNode) { for (std::size_t i = 0; i < IStreamsSize; ++i) { - // Interface SHM <-> FIFO + // Interface SHM <-> SIM fromProducerInterface[i].exchangeDataDownstream(stoken); - // FIFO <-> sim - this->fifo[i].exchangeDataDownstream(stoken); - // Toggle FIFO clock - this->fifo[i].toggleClock(); } } if constexpr (!LastNode) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - // Interface sim <-> SHM + // Interface SIM <-> FIFO this->ostreams[i].exchangeDataDownstream(stoken); - + // Interface FIFO <-> SHM + this->fifo[i].exchangeDataDownstream(stoken); + // Toggle FIFO clock + this->fifo[i].toggleClock(); } } if constexpr (LastNode) { @@ -166,11 +170,11 @@ class SingleNodeSimulation : public Simulationistreams[i]); + fromProducerInterface[i].connectDownstream(this->istreams[i]); } for (std::size_t i = 0; i < OStreamsSize; ++i) { - this->ostreams[i].connectDownstream(toConsumerInterface[i]); + this->ostreams[i].connectDownstream(fifo[i]); + fifo[i].connectDownstream(toConsumerInterface[i]); } } @@ -203,12 +207,16 @@ class SingleNodeSimulation : public Simulation::reset(); - if constexpr (!FirstNode) { + if constexpr (!LastNode) { // Reset FIFOs - for (std::size_t i = 0; i < IStreamsSize; ++i) { + for (std::size_t i = 0; i < OStreamsSize; ++i) { fifo[i].reset(); } } @@ -256,38 +263,38 @@ class SingleNodeSimulation : public Simulation= IStreamsSize) { - throw std::out_of_range(std::format("FIFO index {} out of range (max: {})", index, IStreamsSize - 1)); + if (index >= OStreamsSize) { + throw std::out_of_range(std::format("FIFO index {} out of range (max: {})", index, OStreamsSize - 1)); } fifo[index].setMaxSize(depth); } /// Set the max FIFO depth of all interfaces void setMaxFIFODepth(std::size_t depth) { - if constexpr (!FirstNode) { + if constexpr (!LastNode) { for (FIFO& f : fifo) { f.setMaxSize(depth); } } } - std::array getFIFODepth() const noexcept { - if constexpr (FirstNode) { + std::array getFIFODepth() const noexcept { + if constexpr (LastNode) { return {}; } - std::array utilizations{}; - for (std::size_t i = 0; i < IStreamsSize; ++i) { + std::array utilizations{}; + for (std::size_t i = 0; i < OStreamsSize; ++i) { utilizations[i] = fifo[i].getMaxSize(); } return utilizations; @@ -306,12 +313,12 @@ class SingleNodeSimulation : public Simulation getFIFOUtilization() const noexcept { - if constexpr (FirstNode) { + std::array getFIFOUtilization() const noexcept { + if constexpr (LastNode) { return {}; } - std::array utilizations{}; - for (std::size_t i = 0; i < IStreamsSize; ++i) { + std::array utilizations{}; + for (std::size_t i = 0; i < OStreamsSize; ++i) { utilizations[i] = fifo[i].getMaxUtil(); } return utilizations; diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index d625917d72..34eb2e71f5 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -325,8 +325,8 @@ def run( try: with ThreadPoolExecutor(self.workers) as pool: for i, (name, binary) in enumerate(zip(self.names, self.binaries, strict=True)): - is_first_node = i == 0 - is_special_for_display = is_first_node or i == len(self.names) - 1 + is_last_node = i == len(self.names) - 1 + is_special_for_display = i == 0 or is_last_node futures.append( pool.submit( self._run_binary, @@ -334,7 +334,7 @@ def run( name, i % multiprocessing.cpu_count(), depth[i] if depth is not None else None, - is_first_node, # Only first node has no input FIFOs + is_last_node, # Only last node has no output FIFOs is_special_for_display, # First and last get special coloring max_cycles, ) @@ -439,7 +439,7 @@ def _run_binary( name: str | None, _cpu: int | None, depth: list[int] | None = None, - is_first_node: bool = False, + is_last_node: bool = False, is_special_for_display: bool = False, max_cycles: int | None = None, ) -> tuple[str, list[int], int, int, list[int], bool, list[int]] | None: @@ -450,7 +450,7 @@ def _run_binary( name: Name of simulation node _cpu: CPU affinity (unused) depth: List of FIFO depths for this node's output FIFOs - is_first_node: True if this is the first node (no input FIFOs to configure) + is_last_node: True if this is the last node (no output FIFOs to configure) is_special_for_display: True if this node should get special color in logs max_cycles: Maximum cycles to simulate @@ -488,7 +488,7 @@ def _print(msg: str, color: str = "green") -> None: # Send configuration commands # Last node has no output FIFOs, so don't configure FIFO depths config_payload: dict[str, list[int] | int] = {} - if not is_first_node and depth is not None: + if not is_last_node and depth is not None: config_payload["fifo_depth"] = depth if max_cycles is not None: config_payload["max_cycles"] = max_cycles From f928f0103419c0976ce4b3d3f4b7f03e90d404c3 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:07:00 +0100 Subject: [PATCH 046/170] Finish integrating the reverted changes --- .../finn_xsi/include/IsolatedSimulation.hpp | 12 +-- finn_xsi/finn_xsi/include/Simulation.hpp | 6 +- src/finn/builder/build_dataflow_config.py | 98 +++++++++---------- .../transformation/fpgadataflow/simulation.py | 36 ++++--- .../fpgadataflow/simulation_build.py | 4 +- src/finn/util/exception.py | 10 +- tests/fpgadataflow/test_bram_block_search.py | 20 ++-- 7 files changed, 94 insertions(+), 92 deletions(-) diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index 70c3733494..97ec653ec8 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -104,12 +104,12 @@ class IsolatedSimulation : public Simulation inline void writeLogEntryReady () { readyLog << simState.getCycleStateInput(); - for (S_AXIS_Control& s : this->istreams) { readyLog << "," << s.isReady(); } + for (S_AXIS_Control& s : this->istreams) { readyLog << "," << s.getInputReady(); } } inline void writeLogEntryValid() { validLog << simState.getCycleStateOutput(); - for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.isValid(); } + for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.getOutputValid(); } } public: @@ -167,10 +167,10 @@ class IsolatedSimulation : public Simulation this->clearPorts(); this->reset(); for (S_AXIS_Control& s : this->istreams) { - s.setValid(true); + s.setInputValid(true); } for (M_AXIS_Control& s : this->ostreams) { - s.setReady(true); + s.setOutputReady(true); } } if (!simState.isRunning()) { @@ -181,10 +181,10 @@ class IsolatedSimulation : public Simulation writeLogEntryReady(); writeLogEntryValid(); - if (!simState.inputCyclesProcessed() && this->istreams[simState.inputLargestStreamIndex].isReady()) { + if (!simState.inputCyclesProcessed() && this->istreams[simState.inputLargestStreamIndex].getInputReady()) { ++simState.inputCyclesDone; } - if (!simState.outputCyclesProcessed() && this->ostreams[simState.outputLargestStreamIndex].isValid()) { + if (!simState.outputCyclesProcessed() && this->ostreams[simState.outputLargestStreamIndex].getOutputValid()) { ++simState.outputCyclesDone; } this->clk.toggleClk(); diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index f472e0dbe2..48c07d0b30 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -9,7 +9,7 @@ #include #include -#include +#include #include #include #include @@ -106,8 +106,8 @@ struct CommData{ // └──────────────────────────────────────┘ template class SingleNodeSimulation : public Simulation { - using ConsumingInterface = InterprocessCommunicationChannel; - using ProducingInterface = InterprocessCommunicationChannel; + using ConsumingInterface = InterprocessCommunicationChannelInterface; + using ProducingInterface = InterprocessCommunicationChannelInterface; constexpr static bool FirstNode = NodeIndex == 0; constexpr static bool LastNode = NodeIndex == (TotalNodes - 1); std::array fromProducerInterface; diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index 0c71879ade..5d59f27722 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -27,8 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" -Dataflow build configuration module for FINN. +"""Dataflow build configuration module for FINN. This module provides configuration classes and enums for building dataflow accelerators using the FINN framework. The main class DataflowBuildConfig @@ -45,6 +44,7 @@ This configuration system allows users to customize the entire FINN dataflow build process through a single, serializable configuration object. """ + from __future__ import annotations import numpy as np @@ -56,6 +56,7 @@ from typing import Any, Literal, Optional from finn.util.basic import alveo_default_platform, part_map +from finn.util.exception import FINNConfigurationError class LogLevel(str, Enum): @@ -70,7 +71,7 @@ class LogLevel(str, Enum): class AutoFIFOSizingMethod(str, Enum): - "Select the type of automatic FIFO sizing strategy." + """Select the type of automatic FIFO sizing strategy.""" CHARACTERIZE = "characterize" LARGEFIFO_RTLSIM = "largefifo_rtlsim" @@ -85,7 +86,7 @@ class ShellFlowType(str, Enum): class DataflowOutputType(str, Enum): - "Output product types that can be generated by build_dataflow" + """Output product types that can be generated by build_dataflow.""" STITCHED_IP = "stitched_ip" ESTIMATE_REPORTS = "estimate_reports" @@ -98,7 +99,7 @@ class DataflowOutputType(str, Enum): class VitisOptStrategy(Enum): - "Values applicable to VitisBuild optimization strategy." + """Values applicable to VitisBuild optimization strategy.""" DEFAULT = "0" POWER = "1" @@ -109,7 +110,7 @@ class VitisOptStrategy(Enum): class FpgaMemoryType(str, Enum): - "Memory Type used by the FPGA to store input/output data" + """Memory Type used by the FPGA to store input/output data.""" DEFAULT = "default" HOST_MEM = "host_memory" @@ -125,7 +126,7 @@ class LargeFIFOMemStyle(str, Enum): class VerificationStepType(str, Enum): - "Steps at which FINN ONNX execution can be launched for verification." + """Steps at which FINN ONNX execution can be launched for verification.""" #: verify after step_qonnx_to_finn, using Python execution QONNX_TO_FINN_PYTHON = "finn_onnx_python" @@ -182,7 +183,7 @@ class VerificationStepType(str, Enum): #: List of steps to run for a dataflow build including HW code generation, but #: without any synthesis. -hw_codegen_dataflow_steps = estimate_only_dataflow_steps + ["step_hw_codegen"] +hw_codegen_dataflow_steps = [*estimate_only_dataflow_steps, "step_hw_codegen"] @dataclass @@ -451,8 +452,7 @@ class DataflowBuildConfig(DataClassJSONMixin, DataClassYAMLMixin): vivado_power_simulation_type: Literal["timing", "functional"] = "functional" def _resolve_hls_clk_period(self) -> float: - """ - Resolve the HLS clock period, falling back to synthesis clock period if not set. + """Resolve the HLS clock period, falling back to synthesis clock period if not set. Returns: float: The HLS clock period in nanoseconds. If hls_clk_period_ns is not @@ -461,12 +461,10 @@ def _resolve_hls_clk_period(self) -> float: if self.hls_clk_period_ns is None: # use same clk for synth and hls if not explicitly specified return self.synth_clk_period_ns - else: - return self.hls_clk_period_ns + return self.hls_clk_period_ns def _resolve_driver_platform(self) -> Literal["zynq-iodma", "alveo"]: - """ - Resolve the driver platform based on the shell flow type. + """Resolve the driver platform based on the shell flow type. Returns: str: The driver platform identifier. Returns "zynq-iodma" for Vivado Zynq @@ -477,14 +475,12 @@ def _resolve_driver_platform(self) -> Literal["zynq-iodma", "alveo"]: """ if self.shell_flow_type == ShellFlowType.VIVADO_ZYNQ: return "zynq-iodma" - elif self.shell_flow_type == ShellFlowType.VITIS_ALVEO: + if self.shell_flow_type == ShellFlowType.VITIS_ALVEO: return "alveo" - else: - raise Exception("Couldn't resolve driver platform for " + str(self.shell_flow_type)) + raise Exception("Couldn't resolve driver platform for " + str(self.shell_flow_type)) def _resolve_fpga_part(self) -> str: - """ - Resolve the FPGA part identifier. + """Resolve the FPGA part identifier. If fpga_part is explicitly specified in the configuration, it is returned as-is. If not specified, attempts to look up the part from the board name using the @@ -499,18 +495,23 @@ def _resolve_fpga_part(self) -> str: """ if self.fpga_part is None: # lookup from part map if not specified + if self.board is None: + raise FINNConfigurationError( + "Either board or fpga_part must be specified in flow config." + ) try: fpga_part = part_map[self.board] return fpga_part except KeyError: - raise Exception("Couldn't resolve fpga_part for " + self.board) + raise FINNConfigurationError( + "Couldn't resolve fpga_part for " + self.board + ) # noqa: B904 else: # return as-is when explicitly specified return self.fpga_part def _resolve_cycles_per_frame(self) -> None | int: - """ - Calculate the number of clock cycles available per frame based on target FPS. + """Calculate the number of clock cycles available per frame based on target FPS. Uses the target_fps and synth_clk_period_ns to compute how many clock cycles are available for processing each frame to achieve the target frame rate. @@ -521,14 +522,12 @@ def _resolve_cycles_per_frame(self) -> None | int: """ if self.target_fps is None: return None - else: - n_clock_cycles_per_sec = 10**9 / self.synth_clk_period_ns - n_cycles_per_frame = n_clock_cycles_per_sec / self.target_fps - return int(n_cycles_per_frame) + n_clock_cycles_per_sec = 10**9 / self.synth_clk_period_ns + n_cycles_per_frame = n_clock_cycles_per_sec / self.target_fps + return int(n_cycles_per_frame) def _resolve_vitis_platform(self) -> str: - """ - Resolve the Vitis platform identifier for Alveo board builds. + """Resolve the Vitis platform identifier for Alveo board builds. If vitis_platform is explicitly specified, it is returned as-is. If not specified but a board is given, attempts to look up the default Vitis platform for that @@ -543,16 +542,14 @@ def _resolve_vitis_platform(self) -> str: """ if self.vitis_platform is not None: return self.vitis_platform - elif (self.vitis_platform is None) and (self.board is not None): + if (self.vitis_platform is None) and (self.board is not None): return alveo_default_platform[self.board] - else: - raise Exception( - "Could not resolve Vitis platform:" " need either board or vitis_platform specified" - ) + raise Exception( + "Could not resolve Vitis platform: need either board or vitis_platform specified" + ) def _resolve_verification_steps(self) -> list[VerificationStepType]: - """ - Resolve the list of verification steps to be performed during the build. + """Resolve the list of verification steps to be performed during the build. Returns: List[VerificationStepType]: A list of verification steps to perform. @@ -560,12 +557,10 @@ def _resolve_verification_steps(self) -> list[VerificationStepType]: """ if self.verify_steps is None: return [] - else: - return self.verify_steps + return self.verify_steps def _resolve_verification_io_pair(self) -> None | tuple[Any, Any]: - """ - Load and validate the input/output numpy arrays for verification. + """Load and validate the input/output numpy arrays for verification. Loads the verification input and expected output arrays from the files specified in verify_input_npy and verify_expected_output_npy. Validates @@ -580,16 +575,15 @@ def _resolve_verification_io_pair(self) -> None | tuple[Any, Any]: """ if self.verify_steps is None: return None - else: - assert os.path.isfile(self.verify_input_npy), ( - "verify_input_npy not found: " + self.verify_input_npy - ) - verify_input_npy = np.load(self.verify_input_npy) - assert os.path.isfile(self.verify_expected_output_npy), ( - "verify_expected_output_npy not found: " + self.verify_expected_output_npy - ) - verify_expected_output_npy = np.load(self.verify_expected_output_npy) - return ( - verify_input_npy, - verify_expected_output_npy, - ) + assert os.path.isfile(self.verify_input_npy), ( + "verify_input_npy not found: " + self.verify_input_npy + ) + verify_input_npy = np.load(self.verify_input_npy) + assert os.path.isfile(self.verify_expected_output_npy), ( + "verify_expected_output_npy not found: " + self.verify_expected_output_npy + ) + verify_expected_output_npy = np.load(self.verify_expected_output_npy) + return ( + verify_input_npy, + verify_expected_output_npy, + ) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 007e50bd61..ad1f897750 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,4 +1,5 @@ """Manage FINN simulation variants.""" + import json import math import time @@ -8,7 +9,7 @@ from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames -from typing import Any, cast +from typing import Any, TypeAlias, cast from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp @@ -25,6 +26,8 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import DisabledLoggingConsole, log +FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] + class Simulation: """Manage simulation (runs) in FINN. Upon instance creation, the simulation will be built. @@ -61,7 +64,7 @@ def __init__( ) if any(not p.exists() for p in sim_binaries): raise FINNUserError( - "Simulation binary data points to invalid paths. " "Please rerun BuildSimulation." + "Simulation binary data points to invalid paths. Please rerun BuildSimulation." ) # TODO: Currently we have to recompile even if we just # TODO: called BuildSimulation in the step before @@ -171,7 +174,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: SimulationType.NODE_BASED_CONNECTED, self.fpgapart, self.clk_ns, - self.cfg.functional_sim, + self.cfg.functional_simulation, ) model = sim.model # TODO:clean up @@ -204,7 +207,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: used_size = fifo_depths[i][j] bw = bit_widths[i][j] - needs_minimization[i][j] = self.needs_minimization(used_size, bw) + needs_minimization[i][j] = self._needs_minimization(used_size, bw) # Preserve original baseline depths for testing (deep copy) original_fifo_depths = [row[:] for row in fifo_depths] @@ -348,17 +351,17 @@ def _minimize_fifo_depth( print(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") - # If FIFO depth of 2 works, we dont need FIFOs at all, because AXI buffers some values + # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs success, timeout = self._test_depth( - 2, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + 32, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles ) if success: - return 2 + return 32 if original_size <= self.max_qsrl_depth: upper_luts = calculate_srl16e_luts(original_size, bw) - # Smallest depth that is reasonable is 32 (Fits into bw LUTRAMs) - lower_luts = calculate_srl16e_luts(32, bw) + # LUTRAM based FIFOs have block sizes of 32, so smallest after 32 is 64 + lower_luts = calculate_srl16e_luts(64, bw) # Binary search if there's room to search if upper_luts > lower_luts: @@ -388,8 +391,8 @@ def _minimize_fifo_depth( ) if success: upper_luts = calculate_srl16e_luts(original_size, bw) - # Smallest depth that is reasonable is 32 (Fits into bw LUTRAMs) - lower_luts = calculate_srl16e_luts(32, bw) + # LUTRAM based FIFOs have block sizes of 32, so smallest after 32 is 64 + lower_luts = calculate_srl16e_luts(64, bw) # Binary search if there's room to search if upper_luts > lower_luts: @@ -587,7 +590,7 @@ def _binary_search_srl_depth( return best_working_depth - def needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: + def _needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: """Determine whether a FIFO can be minimized further. Args: @@ -807,7 +810,7 @@ def calculate_srl16e_depth_range(luts: int, bitwidth: int) -> tuple[int, int]: return (min_depth, max_depth) -class RunLayerIsolatedSimulation(Transformation): # noqa +class RunLayerIsolatedSimulation(Transformation): def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: """Run isolated layer simulations.""" super().__init__() @@ -850,7 +853,6 @@ def __init__( else: self.path = fifo_config - FIFODepthConfig = dict[int, dict[str, str | list[int]]] # noqa self.depth: FIFODepthConfig = {} with self.path.open() as f: self.depth = cast("FIFODepthConfig", json.load(f)) @@ -886,7 +888,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: model: ModelWrapper = model.transform(GiveReadableTensorNames()) model = model.transform( PrepareIP( - fpgapart=self.cfg._resolve_fpga_part(), clk=self.cfg.synth_clk_period_ns # noqa + fpgapart=self.cfg._resolve_fpga_part(), + clk=self.cfg.synth_clk_period_ns, # noqa ) ) model = model.transform(HLSSynthIP()) @@ -997,7 +1000,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: model = model.transform(GiveReadableTensorNames()) model = model.transform( PrepareIP( - fpgapart=self.cfg._resolve_fpga_part(), clk=self.cfg.synth_clk_period_ns # noqa + fpgapart=self.cfg._resolve_fpga_part(), + clk=self.cfg.synth_clk_period_ns, # noqa ) ) model = model.transform(HLSSynthIP()) diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 5129fd2319..1feea49ba9 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -650,8 +650,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: if needs_rebuild: self.builder = SimulationBuilder(self.model, self.fpgapart, self.clk_ns) - sys.stdout = sys.stdout.console # type: ignore - sys.stderr = sys.stderr.console # type: ignore + # sys.stdout = sys.stdout.console # type: ignore + # sys.stderr = sys.stderr.console # type: ignore self.binaries = self.builder.build_simulation( self.sim_type, self.workers, diff --git a/src/finn/util/exception.py b/src/finn/util/exception.py index 0010f47427..fa8439c57d 100644 --- a/src/finn/util/exception.py +++ b/src/finn/util/exception.py @@ -1,6 +1,7 @@ """Here we organize FINN+`s exceptions and error handling. It also contains a decorator to snapshot FINN+ when it crashes for debugging purposes. """ + from __future__ import annotations import functools @@ -8,16 +9,19 @@ import os import shutil import traceback +from collections.abc import Callable from datetime import datetime from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp -from typing import Callable +from typing import TYPE_CHECKING, TypeAlias import finn -from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.util.basic import make_build_dir +if TYPE_CHECKING: + from finn.builder.build_dataflow_config import DataflowBuildConfig + """ FINNError is the base class for all errors. FINNUserError is a purely user-facing error that has nothing to do with FINNs internals @@ -70,7 +74,7 @@ def __init__(self, *args: object) -> None: # Alias for a build flow step function apply function -StepFunction = Callable[[ModelWrapper, DataflowBuildConfig], ModelWrapper] +StepFunction: TypeAlias = Callable[[ModelWrapper, "DataflowBuildConfig"], ModelWrapper] def snapshot_on_exception( diff --git a/tests/fpgadataflow/test_bram_block_search.py b/tests/fpgadataflow/test_bram_block_search.py index fb18206991..2542fde769 100644 --- a/tests/fpgadataflow/test_bram_block_search.py +++ b/tests/fpgadataflow/test_bram_block_search.py @@ -364,9 +364,9 @@ def test_small_depths_no_minimization(self): sim.max_qsrl_depth = 256 # Depths <= 32 don't need minimization (fit in bitwidth/2 LUTs) - assert not sim.needs_minimization(32, 8) - assert not sim.needs_minimization(16, 8) - assert not sim.needs_minimization(2, 8) + assert not sim._needs_minimization(32, 8) + assert not sim._needs_minimization(16, 8) + assert not sim._needs_minimization(2, 8) # TODO: Maybe remove this behavior def test_qsrl_range_no_minimization(self): @@ -377,8 +377,8 @@ def test_qsrl_range_no_minimization(self): sim.max_qsrl_depth = 256 # Depths within max_qsrl_depth don't need minimization - assert not sim.needs_minimization(128, 8) - assert not sim.needs_minimization(256, 8) + assert not sim._needs_minimization(128, 8) + assert not sim._needs_minimization(256, 8) def test_large_depths_need_minimization(self): """Test that large depths with multiple BRAM blocks need minimization.""" @@ -398,7 +398,7 @@ def test_large_depths_need_minimization(self): bitwidth = 8 blocks = calculate_bram_blocks(depth, bitwidth) assert blocks > 1, f"depth={depth}, bitwidth={bitwidth} should use >1 BRAM" - assert sim.needs_minimization(depth, bitwidth) + assert sim._needs_minimization(depth, bitwidth) # bitwidth=18: 1 BRAM range is (1, 1024) # Use depth > 1024 to get multiple blocks @@ -406,7 +406,7 @@ def test_large_depths_need_minimization(self): bitwidth = 18 blocks = calculate_bram_blocks(depth, bitwidth) assert blocks > 1, f"depth={depth}, bitwidth={bitwidth} should use >1 BRAM" - assert sim.needs_minimization(depth, bitwidth) + assert sim._needs_minimization(depth, bitwidth) # Verify that depth with 1 BRAM doesn't need minimization # when it's at minimum block count @@ -414,7 +414,7 @@ def test_large_depths_need_minimization(self): bitwidth = 8 blocks = calculate_bram_blocks(depth, bitwidth) assert blocks == 1 - assert not sim.needs_minimization(depth, bitwidth) + assert not sim._needs_minimization(depth, bitwidth) # Exhaustive test: check that depths with MORE than minimum BRAM blocks # need minimization (unless very close to QSRL threshold) @@ -439,7 +439,7 @@ def test_large_depths_need_minimization(self): # Only expect minimization if blocks > minimum achievable if blocks > min_blocks and depth > math.floor(sim.max_qsrl_depth * 1.1): - assert sim.needs_minimization(depth, bw), ( + assert sim._needs_minimization(depth, bw), ( f"depth={depth}, bw={bw}, blocks={blocks}, min_blocks={min_blocks} " f"should need minimization" ) @@ -457,7 +457,7 @@ def test_minimum_bram_edge_case(self): bitwidth = 1 # Verify the method executes without error - result = sim.needs_minimization(depth, bitwidth) + result = sim._needs_minimization(depth, bitwidth) assert isinstance(result, bool) From 1c11e6ff848a44070d6839305e7c8477fc5fb8eb Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 23 Jan 2026 11:30:34 +0100 Subject: [PATCH 047/170] Working isolated sim, todo: cleanup and remove debug output --- .../finn_xsi/IsolatedSimulationBackend.cpp | 24 ++++++++++++------- .../finn_xsi/include/IsolatedSimulation.hpp | 23 ++++++++++++++++-- .../fpgadataflow/simulation_controller.py | 18 ++++++++++---- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp index 999fdde7c2..63aa8614f2 100644 --- a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp @@ -53,6 +53,7 @@ int main(int argc, const char* argv[]) { // Command processing loop while (true) { // Read message + std::cout << "Awaiting message..." << std::endl; auto request = server.receive_message(); if (!request.has_value()) { std::cout << "Connection closed or error occurred" << std::endl; @@ -61,6 +62,7 @@ int main(int argc, const char* argv[]) { // Process message std::size_t cycles = 0; + json response; std::string command = (*request)["command"]; if (command == "start") { std::cout << "Starting simulation" << std::endl; @@ -71,27 +73,24 @@ int main(int argc, const char* argv[]) { sim.simulate(true); } std::cout << "Simulation initialized. Going into main loop." << std::endl; - { - std::lock_guard guard(simMutex); - sim.simulate(false); - std::cout << "Executed first cycle." << std::endl; - std::cout << "Status: " << sim.getStatus() << std::endl; - std::cout << "Is running: " << sim.isRunning() << std::endl; - } while (!stop.stop_requested()) { std::lock_guard guard(simMutex); - if (cycles % 1000 == 0) { + if (cycles % 10000 == 0) { std::cout << cycles << " " << sim.getStatus() << std::endl; } sim.simulate(false); ++cycles; + if (sim.isDone()) { + break; + } } }); - simThread->join(); } else { std::lock_guard guard(simMutex); sim.resume(); } + response["state"] = "running"; + server.send_message(response); } else if (command == "stop") { std::cout << "Stopping simulation." << std::endl; std::lock_guard guard(simMutex); @@ -99,12 +98,16 @@ int main(int argc, const char* argv[]) { if (simThread.has_value()) { simThread->request_stop(); } + response["state"] = "stopped"; + server.send_message(response); } else if (command == "pause") { std::cout << "Pausing simulation." << std::endl; std::lock_guard guard(simMutex); if (simThread.has_value()) { simThread->request_stop(); } + response["state"] = "halted"; + server.send_message(response); } else if (command == "status") { std::cout << "Sending status update." << std::endl; std::lock_guard guard(simMutex); @@ -112,6 +115,8 @@ int main(int argc, const char* argv[]) { } else { std::cout << "Unknown command " << command << std::endl; std::cerr << "Unknown command " << command << std::endl; + response["state"] = "unknown_command"; + server.send_message(response); } // Exit if stop command received @@ -119,6 +124,7 @@ int main(int argc, const char* argv[]) { break; } } + simThread->join(); } else { throw std::runtime_error("Socket path not provided. Socket communication is required."); } diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index 70c3733494..df6cf7857d 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -155,6 +155,10 @@ class IsolatedSimulation : public Simulation bool isRunning() { return simState.isRunning(); } + bool isDone() { + return !simState.isRunning() && simState.allCyclesProcessed(); + } + /*** * Simulate a single cycle ***/ @@ -167,12 +171,27 @@ class IsolatedSimulation : public Simulation this->clearPorts(); this->reset(); for (S_AXIS_Control& s : this->istreams) { - s.setValid(true); + s.valid(true); + if (!s.isValid()) { + std::cout << "ERROR: Stream should be valid now!" << std::endl; + } } for (M_AXIS_Control& s : this->ostreams) { - s.setReady(true); + s.ready(true); + if (!s.isReady()) { + std::cout << "ERROR: Stream should be ready now!" << std::endl; + } } } + + // Sanity check. Eventually remove + if (!std::all_of(this->istreams.begin(), this->istreams.end(), [](S_AXIS_Control& s) {return s.isValid();})) { + std::cout << "ERROR: An input stream is not valid!" << std::endl; + } + if (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](M_AXIS_Control& s) {return s.isReady();})) { + std::cout << "ERROR: An output stream is not ready!" << std::endl; + } + if (!simState.isRunning()) { std::cout << "Simulation not running! Send \"start\" command first." << std::endl; return; diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index 6683134fbe..0ff37e86dc 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -206,6 +206,7 @@ def _receive_response(self, process_idx: int) -> dict[str, Any] | None: # Read 4-byte length prefix length_bytes = sock.recv(4) if not length_bytes: + self.console.log(f"{process_idx}: Client disconnected.") return None length = int.from_bytes(length_bytes, byteorder="little") @@ -343,6 +344,10 @@ def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: logfile.write("Initializing simulation.\n") proc_idx = self._start_process(binary, process_index) response = self._send_and_receive(proc_idx, "start", {}) + if response is None: + logfile.write("Client disconnected / No answer received to start command!\n") + return None + logfile.write(f"Start response: {response}\n") # Main loop logfile.write("Beginning main loop\n") @@ -353,18 +358,21 @@ def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: logfile.flush() while True: time.sleep(self.poll_interval) - logfile.write("Sending status request") + logfile.write("Sending status request\n") response = self._send_and_receive(proc_idx, "status", {}) if response is None: + logfile.write("Empty response. Returning.\n") return None state = response["state"] if state == "done": return self._postprocess_logs(binary.parent) + + # TODO: Order seems wrong logfile.write( - f"{response['totalCycles']}," - f"{response['inputCyclesDone']}," - f"{response['inputCyclesTarget']}" - f"{response['outputCyclesDone']}," + f"{response['totalCycles']}, " + f"{response['inputCyclesDone']}, " + f"{response['inputCyclesTarget']}, " + f"{response['outputCyclesDone']}, " f"{response['outputCyclesTarget']}\n" ) From a9e04716108e6eb5cc9e6b616bba262f8c0ea0e2 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 23 Jan 2026 11:44:29 +0100 Subject: [PATCH 048/170] Add timeout to communication --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 2 + .../fpgadataflow/simulation_controller.py | 52 ++++++++++++++++--- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 7cbdd60202..acfa12e05d 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -258,6 +258,8 @@ int main(int argc, const char* argv[]) { po::store(po::parse_command_line(argc, argv, desc), vm); po::notify(vm); + std::cout << "Connected Simulation Node Index: " << RTLSimConfig::NodeIndex << " / " << RTLSimConfig::TotalNodes << std::endl; + // Construct simulation SingleNodeSimulation sim( RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, "xsim_log_file.txt", "trace_file.txt", RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs, diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index af90ce8506..67fca468e1 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -200,9 +200,15 @@ def _receive_response(self, process_idx: int) -> dict[str, Any] | None: Returns: Dictionary containing the response, or None if error + + Raises: + TimeoutError: If socket times out waiting for response """ sock, _ = self.sockets[process_idx] + # Set 10 second timeout to prevent deadlocks + sock.settimeout(10.0) + # Read 4-byte length prefix length_bytes = sock.recv(4) if not length_bytes: @@ -238,9 +244,37 @@ def _send_and_receive( """ try: self._send_command(process_idx, command, payload) - return self._receive_response(process_idx) - except (BrokenPipeError, ConnectionResetError): - # Connection error means the subprocess has died + response = self._receive_response(process_idx) + + # If we got None (timeout or connection error), check if process crashed + if response is None: + proc, stdout_file, stderr_file = self.processes[process_idx] + returncode = proc.poll() + + if returncode is not None and returncode != 0: + # Process has terminated with an error + # Flush and read error logs + stdout_file.flush() + stderr_file.flush() + + stdout_log = self.logdir / f"{process_idx}_stdout_cpp.log" + stderr_log = self.logdir / f"{process_idx}_stderr_cpp.log" + + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + + # Raise the actual error from the subprocess + msg = ( + f"Subprocess (process_idx={process_idx}) terminated with" + f" exit code {returncode}.\n" + f"Stderr:\n{stderr_output}\n" + f"Stdout:\n{stdout_output}" + ) + raise RuntimeError(msg) from None + + return response + except (BrokenPipeError, ConnectionResetError, TimeoutError): + # Connection error or timeout means the subprocess may have died # Check if it exited with an error and raise that instead proc, stdout_file, stderr_file = self.processes[process_idx] returncode = proc.poll() @@ -251,8 +285,8 @@ def _send_and_receive( stdout_file.flush() stderr_file.flush() - stdout_log = self.logdir / f"{process_idx}_stdout.log" - stderr_log = self.logdir / f"{process_idx}_stderr.log" + stdout_log = self.logdir / f"{process_idx}_stdout_cpp.log" + stderr_log = self.logdir / f"{process_idx}_stderr_cpp.log" stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" @@ -260,7 +294,7 @@ def _send_and_receive( # Raise the actual error from the subprocess msg = ( f"Subprocess (process_idx={process_idx}) terminated with" - " exit code {returncode}.\n" + f" exit code {returncode}.\n" f"Stderr:\n{stderr_output}\n" f"Stdout:\n{stdout_output}" ) @@ -359,7 +393,7 @@ def run(self) -> None: invalid.append(self.names[i]) if len(invalid) > 0: raise FINNInternalError( - f"Lost connection / malformed response " f"from nodes: {', '.join(invalid)}" + f"Lost connection / malformed response from nodes: {', '.join(invalid)}" ) # TODO: Algorithm @@ -378,6 +412,10 @@ def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: proc_idx = self._start_process(binary, process_index) response = self._send_and_receive(proc_idx, "start", {}) + if response is None: + logfile.write("Failed to start simulation: No response\n") + return None + # Main loop logfile.write("Beginning main loop\n") logfile.write( From 697fbcf42cce0c5f7b3b68a729d36b4c14cb9d19 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 23 Jan 2026 15:42:14 +0100 Subject: [PATCH 049/170] Work on isolated node simulation data --- .../finn_xsi/include/IsolatedSimulation.hpp | 2 +- .../transformation/fpgadataflow/simulation.py | 70 ++++++++++++++++--- .../fpgadataflow/simulation_controller.py | 18 ++--- 3 files changed, 71 insertions(+), 19 deletions(-) diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index b9c44e526d..1fb8f134f5 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -109,7 +109,7 @@ class IsolatedSimulation : public Simulation inline void writeLogEntryValid() { validLog << simState.getCycleStateOutput(); - for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.getOutputValid(); } + for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.getOutputValid() << "\n"; } } public: diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index ad1f897750..35867ad141 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -3,14 +3,14 @@ import json import math import time -from onnx.onnx_ml_pb2 import NodeProto from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames -from typing import Any, TypeAlias, cast +from typing import TYPE_CHECKING, Any, TypeAlias, cast +import finn.transformation.fpgadataflow.simulation_controller.IsolatedSimReturnType as IsoSimData from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP @@ -26,6 +26,9 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import DisabledLoggingConsole, log +if TYPE_CHECKING: + from onnx.onnx_ml_pb2 import NodeProto + FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] @@ -129,7 +132,7 @@ def simulate_node_connected( json.dump(data, output_json.open("w"), indent=4) return data, merged_data.get("timeout_occurred", False) - def simulate_node_isolated(self) -> None: + def simulate_node_isolated(self) -> dict[str, IsoSimData]: """Simulate isolated nodes.""" if self.simulation_type != SimulationType.NODE_BASED_ISOLATED: raise FINNInternalError( @@ -142,10 +145,7 @@ def simulate_node_isolated(self) -> None: controller = NodeIsolatedSimulationController( len(self.binaries), names, list(self.binaries.values()), console, 0.1, False ) - _ = controller.run() - - # TODO: Implement algorithm - raise NotImplementedError() + return controller.run() class RunLayerParallelSimulation(Transformation): # noqa @@ -811,6 +811,9 @@ def calculate_srl16e_depth_range(luts: int, bitwidth: int) -> tuple[int, int]: class RunLayerIsolatedSimulation(Transformation): + """Run a layer isolated simulation and calculate some information for a + later layer parallel simulation.""" + def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: """Run isolated layer simulations.""" super().__init__() @@ -818,6 +821,48 @@ def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: self.clk_ns = clk_ns self.functional_sim = functional_sim + def calculate_upper_bounds(self, data: IsoSimData) -> dict[str, int]: + """Try to calculate an upper bound for the incoming FIFO size of the layers. + Return size indexed by node name.""" + # First get the input ready signals of all layers + # TODO: We assume that every cycle gets recorded here + readies: dict[str, list[int]] = { + name: data[name]["ready"].values()[2] for name in data.keys() + } + + # Calculate the count of _not_ ready cycles between the + # first ready and the first ready of the second sample + # TODO: Currently we simply divide target cycles by 2, since + # TODO: this is multiplied on the C++ side, but this may change in the + # TODO: future + cycles_per_sample: dict[str, int] = {} + for name in data.keys(): + cycles: int = data[name]["ready"].values()[1] + if cycles % 2 != 0: + raise FINNInternalError( + f"Layer {name} has an odd number " + f"of ready cycles per sample. This points " + f"towards a change in the C++ version, " + f"since we currently assume that the number " + f"we get here is twice the number per sample " + f"(since we want to simulate 2 samples). " + f"Getting this error might indicate that this " + f"has to be fixed." + ) + cycles_per_sample[name] = cycles / 2 + + # TODO: This calculation assumes, that if the producer does NOT fire the entire time, + # TODO: the consumer can read at least at the same speed as + # if the producer did, and not slower. + # TODO: (Since this would mean that less data pressure from + # the producer makes the consumer _slower_.) + # TODO: This should usually be the case, but is important to keep in mind. + return { + # num of zeroes = num total - num of ones + name: len(readies[name]) + - sum(readies[name]) + } + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run isolated layer simulations.""" sim = Simulation( @@ -827,8 +872,15 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: self.clk_ns, self.functional_sim, ) - # TODO - _ = sim.simulate_node_isolated() + data: dict[str, IsoSimData] = sim.simulate_node_isolated() + in_fifo_upper_bound = self.calculate_upper_bounds(data) + formatted_upper_bounds = "\n\t".join( + [f"{name}: {in_fifo_upper_bound[name]}" for name in in_fifo_upper_bound.keys()] + ) + log.info("Upper bounds: \n" + formatted_upper_bounds) + + raise NotImplementedError() + # TODO: Integrate data into the layer parallel simulation return model, False diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index 87a444638d..68077c60fc 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -305,6 +305,8 @@ def _cleanup_sockets(self) -> None: class NodeIsolatedSimulationController(SimulationController): """Run simulations for node isolated cases.""" + IsolatedSimReturnType = dict[Literal["valid", "ready"], dict[int, tuple[int, int, list[int]]]] + def __init__( self, parallel_simulations: int, @@ -322,7 +324,7 @@ def __init__( def _postprocess_logs( self, d: Path, readylog_name: str = "readylog.txt", validlog_name: str = "validlog.txt" - ) -> dict[Literal["valid", "ready"], dict[int, tuple[int, int, list[int]]]]: + ) -> IsolatedSimReturnType: """Recieve the directory containing a binary and the simulation logs. If no logs are found raises an error, otherwise return the postprocessed logs: {: (, , [, ...]), ...} @@ -342,7 +344,9 @@ def _postprocess_logs( "valid": {line[0]: (line[1], line[2], line[3:]) for line in validdata}, } - def run(self) -> None: + def run(self) -> dict[str, IsolatedSimReturnType]: + """Run a node isolated simulation and return the collected + input ready / output valid data, indexed based on node names.""" futures: list[Future] = [] with self.console.status(f"Running simulation on every node. Log directory: {self.logdir}"): with ThreadPoolExecutor(len(self.binaries)) as tpe: @@ -352,7 +356,7 @@ def run(self) -> None: self._cleanup_sockets() # Read data - data = {} + data: dict[str, self.IsolatedSimReturnType] = {} invalid = [] for i, future in enumerate(futures): data[self.names[i]] = future.result() @@ -360,13 +364,9 @@ def run(self) -> None: invalid.append(self.names[i]) if len(invalid) > 0: raise FINNInternalError( - f"Lost connection / malformed response " f"from nodes: {', '.join(invalid)}" + f"Lost connection / malformed response from nodes: {', '.join(invalid)}" ) - - # TODO: Algorithm - raise NotImplementedError() - - IsolatedSimReturnType = dict[Literal["valid", "ready"], dict[int, tuple[int, int, list[int]]]] + return data def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: """Run simulation. Returning None if connection is lost.""" From 4d376bd615922f7768d74cbe716d102d8d3caa9a Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 23 Jan 2026 16:14:05 +0100 Subject: [PATCH 050/170] Fix import error --- src/finn/transformation/fpgadataflow/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 35867ad141..28122dba66 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -10,7 +10,6 @@ from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames from typing import TYPE_CHECKING, Any, TypeAlias, cast -import finn.transformation.fpgadataflow.simulation_controller.IsolatedSimReturnType as IsoSimData from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP @@ -30,6 +29,7 @@ from onnx.onnx_ml_pb2 import NodeProto FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] +IsoSimData = NodeIsolatedSimulationController.IsolatedSimReturnType class Simulation: From 66390a8a608ce6f4e9707c8c74399cdfe116a527 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:30:57 +0100 Subject: [PATCH 051/170] Revert simulation connection --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 4 +- .../finn_xsi/include/CommunicationChannel.hpp | 9 ++-- .../finn_xsi/include/IsolatedSimulation.hpp | 4 +- finn_xsi/finn_xsi/include/Simulation.hpp | 46 ++++++++----------- .../transformation/fpgadataflow/simulation.py | 1 + .../fpgadataflow/simulation_controller.py | 4 +- 6 files changed, 31 insertions(+), 37 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index acfa12e05d..7b3a620ac9 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -84,6 +84,8 @@ class SimulationController { sim.setFIFODepth(i, fifo_depths[depth_idx]); } + std::cout << "Starting simulation with max cycles: " << max_cycles << std::endl; + // Run the simulation bool timeout = sim.runToStableState(stoken, max_cycles); @@ -99,6 +101,7 @@ class SimulationController { } } catch (const std::exception& e) { std::lock_guard error_lock(state_mutex); + std::cout << "Simulation error: " << e.what() << std::endl; error_message = e.what(); state = SimulationState::ERROR; } @@ -169,7 +172,6 @@ class SimulationController { status["message"] = error_message; break; } - return status; } diff --git a/finn_xsi/finn_xsi/include/CommunicationChannel.hpp b/finn_xsi/finn_xsi/include/CommunicationChannel.hpp index 871b909667..1f94659da9 100644 --- a/finn_xsi/finn_xsi/include/CommunicationChannel.hpp +++ b/finn_xsi/finn_xsi/include/CommunicationChannel.hpp @@ -3,6 +3,7 @@ #include #include +#include template concept ChannelInterface = requires(T t, bool b, std::stop_token stoken) { @@ -48,10 +49,10 @@ class CommunicationChannel { this->setOutputReady(ready, stoken); } - virtual bool getOutputValid([[maybe_unused]] std::stop_token stoken = {}) { return false; } - virtual void setInputValid([[maybe_unused]] bool v, [[maybe_unused]] std::stop_token stoken = {}) {} - virtual bool getInputReady([[maybe_unused]] std::stop_token stoken = {}) { return false; } - virtual void setOutputReady([[maybe_unused]] bool r, [[maybe_unused]] std::stop_token stoken = {}) {} + virtual bool getOutputValid([[maybe_unused]] std::stop_token stoken = {}) = 0; + virtual void setInputValid([[maybe_unused]] bool v, [[maybe_unused]] std::stop_token stoken = {}) = 0; + virtual bool getInputReady([[maybe_unused]] std::stop_token stoken = {}) = 0; + virtual void setOutputReady([[maybe_unused]] bool r, [[maybe_unused]] std::stop_token stoken = {}) = 0; virtual ~CommunicationChannel() = default; }; diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index b9c44e526d..ddec10e83e 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -179,10 +179,10 @@ class IsolatedSimulation : public Simulation } // Sanity check. Eventually remove - if (!std::all_of(this->istreams.begin(), this->istreams.end(), [](S_AXIS_Control& s) {return s.isValid();})) { + if (!std::all_of(this->istreams.begin(), this->istreams.end(), [](S_AXIS_Control& s) {return s.getOutputValid();})) { std::cout << "ERROR: An input stream is not valid!" << std::endl; } - if (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](M_AXIS_Control& s) {return s.isReady();})) { + if (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](M_AXIS_Control& s) {return s.getInputReady();})) { std::cout << "ERROR: An output stream is not ready!" << std::endl; } diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 48c07d0b30..b0467be459 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -90,8 +90,8 @@ class Simulation { } }; -//Small struct used for exange. Will be changed later to more complex data structure. -struct CommData{ +// Small struct used for exange. Will be changed later to more complex data structure. +struct CommData { bool data; }; @@ -106,8 +106,8 @@ struct CommData{ // └──────────────────────────────────────┘ template class SingleNodeSimulation : public Simulation { - using ConsumingInterface = InterprocessCommunicationChannelInterface; - using ProducingInterface = InterprocessCommunicationChannelInterface; + using ConsumingInterface = InterprocessCommunicationChannel; + using ProducingInterface = InterprocessCommunicationChannel; constexpr static bool FirstNode = NodeIndex == 0; constexpr static bool LastNode = NodeIndex == (TotalNodes - 1); std::array fromProducerInterface; @@ -120,16 +120,19 @@ class SingleNodeSimulation : public Simulation SIM - fromProducerInterface[i].exchangeDataDownstream(stoken); + // Interface SHM <-> sim + this->istreams[i].setInputValid(fromProducerInterface[i].receive_request(stoken).data); + fromProducerInterface[i].send_response(CommData{this->istreams[i].getInputReady()}); } } if constexpr (!LastNode) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - // Interface SIM <-> FIFO - this->ostreams[i].exchangeDataDownstream(stoken); + // Interface sim -valid-> FIFO + this->fifo[i].setInputValid(this->ostreams[i].getOutputValid(), stoken); // Interface FIFO <-> SHM - this->fifo[i].exchangeDataDownstream(stoken); + this->fifo[i].setOutputReady(toConsumerInterface[i].send_request(CommData{this->fifo[i].getOutputValid()}, stoken).data, stoken); + // FIFO -ready-> sim + this->ostreams[i].setOutputReady(this->fifo[i].getInputReady()); // Toggle FIFO clock this->fifo[i].toggleClock(); } @@ -167,16 +170,6 @@ class SingleNodeSimulation : public Simulationistreams[i]); - } - for (std::size_t i = 0; i < OStreamsSize; ++i) { - this->ostreams[i].connectDownstream(fifo[i]); - fifo[i].connectDownstream(toConsumerInterface[i]); - } - } - [[gnu::hot, gnu::always_inline]] void runSingleCycle(std::stop_token stoken = {}) { ++cyclesRun; communicate(stoken); @@ -252,8 +245,8 @@ class SingleNodeSimulation : public Simulation::max()) { - while (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](const M_AXIS_Control& stream) { return stream.stableState.is_stable(); }) & - !stoken.stop_requested() & (cyclesRun <= max_cycles)) { + while (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](const M_AXIS_Control& stream) { return stream.stableState.is_stable(); }) & !stoken.stop_requested() & + (cyclesRun <= max_cycles)) { runSingleCycle(stoken); runSingleCycle(stoken); runSingleCycle(stoken); @@ -276,10 +269,7 @@ class SingleNodeSimulation : public Simulation= OStreamsSize) { - auto error = "FIFO index " - + std::to_string(index) - + " out of range (max: " - + std::to_string(OStreamsSize - 1) + ")"; + auto error = "FIFO index " + std::to_string(index) + " out of range (max: " + std::to_string(OStreamsSize - 1) + ")"; throw std::out_of_range(error); } fifo[index].setMaxSize(depth); @@ -329,13 +319,13 @@ class SingleNodeSimulation : public Simulation getOStreamStableStateIntervals() const noexcept { std::array intervals{}; if constexpr (LastNode) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - intervals[i] = this->ostreams[i].interval; - } + intervals[i] = this->ostreams[i].interval; + } } return intervals; } diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index ad1f897750..3c4330fd9f 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -80,6 +80,7 @@ def simulate(self, *args: Any, **kwargs: Any) -> Any: cannot provide typing information.""" match self.simulation_type: case SimulationType.NODE_BASED_CONNECTED: + print("Connected simulation") return self.simulate_node_connected(*args, **kwargs) case SimulationType.NODE_BASED_ISOLATED: return self.simulate_node_isolated(*args, **kwargs) diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index 2a494bb9c9..60347d9024 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -274,7 +274,7 @@ def _send_and_receive( raise RuntimeError(msg) from None return response - except (BrokenPipeError, ConnectionResetError, TimeoutError): + except (BrokenPipeError, ConnectionResetError, TimeoutError) as err: # Connection error or timeout means the subprocess may have died # Check if it exited with an error and raise that instead proc, stdout_file, stderr_file = self.processes[process_idx] @@ -299,7 +299,7 @@ def _send_and_receive( f"Stderr:\n{stderr_output}\n" f"Stdout:\n{stdout_output}" ) - raise RuntimeError(msg) from None + raise RuntimeError(msg) from err # from None # If process exited cleanly (returncode == 0) or hasn't exited yet, # this is an unexpected connection error From e422be1c31fe4b3bbb99db54ecc30727e83b8d91 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Mon, 26 Jan 2026 15:23:05 +0100 Subject: [PATCH 052/170] Working isolated simulation, first bounds results --- .../finn_xsi/include/IsolatedSimulation.hpp | 22 ++------ .../transformation/fpgadataflow/simulation.py | 53 ++++++++++++++----- .../fpgadataflow/simulation_controller.py | 18 +++---- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index 1fb8f134f5..af5030d58a 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -1,5 +1,4 @@ #include -#include #include "SocketServer.h" @@ -21,7 +20,7 @@ class IsolatedSimulation : public Simulation readyLog << "," << s.name; } readyLog << std::endl; - validLog << "totalCycles,outputCycles,doubled_targetOutputCycles" << std::endl; + validLog << "totalCycles,outputCycles,doubled_targetOutputCycles"; for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.name; } @@ -70,12 +69,6 @@ class IsolatedSimulation : public Simulation inputLargestStreamIndex = std::get<0>(largestIn); outputCyclesTarget = std::get<1>(largestOut) * 2; outputLargestStreamIndex = std::get<0>(largestOut); - std::cout << "In Job Sizes: "; - for (auto js : sim.inJobSizes) { - std::cout << js << " "; - } - std::cout << std::endl; - std::cout << "IO cycle targets: " << inputCyclesTarget << ", " << outputCyclesTarget << std::endl; } inline bool inputCyclesProcessed() { return inputCyclesDone >= inputCyclesTarget; } inline bool outputCyclesProcessed() { return outputCyclesDone >= outputCyclesTarget; } @@ -105,11 +98,13 @@ class IsolatedSimulation : public Simulation inline void writeLogEntryReady () { readyLog << simState.getCycleStateInput(); for (S_AXIS_Control& s : this->istreams) { readyLog << "," << s.getInputReady(); } + readyLog << std::endl; } inline void writeLogEntryValid() { validLog << simState.getCycleStateOutput(); - for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.getOutputValid() << "\n"; } + for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.getOutputValid(); } + validLog << std::endl; } public: @@ -139,6 +134,7 @@ class IsolatedSimulation : public Simulation outJobSizes.begin(), [](StreamDescriptor& s) { return s.job_size; } ); + writeLogHeaders(); } json getStatus() { @@ -178,14 +174,6 @@ class IsolatedSimulation : public Simulation } } - // Sanity check. Eventually remove - if (!std::all_of(this->istreams.begin(), this->istreams.end(), [](S_AXIS_Control& s) {return s.isValid();})) { - std::cout << "ERROR: An input stream is not valid!" << std::endl; - } - if (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](M_AXIS_Control& s) {return s.isReady();})) { - std::cout << "ERROR: An output stream is not ready!" << std::endl; - } - if (!simState.isRunning()) { std::cout << "Simulation not running! Send \"start\" command first." << std::endl; return; diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 28122dba66..8588721138 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -29,7 +29,8 @@ from onnx.onnx_ml_pb2 import NodeProto FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] -IsoSimData = NodeIsolatedSimulationController.IsolatedSimReturnType +IsoLayerSimData = NodeIsolatedSimulationController.IsolatedSimReturnType +IsoSimData = dict[str, IsoLayerSimData] # Indexed by layer name class Simulation: @@ -132,7 +133,7 @@ def simulate_node_connected( json.dump(data, output_json.open("w"), indent=4) return data, merged_data.get("timeout_occurred", False) - def simulate_node_isolated(self) -> dict[str, IsoSimData]: + def simulate_node_isolated(self) -> dict[str, IsoLayerSimData]: """Simulate isolated nodes.""" if self.simulation_type != SimulationType.NODE_BASED_ISOLATED: raise FINNInternalError( @@ -821,13 +822,31 @@ def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: self.clk_ns = clk_ns self.functional_sim = functional_sim - def calculate_upper_bounds(self, data: IsoSimData) -> dict[str, int]: + def calculate_upper_bounds(self, data: IsoSimData) -> dict[str, list[int]]: """Try to calculate an upper bound for the incoming FIFO size of the layers. - Return size indexed by node name.""" + Return size indexed by node name. + + >>> step = RunLayerIsolatedSimulation("", 0.0, False) + >>> bounds = step.calculate_upper_bounds( + ... {"A": {"ready": [(0, 10, [1,1,0]), (1, 10, [0,0,0]), (2, 10, [1,0,0])]}, + ... "B": {"ready": [(0, 10, [1]), (1, 10, [0]), (2, 10, [0])]}}) + >>> bounds["A"] + [1, 2, 3] + >>> bounds["B"] + [2] + """ # First get the input ready signals of all layers # TODO: We assume that every cycle gets recorded here - readies: dict[str, list[int]] = { - name: data[name]["ready"].values()[2] for name in data.keys() + + # How many input channels each layer has + input_channel_count: dict[str, int] = {} + for name in data.keys(): + input_channel_count[name] = len(data[name]["ready"][0][2]) + + # Map layer name to ready signals: + # {"Layer1": [[1], [0], [1], ...], ...} + readies: dict[str, list[list[int]]] = { + name: [line[2] for line in data[name]["ready"]] for name in data.keys() } # Calculate the count of _not_ ready cycles between the @@ -837,7 +856,8 @@ def calculate_upper_bounds(self, data: IsoSimData) -> dict[str, int]: # TODO: future cycles_per_sample: dict[str, int] = {} for name in data.keys(): - cycles: int = data[name]["ready"].values()[1] + cycles: int = int(data[name]["ready"][0][1]) + cycles_per_sample[name] = cycles if cycles % 2 != 0: raise FINNInternalError( f"Layer {name} has an odd number " @@ -849,7 +869,7 @@ def calculate_upper_bounds(self, data: IsoSimData) -> dict[str, int]: f"Getting this error might indicate that this " f"has to be fixed." ) - cycles_per_sample[name] = cycles / 2 + cycles_per_sample[name] = int(cycles / 2) # TODO: This calculation assumes, that if the producer does NOT fire the entire time, # TODO: the consumer can read at least at the same speed as @@ -857,11 +877,16 @@ def calculate_upper_bounds(self, data: IsoSimData) -> dict[str, int]: # TODO: (Since this would mean that less data pressure from # the producer makes the consumer _slower_.) # TODO: This should usually be the case, but is important to keep in mind. - return { - # num of zeroes = num total - num of ones - name: len(readies[name]) - - sum(readies[name]) - } + non_ready_cycles = {} + for name in data.keys(): + non_ready_cycles[name] = [0] * input_channel_count[name] + # State of all axi ready signals in a given cycle + for axi_stream_ready_list in readies[name]: + # Count all 0 + # Example: [1,1,0] would mean streams 0 and 1 are ready, stream 2 is not + for i, rdy in enumerate(axi_stream_ready_list): + non_ready_cycles[name][i] += int(rdy == 0) + return non_ready_cycles def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run isolated layer simulations.""" @@ -872,7 +897,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: self.clk_ns, self.functional_sim, ) - data: dict[str, IsoSimData] = sim.simulate_node_isolated() + data: dict[str, IsoLayerSimData] = sim.simulate_node_isolated() in_fifo_upper_bound = self.calculate_upper_bounds(data) formatted_upper_bounds = "\n\t".join( [f"{name}: {in_fifo_upper_bound[name]}" for name in in_fifo_upper_bound.keys()] diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index b81307f8ff..fe17818166 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -339,7 +339,7 @@ def _cleanup_sockets(self) -> None: class NodeIsolatedSimulationController(SimulationController): """Run simulations for node isolated cases.""" - IsolatedSimReturnType = dict[Literal["valid", "ready"], dict[int, tuple[int, int, list[int]]]] + IsolatedSimReturnType = dict[Literal["valid", "ready"], list[tuple[int, int, list[int]]]] def __init__( self, @@ -367,15 +367,13 @@ def _postprocess_logs( validlog = d / validlog_name if not readylog.exists() or not validlog.exists(): raise FINNInternalError(f"Could not find simulation logs at {readylog} and {validlog}") - readydata = [ - [int(elem) for elem in line.split(",")] for line in readylog.read_text().split("\n")[1:] - ] - validdata = [ - [int(elem) for elem in line.split(",")] for line in validlog.read_text().split("\n")[1:] - ] + readylines = readylog.read_text().splitlines()[1:] + validlines = validlog.read_text().splitlines()[1:] + readydata = [[int(elem) for elem in line.split(",")] for line in readylines if line != ""] + validdata = [[int(elem) for elem in line.split(",")] for line in validlines if line != ""] return { - "ready": {line[0]: (line[1], line[2], line[3:]) for line in readydata}, - "valid": {line[0]: (line[1], line[2], line[3:]) for line in validdata}, + "ready": [(line[1], line[2], line[3:]) for line in readydata], + "valid": [(line[1], line[2], line[3:]) for line in validdata], } def run(self) -> dict[str, IsolatedSimReturnType]: @@ -428,6 +426,7 @@ def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: "outputCyclesDone,outputCyclesTarget\n" ) logfile.flush() + while True: time.sleep(self.poll_interval) logfile.write("Sending status request\n") @@ -437,6 +436,7 @@ def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: return None state = response["state"] if state == "done": + self.console.log(f"{process_index} is done and postprocessing data.") return self._postprocess_logs(binary.parent) # TODO: Order seems wrong From 18ac0dced8652325011e78010f38627b4acd9125 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 27 Jan 2026 14:44:03 +0100 Subject: [PATCH 053/170] Reimplemented bounds calculation + doctests --- .../finn_xsi/IsolatedSimulationBackend.cpp | 2 + .../finn_xsi/include/IsolatedSimulation.hpp | 83 ++++--- .../transformation/fpgadataflow/simulation.py | 229 +++++++++++++----- .../fpgadataflow/simulation_controller.py | 29 +-- 4 files changed, 235 insertions(+), 108 deletions(-) diff --git a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp index 63aa8614f2..d8d26a9855 100644 --- a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp @@ -81,6 +81,7 @@ int main(int argc, const char* argv[]) { sim.simulate(false); ++cycles; if (sim.isDone()) { + sim.commitLogsToDisk(true); break; } } @@ -98,6 +99,7 @@ int main(int argc, const char* argv[]) { if (simThread.has_value()) { simThread->request_stop(); } + sim.commitLogsToDisk(true); response["state"] = "stopped"; server.send_message(response); } else if (command == "pause") { diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index af5030d58a..88e46b61ff 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -5,29 +5,13 @@ template class IsolatedSimulation : public Simulation { enum class LogType {READY, VALID}; - std::ofstream readyLog; // Input side - std::ofstream validLog; // Output side + std::string readylogName; + std::string validlogName; + json readyJson; + json validJson; std::vector inJobSizes; std::vector outJobSizes; - - /** - * Write CSV style headers to the files - **/ - void writeLogHeaders() { - readyLog << "totalCycles,inputCycles,doubled_targetInputCycles"; - for (S_AXIS_Control& s: this->istreams) { - readyLog << "," << s.name; - } - readyLog << std::endl; - validLog << "totalCycles,outputCycles,doubled_targetOutputCycles"; - for (M_AXIS_Control& s : this->ostreams) { - validLog << "," << s.name; - } - validLog << std::endl; - } - - /** * For the given streams check which has the largest job size, and return a tuple * (stream_index, job_size) for that stream. @@ -93,20 +77,34 @@ class IsolatedSimulation : public Simulation } }; - SimState simState; - inline void writeLogEntryReady () { - readyLog << simState.getCycleStateInput(); - for (S_AXIS_Control& s : this->istreams) { readyLog << "," << s.getInputReady(); } - readyLog << std::endl; + /** Log the ready and valid signals to the JSON fields **/ + void logReady() { + json j; + j["totalCycles"] = simState.totalCycles; + j["inputCyclesDone"] = simState.inputCyclesDone; + j["inputCyclesTarget"] = simState.inputCyclesTarget; + j["ready"] = json::object(); + std::transform(this->istreams.begin(), this->istreams.end(), j["ready"].begin(), [](S_AXIS_Control& s) { + return s.getInputReady(); + }); + readyJson.push_back(j); } - inline void writeLogEntryValid() { - validLog << simState.getCycleStateOutput(); - for (M_AXIS_Control& s : this->ostreams) { validLog << "," << s.getOutputValid(); } - validLog << std::endl; + void logValid() { + json j; + j["totalCycles"] = simState.totalCycles; + j["outputCyclesDone"] = simState.outputCyclesDone; + j["outputCyclesTarget"] = simState.outputCyclesTarget; + j["valid"] = json::object(); + std::transform(this->ostreams.begin(), this->ostreams.end(), j["valid"].begin(), [](M_AXIS_Control& s) { + return s.getOutputValid(); + }); + validJson.push_back(j); } + SimState simState; + public: IsolatedSimulation( const std::string& kernel_lib, @@ -114,12 +112,11 @@ class IsolatedSimulation : public Simulation const char* xsim_log_file, const char* trace_file, std::array _istream_descs, - std::array _ostream_descs, - const std::string readyLogPath, - const std::string validLogPath + std::array _ostream_descs ) : Simulation( kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs - ), readyLog(readyLogPath), validLog(validLogPath), simState(*this) { + ), simState(*this), readyJson(json::array()), validJson(json::array()), + readylogName("readylog.txt"), validlogName("validlog") { inJobSizes.resize(_istream_descs.size()); outJobSizes.resize(_ostream_descs.size()); std::transform( @@ -134,9 +131,23 @@ class IsolatedSimulation : public Simulation outJobSizes.begin(), [](StreamDescriptor& s) { return s.job_size; } ); - writeLogHeaders(); } + /** Write logs to disk **/ + void commitLogsToDisk(bool clearLogs = true) { + std::ofstream r(readylogName); + std::ofstream v(validlogName); + r << std::setw(4) << readyJson; + v << std::setw(4) << validJson; + r.close(); + v.close(); + if (clearLogs) { + readyJson = json::array(); + validJson = json::array(); + } + } + + json getStatus() { return simState.getStatus(); } @@ -179,8 +190,8 @@ class IsolatedSimulation : public Simulation return; } if (!simState.allCyclesProcessed()) { - writeLogEntryReady(); - writeLogEntryValid(); + logValid(); + logReady(); if (!simState.inputCyclesProcessed() && this->istreams[simState.inputLargestStreamIndex].getInputReady()) { ++simState.inputCyclesDone; diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 789bd2e380..16d2ff6fad 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -29,8 +29,8 @@ from onnx.onnx_ml_pb2 import NodeProto FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] -IsoLayerSimData = NodeIsolatedSimulationController.IsolatedSimReturnType -IsoSimData = dict[str, IsoLayerSimData] # Indexed by layer name +IsoSimLogData = NodeIsolatedSimulationController.IsolatedSimLogData +IsoSimLogDataByLayer = dict[str, IsoSimLogData] # Indexed by layer name class Simulation: @@ -134,7 +134,7 @@ def simulate_node_connected( json.dump(data, output_json.open("w"), indent=4) return data, merged_data.get("timeout_occurred", False) - def simulate_node_isolated(self) -> dict[str, IsoLayerSimData]: + def simulate_node_isolated(self) -> IsoSimLogDataByLayer: """Simulate isolated nodes.""" if self.simulation_type != SimulationType.NODE_BASED_ISOLATED: raise FINNInternalError( @@ -823,54 +823,83 @@ def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: self.clk_ns = clk_ns self.functional_sim = functional_sim - def calculate_upper_bounds(self, data: IsoSimData) -> dict[str, list[int]]: + def calculate_upper_bounds(self, data: IsoSimLogDataByLayer) -> dict[str, dict[str, int]]: """Try to calculate an upper bound for the incoming FIFO size of the layers. - Return size indexed by node name. + Return size indexed by layer name and stream name. >>> step = RunLayerIsolatedSimulation("", 0.0, False) - >>> bounds = step.calculate_upper_bounds( - ... {"A": {"ready": [(0, 10, [1,1,0]), (1, 10, [0,0,0]), (2, 10, [1,0,0])]}, - ... "B": {"ready": [(0, 10, [1]), (1, 10, [0]), (2, 10, [0])]}}) + >>> bounds = step.calculate_upper_bounds({ + ... "A": { + ... "ready": [ + ... {"totalCycles": 43, "inputCyclesDone": 12, + ... "inputCyclesTarget": 24, "s_axi_0": 1, "s_axi_1": 0}, + ... {"totalCycles": 44, "inputCyclesDone": 13, + ... "inputCyclesTarget": 24, "s_axi_0": 0, "s_axi_1": 0}, + ... ], "valid": [] + ... }, + ... "B": { + ... "ready": [ + ... {"totalCycles": 100, "inputCyclesDone": 3, + ... "inputCyclesTarget": 10, "s_axi_0": 1, "s_axi_1": 1, + ... "s_axi_2": 0}, + ... ], "valid": [] + ... }, + ... "C": { + ... "ready": [ + ... {"totalCycles": 43, "inputCyclesDone": 14, + ... "inputCyclesTarget": 24, "s_axi_0": 1, "s_axi_1": 0}, + ... {"totalCycles": 44, "inputCyclesDone": 15, + ... "inputCyclesTarget": 24, "s_axi_0": 0, "s_axi_1": 0}, + ... ], "valid": [] + ... } + ... }) >>> bounds["A"] - [1, 2, 3] + {'s_axi_0': 1, 's_axi_1': 2} >>> bounds["B"] - [2] + {'s_axi_0': 0, 's_axi_1': 0, 's_axi_2': 1} + >>> bounds["C"] + {'s_axi_0': 0, 's_axi_1': 0} """ - # First get the input ready signals of all layers - # TODO: We assume that every cycle gets recorded here - - # How many input channels each layer has - input_channel_count: dict[str, int] = {} - for name in data.keys(): - input_channel_count[name] = len(data[name]["ready"][0][2]) - - # Map layer name to ready signals: - # {"Layer1": [[1], [0], [1], ...], ...} - readies: dict[str, list[list[int]]] = { - name: [line[2] for line in data[name]["ready"]] for name in data.keys() - } - - # Calculate the count of _not_ ready cycles between the - # first ready and the first ready of the second sample - # TODO: Currently we simply divide target cycles by 2, since - # TODO: this is multiplied on the C++ side, but this may change in the - # TODO: future - cycles_per_sample: dict[str, int] = {} - for name in data.keys(): - cycles: int = int(data[name]["ready"][0][1]) - cycles_per_sample[name] = cycles - if cycles % 2 != 0: - raise FINNInternalError( - f"Layer {name} has an odd number " - f"of ready cycles per sample. This points " - f"towards a change in the C++ version, " - f"since we currently assume that the number " - f"we get here is twice the number per sample " - f"(since we want to simulate 2 samples). " - f"Getting this error might indicate that this " - f"has to be fixed." - ) - cycles_per_sample[name] = int(cycles / 2) + + # TODO: Proper pytest tests + def _any_ready(cycle_data: dict[str, int]) -> bool: + for key in cycle_data.keys(): + if ( + key not in ["totalCycles", "inputCyclesDone", "inputCyclesTarget"] + and cycle_data[key] == 1 + ): + return True + return False + + results: dict[str, dict[str, int]] = {} + for layer in data.keys(): + # Save all keys that are not + results[layer] = { + stream_name: 0 + for stream_name in data[layer]["ready"][0].keys() + if stream_name not in ["inputCyclesDone", "inputCyclesTarget", "totalCycles"] + } + for cycle_data in data[layer]["ready"]: + if cycle_data["inputCyclesDone"] > int( + cycle_data["inputCyclesTarget"] / 2 + ) and _any_ready(cycle_data): + break + for stream_name in results[layer].keys(): + # TODO: Currently on the C++ side we multiply the + # TODO: target cycles by 2, to get two samples + # TODO: We keep track of ready signals until we see + # TODO: the first ready after half of all cycles were seen. + # TODO: This might change in the future + if cycle_data["inputCyclesTarget"] % 2 != 0: + raise FINNInternalError( + f"An 'inputCyclesTarget' of layer {layer} seems " + f"to not be an even number. Currently, we double " + f"the target simulation cycles for every layer " + f"on the C++ side. This error may point towards " + f"a change on the C++ side, which may cause the " + f"need to update this function accordingly!" + ) + results[layer][stream_name] += int(cycle_data[stream_name] == 0) # TODO: This calculation assumes, that if the producer does NOT fire the entire time, # TODO: the consumer can read at least at the same speed as @@ -878,16 +907,103 @@ def calculate_upper_bounds(self, data: IsoSimData) -> dict[str, list[int]]: # TODO: (Since this would mean that less data pressure from # the producer makes the consumer _slower_.) # TODO: This should usually be the case, but is important to keep in mind. - non_ready_cycles = {} - for name in data.keys(): - non_ready_cycles[name] = [0] * input_channel_count[name] - # State of all axi ready signals in a given cycle - for axi_stream_ready_list in readies[name]: - # Count all 0 - # Example: [1,1,0] would mean streams 0 and 1 are ready, stream 2 is not - for i, rdy in enumerate(axi_stream_ready_list): - non_ready_cycles[name][i] += int(rdy == 0) - return non_ready_cycles + return results + + def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: + """Do checks on the returned data to make sure it is in spec. + + A correctly formatted example would be: + >>> data = { + ... "layer1": { + ... "ready": [{"totalCycles": 10, "inputCyclesDone": 5, + ... "inputCyclesTarget": 10, "s_axi0_ready": 1}], + ... "valid": [{"totalCycles": 10, "outputCyclesDone": 5, + ... "outputCyclesTarget": 10, "m_axi0_valid": 1}] + ... } + ... } + >>> sim = RunLayerIsolatedSimulation("", 0.0, False) + >>> sim.sanity_check_logged_data(data) + >>> + """ + # 0. Valid and ready are present + for layer, ldata in data.items(): + if "valid" not in ldata.keys(): + raise FINNInternalError( + f"Simulation log data of layer " f"{layer} is missing the VALID log." + ) + if "ready" not in ldata.keys(): + raise FINNInternalError( + f"Simulation log data of layer " f"{layer} is missing the READY log." + ) + # 1. All cycle datas are uniform and have at least one stream signal + for layer, ldata in data.items(): + cycle_data = ldata["ready"] + ldata["valid"] + lengths: set[int] = {len(cycle.keys()) for cycle in cycle_data} + if len(lengths) != 1: + raise FINNInternalError( + f"Simulation log data inconsistent for layer " + f"{layer}. Differing number of fields per cycle." + ) + if next(iter(lengths)) < 4: + raise FINNInternalError( + f"Simulation for layer {layer} must contain " + f"atleast 4 fields (total cycles, AXI cycles " + f"done, AXI cycles target and at least one AXI " + f"ready/valid signal)!" + ) + # 2. All ready logs contain the required keywords + readykeys = ["inputCyclesDone", "inputCyclesTarget", "totalCycles"] + for rlayer, rdata in data.items(): + for cycle in rdata["ready"]: + if any(keyword not in cycle.keys() for keyword in readykeys): + raise FINNInternalError( + f"Simulation READY log of layer {rlayer} " + f"contains cycles that are missing a required key." + ) + if any(key not in readykeys and "axi" not in key for key in cycle.keys()): + raise FINNInternalError( + f"In the READY simulation log of layer " + f"{rlayer} there seem to be fields that " + f"are not expected keywords or AXI streams!" + ) + # 3. All valid logs contain the required keywords + validkeys = ["outputCyclesDone", "outputCyclesTarget", "totalCycles"] + for vlayer, vdata in data.items(): + for cycle in vdata["valid"]: + if any(keyword not in cycle.keys() for keyword in validkeys): + raise FINNInternalError( + f"Simulation VALID log of layer {vlayer} " + f"contains cycles that are missing a required key." + ) + if any(key not in validkeys and "axi" not in key for key in cycle.keys()): + raise FINNInternalError( + f"In the VALID simulation log of layer " + f"{vlayer} there seem to be fields that " + f"are not expected keywords or AXI streams!" + ) + # 4. Cycles done can never be larger then the number of total cycles passed in the sim + for layer, cdata in data.items(): + for line in cdata["ready"] + cdata["valid"]: + if ( + "inputCyclesDone" in line.keys() + and line["inputCyclesDone"] > line["totalCycles"] + ): + raise FINNInternalError( + f"Simulation log of layer {layer} looks incorrect: " + f"Number of active receiving cycles " + f"({line['inputCyclesDone']}) larger than number of " + f"total cycles passed ({line['totalCycles']})." + ) + if ( + "outputCyclesDone" in line.keys() + and line["outputCyclesDone"] > line["totalCycles"] + ): + raise FINNInternalError( + f"Simulation log of layer {layer} looks incorrect: " + f"Number of active producing cycles " + f"({line['outputCyclesDone']}) larger than number of " + f"total cycles passed ({line['totalCycles']})." + ) def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run isolated layer simulations.""" @@ -898,7 +1014,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: self.clk_ns, self.functional_sim, ) - data: dict[str, IsoLayerSimData] = sim.simulate_node_isolated() + data: IsoSimLogDataByLayer = sim.simulate_node_isolated() + self.sanity_check_logged_data(data) in_fifo_upper_bound = self.calculate_upper_bounds(data) formatted_upper_bounds = "\n\t".join( [f"{name}: {in_fifo_upper_bound[name]}" for name in in_fifo_upper_bound.keys()] diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index fccf537428..134baf6770 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -339,7 +339,7 @@ def _cleanup_sockets(self) -> None: class NodeIsolatedSimulationController(SimulationController): """Run simulations for node isolated cases.""" - IsolatedSimReturnType = dict[Literal["valid", "ready"], list[tuple[int, int, list[int]]]] + IsolatedSimLogData = dict[Literal["ready", "valid"], list[dict[str, int]]] def __init__( self, @@ -356,27 +356,23 @@ def __init__( ) self.console.log("Started simulation controller") - def _postprocess_logs( + def postprocess_logs( self, d: Path, readylog_name: str = "readylog.txt", validlog_name: str = "validlog.txt" - ) -> IsolatedSimReturnType: + ) -> IsolatedSimLogData: """Recieve the directory containing a binary and the simulation logs. - If no logs are found raises an error, otherwise return the postprocessed logs: - {: (, , [, ...]), ...} - """ # noqa + If no logs are found raises an error, otherwise return the postprocessed logs + read from JSON. + """ readylog = d / readylog_name validlog = d / validlog_name if not readylog.exists() or not validlog.exists(): raise FINNInternalError(f"Could not find simulation logs at {readylog} and {validlog}") - readylines = readylog.read_text().splitlines()[1:] - validlines = validlog.read_text().splitlines()[1:] - readydata = [[int(elem) for elem in line.split(",")] for line in readylines if line != ""] - validdata = [[int(elem) for elem in line.split(",")] for line in validlines if line != ""] return { - "ready": [(line[1], line[2], line[3:]) for line in readydata], - "valid": [(line[1], line[2], line[3:]) for line in validdata], + "ready": json.loads(readylog.read_text()), + "valid": json.loads(validlog.read_text()), } - def run(self) -> dict[str, IsolatedSimReturnType]: + def run(self) -> dict[str, IsolatedSimLogData]: """Run a node isolated simulation and return the collected input ready / output valid data, indexed based on node names.""" futures: list[Future] = [] @@ -388,7 +384,7 @@ def run(self) -> dict[str, IsolatedSimReturnType]: self._cleanup_sockets() # Read data - data: dict[str, self.IsolatedSimReturnType] = {} + data: dict[str, self.IsolatedSimLogData] = {} invalid = [] for i, future in enumerate(futures): data[self.names[i]] = future.result() @@ -400,7 +396,7 @@ def run(self) -> dict[str, IsolatedSimReturnType]: ) return data - def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: + def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: """Run simulation. Returning None if connection is lost.""" process_index = self.binaries.index(binary) with ( @@ -432,12 +428,13 @@ def _run_binary(self, binary: Path) -> IsolatedSimReturnType | None: logfile.write("Sending status request\n") response = self._send_and_receive(proc_idx, "status", {}) if response is None: + self.console.log(f"Empty response from {proc_idx} at {binary.parent}") logfile.write("Empty response. Returning.\n") return None state = response["state"] if state == "done": self.console.log(f"{process_index} is done and postprocessing data.") - return self._postprocess_logs(binary.parent) + return self.postprocess_logs(binary.parent) # TODO: Order seems wrong logfile.write( From 72e7c7701bbf1bdae1a8a2629ceddb9661a7b6da Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 27 Jan 2026 15:20:29 +0100 Subject: [PATCH 054/170] Restructured simulation code. Fixed some bugs in isolated sims. --- .../finn_xsi/include/IsolatedSimulation.hpp | 16 +- .../transformation/fpgadataflow/simulation.py | 965 +-------------- .../fpgadataflow/simulation_connected.py | 1064 +++++++++++++++++ .../fpgadataflow/simulation_controller.py | 441 +------ .../fpgadataflow/simulation_isolated.py | 385 ++++++ 5 files changed, 1463 insertions(+), 1408 deletions(-) create mode 100644 src/finn/transformation/fpgadataflow/simulation_connected.py create mode 100644 src/finn/transformation/fpgadataflow/simulation_isolated.py diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index 88e46b61ff..a6aebdd2d0 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -84,11 +84,9 @@ class IsolatedSimulation : public Simulation j["totalCycles"] = simState.totalCycles; j["inputCyclesDone"] = simState.inputCyclesDone; j["inputCyclesTarget"] = simState.inputCyclesTarget; - j["ready"] = json::object(); - std::transform(this->istreams.begin(), this->istreams.end(), j["ready"].begin(), [](S_AXIS_Control& s) { - return s.getInputReady(); - }); - readyJson.push_back(j); + for (S_AXIS_Control& s : this->istreams) { + j[s.name] = s.getInputReady(); + } } void logValid() { @@ -96,11 +94,9 @@ class IsolatedSimulation : public Simulation j["totalCycles"] = simState.totalCycles; j["outputCyclesDone"] = simState.outputCyclesDone; j["outputCyclesTarget"] = simState.outputCyclesTarget; - j["valid"] = json::object(); - std::transform(this->ostreams.begin(), this->ostreams.end(), j["valid"].begin(), [](M_AXIS_Control& s) { - return s.getOutputValid(); - }); - validJson.push_back(j); + for (M_AXIS_Control& s : this->ostreams) { + j[s.name] = s.getOutputValid(); + } } SimState simState; diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 16d2ff6fad..759e77d260 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,8 +1,6 @@ -"""Manage FINN simulation variants.""" +"""Manages the Simulation superclass as well as general simulation related transforms.""" import json -import math -import time from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp @@ -11,26 +9,20 @@ from typing import TYPE_CHECKING, Any, TypeAlias, cast from finn.builder.build_dataflow_config import DataflowBuildConfig -from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType -from finn.transformation.fpgadataflow.simulation_controller import ( - NodeConnectedSimulationController, - NodeIsolatedSimulationController, -) from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -from finn.util.basic import make_build_dir from finn.util.exception import FINNInternalError, FINNUserError -from finn.util.logging import DisabledLoggingConsole, log +from finn.util.logging import log if TYPE_CHECKING: from onnx.onnx_ml_pb2 import NodeProto + from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp + FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] -IsoSimLogData = NodeIsolatedSimulationController.IsolatedSimLogData -IsoSimLogDataByLayer = dict[str, IsoSimLogData] # Indexed by layer name class Simulation: @@ -78,953 +70,8 @@ def __init__( ) self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} - def simulate(self, *args: Any, **kwargs: Any) -> Any: - """Run the built simulation and return its results. This function can always be called - and will lookup the correct function to use, but consequently - cannot provide typing information.""" - match self.simulation_type: - case SimulationType.NODE_BASED_CONNECTED: - print("Connected simulation") - return self.simulate_node_connected(*args, **kwargs) - case SimulationType.NODE_BASED_ISOLATED: - return self.simulate_node_isolated(*args, **kwargs) - case _: - raise FINNUserError(f"Unsupported simulation type {self.simulation_type}") - - def simulate_node_connected( - self, depth: int | list[list[int]] | None = None, max_cycles: int | None = None - ) -> tuple[dict[int, dict[str, list[int]]], bool]: - """Simulate the given number of samples for every layer. Layers are completely isolated - and simulated in parallel. Simulation data is returned as a dict (by node name as index). - """ - if self.simulation_type != SimulationType.NODE_BASED_CONNECTED: - raise FINNInternalError( - f"Called simulation function 'simulate_node_connected' " - f"does not match provided simulation type " - f"{self.simulation_type}" - ) - names = [node.name for node in self.model.graph.node] - initial_depth: Any = [[depth]] * len(self.binaries) if isinstance(depth, int) else depth - - # Run simulation - start = time.time() - output_json = Path(make_build_dir("simulation_results_")) / "simulation_data.json" - with DisabledLoggingConsole() as console: - controller = NodeConnectedSimulationController( - len(self.binaries), names, list(self.binaries.values()), console, 0.1, False - ) - controller.run(initial_depth, output_json, max_cycles) - end = time.time() - log.info(f"Simulation took {end - start} seconds!") - - # Load the merged data from JSON - merged_data = json.loads(output_json.read_text()) - - # Return the collected data indexed by node index - data = {} - for i, sim_entry in enumerate(merged_data["simulations"]): - data[i] = { - "name": sim_entry["name"], - "fifo_utilization": sim_entry["fifo_utilization"], - "fifo_depth": sim_entry["fifo_depth"], - "cycles": sim_entry["cycles"], - "samples": sim_entry["samples"], - "intervals": sim_entry["intervals"], - } - json.dump(data, output_json.open("w"), indent=4) - return data, merged_data.get("timeout_occurred", False) - - def simulate_node_isolated(self) -> IsoSimLogDataByLayer: - """Simulate isolated nodes.""" - if self.simulation_type != SimulationType.NODE_BASED_ISOLATED: - raise FINNInternalError( - f"Called simulation function 'simulate_node_isolated' " - f"does not match provided simulation type " - f"{self.simulation_type}" - ) - names = [node.name for node in self.model.graph.node] - with DisabledLoggingConsole() as console: - controller = NodeIsolatedSimulationController( - len(self.binaries), names, list(self.binaries.values()), console, 0.1, False - ) - return controller.run() - - -class RunLayerParallelSimulation(Transformation): # noqa - def __init__( - self, - fpgapart: str, - clk_ns: float, - cfg: DataflowBuildConfig, - max_qsrl_depth: int = 256, - vivado_ram_style: str = "auto", - quality_of_results: str = "default", - ) -> None: - """Run layer parallel simulations.""" - super().__init__() - self.fpgapart = fpgapart - self.clk_ns = clk_ns - self.cfg = cfg - self.max_qsrl_depth = max_qsrl_depth - self.vivado_ram_style = vivado_ram_style - self.quality_of_results = quality_of_results - - def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: - """Run layer parallel simulations.""" - sim = Simulation( - model, - SimulationType.NODE_BASED_CONNECTED, - self.fpgapart, - self.clk_ns, - self.cfg.functional_simulation, - ) - model = sim.model # TODO:clean up - - initial_fifo_depths, _ = sim.simulate() - - fifo_depths = [] # Each entry is a list of fifo sizes for that node - for val in initial_fifo_depths.values(): - fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) - - # Max cycles for any simulation - sim_cycles = max([val["cycles"] for val in initial_fifo_depths.values()]) - - bit_widths = [] - for i in range(len(fifo_depths)): - bit_widths.append([]) - hw_node = getCustomOp(model.graph.node[i]) - if isinstance(hw_node, HWCustomOp): - for j in range(len(fifo_depths[i])): - bit_widths[i].append(hw_node.get_outstream_width(j)) - else: - raise FINNInternalError("Non-HW node found in dataflow graph during simulation") - - needs_minimization = [] - for i in range(len(fifo_depths)): - needs_minimization.append([True] * len(fifo_depths[i])) - for i in range(len(fifo_depths)): - for j in range(len(fifo_depths[i])): - # Check if we can reduce the fifo size - - used_size = fifo_depths[i][j] - bw = bit_widths[i][j] - - needs_minimization[i][j] = self._needs_minimization(used_size, bw) - - # Preserve original baseline depths for testing (deep copy) - original_fifo_depths = [row[:] for row in fifo_depths] - - # Minimize FIFO depths using binary search over BRAM block counts - for i in range(len(fifo_depths)): - for j in range(len(fifo_depths[i])): - if not needs_minimization[i][j]: - continue - - minimized_depth = self._minimize_fifo_depth( - i, - j, - fifo_depths, - original_fifo_depths, - bit_widths, - initial_fifo_depths, - sim, - sim_cycles, - ) - fifo_depths[i][j] = minimized_depth - - print("Final FIFO depths:") - for i in range(len(fifo_depths)): - print(f"{i}: {fifo_depths[i]}") - log.info(f"{i}: {fifo_depths[i]}") - - # Write back results. By default write to output_dir / "fifo_config.json" - assert len(fifo_depths) == len(model.graph.node) - json_results = {} - for i in range(len(fifo_depths)): - json_results[i] = {"node": model.graph.node[i].name, "depths": fifo_depths[i]} - with (Path(self.cfg.output_dir) / "fifo_config.json").open("w") as f: - json.dump(json_results, f) - - return model, False - - def _check_performance(self, new_data: dict, initial_fifo_depths: dict) -> bool: - """Check if performance has degraded compared to baseline. - - Args: - new_data: Simulation results to check - initial_fifo_depths: Baseline performance data - - Returns: - True if performance degraded, False otherwise - """ - for k, v in new_data.items(): - for idx in range(len(v["intervals"])): - if v["intervals"][idx] > initial_fifo_depths[k]["intervals"][idx]: - return True - return False - - def _test_depth( - self, - test_depth: int, - node_idx: int, - fifo_idx: int, - baseline_depths: list, - initial_fifo_depths: dict, - sim: Simulation, - sim_cycles: float, - ) -> tuple[bool, bool]: - """Test a specific FIFO depth. - - Args: - test_depth: Depth to test - node_idx: Node index - fifo_idx: FIFO index within node - baseline_depths: Original baseline FIFO depths (unchanged during minimization) - initial_fifo_depths: Baseline performance data - sim: Simulation controller - sim_cycles: Maximum simulation cycles - - Returns: - Tuple of (success, timeout) where success means depth works without degradation - """ - test_depths = [row[:] for row in baseline_depths] # Deep copy from baseline - test_depths[node_idx][fifo_idx] = test_depth - - new_data, timeout = sim.simulate_node_connected( - test_depths, max_cycles=math.ceil(sim_cycles * 1.1) - ) - - if timeout: - return False, True - - performance_degraded = self._check_performance(new_data, initial_fifo_depths) - return not performance_degraded, False - - def _get_valid_block_counts(self, min_blocks: int, max_blocks: int, bitwidth: int) -> list[int]: - """Get all valid BRAM block counts in the specified range. - - Some block counts are invalid for certain bitwidths due to quantization. - This method returns only the valid configurations. - - Args: - min_blocks: Minimum block count (inclusive) - max_blocks: Maximum block count (inclusive) - bitwidth: Data bitwidth - - Returns: - Sorted list of valid block counts - """ - valid_blocks = [] - for blocks in range(min_blocks, max_blocks + 1): - _, max_d = calculate_bram_depth_range(blocks, bitwidth) - if max_d > 0: # Valid configuration - valid_blocks.append(blocks) - return valid_blocks - - def _minimize_fifo_depth( - self, - node_idx: int, - fifo_idx: int, - current_depths: list, - baseline_depths: list, - bit_widths: list, - initial_fifo_depths: dict, - sim: Simulation, - sim_cycles: int, - ) -> int: - """Minimize a single FIFO depth using binary search. - - Args: - node_idx: Node index - fifo_idx: FIFO index within node - current_depths: Current working FIFO depth configuration - (may have already-minimized values) - baseline_depths: Original baseline FIFO depths (unchanged during minimization) - bit_widths: Bitwidths for all FIFOs - initial_fifo_depths: Baseline performance data - sim: Simulation controller - sim_cycles: Maximum simulation cycles - - Returns: - Minimized FIFO depth - """ - original_size = baseline_depths[node_idx][fifo_idx] - bw = bit_widths[node_idx][fifo_idx] - - print(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") - - # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs - success, timeout = self._test_depth( - 32, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles - ) - if success: - return 32 - - if original_size <= self.max_qsrl_depth: - upper_luts = calculate_srl16e_luts(original_size, bw) - # LUTRAM based FIFOs have block sizes of 32, so smallest after 32 is 64 - lower_luts = calculate_srl16e_luts(64, bw) - - # Binary search if there's room to search - if upper_luts > lower_luts: - best_working_depth = self._binary_search_srl_depth( - node_idx, - fifo_idx, - baseline_depths, - bw, - initial_fifo_depths, - sim, - sim_cycles, - lower_luts=lower_luts, - upper_luts=upper_luts, - ) - return best_working_depth - return original_size - - # Try FIFO depth of 256 next (fits into LUTRAM) - success, timeout = self._test_depth( - self.max_qsrl_depth, - node_idx, - fifo_idx, - baseline_depths, - initial_fifo_depths, - sim, - sim_cycles, - ) - if success: - upper_luts = calculate_srl16e_luts(original_size, bw) - # LUTRAM based FIFOs have block sizes of 32, so smallest after 32 is 64 - lower_luts = calculate_srl16e_luts(64, bw) - - # Binary search if there's room to search - if upper_luts > lower_luts: - best_working_depth = self._binary_search_srl_depth( - node_idx, - fifo_idx, - baseline_depths, - bw, - initial_fifo_depths, - sim, - sim_cycles, - lower_luts=lower_luts, - upper_luts=upper_luts, - ) - return best_working_depth - return self.max_qsrl_depth - - # We know 256 doesn't work, so we have to use BRAMs - # Try one BRAM block less than current - upper_blocks = calculate_bram_blocks(original_size, bw) - # Get all valid block counts in the range - valid_blocks = self._get_valid_block_counts(1, upper_blocks - 1, bw) - if not valid_blocks: - # No valid configurations exist - return original_size - # Test the maximum valid block count first (smallest depth) - max_valid_blocks = valid_blocks[-1] - _, max_d = calculate_bram_depth_range(max_valid_blocks, bw) - - success, timeout = self._test_depth( - max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles - ) - - if timeout or not success: - return original_size - - best_working_depth = max_d - - # Binary search if there's room to search and multiple valid configs - if len(valid_blocks) > 1: - best_working_depth = self._exponential_binary_search_depth( - node_idx, - fifo_idx, - baseline_depths, - bw, - initial_fifo_depths, - sim, - sim_cycles, - valid_blocks=valid_blocks, - ) - - return best_working_depth - - def _exponential_binary_search_depth( - self, - node_idx: int, - fifo_idx: int, - baseline_depths: list, - bitwidth: int, - initial_fifo_depths: dict, - sim: Simulation, - sim_cycles: float, - valid_blocks: list[int], - ) -> int: - """Perform exponential + binary search over valid block configurations. - - Uses exponential search to quickly find the range, then binary search within it. - This is more efficient when smaller block counts are more likely. - Only searches over pre-validated block counts. - - Args: - node_idx: Node index - fifo_idx: FIFO index within node - baseline_depths: Original baseline FIFO depths (unchanged during minimization) - bitwidth: Data bitwidth - initial_fifo_depths: Baseline performance data - sim: Simulation controller - sim_cycles: Maximum simulation cycles - valid_blocks: Sorted list of valid block counts to search over - - Returns: - Best working depth found - """ - if not valid_blocks: - raise FINNInternalError("valid_blocks list cannot be empty") - - # Start with the largest valid block count (known to work from caller) - _, max_d = calculate_bram_depth_range(valid_blocks[-1], bitwidth) - best_working_depth = max_d - - # Exponential search phase: find range where solution exists - # Check positions: 0, 1, 2, 4, 8, ... indices in valid_blocks list - lower_idx = 0 - upper_idx = len(valid_blocks) - 1 - exp_idx = 0 - last_failed_idx = -1 - - while exp_idx < upper_idx: - blocks = valid_blocks[exp_idx] - _, max_d = calculate_bram_depth_range(blocks, bitwidth) - - success, _ = self._test_depth( - max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles - ) - - if success: - # Found a working depth, now binary search in [last_failed_idx+1, exp_idx] - best_working_depth = max_d - lower_idx = last_failed_idx + 1 - upper_idx = exp_idx - break - # This doesn't work, try exponentially larger index - last_failed_idx = exp_idx - exp_idx = min(exp_idx * 2 if exp_idx > 0 else 1, upper_idx) - - # Binary search phase: refine the range - while lower_idx < upper_idx: - mid_idx = (lower_idx + upper_idx) // 2 - blocks = valid_blocks[mid_idx] - _, max_d = calculate_bram_depth_range(blocks, bitwidth) - - success, _ = self._test_depth( - max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles - ) - - if success: - # This depth works, try smaller (lower indices) - best_working_depth = max_d - upper_idx = mid_idx - else: - # This depth doesn't work, need larger (higher indices) - lower_idx = mid_idx + 1 - - return best_working_depth - - def _binary_search_srl_depth( - self, - node_idx: int, - fifo_idx: int, - baseline_depths: list, - bitwidth: int, - initial_fifo_depths: dict, - sim: Simulation, - sim_cycles: float, - lower_luts: int, - upper_luts: int, - ) -> int: - """Perform binary search to find minimal working FIFO depth in LUTRAM range. - - Args: - node_idx: Node index - fifo_idx: FIFO index within node - baseline_depths: Original baseline FIFO depths (unchanged during minimization) - bitwidth: Data bitwidth - initial_fifo_depths: Baseline performance data - sim: Simulation controller - sim_cycles: Maximum simulation cycles - lower_luts: Lower bound for LUT count - upper_luts: Upper bound for LUT count (known to work) - - Returns: - Best working depth found - """ - _, max_d = calculate_srl16e_depth_range(upper_luts, bitwidth) - best_working_depth = max_d - - while lower_luts < upper_luts: - mid_luts = (lower_luts + upper_luts) // 2 - - # Prevent infinite loop - if mid_luts == upper_luts: - mid_luts = upper_luts - 1 - if mid_luts < lower_luts: - break - - # Find valid depth for this LUT count - _, max_d = calculate_srl16e_depth_range(mid_luts, bitwidth) - - if max_d == 0: - # No valid configuration, try more LUTs - lower_luts = mid_luts + 1 - continue - - success, _ = self._test_depth( - max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles - ) - - if success: - # This depth works, try smaller - best_working_depth = max_d - upper_luts = mid_luts - else: - # This depth doesn't work, need larger - lower_luts = mid_luts + 1 - - return best_working_depth - - def _needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: - """Determine whether a FIFO can be minimized further. - - Args: - fifo_depth: Current FIFO depth - bitwidth: Data bitwidth - - Returns: - True if the FIFO can be minimized further, False otherwise. - """ - # Qsrl FIFO Formula: LUTs = ⌈depth/32⌉ x ⌈bitwidth/2⌉ - if fifo_depth <= 32: # FIFOs of depth <=32 fit into bitwidth/2 LUTs - return False - # Return False if exactly the minimum number of possible BRAM blocks is used for this - # bitwidth and depth is sufficiently large that further optimization is unlikely to succeed - return not ( - calculate_bram_blocks(fifo_depth, bitwidth) - <= self._get_valid_block_counts(1, bitwidth, bitwidth)[0] - and fifo_depth > math.floor(self.max_qsrl_depth * 1.1) - ) - - -def calculate_bram_blocks(depth: int, bitwidth: int) -> int: - """Calculate the number of BRAM blocks required for a BRAM FIFO. - - Args: - depth: FIFO depth - bitwidth: Data bitwidth - """ - if bitwidth == 1: - return math.ceil(depth / 16384) - if bitwidth == 2: - return math.ceil(depth / 8192) - if bitwidth <= 4: - return (math.ceil(depth / 4096)) * (math.ceil(bitwidth / 4)) - if bitwidth <= 9: - return (math.ceil(depth / 2048)) * (math.ceil(bitwidth / 9)) - if bitwidth <= 18 or depth > 512: - return (math.ceil(depth / 1024)) * (math.ceil(bitwidth / 18)) - return (math.ceil(depth / 512)) * (math.ceil(bitwidth / 36)) - - -def calculate_bram_depth_range(blocks: int, bitwidth: int) -> tuple[int, int]: - """Calculate the range of FIFO depths that use exactly the given number of BRAM blocks. - - Args: - blocks: Number of BRAM blocks - bitwidth: Data bitwidth - - Returns: - Tuple of (min_depth, max_depth) that uses exactly 'blocks' BRAM blocks. - """ - if blocks < 1: - raise FINNInternalError("Number of BRAM blocks must be at least 1") - - # Invert the formula from calculate_bram_blocks based on bitwidth - if bitwidth == 1: - # blocks = ⌈depth/16384⌉ - # Inversion: (blocks-1)*16384 < depth ≤ blocks*16384 - min_depth = (blocks - 1) * 16384 + 1 if blocks > 1 else 1 - max_depth = blocks * 16384 - elif bitwidth == 2: - # blocks = ⌈depth/8192⌉ - # Inversion: (blocks-1)*8192 < depth ≤ blocks*8192 - min_depth = (blocks - 1) * 8192 + 1 if blocks > 1 else 1 - max_depth = blocks * 8192 - elif bitwidth <= 4: - # blocks = ⌈depth/4096⌉ * ⌈bitwidth/4⌉ - bitwidth_factor = math.ceil(bitwidth / 4) - depth_blocks = math.ceil(blocks / bitwidth_factor) - min_depth = (depth_blocks - 1) * 4096 + 1 if depth_blocks > 1 else 1 - max_depth = depth_blocks * 4096 - elif bitwidth <= 9: - # blocks = ⌈depth/2048⌉ * ⌈bitwidth/9⌉ - bitwidth_factor = math.ceil(bitwidth / 9) - depth_blocks = math.ceil(blocks / bitwidth_factor) - min_depth = (depth_blocks - 1) * 2048 + 1 if depth_blocks > 1 else 1 - max_depth = depth_blocks * 2048 - elif bitwidth <= 18: - # blocks = ⌈depth/1024⌉ * ⌈bitwidth/18⌉ - bitwidth_factor = math.ceil(bitwidth / 18) - depth_blocks = math.ceil(blocks / bitwidth_factor) - min_depth = (depth_blocks - 1) * 1024 + 1 - max_depth = depth_blocks * 1024 - else: - # bitwidth > 18, split into two cases from original function - # Case 1: depth > 512 uses ⌈depth/1024⌉ * ⌈bitwidth/18⌉ - # Case 2: depth ≤ 512 uses ⌈depth/512⌉ * ⌈bitwidth/36⌉ - - # Try the depth > 512 case first (⌈depth/1024⌉ * ⌈bitwidth/18⌉) - bitwidth_factor = math.ceil(bitwidth / 18) - depth_blocks = math.ceil(blocks / bitwidth_factor) - - # Check if blocks is achievable with this bitwidth factor - if blocks % bitwidth_factor != 0 or depth_blocks < 1: - # Try the depth ≤ 512 case instead - pass - else: - min_depth = max((depth_blocks - 1) * 1024 + 1, 513) # Must be > 512 - max_depth = depth_blocks * 1024 - # Check if this range is valid (entirely > 512) - if min_depth > 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: - return (min_depth, max_depth) - - # Try the depth ≤ 512 case (⌈depth/512⌉ * ⌈bitwidth/36⌉) - bitwidth_factor = math.ceil(bitwidth / 36) - depth_blocks = math.ceil(blocks / bitwidth_factor) - - # Check if blocks is achievable with this bitwidth factor - if blocks % bitwidth_factor != 0 or depth_blocks < 1: - return (0, 0) # Invalid block count for this bitwidth - - min_depth = (depth_blocks - 1) * 512 + 1 if depth_blocks > 1 else 1 - max_depth = min(depth_blocks * 512, 512) # Must be ≤ 512 - - # Verify the range is valid (entirely ≤ 512 and produces correct block count) - if max_depth <= 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: - return (min_depth, max_depth) - - return (0, 0) # No valid range found - - # Verify the range is valid - if calculate_bram_blocks(min_depth, bitwidth) != blocks: - raise FINNInternalError("Calculated BRAM depth range is invalid!") - return (min_depth, max_depth) - - -def calculate_uram_blocks(depth: int, bitwidth: int) -> int: - """Calculate the number of URAM blocks required for a URAM FIFO. - - Args: - depth: FIFO depth - bitwidth: Data bitwidth - """ - return (math.ceil(depth / 4096)) * (math.ceil(bitwidth / 72)) - - -def calculate_uram_depth_range(blocks: int, bitwidth: int) -> tuple[int, int]: - """Calculate the range of FIFO depths that use exactly the given number of URAM blocks. - - Args: - blocks: Number of URAM blocks - bitwidth: Data bitwidth - - Returns: - Tuple of (min_depth, max_depth) that uses exactly 'blocks' URAM blocks. - Returns (0, 0) if no valid range exists. - """ - if blocks < 1: - return (0, 0) - - # URAM formula: blocks = ⌈depth/4096⌉ * ⌈bitwidth/72⌉ - bitwidth_factor = math.ceil(bitwidth / 72) - - # Calculate depth range - # Minimum depth: (blocks / bitwidth_factor - 1) * 4096 + 1 - # Maximum depth: (blocks / bitwidth_factor) * 4096 - - if blocks % bitwidth_factor != 0: - return (0, 0) # Invalid block count for this bitwidth - - depth_blocks = blocks // bitwidth_factor - min_depth = (depth_blocks - 1) * 4096 + 1 if depth_blocks > 1 else 1 - max_depth = depth_blocks * 4096 - - # Verify - if calculate_uram_blocks(min_depth, bitwidth) != blocks: - return (0, 0) - - return (min_depth, max_depth) - - -def calculate_srl16e_luts(depth: int, bitwidth: int) -> int: - """Calculate the number of SRL16E LUTs required for a FIFO. - - Args: - depth: FIFO depth (must be >= 2) - bitwidth: Data bitwidth - - Returns: - Number of SRL16E LUTs required without adress LUTs. - - Formula: LUTs = ⌈depth/32⌉ x ⌈bitwidth/2⌉ - """ - ram_luts = (math.ceil(depth / 32)) * (math.ceil(bitwidth / 2)) - return ram_luts - - -def calculate_srl16e_depth_range(luts: int, bitwidth: int) -> tuple[int, int]: - """Calculate the range of FIFO depths that use exactly the given number of SRL16E LUTs. - - Args: - luts: Number of SRL16E LUTs - bitwidth: Data bitwidth - - Returns: - Tuple of (min_depth, max_depth) that uses exactly 'luts' LUTs. - Returns (0, 0) if no valid range exists. - """ - if luts < 1: - return (0, 0) - - # SRL16E formula: luts = ⌈depth/32⌉ * ⌈bitwidth/2⌉ - bitwidth_factor = math.ceil(bitwidth / 2) - - # Calculate depth range - if luts % bitwidth_factor != 0: - return (0, 0) # Invalid LUT count for this bitwidth - - depth_blocks = luts // bitwidth_factor - min_depth = (depth_blocks - 1) * 32 + 1 if depth_blocks > 1 else 2 - max_depth = depth_blocks * 32 - - # Verify - if calculate_srl16e_luts(min_depth, bitwidth) != luts: - return (0, 0) - - return (min_depth, max_depth) - - -class RunLayerIsolatedSimulation(Transformation): - """Run a layer isolated simulation and calculate some information for a - later layer parallel simulation.""" - - def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: - """Run isolated layer simulations.""" - super().__init__() - self.fpgapart = fpgapart - self.clk_ns = clk_ns - self.functional_sim = functional_sim - - def calculate_upper_bounds(self, data: IsoSimLogDataByLayer) -> dict[str, dict[str, int]]: - """Try to calculate an upper bound for the incoming FIFO size of the layers. - Return size indexed by layer name and stream name. - - >>> step = RunLayerIsolatedSimulation("", 0.0, False) - >>> bounds = step.calculate_upper_bounds({ - ... "A": { - ... "ready": [ - ... {"totalCycles": 43, "inputCyclesDone": 12, - ... "inputCyclesTarget": 24, "s_axi_0": 1, "s_axi_1": 0}, - ... {"totalCycles": 44, "inputCyclesDone": 13, - ... "inputCyclesTarget": 24, "s_axi_0": 0, "s_axi_1": 0}, - ... ], "valid": [] - ... }, - ... "B": { - ... "ready": [ - ... {"totalCycles": 100, "inputCyclesDone": 3, - ... "inputCyclesTarget": 10, "s_axi_0": 1, "s_axi_1": 1, - ... "s_axi_2": 0}, - ... ], "valid": [] - ... }, - ... "C": { - ... "ready": [ - ... {"totalCycles": 43, "inputCyclesDone": 14, - ... "inputCyclesTarget": 24, "s_axi_0": 1, "s_axi_1": 0}, - ... {"totalCycles": 44, "inputCyclesDone": 15, - ... "inputCyclesTarget": 24, "s_axi_0": 0, "s_axi_1": 0}, - ... ], "valid": [] - ... } - ... }) - >>> bounds["A"] - {'s_axi_0': 1, 's_axi_1': 2} - >>> bounds["B"] - {'s_axi_0': 0, 's_axi_1': 0, 's_axi_2': 1} - >>> bounds["C"] - {'s_axi_0': 0, 's_axi_1': 0} - """ - - # TODO: Proper pytest tests - def _any_ready(cycle_data: dict[str, int]) -> bool: - for key in cycle_data.keys(): - if ( - key not in ["totalCycles", "inputCyclesDone", "inputCyclesTarget"] - and cycle_data[key] == 1 - ): - return True - return False - - results: dict[str, dict[str, int]] = {} - for layer in data.keys(): - # Save all keys that are not - results[layer] = { - stream_name: 0 - for stream_name in data[layer]["ready"][0].keys() - if stream_name not in ["inputCyclesDone", "inputCyclesTarget", "totalCycles"] - } - for cycle_data in data[layer]["ready"]: - if cycle_data["inputCyclesDone"] > int( - cycle_data["inputCyclesTarget"] / 2 - ) and _any_ready(cycle_data): - break - for stream_name in results[layer].keys(): - # TODO: Currently on the C++ side we multiply the - # TODO: target cycles by 2, to get two samples - # TODO: We keep track of ready signals until we see - # TODO: the first ready after half of all cycles were seen. - # TODO: This might change in the future - if cycle_data["inputCyclesTarget"] % 2 != 0: - raise FINNInternalError( - f"An 'inputCyclesTarget' of layer {layer} seems " - f"to not be an even number. Currently, we double " - f"the target simulation cycles for every layer " - f"on the C++ side. This error may point towards " - f"a change on the C++ side, which may cause the " - f"need to update this function accordingly!" - ) - results[layer][stream_name] += int(cycle_data[stream_name] == 0) - - # TODO: This calculation assumes, that if the producer does NOT fire the entire time, - # TODO: the consumer can read at least at the same speed as - # if the producer did, and not slower. - # TODO: (Since this would mean that less data pressure from - # the producer makes the consumer _slower_.) - # TODO: This should usually be the case, but is important to keep in mind. - return results - - def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: - """Do checks on the returned data to make sure it is in spec. - - A correctly formatted example would be: - >>> data = { - ... "layer1": { - ... "ready": [{"totalCycles": 10, "inputCyclesDone": 5, - ... "inputCyclesTarget": 10, "s_axi0_ready": 1}], - ... "valid": [{"totalCycles": 10, "outputCyclesDone": 5, - ... "outputCyclesTarget": 10, "m_axi0_valid": 1}] - ... } - ... } - >>> sim = RunLayerIsolatedSimulation("", 0.0, False) - >>> sim.sanity_check_logged_data(data) - >>> - """ - # 0. Valid and ready are present - for layer, ldata in data.items(): - if "valid" not in ldata.keys(): - raise FINNInternalError( - f"Simulation log data of layer " f"{layer} is missing the VALID log." - ) - if "ready" not in ldata.keys(): - raise FINNInternalError( - f"Simulation log data of layer " f"{layer} is missing the READY log." - ) - # 1. All cycle datas are uniform and have at least one stream signal - for layer, ldata in data.items(): - cycle_data = ldata["ready"] + ldata["valid"] - lengths: set[int] = {len(cycle.keys()) for cycle in cycle_data} - if len(lengths) != 1: - raise FINNInternalError( - f"Simulation log data inconsistent for layer " - f"{layer}. Differing number of fields per cycle." - ) - if next(iter(lengths)) < 4: - raise FINNInternalError( - f"Simulation for layer {layer} must contain " - f"atleast 4 fields (total cycles, AXI cycles " - f"done, AXI cycles target and at least one AXI " - f"ready/valid signal)!" - ) - # 2. All ready logs contain the required keywords - readykeys = ["inputCyclesDone", "inputCyclesTarget", "totalCycles"] - for rlayer, rdata in data.items(): - for cycle in rdata["ready"]: - if any(keyword not in cycle.keys() for keyword in readykeys): - raise FINNInternalError( - f"Simulation READY log of layer {rlayer} " - f"contains cycles that are missing a required key." - ) - if any(key not in readykeys and "axi" not in key for key in cycle.keys()): - raise FINNInternalError( - f"In the READY simulation log of layer " - f"{rlayer} there seem to be fields that " - f"are not expected keywords or AXI streams!" - ) - # 3. All valid logs contain the required keywords - validkeys = ["outputCyclesDone", "outputCyclesTarget", "totalCycles"] - for vlayer, vdata in data.items(): - for cycle in vdata["valid"]: - if any(keyword not in cycle.keys() for keyword in validkeys): - raise FINNInternalError( - f"Simulation VALID log of layer {vlayer} " - f"contains cycles that are missing a required key." - ) - if any(key not in validkeys and "axi" not in key for key in cycle.keys()): - raise FINNInternalError( - f"In the VALID simulation log of layer " - f"{vlayer} there seem to be fields that " - f"are not expected keywords or AXI streams!" - ) - # 4. Cycles done can never be larger then the number of total cycles passed in the sim - for layer, cdata in data.items(): - for line in cdata["ready"] + cdata["valid"]: - if ( - "inputCyclesDone" in line.keys() - and line["inputCyclesDone"] > line["totalCycles"] - ): - raise FINNInternalError( - f"Simulation log of layer {layer} looks incorrect: " - f"Number of active receiving cycles " - f"({line['inputCyclesDone']}) larger than number of " - f"total cycles passed ({line['totalCycles']})." - ) - if ( - "outputCyclesDone" in line.keys() - and line["outputCyclesDone"] > line["totalCycles"] - ): - raise FINNInternalError( - f"Simulation log of layer {layer} looks incorrect: " - f"Number of active producing cycles " - f"({line['outputCyclesDone']}) larger than number of " - f"total cycles passed ({line['totalCycles']})." - ) - - def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: - """Run isolated layer simulations.""" - sim = Simulation( - model, - SimulationType.NODE_BASED_ISOLATED, - self.fpgapart, - self.clk_ns, - self.functional_sim, - ) - data: IsoSimLogDataByLayer = sim.simulate_node_isolated() - self.sanity_check_logged_data(data) - in_fifo_upper_bound = self.calculate_upper_bounds(data) - formatted_upper_bounds = "\n\t".join( - [f"{name}: {in_fifo_upper_bound[name]}" for name in in_fifo_upper_bound.keys()] - ) - log.info("Upper bounds: \n" + formatted_upper_bounds) - - raise NotImplementedError() - # TODO: Integrate data into the layer parallel simulation - return model, False + def simulate(self) -> Any: + raise NotImplementedError("Call simulate() on subclasses.") class ApplyFIFOSizes(Transformation): diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py new file mode 100644 index 0000000000..ae5d9d29dc --- /dev/null +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -0,0 +1,1064 @@ +"""Node connected parallel simulations.""" + +import json +import math +import multiprocessing +import time +import traceback +from concurrent.futures import Future, ThreadPoolExecutor +from pathlib import Path +from qonnx.core.modelwrapper import ModelWrapper +from qonnx.custom_op.registry import getCustomOp +from qonnx.transformation.base import Transformation +from rich.console import Console +from typing import Any + +from finn.builder.build_dataflow_config import DataflowBuildConfig +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.transformation.fpgadataflow.simulation import Simulation, SimulationType +from finn.transformation.fpgadataflow.simulation_controller import SimulationController +from finn.util.basic import make_build_dir +from finn.util.exception import FINNInternalError, FINNUserError +from finn.util.logging import DisabledLoggingConsole, log + + +class NodeConnectedSimulationController(SimulationController): + """Run simulations for node connected cases.""" + + def __init__( + self, + parallel_simulations: int, + names: list[str], + binaries: list[Path], + console: Console, + poll_interval: float = 1.0, + with_progressbar: bool = True, + ) -> None: + """Set up node connected simulation.""" + super().__init__( + parallel_simulations, names, binaries, console, poll_interval, with_progressbar + ) + for binary in binaries: + if not binary.exists(): + console.log(f"Binary {binary} does not exist!") + raise FINNUserError(f"Binary {binary} does not exist!") + + def run( + self, + depth: list[list[int]] | None = None, + output_json: Path | None = None, + max_cycles: int | None = None, + ) -> dict[str, list[int]]: + """Run the simulation entirely with the given depth and sample count. + + Args: + depth: FIFO depth to configure for simulations. + samples: Number of samples to simulate. + output_json: Optional path to write merged simulation data as JSON. + max_cycles: Max cycles + + Returns: + Dictionary mapping simulation names to their FIFO utilization arrays. + """ + futures: list[Future] = [] + fifo_results: dict[str, list[int]] = {} + cycles_results: dict[str, int] = {} + samples_results: dict[str, int] = {} + intervals_results: dict[str, list[int]] = {} + timeout_result = False + fifo_depths: dict[str, list[int]] = {} + + if self.progress is not None: + self.progress.start() + try: + with ThreadPoolExecutor(self.workers) as pool: + for i, (name, binary) in enumerate(zip(self.names, self.binaries, strict=True)): + is_last_node = i == len(self.names) - 1 + is_special_for_display = i == 0 or is_last_node + futures.append( + pool.submit( + self._run_binary, + binary, + name, + i % multiprocessing.cpu_count(), + depth[i] if depth is not None else None, + is_last_node, # Only last node has no output FIFOs + is_special_for_display, # First and last get special coloring + max_cycles, + ) + ) + + # Wait for first completion or error + from concurrent.futures import FIRST_COMPLETED, wait + + all_futures = list(futures) # Keep track of all futures + while futures: + done, futures = wait(futures, return_when=FIRST_COMPLETED) + + # Check if any completed task indicates we should stop + for future in done: + try: + result = future.result() # This will raise if there was an exception + if result is not None: + ( + sim_name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + ) = result + fifo_depths[sim_name] = fifo_depth + fifo_results[sim_name] = fifo_util + cycles_results[sim_name] = cycles + samples_results[sim_name] = samps + intervals_results[sim_name] = intervals + timeout_result = timeout_result or timeout + except Exception as e: # noqa + self.console.log(f"Simulation failed: {e}") + # Set stop flag and break + with self.stop_lock: + self.should_stop = True + break + + # If we should stop, signal all remaining simulations + with self.stop_lock: + if self.should_stop: + # Don't cancel - let them finish with early stop + break + + # Wait for all futures to complete and collect their results + pool.shutdown(wait=True) + for future in all_futures: + if not future.done(): + continue + try: + result = future.result() + if result is not None: + ( + sim_name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + ) = result + # Only update if not already collected + if sim_name not in fifo_results: + fifo_depths[sim_name] = fifo_depth + fifo_results[sim_name] = fifo_util + cycles_results[sim_name] = cycles + samples_results[sim_name] = samps + intervals_results[sim_name] = intervals + timeout_result = timeout_result or timeout + except Exception as e: + self.console.log(f"Error collecting result: {e}") + finally: + if self.progress is not None: + self.progress.stop() + self._cleanup_sockets() + + # Merge all simulation data + if output_json is not None: + merged_data = { + "simulations": [ + { + "name": name, + "fifo_utilization": fifo_results.get(name, []), + "fifo_depth": fifo_depths.get(name, []), + "cycles": cycles_results.get(name, 0), + "samples": samples_results.get(name, 0), + "intervals": intervals_results.get(name, []), + } + for name in self.names + ], + "depth_configured": depth, + "timeout_occurred": timeout_result, + } + output_json.write_text(json.dumps(merged_data, indent=2)) + + return fifo_results + + def _run_binary( + self, + binary: Path, + name: str | None, + _cpu: int | None, + depth: list[int] | None = None, + is_last_node: bool = False, + is_special_for_display: bool = False, + max_cycles: int | None = None, + ) -> tuple[str, list[int], int, int, list[int], bool, list[int]] | None: + """Run the specified simulation binary in a new subprocess and communicate with it. + + Args: + binary: Path to simulation binary + name: Name of simulation node + _cpu: CPU affinity (unused) + depth: List of FIFO depths for this node's output FIFOs + is_last_node: True if this is the last node (no output FIFOs to configure) + is_special_for_display: True if this node should get special color in logs + max_cycles: Maximum cycles to simulate + + Returns: + Tuple of (simulation_name, fifo_utilization, cycles, samples, intervals, timeout, + fifo_depth) on success, + None on failure. + """ + cwd = binary.parent + if name is None: + name = cwd.name.replace("rtlsim_", "") + + process_index = self.names.index(name) + + with (self.logdir / f"{name}_{process_index}_of_{self.total}.txt").open("w+") as logfile: + + def _print(msg: str, color: str = "green") -> None: + if self.progress is None: + if is_special_for_display: + color = "orange3" + if "ERROR" in msg: + color = "red" + self.console.log( + f"[bold {color}]{name:<35}" + f"[/bold {color}][cornflower_blue]{process_index} " + f"/ {len(self.names) - 1}[/cornflower_blue] {msg:<35}" + ) + logfile.write(f"{msg}\n") + logfile.flush() + + try: + # Start the simulation process with socket communication + proc_idx = self._start_process(binary, process_index) + + # Send configuration commands + # Last node has no output FIFOs, so don't configure FIFO depths + config_payload: dict[str, list[int] | int] = {} + if not is_last_node and depth is not None: + config_payload["fifo_depth"] = depth + if max_cycles is not None: + config_payload["max_cycles"] = max_cycles + + response = self._send_and_receive(proc_idx, "configure", config_payload) + + if not response or response.get("status") != "success": + error_msg = ( + response.get("message", "Unknown error") if response else "No response" + ) + _print(f"Configuration failed: {error_msg}", "red") + return None + + # Start the simulation + response = self._send_and_receive(proc_idx, "start", {}) + + if not response or response.get("status") != "success": + error_msg = ( + response.get("message", "Unknown error") if response else "No response" + ) + _print(f"Failed to start simulation: {error_msg}", "red") + return None + + cycles = 0 + samps = 0 + intervals: list[int] = [] + timeout = False + fifo_util: list[int] = [] + fifo_depth: list[int] = [] + + # Poll for status updates + while True: + # Check if we should stop early + with self.stop_lock: + if self.should_stop: + try: + stop_response = self._send_and_receive(proc_idx, "stop", {}) + except (BrokenPipeError, ConnectionResetError, RuntimeError): + # Process may have already exited - that's ok during shutdown + stop_response = None + if stop_response: + cycles = stop_response.get("cycles", 0) + samps = stop_response.get("samples", 0) + fifo_util = stop_response.get("fifo_utilization", []) + intervals = stop_response.get("intervals", []) + fifo_depth = stop_response.get("fifo_depth", []) + timeout = stop_response.get("timeout", False) + if fifo_util: + logfile.write(f"Final FIFO utilization: {fifo_util}\n") + return (name, fifo_util, cycles, samps, intervals, timeout, fifo_depth) + time.sleep(self.poll_interval) + + response = self._send_and_receive(proc_idx, "status", {}) + + if not response: + _print("Lost connection to simulation", "red") + with self.stop_lock: + self.should_stop = True + raise RuntimeError("Lost connection to simulation") + + state = response.get("state", "unknown") + + if state == "finished" or state == "timeout": + cycles = response.get("cycles", 0) + samps = response.get("samples", 0) + fifo_util = response.get("fifo_utilization", []) + fifo_depth = response.get("fifo_depth", []) + intervals = response.get("intervals", []) + timeout = response.get("timeout", False) + with self.stop_lock: + self.should_stop = True + break + + if state == "running": + # Update progress if available + cycles = response.get("cycles", 0) + + if state == "error": + error_msg = response.get("message", "Unknown error") + _print(f"Simulation error: {error_msg}", "red") + # Signal other simulations to stop + with self.stop_lock: + self.should_stop = True + raise RuntimeError(f"Simulation error: {error_msg}") + + # Stop the simulation + stop_response = self._send_and_receive(proc_idx, "stop", {}) + fifo_util = [] + + if stop_response: + fifo_util = stop_response.get("fifo_utilization", []) + fifo_depth = stop_response.get("fifo_depth", []) + cycles = stop_response.get("cycles", 0) + samps = stop_response.get("samples", 0) + if fifo_util: + logfile.write(f"Final FIFO utilization: {fifo_util}\n") + + return (name, fifo_util, cycles, samps, intervals, timeout, fifo_depth) + + except Exception as e: + self.console.log(f"Exception caught during simulation execution ({name}): {e}") + self.console.log(traceback.format_exc()) + logfile.write(f"Exception: {e}\n") + logfile.write(traceback.format_exc()) + with self.stop_lock: + self.should_stop = True + return None + + +class IsolatedSimulation(Simulation): + def __init__( + self, + model: ModelWrapper, + simulation_type: SimulationType, + fpgapart: str, + clk_ns: float, + functional_sim: bool, + workers: int | None = None, + ) -> None: + super().__init__(model, simulation_type, fpgapart, clk_ns, functional_sim, workers) + + def simulate( + self, depth: int | list[list[int]] | None = None, max_cycles: int | None = None + ) -> tuple[dict[int, dict[str, list[int]]], bool]: + """Simulate the given number of samples for every layer. Layers are completely isolated + and simulated in parallel. Simulation data is returned as a dict (by node name as index). + """ + if self.simulation_type != SimulationType.NODE_BASED_CONNECTED: + raise FINNInternalError( + f"Called simulation function 'simulate_node_connected' " + f"does not match provided simulation type " + f"{self.simulation_type}" + ) + names = [node.name for node in self.model.graph.node] + initial_depth: Any = [[depth]] * len(self.binaries) if isinstance(depth, int) else depth + + # Run simulation + start = time.time() + output_json = Path(make_build_dir("simulation_results_")) / "simulation_data.json" + with DisabledLoggingConsole() as console: + controller = NodeConnectedSimulationController( + len(self.binaries), names, list(self.binaries.values()), console, 0.1, False + ) + controller.run(initial_depth, output_json, max_cycles) + end = time.time() + log.info(f"Simulation took {end - start} seconds!") + + # Load the merged data from JSON + merged_data = json.loads(output_json.read_text()) + + # Return the collected data indexed by node index + data = {} + for i, sim_entry in enumerate(merged_data["simulations"]): + data[i] = { + "name": sim_entry["name"], + "fifo_utilization": sim_entry["fifo_utilization"], + "fifo_depth": sim_entry["fifo_depth"], + "cycles": sim_entry["cycles"], + "samples": sim_entry["samples"], + "intervals": sim_entry["intervals"], + } + json.dump(data, output_json.open("w"), indent=4) + return data, merged_data.get("timeout_occurred", False) + + +class RunLayerParallelSimulation(Transformation): # noqa + def __init__( + self, + fpgapart: str, + clk_ns: float, + cfg: DataflowBuildConfig, + max_qsrl_depth: int = 256, + vivado_ram_style: str = "auto", + quality_of_results: str = "default", + ) -> None: + """Run layer parallel simulations.""" + super().__init__() + self.fpgapart = fpgapart + self.clk_ns = clk_ns + self.cfg = cfg + self.max_qsrl_depth = max_qsrl_depth + self.vivado_ram_style = vivado_ram_style + self.quality_of_results = quality_of_results + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Run layer parallel simulations.""" + sim = Simulation( + model, + SimulationType.NODE_BASED_CONNECTED, + self.fpgapart, + self.clk_ns, + self.cfg.functional_simulation, + ) + model = sim.model # TODO:clean up + + initial_fifo_depths, _ = sim.simulate() + + fifo_depths = [] # Each entry is a list of fifo sizes for that node + for val in initial_fifo_depths.values(): + fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) + + # Max cycles for any simulation + sim_cycles = max([val["cycles"] for val in initial_fifo_depths.values()]) + + bit_widths = [] + for i in range(len(fifo_depths)): + bit_widths.append([]) + hw_node = getCustomOp(model.graph.node[i]) + if isinstance(hw_node, HWCustomOp): + for j in range(len(fifo_depths[i])): + bit_widths[i].append(hw_node.get_outstream_width(j)) + else: + raise FINNInternalError("Non-HW node found in dataflow graph during simulation") + + needs_minimization = [] + for i in range(len(fifo_depths)): + needs_minimization.append([True] * len(fifo_depths[i])) + for i in range(len(fifo_depths)): + for j in range(len(fifo_depths[i])): + # Check if we can reduce the fifo size + + used_size = fifo_depths[i][j] + bw = bit_widths[i][j] + + needs_minimization[i][j] = self._needs_minimization(used_size, bw) + + # Preserve original baseline depths for testing (deep copy) + original_fifo_depths = [row[:] for row in fifo_depths] + + # Minimize FIFO depths using binary search over BRAM block counts + for i in range(len(fifo_depths)): + for j in range(len(fifo_depths[i])): + if not needs_minimization[i][j]: + continue + + minimized_depth = self._minimize_fifo_depth( + i, + j, + fifo_depths, + original_fifo_depths, + bit_widths, + initial_fifo_depths, + sim, + sim_cycles, + ) + fifo_depths[i][j] = minimized_depth + + print("Final FIFO depths:") + for i in range(len(fifo_depths)): + print(f"{i}: {fifo_depths[i]}") + log.info(f"{i}: {fifo_depths[i]}") + + # Write back results. By default write to output_dir / "fifo_config.json" + assert len(fifo_depths) == len(model.graph.node) + json_results = {} + for i in range(len(fifo_depths)): + json_results[i] = {"node": model.graph.node[i].name, "depths": fifo_depths[i]} + with (Path(self.cfg.output_dir) / "fifo_config.json").open("w") as f: + json.dump(json_results, f) + + return model, False + + def _check_performance(self, new_data: dict, initial_fifo_depths: dict) -> bool: + """Check if performance has degraded compared to baseline. + + Args: + new_data: Simulation results to check + initial_fifo_depths: Baseline performance data + + Returns: + True if performance degraded, False otherwise + """ + for k, v in new_data.items(): + for idx in range(len(v["intervals"])): + if v["intervals"][idx] > initial_fifo_depths[k]["intervals"][idx]: + return True + return False + + def _test_depth( + self, + test_depth: int, + node_idx: int, + fifo_idx: int, + baseline_depths: list, + initial_fifo_depths: dict, + sim: Simulation, + sim_cycles: float, + ) -> tuple[bool, bool]: + """Test a specific FIFO depth. + + Args: + test_depth: Depth to test + node_idx: Node index + fifo_idx: FIFO index within node + baseline_depths: Original baseline FIFO depths (unchanged during minimization) + initial_fifo_depths: Baseline performance data + sim: Simulation controller + sim_cycles: Maximum simulation cycles + + Returns: + Tuple of (success, timeout) where success means depth works without degradation + """ + test_depths = [row[:] for row in baseline_depths] # Deep copy from baseline + test_depths[node_idx][fifo_idx] = test_depth + + new_data, timeout = sim.simulate_node_connected( + test_depths, max_cycles=math.ceil(sim_cycles * 1.1) + ) + + if timeout: + return False, True + + performance_degraded = self._check_performance(new_data, initial_fifo_depths) + return not performance_degraded, False + + def _get_valid_block_counts(self, min_blocks: int, max_blocks: int, bitwidth: int) -> list[int]: + """Get all valid BRAM block counts in the specified range. + + Some block counts are invalid for certain bitwidths due to quantization. + This method returns only the valid configurations. + + Args: + min_blocks: Minimum block count (inclusive) + max_blocks: Maximum block count (inclusive) + bitwidth: Data bitwidth + + Returns: + Sorted list of valid block counts + """ + valid_blocks = [] + for blocks in range(min_blocks, max_blocks + 1): + _, max_d = calculate_bram_depth_range(blocks, bitwidth) + if max_d > 0: # Valid configuration + valid_blocks.append(blocks) + return valid_blocks + + def _minimize_fifo_depth( + self, + node_idx: int, + fifo_idx: int, + current_depths: list, + baseline_depths: list, + bit_widths: list, + initial_fifo_depths: dict, + sim: Simulation, + sim_cycles: int, + ) -> int: + """Minimize a single FIFO depth using binary search. + + Args: + node_idx: Node index + fifo_idx: FIFO index within node + current_depths: Current working FIFO depth configuration + (may have already-minimized values) + baseline_depths: Original baseline FIFO depths (unchanged during minimization) + bit_widths: Bitwidths for all FIFOs + initial_fifo_depths: Baseline performance data + sim: Simulation controller + sim_cycles: Maximum simulation cycles + + Returns: + Minimized FIFO depth + """ + original_size = baseline_depths[node_idx][fifo_idx] + bw = bit_widths[node_idx][fifo_idx] + + print(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") + + # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs + success, timeout = self._test_depth( + 32, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + if success: + return 32 + + if original_size <= self.max_qsrl_depth: + upper_luts = calculate_srl16e_luts(original_size, bw) + # LUTRAM based FIFOs have block sizes of 32, so smallest after 32 is 64 + lower_luts = calculate_srl16e_luts(64, bw) + + # Binary search if there's room to search + if upper_luts > lower_luts: + best_working_depth = self._binary_search_srl_depth( + node_idx, + fifo_idx, + baseline_depths, + bw, + initial_fifo_depths, + sim, + sim_cycles, + lower_luts=lower_luts, + upper_luts=upper_luts, + ) + return best_working_depth + return original_size + + # Try FIFO depth of 256 next (fits into LUTRAM) + success, timeout = self._test_depth( + self.max_qsrl_depth, + node_idx, + fifo_idx, + baseline_depths, + initial_fifo_depths, + sim, + sim_cycles, + ) + if success: + upper_luts = calculate_srl16e_luts(original_size, bw) + # LUTRAM based FIFOs have block sizes of 32, so smallest after 32 is 64 + lower_luts = calculate_srl16e_luts(64, bw) + + # Binary search if there's room to search + if upper_luts > lower_luts: + best_working_depth = self._binary_search_srl_depth( + node_idx, + fifo_idx, + baseline_depths, + bw, + initial_fifo_depths, + sim, + sim_cycles, + lower_luts=lower_luts, + upper_luts=upper_luts, + ) + return best_working_depth + return self.max_qsrl_depth + + # We know 256 doesn't work, so we have to use BRAMs + # Try one BRAM block less than current + upper_blocks = calculate_bram_blocks(original_size, bw) + # Get all valid block counts in the range + valid_blocks = self._get_valid_block_counts(1, upper_blocks - 1, bw) + if not valid_blocks: + # No valid configurations exist + return original_size + # Test the maximum valid block count first (smallest depth) + max_valid_blocks = valid_blocks[-1] + _, max_d = calculate_bram_depth_range(max_valid_blocks, bw) + + success, timeout = self._test_depth( + max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + + if timeout or not success: + return original_size + + best_working_depth = max_d + + # Binary search if there's room to search and multiple valid configs + if len(valid_blocks) > 1: + best_working_depth = self._exponential_binary_search_depth( + node_idx, + fifo_idx, + baseline_depths, + bw, + initial_fifo_depths, + sim, + sim_cycles, + valid_blocks=valid_blocks, + ) + + return best_working_depth + + def _exponential_binary_search_depth( + self, + node_idx: int, + fifo_idx: int, + baseline_depths: list, + bitwidth: int, + initial_fifo_depths: dict, + sim: Simulation, + sim_cycles: float, + valid_blocks: list[int], + ) -> int: + """Perform exponential + binary search over valid block configurations. + + Uses exponential search to quickly find the range, then binary search within it. + This is more efficient when smaller block counts are more likely. + Only searches over pre-validated block counts. + + Args: + node_idx: Node index + fifo_idx: FIFO index within node + baseline_depths: Original baseline FIFO depths (unchanged during minimization) + bitwidth: Data bitwidth + initial_fifo_depths: Baseline performance data + sim: Simulation controller + sim_cycles: Maximum simulation cycles + valid_blocks: Sorted list of valid block counts to search over + + Returns: + Best working depth found + """ + if not valid_blocks: + raise FINNInternalError("valid_blocks list cannot be empty") + + # Start with the largest valid block count (known to work from caller) + _, max_d = calculate_bram_depth_range(valid_blocks[-1], bitwidth) + best_working_depth = max_d + + # Exponential search phase: find range where solution exists + # Check positions: 0, 1, 2, 4, 8, ... indices in valid_blocks list + lower_idx = 0 + upper_idx = len(valid_blocks) - 1 + exp_idx = 0 + last_failed_idx = -1 + + while exp_idx < upper_idx: + blocks = valid_blocks[exp_idx] + _, max_d = calculate_bram_depth_range(blocks, bitwidth) + + success, _ = self._test_depth( + max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + + if success: + # Found a working depth, now binary search in [last_failed_idx+1, exp_idx] + best_working_depth = max_d + lower_idx = last_failed_idx + 1 + upper_idx = exp_idx + break + # This doesn't work, try exponentially larger index + last_failed_idx = exp_idx + exp_idx = min(exp_idx * 2 if exp_idx > 0 else 1, upper_idx) + + # Binary search phase: refine the range + while lower_idx < upper_idx: + mid_idx = (lower_idx + upper_idx) // 2 + blocks = valid_blocks[mid_idx] + _, max_d = calculate_bram_depth_range(blocks, bitwidth) + + success, _ = self._test_depth( + max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + + if success: + # This depth works, try smaller (lower indices) + best_working_depth = max_d + upper_idx = mid_idx + else: + # This depth doesn't work, need larger (higher indices) + lower_idx = mid_idx + 1 + + return best_working_depth + + def _binary_search_srl_depth( + self, + node_idx: int, + fifo_idx: int, + baseline_depths: list, + bitwidth: int, + initial_fifo_depths: dict, + sim: Simulation, + sim_cycles: float, + lower_luts: int, + upper_luts: int, + ) -> int: + """Perform binary search to find minimal working FIFO depth in LUTRAM range. + + Args: + node_idx: Node index + fifo_idx: FIFO index within node + baseline_depths: Original baseline FIFO depths (unchanged during minimization) + bitwidth: Data bitwidth + initial_fifo_depths: Baseline performance data + sim: Simulation controller + sim_cycles: Maximum simulation cycles + lower_luts: Lower bound for LUT count + upper_luts: Upper bound for LUT count (known to work) + + Returns: + Best working depth found + """ + _, max_d = calculate_srl16e_depth_range(upper_luts, bitwidth) + best_working_depth = max_d + + while lower_luts < upper_luts: + mid_luts = (lower_luts + upper_luts) // 2 + + # Prevent infinite loop + if mid_luts == upper_luts: + mid_luts = upper_luts - 1 + if mid_luts < lower_luts: + break + + # Find valid depth for this LUT count + _, max_d = calculate_srl16e_depth_range(mid_luts, bitwidth) + + if max_d == 0: + # No valid configuration, try more LUTs + lower_luts = mid_luts + 1 + continue + + success, _ = self._test_depth( + max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + ) + + if success: + # This depth works, try smaller + best_working_depth = max_d + upper_luts = mid_luts + else: + # This depth doesn't work, need larger + lower_luts = mid_luts + 1 + + return best_working_depth + + def _needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: + """Determine whether a FIFO can be minimized further. + + Args: + fifo_depth: Current FIFO depth + bitwidth: Data bitwidth + + Returns: + True if the FIFO can be minimized further, False otherwise. + """ + # Qsrl FIFO Formula: LUTs = ⌈depth/32⌉ x ⌈bitwidth/2⌉ + if fifo_depth <= 32: # FIFOs of depth <=32 fit into bitwidth/2 LUTs + return False + # Return False if exactly the minimum number of possible BRAM blocks is used for this + # bitwidth and depth is sufficiently large that further optimization is unlikely to succeed + return not ( + calculate_bram_blocks(fifo_depth, bitwidth) + <= self._get_valid_block_counts(1, bitwidth, bitwidth)[0] + and fifo_depth > math.floor(self.max_qsrl_depth * 1.1) + ) + + +def calculate_bram_blocks(depth: int, bitwidth: int) -> int: + """Calculate the number of BRAM blocks required for a BRAM FIFO. + + Args: + depth: FIFO depth + bitwidth: Data bitwidth + """ + if bitwidth == 1: + return math.ceil(depth / 16384) + if bitwidth == 2: + return math.ceil(depth / 8192) + if bitwidth <= 4: + return (math.ceil(depth / 4096)) * (math.ceil(bitwidth / 4)) + if bitwidth <= 9: + return (math.ceil(depth / 2048)) * (math.ceil(bitwidth / 9)) + if bitwidth <= 18 or depth > 512: + return (math.ceil(depth / 1024)) * (math.ceil(bitwidth / 18)) + return (math.ceil(depth / 512)) * (math.ceil(bitwidth / 36)) + + +def calculate_bram_depth_range(blocks: int, bitwidth: int) -> tuple[int, int]: + """Calculate the range of FIFO depths that use exactly the given number of BRAM blocks. + + Args: + blocks: Number of BRAM blocks + bitwidth: Data bitwidth + + Returns: + Tuple of (min_depth, max_depth) that uses exactly 'blocks' BRAM blocks. + """ + if blocks < 1: + raise FINNInternalError("Number of BRAM blocks must be at least 1") + + # Invert the formula from calculate_bram_blocks based on bitwidth + if bitwidth == 1: + # blocks = ⌈depth/16384⌉ + # Inversion: (blocks-1)*16384 < depth ≤ blocks*16384 + min_depth = (blocks - 1) * 16384 + 1 if blocks > 1 else 1 + max_depth = blocks * 16384 + elif bitwidth == 2: + # blocks = ⌈depth/8192⌉ + # Inversion: (blocks-1)*8192 < depth ≤ blocks*8192 + min_depth = (blocks - 1) * 8192 + 1 if blocks > 1 else 1 + max_depth = blocks * 8192 + elif bitwidth <= 4: + # blocks = ⌈depth/4096⌉ * ⌈bitwidth/4⌉ + bitwidth_factor = math.ceil(bitwidth / 4) + depth_blocks = math.ceil(blocks / bitwidth_factor) + min_depth = (depth_blocks - 1) * 4096 + 1 if depth_blocks > 1 else 1 + max_depth = depth_blocks * 4096 + elif bitwidth <= 9: + # blocks = ⌈depth/2048⌉ * ⌈bitwidth/9⌉ + bitwidth_factor = math.ceil(bitwidth / 9) + depth_blocks = math.ceil(blocks / bitwidth_factor) + min_depth = (depth_blocks - 1) * 2048 + 1 if depth_blocks > 1 else 1 + max_depth = depth_blocks * 2048 + elif bitwidth <= 18: + # blocks = ⌈depth/1024⌉ * ⌈bitwidth/18⌉ + bitwidth_factor = math.ceil(bitwidth / 18) + depth_blocks = math.ceil(blocks / bitwidth_factor) + min_depth = (depth_blocks - 1) * 1024 + 1 + max_depth = depth_blocks * 1024 + else: + # bitwidth > 18, split into two cases from original function + # Case 1: depth > 512 uses ⌈depth/1024⌉ * ⌈bitwidth/18⌉ + # Case 2: depth ≤ 512 uses ⌈depth/512⌉ * ⌈bitwidth/36⌉ + + # Try the depth > 512 case first (⌈depth/1024⌉ * ⌈bitwidth/18⌉) + bitwidth_factor = math.ceil(bitwidth / 18) + depth_blocks = math.ceil(blocks / bitwidth_factor) + + # Check if blocks is achievable with this bitwidth factor + if blocks % bitwidth_factor != 0 or depth_blocks < 1: + # Try the depth ≤ 512 case instead + pass + else: + min_depth = max((depth_blocks - 1) * 1024 + 1, 513) # Must be > 512 + max_depth = depth_blocks * 1024 + # Check if this range is valid (entirely > 512) + if min_depth > 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: + return (min_depth, max_depth) + + # Try the depth ≤ 512 case (⌈depth/512⌉ * ⌈bitwidth/36⌉) + bitwidth_factor = math.ceil(bitwidth / 36) + depth_blocks = math.ceil(blocks / bitwidth_factor) + + # Check if blocks is achievable with this bitwidth factor + if blocks % bitwidth_factor != 0 or depth_blocks < 1: + return (0, 0) # Invalid block count for this bitwidth + + min_depth = (depth_blocks - 1) * 512 + 1 if depth_blocks > 1 else 1 + max_depth = min(depth_blocks * 512, 512) # Must be ≤ 512 + + # Verify the range is valid (entirely ≤ 512 and produces correct block count) + if max_depth <= 512 and calculate_bram_blocks(min_depth, bitwidth) == blocks: + return (min_depth, max_depth) + + return (0, 0) # No valid range found + + # Verify the range is valid + if calculate_bram_blocks(min_depth, bitwidth) != blocks: + raise FINNInternalError("Calculated BRAM depth range is invalid!") + return (min_depth, max_depth) + + +def calculate_uram_blocks(depth: int, bitwidth: int) -> int: + """Calculate the number of URAM blocks required for a URAM FIFO. + + Args: + depth: FIFO depth + bitwidth: Data bitwidth + """ + return (math.ceil(depth / 4096)) * (math.ceil(bitwidth / 72)) + + +def calculate_uram_depth_range(blocks: int, bitwidth: int) -> tuple[int, int]: + """Calculate the range of FIFO depths that use exactly the given number of URAM blocks. + + Args: + blocks: Number of URAM blocks + bitwidth: Data bitwidth + + Returns: + Tuple of (min_depth, max_depth) that uses exactly 'blocks' URAM blocks. + Returns (0, 0) if no valid range exists. + """ + if blocks < 1: + return (0, 0) + + # URAM formula: blocks = ⌈depth/4096⌉ * ⌈bitwidth/72⌉ + bitwidth_factor = math.ceil(bitwidth / 72) + + # Calculate depth range + # Minimum depth: (blocks / bitwidth_factor - 1) * 4096 + 1 + # Maximum depth: (blocks / bitwidth_factor) * 4096 + + if blocks % bitwidth_factor != 0: + return (0, 0) # Invalid block count for this bitwidth + + depth_blocks = blocks // bitwidth_factor + min_depth = (depth_blocks - 1) * 4096 + 1 if depth_blocks > 1 else 1 + max_depth = depth_blocks * 4096 + + # Verify + if calculate_uram_blocks(min_depth, bitwidth) != blocks: + return (0, 0) + + return (min_depth, max_depth) + + +def calculate_srl16e_luts(depth: int, bitwidth: int) -> int: + """Calculate the number of SRL16E LUTs required for a FIFO. + + Args: + depth: FIFO depth (must be >= 2) + bitwidth: Data bitwidth + + Returns: + Number of SRL16E LUTs required without adress LUTs. + + Formula: LUTs = ⌈depth/32⌉ x ⌈bitwidth/2⌉ + """ + ram_luts = (math.ceil(depth / 32)) * (math.ceil(bitwidth / 2)) + return ram_luts + + +def calculate_srl16e_depth_range(luts: int, bitwidth: int) -> tuple[int, int]: + """Calculate the range of FIFO depths that use exactly the given number of SRL16E LUTs. + + Args: + luts: Number of SRL16E LUTs + bitwidth: Data bitwidth + + Returns: + Tuple of (min_depth, max_depth) that uses exactly 'luts' LUTs. + Returns (0, 0) if no valid range exists. + """ + if luts < 1: + return (0, 0) + + # SRL16E formula: luts = ⌈depth/32⌉ * ⌈bitwidth/2⌉ + bitwidth_factor = math.ceil(bitwidth / 2) + + # Calculate depth range + if luts % bitwidth_factor != 0: + return (0, 0) # Invalid LUT count for this bitwidth + + depth_blocks = luts // bitwidth_factor + min_depth = (depth_blocks - 1) * 32 + 1 if depth_blocks > 1 else 2 + max_depth = depth_blocks * 32 + + # Verify + if calculate_srl16e_luts(min_depth, bitwidth) != luts: + return (0, 0) + + return (min_depth, max_depth) diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index 134baf6770..04bcf5ee20 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -1,20 +1,17 @@ """Control (node based) simulations via unix sockets.""" import json -import multiprocessing import socket import subprocess import threading import time -import traceback -from concurrent.futures import Future, ThreadPoolExecutor from pathlib import Path from rich.console import Console from threading import Lock -from typing import Any, Literal +from typing import Any from finn.util.basic import make_build_dir -from finn.util.exception import FINNInternalError, FINNUserError +from finn.util.exception import FINNInternalError from finn.util.logging import ThreadsafeProgressDisplay @@ -334,437 +331,3 @@ def _cleanup_sockets(self) -> None: finally: stdout_file.close() stderr_file.close() - - -class NodeIsolatedSimulationController(SimulationController): - """Run simulations for node isolated cases.""" - - IsolatedSimLogData = dict[Literal["ready", "valid"], list[dict[str, int]]] - - def __init__( - self, - parallel_simulations: int, - names: list[str], - binaries: list[Path], - console: Console, - poll_interval: float = 1.0, - with_progressbar: bool = False, - ) -> None: - """Set up node isolated simulation.""" - super().__init__( - parallel_simulations, names, binaries, console, poll_interval, with_progressbar - ) - self.console.log("Started simulation controller") - - def postprocess_logs( - self, d: Path, readylog_name: str = "readylog.txt", validlog_name: str = "validlog.txt" - ) -> IsolatedSimLogData: - """Recieve the directory containing a binary and the simulation logs. - If no logs are found raises an error, otherwise return the postprocessed logs - read from JSON. - """ - readylog = d / readylog_name - validlog = d / validlog_name - if not readylog.exists() or not validlog.exists(): - raise FINNInternalError(f"Could not find simulation logs at {readylog} and {validlog}") - return { - "ready": json.loads(readylog.read_text()), - "valid": json.loads(validlog.read_text()), - } - - def run(self) -> dict[str, IsolatedSimLogData]: - """Run a node isolated simulation and return the collected - input ready / output valid data, indexed based on node names.""" - futures: list[Future] = [] - with self.console.status(f"Running simulation on every node. Log directory: {self.logdir}"): - with ThreadPoolExecutor(len(self.binaries)) as tpe: - for binary in self.binaries: - futures.append(tpe.submit(self._run_binary, binary)) - tpe.shutdown(wait=True) - self._cleanup_sockets() - - # Read data - data: dict[str, self.IsolatedSimLogData] = {} - invalid = [] - for i, future in enumerate(futures): - data[self.names[i]] = future.result() - if data[self.names[i]] is None: - invalid.append(self.names[i]) - if len(invalid) > 0: - raise FINNInternalError( - f"Lost connection / malformed response from nodes: {', '.join(invalid)}" - ) - return data - - def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: - """Run simulation. Returning None if connection is lost.""" - process_index = self.binaries.index(binary) - with ( - self.logdir / f"{process_index}_log_isolated_{self.names[process_index]}_python.txt" - ).open("w+") as logfile: - # Initialize - logfile.write("Initializing simulation.\n") - proc_idx = self._start_process(binary, process_index) - response = self._send_and_receive(proc_idx, "start", {}) - if response is None: - logfile.write("Client disconnected / No answer received to start command!\n") - return None - logfile.write(f"Start response: {response}\n") - - if response is None: - logfile.write("Failed to start simulation: No response\n") - return None - - # Main loop - logfile.write("Beginning main loop\n") - logfile.write( - "totalCycles,inputCyclesDone,inputCyclesTarget," - "outputCyclesDone,outputCyclesTarget\n" - ) - logfile.flush() - - while True: - time.sleep(self.poll_interval) - logfile.write("Sending status request\n") - response = self._send_and_receive(proc_idx, "status", {}) - if response is None: - self.console.log(f"Empty response from {proc_idx} at {binary.parent}") - logfile.write("Empty response. Returning.\n") - return None - state = response["state"] - if state == "done": - self.console.log(f"{process_index} is done and postprocessing data.") - return self.postprocess_logs(binary.parent) - - # TODO: Order seems wrong - logfile.write( - f"{response['totalCycles']}, " - f"{response['inputCyclesDone']}, " - f"{response['inputCyclesTarget']}, " - f"{response['outputCyclesDone']}, " - f"{response['outputCyclesTarget']}\n" - ) - - -class NodeConnectedSimulationController(SimulationController): - """Run simulations for node connected cases.""" - - def __init__( - self, - parallel_simulations: int, - names: list[str], - binaries: list[Path], - console: Console, - poll_interval: float = 1.0, - with_progressbar: bool = True, - ) -> None: - """Set up node connected simulation.""" - super().__init__( - parallel_simulations, names, binaries, console, poll_interval, with_progressbar - ) - for binary in binaries: - if not binary.exists(): - console.log(f"Binary {binary} does not exist!") - raise FINNUserError(f"Binary {binary} does not exist!") - - def run( - self, - depth: list[list[int]] | None = None, - output_json: Path | None = None, - max_cycles: int | None = None, - ) -> dict[str, list[int]]: - """Run the simulation entirely with the given depth and sample count. - - Args: - depth: FIFO depth to configure for simulations. - samples: Number of samples to simulate. - output_json: Optional path to write merged simulation data as JSON. - max_cycles: Max cycles - - Returns: - Dictionary mapping simulation names to their FIFO utilization arrays. - """ - futures: list[Future] = [] - fifo_results: dict[str, list[int]] = {} - cycles_results: dict[str, int] = {} - samples_results: dict[str, int] = {} - intervals_results: dict[str, list[int]] = {} - timeout_result = False - fifo_depths: dict[str, list[int]] = {} - - if self.progress is not None: - self.progress.start() - try: - with ThreadPoolExecutor(self.workers) as pool: - for i, (name, binary) in enumerate(zip(self.names, self.binaries, strict=True)): - is_last_node = i == len(self.names) - 1 - is_special_for_display = i == 0 or is_last_node - futures.append( - pool.submit( - self._run_binary, - binary, - name, - i % multiprocessing.cpu_count(), - depth[i] if depth is not None else None, - is_last_node, # Only last node has no output FIFOs - is_special_for_display, # First and last get special coloring - max_cycles, - ) - ) - - # Wait for first completion or error - from concurrent.futures import FIRST_COMPLETED, wait - - all_futures = list(futures) # Keep track of all futures - while futures: - done, futures = wait(futures, return_when=FIRST_COMPLETED) - - # Check if any completed task indicates we should stop - for future in done: - try: - result = future.result() # This will raise if there was an exception - if result is not None: - ( - sim_name, - fifo_util, - cycles, - samps, - intervals, - timeout, - fifo_depth, - ) = result - fifo_depths[sim_name] = fifo_depth - fifo_results[sim_name] = fifo_util - cycles_results[sim_name] = cycles - samples_results[sim_name] = samps - intervals_results[sim_name] = intervals - timeout_result = timeout_result or timeout - except Exception as e: # noqa - self.console.log(f"Simulation failed: {e}") - # Set stop flag and break - with self.stop_lock: - self.should_stop = True - break - - # If we should stop, signal all remaining simulations - with self.stop_lock: - if self.should_stop: - # Don't cancel - let them finish with early stop - break - - # Wait for all futures to complete and collect their results - pool.shutdown(wait=True) - for future in all_futures: - if not future.done(): - continue - try: - result = future.result() - if result is not None: - ( - sim_name, - fifo_util, - cycles, - samps, - intervals, - timeout, - fifo_depth, - ) = result - # Only update if not already collected - if sim_name not in fifo_results: - fifo_depths[sim_name] = fifo_depth - fifo_results[sim_name] = fifo_util - cycles_results[sim_name] = cycles - samples_results[sim_name] = samps - intervals_results[sim_name] = intervals - timeout_result = timeout_result or timeout - except Exception as e: - self.console.log(f"Error collecting result: {e}") - finally: - if self.progress is not None: - self.progress.stop() - self._cleanup_sockets() - - # Merge all simulation data - if output_json is not None: - merged_data = { - "simulations": [ - { - "name": name, - "fifo_utilization": fifo_results.get(name, []), - "fifo_depth": fifo_depths.get(name, []), - "cycles": cycles_results.get(name, 0), - "samples": samples_results.get(name, 0), - "intervals": intervals_results.get(name, []), - } - for name in self.names - ], - "depth_configured": depth, - "timeout_occurred": timeout_result, - } - output_json.write_text(json.dumps(merged_data, indent=2)) - - return fifo_results - - def _run_binary( - self, - binary: Path, - name: str | None, - _cpu: int | None, - depth: list[int] | None = None, - is_last_node: bool = False, - is_special_for_display: bool = False, - max_cycles: int | None = None, - ) -> tuple[str, list[int], int, int, list[int], bool, list[int]] | None: - """Run the specified simulation binary in a new subprocess and communicate with it. - - Args: - binary: Path to simulation binary - name: Name of simulation node - _cpu: CPU affinity (unused) - depth: List of FIFO depths for this node's output FIFOs - is_last_node: True if this is the last node (no output FIFOs to configure) - is_special_for_display: True if this node should get special color in logs - max_cycles: Maximum cycles to simulate - - Returns: - Tuple of (simulation_name, fifo_utilization, cycles, samples, intervals, timeout, - fifo_depth) on success, - None on failure. - """ - cwd = binary.parent - if name is None: - name = cwd.name.replace("rtlsim_", "") - - process_index = self.names.index(name) - - with (self.logdir / f"{name}_{process_index}_of_{self.total}.txt").open("w+") as logfile: - - def _print(msg: str, color: str = "green") -> None: - if self.progress is None: - if is_special_for_display: - color = "orange3" - if "ERROR" in msg: - color = "red" - self.console.log( - f"[bold {color}]{name:<35}" - f"[/bold {color}][cornflower_blue]{process_index} " - f"/ {len(self.names) - 1}[/cornflower_blue] {msg:<35}" - ) - logfile.write(f"{msg}\n") - logfile.flush() - - try: - # Start the simulation process with socket communication - proc_idx = self._start_process(binary, process_index) - - # Send configuration commands - # Last node has no output FIFOs, so don't configure FIFO depths - config_payload: dict[str, list[int] | int] = {} - if not is_last_node and depth is not None: - config_payload["fifo_depth"] = depth - if max_cycles is not None: - config_payload["max_cycles"] = max_cycles - - response = self._send_and_receive(proc_idx, "configure", config_payload) - - if not response or response.get("status") != "success": - error_msg = ( - response.get("message", "Unknown error") if response else "No response" - ) - _print(f"Configuration failed: {error_msg}", "red") - return None - - # Start the simulation - response = self._send_and_receive(proc_idx, "start", {}) - - if not response or response.get("status") != "success": - error_msg = ( - response.get("message", "Unknown error") if response else "No response" - ) - _print(f"Failed to start simulation: {error_msg}", "red") - return None - - cycles = 0 - samps = 0 - intervals: list[int] = [] - timeout = False - fifo_util: list[int] = [] - fifo_depth: list[int] = [] - - # Poll for status updates - while True: - # Check if we should stop early - with self.stop_lock: - if self.should_stop: - try: - stop_response = self._send_and_receive(proc_idx, "stop", {}) - except (BrokenPipeError, ConnectionResetError, RuntimeError): - # Process may have already exited - that's ok during shutdown - stop_response = None - if stop_response: - cycles = stop_response.get("cycles", 0) - samps = stop_response.get("samples", 0) - fifo_util = stop_response.get("fifo_utilization", []) - intervals = stop_response.get("intervals", []) - fifo_depth = stop_response.get("fifo_depth", []) - timeout = stop_response.get("timeout", False) - if fifo_util: - logfile.write(f"Final FIFO utilization: {fifo_util}\n") - return (name, fifo_util, cycles, samps, intervals, timeout, fifo_depth) - time.sleep(self.poll_interval) - - response = self._send_and_receive(proc_idx, "status", {}) - - if not response: - _print("Lost connection to simulation", "red") - with self.stop_lock: - self.should_stop = True - raise RuntimeError("Lost connection to simulation") - - state = response.get("state", "unknown") - - if state == "finished" or state == "timeout": - cycles = response.get("cycles", 0) - samps = response.get("samples", 0) - fifo_util = response.get("fifo_utilization", []) - fifo_depth = response.get("fifo_depth", []) - intervals = response.get("intervals", []) - timeout = response.get("timeout", False) - with self.stop_lock: - self.should_stop = True - break - - if state == "running": - # Update progress if available - cycles = response.get("cycles", 0) - - if state == "error": - error_msg = response.get("message", "Unknown error") - _print(f"Simulation error: {error_msg}", "red") - # Signal other simulations to stop - with self.stop_lock: - self.should_stop = True - raise RuntimeError(f"Simulation error: {error_msg}") - - # Stop the simulation - stop_response = self._send_and_receive(proc_idx, "stop", {}) - fifo_util = [] - - if stop_response: - fifo_util = stop_response.get("fifo_utilization", []) - fifo_depth = stop_response.get("fifo_depth", []) - cycles = stop_response.get("cycles", 0) - samps = stop_response.get("samples", 0) - if fifo_util: - logfile.write(f"Final FIFO utilization: {fifo_util}\n") - - return (name, fifo_util, cycles, samps, intervals, timeout, fifo_depth) - - except Exception as e: - self.console.log(f"Exception caught during simulation execution ({name}): {e}") - self.console.log(traceback.format_exc()) - logfile.write(f"Exception: {e}\n") - logfile.write(traceback.format_exc()) - with self.stop_lock: - self.should_stop = True - return None diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py new file mode 100644 index 0000000000..516c087859 --- /dev/null +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -0,0 +1,385 @@ +"""Simulating layers on their own to observe their behaviour.""" +import json +import time +from concurrent.futures import Future, ThreadPoolExecutor +from pathlib import Path +from qonnx.core.modelwrapper import ModelWrapper +from qonnx.transformation.base import Transformation +from rich.console import Console +from typing import Literal, TypeAlias + +from finn.transformation.fpgadataflow.simulation import Simulation +from finn.transformation.fpgadataflow.simulation_build import SimulationType +from finn.transformation.fpgadataflow.simulation_controller import SimulationController +from finn.util.exception import FINNInternalError +from finn.util.logging import DisabledLoggingConsole, log + + +class NodeIsolatedSimulationController(SimulationController): + """Run simulations for node isolated cases.""" + + IsolatedSimLogData = dict[Literal["ready", "valid"], list[dict[str, int]]] + + def __init__( + self, + parallel_simulations: int, + names: list[str], + binaries: list[Path], + console: Console, + poll_interval: float = 1.0, + with_progressbar: bool = False, + ) -> None: + """Set up node isolated simulation.""" + super().__init__( + parallel_simulations, names, binaries, console, poll_interval, with_progressbar + ) + self.console.log("Started simulation controller") + + def postprocess_logs( + self, d: Path, readylog_name: str = "readylog.txt", validlog_name: str = "validlog.txt" + ) -> IsolatedSimLogData: + """Recieve the directory containing a binary and the simulation logs. + If no logs are found raises an error, otherwise return the postprocessed logs + read from JSON. + """ + readylog = d / readylog_name + validlog = d / validlog_name + if not readylog.exists() or not validlog.exists(): + raise FINNInternalError(f"Could not find simulation logs at {readylog} and {validlog}") + return { + "ready": json.loads(readylog.read_text()), + "valid": json.loads(validlog.read_text()), + } + + def run(self) -> dict[str, IsolatedSimLogData]: + """Run a node isolated simulation and return the collected + input ready / output valid data, indexed based on node names.""" + futures: list[Future] = [] + with self.console.status(f"Running simulation on every node. Log directory: {self.logdir}"): + with ThreadPoolExecutor(len(self.binaries)) as tpe: + for binary in self.binaries: + futures.append(tpe.submit(self._run_binary, binary)) + tpe.shutdown(wait=True) + self._cleanup_sockets() + + # Read data + data: dict[str, self.IsolatedSimLogData] = {} + invalid = [] + for i, future in enumerate(futures): + data[self.names[i]] = future.result() + if data[self.names[i]] is None: + invalid.append(self.names[i]) + if len(invalid) > 0: + raise FINNInternalError( + f"Lost connection / malformed response from nodes: {', '.join(invalid)}" + ) + return data + + def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: + """Run simulation. Returning None if connection is lost.""" + process_index = self.binaries.index(binary) + with ( + self.logdir / f"{process_index}_log_isolated_{self.names[process_index]}_python.txt" + ).open("w+") as logfile: + # Initialize + logfile.write("Initializing simulation.\n") + proc_idx = self._start_process(binary, process_index) + response = self._send_and_receive(proc_idx, "start", {}) + if response is None: + logfile.write("Client disconnected / No answer received to start command!\n") + return None + logfile.write(f"Start response: {response}\n") + + if response is None: + logfile.write("Failed to start simulation: No response\n") + return None + + # Main loop + logfile.write("Beginning main loop\n") + logfile.write( + "totalCycles,inputCyclesDone,inputCyclesTarget," + "outputCyclesDone,outputCyclesTarget\n" + ) + logfile.flush() + + while True: + time.sleep(self.poll_interval) + logfile.write("Sending status request\n") + response = self._send_and_receive(proc_idx, "status", {}) + if response is None: + self.console.log(f"Empty response from {proc_idx} at {binary.parent}") + logfile.write("Empty response. Returning.\n") + return None + state = response["state"] + if state == "done": + self.console.log(f"{process_index} is done and postprocessing data.") + return self.postprocess_logs(binary.parent) + + # TODO: Order seems wrong + logfile.write( + f"{response['totalCycles']}, " + f"{response['inputCyclesDone']}, " + f"{response['inputCyclesTarget']}, " + f"{response['outputCyclesDone']}, " + f"{response['outputCyclesTarget']}\n" + ) + + +FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] +IsoSimLogData = NodeIsolatedSimulationController.IsolatedSimLogData +IsoSimLogDataByLayer = dict[str, IsoSimLogData] # Indexed by layer name + + +class IsolatedSimulation(Simulation): + def __init__( + self, + model: ModelWrapper, + simulation_type: SimulationType, + fpgapart: str, + clk_ns: float, + functional_sim: bool, + workers: int | None = None, + ) -> None: + super().__init__(model, simulation_type, fpgapart, clk_ns, functional_sim, workers) + + def simulate(self) -> IsoSimLogDataByLayer: + """Simulate isolated nodes.""" + if self.simulation_type != SimulationType.NODE_BASED_ISOLATED: + raise FINNInternalError( + f"Called simulation function 'simulate_node_isolated' " + f"does not match provided simulation type " + f"{self.simulation_type}" + ) + names = [node.name for node in self.model.graph.node] + with DisabledLoggingConsole() as console: + controller = NodeIsolatedSimulationController( + len(self.binaries), names, list(self.binaries.values()), console, 0.1, False + ) + return controller.run() + + +class RunLayerIsolatedSimulation(Transformation): + """Run a layer isolated simulation and calculate some information for a + later layer parallel simulation.""" + + def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: + """Run isolated layer simulations.""" + super().__init__() + self.fpgapart = fpgapart + self.clk_ns = clk_ns + self.functional_sim = functional_sim + + def calculate_upper_bounds(self, data: IsoSimLogDataByLayer) -> dict[str, dict[str, int]]: + """Try to calculate an upper bound for the incoming FIFO size of the layers. + Return size indexed by layer name and stream name. + + >>> step = RunLayerIsolatedSimulation("", 0.0, False) + >>> bounds = step.calculate_upper_bounds({ + ... "A": { + ... "ready": [ + ... {"totalCycles": 43, "inputCyclesDone": 12, + ... "inputCyclesTarget": 24, "s_axi_0": 1, "s_axi_1": 0}, + ... {"totalCycles": 44, "inputCyclesDone": 13, + ... "inputCyclesTarget": 24, "s_axi_0": 0, "s_axi_1": 0}, + ... ], "valid": [] + ... }, + ... "B": { + ... "ready": [ + ... {"totalCycles": 100, "inputCyclesDone": 3, + ... "inputCyclesTarget": 10, "s_axi_0": 1, "s_axi_1": 1, + ... "s_axi_2": 0}, + ... ], "valid": [] + ... }, + ... "C": { + ... "ready": [ + ... {"totalCycles": 43, "inputCyclesDone": 14, + ... "inputCyclesTarget": 24, "s_axi_0": 1, "s_axi_1": 0}, + ... {"totalCycles": 44, "inputCyclesDone": 15, + ... "inputCyclesTarget": 24, "s_axi_0": 0, "s_axi_1": 0}, + ... ], "valid": [] + ... } + ... }) + >>> bounds["A"] + {'s_axi_0': 1, 's_axi_1': 2} + >>> bounds["B"] + {'s_axi_0': 0, 's_axi_1': 0, 's_axi_2': 1} + >>> bounds["C"] + {'s_axi_0': 0, 's_axi_1': 0} + """ + + # TODO: Proper pytest tests + def _any_ready(cycle_data: dict[str, int]) -> bool: + for key in cycle_data.keys(): + if ( + key not in ["totalCycles", "inputCyclesDone", "inputCyclesTarget"] + and cycle_data[key] == 1 + ): + return True + return False + + results: dict[str, dict[str, int]] = {} + for layer in data.keys(): + # Save all keys that are not + results[layer] = { + stream_name: 0 + for stream_name in data[layer]["ready"][0].keys() + if stream_name not in ["inputCyclesDone", "inputCyclesTarget", "totalCycles"] + } + for cycle_data in data[layer]["ready"]: + if cycle_data["inputCyclesDone"] > int( + cycle_data["inputCyclesTarget"] / 2 + ) and _any_ready(cycle_data): + break + for stream_name in results[layer].keys(): + # TODO: Currently on the C++ side we multiply the + # TODO: target cycles by 2, to get two samples + # TODO: We keep track of ready signals until we see + # TODO: the first ready after half of all cycles were seen. + # TODO: This might change in the future + if cycle_data["inputCyclesTarget"] % 2 != 0: + raise FINNInternalError( + f"An 'inputCyclesTarget' of layer {layer} seems " + f"to not be an even number. Currently, we double " + f"the target simulation cycles for every layer " + f"on the C++ side. This error may point towards " + f"a change on the C++ side, which may cause the " + f"need to update this function accordingly!" + ) + results[layer][stream_name] += int(cycle_data[stream_name] == 0) + + # TODO: This calculation assumes, that if the producer does NOT fire the entire time, + # TODO: the consumer can read at least at the same speed as + # if the producer did, and not slower. + # TODO: (Since this would mean that less data pressure from + # the producer makes the consumer _slower_.) + # TODO: This should usually be the case, but is important to keep in mind. + return results + + def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: + """Do checks on the returned data to make sure it is in spec. + + A correctly formatted example would be: + >>> data = { + ... "layer1": { + ... "ready": [{"totalCycles": 10, "inputCyclesDone": 5, + ... "inputCyclesTarget": 10, "s_axi0_ready": 1}], + ... "valid": [{"totalCycles": 10, "outputCyclesDone": 5, + ... "outputCyclesTarget": 10, "m_axi0_valid": 1}] + ... } + ... } + >>> sim = RunLayerIsolatedSimulation("", 0.0, False) + >>> sim.sanity_check_logged_data(data) + >>> + """ + # 0. Valid and ready are present + for layer, ldata in data.items(): + if "valid" not in ldata.keys(): + raise FINNInternalError( + f"Simulation log data of layer {layer} is missing the VALID log." + ) + if "ready" not in ldata.keys(): + raise FINNInternalError( + f"Simulation log data of layer {layer} is missing the READY log." + ) + # 1. All cycle datas are uniform and have at least one stream signal + for layer, ldata in data.items(): + cycle_data = ldata["ready"] + ldata["valid"] + lengths: set[int] = {len(cycle.keys()) for cycle in cycle_data} + if len(lengths) != 1: + raise FINNInternalError( + f"Simulation log data inconsistent for layer " + f"{layer}. Differing number of fields per cycle." + ) + if next(iter(lengths)) < 4: + raise FINNInternalError( + f"Simulation for layer {layer} must contain " + f"atleast 4 fields (total cycles, AXI cycles " + f"done, AXI cycles target and at least one AXI " + f"ready/valid signal)!" + ) + # 2. All ready logs contain the required keywords + readykeys = ["inputCyclesDone", "inputCyclesTarget", "totalCycles"] + for rlayer, rdata in data.items(): + for cycle in rdata["ready"]: + if any(keyword not in cycle.keys() for keyword in readykeys): + raise FINNInternalError( + f"Simulation READY log of layer {rlayer} " + f"contains cycles that are missing a required key." + ) + if any(key not in readykeys and "axi" not in key for key in cycle.keys()): + raise FINNInternalError( + f"In the READY simulation log of layer " + f"{rlayer} there seem to be fields that " + f"are not expected keywords or AXI streams!" + ) + # 3. All valid logs contain the required keywords + validkeys = ["outputCyclesDone", "outputCyclesTarget", "totalCycles"] + for vlayer, vdata in data.items(): + for cycle in vdata["valid"]: + if any(keyword not in cycle.keys() for keyword in validkeys): + raise FINNInternalError( + f"Simulation VALID log of layer {vlayer} " + f"contains cycles that are missing a required key." + ) + if any(key not in validkeys and "axi" not in key for key in cycle.keys()): + raise FINNInternalError( + f"In the VALID simulation log of layer " + f"{vlayer} there seem to be fields that " + f"are not expected keywords or AXI streams!" + ) + # 4. Cycles done can never be larger then the number of total cycles passed in the sim + for layer, cdata in data.items(): + for line in cdata["ready"] + cdata["valid"]: + if ( + "inputCyclesDone" in line.keys() + and line["inputCyclesDone"] > line["totalCycles"] + ): + raise FINNInternalError( + f"Simulation log of layer {layer} looks incorrect: " + f"Number of active receiving cycles " + f"({line['inputCyclesDone']}) larger than number of " + f"total cycles passed ({line['totalCycles']})." + ) + if ( + "outputCyclesDone" in line.keys() + and line["outputCyclesDone"] > line["totalCycles"] + ): + raise FINNInternalError( + f"Simulation log of layer {layer} looks incorrect: " + f"Number of active producing cycles " + f"({line['outputCyclesDone']}) larger than number of " + f"total cycles passed ({line['totalCycles']})." + ) + # 5. Stream keywords can never have any other value than 1 (HIGH) or 0 (LOW) + reserved_keywords = readykeys + validkeys + for layer, ldata in data.items(): + for cycle_data in ldata["ready"] + ldata["valid"]: + for key in cycle_data.keys(): + if key not in reserved_keywords and cycle_data[key] not in [0, 1]: + raise FINNInternalError( + f"Layer {layer} has data point where a " + f"non-reserved field (thus an axi stream " + f"ready/valid signal) is neither 0 nor 1: " + f"Key: {key}, Value: {cycle_data[key]}" + ) + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Run isolated layer simulations.""" + sim = Simulation( + model, + SimulationType.NODE_BASED_ISOLATED, + self.fpgapart, + self.clk_ns, + self.functional_sim, + ) + data: IsoSimLogDataByLayer = sim.simulate_node_isolated() + self.sanity_check_logged_data(data) + in_fifo_upper_bound = self.calculate_upper_bounds(data) + formatted_upper_bounds = "\n\t".join( + [f"{name}: {in_fifo_upper_bound[name]}" for name in in_fifo_upper_bound.keys()] + ) + log.info("Upper bounds: \n" + formatted_upper_bounds) + + raise NotImplementedError() + # TODO: Integrate data into the layer parallel simulation + return model, False From 9446fbc3b32f36b76142cf24851212674a864992 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 27 Jan 2026 15:26:54 +0100 Subject: [PATCH 055/170] Fixed naming issues --- .../transformation/fpgadataflow/simulation_connected.py | 8 +++----- .../transformation/fpgadataflow/simulation_isolated.py | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index ae5d9d29dc..2769c286ad 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -346,7 +346,7 @@ def _print(msg: str, color: str = "green") -> None: return None -class IsolatedSimulation(Simulation): +class NodeConnectedSimulation(Simulation): def __init__( self, model: ModelWrapper, @@ -423,7 +423,7 @@ def __init__( def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run layer parallel simulations.""" - sim = Simulation( + sim = NodeConnectedSimulation( model, SimulationType.NODE_BASED_CONNECTED, self.fpgapart, @@ -542,9 +542,7 @@ def _test_depth( test_depths = [row[:] for row in baseline_depths] # Deep copy from baseline test_depths[node_idx][fifo_idx] = test_depth - new_data, timeout = sim.simulate_node_connected( - test_depths, max_cycles=math.ceil(sim_cycles * 1.1) - ) + new_data, timeout = sim.simulate(test_depths, max_cycles=math.ceil(sim_cycles * 1.1)) if timeout: return False, True diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 516c087859..e42006c816 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -365,14 +365,14 @@ def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run isolated layer simulations.""" - sim = Simulation( + sim = IsolatedSimulation( model, SimulationType.NODE_BASED_ISOLATED, self.fpgapart, self.clk_ns, self.functional_sim, ) - data: IsoSimLogDataByLayer = sim.simulate_node_isolated() + data: IsoSimLogDataByLayer = sim.simulate() self.sanity_check_logged_data(data) in_fifo_upper_bound = self.calculate_upper_bounds(data) formatted_upper_bounds = "\n\t".join( From b33d2fd46493abd3591d2bba8284ef09451264c3 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 27 Jan 2026 23:50:14 +0100 Subject: [PATCH 056/170] Fixed socket communication issues in isolated simulations --- .../finn_xsi/IsolatedSimulationBackend.cpp | 20 ++++++++++++------- .../finn_xsi/include/IsolatedSimulation.hpp | 2 +- .../fpgadataflow/simulation_isolated.py | 19 ++++++++++++------ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp index d8d26a9855..b06e600471 100644 --- a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp @@ -23,9 +23,7 @@ int main(int argc, const char* argv[]) { "xsim_log_file.txt", "trace_file.txt", RTLSimConfig::istream_descs, - RTLSimConfig::ostream_descs, - "readylog.txt", - "validlog.txt" + RTLSimConfig::ostream_descs ); @@ -51,7 +49,11 @@ int main(int argc, const char* argv[]) { std::mutex simMutex; // Command processing loop + std::size_t cycles = 0; + std::size_t statusSent = 0; + json response; while (true) { + response = json::object(); // Read message std::cout << "Awaiting message..." << std::endl; auto request = server.receive_message(); @@ -61,9 +63,8 @@ int main(int argc, const char* argv[]) { } // Process message - std::size_t cycles = 0; - json response; std::string command = (*request)["command"]; + std::cout << "[Received command] " << command << std::endl; if (command == "start") { std::cout << "Starting simulation" << std::endl; if (!simThread.has_value()) { @@ -95,6 +96,8 @@ int main(int argc, const char* argv[]) { } else if (command == "stop") { std::cout << "Stopping simulation." << std::endl; std::lock_guard guard(simMutex); + std::cout << "Final status: " << sim.getStatus() << std::endl; + std::cout << "Is done? " << sim.isDone() << std::endl; sim.halt(); if (simThread.has_value()) { simThread->request_stop(); @@ -111,9 +114,12 @@ int main(int argc, const char* argv[]) { response["state"] = "halted"; server.send_message(response); } else if (command == "status") { - std::cout << "Sending status update." << std::endl; + std::cout << "[Sending] Sending status update " << statusSent + 1 << std::endl; std::lock_guard guard(simMutex); - server.send_message(sim.getStatus()); + json status = sim.getStatus(); + server.send_message(status); + statusSent++; + std::cout << "[Sending] Status " << statusSent << " update sent!" << std::endl; } else { std::cout << "Unknown command " << command << std::endl; std::cerr << "Unknown command " << command << std::endl; diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index a6aebdd2d0..e33fc98e14 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -112,7 +112,7 @@ class IsolatedSimulation : public Simulation ) : Simulation( kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs ), simState(*this), readyJson(json::array()), validJson(json::array()), - readylogName("readylog.txt"), validlogName("validlog") { + readylogName("readylog.txt"), validlogName("validlog.txt") { inJobSizes.resize(_istream_descs.size()); outJobSizes.resize(_ostream_descs.size()); std::transform( diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index e42006c816..7246cc1144 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -60,6 +60,7 @@ def run(self) -> dict[str, IsolatedSimLogData]: for binary in self.binaries: futures.append(tpe.submit(self._run_binary, binary)) tpe.shutdown(wait=True) + self.console.log("Thread pool closed. Closing sockets and postprocessing data") self._cleanup_sockets() # Read data @@ -96,10 +97,6 @@ def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: # Main loop logfile.write("Beginning main loop\n") - logfile.write( - "totalCycles,inputCyclesDone,inputCyclesTarget," - "outputCyclesDone,outputCyclesTarget\n" - ) logfile.flush() while True: @@ -107,12 +104,16 @@ def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: logfile.write("Sending status request\n") response = self._send_and_receive(proc_idx, "status", {}) if response is None: - self.console.log(f"Empty response from {proc_idx} at {binary.parent}") - logfile.write("Empty response. Returning.\n") return None state = response["state"] if state == "done": self.console.log(f"{process_index} is done and postprocessing data.") + logfile.write("Received done status. Sending stop signal\n") + resp = self._send_and_receive(proc_idx, "stop", {}) + if resp is None: + logfile.write("No stop response received.\n") + else: + logfile.write("Stop successfully received: " + str(resp)) return self.postprocess_logs(binary.parent) # TODO: Order seems wrong @@ -362,6 +363,12 @@ def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: f"ready/valid signal) is neither 0 nor 1: " f"Key: {key}, Value: {cycle_data[key]}" ) + # 6. Data is not empty + for layer, ldata in data.items(): + if len(ldata["ready"]) == 0: + raise FINNInternalError(f"Layer {layer} has no ready data!") + if len(ldata["valid"]) == 0: + raise FINNInternalError(f"Layer {layer} has no valid data!") def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run isolated layer simulations.""" From e252ad65cbee0d2fe750cf9619c24f0c8bad9dc8 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 28 Jan 2026 16:50:43 +0100 Subject: [PATCH 057/170] IsoSim bugfixes and logging --- .../finn_xsi/IsolatedSimulationBackend.cpp | 39 ++++++---- .../finn_xsi/include/IsolatedSimulation.hpp | 9 ++- .../fpgadataflow/simulation_controller.py | 5 +- .../fpgadataflow/simulation_isolated.py | 78 ++++++++++++++----- 4 files changed, 93 insertions(+), 38 deletions(-) diff --git a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp index b06e600471..ce1a46b381 100644 --- a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp @@ -1,12 +1,21 @@ #include #include #include +#include #include #include namespace po = boost::program_options; +std::string getTime() { + auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + auto formatted = std::put_time(std::localtime(&now), "[%T]"); + std::stringstream ss; + ss << formatted; + return ss.str(); +} + int main(int argc, const char* argv[]) { // Parse CLI options @@ -55,25 +64,25 @@ int main(int argc, const char* argv[]) { while (true) { response = json::object(); // Read message - std::cout << "Awaiting message..." << std::endl; + std::cout << getTime() << " Awaiting message..." << std::endl; auto request = server.receive_message(); if (!request.has_value()) { - std::cout << "Connection closed or error occurred" << std::endl; + std::cout << getTime() << " Connection closed or error occurred" << std::endl; break; } // Process message std::string command = (*request)["command"]; - std::cout << "[Received command] " << command << std::endl; + std::cout << getTime() << " [Received command] " << command << std::endl; if (command == "start") { - std::cout << "Starting simulation" << std::endl; + std::cout << getTime() << " Starting simulation" << std::endl; if (!simThread.has_value()) { simThread = std::jthread([&sim, &simMutex, &cycles](std::stop_token stop) { { std::lock_guard guard(simMutex); sim.simulate(true); } - std::cout << "Simulation initialized. Going into main loop." << std::endl; + std::cout << getTime() << " Simulation initialized. Going into main loop." << std::endl; while (!stop.stop_requested()) { std::lock_guard guard(simMutex); if (cycles % 10000 == 0) { @@ -82,7 +91,11 @@ int main(int argc, const char* argv[]) { sim.simulate(false); ++cycles; if (sim.isDone()) { - sim.commitLogsToDisk(true); + // For now do not clean up the JSON logs, as this is + // done by the "stop" command from the python side of things. + // TODO: However this should be changed when the communication is + // rewritten + sim.commitLogsToDisk(false); break; } } @@ -94,10 +107,10 @@ int main(int argc, const char* argv[]) { response["state"] = "running"; server.send_message(response); } else if (command == "stop") { - std::cout << "Stopping simulation." << std::endl; + std::cout << getTime() << " Stopping simulation." << std::endl; std::lock_guard guard(simMutex); - std::cout << "Final status: " << sim.getStatus() << std::endl; - std::cout << "Is done? " << sim.isDone() << std::endl; + std::cout << getTime() << " Final status: " << sim.getStatus() << std::endl; + std::cout << getTime() << " Is done? " << sim.isDone() << std::endl; sim.halt(); if (simThread.has_value()) { simThread->request_stop(); @@ -106,7 +119,7 @@ int main(int argc, const char* argv[]) { response["state"] = "stopped"; server.send_message(response); } else if (command == "pause") { - std::cout << "Pausing simulation." << std::endl; + std::cout << getTime() << " Pausing simulation." << std::endl; std::lock_guard guard(simMutex); if (simThread.has_value()) { simThread->request_stop(); @@ -114,14 +127,14 @@ int main(int argc, const char* argv[]) { response["state"] = "halted"; server.send_message(response); } else if (command == "status") { - std::cout << "[Sending] Sending status update " << statusSent + 1 << std::endl; + std::cout << getTime() << " [Sending] Sending status update " << statusSent + 1 << std::endl; std::lock_guard guard(simMutex); json status = sim.getStatus(); server.send_message(status); statusSent++; - std::cout << "[Sending] Status " << statusSent << " update sent!" << std::endl; + std::cout << getTime() << " [Sending] Status " << statusSent << " update sent!" << std::endl; } else { - std::cout << "Unknown command " << command << std::endl; + std::cout << getTime() << " Unknown command " << command << std::endl; std::cerr << "Unknown command " << command << std::endl; response["state"] = "unknown_command"; server.send_message(response); diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index e33fc98e14..0e00691691 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -87,6 +87,7 @@ class IsolatedSimulation : public Simulation for (S_AXIS_Control& s : this->istreams) { j[s.name] = s.getInputReady(); } + readyJson.push_back(j); } void logValid() { @@ -97,6 +98,7 @@ class IsolatedSimulation : public Simulation for (M_AXIS_Control& s : this->ostreams) { j[s.name] = s.getOutputValid(); } + validJson.push_back(j); } SimState simState; @@ -131,10 +133,12 @@ class IsolatedSimulation : public Simulation /** Write logs to disk **/ void commitLogsToDisk(bool clearLogs = true) { - std::ofstream r(readylogName); - std::ofstream v(validlogName); + std::ofstream r(readylogName, std::ios::trunc); + std::ofstream v(validlogName, std::ios::trunc); r << std::setw(4) << readyJson; + std::cout << "Writing ready log: " << readyJson.size() << " elements." << std::endl; v << std::setw(4) << validJson; + std::cout << "Writing valid log: " << validJson.size() << " elements." << std::endl; r.close(); v.close(); if (clearLogs) { @@ -143,7 +147,6 @@ class IsolatedSimulation : public Simulation } } - json getStatus() { return simState.getStatus(); } diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index 04bcf5ee20..b06b56e3b1 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -203,8 +203,9 @@ def _receive_response(self, process_idx: int) -> dict[str, Any] | None: """ sock, _ = self.sockets[process_idx] - # Set 10 second timeout to prevent deadlocks - sock.settimeout(10.0) + # Set 120 second timeout to prevent deadlocks + # Needs to be rather larger to give the simulation IO thread time to answer + sock.settimeout(120.0) # Read 4-byte length prefix length_bytes = sock.recv(4) diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 7246cc1144..79405db940 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -1,8 +1,9 @@ """Simulating layers on their own to observe their behaviour.""" +import io import json import time from concurrent.futures import Future, ThreadPoolExecutor -from pathlib import Path +from pathlib import Path, PosixPath, PurePath from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation from rich.console import Console @@ -15,6 +16,10 @@ from finn.util.logging import DisabledLoggingConsole, log +def get_time() -> str: + return f"[{time.strftime('%H:%M:%S')}]" + + class NodeIsolatedSimulationController(SimulationController): """Run simulations for node isolated cases.""" @@ -35,6 +40,24 @@ def __init__( ) self.console.log("Started simulation controller") + def get_logfile_path(self, binary_or_idx: Path | int) -> Path: + """Get the logfile for the given binary or process index.""" + if type(binary_or_idx) is int: + return ( + self.logdir / f"{binary_or_idx}_log_isolated_" + f"{self.names[binary_or_idx]}_python.txt" + ) + elif type(binary_or_idx) in [Path, PurePath, PosixPath]: # noqa + process_idx = self.binaries.index(binary_or_idx) + return self.logdir / f"{process_idx}_log_isolated_{self.names[process_idx]}_python.txt" + raise TypeError("Pass either a simulation binary path of an index") + + def write_log(self, logfile: io.TextIOWrapper, msg: str, flush: bool = True) -> None: + """Write a timestamped message to log.""" + logfile.write(f"{get_time()} {msg}\n") + if flush: + logfile.flush() + def postprocess_logs( self, d: Path, readylog_name: str = "readylog.txt", validlog_name: str = "validlog.txt" ) -> IsolatedSimLogData: @@ -56,11 +79,17 @@ def run(self) -> dict[str, IsolatedSimLogData]: input ready / output valid data, indexed based on node names.""" futures: list[Future] = [] with self.console.status(f"Running simulation on every node. Log directory: {self.logdir}"): + start = time.time() with ThreadPoolExecutor(len(self.binaries)) as tpe: for binary in self.binaries: futures.append(tpe.submit(self._run_binary, binary)) tpe.shutdown(wait=True) self.console.log("Thread pool closed. Closing sockets and postprocessing data") + elapsed = time.strftime("%Hh %Mm %Ss", time.gmtime(time.time() - start)) + self.console.log(f"Simulations took {elapsed}") + for binary in self.binaries: + with self.get_logfile_path(binary).open("a") as logfile: + self.write_log(logfile, "Cleaning up socket.") self._cleanup_sockets() # Read data @@ -69,60 +98,69 @@ def run(self) -> dict[str, IsolatedSimLogData]: for i, future in enumerate(futures): data[self.names[i]] = future.result() if data[self.names[i]] is None: - invalid.append(self.names[i]) + invalid.append((self.names[i], i)) if len(invalid) > 0: raise FINNInternalError( - f"Lost connection / malformed response from nodes: {', '.join(invalid)}" + f"Lost connection / malformed response from nodes: " + f"{', '.join([str(x) for x in invalid])}" ) return data def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: """Run simulation. Returning None if connection is lost.""" process_index = self.binaries.index(binary) - with ( - self.logdir / f"{process_index}_log_isolated_{self.names[process_index]}_python.txt" - ).open("w+") as logfile: + with self.get_logfile_path(binary).open("w+") as logfile: + + def write_log(msg: str) -> None: + self.write_log(logfile, msg) + # Initialize - logfile.write("Initializing simulation.\n") + write_log("Initializing simulation") proc_idx = self._start_process(binary, process_index) response = self._send_and_receive(proc_idx, "start", {}) if response is None: - logfile.write("Client disconnected / No answer received to start command!\n") + write_log("Client disconnected / no answer received to start command!") return None - logfile.write(f"Start response: {response}\n") + write_log(f"Start response: {response}") if response is None: - logfile.write("Failed to start simulation: No response\n") + write_log("Failed to start simulation: No response") return None # Main loop - logfile.write("Beginning main loop\n") + write_log("Beginning main loop") logfile.flush() - + total_status_requests = 0 while True: + # Request status in regular intervals time.sleep(self.poll_interval) - logfile.write("Sending status request\n") + write_log("Sending status request") response = self._send_and_receive(proc_idx, "status", {}) + total_status_requests += 1 + write_log(f"Status request {total_status_requests} sent.") + + # Process response if response is None: return None state = response["state"] + write_log(f"Received answer for status request ({total_status_requests})") if state == "done": self.console.log(f"{process_index} is done and postprocessing data.") - logfile.write("Received done status. Sending stop signal\n") + write_log("Received done status. Sending stop signal to simulation.") resp = self._send_and_receive(proc_idx, "stop", {}) if resp is None: - logfile.write("No stop response received.\n") + write_log("No stop response received.") else: - logfile.write("Stop successfully received: " + str(resp)) + write_log("Stop successfully received.") return self.postprocess_logs(binary.parent) # TODO: Order seems wrong - logfile.write( + write_log( f"{response['totalCycles']}, " f"{response['inputCyclesDone']}, " f"{response['inputCyclesTarget']}, " f"{response['outputCyclesDone']}, " - f"{response['outputCyclesTarget']}\n" + f"{response['outputCyclesTarget']}" ) @@ -283,13 +321,13 @@ def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: f"Simulation log data of layer {layer} is missing the READY log." ) # 1. All cycle datas are uniform and have at least one stream signal - for layer, ldata in data.items(): + for i, (layer, ldata) in enumerate(data.items()): cycle_data = ldata["ready"] + ldata["valid"] lengths: set[int] = {len(cycle.keys()) for cycle in cycle_data} if len(lengths) != 1: raise FINNInternalError( f"Simulation log data inconsistent for layer " - f"{layer}. Differing number of fields per cycle." + f"{layer} ({i}). Differing number of fields per cycle." ) if next(iter(lengths)) < 4: raise FINNInternalError( From 68f871cd7370bf3a039fc2118d60b9d2782f7da4 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:57:42 +0100 Subject: [PATCH 058/170] Working version --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 32 +++++++-------- .../InterprocessCommunicationChannel.hpp | 36 ++++++++++++++-- finn_xsi/finn_xsi/include/Simulation.hpp | 15 ++++--- .../fpgadataflow/simulation_connected.py | 41 +++++++++++++++++++ 4 files changed, 96 insertions(+), 28 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 7b3a620ac9..d4c902665b 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -55,6 +55,22 @@ class SimulationController { current_samples = 0; max_cycles = maxCycles; state = SimulationState::CONFIGURED; + + // Reset simulation first + sim.reset(); + + // Configure FIFO depths AFTER reset + std::size_t num_fifos = sim.getFIFOCount(); + + if (fifo_depths.empty()) { + throw std::runtime_error("FIFO depths not configured"); + } + + // Apply depths: if list is shorter, use last value for remaining FIFOs + for (std::size_t i = 0; i < num_fifos; ++i) { + std::size_t depth_idx = std::min(i, fifo_depths.size() - 1); + sim.setFIFODepth(i, fifo_depths[depth_idx]); + } } void start() { @@ -68,22 +84,6 @@ class SimulationController { // Start simulation in a separate thread sim_thread = std::jthread([this](std::stop_token stoken) { try { - // Reset simulation first - sim.reset(); - - // Configure FIFO depths AFTER reset - std::size_t num_fifos = sim.getFIFOCount(); - - if (fifo_depths.empty()) { - throw std::runtime_error("FIFO depths not configured"); - } - - // Apply depths: if list is shorter, use last value for remaining FIFOs - for (std::size_t i = 0; i < num_fifos; ++i) { - std::size_t depth_idx = std::min(i, fifo_depths.size() - 1); - sim.setFIFODepth(i, fifo_depths[depth_idx]); - } - std::cout << "Starting simulation with max cycles: " << max_cycles << std::endl; // Run the simulation diff --git a/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp b/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp index 59bc4cb140..adaaa96f90 100644 --- a/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp +++ b/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp @@ -21,6 +21,8 @@ namespace bip = boost::interprocess; template concept Sender = IsSender; +constexpr int MAX_SPIN_WAIT = 100; + template class InterprocessCommunicationChannel { private: @@ -88,6 +90,20 @@ class InterprocessCommunicationChannel { // Construct the channel data in shared memory channel = shmem.find_or_construct("ChannelData")(); + + // Perform handshake to verify communication works + if constexpr (IsSender) { + // Sender: send test request and wait for response + Request test_request{}; + Response test_response = send_request(test_request); + // Communication verified if we got here without hanging + } else { + // Receiver: wait for test request and send response + Request test_request = receive_request(); + Response test_response{}; + send_response(test_response); + // Communication verified if we got here + } } // Delete copy operations @@ -150,12 +166,18 @@ class InterprocessCommunicationChannel { // Wait for response in corresponding slot int read_slot = channel->response_read_idx.load(std::memory_order_acquire) % 2; + int spin_count = 0; while (!channel->responses[read_slot].valid.load(std::memory_order_acquire) && !stoken.stop_requested()) { + if (spin_count++ >= MAX_SPIN_WAIT) { + std::this_thread::yield(); + spin_count = 0; + } else { #if defined(__x86_64__) || defined(_M_X64) - __builtin_ia32_pause(); + __builtin_ia32_pause(); #elif defined(__aarch64__) - asm volatile("yield" ::: "memory"); + asm volatile("yield" ::: "memory"); #endif + } } if (stoken.stop_requested()) { @@ -174,13 +196,19 @@ class InterprocessCommunicationChannel { requires(!Sender) { int read_slot = channel->request_read_idx.load(std::memory_order_acquire) % 2; + int spin_count = 0; while (!channel->requests[read_slot].valid.load(std::memory_order_acquire) && !stoken.stop_requested()) { + if (spin_count++ >= MAX_SPIN_WAIT) { + std::this_thread::yield(); + spin_count = 0; + } else { #if defined(__x86_64__) || defined(_M_X64) - __builtin_ia32_pause(); + __builtin_ia32_pause(); #elif defined(__aarch64__) - asm volatile("yield" ::: "memory"); + asm volatile("yield" ::: "memory"); #endif + } } if (stoken.stop_requested()) { diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index b0467be459..e51d2d7751 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -206,14 +206,6 @@ class SingleNodeSimulation : public Simulation None: + """Remove any existing shared memory segments and semaphores from /dev/shm.""" + try: + # Collect potential shared memory and semaphore names based on node names + shm_patterns = [] + # Pattern for shared memory segments (e.g., /nodename_0, /nodename_1) + shm_patterns.append("/dev/shm/*") + + removed_count = 0 + for pattern in shm_patterns: + for filepath in glob.glob(pattern): + try: + os.unlink(filepath) + removed_count += 1 + except (FileNotFoundError, PermissionError): + # File might already be removed or we don't have permission + pass + + if removed_count > 0: + self.console.log(f"Cleaned up {removed_count} existing shared memory resources") + except Exception as e: + # Don't fail if cleanup fails - just log it + self.console.log(f"Warning: Error during shared memory cleanup: {e}") + def run( self, depth: list[list[int]] | None = None, @@ -68,6 +97,12 @@ def run( timeout_result = False fifo_depths: dict[str, list[int]] = {} + # Clean up any existing shared memory resources before starting + self._cleanup_shm_resources() + + # Initialize barrier for all simulations to synchronize after configuration + self.sync_barrier = Barrier(len(self.names)) + if self.progress is not None: self.progress.start() try: @@ -250,6 +285,12 @@ def _print(msg: str, color: str = "green") -> None: _print(f"Configuration failed: {error_msg}", "red") return None + # Wait for all simulations to complete configuration before starting + _print("Waiting for all simulations to complete configuration...") + if self.sync_barrier is not None: + self.sync_barrier.wait() + _print("All simulations configured, starting...") + # Start the simulation response = self._send_and_receive(proc_idx, "start", {}) From d2820799733f2beb6eba2b34d5ccf82f4ed11956 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 4 Feb 2026 16:45:36 +0100 Subject: [PATCH 059/170] Integrated steps into FINN itself --- .gitignore | 1 + src/finn/builder/build_dataflow_steps.py | 57 +++++++++++++++++++ .../fpgadataflow/simulation_build.py | 3 + .../fpgadataflow/simulation_isolated.py | 8 ++- 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c3e086d43a..6576bb08e7 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ finn_xsi/finn_xsi/rtlsim_config.hpp # downloaded dep repos /deps/ +finn_deps/ # local test directories for benchmarking infrastructure bench_input diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 74a31f64d0..050e7bea58 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -638,6 +638,59 @@ def step_hw_ipgen(model: ModelWrapper, cfg: DataflowBuildConfig): return model + + + + +# TODO: Both this and the step_size_... steps will be reworked before merging into dev +def step_build_simulation(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Build the simulation binaries for isolated and connected simulations.""" + from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType + model = model.transform( + BuildSimulation( + cfg._resolve_fpga_part(), # noqa + cfg._resolve_hls_clk_period(), # noqa + cfg.functional_simulation, + SimulationType.NODE_BASED_CONNECTED + ) + ) + return model + +def step_size_fifo_isolated(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Simulate layers in isolation and use the observed behaviour to size the FIFOs accordingly.""" + from finn.transformation.fpgadataflow.simulation_isolated import RunLayerIsolatedSimulation + from pathlib import Path + model = model.transform( + RunLayerIsolatedSimulation( + cfg._resolve_fpga_part(), # noqa + cfg._resolve_hls_clk_period(), # noqa + cfg.functional_simulation, + Path(cfg.output_dir) + ) + ) + return model + +def step_size_fifo_connected(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Simulate layers connected and use the observed behaviour to size the FIFOs accordingly.""" + from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation + model = model.transform( + RunLayerParallelSimulation( + cfg._resolve_fpga_part(), # noqa + cfg._resolve_hls_clk_period(), # noqa + cfg + ) + ) + return model + +def step_apply_fifosizes(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Apply the previously found FIFO sizes to the model.""" + from finn.transformation.fpgadataflow.simulation import ApplyFIFOSizes + return model.transform(ApplyFIFOSizes(cfg)) + + + + + def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig): """ Depending on the auto_fifo_depths setting, do one of the following: @@ -1150,6 +1203,10 @@ def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig): "step_generate_estimate_reports": step_generate_estimate_reports, "step_hw_codegen": step_hw_codegen, "step_hw_ipgen": step_hw_ipgen, + "step_build_simulation": step_build_simulation, + "step_size_fifo_isolated": step_size_fifo_isolated, + "step_size_fifo_connected": step_size_fifo_connected, + "step_apply_fifosizes": step_apply_fifosizes, "step_set_fifo_depths": step_set_fifo_depths, "step_create_stitched_ip": step_create_stitched_ip, "step_measure_rtlsim_performance": step_measure_rtlsim_performance, diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 1feea49ba9..a8d3d31376 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -36,6 +36,9 @@ from collections.abc import Sequence +# TODO: Fix that BuildSimulation has to return binaries for either SimulationType +# TODO: Just store the directory instead - since we build all targets anyways + class SimulationType(str, Enum): """Type of simulation.""" diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 79405db940..9f34782367 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -201,12 +201,13 @@ class RunLayerIsolatedSimulation(Transformation): """Run a layer isolated simulation and calculate some information for a later layer parallel simulation.""" - def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool) -> None: + def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool, output_dir: Path) -> None: """Run isolated layer simulations.""" super().__init__() self.fpgapart = fpgapart self.clk_ns = clk_ns self.functional_sim = functional_sim + self.output_dir = output_dir def calculate_upper_bounds(self, data: IsoSimLogDataByLayer) -> dict[str, dict[str, int]]: """Try to calculate an upper bound for the incoming FIFO size of the layers. @@ -425,6 +426,11 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: ) log.info("Upper bounds: \n" + formatted_upper_bounds) + # Write into report file + upper_bounds_file = self.output_dir / "report" / "estimate_upper_fifo_bound.json" + upper_bounds_file.write_text(json.dumps(in_fifo_upper_bound)) + log.info(f"Wrote results to: {upper_bounds_file}") + raise NotImplementedError() # TODO: Integrate data into the layer parallel simulation return model, False From 915a7ce137242e73c226d21d6ee3e75a3e8e1cc5 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 5 Feb 2026 09:52:39 +0100 Subject: [PATCH 060/170] Simulations utilize the correct binaries --- .../transformation/fpgadataflow/simulation.py | 19 +++++++++++++++++++ .../fpgadataflow/simulation_isolated.py | 1 + 2 files changed, 20 insertions(+) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 759e77d260..398e5c4a44 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -69,6 +69,25 @@ def __init__( BuildSimulation(fpgapart, clk_ns, functional_sim, simulation_type, workers) ) self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} + self.correct_binaries_paths() + + + def correct_binaries_paths(self) -> None: + """Since we use the same directory for isolated and connected simulation, the binaries might + point to a simulation executable of the other variant. This function modifies them to point + to the correct one. + TODO: Rework this mechanism. + """ + binary_names = { + SimulationType.NODE_BASED_CONNECTED: "LayerSimulationBackend", + SimulationType.NODE_BASED_ISOLATED: "IsolatedSimulationBackend" + } + if self.simulation_type == SimulationType.COMPLETE_DESIGN: + raise FINNUserError("Unsupported SimulationType: COMPLETE_DESIGN") + updated = {} + for i, binary in self.binaries.items(): + updated[i] = binary.parent / binary_names[self.simulation_type] + self.binaries = updated def simulate(self) -> Any: raise NotImplementedError("Call simulate() on subclasses.") diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 9f34782367..45a3af4bbb 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -116,6 +116,7 @@ def write_log(msg: str) -> None: # Initialize write_log("Initializing simulation") + write_log(f"Binary is: {binary}") proc_idx = self._start_process(binary, process_index) response = self._send_and_receive(proc_idx, "start", {}) if response is None: From bf7805f3ac49e18f0f3d469340a6b3eecee9b732 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 6 Feb 2026 15:39:18 +0100 Subject: [PATCH 061/170] BuildSimulation independent of simtype, smaller fixes and additions --- src/finn/builder/build_dataflow_steps.py | 1 - .../transformation/fpgadataflow/simulation.py | 36 ++++---- .../fpgadataflow/simulation_build.py | 87 +++++++++---------- .../fpgadataflow/simulation_connected.py | 4 + .../fpgadataflow/simulation_isolated.py | 3 +- 5 files changed, 63 insertions(+), 68 deletions(-) diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 050e7bea58..25f06a41e6 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -651,7 +651,6 @@ def step_build_simulation(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mode cfg._resolve_fpga_part(), # noqa cfg._resolve_hls_clk_period(), # noqa cfg.functional_simulation, - SimulationType.NODE_BASED_CONNECTED ) ) return model diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 398e5c4a44..42bf23c453 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -69,25 +69,27 @@ def __init__( BuildSimulation(fpgapart, clk_ns, functional_sim, simulation_type, workers) ) self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} - self.correct_binaries_paths() + match simulation_type: + case SimulationType.NODE_BASED_CONNECTED: + self.binaries = { + i: self.binaries[i] / "LayerSimulationBackend" + for i in self.binaries.keys() + } + case SimulationType.NODE_BASED_ISOLATED: + self.binaries = { + i: self.binaries[i] / "IsolatedSimulationBackend" + for i in self.binaries.keys() + } + case _: + raise FINNInternalError(f"Unsupported simulation type: {simulation_type}") + errors = [] + for binary in self.binaries.values(): + if not binary.exists(): + errors.append(f"Binary {binary} does not exist! Please rerun BuildSimulation!") + if len(errors) > 0: + raise FINNInternalError("Errors occurred: \n" + "\n\t".join(errors)) - def correct_binaries_paths(self) -> None: - """Since we use the same directory for isolated and connected simulation, the binaries might - point to a simulation executable of the other variant. This function modifies them to point - to the correct one. - TODO: Rework this mechanism. - """ - binary_names = { - SimulationType.NODE_BASED_CONNECTED: "LayerSimulationBackend", - SimulationType.NODE_BASED_ISOLATED: "IsolatedSimulationBackend" - } - if self.simulation_type == SimulationType.COMPLETE_DESIGN: - raise FINNUserError("Unsupported SimulationType: COMPLETE_DESIGN") - updated = {} - for i, binary in self.binaries.items(): - updated[i] = binary.parent / binary_names[self.simulation_type] - self.binaries = updated def simulate(self) -> Any: raise NotImplementedError("Call simulate() on subclasses.") diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index a8d3d31376..93e23f70e4 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -273,7 +273,7 @@ def _create_sim_so( return Path(sim_base), Path(sim_rel) def _compile_simulation( - self, sim_base: Path, sim_type: SimulationType, silent: bool = False + self, sim_base: Path, silent: bool = False ) -> Path: """Compile an existing RTLSIM directory. Requires _create_sim_so to be run before. Expects rtlsim_config.hpp to be templated already. @@ -282,19 +282,11 @@ def _compile_simulation( Path: Path to the executable shell script to run the binary """ # Determine executable name - execname = "" - match sim_type: - case SimulationType.NODE_BASED_CONNECTED: - execname = "LayerSimulationBackend" - case SimulationType.NODE_BASED_ISOLATED: - execname = "IsolatedSimulationBackend" - case _: - raise FINNInternalError(f"Unknown simulation type: {sim_type}") - simulation_executable = Path(sim_base) / execname - if simulation_executable.exists(): + compile_targets = ["LayerSimulationBackend", "IsolatedSimulationBackend"] + if all((Path(sim_base) / execname).exists() for execname in compile_targets): # Simulation was already compiled, we can return early self.progress_bar.update("Make") - return simulation_executable + return Path(sim_base) # Check where FINNXSI is finnxsi_dir = os.environ["FINN_XSI"] @@ -329,10 +321,16 @@ def _compile_simulation( except CalledProcessError as e: raise FINNInternalError(f"Failed to create executable in {sim_base}!") from e - if not simulation_executable.exists(): - raise FINNInternalError(f"Make call in {sim_base} failed!") + errors = [] + for target in compile_targets: + simulation_executable = Path(sim_base) / target + if not simulation_executable.exists(): + errors.append(f"Simulation compile target {target} was not created. " + f"Check {sim_base} to run make manually.") + if len(errors) > 0: + raise FINNInternalError("Error compiling simulations: \n" + "\n\t".join(errors)) self.progress_bar.update("Make") - return simulation_executable + return sim_base def _template_rtlsim_config( self, @@ -400,7 +398,6 @@ def build_single_node_simulation( total_nodes: int, previous_node_name: str | None, build_dir: Path | None, - sim_type: SimulationType, timeout_cycles: int = 0, silent: bool = False, ) -> Path: @@ -422,7 +419,6 @@ def build_single_node_simulation( this node and the previous one. build_dir: If given, use this directory for building the simulation. Otherwise one is created from the nodes name. - sim_type: Simulation Type - determines the name of the executable that will be built timeout_cycles: Number of cycles until simulation timeout. When set to 0 (default), no timeout is given. silent: If True, silences the Cmake and make output (including stderr) @@ -483,7 +479,7 @@ def build_single_node_simulation( ) # Building the whole simulation - return self._compile_simulation(sim_base, sim_type=sim_type, silent=silent).absolute() + return self._compile_simulation(sim_base, silent=silent).absolute() def _get_randomized_names(self, model: ModelWrapper, suffix_length: int = 5) -> dict[int, str]: """Add a randomized suffix to every name in the model. Used to avoid interference with @@ -499,7 +495,7 @@ def _get_randomized_names(self, model: ModelWrapper, suffix_length: int = 5) -> } def _build_simulations_parallel( - self, workers: int, with_live_display: bool, functional_sim: bool, sim_type: SimulationType + self, workers: int, with_live_display: bool, functional_sim: bool ) -> dict[int, Path]: """Build all nodes in the model in parallel, as isolated simulations, ready for usage in an IPC connected simulation chain. @@ -515,7 +511,7 @@ def _build_simulations_parallel( indexed by the node-index. These are in their respective FINN_TMP directories. """ - + log.info(f"Building simulation binaries for {len(self.model.graph.node)} layers.") def _build( node_name: str, node_index: int, @@ -537,7 +533,6 @@ def _build( total_nodes, prev_node_name, build_dir, - sim_type, silent=with_live_display, ) @@ -576,10 +571,10 @@ def _build( return {i: future.result() for i, future in futures.items()} def build_simulation( - self, simtype: SimulationType, workers: int, with_live_display: bool, functional_sim: bool + self, workers: int, with_live_display: bool, functional_sim: bool ) -> dict[int, Path]: - """Build a simulation of the given type, return the resulting executable (indexed by the - corresponding node index in the graph). + """Build a simulation of the given type, return the path to the executable directory + (indexed by the corresponding node index in the graph). Args: simtype: Simulation type to build. @@ -588,23 +583,19 @@ def build_simulation( with_live_display: If True, display a live progress-bar. functional_sim: If True, use functional simulation (faster but takes some time to build) """ - match simtype: - case SimulationType.NODE_BASED_CONNECTED | SimulationType.NODE_BASED_ISOLATED: - node_count = len(self.model.graph.node) - self.progress_bar = ThreadsafeProgressDisplay( - ["StitchedIP", "CMake", "Make"], - [node_count] * 3, - [ - "[bold blue](1)[/bold blue] Creating stitched IPs", - "[bold blue](2)[/bold blue] Configuring project with CMake", - "[bold blue](3)[/bold blue] Building simulation binaries", - ], - ) - return self._build_simulations_parallel( - workers, with_live_display, functional_sim, simtype - ) - case SimulationType.COMPLETE_DESIGN: - raise FINNUserError(f"Simulation method {simtype} is deprecated!") + node_count = len(self.model.graph.node) + self.progress_bar = ThreadsafeProgressDisplay( + ["StitchedIP", "CMake", "Make"], + [node_count] * 3, + [ + "[bold blue](1)[/bold blue] Creating stitched IPs", + "[bold blue](2)[/bold blue] Configuring project with CMake", + "[bold blue](3)[/bold blue] Building simulation binaries", + ], + ) + return self._build_simulations_parallel( + workers, with_live_display, functional_sim + ) class BuildSimulation(Transformation): @@ -617,7 +608,6 @@ def __init__( fpgapart: str, clk_ns: float, functional_sim: bool, - simulation_type: SimulationType, workers: int | None = None, ) -> None: """Create a new BuildSimulation transform.""" @@ -625,7 +615,6 @@ def __init__( self.functional_sim = functional_sim self.fpgapart = fpgapart self.clk_ns = clk_ns - self.sim_type = simulation_type def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Build / compile the model. Modifies the model.""" @@ -656,7 +645,6 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # sys.stdout = sys.stdout.console # type: ignore # sys.stderr = sys.stderr.console # type: ignore self.binaries = self.builder.build_simulation( - self.sim_type, self.workers, with_live_display=True, functional_sim=self.functional_sim, @@ -670,17 +658,20 @@ def _compile(binary: Path, progress: ThreadsafeProgressDisplay) -> None: result = subprocess.run( "cmake .;make", shell=True, - cwd=str(binary.parent), + cwd=str(binary), text=True, capture_output=True, ) if result.returncode != 0: - raise FINNUserError(f"Failed compilation in {binary.parent}: {result.stderr}") + raise FINNUserError(f"Failed compilation in {binary}: {result.stderr}") progress.update("Compilation") sim_binaries = [Path(p) for p in sim_binaries] - sys.stdout = sys.stdout.console # type: ignore - sys.stderr = sys.stderr.console # type: ignore + try: + sys.stdout = sys.stdout.console # type: ignore + sys.stderr = sys.stderr.console # type: ignore + except AttributeError: + pass with DisabledLoggingConsole() as cons: # noqa progress = ThreadsafeProgressDisplay( ["Compilation"], [len(sim_binaries)], ["Compilation"] diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index bd0886e2eb..d188225b4e 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -510,7 +510,10 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Minimize FIFO depths using binary search over BRAM block counts for i in range(len(fifo_depths)): for j in range(len(fifo_depths[i])): + log.info(f"Minimizing Layer {i} / {len(fifo_depths)} " + f"(FIFO {j} / {len(fifo_depths[i])})") if not needs_minimization[i][j]: + log.info("Skipping minimization for this stream.") continue minimized_depth = self._minimize_fifo_depth( @@ -643,6 +646,7 @@ def _minimize_fifo_depth( bw = bit_widths[node_idx][fifo_idx] print(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") + log.info(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs success, timeout = self._test_depth( diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 45a3af4bbb..cafae7e911 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -429,9 +429,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Write into report file upper_bounds_file = self.output_dir / "report" / "estimate_upper_fifo_bound.json" - upper_bounds_file.write_text(json.dumps(in_fifo_upper_bound)) + upper_bounds_file.write_text(json.dumps(in_fifo_upper_bound, indent=4)) log.info(f"Wrote results to: {upper_bounds_file}") - raise NotImplementedError() # TODO: Integrate data into the layer parallel simulation return model, False From 8a725f1d0acd2222867664b5fcf42640686754b4 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 9 Feb 2026 11:07:02 +0100 Subject: [PATCH 062/170] Start adding resnet support --- .../builder/custom_step_library/resnet.py | 228 ++++++++-------- src/finn/custom_op/fpgadataflow/memstream.py | 9 +- .../fpgadataflow/simulation_build.py | 243 ++++++++++++------ .../qonnx/infer_quant_avg_pool_2d.py | 207 ++++++++++++++- 4 files changed, 477 insertions(+), 210 deletions(-) diff --git a/src/finn/builder/custom_step_library/resnet.py b/src/finn/builder/custom_step_library/resnet.py index 3e1c61063b..dc546996d1 100644 --- a/src/finn/builder/custom_step_library/resnet.py +++ b/src/finn/builder/custom_step_library/resnet.py @@ -1,3 +1,4 @@ + # Copyright (C) 2020-2022, Xilinx, Inc. # Copyright (C) 2022-2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -27,156 +28,148 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from qonnx.core.datatype import DataType +"""Custom build steps for ResNet model processing. + +This module provides specialized transformation steps for converting quantized +ResNet models from QONNX format through various stages of optimization and +hardware conversion. +""" + +from finn.transformation.qonnx.fold_quant_weights import FoldQuantWeights +from finn.transformation.qonnx.infer_quant_avg_pool_2d import ( + AvgPoolAndTruncv2ToQuantAvgPool, +) +from finn.transformation.qonnx.quant_act_to_multithreshold import ( + ConvertQuantActToMultiThreshold, + default_filter_function_generator, +) +from finn.transformation.streamline.streamline_plus import StreamlinePlus as Streamline +from finn.transformation.streamline.remove import RemoveIdentityReshape, RemoveIdentityTranspose from qonnx.core.modelwrapper import ModelWrapper -from qonnx.transformation.batchnorm_to_affine import BatchNormToAffine from qonnx.transformation.composed import ComposedTransformation from qonnx.transformation.double_to_single_float import DoubleToSingleFloat from qonnx.transformation.fold_constants import FoldConstants +from qonnx.transformation.extract_conv_bias import ExtractBiasFromConv +from qonnx.transformation.gemm_to_matmul import GemmToMatMul +from qonnx.transformation.infer_data_layouts import InferDataLayouts +from qonnx.transformation.infer_datatypes import InferDataTypes +from qonnx.transformation.quant_constant_folding import FoldTransposeIntoQuantInit +from qonnx.transformation.remove import RemoveIdentityOps from qonnx.transformation.general import ( - ConvertDivToMul, - ConvertSubToAdd, GiveReadableTensorNames, GiveUniqueNodeNames, GiveUniqueParameterTensors, - RemoveStaticGraphInputs, RemoveUnusedTensors, SortGraph, ) -from qonnx.transformation.infer_data_layouts import InferDataLayouts -from qonnx.transformation.infer_datatypes import InferDataTypes from qonnx.transformation.infer_shapes import InferShapes -from qonnx.transformation.insert_topk import InsertTopK from qonnx.transformation.lower_convs_to_matmul import LowerConvsToMatMul -from qonnx.transformation.remove import RemoveIdentityOps +from qonnx.util.cleanup import cleanup_model import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw +from finn.transformation.fpgadataflow.replicate_stream import InferReplicateStream from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.transformation.move_reshape import RemoveCNVtoFCFlatten from finn.transformation.streamline.absorb import ( - Absorb1BitMulIntoConv, - Absorb1BitMulIntoMatMul, AbsorbAddIntoMultiThreshold, - AbsorbConsecutiveTransposes, - AbsorbMulIntoMultiThreshold, - AbsorbScalarMulAddIntoTopK, + AbsorbSignBiasIntoMultiThreshold, AbsorbTransposeIntoMultiThreshold, - FactorOutMulSignMagnitude, -) -from finn.transformation.streamline.collapse_repeated import ( - CollapseRepeatedAdd, - CollapseRepeatedMul, ) # just for not linear from finn.transformation.streamline.reorder import ( - MoveAddPastConv, - MoveAddPastMul, - MoveLinearPastEltwiseAdd, - MoveLinearPastFork, - MoveMaxPoolPastMultiThreshold, - MoveScalarAddPastMatMul, - MoveScalarLinearPastInvariants, - MoveScalarMulPastConv, - MoveScalarMulPastMatMul, - MoveTransposePastEltwise, - MoveTransposePastFork, - MoveTransposePastJoinAdd, + MoveMulPastAdd, ) -from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds -from finn.transformation.streamline.sign_to_thres import ConvertSignToThres - - -def step_resnet50_tidy(model: ModelWrapper, cfg: DataflowBuildConfig): - model = model.transform(GiveUniqueParameterTensors()) - model = model.transform(InferShapes()) - model = model.transform(FoldConstants()) - model = model.transform(RemoveStaticGraphInputs()) - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) - model = model.transform(InferDataTypes()) - model = model.transform(InsertTopK()) - model = model.transform(InferShapes()) - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) - model = model.transform(InferDataTypes()) - return model - - -def step_resnet50_streamline_linear(model: ModelWrapper, cfg: DataflowBuildConfig): - streamline_transformations = [ - AbsorbScalarMulAddIntoTopK(), # before MoveAddPastMul to avoid int->float - ConvertSubToAdd(), - ConvertDivToMul(), - RemoveIdentityOps(), - CollapseRepeatedMul(), - BatchNormToAffine(), - ConvertSignToThres(), - MoveAddPastMul(), - MoveScalarAddPastMatMul(), - MoveAddPastConv(), - MoveScalarMulPastMatMul(), - MoveScalarMulPastConv(), - MoveScalarLinearPastInvariants(), - MoveAddPastMul(), - CollapseRepeatedAdd(), - CollapseRepeatedMul(), - AbsorbAddIntoMultiThreshold(), - FactorOutMulSignMagnitude(), - MoveMaxPoolPastMultiThreshold(), - AbsorbMulIntoMultiThreshold(), - Absorb1BitMulIntoMatMul(), - Absorb1BitMulIntoConv(), - RoundAndClipThresholds(), - ] - for trn in streamline_transformations: - model = model.transform(trn) - model = model.transform(GiveUniqueNodeNames()) - return model -def step_resnet50_streamline_nonlinear(model: ModelWrapper, cfg: DataflowBuildConfig): - streamline_transformations = [ - MoveLinearPastEltwiseAdd(), - MoveLinearPastFork(), - ] - for trn in streamline_transformations: - model = model.transform(trn) - model = model.transform(GiveUniqueNodeNames()) +def step_resnet_tidy(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: # noqa: ARG001 + """Tidy up ResNet models.""" + model = model.transform( + ComposedTransformation( + [ + # Adds shape and datatype annotations to all tensors in this graph + InferDataTypes(), + InferShapes(), + # Cleanup the graph by removing redundant, unnecessary and constant + # nodes and tensors and give unique names to everything remaining + GiveUniqueNodeNames(), + GiveReadableTensorNames(), + RemoveUnusedTensors(), + GiveUniqueParameterTensors(), + FoldConstants(), + # Remove unnecessary shape and layout transformations + RemoveIdentityReshape(), + RemoveIdentityTranspose(), + # Redo shape and datatype annotations after removing nodes and + # tensors + InferShapes(), + InferDataTypes(), + ] + ) + ) return model -def step_resnet50_streamline(model: ModelWrapper, cfg: DataflowBuildConfig): - for iter_id in range(4): - model = step_resnet50_streamline_linear(model, cfg) - model = step_resnet50_streamline_nonlinear(model, cfg) - - # big loop tidy up - model = model.transform(RemoveUnusedTensors()) - model = model.transform(GiveReadableTensorNames()) - model = model.transform(InferDataTypes()) - model = model.transform(SortGraph()) +# Temporary step function to replace ConvertQONNXtoFINN class, because qonnx version is to old to +# handle avgpool version parameter +def step_temp_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: # noqa: ARG001 + """Convert QONNX dialect to FINN ONNX dialect.""" + model = cleanup_model(model) - model = model.transform(DoubleToSingleFloat()) + model = model.transform(ExtractBiasFromConv()) + # Gemm operations are not supported by FINN, so we convert them to MatMul + model = model.transform(GemmToMatMul()) + model = model.transform(FoldTransposeIntoQuantInit()) + # Make sure the datatypes exist, these are required for folding the weights + model = model.transform(InferDataTypes()) + # Fold weights + model = model.transform(FoldQuantWeights()) + # Convert activations - # Lower convolutions and streamline resulting transposes - model = model.transform(LowerConvsToMatMul()) + # Perform layout inference so that QuantActBaseHandler can set data_layout + # attribute of MT for use in later layout inference and NCHW->NHWC conversion + # in the InferThresholding transformation. + model = model.transform(InferDataLayouts()) + model = model.transform(InferShapes()) model = model.transform( - ComposedTransformation( - [ - MoveTransposePastJoinAdd(), - MoveTransposePastFork(), - MoveTransposePastEltwise(), - AbsorbConsecutiveTransposes(), - AbsorbTransposeIntoMultiThreshold(), - ] + ConvertQuantActToMultiThreshold( + filter_function=default_filter_function_generator(max_multithreshold_bit_width=8), ) ) + # Recompute datatypes + model = model.transform(InferDataTypes()) + model = model.transform(InferDataLayouts()) + model = model.transform(InferShapes()) + # Convert AvgPool -> Mul -> Trunc structure to QuantAvgPool2d + model = model.transform(AvgPoolAndTruncv2ToQuantAvgPool()) + # Remove empty padding if it exists + model = model.transform(RemoveIdentityOps()) + return model + +def step_resnet_streamline(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: # noqa: ARG001 + """Streamline ResNet models.""" + transform = ComposedTransformation([ + MoveMulPastAdd(), + AbsorbSignBiasIntoMultiThreshold(), + ]) + model = model.transform(transform) + model = model.transform(Streamline()) + transform2 = ComposedTransformation([ + LowerConvsToMatMul(), + AbsorbAddIntoMultiThreshold(), + AbsorbTransposeIntoMultiThreshold() + ]) + model = model.transform(transform2) + model = model.transform(Streamline()) + #model = model.transform(InsertTopK()) + #model = model.transform(AbsorbScalarMulAddIntoTopK()) + return model -def step_resnet50_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig): - model.set_tensor_datatype(model.graph.input[0].name, DataType["UINT8"]) +def step_resnet_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: # noqa: ARG001 + """Convert ResNet models to hardware-specific operations.""" + # Convert Squeeze and Unsqueeze operators to hardware operations model = model.transform(InferDataLayouts()) model = model.transform(DoubleToSingleFloat()) model = model.transform(InferDataTypes()) @@ -184,15 +177,9 @@ def step_resnet50_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig): to_hw_transformations = [ to_hw.InferChannelwiseLinearLayer, - to_hw.InferPool, - AbsorbConsecutiveTransposes, - RoundAndClipThresholds, - to_hw.InferQuantizedMatrixVectorActivation, - to_hw.InferThresholdingLayer, - to_hw.InferConvInpGen, - to_hw.InferDuplicateStreamsLayer, - to_hw.InferAddStreamsLayer, + InferReplicateStream, to_hw.InferLabelSelectLayer, + to_hw.InferElementwiseBinaryOperation ] for trn in to_hw_transformations: model = model.transform(trn()) @@ -204,5 +191,4 @@ def step_resnet50_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig): model = model.transform(GiveReadableTensorNames()) model = model.transform(RemoveUnusedTensors()) model = model.transform(SortGraph()) - return model diff --git a/src/finn/custom_op/fpgadataflow/memstream.py b/src/finn/custom_op/fpgadataflow/memstream.py index 9fead1a0e7..3e420f04f5 100644 --- a/src/finn/custom_op/fpgadataflow/memstream.py +++ b/src/finn/custom_op/fpgadataflow/memstream.py @@ -1,7 +1,6 @@ """Support for memory stream operations in FPGA dataflow.""" import os -from abc import ABC, abstractmethod from pathlib import Path from typing import cast @@ -9,12 +8,14 @@ from finn.util.basic import is_versal -class MemStreamSupport(HWCustomOp, ABC): +class MemStreamSupport(HWCustomOp): """Custom Op for memory stream operations in FPGA dataflow.""" - @abstractmethod def calc_tmem(self) -> int: - """Abstract method to calculate threshold memory size.""" + """Abstract method to calculate threshold memory size. + The default implementation raises NotImplementedError because + some subclasses dont implement calc_tmem.""" + raise NotImplementedError() def calc_wmem(self) -> int: """Abstract method to calculate weight memory size. diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 1feea49ba9..d798201547 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -1,4 +1,5 @@ """Build FINN Simulations.""" + import finn_xsi.adapter as finnxsi import numpy as np import onnx @@ -11,8 +12,9 @@ from contextlib import nullcontext from copy import deepcopy from enum import Enum -from onnx import NodeProto, TensorProto +from onnx import NodeProto, TensorProto, ValueInfoProto from pathlib import Path +from qonnx.util.basic import get_by_name from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation @@ -95,81 +97,174 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: f"(NodeProto)." ) - # Copy model to modify - node_model = deepcopy(self.model) - - # Remove any other node - # TODO: Refactor this following section - for i, node in enumerate(self.model.graph.node): - if i != index: - node_model.graph.node.remove(node) - target_op = getCustomOp(node_model.graph.node[0]) + target_op = getCustomOp(self.model.graph.node[index]) if not isinstance(target_op, HWCustomOp): raise FINNInternalError( - f"Node {node_model.graph.node[0].name} is not a HWCustomOp, cannot " - f"isolate for simulation." + f"Node {target_op.name} is not a HWCustomOp, cannot isolate for simulation." ) - inp = onnx.helper.make_tensor_value_info( - "inp", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_folded_input_shape()) - ) - inp_dummy_out = onnx.helper.make_tensor_value_info( # noqa - "inp_dummy_out", - TensorProto.FLOAT, - cast("Sequence[int]", target_op.get_folded_input_shape()), - ) - outp = onnx.helper.make_tensor_value_info( # noqa - "outp", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_output_shape()) - ) - outp_dummy_out = onnx.helper.make_tensor_value_info( - "outp_dummy_out", - TensorProto.FLOAT, - cast("Sequence[int]", target_op.get_normal_output_shape()), - ) - input_dummy_node = onnx.helper.make_node( - "RemoveDataPath_rtl", - inputs=["inp"], - outputs=["inp_dummy_out"], - domain="finn.custom_op.fpgadataflow.rtl", - backend="fpgadataflow", - folded_shape=target_op.get_folded_input_shape(), - normal_shape=target_op.get_normal_input_shape(), - dataType=target_op.get_input_datatype().name, - name=node_model.graph.node[0].name + "_input_dummy", - ) - output_dummy_node = onnx.helper.make_node( - "RemoveDataPath_rtl", - inputs=["outp"], - outputs=["outp_dummy_out"], - domain="finn.custom_op.fpgadataflow.rtl", - backend="fpgadataflow", - folded_shape=target_op.get_folded_output_shape(), - normal_shape=target_op.get_normal_output_shape(), - dataType=target_op.get_output_datatype().name, - name=node_model.graph.node[0].name + "_output_dummy", - ) - node_model.graph.node.insert(0, input_dummy_node) - node_model.graph.node.append(output_dummy_node) + initializers: list[TensorProto] = [] + value_info_protos: list[ValueInfoProto] = [] + inputs_graph: list[ValueInfoProto] = [] + inputs_node: list[ValueInfoProto] = [] + outputs_graph: list[ValueInfoProto] = [] + outputs_node: list[ValueInfoProto] = [] + nodes_graph: list[NodeProto] = [] + + preds_list: list | None = self.model.find_direct_predecessors(self.model.graph.node[index]) + succs_list: list | None = self.model.find_direct_successors(self.model.graph.node[index]) + + num_preds = len(preds_list) if preds_list is not None else 0 + num_succs = len(succs_list) if succs_list is not None else 0 + + # Set correct input/output count for input and output nodes, since they have no pred/succ. + if num_preds == 0: + inputs = self.model.graph.input + ret = get_by_name( + inputs, self.model.graph.node[index].input[0] + ) # Check that node is graph input + if ret is not None: + num_preds = 1 + if num_succs == 0: + outputs = self.model.graph.output + ret = get_by_name( + outputs, self.model.graph.node[index].output[0] + ) # Check that node is graph output + if ret is not None: + num_succs = 1 + + num_inputs = len(self.model.graph.node[index].input) + num_outputs = len(self.model.graph.node[index].output) + + if num_outputs != num_succs: + raise FINNInternalError( + f"Node {self.model.graph.node[index].name} has {num_outputs} outputs but " + f"{num_succs} successor nodes. This is not supported for isolation." + ) - # Remove old io - for _ in range(len(node_model.graph.node[1].input)): - node_model.graph.node[1].input.pop() - for _ in range(len(node_model.graph.node[1].output)): - node_model.graph.node[1].output.pop() + # Handle initializers of nodes + for i in range(num_preds, num_inputs): + ret = self.model.get_initializer( + self.model.graph.node[index].input[i], return_dtype=True + ) + info = self.model.get_tensor_valueinfo(self.model.graph.node[index].input[i]) + if ret is None or info is None: + raise FINNInternalError( + f"Failed to get initializer for {self.model.graph.node[index].input[i]} " + f"while isolating node {self.model.graph.node[index].name}." + ) + vals, dtype = cast("tuple[np.ndarray, int]", ret) + initializers.append(onnx.helper.make_tensor(info.name, dtype, vals.shape, vals)) + val_info = onnx.helper.make_tensor_value_info(info.name, dtype, vals.shape) + value_info_protos.append(val_info) + + for i in range(num_preds): + info = self.model.get_tensor_valueinfo(self.model.graph.node[index].input[i]) + if info is None: + raise FINNInternalError( + f"Failed to get value info for {self.model.graph.node[index].input[i]} " + f"while isolating node {self.model.graph.node[index].name}." + ) + # Setup new input tensors + new_input_info = onnx.helper.make_tensor_value_info( + info.name+"_"+str(i), + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_input_shape(i)), + ) + new_input_dummy_info = onnx.helper.make_tensor_value_info( + info.name + "_dummy_"+str(i), + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_input_shape(i)), + ) + #value_info_protos.append(new_input_info) + value_info_protos.append(new_input_dummy_info) + inputs_graph.append(new_input_info) + inputs_node.append(new_input_dummy_info) + + # Create new dummy node to remove data path for input i + dummy_node = onnx.helper.make_node( + "RemoveDataPath_rtl", + inputs=[new_input_info.name], + outputs=[new_input_dummy_info.name], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + folded_shape=target_op.get_folded_input_shape(i), + normal_shape=target_op.get_normal_input_shape(i), + dataType=target_op.get_input_datatype(i).name, + name=self.model.graph.node[index].name + f"_input_dummy_{i}", + ) + + nodes_graph.append(dummy_node) + for i in range(num_succs): + info = self.model.get_tensor_valueinfo(self.model.graph.node[index].output[i]) + if info is None: + raise FINNInternalError( + f"Failed to get value info for {self.model.graph.node[index].output[i]} " + f"while isolating node {self.model.graph.node[index].name}." + ) + # Setup new input tensors + new_output_info = onnx.helper.make_tensor_value_info( + info.name+"_"+str(i), + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_output_shape(i)), + ) + new_output_dummy_info = onnx.helper.make_tensor_value_info( + info.name + "_dummy_"+str(i), + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_output_shape(i)), + ) + #value_info_protos.append(new_output_info) + value_info_protos.append(new_output_dummy_info) + outputs_graph.append(new_output_info) + outputs_node.append(new_output_dummy_info) + + # Create new dummy node to remove data path for output i + dummy_node = onnx.helper.make_node( + "RemoveDataPath_rtl", + inputs=[new_output_dummy_info.name], + outputs=[new_output_info.name], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + folded_shape=target_op.get_folded_output_shape(i), + normal_shape=target_op.get_normal_output_shape(i), + dataType=target_op.get_output_datatype(i).name, + name=self.model.graph.node[index].name + f"_output_dummy_{i}", + ) - # Set new io - node_model.graph.node[1].input.append("inp_dummy_out") - node_model.graph.node[1].output.append("outp") + nodes_graph.append(dummy_node) + + target_op_attrs = target_op.get_nodeattr_types() + params = {} + for attr in target_op_attrs.keys(): + attr_val = target_op.get_nodeattr(attr) + if (attr_val == "" or attr_val == [] + or (isinstance(attr_val, np.ndarray) and attr_val.size == 0) + ): # Empty value, skip + continue + params[attr] = target_op.get_nodeattr(attr) + new_node = onnx.helper.make_node( + self.model.graph.node[index].op_type, + inputs=[inp.name for inp in inputs_node], + outputs=[outp.name for outp in outputs_node], + domain=self.model.graph.node[index].domain, + name=self.model.graph.node[index].name, + **params, + ) + nodes_graph.append(new_node) + + graph = onnx.helper.make_graph( + nodes_graph, + f"isolated_node_graph_{self.model.graph.node[index].name}", + inputs_graph, + outputs_graph, + initializer=initializers, + value_info=value_info_protos, + ) - # Remove graph io - for _ in range(len(node_model.graph.input)): - node_model.graph.input.pop() - for _ in range(len(node_model.graph.output)): - node_model.graph.output.pop() + node_model = onnx.helper.make_model(graph) + node_model = ModelWrapper(node_model) - # Set new graph io - node_model.graph.input.append(inp) - node_model.graph.output.append(outp_dummy_out) + #node_model.save(f"isolated_node_model_{self.model.graph.node[index].name}.onnx") return node_model @@ -430,14 +525,6 @@ def build_single_node_simulation( # TODO: Check if something is an output node instead of checking the node index # TODO: Requires changes in the C++ code as well - # Sanity checks (2 Dummy nodes + 1 target node) - if len(node_model.graph.node) != 3: - raise FINNUserError( - "Cannot create single-node simulation for a model with more than " - "1 node. Make sure to pass the ModelWrapper containing only" - "the relevant node." - ) - # Check that the relevant data exists wrapper_filename = node_model.get_metadata_prop("wrapper_filename") if wrapper_filename is None or not Path(wrapper_filename).exists(): @@ -547,8 +634,8 @@ def _build( # Build sims in parallel synth_workers = max( - 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 20) - ) # 20GB per synthesis + 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 10) + ) # 10GB per synthesis if not functional_sim: # When not having to do synthesis, the build is not memory bottlenecked and # can be executed as parallel as possible diff --git a/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py b/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py index 52eb55355a..4f3b8ba093 100644 --- a/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py +++ b/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py @@ -28,6 +28,7 @@ import math +import numpy as np from onnx import TensorProto, helper from qonnx.core.datatype import DataType from qonnx.custom_op.registry import getCustomOp @@ -38,8 +39,7 @@ def _get_signed_from_upstream(model, trunc_node): - """ - Find out what the sign of the input to the trunc node is, + """Find out what the sign of the input to the trunc node is, by looking at the upstream nodes. """ node = trunc_node @@ -111,10 +111,36 @@ def _get_signed_from_upstream(model, trunc_node): class AvgPoolAndTruncToQuantAvgPool(Transformation): - """ - Convert a section of nodes of the pattern: + """Convert a section of nodes of the pattern: AveragePool -> Mul (scalar) -> Trunc - To the FINN op: QuantAvgPool2d + To the FINN op: QuantAvgPool2d. + """ + + def apply(self, model): + opset_imports = model.get_opset_imports() + if "qonnx.custom_op.general" in opset_imports: + trunc_opset = opset_imports["qonnx.custom_op.general"] + elif "onnx.brevitas" in opset_imports: + trunc_opset = opset_imports["onnx.brevitas"] + else: + trunc_opset = 1 # Default to v1 if no opset found + if trunc_opset == 1: + model = model.transform(AvgPoolAndTruncv1ToQuantAvgPool()) + return model, False + elif trunc_opset == 2: + model = model.transform(AvgPoolAndTruncv2ToQuantAvgPool()) + return model, False + else: + raise NotImplementedError( + f"AvgPoolAndTruncToQuantAvgPool not implemented for " + f"Trunc opset version {trunc_opset}." + ) + + +class AvgPoolAndTruncv1ToQuantAvgPool(Transformation): + """Convert a section of nodes of the pattern: + AveragePool -> Mul (scalar) -> Trunc (v1) + To the FINN op: Div -> QuantAvgPool2d -> Mul. """ def apply(self, model): @@ -135,7 +161,7 @@ def apply(self, model): k_s = get_by_name(n.attribute, "kernel_shape") if k_s is None or len(k_s.ints) != 2 or len(set(k_s.ints)) != 1: raise ValueError( - "FINN only supports average pooling with " "2D square kernels." + "FINN only supports average pooling with 2D square kernels." ) k_s = k_s.ints[0] @@ -168,7 +194,7 @@ def apply(self, model): normalized_mode_string = rounding_mode.s.upper() if rounding_mode is None or normalized_mode_string != b"FLOOR": raise ValueError( - "The Trunc node must have the rounding_mode " "set to 'FLOOR'." + "The Trunc node must have the rounding_mode set to 'FLOOR'." ) for inp in t_node.input[1:]: if model.get_initializer(inp) is None: @@ -282,3 +308,170 @@ def apply(self, model): return model, True return model, False + + +class AvgPoolAndTruncv2ToQuantAvgPool(Transformation): + """Convert a section of nodes of the pattern: + AveragePool -> Trunc (v2) + To the FINN op: Div -> QuantAvgPool2d -> Mul. + """ + + def apply(self, model): + graph = model.graph + node_ind = 0 + for node in graph.node: + node_ind += 1 + if node.op_type == "AveragePool": + t_node = model.find_direct_successors(node) + if t_node is not None and len(t_node) == 1 and t_node[0].op_type == "Trunc": + t_node = t_node[0] + running_node_index = node_ind + # Check node for compatibility + # Avg pooling node + k_s = get_by_name(node.attribute, "kernel_shape") + if k_s is None or len(k_s.ints) != 2 or len(set(k_s.ints)) != 1: + raise ValueError( + "FINN only supports average pooling with 2D square kernels." + ) + k_s = k_s.ints[0] + + pads = get_by_name(node.attribute, "pads") + if pads is None or len(set(pads.ints)) != 1 or pads.ints[0] != 0: + raise ValueError("FINN dosn't support padding for average pooling.") + + stride = get_by_name(node.attribute, "strides") + if stride is None or len(stride.ints) != 2 or len(set(stride.ints)) != 1: + raise ValueError( + "FINN only supports 2D strides with equal values in each direction." + ) + stride = stride.ints[0] + + # Trunc node + rounding_mode = get_by_name(t_node.attribute, "rounding_mode") + normalized_mode_string = rounding_mode.s.upper() + if rounding_mode is None or normalized_mode_string != b"FLOOR": + raise ValueError( + "The Trunc node must have the rounding_mode set to 'FLOOR'." + ) + for inp in t_node.input[1:]: + if model.get_initializer(inp) is None: + raise ValueError( + f"All inputs of the Trunc node, " + f"except the first, must be statically " + f"initialized. However, {inp} is not." + ) + zero_pt = model.get_initializer(t_node.input[2]) + if len(zero_pt.shape) != 0 or zero_pt != 0: + raise ValueError( + f"Finn only supports 0 as the zero point for " + f"the Trunc node, it currently is {zero_pt}." + ) + scale = model.get_initializer(t_node.input[1]).flatten() + out_scale = model.get_initializer(t_node.input[4]).flatten() + + trunc_in_bits = model.get_initializer(t_node.input[3]).flatten() + trunc_out_bits = model.get_initializer(t_node.input[5]).flatten() + if len(trunc_in_bits.shape) != 1 or len(trunc_out_bits.shape) != 1: + raise ValueError( + f"Finn only supports scalar bit widths " + f"for the Trunc node. The input bit width " + f"currently is: {trunc_in_bits}, " + f"while the output bit width is: {trunc_out_bits}." + ) + trunc_in_bits = int(trunc_in_bits[0]) + trunc_out_bits = int(trunc_out_bits[0]) + if np.round(np.log2(out_scale / scale) != trunc_in_bits - trunc_out_bits): + raise ValueError( + f"The scale values for the Trunc node are not " + f"compatible with the specified bit widths. " + f"Input scale: {scale}, output scale: {out_scale}, " + f"input bits: {trunc_in_bits}, output bits: {trunc_out_bits}." + ) + + # Calculate parameters for the QuantAvgPool2d node, + # Calculate input bit width. Basically this backwards: + # https://github.com/Xilinx/finn-base/blob/ + # 7c2603a95e90e4de2575020e575c24eab6a15889/src/finn/custom_op/ + # general/quantavgpool2d.py#L94 + ibits = math.floor(math.log(2**trunc_in_bits / (k_s * k_s), 2)) + # Get sign + signed = _get_signed_from_upstream(model, t_node) + # ToDo: Change this to NHWC, + # when the channels last layout comes around. + data_layout = "NCHW" + + # Insert scale nodes, QuantAvgPool2d node and required tensors + scale = model.get_initializer(t_node.input[1]) + # for Trunc v2 update input scale by receptive field + scale = (scale * k_s * k_s).astype(scale.dtype) + scale_div_tensor = helper.make_tensor_value_info( + model.make_new_valueinfo_name(), + TensorProto.FLOAT, + None, + ) + graph.value_info.append(scale_div_tensor) + model.set_initializer(scale_div_tensor.name, scale) + + act_scale_div_tensor = helper.make_tensor_value_info( + model.make_new_valueinfo_name(), + TensorProto.FLOAT, + None, + ) + graph.value_info.append(act_scale_div_tensor) + + scale_div_node = helper.make_node( + "Div", + [node.input[0], scale_div_tensor.name], + [act_scale_div_tensor.name], + ) + graph.node.insert(running_node_index, scale_div_node) + running_node_index += 1 + + act_scale_mul_tensor = helper.make_tensor_value_info( + model.make_new_valueinfo_name(), + TensorProto.FLOAT, + None, + ) + graph.value_info.append(act_scale_mul_tensor) + QuantAvgPool2d_node = helper.make_node( + "QuantAvgPool2d", + [act_scale_div_tensor.name], + [act_scale_mul_tensor.name], + domain="qonnx.custom_op.general", + stride=stride, + kernel=k_s, + ibits=ibits, + obits=trunc_out_bits, + signed=int(signed), + data_layout=data_layout, + ) + graph.node.insert(running_node_index, QuantAvgPool2d_node) + running_node_index += 1 + + scale_mul_tensor = helper.make_tensor_value_info( + model.make_new_valueinfo_name(), + TensorProto.FLOAT, + None, + ) + graph.value_info.append(scale_mul_tensor) + model.set_initializer(scale_mul_tensor.name, out_scale) + + scale_mul_node = helper.make_node( + "Mul", + [act_scale_mul_tensor.name, scale_mul_tensor.name], + [t_node.output[0]], + ) + graph.node.insert(running_node_index, scale_mul_node) + running_node_index += 1 + + # Remove old nodes + graph.node.remove(node) + graph.node.remove(t_node) + + # Recompute shapes and datatypes + model = model.transform(InferShapes()) + model = model.transform(InferDataTypes()) + + return model, True + + return model, False From a8dbc9f14dd4bd58231efb94f1d91ebb8bba5ad9 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:12:52 +0100 Subject: [PATCH 063/170] Fix building sim for residual connections --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 12 +- finn_xsi/finn_xsi/include/Simulation.hpp | 29 +-- finn_xsi/finn_xsi/include/helper.h | 4 +- finn_xsi/finn_xsi/rtlsim_config.hpp.template | 11 +- .../transformation/fpgadataflow/simulation.py | 2 +- .../fpgadataflow/simulation_build.py | 219 +++++++++--------- 6 files changed, 138 insertions(+), 139 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index d4c902665b..658da82a9a 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -25,12 +26,15 @@ namespace po = boost::program_options; constexpr std::size_t InstreamCount = RTLSimConfig::istream_descs.size(); constexpr std::size_t OutstreamCount = RTLSimConfig::ostream_descs.size(); +static_assert(InstreamCount == RTLSimConfig::inputInterfaceNames.size(), "Number of input streams must match number of previous nodes"); +static_assert(OutstreamCount == RTLSimConfig::outputInterfaceNames.size(), "Number of output streams must match number of next nodes"); + // Simulation state management enum class SimulationState { IDLE, CONFIGURED, RUNNING, FINISHED, ERROR }; class SimulationController { private: - SingleNodeSimulation& sim; + SingleNodeSimulation& sim; std::atomic state{SimulationState::IDLE}; std::atomic current_cycles{0}; std::atomic current_samples{0}; @@ -42,7 +46,7 @@ class SimulationController { bool timeout_occurred{false}; public: - explicit SimulationController(SingleNodeSimulation& simulation) + explicit SimulationController(SingleNodeSimulation& simulation) : sim(simulation) {} void configure(const std::vector& depths, std::size_t maxCycles) { @@ -263,9 +267,9 @@ int main(int argc, const char* argv[]) { std::cout << "Connected Simulation Node Index: " << RTLSimConfig::NodeIndex << " / " << RTLSimConfig::TotalNodes << std::endl; // Construct simulation - SingleNodeSimulation sim( + SingleNodeSimulation sim( RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, "xsim_log_file.txt", "trace_file.txt", RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs, - RTLSimConfig::previousNodeName, RTLSimConfig::currentNodeName, 2); + RTLSimConfig::inputInterfaceNames, RTLSimConfig::outputInterfaceNames, 2); // Create simulation controller SimulationController controller(sim); diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index e51d2d7751..5737c7258d 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -47,7 +47,7 @@ class Simulation { // Find I/O Streams and initialize their Status for (size_t i = 0; i < _istream_descs.size(); ++i) { - istreams[i] = S_AXIS_Control{top, clk, std::data(_istream_descs)[i].job_size, std::data(_istream_descs)[i].job_ticks, std::data(_istream_descs)[i].name}; + istreams[i] = S_AXIS_Control{top, clk, std::data(_istream_descs)[i].job_size, std::data(_istream_descs)[i].job_size, std::data(_istream_descs)[i].name}; } for (size_t i = 0; i < _ostream_descs.size(); ++i) { ostreams[i] = M_AXIS_Control{top, clk, std::data(_ostream_descs)[i].job_size, std::data(_ostream_descs)[i].name}; @@ -104,12 +104,10 @@ struct CommData { // │ ready ready │ // │ (sim) │ // └──────────────────────────────────────┘ -template +template class SingleNodeSimulation : public Simulation { using ConsumingInterface = InterprocessCommunicationChannel; using ProducingInterface = InterprocessCommunicationChannel; - constexpr static bool FirstNode = NodeIndex == 0; - constexpr static bool LastNode = NodeIndex == (TotalNodes - 1); std::array fromProducerInterface; std::array toConsumerInterface; std::size_t cyclesRun = 0; @@ -179,26 +177,17 @@ class SingleNodeSimulation : public Simulation _istream_descs, std::array _ostream_descs, - std::optional prevNodeName = std::nullopt, std::optional nodeName = std::nullopt, unsigned int initialFIFODepth = 2) + std::array inputInterfaceNames, std::array outputInterfaceNames, unsigned int initialFIFODepth = 2) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { - if (!FirstNode && !prevNodeName) { + if (!FirstNode && inputInterfaceNames.empty()) { throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); - } else if (FirstNode && prevNodeName) { - std::cout << "Simulation was passed the previous nodes name but is " - "NOT marked for communication with predecessor node. No " - "shared memory will be created." - << std::endl; - } - if (!LastNode && !nodeName) { + } + if (!LastNode && outputInterfaceNames.empty()) { throw std::runtime_error( "Cannot communicate with successor because " "current node name was not given!"); - } else if (LastNode && nodeName) { - std::cout << "Simulation was passed the current nodes name but is NOT " - "marked for communication with successor node. No shared " - "memory will be created." - << std::endl; } + if constexpr (!LastNode) { // Create FIFO buffer for (std::size_t i = 0; i < OStreamsSize; ++i) { @@ -209,14 +198,14 @@ class SingleNodeSimulation : public Simulation HEX = {'0', '1', '2', '3', '4', '5', '6', '7', struct StreamDescriptor { std::string name; std::size_t job_size; - // Next job can only start this many clock ticks after start of predecessor. - std::size_t job_ticks; + // // Next job can only start this many clock ticks after start of predecessor. + // std::size_t job_ticks; }; #ifdef NDEBUG diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index 217de6c038..6b442ade25 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -16,14 +16,17 @@ #include #include #include +#include namespace RTLSimConfig { // Log during simulation. Turned off by default. Might increase runtime if used. constexpr bool LoggingEnabled = true; + constexpr bool IsInputNode = @IS_INPUT_NODE@; + constexpr bool IsOutputNode = @IS_OUTPUT_NODE@; /**** General RTLSIM Configuration Parameters ****/ - const std::optional currentNodeName = "@NODE_NAME@"; - const std::optional previousNodeName = @PREVIOUS_NODE_NAME@; + constexpr std::array inputInterfaceNames { @INPUT_INTERFACE_NAMES@ }; + constexpr std::array outputInterfaceNames { @OUTPUT_INTERFACE_NAMES@ }; // Which index node this simulation executes // In a complete design simulation this is 0 @@ -42,10 +45,10 @@ namespace RTLSimConfig { // AXI stream descriptors {stream_name, transactions_per_inference} // input AXI stream descriptors - std::array istream_descs { @ISTREAM_DESC@ }; + constexpr std::array istream_descs { @ISTREAM_DESC@ }; // output AXI stream descriptors - std::array ostream_descs { @OSTREAM_DESC@ }; + constexpr std::array ostream_descs { @OSTREAM_DESC@ }; // max number of cycles to wait for output activity on any stream before timeout constexpr unsigned max_iters = @TIMEOUT_CYCLES@; diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 42bf23c453..db382fa739 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -66,7 +66,7 @@ def __init__( # TODO: called BuildSimulation in the step before # (However this only compiles, it should NOT stitch the IPs again) self.model = self.model.transform( - BuildSimulation(fpgapart, clk_ns, functional_sim, simulation_type, workers) + BuildSimulation(fpgapart, clk_ns, functional_sim) ) self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} match simulation_type: diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 94d0381c87..7a45be8aa9 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -10,7 +10,6 @@ import sys from concurrent.futures import Future, ThreadPoolExecutor from contextlib import nullcontext -from copy import deepcopy from enum import Enum from onnx import NodeProto, TensorProto, ValueInfoProto from pathlib import Path @@ -20,7 +19,7 @@ from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames from qonnx.transformation.infer_shapes import InferShapes -from random import Random +from ast import literal_eval from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, cast @@ -41,6 +40,7 @@ # TODO: Fix that BuildSimulation has to return binaries for either SimulationType # TODO: Just store the directory instead - since we build all targets anyways + class SimulationType(str, Enum): """Type of simulation.""" @@ -50,9 +50,6 @@ class SimulationType(str, Enum): # Individual node simulations, isolated. E.g. for analysis purposes NODE_BASED_ISOLATED = "NODE_BASED_ISOLATED" - # Legacy method (deprecated) - COMPLETE_DESIGN = "COMPLETE_DESIGN" - class SimulationBuilder: """Build simulations in FINN.""" @@ -120,6 +117,9 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: num_preds = len(preds_list) if preds_list is not None else 0 num_succs = len(succs_list) if succs_list is not None else 0 + input_node = False + output_node = False + # Set correct input/output count for input and output nodes, since they have no pred/succ. if num_preds == 0: inputs = self.model.graph.input @@ -128,6 +128,7 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ) # Check that node is graph input if ret is not None: num_preds = 1 + input_node = True if num_succs == 0: outputs = self.model.graph.output ret = get_by_name( @@ -135,6 +136,7 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ) # Check that node is graph output if ret is not None: num_succs = 1 + output_node = True num_inputs = len(self.model.graph.node[index].input) num_outputs = len(self.model.graph.node[index].output) @@ -145,23 +147,33 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: f"{num_succs} successor nodes. This is not supported for isolation." ) + initializer_inputs_list = [ + self.model.graph.node[index].input[i] + for i in range(num_inputs) + if self.model.get_initializer(self.model.graph.node[index].input[i]) is not None + ] + # Handle initializers of nodes - for i in range(num_preds, num_inputs): - ret = self.model.get_initializer( - self.model.graph.node[index].input[i], return_dtype=True - ) - info = self.model.get_tensor_valueinfo(self.model.graph.node[index].input[i]) + initializer_inputs = [] + for init in initializer_inputs_list: + ret = self.model.get_initializer(init, return_dtype=True) + info = self.model.get_tensor_valueinfo(init) if ret is None or info is None: raise FINNInternalError( - f"Failed to get initializer for {self.model.graph.node[index].input[i]} " + f"Failed to get initializer for {init} " f"while isolating node {self.model.graph.node[index].name}." ) vals, dtype = cast("tuple[np.ndarray, int]", ret) initializers.append(onnx.helper.make_tensor(info.name, dtype, vals.shape, vals)) val_info = onnx.helper.make_tensor_value_info(info.name, dtype, vals.shape) value_info_protos.append(val_info) + initializer_inputs.append(val_info) - for i in range(num_preds): + pred_count = 0 + for i in range(num_inputs): + if self.model.graph.node[index].input[i] in initializer_inputs_list: + continue # This input is handled as an initializer, skip + pred_count += 1 info = self.model.get_tensor_valueinfo(self.model.graph.node[index].input[i]) if info is None: raise FINNInternalError( @@ -170,16 +182,16 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ) # Setup new input tensors new_input_info = onnx.helper.make_tensor_value_info( - info.name+"_"+str(i), + info.name + "_" + str(i), TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_input_shape(i)), ) new_input_dummy_info = onnx.helper.make_tensor_value_info( - info.name + "_dummy_"+str(i), + info.name + "_dummy_" + str(i), TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_input_shape(i)), ) - #value_info_protos.append(new_input_info) + # value_info_protos.append(new_input_info) value_info_protos.append(new_input_dummy_info) inputs_graph.append(new_input_info) inputs_node.append(new_input_dummy_info) @@ -198,6 +210,12 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ) nodes_graph.append(dummy_node) + inputs_node.extend(initializer_inputs) + if pred_count != num_preds: + raise FINNInternalError( + f"Node {self.model.graph.node[index].name} has {num_preds} pred. nodes but only " + f"{pred_count} inputs have been handled." + ) for i in range(num_succs): info = self.model.get_tensor_valueinfo(self.model.graph.node[index].output[i]) if info is None: @@ -207,16 +225,16 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ) # Setup new input tensors new_output_info = onnx.helper.make_tensor_value_info( - info.name+"_"+str(i), + info.name + "_" + str(i), TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_output_shape(i)), ) new_output_dummy_info = onnx.helper.make_tensor_value_info( - info.name + "_dummy_"+str(i), + info.name + "_dummy_" + str(i), TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_output_shape(i)), ) - #value_info_protos.append(new_output_info) + # value_info_protos.append(new_output_info) value_info_protos.append(new_output_dummy_info) outputs_graph.append(new_output_info) outputs_node.append(new_output_dummy_info) @@ -240,7 +258,9 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: params = {} for attr in target_op_attrs.keys(): attr_val = target_op.get_nodeattr(attr) - if (attr_val == "" or attr_val == [] + if ( + attr_val == "" + or attr_val == [] or (isinstance(attr_val, np.ndarray) and attr_val.size == 0) ): # Empty value, skip continue @@ -267,18 +287,22 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: node_model = onnx.helper.make_model(graph) node_model = ModelWrapper(node_model) + node_model.set_metadata_prop("predecessors", str([pred.name for pred in inputs_graph])) + node_model.set_metadata_prop("successors", str([succ.name for succ in outputs_graph])) + node_model.set_metadata_prop("input_node", str(input_node).lower()) + node_model.set_metadata_prop("output_node", str(output_node).lower()) + #node_model.save(f"isolated_node_model_{self.model.graph.node[index].name}.onnx") return node_model - def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, int]: + def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, str]: """Return the stream descriptions for the given model for the C++ sim config header. Used by for example _build_single_node_simulation(). Returns: - tuple[str, int, str, int]: Strings of stream descriptions together with - their count (in, out) + tuple[str, str]: Strings of stream descriptions """ # Get IO iterations required instream_iters = [] @@ -297,15 +321,14 @@ def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, top_ind = list(last_node.output).index(oname) oshape_folded = getCustomOp(last_node).get_folded_output_shape(ind=top_ind) outstream_iters.append(int(np.prod(oshape_folded[:-1]))) + interface_names = model.get_metadata_prop("vivado_stitch_ifnames") if interface_names is None: raise FINNInternalError( f"{model}: Could not find stitched-IP interface names. " f"Did you run IP Stitching first?" ) - - # TODO: Copied from rtlsim_exec_cppxsi. Remove eval(). - interface_names = eval(interface_names) + interface_names = literal_eval(interface_names) if "aximm" in interface_names.keys() and interface_names["aximm"] != []: raise FINNInternalError( f"{model}: CPP XSI Sim does not know how to handle full " @@ -314,25 +337,21 @@ def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, int, str, instream_names = [x[0] for x in interface_names["s_axis"]] outstream_names = [x[0] for x in interface_names["m_axis"]] - # Format stream descriptions - def _format_descr_name(s: str) -> str: - for old, new in [("[", ""), ("]", ""), ("(", "{"), (")", "}"), ("'", '"')]: - s = s.replace(old, new) - return s + # Convert to the format required by the C++ sim config header + # (initializer list of pairs of name and iters) + def _format_descr_name(s: list[tuple[str, int]]) -> str: + return ", ".join([f'StreamDescriptor{{"{name}", {iters}}}' for name, iters in s]) - # TODO: Change this since we don't have throttling instream_descrs = [ - (instream_names[i], instream_iters[i], instream_iters[i]) - for i in range(len(instream_names)) + (instream_names[i], instream_iters[i]) for i in range(len(instream_names)) ] - instream_descrs_str = _format_descr_name(str(instream_descrs)) + instream_descrs_str = _format_descr_name(instream_descrs) outstream_descrs = [ - (outstream_names[i], outstream_iters[i], outstream_iters[i]) - for i in range(len(outstream_names)) + (outstream_names[i], outstream_iters[i]) for i in range(len(outstream_names)) ] - outstream_descrs_str = _format_descr_name(str(outstream_descrs)) - return instream_descrs_str, len(instream_names), outstream_descrs_str, len(outstream_names) + outstream_descrs_str = _format_descr_name(outstream_descrs) + return instream_descrs_str, outstream_descrs_str def _create_sim_so( self, @@ -367,9 +386,7 @@ def _create_sim_so( sim_rel = "xsim.dir" + sim_rel return Path(sim_base), Path(sim_rel) - def _compile_simulation( - self, sim_base: Path, silent: bool = False - ) -> Path: + def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: """Compile an existing RTLSIM directory. Requires _create_sim_so to be run before. Expects rtlsim_config.hpp to be templated already. @@ -420,8 +437,10 @@ def _compile_simulation( for target in compile_targets: simulation_executable = Path(sim_base) / target if not simulation_executable.exists(): - errors.append(f"Simulation compile target {target} was not created. " - f"Check {sim_base} to run make manually.") + errors.append( + f"Simulation compile target {target} was not created. " + f"Check {sim_base} to run make manually." + ) if len(errors) > 0: raise FINNInternalError("Error compiling simulations: \n" + "\n\t".join(errors)) self.progress_bar.update("Make") @@ -431,8 +450,8 @@ def _template_rtlsim_config( self, model: ModelWrapper, sim_base: Path, - node_name: str, - previous_node_name: str | None, + input_interface_names: list[str] | None, + output_interface_names: list[str] | None, node_index: int, total_nodes: int, timeout_cycles: int, @@ -446,9 +465,7 @@ def _template_rtlsim_config( # Prepare the C++ driver config template ( instream_descrs_str, - len_instreams, outstream_descrs_str, - len_outstreams, ) = self._get_stream_descriptions(model) template_dict = { "TIMEOUT_CYCLES": timeout_cycles, @@ -456,30 +473,37 @@ def _template_rtlsim_config( "TOP_MODULE_NAME": top_module_name, # top-level AXI stream descriptors "ISTREAM_DESC": instream_descrs_str, - "ISTREAM_LEN": len_instreams, "OSTREAM_DESC": outstream_descrs_str, - "OSTREAM_LEN": len_outstreams, # control tracing and trace filename "TRACE_FILE": "std::nullopt" if trace_file is None else f'"{trace_file}"', # sim kernel .so to use (depends on Vivado version) "SIMKERNEL_SO": finnxsi.get_simkernel_so(), # log file for xsi (not the sim driver) "XSIM_LOG_FILE": '"xsi.log"', - # Node name in case of single-node simulation - "NODE_NAME": node_name, - # Previous node name (for single node simulation) - "PREVIOUS_NODE_NAME": ( - "std::nullopt" if previous_node_name is None else f'"{previous_node_name}"' - ), + "INPUT_INTERFACE_NAMES": ",".join(['"' + name + '"' for name in input_interface_names]) + if input_interface_names is not None + else "", + "OUTPUT_INTERFACE_NAMES": ",".join( + ['"' + name + '"' for name in output_interface_names] + ) + if output_interface_names is not None + else "", + "INPUT_INTERFACE_COUNT": len(input_interface_names) + if input_interface_names is not None + else 0, + "OUTPUT_INTERFACE_COUNT": len(output_interface_names) + if output_interface_names is not None + else 0, "NODE_INDEX": node_index, "TOTAL_NODES": total_nodes, + "IS_INPUT_NODE": model.get_metadata_prop("input_node"), + "IS_OUTPUT_NODE": model.get_metadata_prop("output_node"), } fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" fsim_config = fifosim_config_fname.read_text() for key, val in template_dict.items(): fsim_config = fsim_config.replace(f"@{key}@", str(val)) - # Write the config to the simulation directory rtlsim_config = Path(sim_base) / "rtlsim_config.hpp" rtlsim_config.write_text(fsim_config) @@ -487,11 +511,11 @@ def _template_rtlsim_config( def build_single_node_simulation( self, - node_name: str, node_model: ModelWrapper, node_index: int, total_nodes: int, - previous_node_name: str | None, + input_interface_names: list[str] | None, + output_interface_names: list[str] | None, build_dir: Path | None, timeout_cycles: int = 0, silent: bool = False, @@ -503,15 +527,16 @@ def build_single_node_simulation( Much of this is from the rtlsim_exec.py in core/ Args: - node_name: Despite the fact that we receive an isolated node model, we can still - manually pass a node name. This is useful to give unique names (e.g. for IPC) node_model: The single node ModelWrapper to build the simulation from. node_index: The index of the simulated node. Used to determine whether a node shares IO with successors or predecessors. total_nodes: The total number of nodes in the complete design. - previous_node_name: Required by the connected simulation. In the simulation binary this - is used to get access to the correct shared memory segment between - this node and the previous one. + input_interface_names: Names of input interfaces for IPC communication. Required by the + connected simulation to access the correct shared memory segment + between this node and its predecessors. + output_interface_names: Names of output interfaces for IPC communication. Required by + the connected simulation to access the correct shared memory segment + between this node and its successors. build_dir: If given, use this directory for building the simulation. Otherwise one is created from the nodes name. timeout_cycles: Number of cycles until simulation timeout. When set to 0 (default), no @@ -529,7 +554,7 @@ def build_single_node_simulation( if wrapper_filename is None or not Path(wrapper_filename).exists(): raise FINNUserError( f"Call CreateStitchedIP prior to building " - f"the simulation for {node_name}. " + f"the simulation for {self.model.graph.node[node_index].name}. " f"wrapper_filename is set to {wrapper_filename}!" ) @@ -537,7 +562,8 @@ def build_single_node_simulation( if vivado_stitched_proj is None or not Path(vivado_stitched_proj).exists(): raise FINNUserError( f"Call CreateStitchedIP prior to building " - f"the simulation for {node_name}. (vivado_stitch_proj not set!)" + f"the simulation for {self.model.graph.node[node_index].name}." + "(vivado_stitch_proj not set!)" ) trace_file = cast("str | None", node_model.get_metadata_prop("rtlsim_trace")) @@ -556,8 +582,8 @@ def build_single_node_simulation( _ = self._template_rtlsim_config( node_model, sim_base, - node_name, - previous_node_name, + input_interface_names, + output_interface_names, node_index, total_nodes, timeout_cycles, @@ -568,21 +594,8 @@ def build_single_node_simulation( # Building the whole simulation return self._compile_simulation(sim_base, silent=silent).absolute() - def _get_randomized_names(self, model: ModelWrapper, suffix_length: int = 5) -> dict[int, str]: - """Add a randomized suffix to every name in the model. Used to avoid interference with - previous or parallel running IPC simulations.""" - rand = Random() - rand.seed() - return { - i: model.graph.node[i].name - + "".join( - rand.choice("abcdefghijklmnopqrstuvwxyz0123456789") for _ in range(suffix_length) - ) - for i in range(len(model.graph.node)) - } - def _build_simulations_parallel( - self, workers: int, with_live_display: bool, functional_sim: bool + self, with_live_display: bool, functional_sim: bool ) -> dict[int, Path]: """Build all nodes in the model in parallel, as isolated simulations, ready for usage in an IPC connected simulation chain. @@ -599,11 +612,10 @@ def _build_simulations_parallel( directories. """ log.info(f"Building simulation binaries for {len(self.model.graph.node)} layers.") + def _build( - node_name: str, node_index: int, total_nodes: int, - prev_node_name: str | None, build_dir: Path, ) -> Any: nodemodel = self._isolated_node_model(node_index) @@ -613,20 +625,22 @@ def _build( CreateStitchedIP(self.fpgapart, self.clk_ns, functional_simulation=functional_sim) ) self.progress_bar.update("StitchedIP") + input_interface_names = nodemodel.get_metadata_prop("predecessors") + if input_interface_names is not None: + input_interface_names = literal_eval(input_interface_names) + output_interface_names = nodemodel.get_metadata_prop("successors") + if output_interface_names is not None: + output_interface_names = literal_eval(output_interface_names) return self.build_single_node_simulation( - node_name, nodemodel, node_index, total_nodes, - prev_node_name, + input_interface_names, + output_interface_names, build_dir, silent=with_live_display, ) - # Create randomized names to avoid clashes with old IPC shared memory - randomized_names = self._get_randomized_names(self.model) - - # TODO: Currently ignores workers argument total_nodes = len(self.model.graph.node) futures: dict[int, Future] = {} @@ -647,19 +661,16 @@ def _build( ) with ThreadPoolExecutor(max_workers=synth_workers) as pool: for i in range(total_nodes): + node_name = self.model.graph.node[i].name futures[i] = pool.submit( _build, - randomized_names[i], i, total_nodes, - randomized_names[i - 1] if i >= 1 else None, # type: ignore - Path(make_build_dir(f"rtlsim_{randomized_names[i]}_")), + Path(make_build_dir(f"rtlsim_{node_name}")), ) return {i: future.result() for i, future in futures.items()} - def build_simulation( - self, workers: int, with_live_display: bool, functional_sim: bool - ) -> dict[int, Path]: + def build_simulation(self, with_live_display: bool, functional_sim: bool) -> dict[int, Path]: """Build a simulation of the given type, return the path to the executable directory (indexed by the corresponding node index in the graph). @@ -672,17 +683,13 @@ def build_simulation( """ node_count = len(self.model.graph.node) self.progress_bar = ThreadsafeProgressDisplay( - ["StitchedIP", "CMake", "Make"], - [node_count] * 3, + ["StitchedIP"], + [node_count], [ "[bold blue](1)[/bold blue] Creating stitched IPs", - "[bold blue](2)[/bold blue] Configuring project with CMake", - "[bold blue](3)[/bold blue] Building simulation binaries", ], ) - return self._build_simulations_parallel( - workers, with_live_display, functional_sim - ) + return self._build_simulations_parallel(with_live_display, functional_sim) class BuildSimulation(Transformation): @@ -695,10 +702,8 @@ def __init__( fpgapart: str, clk_ns: float, functional_sim: bool, - workers: int | None = None, ) -> None: """Create a new BuildSimulation transform.""" - self.workers = int(os.environ["NUM_DEFAULT_WORKERS"]) if workers is None else workers self.functional_sim = functional_sim self.fpgapart = fpgapart self.clk_ns = clk_ns @@ -729,10 +734,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: if needs_rebuild: self.builder = SimulationBuilder(self.model, self.fpgapart, self.clk_ns) - # sys.stdout = sys.stdout.console # type: ignore - # sys.stderr = sys.stderr.console # type: ignore + sys.stdout = sys.stdout.console # type: ignore self.binaries = self.builder.build_simulation( - self.workers, with_live_display=True, functional_sim=self.functional_sim, ) @@ -765,7 +768,7 @@ def _compile(binary: Path, progress: ThreadsafeProgressDisplay) -> None: ) progress.start() futures = [] - with ThreadPoolExecutor(self.workers) as tpe: + with ThreadPoolExecutor(int(os.environ.get("NUM_DEFAULT_WORKERS", "8"))) as tpe: for binary in sim_binaries: futures.append(tpe.submit(_compile, binary, progress)) tpe.shutdown() From f7806f2ab8baf534b40212dcda3539708c692a43 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 11 Feb 2026 11:53:26 +0100 Subject: [PATCH 064/170] Mostly formatting for easier readable stdout --- .../fpgadataflow/hlssynth_ip.py | 2 +- .../transformation/fpgadataflow/prepare_ip.py | 2 +- .../fpgadataflow/simulation_build.py | 148 +++++++++++------- .../fpgadataflow/simulation_connected.py | 47 ++++-- .../fpgadataflow/simulation_isolated.py | 107 ++++++++----- 5 files changed, 193 insertions(+), 113 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/hlssynth_ip.py b/src/finn/transformation/fpgadataflow/hlssynth_ip.py index ebc60d31d6..8b390eaa00 100644 --- a/src/finn/transformation/fpgadataflow/hlssynth_ip.py +++ b/src/finn/transformation/fpgadataflow/hlssynth_ip.py @@ -71,7 +71,7 @@ def applyNodeLocal(self, node): # call the compilation function for this node inst.ipgen_singlenode_code() else: - log.info(f"Using pre-existing IP for {node.name}") + log.debug(f"Using pre-existing IP for {node.name}") # ensure that executable path is now set assert ( inst.get_nodeattr("ipgen_path") != "" diff --git a/src/finn/transformation/fpgadataflow/prepare_ip.py b/src/finn/transformation/fpgadataflow/prepare_ip.py index a60c8b6b49..57539c9afc 100644 --- a/src/finn/transformation/fpgadataflow/prepare_ip.py +++ b/src/finn/transformation/fpgadataflow/prepare_ip.py @@ -53,7 +53,7 @@ def _codegen_single_node(node, model, fpgapart, clk): # ensure that there is generated code inside the dir inst.code_generation_ipgen(model, fpgapart, clk) else: - log.info(f"Using pre-existing code for {node.name}") + log.debug(f"Using pre-existing code for {node.name}") except KeyError: # exception if op_type is not supported raise Exception(f"Custom op_type {op_type} is currently not supported.") diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 7a45be8aa9..9e5a96c610 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -3,13 +3,13 @@ import finn_xsi.adapter as finnxsi import numpy as np import onnx +import time import os import psutil import shlex import subprocess import sys from concurrent.futures import Future, ThreadPoolExecutor -from contextlib import nullcontext from enum import Enum from onnx import NodeProto, TensorProto, ValueInfoProto from pathlib import Path @@ -22,6 +22,7 @@ from ast import literal_eval from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, cast +from collections.abc import Callable from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -31,7 +32,7 @@ from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.basic import launch_process_helper, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError -from finn.util.logging import DisabledLoggingConsole, ThreadsafeProgressDisplay, log +from finn.util.logging import log if TYPE_CHECKING: from collections.abc import Sequence @@ -59,7 +60,6 @@ def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: self.model = model self.fpgapart = fpgapart self.clk_ns = clk_ns - self.progress_bar = ThreadsafeProgressDisplay([], [], []) def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: """Return a modelwrapper that has only the specified node. @@ -386,7 +386,7 @@ def _create_sim_so( sim_rel = "xsim.dir" + sim_rel return Path(sim_base), Path(sim_rel) - def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: + def _compile_simulation(self, sim_base: Path, silent: bool = True) -> Path: """Compile an existing RTLSIM directory. Requires _create_sim_so to be run before. Expects rtlsim_config.hpp to be templated already. @@ -397,7 +397,6 @@ def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: compile_targets = ["LayerSimulationBackend", "IsolatedSimulationBackend"] if all((Path(sim_base) / execname).exists() for execname in compile_targets): # Simulation was already compiled, we can return early - self.progress_bar.update("Make") return Path(sim_base) # Check where FINNXSI is @@ -416,7 +415,6 @@ def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: ) except CalledProcessError as e: raise FINNInternalError(f"Failed to run cmake in {sim_base}") from e - self.progress_bar.update("CMake") # Calling make to actually build the simulation makefile = Path(sim_base) / "Makefile" @@ -443,7 +441,6 @@ def _compile_simulation(self, sim_base: Path, silent: bool = False) -> Path: ) if len(errors) > 0: raise FINNInternalError("Error compiling simulations: \n" + "\n\t".join(errors)) - self.progress_bar.update("Make") return sim_base def _template_rtlsim_config( @@ -624,7 +621,6 @@ def _build( nodemodel = nodemodel.transform( CreateStitchedIP(self.fpgapart, self.clk_ns, functional_simulation=functional_sim) ) - self.progress_bar.update("StitchedIP") input_interface_names = nodemodel.get_metadata_prop("predecessors") if input_interface_names is not None: input_interface_names = literal_eval(input_interface_names) @@ -642,7 +638,22 @@ def _build( ) total_nodes = len(self.model.graph.node) + log.info(f"[BuildSimulation] Preparing to build {total_nodes} nodes for the simulation.") futures: dict[int, Future] = {} + built_nodes = 0 + + # Progress display callback + def _callback_progress(name: str) -> Callable: + nonlocal total_nodes, built_nodes + def _f(f: Future) -> None: + nonlocal total_nodes, built_nodes + built_nodes += 1 + log.info(f"[ [bold green]{int(100.0*float(built_nodes)/float(total_nodes))}%[/bold green]" + f" ] {name}", extra={"markup": True, "highlighter": None}) + # Unpack result once so that the pool fails immediately, instead of waiting for + # all futures to be completed. + f.result() + return _f # Build sims in parallel synth_workers = max( @@ -655,20 +666,30 @@ def _build( # Build (stitched IP, cmake, make) all sims in parallel and return paths to # the compiled executables - with DisabledLoggingConsole(), self.progress_bar if with_live_display else nullcontext(): - self.progress_bar.progress.console.log( - f"Building simulations using {int(synth_workers)} workers.." - ) - with ThreadPoolExecutor(max_workers=synth_workers) as pool: - for i in range(total_nodes): - node_name = self.model.graph.node[i].name - futures[i] = pool.submit( - _build, - i, - total_nodes, - Path(make_build_dir(f"rtlsim_{node_name}")), - ) - return {i: future.result() for i, future in futures.items()} + log.info("[BuildSimulation] Starting the build process.") + with ThreadPoolExecutor(max_workers=synth_workers) as pool: + for i in range(total_nodes): + node_name = self.model.graph.node[i].name + futures[i] = pool.submit( + _build, + i, + total_nodes, + Path(make_build_dir(f"rtlsim_{node_name}")), + ) + futures[i].add_done_callback(_callback_progress(node_name)) + pool.shutdown(wait=True) + + # Check if all binaries were compiled successfully + binaries = {i: future.result() for i, future in futures.items()} + not_found_binaries = [] + for i, binary in binaries.items(): + if binary is None: + not_found_binaries.append(i) + if len(not_found_binaries) > 0: + raise FINNInternalError("Building simulations failed. " + "Failed simulation binaries: " + ", ".join(not_found_binaries)) + return binaries + def build_simulation(self, with_live_display: bool, functional_sim: bool) -> dict[int, Path]: """Build a simulation of the given type, return the path to the executable directory @@ -681,14 +702,6 @@ def build_simulation(self, with_live_display: bool, functional_sim: bool) -> dic with_live_display: If True, display a live progress-bar. functional_sim: If True, use functional simulation (faster but takes some time to build) """ - node_count = len(self.model.graph.node) - self.progress_bar = ThreadsafeProgressDisplay( - ["StitchedIP"], - [node_count], - [ - "[bold blue](1)[/bold blue] Creating stitched IPs", - ], - ) return self._build_simulations_parallel(with_live_display, functional_sim) @@ -711,18 +724,22 @@ def __init__( def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Build / compile the model. Modifies the model.""" self.model = model - with DisabledLoggingConsole() as console: # noqa - with console.status("Preparing model for the simulation step..."): - self._prepare_model() + log.info("[BuildSimulation] Starting model preparation.") + self._prepare_model() - # Check if we already have stitched IPs and built simulations. If so, rerun cmake/make + # Check if we already have stitched IPs and built simulations. If so, rerun only cmake/make needs_rebuild = True sim_binaries = self.model.get_metadata_prop("simulation_binaries") + + # 1. Check if binary paths are saved in the model if sim_binaries is not None: sim_binaries = sim_binaries.split("\n") + + # 2. Check that the model size hasn't changed since creating the binaries. Otherwise + # we should rebuild. if len(sim_binaries) != len(self.model.graph.node): log.info( - f"Found existing binaries, but number ({len(sim_binaries)}) " + f"[BuildSimulation] Found existing binaries, but number ({len(sim_binaries)}) " f"does not match number of nodes in the graph " f"({len(self.model.graph.node)}). Rebuilding..." ) @@ -730,8 +747,10 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: log.info("Existing simulations found. Re-running only CMake/Make..") needs_rebuild = False else: - log.info("No simulation binaries found, building now.") + log.info("[BuildSimulation] No simulation binaries found, building now.") + # If needed, call the Builder to create the layer simulation binaries. + # This creates both the isolated and connected binaries in one go. if needs_rebuild: self.builder = SimulationBuilder(self.model, self.fpgapart, self.clk_ns) sys.stdout = sys.stdout.console # type: ignore @@ -743,8 +762,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: "simulation_binaries", "\n".join([str(p) for p in self.binaries.values()]) ) else: - - def _compile(binary: Path, progress: ThreadsafeProgressDisplay) -> None: + # Run only compilation again, and avoid repeating building of the stitched IPs + def _compile(binary: Path) -> None: result = subprocess.run( "cmake .;make", shell=True, @@ -754,35 +773,50 @@ def _compile(binary: Path, progress: ThreadsafeProgressDisplay) -> None: ) if result.returncode != 0: raise FINNUserError(f"Failed compilation in {binary}: {result.stderr}") - progress.update("Compilation") + # Since we dont need a rebuild, sim_binaries contains the paths to the binaries sim_binaries = [Path(p) for p in sim_binaries] - try: - sys.stdout = sys.stdout.console # type: ignore - sys.stderr = sys.stderr.console # type: ignore - except AttributeError: - pass - with DisabledLoggingConsole() as cons: # noqa - progress = ThreadsafeProgressDisplay( - ["Compilation"], [len(sim_binaries)], ["Compilation"] - ) - progress.start() - futures = [] - with ThreadPoolExecutor(int(os.environ.get("NUM_DEFAULT_WORKERS", "8"))) as tpe: - for binary in sim_binaries: - futures.append(tpe.submit(_compile, binary, progress)) - tpe.shutdown() - progress.stop() - for future in futures: + total = len(sim_binaries) + + # Prepare compiling the binaries again + done = 0 + def _progress_callback(binary: str | Path) -> Callable: + nonlocal done, total + def _f(future: Future) -> None: + nonlocal done, total + done += 1 + log.info( + f"[ [bold green]{int(100.0*float(done)/float(total))}%[/bold green] ] " + f"Simulation [green italic]{binary}[/green italic] built.", + extra={"markup": True, "highlighter": None} + ) future.result() - log.info("Compilation done.") + return _f + + # Run the compilation in parallel with the number of workers specified. + # If not specified, use 8 + compile_start = time.time() + futures: list[Future] = [] + with ThreadPoolExecutor(int(os.environ.get("NUM_DEFAULT_WORKERS", "8"))) as tpe: + for binary in sim_binaries: + futures.append(tpe.submit(_compile, binary)) + futures[-1].add_done_callback(_progress_callback(binary.name)) + tpe.shutdown() + compile_end = time.time() + log.info(f"Compilation done. Took {compile_end - compile_start} seconds") return self.model, False def _prepare_model(self) -> None: """Execute some preparation transformations on the model.""" + log.info("[BuildSimulation] Inserting DataWidthConverters...") self.model = self.model.transform(InsertDWC()) + log.info("[BuildSimulation] Specializing layers...") self.model = self.model.transform(SpecializeLayers(self.fpgapart)) + log.info("[BuildSimulation] Assigning unique and readable node and tensor names...") self.model = self.model.transform(GiveUniqueNodeNames()) self.model = self.model.transform(GiveReadableTensorNames()) + log.info("[BuildSimulation] Preparing IPs...") self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) + log.info("[BuildSimulation] Synthesizing IPs...") self.model = self.model.transform(HLSSynthIP()) + log.info("[BuildSimulation] Model preparation done.") diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index d188225b4e..adb123985f 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -25,6 +25,7 @@ from finn.util.logging import DisabledLoggingConsole, log + class NodeConnectedSimulationController(SimulationController): """Run simulations for node connected cases.""" @@ -67,7 +68,7 @@ def _cleanup_shm_resources(self) -> None: pass if removed_count > 0: - self.console.log(f"Cleaned up {removed_count} existing shared memory resources") + log.info(f"Cleaned up {removed_count} existing shared memory resources") except Exception as e: # Don't fail if cleanup fails - just log it self.console.log(f"Warning: Error during shared memory cleanup: {e}") @@ -256,7 +257,7 @@ def _print(msg: str, color: str = "green") -> None: color = "orange3" if "ERROR" in msg: color = "red" - self.console.log( + log.debug( f"[bold {color}]{name:<35}" f"[/bold {color}][cornflower_blue]{process_index} " f"/ {len(self.names) - 1}[/cornflower_blue] {msg:<35}" @@ -417,13 +418,12 @@ def simulate( # Run simulation start = time.time() output_json = Path(make_build_dir("simulation_results_")) / "simulation_data.json" - with DisabledLoggingConsole() as console: - controller = NodeConnectedSimulationController( - len(self.binaries), names, list(self.binaries.values()), console, 0.1, False - ) - controller.run(initial_depth, output_json, max_cycles) + controller = NodeConnectedSimulationController( + len(self.binaries), names, list(self.binaries.values()), Console(), 0.1, False + ) + controller.run(initial_depth, output_json, max_cycles) end = time.time() - log.info(f"Simulation took {end - start} seconds!") + log.debug(f"Simulation took {end - start} seconds!") # Load the merged data from JSON merged_data = json.loads(output_json.read_text()) @@ -473,8 +473,20 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: ) model = sim.model # TODO:clean up + # Running the initial simulation + log.info("Running initial node-connected simulation.") initial_fifo_depths, _ = sim.simulate() + # Store the initial sizes as a report + initial_sizes_path = ( + Path(self.cfg.output_dir) + / "report" + / "initial_fifo_sizes_sim_connected.json" + ) + initial_sizes_path.write_text(json.dumps(initial_fifo_depths, indent=4)) + log.info(f"Wrote initial sizes to: {initial_sizes_path}") + + fifo_depths = [] # Each entry is a list of fifo sizes for that node for val in initial_fifo_depths.values(): fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) @@ -492,6 +504,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: else: raise FINNInternalError("Non-HW node found in dataflow graph during simulation") + log.info("Minimizing layers...") needs_minimization = [] for i in range(len(fifo_depths)): needs_minimization.append([True] * len(fifo_depths[i])) @@ -510,10 +523,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Minimize FIFO depths using binary search over BRAM block counts for i in range(len(fifo_depths)): for j in range(len(fifo_depths[i])): - log.info(f"Minimizing Layer {i} / {len(fifo_depths)} " - f"(FIFO {j} / {len(fifo_depths[i])})") if not needs_minimization[i][j]: - log.info("Skipping minimization for this stream.") + log.debug(f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Skipping minimization for this stream.") continue minimized_depth = self._minimize_fifo_depth( @@ -527,19 +538,24 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: sim_cycles, ) fifo_depths[i][j] = minimized_depth + percentage = int(100.0 * float(i+1) / float (len(fifo_depths))) + log.info(f"[ [bold green]{percentage}%[/bold green] ] " + f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed.", + extra={"markup": True, "highlighter": None}) - print("Final FIFO depths:") + log.info("Final FIFO depths:") for i in range(len(fifo_depths)): - print(f"{i}: {fifo_depths[i]}") log.info(f"{i}: {fifo_depths[i]}") # Write back results. By default write to output_dir / "fifo_config.json" + writeback_path = Path(self.cfg.output_dir) / "fifo_config.json" assert len(fifo_depths) == len(model.graph.node) json_results = {} for i in range(len(fifo_depths)): json_results[i] = {"node": model.graph.node[i].name, "depths": fifo_depths[i]} - with (Path(self.cfg.output_dir) / "fifo_config.json").open("w") as f: + with writeback_path.open("w") as f: json.dump(json_results, f) + log.info(f"Wrote results back to {writeback_path}") return model, False @@ -645,8 +661,7 @@ def _minimize_fifo_depth( original_size = baseline_depths[node_idx][fifo_idx] bw = bit_widths[node_idx][fifo_idx] - print(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") - log.info(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") + log.debug(f"Minimizing Node {node_idx + 1} FIFO {fifo_idx + 1}: original depth {original_size}") # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs success, timeout = self._test_depth( diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index cafae7e911..4a82f63747 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -3,11 +3,13 @@ import json import time from concurrent.futures import Future, ThreadPoolExecutor +from threading import Lock from pathlib import Path, PosixPath, PurePath from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation from rich.console import Console from typing import Literal, TypeAlias +from collections.abc import Callable from finn.transformation.fpgadataflow.simulation import Simulation from finn.transformation.fpgadataflow.simulation_build import SimulationType @@ -17,6 +19,7 @@ def get_time() -> str: + """Return the current time in a formatted hour:minutes:second string.""" return f"[{time.strftime('%H:%M:%S')}]" @@ -38,7 +41,7 @@ def __init__( super().__init__( parallel_simulations, names, binaries, console, poll_interval, with_progressbar ) - self.console.log("Started simulation controller") + log.info("Started simulation controller") def get_logfile_path(self, binary_or_idx: Path | int) -> Path: """Get the logfile for the given binary or process index.""" @@ -48,7 +51,7 @@ def get_logfile_path(self, binary_or_idx: Path | int) -> Path: f"{self.names[binary_or_idx]}_python.txt" ) elif type(binary_or_idx) in [Path, PurePath, PosixPath]: # noqa - process_idx = self.binaries.index(binary_or_idx) + process_idx = self.binaries.index(binary_or_idx) # type: ignore return self.logdir / f"{process_idx}_log_isolated_{self.names[process_idx]}_python.txt" raise TypeError("Pass either a simulation binary path of an index") @@ -58,7 +61,7 @@ def write_log(self, logfile: io.TextIOWrapper, msg: str, flush: bool = True) -> if flush: logfile.flush() - def postprocess_logs( + def collect_results( self, d: Path, readylog_name: str = "readylog.txt", validlog_name: str = "validlog.txt" ) -> IsolatedSimLogData: """Recieve the directory containing a binary and the simulation logs. @@ -78,27 +81,47 @@ def run(self) -> dict[str, IsolatedSimLogData]: """Run a node isolated simulation and return the collected input ready / output valid data, indexed based on node names.""" futures: list[Future] = [] + data: dict[str, self.IsolatedSimLogData] = {} + datalock = Lock() + total = len(self.binaries) + done = 0 + + ## Callback to show progress and save the simulation result + def _done_callback_generator(name: str) -> Callable: + nonlocal total, done, data, datalock + def _f(future: Future) -> None: + nonlocal total, done, data, datalock + with datalock: + done += 1 + log.info(f"[ [bold green]{int(100 * float(done)/float(total))}%[/bold green] ] {name} done!", + extra={"markup": True, "highlighter": None}) + data[name] = future.result() + return _f + + # Running the simulation threads + assert len(self.names) == len(self.binaries) with self.console.status(f"Running simulation on every node. Log directory: {self.logdir}"): start = time.time() with ThreadPoolExecutor(len(self.binaries)) as tpe: - for binary in self.binaries: + for i, binary in enumerate(self.binaries): futures.append(tpe.submit(self._run_binary, binary)) - tpe.shutdown(wait=True) - self.console.log("Thread pool closed. Closing sockets and postprocessing data") - elapsed = time.strftime("%Hh %Mm %Ss", time.gmtime(time.time() - start)) - self.console.log(f"Simulations took {elapsed}") + futures[-1].add_done_callback(_done_callback_generator(self.names[i])) + tpe.shutdown(wait=True) + elapsed = time.strftime("%Hh %Mm %Ss", time.gmtime(time.time() - start)) + self.console.log("Thread pool closed. Closing sockets and postprocessing data") + self.console.log(f"Simulations took {elapsed}") + + # Finish the logs and clean up the sockets for binary in self.binaries: with self.get_logfile_path(binary).open("a") as logfile: self.write_log(logfile, "Cleaning up socket.") self._cleanup_sockets() - # Read data - data: dict[str, self.IsolatedSimLogData] = {} + # Check for invalid data points invalid = [] - for i, future in enumerate(futures): - data[self.names[i]] = future.result() - if data[self.names[i]] is None: - invalid.append((self.names[i], i)) + for i, name in enumerate(data.keys()): + if data[name] is None: + invalid.append((name, i)) if len(invalid) > 0: raise FINNInternalError( f"Lost connection / malformed response from nodes: " @@ -106,28 +129,27 @@ def run(self) -> dict[str, IsolatedSimLogData]: ) return data + def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: - """Run simulation. Returning None if connection is lost.""" + """Thread routine: Run a single simulation from the given path and return + the collected results. Returns None if connection is lost.""" process_index = self.binaries.index(binary) with self.get_logfile_path(binary).open("w+") as logfile: - + # Logging helper def write_log(msg: str) -> None: self.write_log(logfile, msg) - # Initialize + # Initialize: Start simulation process and give the start command write_log("Initializing simulation") write_log(f"Binary is: {binary}") proc_idx = self._start_process(binary, process_index) response = self._send_and_receive(proc_idx, "start", {}) if response is None: - write_log("Client disconnected / no answer received to start command!") + write_log("No answer for the clients 'start' " + "command received. Timeout or disconnect.") return None write_log(f"Start response: {response}") - if response is None: - write_log("Failed to start simulation: No response") - return None - # Main loop write_log("Beginning main loop") logfile.flush() @@ -142,27 +164,36 @@ def write_log(msg: str) -> None: # Process response if response is None: + write_log("Status request answered with None: Timeout or connection lost.") return None state = response["state"] write_log(f"Received answer for status request ({total_status_requests})") + + # If the simulation is done, postprocess and return the collected data if state == "done": - self.console.log(f"{process_index} is done and postprocessing data.") write_log("Received done status. Sending stop signal to simulation.") resp = self._send_and_receive(proc_idx, "stop", {}) if resp is None: write_log("No stop response received.") else: write_log("Stop successfully received.") - return self.postprocess_logs(binary.parent) - - # TODO: Order seems wrong - write_log( - f"{response['totalCycles']}, " - f"{response['inputCyclesDone']}, " - f"{response['inputCyclesTarget']}, " - f"{response['outputCyclesDone']}, " - f"{response['outputCyclesTarget']}" - ) + return self.collect_results(binary.parent) + + # Otherwise log the current status + # TODO: Field name - meaning wrong? + in_done = response["inputCyclesDone"] + in_target = response["inputCyclesTarget"] + out_done = response["outputCyclesDone"] + out_target = response["outputCyclesTarget"] + total_cycles = response["totalCycles"] + percent_simulated_input = int(100.0 * float(in_done) / float(in_target)) + percent_simulated_output = int(100.0 * float(out_done) / float(out_target)) + write_log("Status response:") + write_log(f"\tTotal cycles: {total_cycles}") + write_log(f"\tInput data simulated: {percent_simulated_input}% " + f"({in_done} / {in_target})") + write_log(f"\tOutput data simulated: {percent_simulated_output}% " + f"({out_done} / {out_target})") FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] @@ -191,11 +222,11 @@ def simulate(self) -> IsoSimLogDataByLayer: f"{self.simulation_type}" ) names = [node.name for node in self.model.graph.node] - with DisabledLoggingConsole() as console: - controller = NodeIsolatedSimulationController( - len(self.binaries), names, list(self.binaries.values()), console, 0.1, False - ) - return controller.run() + console = Console() + controller = NodeIsolatedSimulationController( + len(self.binaries), names, list(self.binaries.values()), console, 0.1, False + ) + return controller.run() class RunLayerIsolatedSimulation(Transformation): From 64ac0076d981020cf3e50c19c175a60814a5058c Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 11 Feb 2026 14:43:03 +0100 Subject: [PATCH 065/170] Use ordered_json to keep order in simulations in cases where multiple streams are used --- finn_xsi/finn_xsi/include/IsolatedSimulation.hpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index 0e00691691..99abbdc66f 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -7,8 +7,8 @@ class IsolatedSimulation : public Simulation enum class LogType {READY, VALID}; std::string readylogName; std::string validlogName; - json readyJson; - json validJson; + nlohmann::ordered_json readyJson; + nlohmann::ordered_json validJson; std::vector inJobSizes; std::vector outJobSizes; @@ -80,7 +80,7 @@ class IsolatedSimulation : public Simulation /** Log the ready and valid signals to the JSON fields **/ void logReady() { - json j; + nlohmann::ordered_json j; j["totalCycles"] = simState.totalCycles; j["inputCyclesDone"] = simState.inputCyclesDone; j["inputCyclesTarget"] = simState.inputCyclesTarget; @@ -91,7 +91,7 @@ class IsolatedSimulation : public Simulation } void logValid() { - json j; + nlohmann::ordered_json j; j["totalCycles"] = simState.totalCycles; j["outputCyclesDone"] = simState.outputCyclesDone; j["outputCyclesTarget"] = simState.outputCyclesTarget; From 3d9ceba132681613215e4f16fdd8ecd03946eafc Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 12 Feb 2026 09:31:05 +0100 Subject: [PATCH 066/170] Get ResNet to work --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 23 +++++++++-------- .../InterprocessCommunicationChannel.hpp | 12 +++++++++ finn_xsi/finn_xsi/include/Simulation.hpp | 25 ++++++++++++++++--- .../fpgadataflow/simulation_build.py | 12 ++++----- 4 files changed, 53 insertions(+), 19 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 658da82a9a..09810f7085 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -34,7 +34,8 @@ enum class SimulationState { IDLE, CONFIGURED, RUNNING, FINISHED, ERROR }; class SimulationController { private: - SingleNodeSimulation& sim; + SingleNodeSimulation& sim; std::atomic state{SimulationState::IDLE}; std::atomic current_cycles{0}; std::atomic current_samples{0}; @@ -46,7 +47,8 @@ class SimulationController { bool timeout_occurred{false}; public: - explicit SimulationController(SingleNodeSimulation& simulation) + explicit SimulationController(SingleNodeSimulation& simulation) : sim(simulation) {} void configure(const std::vector& depths, std::size_t maxCycles) { @@ -266,14 +268,6 @@ int main(int argc, const char* argv[]) { std::cout << "Connected Simulation Node Index: " << RTLSimConfig::NodeIndex << " / " << RTLSimConfig::TotalNodes << std::endl; - // Construct simulation - SingleNodeSimulation sim( - RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, "xsim_log_file.txt", "trace_file.txt", RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs, - RTLSimConfig::inputInterfaceNames, RTLSimConfig::outputInterfaceNames, 2); - - // Create simulation controller - SimulationController controller(sim); - // Check if socket communication is enabled if (vm.count("socket")) { const std::string socket_path = vm["socket"].as(); @@ -290,6 +284,15 @@ int main(int argc, const char* argv[]) { std::cout << "Socket server initialized, waiting for commands..." << std::endl; std::cout.flush(); + // Construct simulation + SingleNodeSimulation + sim(RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, "xsim_log_file.txt", "trace_file.txt", RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs, + RTLSimConfig::inputInterfaceNames, RTLSimConfig::outputInterfaceNames, 2); + + // Create simulation controller + SimulationController controller(sim); + // Command processing loop while (true) { auto request = server.receive_message(); diff --git a/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp b/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp index adaaa96f90..e8ba6d85fc 100644 --- a/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp +++ b/finn_xsi/finn_xsi/include/InterprocessCommunicationChannel.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #ifndef CACHE_LINE_SIZE #ifdef __cpp_lib_hardware_interference_size @@ -74,14 +75,17 @@ class InterprocessCommunicationChannel { // Sender creates shared memory bip::shared_memory_object::remove(sharedMemoryName.c_str()); shmem = bip::managed_shared_memory(bip::create_only, sharedMemoryName.c_str(), SharedMemorySize); + std::cout << "Created shared memory: " << sharedMemoryName << std::endl; } else { // Receiver opens existing shared memory + std::cout << "Waiting to connect to shared memory: " << sharedMemoryName << std::endl; while (true) { try { shmem = bip::managed_shared_memory(bip::open_only, sharedMemoryName.c_str()); break; } catch (const bip::interprocess_exception& e) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } + std::cout << "Connected to shared memory: " << sharedMemoryName << std::endl; } // Construct or find the reference counter @@ -91,17 +95,25 @@ class InterprocessCommunicationChannel { // Construct the channel data in shared memory channel = shmem.find_or_construct("ChannelData")(); + } + + void handshake() { // Perform handshake to verify communication works if constexpr (IsSender) { // Sender: send test request and wait for response + std::cout << "Sending handshake test request for " << sharedMemoryName << std::endl; Request test_request{}; Response test_response = send_request(test_request); + std::cout << "Received handshake test response for " << sharedMemoryName << std::endl; // Communication verified if we got here without hanging } else { // Receiver: wait for test request and send response + std::cout << "Waiting for handshake test request for " << sharedMemoryName << std::endl; Request test_request = receive_request(); + std::cout << "Received handshake test request for " << sharedMemoryName << std::endl; Response test_response{}; send_response(test_response); + std::cout << "Sent handshake test response for " << sharedMemoryName << std::endl; // Communication verified if we got here } } diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 5737c7258d..e9cd370856 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -177,11 +177,12 @@ class SingleNodeSimulation : public Simulation _istream_descs, std::array _ostream_descs, - std::array inputInterfaceNames, std::array outputInterfaceNames, unsigned int initialFIFODepth = 2) + std::array inputInterfaceNames, std::array outputInterfaceNames, + unsigned int initialFIFODepth = 2) : Simulation(kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs) { if (!FirstNode && inputInterfaceNames.empty()) { throw std::runtime_error("Cannot communicate with predecessor because previous node name was not given!"); - } + } if (!LastNode && outputInterfaceNames.empty()) { throw std::runtime_error( "Cannot communicate with successor because " @@ -195,6 +196,8 @@ class SingleNodeSimulation : public Simulation ModelWrapper: ) # Setup new input tensors new_input_info = onnx.helper.make_tensor_value_info( - info.name + "_" + str(i), + info.name, TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_input_shape(i)), ) new_input_dummy_info = onnx.helper.make_tensor_value_info( - info.name + "_dummy_" + str(i), + info.name + "_dummy", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_input_shape(i)), ) @@ -225,12 +225,12 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ) # Setup new input tensors new_output_info = onnx.helper.make_tensor_value_info( - info.name + "_" + str(i), + info.name, TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_output_shape(i)), ) new_output_dummy_info = onnx.helper.make_tensor_value_info( - info.name + "_dummy_" + str(i), + info.name + "_dummy", TensorProto.FLOAT, cast("Sequence[int]", target_op.get_normal_output_shape(i)), ) @@ -665,8 +665,8 @@ def _build( futures[i] = pool.submit( _build, i, - total_nodes, - Path(make_build_dir(f"rtlsim_{node_name}")), + total_nodes - 1, + Path(make_build_dir(f"rtlsim_{node_name}_")), ) return {i: future.result() for i, future in futures.items()} From f5532dc216d0d55259d859a7148b817d5e0d11d3 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 12 Feb 2026 12:05:13 +0100 Subject: [PATCH 067/170] Added dataframe storage of sim data. Also moved model preparation to not be run unnecessarily --- finn_xsi/finn_xsi/src/AXIS_Control.cpp | 15 +- .../transformation/fpgadataflow/simulation.py | 55 ++++- .../fpgadataflow/simulation_build.py | 36 +-- .../fpgadataflow/simulation_connected.py | 30 ++- .../fpgadataflow/simulation_isolated.py | 205 +++++++++++++++--- 5 files changed, 278 insertions(+), 63 deletions(-) diff --git a/finn_xsi/finn_xsi/src/AXIS_Control.cpp b/finn_xsi/finn_xsi/src/AXIS_Control.cpp index 758c4ca037..6aaca6d901 100644 --- a/finn_xsi/finn_xsi/src/AXIS_Control.cpp +++ b/finn_xsi/finn_xsi/src/AXIS_Control.cpp @@ -16,16 +16,25 @@ std::string sanitize_prefix(const std::string& prefix) { return sanitized; } +std::string remove_trailing_underscore(const std::string& prefix) { + std::string clean = prefix; + if (clean.back() == '_') { + // If checks implicitly that pop_back() doesn't have undefined behav. + clean.pop_back(); + } + return clean; +} + AXIS_Control::AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, const std::string& prefix) : job_size(job_sz), job_txns(0), total_txns(0), first_complete(0), - name(sanitize_prefix(prefix)), + name(remove_trailing_underscore(sanitize_prefix(prefix))), design(&des), clk(&clock), - port_vld(&des.getPort(name + "tvalid")), - port_rdy(&des.getPort(name + "tready")) {} + port_vld(&des.getPort(sanitize_prefix(prefix) + "tvalid")), + port_rdy(&des.getPort(sanitize_prefix(prefix) + "tready")) {} void AXIS_Control::inititialized_or_throw() { if (!design || !clk || !port_rdy || !port_vld) { diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index db382fa739..358e41663d 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,6 +1,7 @@ """Manages the Simulation superclass as well as general simulation related transforms.""" import json +import pandas as pd from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp @@ -25,8 +26,51 @@ FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] +def store_fifo_data( + model: ModelWrapper, + data: pd.DataFrame, + default_path: Path, + delete_existing: bool, + merge_on: list[str] | None = None, +) -> ModelWrapper: + """Store the given dataframe in a CSV file. + + If the model already points to data, merge with it and store at the + path used before (unless delete_existing=True, then simply overwrite at that same path). + If no data is stored beforehand, use the `default_path` and simply store + the data there. The path is then entered into the `"fifo_data_path"` metadata prop of the model. + + The function can be used to aggregate benchmarking data across several flow steps. + + Args: + model: The model that we check for a path to existing FIFO data. + data: The data to store. + default_path: Path to use in case that the model doesn't reference a data file yet. + Is then stored as a metadata prop in the model. + delete_existing: If true, delete the table and start a new one. + merge_on: What columns to merge on. If "None", use `["node", "stream"]` + + Returns: + model: Return the model since we might have modified its metadata. + """ + fifo_data_path = model.get_metadata_prop("fifo_data_path") + if fifo_data_path is not None: + if delete_existing: + merged = data + else: + merged = pd.merge(data, pd.read_csv(fifo_data_path), on=merge_on, how="outer") + merged.to_csv(fifo_data_path) + log.info(f"Stored FIFO dataframe to {fifo_data_path}.") + else: + data.to_csv(default_path) + model.set_metadata_prop("fifo_data_path", str(default_path)) + log.info(f"Stored FIFO dataframe to {default_path}.") + return model + + class Simulation: """Manage simulation (runs) in FINN. Upon instance creation, the simulation will be built. + Simulations should inherit from this class and expand for their specific needs. IMPORTANT: If the modelwrapper was somehow changed, create a NEW simulation object! """ @@ -65,20 +109,16 @@ def __init__( # TODO: Currently we have to recompile even if we just # TODO: called BuildSimulation in the step before # (However this only compiles, it should NOT stitch the IPs again) - self.model = self.model.transform( - BuildSimulation(fpgapart, clk_ns, functional_sim) - ) + self.model = self.model.transform(BuildSimulation(fpgapart, clk_ns, functional_sim)) self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} match simulation_type: case SimulationType.NODE_BASED_CONNECTED: self.binaries = { - i: self.binaries[i] / "LayerSimulationBackend" - for i in self.binaries.keys() + i: self.binaries[i] / "LayerSimulationBackend" for i in self.binaries.keys() } case SimulationType.NODE_BASED_ISOLATED: self.binaries = { - i: self.binaries[i] / "IsolatedSimulationBackend" - for i in self.binaries.keys() + i: self.binaries[i] / "IsolatedSimulationBackend" for i in self.binaries.keys() } case _: raise FINNInternalError(f"Unsupported simulation type: {simulation_type}") @@ -90,7 +130,6 @@ def __init__( if len(errors) > 0: raise FINNInternalError("Errors occurred: \n" + "\n\t".join(errors)) - def simulate(self) -> Any: raise NotImplementedError("Call simulate() on subclasses.") diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 9e5a96c610..f44c57ce0a 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -3,26 +3,26 @@ import finn_xsi.adapter as finnxsi import numpy as np import onnx -import time import os import psutil import shlex import subprocess import sys +import time +from ast import literal_eval +from collections.abc import Callable from concurrent.futures import Future, ThreadPoolExecutor from enum import Enum from onnx import NodeProto, TensorProto, ValueInfoProto from pathlib import Path -from qonnx.util.basic import get_by_name from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames from qonnx.transformation.infer_shapes import InferShapes -from ast import literal_eval +from qonnx.util.basic import get_by_name from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, cast -from collections.abc import Callable from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -292,7 +292,7 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: node_model.set_metadata_prop("input_node", str(input_node).lower()) node_model.set_metadata_prop("output_node", str(output_node).lower()) - #node_model.save(f"isolated_node_model_{self.model.graph.node[index].name}.onnx") + # node_model.save(f"isolated_node_model_{self.model.graph.node[index].name}.onnx") return node_model @@ -645,14 +645,20 @@ def _build( # Progress display callback def _callback_progress(name: str) -> Callable: nonlocal total_nodes, built_nodes + def _f(f: Future) -> None: nonlocal total_nodes, built_nodes built_nodes += 1 - log.info(f"[ [bold green]{int(100.0*float(built_nodes)/float(total_nodes))}%[/bold green]" - f" ] {name}", extra={"markup": True, "highlighter": None}) + log.info( + f"[ [bold green]" + f"{int(100.0*float(built_nodes)/float(total_nodes))}%[/bold green]" + f" ] {name}", + extra={"markup": True, "highlighter": None}, + ) # Unpack result once so that the pool fails immediately, instead of waiting for # all futures to be completed. f.result() + return _f # Build sims in parallel @@ -686,11 +692,12 @@ def _f(f: Future) -> None: if binary is None: not_found_binaries.append(i) if len(not_found_binaries) > 0: - raise FINNInternalError("Building simulations failed. " - "Failed simulation binaries: " + ", ".join(not_found_binaries)) + raise FINNInternalError( + "Building simulations failed. " + "Failed simulation binaries: " + ", ".join(not_found_binaries) + ) return binaries - def build_simulation(self, with_live_display: bool, functional_sim: bool) -> dict[int, Path]: """Build a simulation of the given type, return the path to the executable directory (indexed by the corresponding node index in the graph). @@ -724,8 +731,6 @@ def __init__( def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Build / compile the model. Modifies the model.""" self.model = model - log.info("[BuildSimulation] Starting model preparation.") - self._prepare_model() # Check if we already have stitched IPs and built simulations. If so, rerun only cmake/make needs_rebuild = True @@ -752,6 +757,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # If needed, call the Builder to create the layer simulation binaries. # This creates both the isolated and connected binaries in one go. if needs_rebuild: + log.info("[BuildSimulation] Starting model preparation.") + self._prepare_model() self.builder = SimulationBuilder(self.model, self.fpgapart, self.clk_ns) sys.stdout = sys.stdout.console # type: ignore self.binaries = self.builder.build_simulation( @@ -780,17 +787,20 @@ def _compile(binary: Path) -> None: # Prepare compiling the binaries again done = 0 + def _progress_callback(binary: str | Path) -> Callable: nonlocal done, total + def _f(future: Future) -> None: nonlocal done, total done += 1 log.info( f"[ [bold green]{int(100.0*float(done)/float(total))}%[/bold green] ] " f"Simulation [green italic]{binary}[/green italic] built.", - extra={"markup": True, "highlighter": None} + extra={"markup": True, "highlighter": None}, ) future.result() + return _f # Run the compilation in parallel with the number of workers specified. diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index adb123985f..7b032dbec6 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -22,8 +22,7 @@ from finn.transformation.fpgadataflow.simulation_controller import SimulationController from finn.util.basic import make_build_dir from finn.util.exception import FINNInternalError, FINNUserError -from finn.util.logging import DisabledLoggingConsole, log - +from finn.util.logging import log class NodeConnectedSimulationController(SimulationController): @@ -474,19 +473,19 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: model = sim.model # TODO:clean up # Running the initial simulation + + # raise NotImplementedError() + log.info("Running initial node-connected simulation.") initial_fifo_depths, _ = sim.simulate() # Store the initial sizes as a report initial_sizes_path = ( - Path(self.cfg.output_dir) - / "report" - / "initial_fifo_sizes_sim_connected.json" + Path(self.cfg.output_dir) / "report" / "initial_fifo_sizes_sim_connected.json" ) initial_sizes_path.write_text(json.dumps(initial_fifo_depths, indent=4)) log.info(f"Wrote initial sizes to: {initial_sizes_path}") - fifo_depths = [] # Each entry is a list of fifo sizes for that node for val in initial_fifo_depths.values(): fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) @@ -524,7 +523,10 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: for i in range(len(fifo_depths)): for j in range(len(fifo_depths[i])): if not needs_minimization[i][j]: - log.debug(f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Skipping minimization for this stream.") + log.debug( + f"[ {i+1}.{j+1} / {len(fifo_depths)} ] " + f"Skipping minimization for this stream." + ) continue minimized_depth = self._minimize_fifo_depth( @@ -538,10 +540,12 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: sim_cycles, ) fifo_depths[i][j] = minimized_depth - percentage = int(100.0 * float(i+1) / float (len(fifo_depths))) - log.info(f"[ [bold green]{percentage}%[/bold green] ] " - f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed.", - extra={"markup": True, "highlighter": None}) + percentage = int(100.0 * float(i + 1) / float(len(fifo_depths))) + log.info( + f"[ [bold green]{percentage}%[/bold green] ] " + f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed.", + extra={"markup": True, "highlighter": None}, + ) log.info("Final FIFO depths:") for i in range(len(fifo_depths)): @@ -661,7 +665,9 @@ def _minimize_fifo_depth( original_size = baseline_depths[node_idx][fifo_idx] bw = bit_widths[node_idx][fifo_idx] - log.debug(f"Minimizing Node {node_idx + 1} FIFO {fifo_idx + 1}: original depth {original_size}") + log.debug( + f"Minimizing Node {node_idx + 1} FIFO {fifo_idx + 1}: original depth {original_size}" + ) # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs success, timeout = self._test_depth( diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 4a82f63747..ad40970f23 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -1,21 +1,23 @@ """Simulating layers on their own to observe their behaviour.""" import io import json +import pandas as pd +import re import time +from collections.abc import Callable from concurrent.futures import Future, ThreadPoolExecutor -from threading import Lock from pathlib import Path, PosixPath, PurePath from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation from rich.console import Console -from typing import Literal, TypeAlias -from collections.abc import Callable +from threading import Lock +from typing import Any, Literal, TypeAlias -from finn.transformation.fpgadataflow.simulation import Simulation +from finn.transformation.fpgadataflow.simulation import Simulation, store_fifo_data from finn.transformation.fpgadataflow.simulation_build import SimulationType from finn.transformation.fpgadataflow.simulation_controller import SimulationController from finn.util.exception import FINNInternalError -from finn.util.logging import DisabledLoggingConsole, log +from finn.util.logging import log def get_time() -> str: @@ -51,7 +53,7 @@ def get_logfile_path(self, binary_or_idx: Path | int) -> Path: f"{self.names[binary_or_idx]}_python.txt" ) elif type(binary_or_idx) in [Path, PurePath, PosixPath]: # noqa - process_idx = self.binaries.index(binary_or_idx) # type: ignore + process_idx = self.binaries.index(binary_or_idx) # type: ignore return self.logdir / f"{process_idx}_log_isolated_{self.names[process_idx]}_python.txt" raise TypeError("Pass either a simulation binary path of an index") @@ -86,16 +88,24 @@ def run(self) -> dict[str, IsolatedSimLogData]: total = len(self.binaries) done = 0 - ## Callback to show progress and save the simulation result + # TODO: Lock not needed; futures are not consumed just by + # TODO: using the callback, so we can unpack them later + + # Callback to show progress and save the simulation result def _done_callback_generator(name: str) -> Callable: nonlocal total, done, data, datalock + def _f(future: Future) -> None: nonlocal total, done, data, datalock with datalock: done += 1 - log.info(f"[ [bold green]{int(100 * float(done)/float(total))}%[/bold green] ] {name} done!", - extra={"markup": True, "highlighter": None}) + log.info( + f"[ [bold green]{int(100 * float(done)/float(total))}%" + f"[/bold green] ] {name} done!", + extra={"markup": True, "highlighter": None}, + ) data[name] = future.result() + return _f # Running the simulation threads @@ -108,8 +118,8 @@ def _f(future: Future) -> None: futures[-1].add_done_callback(_done_callback_generator(self.names[i])) tpe.shutdown(wait=True) elapsed = time.strftime("%Hh %Mm %Ss", time.gmtime(time.time() - start)) - self.console.log("Thread pool closed. Closing sockets and postprocessing data") - self.console.log(f"Simulations took {elapsed}") + log.info("Thread pool closed. Closing sockets and postprocessing data") + log.info(f"Simulations took {elapsed}") # Finish the logs and clean up the sockets for binary in self.binaries: @@ -129,7 +139,6 @@ def _f(future: Future) -> None: ) return data - def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: """Thread routine: Run a single simulation from the given path and return the collected results. Returns None if connection is lost.""" @@ -145,8 +154,9 @@ def write_log(msg: str) -> None: proc_idx = self._start_process(binary, process_index) response = self._send_and_receive(proc_idx, "start", {}) if response is None: - write_log("No answer for the clients 'start' " - "command received. Timeout or disconnect.") + write_log( + "No answer for the clients 'start' " "command received. Timeout or disconnect." + ) return None write_log(f"Start response: {response}") @@ -190,10 +200,14 @@ def write_log(msg: str) -> None: percent_simulated_output = int(100.0 * float(out_done) / float(out_target)) write_log("Status response:") write_log(f"\tTotal cycles: {total_cycles}") - write_log(f"\tInput data simulated: {percent_simulated_input}% " - f"({in_done} / {in_target})") - write_log(f"\tOutput data simulated: {percent_simulated_output}% " - f"({out_done} / {out_target})") + write_log( + f"\tInput data simulated: {percent_simulated_input}% " + f"({in_done} / {in_target})" + ) + write_log( + f"\tOutput data simulated: {percent_simulated_output}% " + f"({out_done} / {out_target})" + ) FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] @@ -231,16 +245,25 @@ def simulate(self) -> IsoSimLogDataByLayer: class RunLayerIsolatedSimulation(Transformation): """Run a layer isolated simulation and calculate some information for a - later layer parallel simulation.""" + later layer parallel simulation. - def __init__(self, fpgapart: str, clk_ns: float, functional_sim: bool, output_dir: Path) -> None: - """Run isolated layer simulations.""" + This modifies or creates a pandas DF and stores it in a csv file. This file can be + modified by the node connected simulation as well.""" + + def __init__( + self, fpgapart: str, clk_ns: float, functional_sim: bool, output_dir: Path + ) -> None: + """Run isolated layer simulations. The + default location is at cfg.output_dir/report/fifo_data.csv.""" super().__init__() self.fpgapart = fpgapart self.clk_ns = clk_ns self.functional_sim = functional_sim self.output_dir = output_dir + # Read / create dataframe with default path + self.default_fifo_data_path = self.output_dir / "report" / "fifo_data.csv" + def calculate_upper_bounds(self, data: IsoSimLogDataByLayer) -> dict[str, dict[str, int]]: """Try to calculate an upper bound for the incoming FIFO size of the layers. Return size indexed by layer name and stream name. @@ -334,9 +357,9 @@ def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: >>> data = { ... "layer1": { ... "ready": [{"totalCycles": 10, "inputCyclesDone": 5, - ... "inputCyclesTarget": 10, "s_axi0_ready": 1}], + ... "inputCyclesTarget": 10, "s_axi_0": 1}], ... "valid": [{"totalCycles": 10, "outputCyclesDone": 5, - ... "outputCyclesTarget": 10, "m_axi0_valid": 1}] + ... "outputCyclesTarget": 10, "m_axi_0": 1}] ... } ... } >>> sim = RunLayerIsolatedSimulation("", 0.0, False) @@ -440,9 +463,48 @@ def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: raise FINNInternalError(f"Layer {layer} has no ready data!") if len(ldata["valid"]) == 0: raise FINNInternalError(f"Layer {layer} has no valid data!") + # 7. Check that the order of axi streams corresponds to their names. This helps + # somewhat to guarantee that the order always stayed the same from building the simulations + # to evaluating their data + + # The number in the name should increase with every stream, from 0, without gaps + # and streams should be called "s_axis_" + readykeys = ["inputCyclesDone", "inputCyclesTarget", "totalCycles"] + for layer, ldata in data.items(): + for cycledict in ldata["ready"]: + current_stream_idx = 0 + for key in cycledict.keys(): + if key not in readykeys: + m = re.fullmatch(r"^s_axis_(\d+)$", key) + if m is None: + raise FINNInternalError( + f"Layer {layer} has a non-expected key that " + f"does not match the names of streams expected " + f"(s_axis_).\n\tKey is: {key}" + ) + stream_idx = m.group(1) + if int(stream_idx) != current_stream_idx: + raise FINNInternalError( + f"Layer {layer} has non-expected stream key " + f"that does not follow the expected index " + f"scheme: Current expected index is " + f"{current_stream_idx}. Got instead: " + f"{stream_idx}" + ) + current_stream_idx += 1 + # TODO: Check that names match vivado_stitch_ifnames. + # TODO: Currently there is no easy way to do this, since we never save the isolated + # TODO: node-models and vivado_stitch_ifnames is a metadata prop of that isolated model + + def percent_ready(self, data: IsoSimLogDataByLayer) -> dict[str, float]: + """Calculate how many percent of the time the layer was ready for input data. + Return indexed by layer name.""" + # TODO: Implement + return dict.fromkeys(data, 0) def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run isolated layer simulations.""" + # Run the simulation sim = IsolatedSimulation( model, SimulationType.NODE_BASED_ISOLATED, @@ -451,17 +513,106 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: self.functional_sim, ) data: IsoSimLogDataByLayer = sim.simulate() + + # Check if data looks good + log.info("Checking validity of received simulation data...") + start = time.time() self.sanity_check_logged_data(data) + log.info(f"Validity check took {time.time() - start} seconds.") + + # Calculate upper bounds + log.info("Estimating upper bounds...") + start = time.time() in_fifo_upper_bound = self.calculate_upper_bounds(data) - formatted_upper_bounds = "\n\t".join( - [f"{name}: {in_fifo_upper_bound[name]}" for name in in_fifo_upper_bound.keys()] - ) - log.info("Upper bounds: \n" + formatted_upper_bounds) + log.info(f"Estimation took {time.time() - start} seconds.") # Write into report file upper_bounds_file = self.output_dir / "report" / "estimate_upper_fifo_bound.json" upper_bounds_file.write_text(json.dumps(in_fifo_upper_bound, indent=4)) log.info(f"Wrote results to: {upper_bounds_file}") + # Save data into dataframe + # NOTE: We actually have to swap the order here: We recorded the _incoming_ FIFO sizes + # However the connected simulation stores the depths on the layers before it, so + # essentially _outgoing_ FIFO sizes. + + # NOTE: For this mapping to work, ordering has to be kept correctly in each step: + # 1. Mapping node.inputs to vivado_stitch_ifnames metadata prop (CreateStitchedIP) + # 2. Mapping IO shapes to ifnames from before (simulation_builder.py) + # 3. Mapping stream_descrs to M/S_AXIS_CONTROL array (C++ simulation creation) + # 4. Writing the data to json. Order of S_AXIS_CONTROL -> order in which JSON gets written + # IMPORTANT: Use nlohmann::ordered_json to keep the insertion order! + # 5. Reading the JSON into python (python dicts are ordered since 3.7) + # 6. Syncing node.inputs to order of s_axi_... streams read from the JSON. + edited_bounds = {} + + # Fill edited_bounds with empty values + for node in model.graph.node: + suc = model.find_direct_successors(node) + if suc is None: + continue + edited_bounds[node.name] = [-1] * len(suc) + + # For every node check its predecessors. + # Find the index/tensor that connects the predecessor and the current one + # Use that index to retrieve the fifo depth between them and save it + def get_index(a: Any, values: Any) -> int | None: + for i, val in enumerate(values): + if val == a: + return i + return None + + for node in model.graph.node: + # Rely on the fact that find_direct_predecessors gives the streams in-order + predecessors = model.find_direct_predecessors(node) + if predecessors is None: + continue + for predecessor in predecessors: + # Find out which m_axis stream of the predecessor leads to node + for producer_idx, pre_out in enumerate(predecessor.output): + if pre_out in node.input: + consumer_idx = get_index(pre_out, node.input) + if consumer_idx is None: + raise FINNInternalError( + f"Could not find index of " + f"{predecessor.name}'s output and " + f"{node.name}'s input: {pre_out}. " + f"Index in predecessor.output is " + f"{producer_idx}" + ) + # TODO: Switch to array instead of dict? + # We have to conver the string-key (s_axi_...) into the index of the dict + key = list(in_fifo_upper_bound[node.name].keys())[consumer_idx] + # TODO: Tests + edited_bounds[predecessor.name][producer_idx] = in_fifo_upper_bound[ + node.name + ][ + key + ] # noqa + log.info( + f"Incoming FIFO {node.name}[{key}/{consumer_idx}] " + f"-> outgoing FIFO {predecessor.name}[{producer_idx}]" + ) + + # Prepare the data + df_data = {"node": [], "stream": [], "out_fifo_upper_bound": [], "input_ready_percent": []} + for layer, layerdata in edited_bounds.items(): + for idx in range(len(layerdata)): + df_data["node"].append(layer) + df_data["stream"].append(idx) + df_data["out_fifo_upper_bound"].append(layerdata[idx]) + # TODO: Remove input_ready_percent? + df_data["input_ready_percent"].append(self.percent_ready(data)[layer]) + + # Create the DF + self.fifo_data = pd.DataFrame(df_data) + log.info("First few entries of collected data:") + log.info(str(self.fifo_data)) + + # Save in dataframe and model + model = store_fifo_data( + model, self.fifo_data, self.default_fifo_data_path, delete_existing=True + ) + # TODO: Integrate data into the layer parallel simulation return model, False From 085fbb6b8c738311743bb2a2177dba08b162f77f Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:37:59 +0100 Subject: [PATCH 068/170] Fix Insert FIFOs --- .../transformation/fpgadataflow/simulation.py | 227 +++++------------- .../fpgadataflow/simulation_connected.py | 2 +- 2 files changed, 67 insertions(+), 162 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index db382fa739..3d6b6c9d79 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -6,24 +6,16 @@ from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames -from typing import TYPE_CHECKING, Any, TypeAlias, cast +from typing import Any, TypeAlias, cast from finn.builder.build_dataflow_config import DataflowBuildConfig -from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO -from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log -if TYPE_CHECKING: - from onnx.onnx_ml_pb2 import NodeProto - - from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp - -FIFODepthConfig: TypeAlias = dict[int, dict[str, str | list[int]]] - +FIFODepthConfig: TypeAlias = dict[str, dict[str, str | list[int]]] class Simulation: """Manage simulation (runs) in FINN. Upon instance creation, the simulation will be built. @@ -65,20 +57,16 @@ def __init__( # TODO: Currently we have to recompile even if we just # TODO: called BuildSimulation in the step before # (However this only compiles, it should NOT stitch the IPs again) - self.model = self.model.transform( - BuildSimulation(fpgapart, clk_ns, functional_sim) - ) + self.model = self.model.transform(BuildSimulation(fpgapart, clk_ns, functional_sim)) self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} match simulation_type: case SimulationType.NODE_BASED_CONNECTED: self.binaries = { - i: self.binaries[i] / "LayerSimulationBackend" - for i in self.binaries.keys() + i: self.binaries[i] / "LayerSimulationBackend" for i in self.binaries.keys() } case SimulationType.NODE_BASED_ISOLATED: self.binaries = { - i: self.binaries[i] / "IsolatedSimulationBackend" - for i in self.binaries.keys() + i: self.binaries[i] / "IsolatedSimulationBackend" for i in self.binaries.keys() } case _: raise FINNInternalError(f"Unsupported simulation type: {simulation_type}") @@ -90,20 +78,19 @@ def __init__( if len(errors) > 0: raise FINNInternalError("Errors occurred: \n" + "\n\t".join(errors)) - def simulate(self) -> Any: raise NotImplementedError("Call simulate() on subclasses.") - class ApplyFIFOSizes(Transformation): - """Apply a FIFO sizing configuration to the model. If not existing, inserts FIFOs beforehand.""" + """Apply a FIFO sizing configuration to the model. + If FIFOs already exist the step is skipped.""" def __init__( self, cfg: DataflowBuildConfig, fifo_config: Path | None = None, max_qsrl_depth: int = 256, - vivado_ram_style: str = "auto", + vivado_ram_style: str = "block", ) -> None: """If given read the config json from the given path. Otherwise check in the output directory. @@ -116,164 +103,82 @@ def __init__( else: self.path = fifo_config - self.depth: FIFODepthConfig = {} + self.fifo_depths: FIFODepthConfig = {} with self.path.open() as f: - self.depth = cast("FIFODepthConfig", json.load(f)) + self.fifo_depths = cast("FIFODepthConfig", json.load(f)) def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Apply FIFO Simulation Depths to the model.""" - # TODO: Better way to check for fifos (op_type for example) if len(list(filter(lambda node: "StreamingFIFO" in node.op_type, model.graph.node))) > 0: log.warning( "It seems that StreamingFIFOs have already " "been inserted into the graph. Skipping insertion of FIFOs." ) - else: - if len(model.graph.node) != len(self.depth): - raise FINNUserError( - "There are no StreamingFIFOs in the graph, yet the number " - "of nodes and number of FIFO settings differ. There may be " - "unaccounted for nodes that have not been part of the FIFO " - "simulation. Consider re-running simulation directly before " - "applying the FIFO sizes. It might also be that your model " - "or config is outdated, in which case it is recommended to " - "re-run the entire flow from start to finish." - ) - - # Inser the FIFOs into the model - model = model.transform(InsertFIFO(True, self.max_qsrl_depth, self.vivado_ram_style)) + return model, False - # Synthesize the nodes (TODO: Remove) - model = model.transform(GiveUniqueNodeNames()) - model: ModelWrapper = model.transform(GiveReadableTensorNames()) - model = model.transform(SpecializeLayers(self.cfg._resolve_fpga_part())) # noqa - model = model.transform(GiveUniqueNodeNames()) - model: ModelWrapper = model.transform(GiveReadableTensorNames()) - model = model.transform( - PrepareIP( - fpgapart=self.cfg._resolve_fpga_part(), - clk=self.cfg.synth_clk_period_ns, # noqa - ) + if len(model.graph.node) != len(self.fifo_depths): + raise FINNUserError( + "There are no StreamingFIFOs in the graph, yet the number " + "of nodes and number of FIFO sizes differ. There may be " + "unaccounted for nodes that have not been part of the FIFO " + "simulation. Consider re-running simulation directly before " + "applying the FIFO sizes. It might also be that your model " + "or config is outdated, in which case it is recommended to " + "re-run the entire flow from start to finish." ) - model = model.transform(HLSSynthIP()) - # Sanity check to make sure fifos were inserted - inserted_fifo_count = sum( - [int("StreamingFIFO" in node.op_type) for node in model.graph.node] - ) - if inserted_fifo_count == 0: + # FIFO sizes are set as the maximum of outFIFODepth and inFIFODepth of the successor node + # Only set the outFIFODepth, because setting both is redundant as inFIFODepth defaults to 0. + # Remove all in/outFIFODepths in model for clean slate + graph = model.graph + for node in graph.node: + predecessors = model.find_direct_predecessors(node) + successors = model.find_direct_successors(node) + n = getCustomOp(node) + if n is not None: + if predecessors is not None: + n.set_nodeattr("inFIFODepths", [0] * len(predecessors)) + if successors is not None: + n.set_nodeattr("outFIFODepths", [0] * len(successors)) + + # Set new outFIFODepths according to config + graph = model.graph + node_ind = -1 + for first_node in graph.node: + node_ind += 1 + n0 = getCustomOp(first_node) + if n0 is None: raise FINNInternalError( - "No FIFOs were inserted. This may be due to " - "wrong network configuration, step order or " - "a number of other things." - ) - if inserted_fifo_count < int(0.1 * float(len(model.graph.node))): - log.warning( - "The number of inserted FIFOs makes up less than 10%" - " of the total number of nodes in the model. This could " - "point to a potential error." + f"Node {first_node.name} does not have a custom op instance." + " This is required for FIFO insertion." ) + fifos = cast("list[int]", (self.fifo_depths[str(node_ind)]["depths"])) + n0.set_nodeattr("outFIFODepths", fifos) - # Assign data based on the names of the nodes. Since no FIFOs were in the graph - # before, the names should stay the same. - # TODO: This currently assumes that the FIFO to be sized comes AFTER the actual node. - # TODO: If the simulation code is changed, this needs to be changed as well - for i in range(len(model.graph.node)): - node: NodeProto = model.graph.node[i] - node_inst: HWCustomOp = getCustomOp(model.graph.node[i]) - if node.op_type.startswith("StreamingFIFO"): - # FIFOs can only have one producer, so this must - # be the node whoose simulated depth we have to get - predecessors: list[NodeProto] | None = model.find_direct_predecessors(node) - if predecessors is not None and len(predecessors) > 1: - raise FINNInternalError(f"FIFO node {node.name} has multiple producers!") - if predecessors is None: - continue - predecessor: NodeProto = predecessors[0] - - # Check which of the predecessors outputs this FIFO is connected to - # and use the depth at that index from the simulation - for sim_node_name, sim_depths in self.depth.values(): - if sim_node_name == predecessor.name: - depth_index = predecessor.output.index(node.input[0]) - depth = int(sim_depths[depth_index]) - node_inst.set_nodeattr("depth", depth) + # Insert the FIFOs into the model + model = model.transform(InsertFIFO(True, self.max_qsrl_depth, self.vivado_ram_style)) - # TODO: Code copied from old FIFO sizing - # exception for top-level IO FIFOs which cause a bug in simulation - # (top-level IOs should not have impl_style=vivado) - toplevel_in = node.input[0] in [x.name for x in model.graph.input] - toplevel_out = node.output[0] in [x.name for x in model.graph.output] - toplevel_style_exception = toplevel_in or toplevel_out - # Set FIFO implementation/ram styles - if (depth > self.max_qsrl_depth) and (not toplevel_style_exception): - node_inst.set_nodeattr("impl_style", "vivado") - node_inst.set_nodeattr("ram_style", self.vivado_ram_style) - else: - node_inst.set_nodeattr("impl_style", "rtl") - - # TODO: Following code is copied from the old FIFO sizing. Might be shortenable - for node in model.graph.node: - if not node.op_type.startswith("StreamingFIFO"): - node_inst = getCustomOp(node) - fifodepth_in = [] - for node_inp in node.input: - prod = model.find_producer(node_inp) - if prod is None: - # no producer for this input - if node_inp in [x.name for x in model.graph.input]: - # top-level input with no FIFO - fifodepth_in.append(0) - else: - # FIFO depth attr applies only to dynamic attributes - pass - else: - # there is a producer for this input - if prod.op_type.startswith("StreamingFIFO"): - prod_inst = getCustomOp(prod) - fifodepth_in.append(prod_inst.get_nodeattr("depth")) - else: - # explicitly no FIFO on this dynamic input - fifodepth_in.append(0) - fifodepth_out = [] - for node_out in node.output: - cons = model.find_consumer(node_out) - if cons is None: - # no consumer for this output - if node_out in [x.name for x in model.graph.output]: - # top-level output with no FIFO - fifodepth_out.append(0) - else: - # FIFO depth attr applies only to dynamic attributes - pass - else: - # there is a consumer for this input - if cons.op_type.startswith("StreamingFIFO"): - cons_inst = getCustomOp(cons) - fifodepth_out.append(cons_inst.get_nodeattr("depth")) - else: - # explicitly no FIFO on this dynamic output - fifodepth_out.append(0) - node_inst.set_nodeattr("inFIFODepths", fifodepth_in) - node_inst.set_nodeattr("outFIFODepths", fifodepth_out) - - # Synthesize with the proper sizes set + model = model.transform(GiveUniqueNodeNames()) + model: ModelWrapper = model.transform(GiveReadableTensorNames()) model = model.transform(SpecializeLayers(self.cfg._resolve_fpga_part())) # noqa model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) - model = model.transform( - PrepareIP( - fpgapart=self.cfg._resolve_fpga_part(), - clk=self.cfg.synth_clk_period_ns, # noqa - ) + model: ModelWrapper = model.transform(GiveReadableTensorNames()) + + # Sanity check to make sure fifos were inserted + inserted_fifo_count = sum( + [int("StreamingFIFO" in node.op_type) for node in model.graph.node] ) - model = model.transform(HLSSynthIP()) - # model.set_metadata_prop("rtlsim_trace", "") - # model.set_metadata_prop("rtlsim_so", "") - # model.set_metadata_prop("vivado_stitch_proj", "") - # model.set_metadata_prop("wrapper_filename", "") - # model.set_metadata_prop("vivado_stitch_vlnv", "") - # model.set_metadata_prop("vivado_stitch_ifnames", "") - # model.set_metadata_prop("exec_mode", "") + if inserted_fifo_count == 0: + raise FINNInternalError( + "No FIFOs were inserted. This may be due to " + "wrong network configuration, step order or " + "a number of other things." + ) + if inserted_fifo_count < int(0.4 * float(len(model.graph.node))): + log.warning( + "The number of inserted FIFOs makes up less than 40%" + " of the total number of nodes in the model. This could " + "point to a potential error." + ) return model, False diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index adb123985f..d86542bf7a 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -22,7 +22,7 @@ from finn.transformation.fpgadataflow.simulation_controller import SimulationController from finn.util.basic import make_build_dir from finn.util.exception import FINNInternalError, FINNUserError -from finn.util.logging import DisabledLoggingConsole, log +from finn.util.logging import log From d68c0cf64b23fbccc4ff7288d8b20a6834b85e3e Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 12 Feb 2026 18:41:12 +0100 Subject: [PATCH 069/170] Finished adding all FIFO-related simulation data into a unified dataframe --- .../transformation/fpgadataflow/simulation.py | 40 ++++- .../fpgadataflow/simulation_connected.py | 149 ++++++++++++++---- .../fpgadataflow/simulation_isolated.py | 29 +++- 3 files changed, 176 insertions(+), 42 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 358e41663d..f84c7a6c36 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -31,7 +31,10 @@ def store_fifo_data( data: pd.DataFrame, default_path: Path, delete_existing: bool, + sort_on: str = "onnx_index", merge_on: list[str] | None = None, + merge_how: str = "inner", + store_html: bool = True, ) -> ModelWrapper: """Store the given dataframe in a CSV file. @@ -48,21 +51,50 @@ def store_fifo_data( default_path: Path to use in case that the model doesn't reference a data file yet. Is then stored as a metadata prop in the model. delete_existing: If true, delete the table and start a new one. - merge_on: What columns to merge on. If "None", use `["node", "stream"]` + sort_on: The column to sort on after merging. + merge_on: What columns to merge on. If "None", use `["onnx_index", "node", "stream"]` + merge_how: How to merge. Forwarded to pd.merge(). + store_html: If True, also store the data as a HTML with the same name next to the CSV. Returns: model: Return the model since we might have modified its metadata. """ + # TODO: Check if all layers are accounted for + if len(data.index) != len(model.graph.node): + raise FINNInternalError( + f"Tried storing FIFO data for {len(data.index)} " + f"values but expected {len(model.graph.node)}" + ) fifo_data_path = model.get_metadata_prop("fifo_data_path") if fifo_data_path is not None: + if not fifo_data_path.endswith(".csv"): + raise FINNInternalError( + f"It seems the model saved path to store " + f"the dataframe does not point to a csv file: {fifo_data_path}" + ) if delete_existing: + Path(fifo_data_path).unlink(missing_ok=True) merged = data else: - merged = pd.merge(data, pd.read_csv(fifo_data_path), on=merge_on, how="outer") - merged.to_csv(fifo_data_path) + merged = pd.merge( + data, pd.read_csv(fifo_data_path), on=merge_on, how=merge_how # type: ignore + ) + merged.sort_values(sort_on) + merged.to_csv(fifo_data_path, index=False) + if store_html: + merged.to_html(fifo_data_path.replace(".csv", ".html")) log.info(f"Stored FIFO dataframe to {fifo_data_path}.") else: - data.to_csv(default_path) + if not default_path.suffix == ".csv": + raise FINNInternalError( + f"It seems the provided default path to store " + f"the dataframe does not point to a csv file: {fifo_data_path}" + ) + if delete_existing: + default_path.unlink(missing_ok=True) + data.to_csv(default_path, index=False) + if store_html: + data.to_html(str(default_path).replace(".csv", ".html")) model.set_metadata_prop("fifo_data_path", str(default_path)) log.info(f"Stored FIFO dataframe to {default_path}.") return model diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 7b032dbec6..7b139a9ad6 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -5,6 +5,7 @@ import math import multiprocessing import os +import pandas as pd import time import traceback from concurrent.futures import Future, ThreadPoolExecutor @@ -14,11 +15,11 @@ from qonnx.transformation.base import Transformation from rich.console import Console from threading import Barrier -from typing import Any +from typing import Any, cast from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.transformation.fpgadataflow.simulation import Simulation, SimulationType +from finn.transformation.fpgadataflow.simulation import Simulation, SimulationType, store_fifo_data from finn.transformation.fpgadataflow.simulation_controller import SimulationController from finn.util.basic import make_build_dir from finn.util.exception import FINNInternalError, FINNUserError @@ -401,7 +402,7 @@ def __init__( def simulate( self, depth: int | list[list[int]] | None = None, max_cycles: int | None = None - ) -> tuple[dict[int, dict[str, list[int]]], bool]: + ) -> tuple[dict[int, dict[str, str | list[int]]], bool]: """Simulate the given number of samples for every layer. Layers are completely isolated and simulated in parallel. Simulation data is returned as a dict (by node name as index). """ @@ -472,10 +473,30 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: ) model = sim.model # TODO:clean up - # Running the initial simulation - - # raise NotImplementedError() + # Create empty table for datapoints that will be collected + # First create as a nested dict, since not all data is avilable at the same time + # It is then flattened when creating the dataframe, so that node and stream are columns too + # df_data[node][stream_idx][columnm] = ... + df_data: dict[str, list[dict[str, Any]]] = {} + for nodeindex, node in enumerate(model.graph.node): + df_data[node.name] = [] + for i in range(len(node.output)): + df_data[node.name].append( + { + "onnx_index": nodeindex, + "out_bitwidth": -1, + "out_initial_fifo_depths": -1, + "out_final_fifo_depths": -1, + "minimization_iterations": -1, + "minimization_order": "TODO", # TODO + "simulation_time": -1, + "successor_node": ", ".join( + [node.name for node in model.find_consumers(node.output[i])] + ), + } + ) + # Running the initial simulation log.info("Running initial node-connected simulation.") initial_fifo_depths, _ = sim.simulate() @@ -486,13 +507,22 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: initial_sizes_path.write_text(json.dumps(initial_fifo_depths, indent=4)) log.info(f"Wrote initial sizes to: {initial_sizes_path}") - fifo_depths = [] # Each entry is a list of fifo sizes for that node + # Store initial sizes in dataframe as well + for layerdata in initial_fifo_depths.values(): + for idx in range(len(layerdata["fifo_utilization"])): + name: str = cast("str", layerdata["name"]) + df_data[name][idx]["out_initial_fifo_depths"] = layerdata["fifo_utilization"][idx] + + # Create fifo_depths (indexed by layer index and then stream index) + fifo_depths: list[list[int]] = [] # Each entry is a list of fifo sizes for that node for val in initial_fifo_depths.values(): - fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) + utilization: list[int] = cast("list[int]", val["fifo_utilization"]) + fifo_depths.append([v + 1 for v in utilization]) # Max cycles for any simulation sim_cycles = max([val["cycles"] for val in initial_fifo_depths.values()]) + # Extract bitwidths from outstream widths of hw nodes bit_widths = [] for i in range(len(fifo_depths)): bit_widths.append([]) @@ -503,6 +533,12 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: else: raise FINNInternalError("Non-HW node found in dataflow graph during simulation") + # Store bitwidths into dataframe as well + for i in range(len(bit_widths)): + for j in range(len(bit_widths[i])): + df_data[model.graph.node[i].name][j]["out_bitwidth"] = bit_widths[i][j] + + # Run minimization for every layer/stream log.info("Minimizing layers...") needs_minimization = [] for i in range(len(fifo_depths)): @@ -523,13 +559,15 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: for i in range(len(fifo_depths)): for j in range(len(fifo_depths[i])): if not needs_minimization[i][j]: + df_data[model.graph.node[i].name][j]["simulation_time"] = 0.0 log.debug( f"[ {i+1}.{j+1} / {len(fifo_depths)} ] " f"Skipping minimization for this stream." ) continue - minimized_depth = self._minimize_fifo_depth( + minimization_start = time.time() + minimized_depth, iterations_needed = self._minimize_fifo_depth( i, j, fifo_depths, @@ -539,11 +577,18 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: sim, sim_cycles, ) + minimization_time = time.time() - minimization_start + df_data[model.graph.node[i].name][j]["simulation_time"] = minimization_time + df_data[model.graph.node[i].name][j]["minimization_iterations"] = iterations_needed + + # Store the minimized size fifo_depths[i][j] = minimized_depth + percentage = int(100.0 * float(i + 1) / float(len(fifo_depths))) log.info( f"[ [bold green]{percentage}%[/bold green] ] " - f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed.", + f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed " + f"({iterations_needed} iterations).", extra={"markup": True, "highlighter": None}, ) @@ -561,6 +606,34 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: json.dump(json_results, f) log.info(f"Wrote results back to {writeback_path}") + # Write final FIFO sizes into dataframe + for i in range(len(fifo_depths)): + for j in range(len(fifo_depths[i])): + df_data[model.graph.node[i].name][j]["out_final_fifo_depths"] = fifo_depths[i][j] + + # Store dataframe + df_keys = list(df_data[model.graph.node[0].name][0].keys()) + df_dict = {} + df_dict["node"] = [] + df_dict["stream"] = [] + for k in df_keys: + df_dict[k] = [] + for node, nodedata in df_data.items(): + for streamindex, streamdata in enumerate(nodedata): + df_dict["node"].append(node) + df_dict["stream"].append(streamindex) + for key in df_keys: + df_dict[key].append(streamdata[key]) + + df = pd.DataFrame(df_dict) + model = store_fifo_data( + model, + df, + Path(self.cfg.output_dir) / "report" / "fifo_data.csv", + delete_existing=False, + store_html=True, + ) + return model, False def _check_performance(self, new_data: dict, initial_fifo_depths: dict) -> bool: @@ -586,7 +659,7 @@ def _test_depth( fifo_idx: int, baseline_depths: list, initial_fifo_depths: dict, - sim: Simulation, + sim: NodeConnectedSimulation, sim_cycles: float, ) -> tuple[bool, bool]: """Test a specific FIFO depth. @@ -643,9 +716,9 @@ def _minimize_fifo_depth( baseline_depths: list, bit_widths: list, initial_fifo_depths: dict, - sim: Simulation, + sim: NodeConnectedSimulation, sim_cycles: int, - ) -> int: + ) -> tuple[int, int]: """Minimize a single FIFO depth using binary search. Args: @@ -660,8 +733,9 @@ def _minimize_fifo_depth( sim_cycles: Maximum simulation cycles Returns: - Minimized FIFO depth + Tuple: Minimized FIFO depth, Iterations required to arrive at the result """ + iterations = 0 original_size = baseline_depths[node_idx][fifo_idx] bw = bit_widths[node_idx][fifo_idx] @@ -673,8 +747,9 @@ def _minimize_fifo_depth( success, timeout = self._test_depth( 32, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles ) + iterations += 1 if success: - return 32 + return 32, iterations if original_size <= self.max_qsrl_depth: upper_luts = calculate_srl16e_luts(original_size, bw) @@ -683,7 +758,7 @@ def _minimize_fifo_depth( # Binary search if there's room to search if upper_luts > lower_luts: - best_working_depth = self._binary_search_srl_depth( + best_working_depth, bin_it = self._binary_search_srl_depth( node_idx, fifo_idx, baseline_depths, @@ -694,8 +769,9 @@ def _minimize_fifo_depth( lower_luts=lower_luts, upper_luts=upper_luts, ) - return best_working_depth - return original_size + iterations += bin_it + return best_working_depth, iterations + return original_size, iterations # Try FIFO depth of 256 next (fits into LUTRAM) success, timeout = self._test_depth( @@ -707,6 +783,7 @@ def _minimize_fifo_depth( sim, sim_cycles, ) + iterations += 1 if success: upper_luts = calculate_srl16e_luts(original_size, bw) # LUTRAM based FIFOs have block sizes of 32, so smallest after 32 is 64 @@ -714,7 +791,7 @@ def _minimize_fifo_depth( # Binary search if there's room to search if upper_luts > lower_luts: - best_working_depth = self._binary_search_srl_depth( + best_working_depth, bin_it = self._binary_search_srl_depth( node_idx, fifo_idx, baseline_depths, @@ -725,8 +802,9 @@ def _minimize_fifo_depth( lower_luts=lower_luts, upper_luts=upper_luts, ) - return best_working_depth - return self.max_qsrl_depth + iterations += bin_it + return best_working_depth, iterations + return self.max_qsrl_depth, iterations # We know 256 doesn't work, so we have to use BRAMs # Try one BRAM block less than current @@ -743,15 +821,16 @@ def _minimize_fifo_depth( success, timeout = self._test_depth( max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles ) + iterations += 1 if timeout or not success: - return original_size + return original_size, iterations best_working_depth = max_d # Binary search if there's room to search and multiple valid configs if len(valid_blocks) > 1: - best_working_depth = self._exponential_binary_search_depth( + best_working_depth, bin_it = self._exponential_binary_search_depth( node_idx, fifo_idx, baseline_depths, @@ -761,8 +840,9 @@ def _minimize_fifo_depth( sim_cycles, valid_blocks=valid_blocks, ) + iterations += bin_it - return best_working_depth + return best_working_depth, iterations def _exponential_binary_search_depth( self, @@ -771,10 +851,10 @@ def _exponential_binary_search_depth( baseline_depths: list, bitwidth: int, initial_fifo_depths: dict, - sim: Simulation, + sim: NodeConnectedSimulation, sim_cycles: float, valid_blocks: list[int], - ) -> int: + ) -> tuple[int, int]: """Perform exponential + binary search over valid block configurations. Uses exponential search to quickly find the range, then binary search within it. @@ -792,8 +872,9 @@ def _exponential_binary_search_depth( valid_blocks: Sorted list of valid block counts to search over Returns: - Best working depth found + Tuple: Best working depth found, Number of iterations required to arrive at this result. """ + iterations = 0 if not valid_blocks: raise FINNInternalError("valid_blocks list cannot be empty") @@ -815,6 +896,7 @@ def _exponential_binary_search_depth( success, _ = self._test_depth( max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles ) + iterations += 1 if success: # Found a working depth, now binary search in [last_failed_idx+1, exp_idx] @@ -835,6 +917,7 @@ def _exponential_binary_search_depth( success, _ = self._test_depth( max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles ) + iterations += 1 if success: # This depth works, try smaller (lower indices) @@ -844,7 +927,7 @@ def _exponential_binary_search_depth( # This depth doesn't work, need larger (higher indices) lower_idx = mid_idx + 1 - return best_working_depth + return best_working_depth, iterations def _binary_search_srl_depth( self, @@ -853,11 +936,11 @@ def _binary_search_srl_depth( baseline_depths: list, bitwidth: int, initial_fifo_depths: dict, - sim: Simulation, + sim: NodeConnectedSimulation, sim_cycles: float, lower_luts: int, upper_luts: int, - ) -> int: + ) -> tuple[int, int]: """Perform binary search to find minimal working FIFO depth in LUTRAM range. Args: @@ -872,8 +955,9 @@ def _binary_search_srl_depth( upper_luts: Upper bound for LUT count (known to work) Returns: - Best working depth found + Tuple: Best working depth found, Number of Iterations required to arrive at this result """ + iterations = 0 _, max_d = calculate_srl16e_depth_range(upper_luts, bitwidth) best_working_depth = max_d @@ -897,6 +981,7 @@ def _binary_search_srl_depth( success, _ = self._test_depth( max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles ) + iterations += 1 if success: # This depth works, try smaller @@ -906,7 +991,7 @@ def _binary_search_srl_depth( # This depth doesn't work, need larger lower_luts = mid_luts + 1 - return best_working_depth + return best_working_depth, iterations def _needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: """Determine whether a FIFO can be minimized further. diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index ad40970f23..a813df3414 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -83,11 +83,14 @@ def run(self) -> dict[str, IsolatedSimLogData]: """Run a node isolated simulation and return the collected input ready / output valid data, indexed based on node names.""" futures: list[Future] = [] - data: dict[str, self.IsolatedSimLogData] = {} datalock = Lock() total = len(self.binaries) done = 0 + # Important to initialize from names. Otherwise the results are added into the dict + # in the order in which they finished simulating. But we want to keep the model order. + data: dict[str, self.IsolatedSimLogData] = {name: {} for name in self.names} + # TODO: Lock not needed; futures are not consumed just by # TODO: using the callback, so we can unpack them later @@ -543,6 +546,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # 4. Writing the data to json. Order of S_AXIS_CONTROL -> order in which JSON gets written # IMPORTANT: Use nlohmann::ordered_json to keep the insertion order! # 5. Reading the JSON into python (python dicts are ordered since 3.7) + # According to docs, the Python JSON module also keeps order # 6. Syncing node.inputs to order of s_axi_... streams read from the JSON. edited_bounds = {} @@ -550,8 +554,9 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: for node in model.graph.node: suc = model.find_direct_successors(node) if suc is None: - continue - edited_bounds[node.name] = [-1] * len(suc) + edited_bounds[node.name] = [-1] + else: + edited_bounds[node.name] = [-1] * len(suc) # For every node check its predecessors. # Find the index/tensor that connects the predecessor and the current one @@ -595,14 +600,22 @@ def get_index(a: Any, values: Any) -> int | None: ) # Prepare the data - df_data = {"node": [], "stream": [], "out_fifo_upper_bound": [], "input_ready_percent": []} + df_data = { + "onnx_index": [], + "node": [], + "stream": [], + "out_fifo_upper_bound": [], + "input_ready_percent": [], + } for layer, layerdata in edited_bounds.items(): for idx in range(len(layerdata)): + df_data["onnx_index"].append([n.name for n in model.graph.node].index(layer)) df_data["node"].append(layer) df_data["stream"].append(idx) df_data["out_fifo_upper_bound"].append(layerdata[idx]) # TODO: Remove input_ready_percent? - df_data["input_ready_percent"].append(self.percent_ready(data)[layer]) + # df_data["input_ready_percent"].append(self.percent_ready(data)[layer]) + df_data["input_ready_percent"].append(0.0) # Create the DF self.fifo_data = pd.DataFrame(df_data) @@ -611,7 +624,11 @@ def get_index(a: Any, values: Any) -> int | None: # Save in dataframe and model model = store_fifo_data( - model, self.fifo_data, self.default_fifo_data_path, delete_existing=True + model, + self.fifo_data, + self.default_fifo_data_path, + delete_existing=True, + store_html=True, ) # TODO: Integrate data into the layer parallel simulation From 14f4e0de569ecaf34bfbd6cafb7f748525dbf7c9 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Wed, 18 Feb 2026 15:18:30 +0100 Subject: [PATCH 070/170] Add per layer timeout for late first valid --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 37 +++- finn_xsi/finn_xsi/include/FIFO.h | 6 +- finn_xsi/finn_xsi/include/Simulation.hpp | 46 +++-- finn_xsi/finn_xsi/src/FIFO.cpp | 15 +- finn_xsi/finn_xsi/unittests/CMakeLists.txt | 8 +- finn_xsi/finn_xsi/unittests/FIFO_test.cpp | 27 +++ .../fpgadataflow/simulation_build.py | 12 +- .../fpgadataflow/simulation_connected.py | 158 +++++++++++++++--- .../fpgadataflow/simulation_controller.py | 10 +- 9 files changed, 266 insertions(+), 53 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 09810f7085..f7140f4b0f 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -51,7 +51,7 @@ class SimulationController { RTLSimConfig::IsInputNode, RTLSimConfig::IsOutputNode>& simulation) : sim(simulation) {} - void configure(const std::vector& depths, std::size_t maxCycles) { + void configure(const std::vector& depths, const std::vector& expected_first_valid_cycles, std::size_t maxCycles) { std::lock_guard lock(state_mutex); if (state != SimulationState::IDLE && state != SimulationState::FINISHED) { throw std::runtime_error("Cannot configure while simulation is running"); @@ -77,6 +77,11 @@ class SimulationController { std::size_t depth_idx = std::min(i, fifo_depths.size() - 1); sim.setFIFODepth(i, fifo_depths[depth_idx]); } + + for (std::size_t i = 0; i < expected_first_valid_cycles.size(); ++i) { + std::size_t cycles_idx = std::min(i, expected_first_valid_cycles.size() - 1); + sim.setFIFOCyclesUntilExpectedFirstValid(i, expected_first_valid_cycles[cycles_idx]); + } } void start() { @@ -172,6 +177,17 @@ class SimulationController { status["fifo_utilization"] = fifo_util; } } + // Add FIFO cycles until first valid data + { + auto cycles_until_valid = sim.getFIFOCyclesUntilFirstValid(); + json fifo_cycles = json::array(); + for (size_t i = 0; i < cycles_until_valid.size(); ++i) { + fifo_cycles.push_back(cycles_until_valid[i]); + } + if (!fifo_cycles.empty()) { + status["fifo_cycles_until_first_valid"] = fifo_cycles; + } + } break; case SimulationState::ERROR: status["state"] = "error"; @@ -192,6 +208,8 @@ void process_command(const json& request, json& response, SimulationController& if (command == "configure") { std::vector fifo_depths; + std::cout << "Payload: " << payload << std::endl; + // Handle fifo_depth as either a single value or an array if (payload.contains("fifo_depth")) { const auto& depth_value = payload["fifo_depth"]; @@ -206,6 +224,18 @@ void process_command(const json& request, json& response, SimulationController& fifo_depths.push_back(std::numeric_limits::max()); // Default value } + std::vector expected_first_valid_cycles; + if (payload.contains("fifo_first_valid_cycles")) { + const auto& expected_cycles_value = payload["fifo_first_valid_cycles"]; + if (expected_cycles_value.is_array()) { + for (const auto& val : expected_cycles_value) { + expected_first_valid_cycles.push_back(val.get()); + } + } else { + expected_first_valid_cycles.push_back(expected_cycles_value.get()); + } + } + if (fifo_depths.empty()) { throw std::runtime_error("FIFO depth list cannot be empty"); } @@ -215,7 +245,7 @@ void process_command(const json& request, json& response, SimulationController& max_cycles = payload["max_cycles"].get(); } - controller.configure(fifo_depths, max_cycles); + controller.configure(fifo_depths, expected_first_valid_cycles, max_cycles); response["status"] = "success"; response["message"] = "Configuration successful"; } else if (command == "start") { @@ -248,6 +278,9 @@ void process_command(const json& request, json& response, SimulationController& if (final_status.contains("timeout")) { response["timeout"] = final_status["timeout"]; } + if (final_status.contains("fifo_cycles_until_first_valid")) { + response["fifo_cycles_until_first_valid"] = final_status["fifo_cycles_until_first_valid"]; + } } else { response["status"] = "error"; response["message"] = "Unknown command: " + command; diff --git a/finn_xsi/finn_xsi/include/FIFO.h b/finn_xsi/finn_xsi/include/FIFO.h index 803e827fac..9cb09f9970 100644 --- a/finn_xsi/finn_xsi/include/FIFO.h +++ b/finn_xsi/finn_xsi/include/FIFO.h @@ -11,17 +11,21 @@ class FIFO : public CommunicationChannel { uint64_t currentUtil = 0; uint64_t maxSize = 0; uint64_t nextUtil = 0; + uint64_t cyclesUntilExpectedFirstValid = std::numeric_limits::max(); + uint64_t initialCyclesUntilExpectedFirstValid = std::numeric_limits::max(); public: FIFO(uint64_t size = std::numeric_limits::max()); ~FIFO(); void update(bool incomingValid, bool incomingReady); - void toggleClock(); + bool toggleClock(); virtual bool getInputReady(std::stop_token stoken = {}) noexcept override; virtual bool getOutputValid(std::stop_token stoken = {}) noexcept override; bool isEmpty() const; void reset(uint64_t size = std::numeric_limits::max()); + void setCyclesUntilExpectedFirstValid(uint64_t cycles); + uint64_t getCyclesUntilFirstValid() const; void setMaxSize(const uint64_t size); uint64_t getMaxSize() const; uint64_t getSpaceLeft() const; diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index e9cd370856..8745974213 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -115,7 +115,8 @@ class SingleNodeSimulation : public Simulation fifo; /// Communicate with predecessors and successors and update their values and our own - [[gnu::hot, gnu::flatten, gnu::always_inline]] void communicate(std::stop_token stoken = {}) { + [[gnu::hot, gnu::flatten, gnu::always_inline]] bool communicate(std::stop_token stoken = {}) { + bool ret = false; if constexpr (!FirstNode) { for (std::size_t i = 0; i < IStreamsSize; ++i) { // Interface SHM <-> sim @@ -132,7 +133,7 @@ class SingleNodeSimulation : public Simulation sim this->ostreams[i].setOutputReady(this->fifo[i].getInputReady()); // Toggle FIFO clock - this->fifo[i].toggleClock(); + ret |= this->fifo[i].toggleClock(); } } if constexpr (LastNode) { @@ -151,6 +152,7 @@ class SingleNodeSimulation : public Simulationclk.toggleClk(); + return ret; } public: @@ -252,14 +255,15 @@ class SingleNodeSimulation : public Simulation::max()) { + bool timeout = false; while (!std::all_of(this->ostreams.begin(), this->ostreams.end(), [](const M_AXIS_Control& stream) { return stream.stableState.is_stable(); }) & !stoken.stop_requested() & - (cyclesRun <= max_cycles)) { - runSingleCycle(stoken); - runSingleCycle(stoken); - runSingleCycle(stoken); - runSingleCycle(stoken); + (cyclesRun <= max_cycles) & !timeout) { + timeout |= runSingleCycle(stoken); + timeout |= runSingleCycle(stoken); + timeout |= runSingleCycle(stoken); + timeout |= runSingleCycle(stoken); } - return cyclesRun > max_cycles; + return timeout || cyclesRun > max_cycles; } /// Get the number of FIFOs @@ -282,6 +286,17 @@ class SingleNodeSimulation : public Simulation= OStreamsSize) { + auto error = "FIFO index " + std::to_string(index) + " out of range (max: " + std::to_string(OStreamsSize - 1) + ")"; + throw std::out_of_range(error); + } + fifo[index].setCyclesUntilExpectedFirstValid(cycles); + } + /// Set the max FIFO depth of all interfaces void setMaxFIFODepth(std::size_t depth) { if constexpr (!LastNode) { @@ -302,6 +317,17 @@ class SingleNodeSimulation : public Simulation getFIFOCyclesUntilFirstValid() const noexcept { + if constexpr (LastNode) { + return {}; + } + std::array cycles{}; + for (std::size_t i = 0; i < OStreamsSize; ++i) { + cycles[i] = fifo[i].getCyclesUntilFirstValid(); + } + return cycles; + } + /// Get the job size of the specified output stream std::size_t getOutputJobSize(std::size_t outputIndex = 0) { return this->ostreams[outputIndex].job_size; } diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp index abfe9f6a43..76f1144288 100644 --- a/finn_xsi/finn_xsi/src/FIFO.cpp +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -2,6 +2,7 @@ #include #include +#include FIFO::FIFO(uint64_t size) : maxSize(size) {} FIFO::~FIFO() {} @@ -25,10 +26,13 @@ void FIFO::update(bool incomingValid, bool incomingReady) { /// Toggle the clock cycle, and update the previously set values. /// nextUtil is guaranteed to be in [0, maxSize] by all operations. -void FIFO::toggleClock() { +/// Returns false if a first valid signal was expected, but has not been observed. +bool FIFO::toggleClock() { currentUtil = nextUtil; maxUtil = std::max(maxUtil, currentUtil); nextUtil = currentUtil; + cyclesUntilExpectedFirstValid -= static_cast(static_cast(cyclesUntilExpectedFirstValid) & !static_cast(maxUtil)); // Underflow-safe decrement + return (cyclesUntilExpectedFirstValid == 0) & (maxUtil == 0); } /// Return whether the FIFO can accept inputs (for the current utilization) @@ -47,8 +51,17 @@ void FIFO::reset(uint64_t size) { maxUtil = 0; maxSize = size; nextUtil = 0; + cyclesUntilExpectedFirstValid = std::numeric_limits::max(); } +void FIFO::setCyclesUntilExpectedFirstValid(uint64_t cycles) { + cyclesUntilExpectedFirstValid = cycles; + initialCyclesUntilExpectedFirstValid = cycles; + std::cout << "FIFO set to expect first valid after " << cycles << " cycles" << std::endl; +} + +uint64_t FIFO::getCyclesUntilFirstValid() const { return initialCyclesUntilExpectedFirstValid - cyclesUntilExpectedFirstValid; } + /// Set the FIFOs max size void FIFO::setMaxSize(const uint64_t size) { maxSize = size; } diff --git a/finn_xsi/finn_xsi/unittests/CMakeLists.txt b/finn_xsi/finn_xsi/unittests/CMakeLists.txt index 06ea3ed332..20a699ae33 100644 --- a/finn_xsi/finn_xsi/unittests/CMakeLists.txt +++ b/finn_xsi/finn_xsi/unittests/CMakeLists.txt @@ -18,11 +18,6 @@ target_link_libraries(FIFO_test PRIVATE nlohmann_json::nlohmann_json GTest::gtes target_include_directories(FIFO_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include) target_include_directories(FIFO_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsim/include") -# Add InterSimulationInterface unit tests -add_executable(InterSimulationInterface_test InterSimulationInterface_test.cpp) -target_link_libraries(InterSimulationInterface_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt nlohmann_json::nlohmann_json) -target_include_directories(InterSimulationInterface_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../include ${Boost_INCLUDE_DIRS}) - # Add InterprocessCommunicationChannel unit tests add_executable(InterprocessCommunicationChannel_test InterprocessCommunicationChannel_test.cpp) target_link_libraries(InterprocessCommunicationChannel_test PRIVATE GTest::gtest_main Threads::Threads -ldl -lrt nlohmann_json::nlohmann_json) @@ -37,10 +32,9 @@ target_include_directories(Integration_test PUBLIC "$ENV{XILINX_VIVADO}/data/xsi # Register tests with CTest include(GoogleTest) gtest_discover_tests(FIFO_test) -gtest_discover_tests(InterSimulationInterface_test) gtest_discover_tests(InterprocessCommunicationChannel_test) gtest_discover_tests(Integration_test) # Create a target to build all unittests at once add_custom_target(all_unittests) -add_dependencies(all_unittests FIFO_test InterSimulationInterface_test InterprocessCommunicationChannel_test Integration_test) +add_dependencies(all_unittests FIFO_test InterprocessCommunicationChannel_test Integration_test) diff --git a/finn_xsi/finn_xsi/unittests/FIFO_test.cpp b/finn_xsi/finn_xsi/unittests/FIFO_test.cpp index 2fff65192e..835ab0dd63 100644 --- a/finn_xsi/finn_xsi/unittests/FIFO_test.cpp +++ b/finn_xsi/finn_xsi/unittests/FIFO_test.cpp @@ -820,6 +820,33 @@ TEST_F(FIFOTest, TryMethodsWithReset) { EXPECT_EQ(fifo.size(), 1); } +TEST_F(FIFOTest, TestTimeout){ + FIFO fifo(10); + fifo.setCyclesUntilExpectedFirstValid(3); + EXPECT_TRUE(fifo.toggleClock()); // 3 cycles left + EXPECT_TRUE(fifo.toggleClock()); // 2 cycles left + EXPECT_FALSE(fifo.toggleClock()); // 0 cycles left, should return false + + fifo.reset(10); + fifo.setCyclesUntilExpectedFirstValid(2); + EXPECT_TRUE(fifo.toggleClock()); // 2 cycles left + fifo.update(true, false); // Set valid, should disable timeout + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + + fifo.reset(10); + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true + EXPECT_TRUE(fifo.toggleClock()); // Still should return true +} + // Main function to run all tests int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index d8272aa82c..8518ff8365 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -404,13 +404,13 @@ def _compile_simulation(self, sim_base: Path, silent: bool = True) -> Path: # Running CMake first cmake_call = f"{sys.executable} -m cmake -S {finnxsi_dir} -B {sim_base}" - log.info(f"Running cmake on RTLSIM Wrapper in {sim_base}") + log.debug(f"Running cmake on RTLSIM Wrapper in {sim_base}") try: launch_process_helper( shlex.split(cmake_call), cwd=finnxsi_dir, - print_stdout=not silent, - print_stderr=not silent, + print_stdout=silent, + print_stderr=silent, proc_env=os.environ.copy(), ) except CalledProcessError as e: @@ -425,8 +425,8 @@ def _compile_simulation(self, sim_base: Path, silent: bool = True) -> Path: ["make"], proc_env=os.environ.copy(), cwd=sim_base, - print_stdout=not silent, - print_stderr=not silent, + print_stdout=silent, + print_stderr=silent, ) except CalledProcessError as e: raise FINNInternalError(f"Failed to create executable in {sim_base}!") from e @@ -762,7 +762,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: self.builder = SimulationBuilder(self.model, self.fpgapart, self.clk_ns) sys.stdout = sys.stdout.console # type: ignore self.binaries = self.builder.build_simulation( - with_live_display=True, + with_live_display=False, functional_sim=self.functional_sim, ) self.model.set_metadata_prop( diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index a830e3b972..46ee718649 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -3,7 +3,6 @@ import glob import json import math -import multiprocessing import os import time import traceback @@ -25,7 +24,6 @@ from finn.util.logging import log - class NodeConnectedSimulationController(SimulationController): """Run simulations for node connected cases.""" @@ -78,6 +76,7 @@ def run( depth: list[list[int]] | None = None, output_json: Path | None = None, max_cycles: int | None = None, + fifo_first_valid_cycles: list[list[int]] | None = None, ) -> dict[str, list[int]]: """Run the simulation entirely with the given depth and sample count. @@ -86,6 +85,7 @@ def run( samples: Number of samples to simulate. output_json: Optional path to write merged simulation data as JSON. max_cycles: Max cycles + fifo_first_valid_cycles: First valid cycle for each FIFO (used for timeout detection) Returns: Dictionary mapping simulation names to their FIFO utilization arrays. @@ -97,6 +97,7 @@ def run( intervals_results: dict[str, list[int]] = {} timeout_result = False fifo_depths: dict[str, list[int]] = {} + fifo_cycles_until_first_valid_results: dict[str, list[int]] = {} # Clean up any existing shared memory resources before starting self._cleanup_shm_resources() @@ -116,11 +117,18 @@ def run( self._run_binary, binary, name, - i % multiprocessing.cpu_count(), + i % len(os.sched_getaffinity(0)) + if len(os.sched_getaffinity(0)) < len(self.names) + else -1, # sched_getaffinity needed, because + # cpu_count does not handle well with workload schedulers. + # We only pin the core if we have more simulations than cores to avoid + # simulations moving around too much and hurting performance. If we have + # more cores than simulations, we leave it to the OS to schedule. depth[i] if depth is not None else None, is_last_node, # Only last node has no output FIFOs is_special_for_display, # First and last get special coloring max_cycles, + fifo_first_valid_cycles[i] if fifo_first_valid_cycles is not None else None, ) ) @@ -144,12 +152,16 @@ def run( intervals, timeout, fifo_depth, + fifo_cycles_until_first_valid, ) = result fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -180,9 +192,13 @@ def run( intervals, timeout, fifo_depth, + fifo_cycles_until_first_valid, ) = result # Only update if not already collected if sim_name not in fifo_results: + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles @@ -207,6 +223,9 @@ def run( "cycles": cycles_results.get(name, 0), "samples": samples_results.get(name, 0), "intervals": intervals_results.get(name, []), + "fifo_cycles_until_first_valid": fifo_cycles_until_first_valid_results.get( + name, [] + ), } for name in self.names ], @@ -226,7 +245,8 @@ def _run_binary( is_last_node: bool = False, is_special_for_display: bool = False, max_cycles: int | None = None, - ) -> tuple[str, list[int], int, int, list[int], bool, list[int]] | None: + fifo_first_valid_cycles: list[int] | None = None, + ) -> tuple[str, list[int], int, int, list[int], bool, list[int], list[int]] | None: """Run the specified simulation binary in a new subprocess and communicate with it. Args: @@ -237,10 +257,11 @@ def _run_binary( is_last_node: True if this is the last node (no output FIFOs to configure) is_special_for_display: True if this node should get special color in logs max_cycles: Maximum cycles to simulate + fifo_first_valid_cycles: First valid cycle for each FIFO (used for timeout detection) Returns: Tuple of (simulation_name, fifo_utilization, cycles, samples, intervals, timeout, - fifo_depth) on success, + fifo_depth, fifo_cycles_until_first_valid) on success, None on failure. """ cwd = binary.parent @@ -267,7 +288,9 @@ def _print(msg: str, color: str = "green") -> None: try: # Start the simulation process with socket communication - proc_idx = self._start_process(binary, process_index) + proc_idx = self._start_process( + binary, process_index, cpu=_cpu if _cpu is not None else -1 + ) # Send configuration commands # Last node has no output FIFOs, so don't configure FIFO depths @@ -276,6 +299,8 @@ def _print(msg: str, color: str = "green") -> None: config_payload["fifo_depth"] = depth if max_cycles is not None: config_payload["max_cycles"] = max_cycles + if not is_last_node and fifo_first_valid_cycles is not None: + config_payload["fifo_first_valid_cycles"] = fifo_first_valid_cycles response = self._send_and_receive(proc_idx, "configure", config_payload) @@ -308,6 +333,7 @@ def _print(msg: str, color: str = "green") -> None: timeout = False fifo_util: list[int] = [] fifo_depth: list[int] = [] + fifo_cycles_until_first_valid: list[int] = [] # Poll for status updates while True: @@ -326,9 +352,21 @@ def _print(msg: str, color: str = "green") -> None: intervals = stop_response.get("intervals", []) fifo_depth = stop_response.get("fifo_depth", []) timeout = stop_response.get("timeout", False) + fifo_cycles_until_first_valid = stop_response.get( + "fifo_cycles_until_first_valid", [] + ) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") - return (name, fifo_util, cycles, samps, intervals, timeout, fifo_depth) + return ( + name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + fifo_cycles_until_first_valid, + ) time.sleep(self.poll_interval) response = self._send_and_receive(proc_idx, "status", {}) @@ -348,6 +386,9 @@ def _print(msg: str, color: str = "green") -> None: fifo_depth = response.get("fifo_depth", []) intervals = response.get("intervals", []) timeout = response.get("timeout", False) + fifo_cycles_until_first_valid = response.get( + "fifo_cycles_until_first_valid", [] + ) with self.stop_lock: self.should_stop = True break @@ -366,17 +407,28 @@ def _print(msg: str, color: str = "green") -> None: # Stop the simulation stop_response = self._send_and_receive(proc_idx, "stop", {}) - fifo_util = [] if stop_response: fifo_util = stop_response.get("fifo_utilization", []) fifo_depth = stop_response.get("fifo_depth", []) cycles = stop_response.get("cycles", 0) samps = stop_response.get("samples", 0) + fifo_cycles_until_first_valid = stop_response.get( + "fifo_cycles_until_first_valid", [] + ) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") - return (name, fifo_util, cycles, samps, intervals, timeout, fifo_depth) + return ( + name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + fifo_cycles_until_first_valid, + ) except Exception as e: self.console.log(f"Exception caught during simulation execution ({name}): {e}") @@ -401,7 +453,10 @@ def __init__( super().__init__(model, simulation_type, fpgapart, clk_ns, functional_sim, workers) def simulate( - self, depth: int | list[list[int]] | None = None, max_cycles: int | None = None + self, + depth: int | list[list[int]] | None = None, + max_cycles: int | None = None, + fifo_first_valid_cycles: list[list[int]] | None = None, ) -> tuple[dict[int, dict[str, list[int]]], bool]: """Simulate the given number of samples for every layer. Layers are completely isolated and simulated in parallel. Simulation data is returned as a dict (by node name as index). @@ -421,9 +476,10 @@ def simulate( controller = NodeConnectedSimulationController( len(self.binaries), names, list(self.binaries.values()), Console(), 0.1, False ) - controller.run(initial_depth, output_json, max_cycles) + controller.run(initial_depth, output_json, max_cycles, fifo_first_valid_cycles) end = time.time() log.debug(f"Simulation took {end - start} seconds!") + print(f"Simulation took {end - start} seconds!") # Load the merged data from JSON merged_data = json.loads(output_json.read_text()) @@ -438,6 +494,7 @@ def simulate( "cycles": sim_entry["cycles"], "samples": sim_entry["samples"], "intervals": sim_entry["intervals"], + "fifo_cycles_until_first_valid": sim_entry["fifo_cycles_until_first_valid"], } json.dump(data, output_json.open("w"), indent=4) return data, merged_data.get("timeout_occurred", False) @@ -475,8 +532,6 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Running the initial simulation - # raise NotImplementedError() - log.info("Running initial node-connected simulation.") initial_fifo_depths, _ = sim.simulate() @@ -490,9 +545,14 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: fifo_depths = [] # Each entry is a list of fifo sizes for that node for val in initial_fifo_depths.values(): fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) + fifo_first_valid_cycles: list[list[int]] = [] + for val in initial_fifo_depths.values(): + fifo_first_valid_cycles.append( + [v + 10 for v in val["fifo_cycles_until_first_valid"]] + ) # Add 10 cycles grace period # Max cycles for any simulation - sim_cycles = max([val["cycles"] for val in initial_fifo_depths.values()]) + sim_cycles: int = max([val["cycles"] for val in initial_fifo_depths.values()]) # type: ignore bit_widths = [] for i in range(len(fifo_depths)): @@ -525,7 +585,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: for j in range(len(fifo_depths[i])): if not needs_minimization[i][j]: log.debug( - f"[ {i+1}.{j+1} / {len(fifo_depths)} ] " + f"[ {i + 1}.{j + 1} / {len(fifo_depths)} ] " f"Skipping minimization for this stream." ) continue @@ -539,12 +599,13 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: initial_fifo_depths, sim, sim_cycles, + fifo_first_valid_cycles, ) fifo_depths[i][j] = minimized_depth percentage = int(100.0 * float(i + 1) / float(len(fifo_depths))) log.info( f"[ [bold green]{percentage}%[/bold green] ] " - f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed.", + f"[ {i + 1}.{j + 1} / {len(fifo_depths)} ] Simulation completed.", extra={"markup": True, "highlighter": None}, ) @@ -589,6 +650,7 @@ def _test_depth( initial_fifo_depths: dict, sim: Simulation, sim_cycles: float, + fifo_first_valid_cycles: list[list[int]], ) -> tuple[bool, bool]: """Test a specific FIFO depth. @@ -600,14 +662,18 @@ def _test_depth( initial_fifo_depths: Baseline performance data sim: Simulation controller sim_cycles: Maximum simulation cycles - + fifo_first_valid_cycles: First valid cycle for each FIFO Returns: Tuple of (success, timeout) where success means depth works without degradation """ test_depths = [row[:] for row in baseline_depths] # Deep copy from baseline test_depths[node_idx][fifo_idx] = test_depth - new_data, timeout = sim.simulate(test_depths, max_cycles=math.ceil(sim_cycles * 1.1)) + new_data, timeout = sim.simulate( + test_depths, + max_cycles=min(math.ceil(sim_cycles * 1.05), sim_cycles + 10 * len(test_depths)), + fifo_first_valid_cycles=fifo_first_valid_cycles, + ) if timeout: return False, True @@ -646,6 +712,7 @@ def _minimize_fifo_depth( initial_fifo_depths: dict, sim: Simulation, sim_cycles: int, + fifo_first_valid_cycles: list[list[int]], ) -> int: """Minimize a single FIFO depth using binary search. @@ -659,7 +726,7 @@ def _minimize_fifo_depth( initial_fifo_depths: Baseline performance data sim: Simulation controller sim_cycles: Maximum simulation cycles - + fifo_first_valid_cycles: First valid cycle for each FIFO Returns: Minimized FIFO depth """ @@ -672,7 +739,14 @@ def _minimize_fifo_depth( # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs success, timeout = self._test_depth( - 32, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + 32, + node_idx, + fifo_idx, + baseline_depths, + initial_fifo_depths, + sim, + sim_cycles, + fifo_first_valid_cycles, ) if success: return 32 @@ -692,6 +766,7 @@ def _minimize_fifo_depth( initial_fifo_depths, sim, sim_cycles, + fifo_first_valid_cycles, lower_luts=lower_luts, upper_luts=upper_luts, ) @@ -707,6 +782,7 @@ def _minimize_fifo_depth( initial_fifo_depths, sim, sim_cycles, + fifo_first_valid_cycles, ) if success: upper_luts = calculate_srl16e_luts(original_size, bw) @@ -723,6 +799,7 @@ def _minimize_fifo_depth( initial_fifo_depths, sim, sim_cycles, + fifo_first_valid_cycles, lower_luts=lower_luts, upper_luts=upper_luts, ) @@ -742,7 +819,14 @@ def _minimize_fifo_depth( _, max_d = calculate_bram_depth_range(max_valid_blocks, bw) success, timeout = self._test_depth( - max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + max_d, + node_idx, + fifo_idx, + baseline_depths, + initial_fifo_depths, + sim, + sim_cycles, + fifo_first_valid_cycles, ) if timeout or not success: @@ -760,6 +844,7 @@ def _minimize_fifo_depth( initial_fifo_depths, sim, sim_cycles, + fifo_first_valid_cycles, valid_blocks=valid_blocks, ) @@ -774,6 +859,7 @@ def _exponential_binary_search_depth( initial_fifo_depths: dict, sim: Simulation, sim_cycles: float, + fifo_first_valid_cycles: list[list[int]], valid_blocks: list[int], ) -> int: """Perform exponential + binary search over valid block configurations. @@ -790,6 +876,7 @@ def _exponential_binary_search_depth( initial_fifo_depths: Baseline performance data sim: Simulation controller sim_cycles: Maximum simulation cycles + fifo_first_valid_cycles: First valid cycle for each FIFO valid_blocks: Sorted list of valid block counts to search over Returns: @@ -814,7 +901,14 @@ def _exponential_binary_search_depth( _, max_d = calculate_bram_depth_range(blocks, bitwidth) success, _ = self._test_depth( - max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + max_d, + node_idx, + fifo_idx, + baseline_depths, + initial_fifo_depths, + sim, + sim_cycles, + fifo_first_valid_cycles, ) if success: @@ -834,7 +928,14 @@ def _exponential_binary_search_depth( _, max_d = calculate_bram_depth_range(blocks, bitwidth) success, _ = self._test_depth( - max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + max_d, + node_idx, + fifo_idx, + baseline_depths, + initial_fifo_depths, + sim, + sim_cycles, + fifo_first_valid_cycles, ) if success: @@ -856,6 +957,7 @@ def _binary_search_srl_depth( initial_fifo_depths: dict, sim: Simulation, sim_cycles: float, + fifo_first_valid_cycles: list[list[int]], lower_luts: int, upper_luts: int, ) -> int: @@ -869,6 +971,7 @@ def _binary_search_srl_depth( initial_fifo_depths: Baseline performance data sim: Simulation controller sim_cycles: Maximum simulation cycles + fifo_first_valid_cycles: First valid cycle for each FIFO lower_luts: Lower bound for LUT count upper_luts: Upper bound for LUT count (known to work) @@ -896,7 +999,14 @@ def _binary_search_srl_depth( continue success, _ = self._test_depth( - max_d, node_idx, fifo_idx, baseline_depths, initial_fifo_depths, sim, sim_cycles + max_d, + node_idx, + fifo_idx, + baseline_depths, + initial_fifo_depths, + sim, + sim_cycles, + fifo_first_valid_cycles, ) if success: diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index b06b56e3b1..feaff91bad 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -1,6 +1,7 @@ """Control (node based) simulations via unix sockets.""" import json +import os import socket import subprocess import threading @@ -63,12 +64,13 @@ def __init__( self.should_stop = False self.stop_lock = Lock() - def _start_process(self, binary: Path, process_id: int) -> int: + def _start_process(self, binary: Path, process_id: int, cpu: int = -1) -> int: """Start a single C++ simulation process with its own Unix socket. Args: binary: Path to the simulation executable process_id: Unique identifier for this process + cpu: CPU core to bind to (if -1, no binding) Returns: Index of the started process @@ -97,7 +99,11 @@ def _start_process(self, binary: Path, process_id: int) -> int: # Start C++ process - redirect stdout/stderr to files cwd = binary.parent - proc = subprocess.Popen(cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd) + # Set CPU affinity if a specific core is requested + preexec_fn = (lambda: os.sched_setaffinity(0, {cpu})) if cpu != -1 else None + proc = subprocess.Popen( + cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd, preexec_fn=preexec_fn + ) # Check if process started successfully time.sleep(0.2) # Give process time to fail if there's an immediate error From 5d367bf3b23e3eb8b37ccbdae8d8f152680ac4d4 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Wed, 18 Feb 2026 17:06:35 +0100 Subject: [PATCH 071/170] Various fixes to isolated simulation --- .../finn_xsi/IsolatedSimulationBackend.cpp | 2 +- .../finn_xsi/include/IsolatedSimulation.hpp | 1 + .../transformation/fpgadataflow/simulation.py | 2 +- .../fpgadataflow/simulation_connected.py | 2 +- .../fpgadataflow/simulation_isolated.py | 21 ++++++++++++------- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp index ce1a46b381..92de4a1772 100644 --- a/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/IsolatedSimulationBackend.cpp @@ -30,7 +30,7 @@ int main(int argc, const char* argv[]) { RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, "xsim_log_file.txt", - "trace_file.txt", + "trace_file.wdb", RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs ); diff --git a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp index 99abbdc66f..8b9636efc0 100644 --- a/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp +++ b/finn_xsi/finn_xsi/include/IsolatedSimulation.hpp @@ -115,6 +115,7 @@ class IsolatedSimulation : public Simulation kernel_lib, design_lib, xsim_log_file, trace_file, _istream_descs, _ostream_descs ), simState(*this), readyJson(json::array()), validJson(json::array()), readylogName("readylog.txt"), validlogName("validlog.txt") { + // TODO: Clearly split names between connected and isolated sim (ready_log.txt and readylog.txt) inJobSizes.resize(_istream_descs.size()); outJobSizes.resize(_ostream_descs.size()); std::transform( diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index f84c7a6c36..fb77e50cfd 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -79,7 +79,7 @@ def store_fifo_data( merged = pd.merge( data, pd.read_csv(fifo_data_path), on=merge_on, how=merge_how # type: ignore ) - merged.sort_values(sort_on) + merged = merged.sort_values(sort_on) merged.to_csv(fifo_data_path, index=False) if store_html: merged.to_html(fifo_data_path.replace(".csv", ".html")) diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 7b139a9ad6..bf7a315ea7 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -487,7 +487,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: "out_bitwidth": -1, "out_initial_fifo_depths": -1, "out_final_fifo_depths": -1, - "minimization_iterations": -1, + "minimization_iterations": 0, "minimization_order": "TODO", # TODO "simulation_time": -1, "successor_node": ", ".join( diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index a813df3414..a597918c45 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -323,10 +323,14 @@ def _any_ready(cycle_data: dict[str, int]) -> bool: for stream_name in data[layer]["ready"][0].keys() if stream_name not in ["inputCyclesDone", "inputCyclesTarget", "totalCycles"] } - for cycle_data in data[layer]["ready"]: - if cycle_data["inputCyclesDone"] > int( - cycle_data["inputCyclesTarget"] / 2 - ) and _any_ready(cycle_data): + for cycle_data_ready, cycle_data_valid in zip( + data[layer]["ready"], data[layer]["valid"], strict=True + ): + if cycle_data_ready["inputCyclesDone"] > int( + cycle_data_ready["inputCyclesTarget"] / 2.0 + ) and cycle_data_valid["outputCyclesDone"] > int( + cycle_data_valid["outputCyclesTarget"] / 2.0 + ): break for stream_name in results[layer].keys(): # TODO: Currently on the C++ side we multiply the @@ -334,16 +338,19 @@ def _any_ready(cycle_data: dict[str, int]) -> bool: # TODO: We keep track of ready signals until we see # TODO: the first ready after half of all cycles were seen. # TODO: This might change in the future - if cycle_data["inputCyclesTarget"] % 2 != 0: + if ( + cycle_data_ready["inputCyclesTarget"] % 2 != 0 + or cycle_data_valid["outputCyclesTarget"] % 2 != 0 + ): raise FINNInternalError( - f"An 'inputCyclesTarget' of layer {layer} seems " + f"An 'inputCyclesTarget' / 'outputCyclesTarget' of layer {layer} seems " f"to not be an even number. Currently, we double " f"the target simulation cycles for every layer " f"on the C++ side. This error may point towards " f"a change on the C++ side, which may cause the " f"need to update this function accordingly!" ) - results[layer][stream_name] += int(cycle_data[stream_name] == 0) + results[layer][stream_name] += int(cycle_data_ready[stream_name] == 0) # TODO: This calculation assumes, that if the producer does NOT fire the entire time, # TODO: the consumer can read at least at the same speed as From 89d8436771fd656efd41427cd24c04eb43c21cc6 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:21:32 +0100 Subject: [PATCH 072/170] Adjust grace cycles to be dynamic --- src/finn/transformation/fpgadataflow/simulation_connected.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 46ee718649..23e09d3af0 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -548,8 +548,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: fifo_first_valid_cycles: list[list[int]] = [] for val in initial_fifo_depths.values(): fifo_first_valid_cycles.append( - [v + 10 for v in val["fifo_cycles_until_first_valid"]] - ) # Add 10 cycles grace period + [v + math.ceil(v*0.01) for v in val["fifo_cycles_until_first_valid"]] + ) # Add 1% cycles grace period # Max cycles for any simulation sim_cycles: int = max([val["cycles"] for val in initial_fifo_depths.values()]) # type: ignore From ee425d2d4d014964a14f672e5d07e3be16a13317 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:17:59 +0100 Subject: [PATCH 073/170] Linting --- .../transformation/fpgadataflow/simulation.py | 2 +- .../fpgadataflow/simulation_connected.py | 34 +++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 4fe7a3ee68..59303f0a34 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -16,7 +16,7 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log -FIFODepthConfig: TypeAlias = dict[str, dict[str, str | list[int]]] +FIFODepthConfig: TypeAlias = dict[str, dict[str, list[int]]] def store_fifo_data( diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index ce0863f410..7bba816388 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -162,9 +162,9 @@ def run( cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[ - sim_name - ] = fifo_cycles_until_first_valid + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -199,9 +199,9 @@ def run( ) = result # Only update if not already collected if sim_name not in fifo_results: - fifo_cycles_until_first_valid_results[ - sim_name - ] = fifo_cycles_until_first_valid + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles @@ -460,7 +460,7 @@ def simulate( depth: int | list[list[int]] | None = None, max_cycles: int | None = None, fifo_first_valid_cycles: list[list[int]] | None = None, - ) -> tuple[dict[int, dict[str, str | list[int]]], bool]: + ) -> tuple[dict[int, dict[str, list[int]]], bool]: """Simulate the given number of samples for every layer. Layers are completely isolated and simulated in parallel. Simulation data is returned as a dict (by node name as index). """ @@ -547,6 +547,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: "out_bitwidth": -1, "out_initial_fifo_depths": -1, "out_final_fifo_depths": -1, + "fifo_cycles_until_first_valid": -1, "minimization_iterations": 0, "minimization_order": "TODO", # TODO "simulation_time": -1, @@ -572,21 +573,22 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: for idx in range(len(layerdata["fifo_utilization"])): name: str = cast("str", layerdata["name"]) df_data[name][idx]["out_initial_fifo_depths"] = layerdata["fifo_utilization"][idx] + df_data[name][idx]["fifo_cycles_until_first_valid"] = layerdata[ + "fifo_cycles_until_first_valid" + ][idx] # Create fifo_depths (indexed by layer index and then stream index) fifo_depths: list[list[int]] = [] # Each entry is a list of fifo sizes for that node for val in initial_fifo_depths.values(): - fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) + fifo_depths.append([max(v + 1, 32) for v in val["fifo_utilization"]]) fifo_first_valid_cycles: list[list[int]] = [] for val in initial_fifo_depths.values(): fifo_first_valid_cycles.append( - [v + math.ceil(v*0.01) for v in val["fifo_cycles_until_first_valid"]] + [v + math.ceil(v * 0.01) for v in val["fifo_cycles_until_first_valid"]] ) # Add 1% cycles grace period # Max cycles for any simulation - sim_cycles: int = max( - [val["cycles"] for val in initial_fifo_depths.values()] - ) # type: ignore + sim_cycles: int = max([val["cycles"] for val in initial_fifo_depths.values()]) # type: ignore # Extract bitwidths from outstream widths of hw nodes bit_widths = [] @@ -654,7 +656,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: percentage = int(100.0 * float(i + 1) / float(len(fifo_depths))) log.info( f"[ [bold green]{percentage}%[/bold green] ] " - f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed " + f"[ {i + 1}.{j + 1} / {len(fifo_depths)} ] Simulation completed " f"({iterations_needed} iterations).", extra={"markup": True, "highlighter": None}, ) @@ -749,7 +751,9 @@ def _test_depth( new_data, timeout = sim.simulate( test_depths, - max_cycles=min(math.ceil(sim_cycles * 1.05), sim_cycles + 10 * len(test_depths)), + max_cycles=min( + math.ceil(sim_cycles * 1.05), math.ceil(sim_cycles) + 10 * len(test_depths) + ), fifo_first_valid_cycles=fifo_first_valid_cycles, ) @@ -896,7 +900,7 @@ def _minimize_fifo_depth( valid_blocks = self._get_valid_block_counts(1, upper_blocks - 1, bw) if not valid_blocks: # No valid configurations exist - return original_size + return original_size, iterations # Test the maximum valid block count first (smallest depth) max_valid_blocks = valid_blocks[-1] _, max_d = calculate_bram_depth_range(max_valid_blocks, bw) From c6b9cdbdb4fed586b27884359ecddd2b3f7483d7 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 24 Feb 2026 17:40:28 +0100 Subject: [PATCH 074/170] Added minimization order options --- .../fpgadataflow/simulation_connected.py | 287 ++++++++++++++---- 1 file changed, 220 insertions(+), 67 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index e9940350ec..e9f8eaf3eb 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -1,5 +1,4 @@ """Node connected parallel simulations.""" - import glob import json import math @@ -8,6 +7,8 @@ import time import traceback from concurrent.futures import Future, ThreadPoolExecutor +from copy import deepcopy +from enum import Enum from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp @@ -25,6 +26,24 @@ from finn.util.logging import log +class MinimizationOrder(Enum): + """The order in which the search algorithm minimizes the FIFO depths.""" + + NODE_ORDER = 0 + REVERSE_NODE_ORDER = 1 + LARGEST_BITWIDTH_DIFF_FIRST = 2 + SMALLEST_BITWIDTH_DIFF_FIRST = 3 + + # Non black-box model orders + AFTER_THRESHOLDS_FIRST = 4 + AFTER_DWC_FIRST = 5 + + # Half black-box + # If we ran a sim before, we know the largest FIFOs, so start with these. + # This strategy might work, if the changes to the model are small enough + REUSE_PREVIOUS_ORDER = 6 + + class NodeConnectedSimulationController(SimulationController): """Run simulations for node connected cases.""" @@ -482,7 +501,6 @@ def simulate( controller.run(initial_depth, output_json, max_cycles, fifo_first_valid_cycles) end = time.time() log.debug(f"Simulation took {end - start} seconds!") - print(f"Simulation took {end - start} seconds!") # Load the merged data from JSON merged_data = json.loads(output_json.read_text()) @@ -509,6 +527,7 @@ def __init__( fpgapart: str, clk_ns: float, cfg: DataflowBuildConfig, + minimization_orders: list[MinimizationOrder] | None = None, max_qsrl_depth: int = 256, vivado_ram_style: str = "auto", quality_of_results: str = "default", @@ -521,6 +540,76 @@ def __init__( self.max_qsrl_depth = max_qsrl_depth self.vivado_ram_style = vivado_ram_style self.quality_of_results = quality_of_results + if minimization_orders is not None: + self.minimization_orders = minimization_orders + else: + self.minimization_orders = [ + MinimizationOrder.LARGEST_BITWIDTH_DIFF_FIRST, + MinimizationOrder.NODE_ORDER, + MinimizationOrder.REVERSE_NODE_ORDER, + MinimizationOrder.SMALLEST_BITWIDTH_DIFF_FIRST, + ] + + self.final_depths: dict[MinimizationOrder, list[list[int]] | None] = dict.fromkeys( + self.minimization_orders + ) + + def create_starting_fifo_depths( + self, initial_fifo_depths: dict + ) -> tuple[list[list[int]], list[list[int]]]: + """From the given initial_fifo_depths returned by the simulation, create a starting + FIFO depth configuration that can be modified sequentially by the minimization algorithm. + Also return the fifo_first_valid_cycles. + """ + # Create fifo_depths (indexed by layer index and then stream index) + fifo_depths: list[list[int]] = [] # Each entry is a list of fifo sizes for that node + for val in initial_fifo_depths.values(): + fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) + fifo_first_valid_cycles: list[list[int]] = [] + for val in initial_fifo_depths.values(): + fifo_first_valid_cycles.append( + [v + 10 for v in val["fifo_cycles_until_first_valid"]] + ) # Add 10 cycles grace period + return fifo_depths, fifo_first_valid_cycles + + def get_minimization_order_indices( + self, + min_order: MinimizationOrder, + model: ModelWrapper, + bitwidths: list[int], + ) -> list[int]: + """Given a MinimizationOrder, return the list of indices to + access/minimize `fifo_depths` for that order. For example, NODE_ORDER would return + [0,1,2,...] and NODE_ORDER_REVERSED [N, N-1, N-2, ..., 0]. + """ + assert len(model.graph.node) == len(bitwidths) + match min_order: + case MinimizationOrder.NODE_ORDER: + return list(range(len(model.graph.node))) + case MinimizationOrder.REVERSE_NODE_ORDER: + return list(range(len(model.graph.node)))[::-1] + case ( + MinimizationOrder.LARGEST_BITWIDTH_DIFF_FIRST + | MinimizationOrder.SMALLEST_BITWIDTH_DIFF_FIRST + ): + diffs: list[tuple[int, int]] = [] # (index, diff) + for i in range(len(model.graph.node)): + hw: HWCustomOp = getCustomOp(model.graph.node[i]) + in_width = max( + [hw.get_instream_width(j) for j in range(len(model.graph.node[i].input))] + ) + out_width = max( + [hw.get_outstream_width(j) for j in range(len(model.graph.node[i].output))] + ) + diffs.append((i, in_width - out_width)) + sorted_order = sorted( + diffs, + key=lambda x: x[1], + reverse=(min_order == MinimizationOrder.LARGEST_BITWIDTH_DIFF_FIRST), + ) + return [idx for idx, diff in sorted_order] + case _: + raise NotImplementedError() def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run layer parallel simulations.""" @@ -546,15 +635,19 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: "onnx_index": nodeindex, "out_bitwidth": -1, "out_initial_fifo_depths": -1, - "out_final_fifo_depths": -1, - "minimization_iterations": 0, - "minimization_order": "TODO", # TODO - "simulation_time": -1, "successor_node": ", ".join( [node.name for node in model.find_consumers(node.output[i])] ), } ) + for min_order in self.minimization_orders: + df_data[node.name][-1][f"out_final_depth_{min_order.name}"] = -1 + df_data[node.name][-1][f"simulation_time_{min_order.name}"] = -1 + df_data[node.name][-1][f"minimization_iterations_{min_order.name}"] = -1 + + # TODO: The final depths contained a lot of -1 (default values). + # Did we need to write the initial depths into there? + # Or in case of minimization skip we likely need to write the values still. # Running the initial simulation log.info("Running initial node-connected simulation.") @@ -573,15 +666,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: name: str = cast("str", layerdata["name"]) df_data[name][idx]["out_initial_fifo_depths"] = layerdata["fifo_utilization"][idx] - # Create fifo_depths (indexed by layer index and then stream index) - fifo_depths: list[list[int]] = [] # Each entry is a list of fifo sizes for that node - for val in initial_fifo_depths.values(): - fifo_depths.append([v + 1 for v in val["fifo_utilization"]]) - fifo_first_valid_cycles: list[list[int]] = [] - for val in initial_fifo_depths.values(): - fifo_first_valid_cycles.append( - [v + 10 for v in val["fifo_cycles_until_first_valid"]] - ) # Add 10 cycles grace period + # List of list of fifo depths + fifo_depths, fifo_first_valid_cycles = self.create_starting_fifo_depths(initial_fifo_depths) # Max cycles for any simulation sim_cycles: int = max( @@ -621,65 +707,96 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Preserve original baseline depths for testing (deep copy) original_fifo_depths = [row[:] for row in fifo_depths] - # Minimize FIFO depths using binary search over BRAM block counts - for i in range(len(fifo_depths)): - for j in range(len(fifo_depths[i])): - if not needs_minimization[i][j]: - df_data[model.graph.node[i].name][j]["simulation_time"] = 0.0 + # Total minimizations + total_minimizations = sum(len(streams) for streams in fifo_depths) + + for k, minimization_order in enumerate(self.minimization_orders): + # Create a new empty FIFO depth list + fifo_depths, fifo_first_valid_cycles = self.create_starting_fifo_depths( + initial_fifo_depths + ) + + # Minimize FIFO depths using binary search over BRAM block counts + idx_order = self.get_minimization_order_indices(minimization_order, model, bit_widths) + + log.info( + f"Minimizing using order: {minimization_order.name}. Index order is: {idx_order}" + ) + + done = 0 + for i in idx_order: + for j in range(len(fifo_depths[i])): + if not needs_minimization[i][j]: + df_data[model.graph.node[i].name][j][ + f"simulation_time_{minimization_order.name}" + ] = 0.0 + df_data[model.graph.node[i].name][j][ + f"out_final_depth_{minimization_order.name}" + ] = fifo_depths[i][j] + df_data[model.graph.node[i].name][j][ + f"minimization_iterations_{minimization_order.name}" + ] = 0 + log.info( + f"[ {i + 1}.{j + 1} / {len(fifo_depths)} ] " + f"Skipping minimization for this stream." + ) + done += 1 + continue + + minimization_start = time.time() + minimized_depth, iterations_needed = self._minimize_fifo_depth( + i, + j, + fifo_depths, + original_fifo_depths, + bit_widths, + initial_fifo_depths, + sim, + sim_cycles, + fifo_first_valid_cycles, + ) + minimization_time = time.time() - minimization_start + + # Store the minimized size + fifo_depths[i][j] = minimized_depth + done += 1 + + # Store data into dataframe + df_data[model.graph.node[i].name][j][ + f"simulation_time_{minimization_order.name}" + ] = minimization_time + df_data[model.graph.node[i].name][j][ + f"minimization_iterations_{minimization_order.name}" + ] = iterations_needed + df_data[model.graph.node[i].name][j][ + f"out_final_depth_{minimization_order.name}" + ] = fifo_depths[i][j] log.debug( - f"[ {i + 1}.{j + 1} / {len(fifo_depths)} ] " - f"Skipping minimization for this stream." + f"Set node/stream {i}.{j} to depth {fifo_depths[i][j]}, in " + f"{iterations_needed} iterations and {minimization_time} " + f"seconds. (To {minimization_order.name})" ) - continue - - minimization_start = time.time() - minimized_depth, iterations_needed = self._minimize_fifo_depth( - i, - j, - fifo_depths, - original_fifo_depths, - bit_widths, - initial_fifo_depths, - sim, - sim_cycles, - fifo_first_valid_cycles, - ) - minimization_time = time.time() - minimization_start - df_data[model.graph.node[i].name][j]["simulation_time"] = minimization_time - df_data[model.graph.node[i].name][j]["minimization_iterations"] = iterations_needed - - # Store the minimized size - fifo_depths[i][j] = minimized_depth - - percentage = int(100.0 * float(i + 1) / float(len(fifo_depths))) - log.info( - f"[ [bold green]{percentage}%[/bold green] ] " - f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed " - f"({iterations_needed} iterations).", - extra={"markup": True, "highlighter": None}, - ) - log.info("Final FIFO depths:") - for i in range(len(fifo_depths)): - log.info(f"{i}: {fifo_depths[i]}") + percentage = int(100.0 * float(done) / float(total_minimizations)) + log.info( + f"[ [bold green]{percentage}%[/bold green] ] " + f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed " + f"({iterations_needed} iterations).", + extra={"markup": True, "highlighter": None}, + ) - # Write back results. By default write to output_dir / "fifo_config.json" - writeback_path = Path(self.cfg.output_dir) / "fifo_config.json" - assert len(fifo_depths) == len(model.graph.node) - json_results = {} - for i in range(len(fifo_depths)): - json_results[i] = {"node": model.graph.node[i].name, "depths": fifo_depths[i]} - with writeback_path.open("w") as f: - json.dump(json_results, f) - log.info(f"Wrote results back to {writeback_path}") + self.final_depths[minimization_order] = deepcopy(fifo_depths) - # Write final FIFO sizes into dataframe - for i in range(len(fifo_depths)): - for j in range(len(fifo_depths[i])): - df_data[model.graph.node[i].name][j]["out_final_fifo_depths"] = fifo_depths[i][j] + order_percent = int(100.0 * float(k + 1) / float(len(self.minimization_orders))) + log.info( + f"[ [bold gold1]{order_percent}%[/bold gold1] ] " + f"----- Minimization order {minimization_order.name} completed -----", + extra={"markup": True, "highlighter": None}, + ) # Store dataframe df_keys = list(df_data[model.graph.node[0].name][0].keys()) + log.debug(f"Saving keys: {df_keys} + [node, stream]") df_dict = {} df_dict["node"] = [] df_dict["stream"] = [] @@ -689,7 +806,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: for streamindex, streamdata in enumerate(nodedata): df_dict["node"].append(node) df_dict["stream"].append(streamindex) - for key in df_keys: + for key in streamdata.keys(): df_dict[key].append(streamdata[key]) df = pd.DataFrame(df_dict) @@ -701,6 +818,42 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: store_html=True, ) + # Use the smallest fifo depths found (by total bytes) + smallest_order = self.minimization_orders[0] + smallest_size = None + for order in self.minimization_orders: + current_size = 0 + depths = self.final_depths[order] + if depths is None: + raise FINNInternalError( + f"Expected FIFO sizes for minimization order " f"{order.name}, but found None." + ) + for i in range(len(depths)): + for j in range(len(depths[i])): + current_size += depths[i][j] * bit_widths[i][j] + + if smallest_size is None or current_size < smallest_size: + smallest_size = current_size + smallest_order = order + + # Set the result fifo depths + fifo_depths = self.final_depths[smallest_order] + assert fifo_depths is not None + + log.info("Final FIFO depths:") + for i in range(len(fifo_depths)): + log.info(f"{i}: {fifo_depths[i]}") + + # Write back results. By default write to output_dir / "fifo_config.json" + writeback_path = Path(self.cfg.output_dir) / "fifo_config.json" + assert len(fifo_depths) == len(model.graph.node) + json_results = {} + for i in range(len(fifo_depths)): + json_results[i] = {"node": model.graph.node[i].name, "depths": fifo_depths[i]} + with writeback_path.open("w") as f: + json.dump(json_results, f) + log.info(f"Wrote results back to {writeback_path}") + return model, False def _check_performance(self, new_data: dict, initial_fifo_depths: dict) -> bool: From 1acbddbbb85659c4bf9bbe3ce1d2af58bbcf07c2 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:16:51 +0100 Subject: [PATCH 075/170] Fix multi connection models --- finn_xsi/finn_xsi/adapter.py | 4 ++-- src/finn/transformation/fpgadataflow/simulation.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 5ad07a6ab1..037a5dd4ca 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -103,8 +103,8 @@ def compile_sim_obj(top_module_name, source_list, sim_out_dir, debug=False): cmd_xvlog = "xvlog --incr --relax -prj rtlsim.prj".split() - launch_process_helper(cmd_xvlog, cwd=sim_out_dir) - launch_process_helper(cmd_xelab, cwd=sim_out_dir) + launch_process_helper(cmd_xvlog, cwd=sim_out_dir, print_stdout=False) + launch_process_helper(cmd_xelab, cwd=sim_out_dir, print_stdout=False) out_so_relative_path = "xsim.dir/%s/xsimk.so" % top_module_name out_so_full_path = sim_out_dir + "/" + out_so_relative_path diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 59303f0a34..6cbbcd5a47 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -52,8 +52,16 @@ def store_fifo_data( Returns: model: Return the model since we might have modified its metadata. """ - # TODO: Check if all layers are accounted for - if len(data.index) != len(model.graph.node): + # Check if all layers are accounted for + # Note: data may have multiple rows per node (one per output stream) + if "node" in data.columns: + num_unique_nodes = len(data["node"].unique()) + if num_unique_nodes != len(model.graph.node): + raise FINNInternalError( + f"Tried storing FIFO data for {num_unique_nodes} unique nodes " + f"but expected {len(model.graph.node)}" + ) + elif len(data.index) != len(model.graph.node): raise FINNInternalError( f"Tried storing FIFO data for {len(data.index)} " f"values but expected {len(model.graph.node)}" From 3772d9c3a3d18c77c10d1387b127aac0c5adf736 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:23:24 +0100 Subject: [PATCH 076/170] Small fixes --- finn_xsi/finn_xsi/adapter.py | 4 +- src/finn/builder/build_dataflow_steps.py | 42 +++++++++---------- .../custom_op/fpgadataflow/thresholding.py | 4 +- .../fpgadataflow/set_fifo_depths.py | 36 ++++++++-------- .../fpgadataflow/simulation_build.py | 6 +-- .../fpgadataflow/simulation_connected.py | 24 +++++++---- 6 files changed, 62 insertions(+), 54 deletions(-) diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 5ad07a6ab1..037a5dd4ca 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -103,8 +103,8 @@ def compile_sim_obj(top_module_name, source_list, sim_out_dir, debug=False): cmd_xvlog = "xvlog --incr --relax -prj rtlsim.prj".split() - launch_process_helper(cmd_xvlog, cwd=sim_out_dir) - launch_process_helper(cmd_xelab, cwd=sim_out_dir) + launch_process_helper(cmd_xvlog, cwd=sim_out_dir, print_stdout=False) + launch_process_helper(cmd_xelab, cwd=sim_out_dir, print_stdout=False) out_so_relative_path = "xsim.dir/%s/xsimk.so" % top_module_name out_so_full_path = sim_out_dir + "/" + out_so_relative_path diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 25f06a41e6..d4c1df0d69 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -28,7 +28,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Collection of default build steps for building and verifying a dataflow - accelerator from an ONNX model.""" +accelerator from an ONNX model.""" import json import numpy as np @@ -638,56 +638,56 @@ def step_hw_ipgen(model: ModelWrapper, cfg: DataflowBuildConfig): return model - - - - # TODO: Both this and the step_size_... steps will be reworked before merging into dev def step_build_simulation(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Build the simulation binaries for isolated and connected simulations.""" - from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType + from finn.transformation.fpgadataflow.simulation_build import BuildSimulation + model = model.transform( BuildSimulation( - cfg._resolve_fpga_part(), # noqa - cfg._resolve_hls_clk_period(), # noqa + cfg._resolve_fpga_part(), # noqa + cfg._resolve_hls_clk_period(), # noqa cfg.functional_simulation, ) ) return model + def step_size_fifo_isolated(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Simulate layers in isolation and use the observed behaviour to size the FIFOs accordingly.""" - from finn.transformation.fpgadataflow.simulation_isolated import RunLayerIsolatedSimulation from pathlib import Path + + from finn.transformation.fpgadataflow.simulation_isolated import RunLayerIsolatedSimulation + model = model.transform( - RunLayerIsolatedSimulation( - cfg._resolve_fpga_part(), # noqa - cfg._resolve_hls_clk_period(), # noqa + RunLayerIsolatedSimulation( + cfg._resolve_fpga_part(), # noqa + cfg._resolve_hls_clk_period(), # noqa cfg.functional_simulation, - Path(cfg.output_dir) - ) + Path(cfg.output_dir), + ) ) return model + def step_size_fifo_connected(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Simulate layers connected and use the observed behaviour to size the FIFOs accordingly.""" from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation + model = model.transform( RunLayerParallelSimulation( - cfg._resolve_fpga_part(), # noqa - cfg._resolve_hls_clk_period(), # noqa - cfg - ) + cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg # noqa # noqa + ) ) + model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) return model + def step_apply_fifosizes(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Apply the previously found FIFO sizes to the model.""" from finn.transformation.fpgadataflow.simulation import ApplyFIFOSizes - return model.transform(ApplyFIFOSizes(cfg)) - - + return model.transform(ApplyFIFOSizes(cfg)) def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig): diff --git a/src/finn/custom_op/fpgadataflow/thresholding.py b/src/finn/custom_op/fpgadataflow/thresholding.py index 14e91ae0bd..cbee07447f 100644 --- a/src/finn/custom_op/fpgadataflow/thresholding.py +++ b/src/finn/custom_op/fpgadataflow/thresholding.py @@ -135,8 +135,8 @@ def minimize_accumulator_width(self, model): max_threshold = thresholds.max() min_input = self.get_input_datatype(0).min() max_input = self.get_input_datatype(0).max() - tdt_min = min(min_input, min_threshold) - tdt_max = max(max_input, max_threshold) + tdt_min = float(min(min_input, min_threshold)) + tdt_max = float(max(max_input, max_threshold)) if tdt_min < 0: if abs(tdt_min) > tdt_max: tdt = DataType.get_smallest_possible(tdt_min) diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index 18eebfb4e8..796c25bee4 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -505,24 +505,23 @@ def apply(self, model): return (model, False) -def get_fifo_split_configs(depth, max_qsrl_depth=256, max_vivado_depth=32768): - """Break non-power-of-2 sized FIFO depths into several ones""" +def get_fifo_split_configs( + depth: int, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768 +) -> list[tuple[int, str]]: + """Break non-power-of-2 sized FIFO depths into several ones.""" - def floor_pow2(x): + def floor_pow2(x: int) -> int: if (x & (x - 1) == 0) and x != 0: return x - else: - return 1 << ((x - 1).bit_length() - 1) + return 1 << ((x - 1).bit_length() - 1) - def decompose_pow2(x): + def decompose_pow2(x: int) -> list[int]: if x <= max_qsrl_depth: return [x] - else: - r = floor_pow2(x) - if x == r: - return [x] - else: - return [r, *decompose_pow2(x - r)] + r = floor_pow2(x) + if x == r: + return [x] + return [r, *decompose_pow2(x - r)] ret = [] # trivial case: for small FIFOs, return as-is with rtl style @@ -557,7 +556,7 @@ def decompose_pow2(x): class SplitLargeFIFOs(Transformation): - """Split large FIFOs before implementation, for two reasons: + """Split large FIFOs before implementation, for two reasons. - impl_style="vivado" supports a max depth of 32k. Any larger FIFOs must be implemented as a sequence of smaller FIFOs. @@ -569,7 +568,7 @@ class SplitLargeFIFOs(Transformation): """ - def __init__(self, max_qsrl_depth=256, max_vivado_depth=32768): + def __init__(self, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768): super().__init__() self.max_qsrl_depth = max_qsrl_depth self.max_vivado_depth = max_vivado_depth @@ -590,11 +589,12 @@ def apply(self, model): dtype = n_inst.get_nodeattr("dataType") ram_style = n_inst.get_nodeattr("ram_style") shape = model.get_tensor_shape(node.input[0]) + log.info( + f"Splitting FIFO {node.name} of depth {depth} " + f"into {len(cfgs)} FIFOs with depths {[c[0] for c in cfgs]}" + ) for i, (fifo_depth, impl_style) in enumerate(cfgs): - if i == 0: - inp = node.input[0] - else: - inp = node.name + "_" + str(i - 1) + "_out" + inp = node.input[0] if i == 0 else node.name + "_" + str(i - 1) + "_out" if i == len(cfgs) - 1: outp = node.output[0] else: diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 8518ff8365..a21102b8f0 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -259,9 +259,9 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: for attr in target_op_attrs.keys(): attr_val = target_op.get_nodeattr(attr) if ( - attr_val == "" + (isinstance(attr_val, np.ndarray) and attr_val.size == 0) + or attr_val == "" or attr_val == [] - or (isinstance(attr_val, np.ndarray) and attr_val.size == 0) ): # Empty value, skip continue params[attr] = target_op.get_nodeattr(attr) @@ -651,7 +651,7 @@ def _f(f: Future) -> None: built_nodes += 1 log.info( f"[ [bold green]" - f"{int(100.0*float(built_nodes)/float(total_nodes))}%[/bold green]" + f"{int(100.0 * float(built_nodes) / float(total_nodes))}%[/bold green]" f" ] {name}", extra={"markup": True, "highlighter": None}, ) diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index ce0863f410..b130810324 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -60,9 +60,9 @@ def _cleanup_shm_resources(self) -> None: for pattern in shm_patterns: for filepath in glob.glob(pattern): try: - os.unlink(filepath) + Path(filepath).unlink() removed_count += 1 - except (FileNotFoundError, PermissionError): + except (FileNotFoundError, PermissionError): # noqa: PERF203 # File might already be removed or we don't have permission pass @@ -460,7 +460,7 @@ def simulate( depth: int | list[list[int]] | None = None, max_cycles: int | None = None, fifo_first_valid_cycles: list[list[int]] | None = None, - ) -> tuple[dict[int, dict[str, str | list[int]]], bool]: + ) -> tuple[dict[int, dict[str, list[int]]], bool]: """Simulate the given number of samples for every layer. Layers are completely isolated and simulated in parallel. Simulation data is returned as a dict (by node name as index). """ @@ -580,13 +580,11 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: fifo_first_valid_cycles: list[list[int]] = [] for val in initial_fifo_depths.values(): fifo_first_valid_cycles.append( - [v + math.ceil(v*0.01) for v in val["fifo_cycles_until_first_valid"]] + [v + math.ceil(v * 0.01) for v in val["fifo_cycles_until_first_valid"]] ) # Add 1% cycles grace period # Max cycles for any simulation - sim_cycles: int = max( - [val["cycles"] for val in initial_fifo_depths.values()] - ) # type: ignore + sim_cycles: int = cast("int", max([val["cycles"] for val in initial_fifo_depths.values()])) # Extract bitwidths from outstream widths of hw nodes bit_widths = [] @@ -654,11 +652,21 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: percentage = int(100.0 * float(i + 1) / float(len(fifo_depths))) log.info( f"[ [bold green]{percentage}%[/bold green] ] " - f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed " + f"[ {i + 1}.{j + 1} / {len(fifo_depths)} ] Simulation completed " f"({iterations_needed} iterations).", extra={"markup": True, "highlighter": None}, ) + # Make sure that all FIFOs with depth > 256 use a full BRAM block, + # since partial blocks are not supported by Vivado HLS + for i in range(len(fifo_depths)): + for j in range(len(fifo_depths[i])): + if fifo_depths[i][j] > 256: + bw = bit_widths[i][j] + blocks = calculate_bram_blocks(fifo_depths[i][j], bw) + _, max_d = calculate_bram_depth_range(blocks, bw) + fifo_depths[i][j] = max_d + log.info("Final FIFO depths:") for i in range(len(fifo_depths)): log.info(f"{i}: {fifo_depths[i]}") From ead07ce07cd6b23b471ca3158cf79d8ce8d9a03d Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:40:00 +0100 Subject: [PATCH 077/170] Small changes --- src/finn/builder/build_dataflow_steps.py | 5 +++-- src/finn/transformation/fpgadataflow/simulation.py | 9 +++++++++ .../fpgadataflow/simulation_connected.py | 12 ++++-------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index d4c1df0d69..d3ab1cceff 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -679,7 +679,6 @@ def step_size_fifo_connected(model: ModelWrapper, cfg: DataflowBuildConfig) -> M cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg # noqa # noqa ) ) - model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) return model @@ -687,7 +686,9 @@ def step_apply_fifosizes(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model """Apply the previously found FIFO sizes to the model.""" from finn.transformation.fpgadataflow.simulation import ApplyFIFOSizes - return model.transform(ApplyFIFOSizes(cfg)) + model = model.transform(ApplyFIFOSizes(cfg)) + model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) + return model def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig): diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 6cbbcd5a47..ec94a0d30e 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -227,6 +227,15 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: if successors is not None: n.set_nodeattr("outFIFODepths", [0] * len(successors)) + #TODO: Remove later, just for testing + graph_in_names = [x.name for x in model.graph.input] + for graph_in_name in graph_in_names: + first_node = model.find_consumer(graph_in_name) + if first_node is not None: + n = getCustomOp(first_node) + if n is not None: + n.set_nodeattr("inFIFODepths", [8192]) + # Set new outFIFODepths according to config graph = model.graph node_ind = -1 diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 231555a7e2..8f01ffd155 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -527,7 +527,7 @@ def __init__( fpgapart: str, clk_ns: float, cfg: DataflowBuildConfig, - minimization_orders: list[MinimizationOrder] | None = None, + minimization_orders: list[MinimizationOrder] | None = [MinimizationOrder.NODE_ORDER], max_qsrl_depth: int = 256, vivado_ram_style: str = "auto", quality_of_results: str = "default", @@ -706,9 +706,6 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: needs_minimization[i][j] = self._needs_minimization(used_size, bw) - # Preserve original baseline depths for testing (deep copy) - original_fifo_depths = [row[:] for row in fifo_depths] - # Total minimizations total_minimizations = sum(len(streams) for streams in fifo_depths) @@ -750,7 +747,6 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: i, j, fifo_depths, - original_fifo_depths, bit_widths, initial_fifo_depths, sim, @@ -828,7 +824,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: depths = self.final_depths[order] if depths is None: raise FINNInternalError( - f"Expected FIFO sizes for minimization order " f"{order.name}, but found None." + f"Expected FIFO sizes for minimization order {order.name}, but found None." ) for i in range(len(depths)): for j in range(len(depths[i])): @@ -849,7 +845,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: if fifo_depths[i][j] > 256: bw = bit_widths[i][j] blocks = calculate_bram_blocks(fifo_depths[i][j], bw) - _, max_d = calculate_bram_depth_range(blocks, bw) + blocks_plus_one = self._get_valid_block_counts(blocks+1, blocks+1000, bw) + _, max_d = calculate_bram_depth_range(blocks_plus_one[0], bw) fifo_depths[i][j] = max_d log.info("Final FIFO depths:") @@ -951,7 +948,6 @@ def _minimize_fifo_depth( self, node_idx: int, fifo_idx: int, - current_depths: list, baseline_depths: list, bit_widths: list, initial_fifo_depths: dict, From 4797860ce952cfebd1e2439fa70e85dedb16e86a Mon Sep 17 00:00:00 2001 From: bwintermann Date: Mon, 2 Mar 2026 14:58:15 +0100 Subject: [PATCH 078/170] Added test for diamond communication pattern --- .../InterprocessCommunicationChannel_test.cpp | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/finn_xsi/finn_xsi/unittests/InterprocessCommunicationChannel_test.cpp b/finn_xsi/finn_xsi/unittests/InterprocessCommunicationChannel_test.cpp index 45db5496e7..8952c6e002 100644 --- a/finn_xsi/finn_xsi/unittests/InterprocessCommunicationChannel_test.cpp +++ b/finn_xsi/finn_xsi/unittests/InterprocessCommunicationChannel_test.cpp @@ -167,6 +167,132 @@ TEST_F(InterprocessCommunicationChannelTest, RequestResponseWithDifferentValues) } } +TEST_F(InterprocessCommunicationChannelTest, SingleSplitJoinRequest) { + // Test that a diamond pattern of communication works + pid_t p1 = fork(); + pid_t p2 = fork(); + pid_t p3 = fork(); + std::string leftName = shmName + "_left_in"; + std::string rightName = shmName + "_right_in"; + std::string leftOutName = shmName + "_left_out"; + std::string rightOutName = shmName + "_right_out"; + + if (p1 != 0 && p2 != 0 && p3 != 0) { + // Parent (origin) + InterprocessCommunicationChannel originToLeft(leftName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + InterprocessCommunicationChannel originToRight(rightName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + originToLeft.handshake(); + originToRight.handshake(); + + // Send message to the left + TestRequest reqLeft(100, false); + TestResponse respLeft = originToLeft.send_request(reqLeft); + EXPECT_EQ(respLeft.result, 600); + + // Send message to the right + TestRequest reqRight(130, false); + TestResponse respRight = originToRight.send_request(reqRight); + EXPECT_EQ(respRight.result, 780); + std::cout << "Origin done." << std::endl; + + } else if (p1 == 0 && p2 != 0 && p3 != 0) { + // P1 (Left) + InterprocessCommunicationChannel fromOrigin(leftName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + InterprocessCommunicationChannel toEnd(leftOutName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + fromOrigin.handshake(); + toEnd.handshake(); + + // Receive from origin + TestRequest req = fromOrigin.receive_request(); + EXPECT_EQ(req.value, 100); + EXPECT_FALSE(req.flag); + + // Forward triple + TestRequest reqForward(req.value * 3, req.flag); + TestResponse resp = toEnd.send_request(reqForward); + auto expectedResponseFromEnd = req.value * 2 * 3; + EXPECT_EQ(resp.result, expectedResponseFromEnd); + + // Answer with value from end + TestResponse respOrigin(resp.result, resp.success); + fromOrigin.send_response(respOrigin); + + std::cout << "Left done." << std::endl; + exit((req.value == 100 && resp.result == expectedResponseFromEnd) ? 0 : 1); + + } else if (p1 != 0 && p2 == 0 && p3 != 0) { + // P2 (Right) + InterprocessCommunicationChannel fromOrigin(rightName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + InterprocessCommunicationChannel toEnd(rightOutName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + fromOrigin.handshake(); + toEnd.handshake(); + + // Receive from origin + TestRequest req = fromOrigin.receive_request(); + EXPECT_EQ(req.value, 130); + EXPECT_FALSE(req.flag); + + // Forward triple + TestRequest reqForward(req.value * 3, req.flag); + TestResponse resp = toEnd.send_request(reqForward); + auto expectedResponseFromEnd = req.value * 2 * 3; + EXPECT_EQ(resp.result, expectedResponseFromEnd); + + // Answer with value from end + TestResponse respOrigin(resp.result, resp.success); + fromOrigin.send_response(respOrigin); + + std::cout << "Right done." << std::endl; + exit((req.value == 130 && resp.result == expectedResponseFromEnd) ? 0 : 1); + + } else if (p1 != 0 && p2 != 0 && p3 == 0) { + // End + InterprocessCommunicationChannel endLeft(leftOutName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + InterprocessCommunicationChannel endRight(rightOutName); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + + endLeft.handshake(); + endRight.handshake(); + + // Receive and return double + TestRequest reqLeft = endLeft.receive_request(); + EXPECT_EQ(reqLeft.value, 300); + TestResponse respLeft(reqLeft.value * 2, true); + endLeft.send_response(respLeft); + + // Receive and return double + TestRequest reqRight = endRight.receive_request(); + EXPECT_EQ(reqRight.value, 390); + TestResponse respRight(reqRight.value * 2, true); + endRight.send_response(respRight); + + std::cout << "End done." << std::endl; + exit((reqLeft.value == 300 && reqRight.value == 390) ? 0 : 1); + } + + // Wait for all forks to shut down + if (p1 != 0 && p2 != 0 && p3 != 0) { + int status; + waitpid(p1, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + waitpid(p2, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + waitpid(p3, &status, 0); + EXPECT_EQ(WEXITSTATUS(status), 0); + } + +} + // ===== Multiple Request-Response Tests ===== TEST_F(InterprocessCommunicationChannelTest, MultipleRequestResponseSequential) { From 351554d3c08fcaedbf6deb7598bf76021bac870c Mon Sep 17 00:00:00 2001 From: bwintermann Date: Mon, 2 Mar 2026 15:05:02 +0100 Subject: [PATCH 079/170] Small fixes --- src/finn/transformation/fpgadataflow/simulation.py | 12 ++++++------ .../fpgadataflow/simulation_connected.py | 12 ++++-------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index ec94a0d30e..3f286dc3ab 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -227,14 +227,14 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: if successors is not None: n.set_nodeattr("outFIFODepths", [0] * len(successors)) - #TODO: Remove later, just for testing + # TODO: Remove later, just for testing graph_in_names = [x.name for x in model.graph.input] for graph_in_name in graph_in_names: - first_node = model.find_consumer(graph_in_name) - if first_node is not None: - n = getCustomOp(first_node) - if n is not None: - n.set_nodeattr("inFIFODepths", [8192]) + first_node = model.find_consumer(graph_in_name) + if first_node is not None: + n = getCustomOp(first_node) + if n is not None: + n.set_nodeattr("inFIFODepths", [8192]) # Set new outFIFODepths according to config graph = model.graph diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 8f01ffd155..2e7387224e 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -527,7 +527,7 @@ def __init__( fpgapart: str, clk_ns: float, cfg: DataflowBuildConfig, - minimization_orders: list[MinimizationOrder] | None = [MinimizationOrder.NODE_ORDER], + minimization_orders: list[MinimizationOrder] | None = None, max_qsrl_depth: int = 256, vivado_ram_style: str = "auto", quality_of_results: str = "default", @@ -543,12 +543,8 @@ def __init__( if minimization_orders is not None: self.minimization_orders = minimization_orders else: - self.minimization_orders = [ - MinimizationOrder.LARGEST_BITWIDTH_DIFF_FIRST, - MinimizationOrder.NODE_ORDER, - MinimizationOrder.REVERSE_NODE_ORDER, - MinimizationOrder.SMALLEST_BITWIDTH_DIFF_FIRST, - ] + # TODO: Set to ALL search orders + self.minimization_orders = [MinimizationOrder.NODE_ORDER] self.final_depths: dict[MinimizationOrder, list[list[int]] | None] = dict.fromkeys( self.minimization_orders @@ -845,7 +841,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: if fifo_depths[i][j] > 256: bw = bit_widths[i][j] blocks = calculate_bram_blocks(fifo_depths[i][j], bw) - blocks_plus_one = self._get_valid_block_counts(blocks+1, blocks+1000, bw) + blocks_plus_one = self._get_valid_block_counts(blocks + 1, blocks + 1000, bw) _, max_d = calculate_bram_depth_range(blocks_plus_one[0], bw) fifo_depths[i][j] = max_d From 1d6d10bc531618b94dfa8ff7653bf0cfbe9b352b Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:17:02 +0100 Subject: [PATCH 080/170] Try fixing the simulation deadlock --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 2 +- finn_xsi/finn_xsi/include/SocketServer.h | 2 +- .../transformation/fpgadataflow/simulation.py | 23 +-- .../fpgadataflow/simulation_connected.py | 188 ++++++++++-------- 4 files changed, 116 insertions(+), 99 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index f7140f4b0f..bff105cfbe 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -208,7 +208,7 @@ void process_command(const json& request, json& response, SimulationController& if (command == "configure") { std::vector fifo_depths; - std::cout << "Payload: " << payload << std::endl; + //std::cout << "Payload: " << payload << std::endl; // Handle fifo_depth as either a single value or an array if (payload.contains("fifo_depth")) { diff --git a/finn_xsi/finn_xsi/include/SocketServer.h b/finn_xsi/finn_xsi/include/SocketServer.h index 3b9778d50c..9d7e597b9c 100644 --- a/finn_xsi/finn_xsi/include/SocketServer.h +++ b/finn_xsi/finn_xsi/include/SocketServer.h @@ -6,7 +6,7 @@ #include #include -using json = nlohmann::json; +using json = nlohmann::ordered_json; class SocketServer { private: diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index ec94a0d30e..a3ab1e52b1 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -16,7 +16,7 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log -FIFODepthConfig: TypeAlias = dict[str, dict[str, list[int]]] +FIFODepthConfig: TypeAlias = list[dict[str, list[int]]] def store_fifo_data( @@ -189,7 +189,7 @@ def __init__( else: self.path = fifo_config - self.fifo_depths: FIFODepthConfig = {} + self.fifo_depths: FIFODepthConfig = [] with self.path.open() as f: self.fifo_depths = cast("FIFODepthConfig", json.load(f)) @@ -227,15 +227,6 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: if successors is not None: n.set_nodeattr("outFIFODepths", [0] * len(successors)) - #TODO: Remove later, just for testing - graph_in_names = [x.name for x in model.graph.input] - for graph_in_name in graph_in_names: - first_node = model.find_consumer(graph_in_name) - if first_node is not None: - n = getCustomOp(first_node) - if n is not None: - n.set_nodeattr("inFIFODepths", [8192]) - # Set new outFIFODepths according to config graph = model.graph node_ind = -1 @@ -247,7 +238,15 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: f"Node {first_node.name} does not have a custom op instance." " This is required for FIFO insertion." ) - fifos = cast("list[int]", (self.fifo_depths[str(node_ind)]["depths"])) + if first_node.name != self.fifo_depths[node_ind]["name"]: + raise FINNInternalError( + f"Node name {n0.name} does not match expected name " + f"{self.fifo_depths[node_ind]['name']} at index {node_ind}. " + "This may be due to a mismatch between the model and the config, " + "or due to changes in the model after the simulation was run. " + "Consider re-running the entire flow from start to finish." + ) + fifos = cast("list[int]", (self.fifo_depths[node_ind]["depths"])) n0.set_nodeattr("outFIFODepths", fifos) # Insert the FIFOs into the model diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 8f01ffd155..160b3805e0 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -1,4 +1,5 @@ """Node connected parallel simulations.""" + import glob import json import math @@ -181,9 +182,9 @@ def run( cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[ - sim_name - ] = fifo_cycles_until_first_valid + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -218,9 +219,9 @@ def run( ) = result # Only update if not already collected if sim_name not in fifo_results: - fifo_cycles_until_first_valid_results[ - sim_name - ] = fifo_cycles_until_first_valid + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles @@ -479,9 +480,9 @@ def simulate( depth: int | list[list[int]] | None = None, max_cycles: int | None = None, fifo_first_valid_cycles: list[list[int]] | None = None, - ) -> tuple[dict[int, dict[str, list[int]]], bool]: + ) -> tuple[list[dict[str, list[int]]], bool]: """Simulate the given number of samples for every layer. Layers are completely isolated - and simulated in parallel. Simulation data is returned as a dict (by node name as index). + and simulated in parallel. Simulation data is returned as a list of dicts (by node name as index). """ if self.simulation_type != SimulationType.NODE_BASED_CONNECTED: raise FINNInternalError( @@ -506,9 +507,9 @@ def simulate( merged_data = json.loads(output_json.read_text()) # Return the collected data indexed by node index - data = {} - for i, sim_entry in enumerate(merged_data["simulations"]): - data[i] = { + data = [] + for sim_entry in merged_data["simulations"]: + data.append({ "name": sim_entry["name"], "fifo_utilization": sim_entry["fifo_utilization"], "fifo_depth": sim_entry["fifo_depth"], @@ -516,7 +517,7 @@ def simulate( "samples": sim_entry["samples"], "intervals": sim_entry["intervals"], "fifo_cycles_until_first_valid": sim_entry["fifo_cycles_until_first_valid"], - } + }) json.dump(data, output_json.open("w"), indent=4) return data, merged_data.get("timeout_occurred", False) @@ -555,7 +556,7 @@ def __init__( ) def create_starting_fifo_depths( - self, initial_fifo_depths: dict + self, initial_fifo_depths: list[dict[str, list[int]]] ) -> tuple[list[list[int]], list[list[int]]]: """From the given initial_fifo_depths returned by the simulation, create a starting FIFO depth configuration that can be modified sequentially by the minimization algorithm. @@ -563,10 +564,10 @@ def create_starting_fifo_depths( """ # Create fifo_depths (indexed by layer index and then stream index) fifo_depths: list[list[int]] = [] # Each entry is a list of fifo sizes for that node - for val in initial_fifo_depths.values(): + for val in initial_fifo_depths: fifo_depths.append([max(v + 1, 32) for v in val["fifo_utilization"]]) fifo_first_valid_cycles: list[list[int]] = [] - for val in initial_fifo_depths.values(): + for val in initial_fifo_depths: fifo_first_valid_cycles.append( [v + math.ceil(v * 0.01) for v in val["fifo_cycles_until_first_valid"]] ) # Add 1% cycles grace period @@ -629,7 +630,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: df_data: dict[str, list[dict[str, Any]]] = {} for nodeindex, node in enumerate(model.graph.node): df_data[node.name] = [] - for i in range(len(node.output)): + for node_idx in range(len(node.output)): df_data[node.name].append( { "onnx_index": nodeindex, @@ -637,7 +638,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: "out_initial_fifo_depths": -1, "fifo_cycles_until_first_valid": -1, "successor_node": ", ".join( - [node.name for node in model.find_consumers(node.output[i])] + [node.name for node in model.find_consumers(node.output[node_idx])] ), } ) @@ -662,7 +663,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: log.info(f"Wrote initial sizes to: {initial_sizes_path}") # Store initial sizes in dataframe as well - for layerdata in initial_fifo_depths.values(): + for layerdata in initial_fifo_depths: for idx in range(len(layerdata["fifo_utilization"])): name: str = cast("str", layerdata["name"]) df_data[name][idx]["out_initial_fifo_depths"] = layerdata["fifo_utilization"][idx] @@ -674,37 +675,39 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: fifo_depths, fifo_first_valid_cycles = self.create_starting_fifo_depths(initial_fifo_depths) # Max cycles for any simulation - sim_cycles: int = cast("int", max([val["cycles"] for val in initial_fifo_depths.values()])) + sim_cycles: int = cast("int", max([val["cycles"] for val in initial_fifo_depths])) # Extract bitwidths from outstream widths of hw nodes bit_widths = [] - for i in range(len(fifo_depths)): + for node_idx in range(len(fifo_depths)): bit_widths.append([]) - hw_node = getCustomOp(model.graph.node[i]) + hw_node = getCustomOp(model.graph.node[node_idx]) if isinstance(hw_node, HWCustomOp): - for j in range(len(fifo_depths[i])): - bit_widths[i].append(hw_node.get_outstream_width(j)) + for fifo_idx in range(len(fifo_depths[node_idx])): + bit_widths[node_idx].append(hw_node.get_outstream_width(fifo_idx)) else: raise FINNInternalError("Non-HW node found in dataflow graph during simulation") # Store bitwidths into dataframe as well - for i in range(len(bit_widths)): - for j in range(len(bit_widths[i])): - df_data[model.graph.node[i].name][j]["out_bitwidth"] = bit_widths[i][j] + for node_idx in range(len(bit_widths)): + for fifo_idx in range(len(bit_widths[node_idx])): + df_data[model.graph.node[node_idx].name][fifo_idx]["out_bitwidth"] = bit_widths[ + node_idx + ][fifo_idx] # Run minimization for every layer/stream log.info("Minimizing layers...") needs_minimization = [] - for i in range(len(fifo_depths)): - needs_minimization.append([True] * len(fifo_depths[i])) - for i in range(len(fifo_depths)): - for j in range(len(fifo_depths[i])): + for node_idx in range(len(fifo_depths)): + needs_minimization.append([True] * len(fifo_depths[node_idx])) + for node_idx in range(len(fifo_depths)): + for fifo_idx in range(len(fifo_depths[node_idx])): # Check if we can reduce the fifo size - used_size = fifo_depths[i][j] - bw = bit_widths[i][j] + used_size = fifo_depths[node_idx][fifo_idx] + bw = bit_widths[node_idx][fifo_idx] - needs_minimization[i][j] = self._needs_minimization(used_size, bw) + needs_minimization[node_idx][fifo_idx] = self._needs_minimization(used_size, bw) # Total minimizations total_minimizations = sum(len(streams) for streams in fifo_depths) @@ -717,26 +720,30 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Minimize FIFO depths using binary search over BRAM block counts idx_order = self.get_minimization_order_indices(minimization_order, model, bit_widths) + if len(idx_order) != len(model.graph.node): + raise FINNInternalError( + f"Expected index order length {len(model.graph.node)}, but got {len(idx_order)}" + ) log.info( f"Minimizing using order: {minimization_order.name}. Index order is: {idx_order}" ) done = 0 - for i in idx_order: - for j in range(len(fifo_depths[i])): - if not needs_minimization[i][j]: - df_data[model.graph.node[i].name][j][ + for node_idx in idx_order: + for fifo_idx in range(len(fifo_depths[node_idx])): + if not needs_minimization[node_idx][fifo_idx]: + df_data[model.graph.node[node_idx].name][fifo_idx][ f"simulation_time_{minimization_order.name}" ] = 0.0 - df_data[model.graph.node[i].name][j][ + df_data[model.graph.node[node_idx].name][fifo_idx][ f"out_final_depth_{minimization_order.name}" - ] = fifo_depths[i][j] - df_data[model.graph.node[i].name][j][ + ] = fifo_depths[node_idx][fifo_idx] + df_data[model.graph.node[node_idx].name][fifo_idx][ f"minimization_iterations_{minimization_order.name}" ] = 0 log.info( - f"[ {i + 1}.{j + 1} / {len(fifo_depths)} ] " + f"[ {node_idx}.{fifo_idx} / {len(fifo_depths) - 1} ] " f"Skipping minimization for this stream." ) done += 1 @@ -744,8 +751,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: minimization_start = time.time() minimized_depth, iterations_needed = self._minimize_fifo_depth( - i, - j, + node_idx, + fifo_idx, fifo_depths, bit_widths, initial_fifo_depths, @@ -756,21 +763,22 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: minimization_time = time.time() - minimization_start # Store the minimized size - fifo_depths[i][j] = minimized_depth + fifo_depths[node_idx][fifo_idx] = minimized_depth done += 1 # Store data into dataframe - df_data[model.graph.node[i].name][j][ + df_data[model.graph.node[node_idx].name][fifo_idx][ f"simulation_time_{minimization_order.name}" ] = minimization_time - df_data[model.graph.node[i].name][j][ + df_data[model.graph.node[node_idx].name][fifo_idx][ f"minimization_iterations_{minimization_order.name}" ] = iterations_needed - df_data[model.graph.node[i].name][j][ + df_data[model.graph.node[node_idx].name][fifo_idx][ f"out_final_depth_{minimization_order.name}" - ] = fifo_depths[i][j] + ] = fifo_depths[node_idx][fifo_idx] log.debug( - f"Set node/stream {i}.{j} to depth {fifo_depths[i][j]}, in " + f"Set node/stream {node_idx}.{fifo_idx} to " + f"depth {fifo_depths[node_idx][fifo_idx]}, in " f"{iterations_needed} iterations and {minimization_time} " f"seconds. (To {minimization_order.name})" ) @@ -778,7 +786,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: percentage = int(100.0 * float(done) / float(total_minimizations)) log.info( f"[ [bold green]{percentage}%[/bold green] ] " - f"[ {i+1}.{j+1} / {len(fifo_depths)} ] Simulation completed " + f"[ {node_idx}.{fifo_idx} / {len(fifo_depths) - 1} ] Simulation completed " f"({iterations_needed} iterations).", extra={"markup": True, "highlighter": None}, ) @@ -826,9 +834,9 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: raise FINNInternalError( f"Expected FIFO sizes for minimization order {order.name}, but found None." ) - for i in range(len(depths)): - for j in range(len(depths[i])): - current_size += depths[i][j] * bit_widths[i][j] + for node_idx in range(len(depths)): + for fifo_idx in range(len(depths[node_idx])): + current_size += depths[node_idx][fifo_idx] * bit_widths[node_idx][fifo_idx] if smallest_size is None or current_size < smallest_size: smallest_size = current_size @@ -840,32 +848,39 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Make sure that all FIFOs with depth > 256 use a full BRAM block, # since partial blocks are not supported by Vivado HLS - for i in range(len(fifo_depths)): - for j in range(len(fifo_depths[i])): - if fifo_depths[i][j] > 256: - bw = bit_widths[i][j] - blocks = calculate_bram_blocks(fifo_depths[i][j], bw) - blocks_plus_one = self._get_valid_block_counts(blocks+1, blocks+1000, bw) - _, max_d = calculate_bram_depth_range(blocks_plus_one[0], bw) - fifo_depths[i][j] = max_d + for node_idx in range(len(fifo_depths)): + for fifo_idx in range(len(fifo_depths[node_idx])): + if fifo_depths[node_idx][fifo_idx] > self.max_qsrl_depth: + bw = bit_widths[node_idx][fifo_idx] + blocks = calculate_bram_blocks(fifo_depths[node_idx][fifo_idx], bw) + # if len(fifo_depths[i]) > 1: + # blocks_plus_one = self._get_valid_block_counts( + # blocks + 1, blocks + 1000, bw + # ) + # _, max_d = calculate_bram_depth_range(blocks_plus_one[0], bw) + # else: + _, max_d = calculate_bram_depth_range(blocks, bw) + fifo_depths[node_idx][fifo_idx] = max_d log.info("Final FIFO depths:") - for i in range(len(fifo_depths)): - log.info(f"{i}: {fifo_depths[i]}") + for node_idx in range(len(fifo_depths)): + log.info(f"{node_idx}: {fifo_depths[node_idx]}") # Write back results. By default write to output_dir / "fifo_config.json" writeback_path = Path(self.cfg.output_dir) / "fifo_config.json" assert len(fifo_depths) == len(model.graph.node) - json_results = {} - for i in range(len(fifo_depths)): - json_results[i] = {"node": model.graph.node[i].name, "depths": fifo_depths[i]} + json_results = [] + for node_idx, node in enumerate(model.graph.node): + json_results.append({"node": node.name, "depths": fifo_depths[node_idx]}) with writeback_path.open("w") as f: json.dump(json_results, f) log.info(f"Wrote results back to {writeback_path}") return model, False - def _check_performance(self, new_data: dict, initial_fifo_depths: dict) -> bool: + def _check_performance( + self, new_data: list[dict[str, list[int]]], initial_fifo_depths: list[dict[str, list[int]]] + ) -> bool: """Check if performance has degraded compared to baseline. Args: @@ -875,9 +890,13 @@ def _check_performance(self, new_data: dict, initial_fifo_depths: dict) -> bool: Returns: True if performance degraded, False otherwise """ - for k, v in new_data.items(): - for idx in range(len(v["intervals"])): - if v["intervals"][idx] > initial_fifo_depths[k]["intervals"][idx]: + for new, initial in zip(new_data, initial_fifo_depths, strict=True): + if len(new["intervals"]) != len(initial["intervals"]): + raise FINNInternalError( + "New simulation data has different number of streams than baseline." + ) + for idx in range(len(new["intervals"])): + if new["intervals"][idx] > initial["intervals"][idx]: return True return False @@ -886,8 +905,8 @@ def _test_depth( test_depth: int, node_idx: int, fifo_idx: int, - baseline_depths: list, - initial_fifo_depths: dict, + baseline_depths: list[list[int]], + initial_fifo_depths: list[dict[str, list[int]]], sim: NodeConnectedSimulation, sim_cycles: float, fifo_first_valid_cycles: list[list[int]], @@ -906,10 +925,10 @@ def _test_depth( Returns: Tuple of (success, timeout) where success means depth works without degradation """ - test_depths = [row[:] for row in baseline_depths] # Deep copy from baseline + test_depths = deepcopy(baseline_depths) # Deep copy from baseline test_depths[node_idx][fifo_idx] = test_depth - new_data, timeout = sim.simulate( + new_simulation_data, timeout = sim.simulate( test_depths, max_cycles=min( math.ceil(sim_cycles * 1.05), math.ceil(sim_cycles) + 10 * len(test_depths) @@ -920,7 +939,7 @@ def _test_depth( if timeout: return False, True - performance_degraded = self._check_performance(new_data, initial_fifo_depths) + performance_degraded = self._check_performance(new_simulation_data, initial_fifo_depths) return not performance_degraded, False def _get_valid_block_counts(self, min_blocks: int, max_blocks: int, bitwidth: int) -> list[int]: @@ -948,9 +967,9 @@ def _minimize_fifo_depth( self, node_idx: int, fifo_idx: int, - baseline_depths: list, - bit_widths: list, - initial_fifo_depths: dict, + baseline_depths: list[list[int]], + bit_widths: list[list[int]], + initial_fifo_depths: list[dict[str, list[int]]], sim: NodeConnectedSimulation, sim_cycles: int, fifo_first_valid_cycles: list[list[int]], @@ -975,9 +994,7 @@ def _minimize_fifo_depth( original_size = baseline_depths[node_idx][fifo_idx] bw = bit_widths[node_idx][fifo_idx] - log.debug( - f"Minimizing Node {node_idx + 1} FIFO {fifo_idx + 1}: original depth {original_size}" - ) + log.debug(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs success, timeout = self._test_depth( @@ -1030,7 +1047,7 @@ def _minimize_fifo_depth( ) iterations += 1 if success: - upper_luts = calculate_srl16e_luts(original_size, bw) + upper_luts = calculate_srl16e_luts(self.max_qsrl_depth, bw) # LUTRAM based FIFOs have block sizes of 32, so smallest after 32 is 64 lower_luts = calculate_srl16e_luts(64, bw) @@ -1060,7 +1077,8 @@ def _minimize_fifo_depth( if not valid_blocks: # No valid configurations exist return original_size, iterations - # Test the maximum valid block count first (smallest depth) + # Test the maximum valid block count first + # (largest depth below original, most likely to succeed) max_valid_blocks = valid_blocks[-1] _, max_d = calculate_bram_depth_range(max_valid_blocks, bw) @@ -1104,7 +1122,7 @@ def _exponential_binary_search_depth( fifo_idx: int, baseline_depths: list, bitwidth: int, - initial_fifo_depths: dict, + initial_fifo_depths: list[dict[str, list[int]]], sim: NodeConnectedSimulation, sim_cycles: float, fifo_first_valid_cycles: list[list[int]], @@ -1205,7 +1223,7 @@ def _binary_search_srl_depth( fifo_idx: int, baseline_depths: list, bitwidth: int, - initial_fifo_depths: dict, + initial_fifo_depths: list[dict[str, list[int]]], sim: NodeConnectedSimulation, sim_cycles: float, fifo_first_valid_cycles: list[list[int]], From 92fc046215e5a92c468a0474c35d74cf922b4f5f Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 5 Mar 2026 10:07:26 +0100 Subject: [PATCH 081/170] Fix wrong key in apply depths --- src/finn/transformation/fpgadataflow/simulation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index a3ab1e52b1..e0d57197b4 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -238,10 +238,10 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: f"Node {first_node.name} does not have a custom op instance." " This is required for FIFO insertion." ) - if first_node.name != self.fifo_depths[node_ind]["name"]: + if first_node.name != self.fifo_depths[node_ind]["node"]: raise FINNInternalError( - f"Node name {n0.name} does not match expected name " - f"{self.fifo_depths[node_ind]['name']} at index {node_ind}. " + f"Node name {first_node.name} does not match expected name " + f"{self.fifo_depths[node_ind]['node']} at index {node_ind}. " "This may be due to a mismatch between the model and the config, " "or due to changes in the model after the simulation was run. " "Consider re-running the entire flow from start to finish." From 5ecbaee81a3289c7add8bcf68a5c404adf7aee57 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:04:15 +0100 Subject: [PATCH 082/170] Add offsets --- .../fpgadataflow/simulation_connected.py | 93 ++++++++++++++++--- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 824c4fde53..5ad601f2c9 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -20,6 +20,7 @@ from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.transformation.fpgadataflow.set_fifo_depths import get_fifo_split_configs from finn.transformation.fpgadataflow.simulation import Simulation, SimulationType, store_fifo_data from finn.transformation.fpgadataflow.simulation_controller import SimulationController from finn.util.basic import make_build_dir @@ -27,6 +28,44 @@ from finn.util.logging import log +# Hardware BRAM FIFOs lose entries to internal pipeline registers compared to the software FIFO +# model (which has exact capacity). This constant accounts for that overhead so that the +# minimization algorithm finds depths that are safe to deploy on hardware. +BRAM_FIFO_PIPELINE_OVERHEAD = 2 + + +def _count_bram_sub_fifos(depth: int, max_qsrl_depth: int) -> int: + """Return the number of BRAM (vivado) sub-FIFOs that *depth* decomposes into. + + Non-power-of-two BRAM FIFOs are decomposed into several power-of-two sub-FIFOs by + get_fifo_split_configs. Each sub-FIFO whose style is "vivado" has its own pipeline + register overhead, so the total overhead scales with the sub-FIFO count. + """ + return sum(1 for _, style in get_fifo_split_configs(depth, max_qsrl_depth) if style == "vivado") + + +def _safe_bram_starting_depth(peak_util: int, max_qsrl_depth: int) -> int: + """Return the smallest depth d such that d minus its BRAM pipeline overhead >= peak_util + 1. + + For LUTRAM depths (d <= max_qsrl_depth) the software model is exact so no overhead is needed. + For BRAM depths the overhead depends on how many sub-FIFOs the decomposition produces, + which itself depends on d. We iterate (typically 1-2 steps) until the overhead stabilises. + """ + d = max(peak_util + 1, 32) + if d <= max_qsrl_depth: + return d + # Iteratively find d where d - num_vivado(d)*overhead >= peak_util + 1 + overhead = 0 + while True: + d = peak_util + 1 + overhead + num_vivado = _count_bram_sub_fifos(d, max_qsrl_depth) + new_overhead = num_vivado * BRAM_FIFO_PIPELINE_OVERHEAD + if new_overhead <= overhead: + break + overhead = new_overhead + return max(d, 32) + + class MinimizationOrder(Enum): """The order in which the search algorithm minimizes the FIFO depths.""" @@ -472,8 +511,10 @@ def __init__( clk_ns: float, functional_sim: bool, workers: int | None = None, + max_qsrl_depth: int = 256, ) -> None: super().__init__(model, simulation_type, fpgapart, clk_ns, functional_sim, workers) + self.max_qsrl_depth = max_qsrl_depth def simulate( self, @@ -494,13 +535,33 @@ def simulate( names = [node.name for node in self.model.graph.node] initial_depth: Any = [[depth]] * len(self.binaries) if isinstance(depth, int) else depth + # For BRAM FIFOs (depth > max_qsrl_depth), hardware loses BRAM_FIFO_PIPELINE_OVERHEAD + # entries to internal pipeline registers *per BRAM sub-FIFO*. Non-power-of-two depths + # are decomposed into several power-of-two sub-FIFOs (see get_fifo_split_configs), so + # the total overhead is num_bram_sub_fifos * BRAM_FIFO_PIPELINE_OVERHEAD. + # Rounding to a full BRAM block before calling get_fifo_split_configs is NOT needed: + # the decomposition works on any depth, and we want the sub-FIFO count for the exact + # depth under test. + if initial_depth is not None and not isinstance(initial_depth, int): + adjusted_depth: Any = [ + [ + d - _count_bram_sub_fifos(d, self.max_qsrl_depth) * BRAM_FIFO_PIPELINE_OVERHEAD + if d > self.max_qsrl_depth + else d + for d in node_depths + ] + for node_depths in initial_depth + ] + else: + adjusted_depth = initial_depth + # Run simulation start = time.time() output_json = Path(make_build_dir("simulation_results_")) / "simulation_data.json" controller = NodeConnectedSimulationController( len(self.binaries), names, list(self.binaries.values()), Console(), 0.1, False ) - controller.run(initial_depth, output_json, max_cycles, fifo_first_valid_cycles) + controller.run(adjusted_depth, output_json, max_cycles, fifo_first_valid_cycles) end = time.time() log.debug(f"Simulation took {end - start} seconds!") @@ -510,15 +571,17 @@ def simulate( # Return the collected data indexed by node index data = [] for sim_entry in merged_data["simulations"]: - data.append({ - "name": sim_entry["name"], - "fifo_utilization": sim_entry["fifo_utilization"], - "fifo_depth": sim_entry["fifo_depth"], - "cycles": sim_entry["cycles"], - "samples": sim_entry["samples"], - "intervals": sim_entry["intervals"], - "fifo_cycles_until_first_valid": sim_entry["fifo_cycles_until_first_valid"], - }) + data.append( + { + "name": sim_entry["name"], + "fifo_utilization": sim_entry["fifo_utilization"], + "fifo_depth": sim_entry["fifo_depth"], + "cycles": sim_entry["cycles"], + "samples": sim_entry["samples"], + "intervals": sim_entry["intervals"], + "fifo_cycles_until_first_valid": sim_entry["fifo_cycles_until_first_valid"], + } + ) json.dump(data, output_json.open("w"), indent=4) return data, merged_data.get("timeout_occurred", False) @@ -562,7 +625,14 @@ def create_starting_fifo_depths( # Create fifo_depths (indexed by layer index and then stream index) fifo_depths: list[list[int]] = [] # Each entry is a list of fifo sizes for that node for val in initial_fifo_depths: - fifo_depths.append([max(v + 1, 32) for v in val["fifo_utilization"]]) + # Use _safe_bram_starting_depth so that simulate() (which subtracts + # num_sub_fifos*BRAM_FIFO_PIPELINE_OVERHEAD for BRAM depths) still sees a depth + # that covers the observed peak utilisation. A flat +2 is insufficient when a + # depth decomposes into multiple BRAM sub-FIFOs (e.g. depth 1537 → 2 sub-FIFOs + # → 4 entries of overhead). + fifo_depths.append( + [_safe_bram_starting_depth(v, self.max_qsrl_depth) for v in val["fifo_utilization"]] + ) fifo_first_valid_cycles: list[list[int]] = [] for val in initial_fifo_depths: fifo_first_valid_cycles.append( @@ -617,6 +687,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: self.fpgapart, self.clk_ns, self.cfg.functional_simulation, + max_qsrl_depth=self.max_qsrl_depth, ) model = sim.model # TODO:clean up From 20539b94f0af30f922464aaec96e09ae2c0987e6 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 6 Mar 2026 09:21:19 +0100 Subject: [PATCH 083/170] More small changes to track error --- finn_xsi/finn_xsi/include/AXIS_Control.h | 2 +- finn_xsi/finn_xsi/include/Simulation.hpp | 15 ++- .../finn_xsi/include/StableStateTracker.hpp | 6 +- .../fpgadataflow/simulation_connected.py | 94 ++++++++++++++----- 4 files changed, 89 insertions(+), 28 deletions(-) diff --git a/finn_xsi/finn_xsi/include/AXIS_Control.h b/finn_xsi/finn_xsi/include/AXIS_Control.h index 45cdb43336..2769ad2160 100644 --- a/finn_xsi/finn_xsi/include/AXIS_Control.h +++ b/finn_xsi/finn_xsi/include/AXIS_Control.h @@ -78,7 +78,7 @@ class M_AXIS_Control : public AXIS_Control { M_AXIS_Control& operator=(M_AXIS_Control&& other) = default; size_t lastComplete = 0; - size_t interval; + size_t interval = 0; StableStateTracker<> stableState; }; diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 8745974213..78a08be9eb 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -352,12 +353,22 @@ class SingleNodeSimulation : public Simulation getOStreamStableStateIntervals() const noexcept { std::array intervals{}; if constexpr (LastNode) { for (std::size_t i = 0; i < OStreamsSize; ++i) { - intervals[i] = this->ostreams[i].interval; + const double ema = this->ostreams[i].stableState.get_ema(); + // Fall back to the raw interval when the EMA has never been updated + // (ema == 0.0 means no second job completion has occurred yet). + intervals[i] = (ema > 0.0) + ? static_cast(std::round(ema)) + : this->ostreams[i].interval; } } return intervals; diff --git a/finn_xsi/finn_xsi/include/StableStateTracker.hpp b/finn_xsi/finn_xsi/include/StableStateTracker.hpp index e4e614f18f..e7da06726d 100644 --- a/finn_xsi/finn_xsi/include/StableStateTracker.hpp +++ b/finn_xsi/finn_xsi/include/StableStateTracker.hpp @@ -1,5 +1,5 @@ -#ifndef STABLESTATETRACKER_HPP -#define STABLESTATETRACKER_HPP +#ifndef STABLESTATETRACKER +#define STABLESTATETRACKER #include #include @@ -81,4 +81,4 @@ class StableStateTracker { static consteval uint8_t get_required_stable_count() { return RequiredStableCount; } }; -#endif // STABLESTATETRACKER_HPP +#endif /* STABLESTATETRACKER */ diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 5ad601f2c9..7aca8ac069 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -269,6 +269,23 @@ def run( timeout_result = timeout_result or timeout except Exception as e: self.console.log(f"Error collecting result: {e}") + + # Detect nodes whose _run_binary returned None (subprocess + # crash / unhandled exception). Their names were never inserted into + # fifo_results, so the merged JSON would contain empty 'intervals' lists + # for those nodes. _check_performance would then silently return False + # (no degradation detected) and the minimisation algorithm would treat a + # failed simulation as a successful one. Mark the run as timed-out so + # that _test_depth correctly rejects the candidate depth. + missing_nodes = [name for name in self.names if name not in fifo_results] + if missing_nodes: + self.console.log( + f"[bold red]WARNING: simulation results missing for node(s) " + f"{missing_nodes} (subprocess likely crashed). " + f"Marking run as timed-out to prevent false-success " + f"classification.[/bold red]" + ) + timeout_result = True finally: if self.progress is not None: self.progress.stop() @@ -821,7 +838,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: minimized_depth, iterations_needed = self._minimize_fifo_depth( node_idx, fifo_idx, - fifo_depths, + fifo_depths, # current_depths: evolves as FIFOs are minimised bit_widths, initial_fifo_depths, sim, @@ -934,6 +951,29 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: for node_idx in range(len(fifo_depths)): log.info(f"{node_idx}: {fifo_depths[node_idx]}") + log.info("Running final end-to-end validation simulation with minimised FIFO depths...") + validation_data, validation_timeout = sim.simulate( + fifo_depths, + max_cycles=math.ceil(sim_cycles * 1.05), + fifo_first_valid_cycles=fifo_first_valid_cycles, + ) + if validation_timeout: + raise FINNUserError( + "Final validation simulation timed out with the jointly-minimised FIFO depths. " + "The per-FIFO minimisation may have produced a configuration that is " + "collectively too small. Re-run with a larger initial depth or fewer " + "minimisation orders." + ) + if self._check_performance(validation_data, initial_fifo_depths): + raise FINNUserError( + "Final validation simulation detected throughput degradation with the " + "jointly-minimised FIFO depths (intervals exceeded baseline). " + "The per-FIFO minimisation may have produced a configuration that is " + "collectively too small. Re-run with a larger initial depth or fewer " + "minimisation orders." + ) + log.info("Final validation simulation passed - minimised depths are correct.") + # Write back results. By default write to output_dir / "fifo_config.json" writeback_path = Path(self.cfg.output_dir) / "fifo_config.json" assert len(fifo_depths) == len(model.graph.node) @@ -973,7 +1013,7 @@ def _test_depth( test_depth: int, node_idx: int, fifo_idx: int, - baseline_depths: list[list[int]], + current_depths: list[list[int]], initial_fifo_depths: list[dict[str, list[int]]], sim: NodeConnectedSimulation, sim_cycles: float, @@ -985,7 +1025,11 @@ def _test_depth( test_depth: Depth to test node_idx: Node index fifo_idx: FIFO index within node - baseline_depths: Original baseline FIFO depths (unchanged during minimization) + current_depths: Current working FIFO depth configuration. FIFOs that have + already been minimised contain their final minimised depth; FIFOs not yet + processed still carry the safe starting depth. This list is never + modified by this method - a deep copy is made before inserting + ``test_depth``. initial_fifo_depths: Baseline performance data sim: Simulation controller sim_cycles: Maximum simulation cycles @@ -993,7 +1037,7 @@ def _test_depth( Returns: Tuple of (success, timeout) where success means depth works without degradation """ - test_depths = deepcopy(baseline_depths) # Deep copy from baseline + test_depths = deepcopy(current_depths) test_depths[node_idx][fifo_idx] = test_depth new_simulation_data, timeout = sim.simulate( @@ -1035,7 +1079,7 @@ def _minimize_fifo_depth( self, node_idx: int, fifo_idx: int, - baseline_depths: list[list[int]], + current_depths: list[list[int]], bit_widths: list[list[int]], initial_fifo_depths: list[dict[str, list[int]]], sim: NodeConnectedSimulation, @@ -1047,9 +1091,11 @@ def _minimize_fifo_depth( Args: node_idx: Node index fifo_idx: FIFO index within node - current_depths: Current working FIFO depth configuration - (may have already-minimized values) - baseline_depths: Original baseline FIFO depths (unchanged during minimization) + current_depths: Current working FIFO depth configuration. FIFOs that have + already been minimised in this pass carry their final minimised depth; + FIFOs not yet processed still carry the safe starting depth. This list + is mutated by the caller (``apply``) after each call to store the + minimised result, so successive calls see the evolving state. bit_widths: Bitwidths for all FIFOs initial_fifo_depths: Baseline performance data sim: Simulation controller @@ -1059,7 +1105,7 @@ def _minimize_fifo_depth( Tuple: Minimized FIFO depth, Iterations required to arrive at the result """ iterations = 0 - original_size = baseline_depths[node_idx][fifo_idx] + original_size = current_depths[node_idx][fifo_idx] bw = bit_widths[node_idx][fifo_idx] log.debug(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") @@ -1069,7 +1115,7 @@ def _minimize_fifo_depth( 32, node_idx, fifo_idx, - baseline_depths, + current_depths, initial_fifo_depths, sim, sim_cycles, @@ -1089,7 +1135,7 @@ def _minimize_fifo_depth( best_working_depth, bin_it = self._binary_search_srl_depth( node_idx, fifo_idx, - baseline_depths, + current_depths, bw, initial_fifo_depths, sim, @@ -1107,7 +1153,7 @@ def _minimize_fifo_depth( self.max_qsrl_depth, node_idx, fifo_idx, - baseline_depths, + current_depths, initial_fifo_depths, sim, sim_cycles, @@ -1124,7 +1170,7 @@ def _minimize_fifo_depth( best_working_depth, bin_it = self._binary_search_srl_depth( node_idx, fifo_idx, - baseline_depths, + current_depths, bw, initial_fifo_depths, sim, @@ -1154,7 +1200,7 @@ def _minimize_fifo_depth( max_d, node_idx, fifo_idx, - baseline_depths, + current_depths, initial_fifo_depths, sim, sim_cycles, @@ -1172,7 +1218,7 @@ def _minimize_fifo_depth( best_working_depth, bin_it = self._exponential_binary_search_depth( node_idx, fifo_idx, - baseline_depths, + current_depths, bw, initial_fifo_depths, sim, @@ -1188,7 +1234,7 @@ def _exponential_binary_search_depth( self, node_idx: int, fifo_idx: int, - baseline_depths: list, + current_depths: list, bitwidth: int, initial_fifo_depths: list[dict[str, list[int]]], sim: NodeConnectedSimulation, @@ -1205,7 +1251,9 @@ def _exponential_binary_search_depth( Args: node_idx: Node index fifo_idx: FIFO index within node - baseline_depths: Original baseline FIFO depths (unchanged during minimization) + current_depths: Current working FIFO depth configuration. FIFOs already + minimised in this pass carry their final depth; this list must not be + modified directly (``_test_depth`` deep-copies it before trial edits). bitwidth: Data bitwidth initial_fifo_depths: Baseline performance data sim: Simulation controller @@ -1239,7 +1287,7 @@ def _exponential_binary_search_depth( max_d, node_idx, fifo_idx, - baseline_depths, + current_depths, initial_fifo_depths, sim, sim_cycles, @@ -1267,7 +1315,7 @@ def _exponential_binary_search_depth( max_d, node_idx, fifo_idx, - baseline_depths, + current_depths, initial_fifo_depths, sim, sim_cycles, @@ -1289,7 +1337,7 @@ def _binary_search_srl_depth( self, node_idx: int, fifo_idx: int, - baseline_depths: list, + current_depths: list, bitwidth: int, initial_fifo_depths: list[dict[str, list[int]]], sim: NodeConnectedSimulation, @@ -1303,7 +1351,9 @@ def _binary_search_srl_depth( Args: node_idx: Node index fifo_idx: FIFO index within node - baseline_depths: Original baseline FIFO depths (unchanged during minimization) + current_depths: Current working FIFO depth configuration. FIFOs already + minimised in this pass carry their final depth; this list must not be + modified directly (``_test_depth`` deep-copies it before trial edits). bitwidth: Data bitwidth initial_fifo_depths: Baseline performance data sim: Simulation controller @@ -1340,7 +1390,7 @@ def _binary_search_srl_depth( max_d, node_idx, fifo_idx, - baseline_depths, + current_depths, initial_fifo_depths, sim, sim_cycles, From cffd9c068a936a9278a92b7c4daa5a2f51843991 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 12 Mar 2026 09:52:11 +0100 Subject: [PATCH 084/170] Fix bugs and add debug stuff --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 22 +- finn_xsi/finn_xsi/include/AXIS_Control.h | 8 +- finn_xsi/finn_xsi/include/Clock.h | 5 + finn_xsi/finn_xsi/include/Simulation.hpp | 52 ++-- finn_xsi/finn_xsi/src/AXIS_Control.cpp | 8 + finn_xsi/finn_xsi/src/Clock.cpp | 55 ++++- .../fpgadataflow/simulation_connected.py | 222 ++++++++++++++---- 7 files changed, 287 insertions(+), 85 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index bff105cfbe..54e862736a 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -188,6 +188,20 @@ class SimulationController { status["fifo_cycles_until_first_valid"] = fifo_cycles; } } + // Add input/output job sizes + { + json in_job_sizes = json::array(); + for (size_t i = 0; i < InstreamCount; ++i) { + in_job_sizes.push_back(sim.getInputJobSize(i)); + } + status["input_job_size"] = in_job_sizes; + + json out_job_sizes = json::array(); + for (size_t i = 0; i < OutstreamCount; ++i) { + out_job_sizes.push_back(sim.getOutputJobSize(i)); + } + status["output_job_size"] = out_job_sizes; + } break; case SimulationState::ERROR: status["state"] = "error"; @@ -208,7 +222,7 @@ void process_command(const json& request, json& response, SimulationController& if (command == "configure") { std::vector fifo_depths; - //std::cout << "Payload: " << payload << std::endl; + // std::cout << "Payload: " << payload << std::endl; // Handle fifo_depth as either a single value or an array if (payload.contains("fifo_depth")) { @@ -281,6 +295,12 @@ void process_command(const json& request, json& response, SimulationController& if (final_status.contains("fifo_cycles_until_first_valid")) { response["fifo_cycles_until_first_valid"] = final_status["fifo_cycles_until_first_valid"]; } + if (final_status.contains("input_job_size")) { + response["input_job_size"] = final_status["input_job_size"]; + } + if (final_status.contains("output_job_size")) { + response["output_job_size"] = final_status["output_job_size"]; + } } else { response["status"] = "error"; response["message"] = "Unknown command: " + command; diff --git a/finn_xsi/finn_xsi/include/AXIS_Control.h b/finn_xsi/finn_xsi/include/AXIS_Control.h index 2769ad2160..c7f9a96f8b 100644 --- a/finn_xsi/finn_xsi/include/AXIS_Control.h +++ b/finn_xsi/finn_xsi/include/AXIS_Control.h @@ -36,6 +36,8 @@ class AXIS_Control : public CommunicationChannel { std::reference_wrapper setValid(bool value = true); std::reference_wrapper setReady(bool value = true); + virtual void writeBack() = 0; + // Job Size and Transaction Statistics size_t job_size; size_t job_txns; // [0:job_size] @@ -45,7 +47,7 @@ class AXIS_Control : public CommunicationChannel { // AXI interface prefix std::string name; - private: + protected: const xsi::Design* design; const Clock* clk; @@ -63,6 +65,8 @@ class S_AXIS_Control : public AXIS_Control { S_AXIS_Control(S_AXIS_Control&& other) = default; S_AXIS_Control& operator=(S_AXIS_Control&& other) = default; + void writeBack() override; + size_t job_ticks; // throttle if job_size < job_ticks size_t await_iter; // iteration allowing start of next job }; @@ -77,6 +81,8 @@ class M_AXIS_Control : public AXIS_Control { M_AXIS_Control(M_AXIS_Control&& other) = default; M_AXIS_Control& operator=(M_AXIS_Control&& other) = default; + void writeBack() override; + size_t lastComplete = 0; size_t interval = 0; StableStateTracker<> stableState; diff --git a/finn_xsi/finn_xsi/include/Clock.h b/finn_xsi/finn_xsi/include/Clock.h index b9c6e1235b..334d69690b 100644 --- a/finn_xsi/finn_xsi/include/Clock.h +++ b/finn_xsi/finn_xsi/include/Clock.h @@ -22,10 +22,15 @@ class Clock { Clock& operator=(Clock&&) noexcept = default; ~Clock() noexcept = default; + std::function clkHigh; + std::function clkLow; std::function cycle; void toggleClk() noexcept; + + void clockHigh() noexcept; + void clockLow() noexcept; }; #endif /* CLOCK */ diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 78a08be9eb..9b77b5dd28 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -115,13 +115,28 @@ class SingleNodeSimulation : public Simulation fifo; - /// Communicate with predecessors and successors and update their values and our own - [[gnu::hot, gnu::flatten, gnu::always_inline]] bool communicate(std::stop_token stoken = {}) { + /** + * Initialize streams according to nodeindex + */ + void initStreams() { + if constexpr (FirstNode) { // First Node; no predecessor + for (auto&& s : this->istreams) { // Input into sim valid + s.setInputValid(true); + } + } else if constexpr (LastNode) { // Last Node; no successor + for (auto&& s : this->ostreams) { // Output from sim ready + s.setOutputReady(true); + } + } + } + + [[gnu::hot, gnu::always_inline]] bool runSingleCycle(std::stop_token stoken = {}) { + ++cyclesRun; bool ret = false; if constexpr (!FirstNode) { for (std::size_t i = 0; i < IStreamsSize; ++i) { // Interface SHM <-> sim - this->istreams[i].setInputValid(fromProducerInterface[i].receive_request(stoken).data); + this->istreams[i].setValid(fromProducerInterface[i].receive_request(stoken).data); fromProducerInterface[i].send_response(CommData{this->istreams[i].getInputReady()}); } } @@ -132,7 +147,7 @@ class SingleNodeSimulation : public Simulation SHM this->fifo[i].setOutputReady(toConsumerInterface[i].send_request(CommData{this->fifo[i].getOutputValid()}, stoken).data, stoken); // FIFO -ready-> sim - this->ostreams[i].setOutputReady(this->fifo[i].getInputReady()); + this->ostreams[i].setReady(this->fifo[i].getInputReady()); // Toggle FIFO clock ret |= this->fifo[i].toggleClock(); } @@ -153,28 +168,15 @@ class SingleNodeSimulation : public Simulationistreams) { // Input into sim valid - s.setInputValid(true); - } - } else if constexpr (LastNode) { // Last Node; no successor - for (auto&& s : this->ostreams) { // Output from sim ready - s.setOutputReady(true); + //this->clk.toggleClk(); + this->clk.clockHigh(); + for (std::size_t i = 0; i < IStreamsSize; ++i) { + this->istreams[i].writeBack(); } + for (std::size_t i = 0; i < OStreamsSize; ++i) { + this->ostreams[i].writeBack(); } - } - - [[gnu::hot, gnu::always_inline]] bool runSingleCycle(std::stop_token stoken = {}) { - ++cyclesRun; - bool ret = communicate(stoken); - this->clk.toggleClk(); + this->clk.clockLow(); return ret; } @@ -233,7 +235,9 @@ class SingleNodeSimulation : public Simulationclk.clockHigh(); initStreams(); + this->clk.clockLow(); std::cout << "Finished initializing simulation." << std::endl; } diff --git a/finn_xsi/finn_xsi/src/AXIS_Control.cpp b/finn_xsi/finn_xsi/src/AXIS_Control.cpp index 6aaca6d901..9e91cbaa38 100644 --- a/finn_xsi/finn_xsi/src/AXIS_Control.cpp +++ b/finn_xsi/finn_xsi/src/AXIS_Control.cpp @@ -61,8 +61,16 @@ S_AXIS_Control::S_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, si } } +void S_AXIS_Control::writeBack() { + this->port_vld->write_back(); +} + M_AXIS_Control::M_AXIS_Control(xsi::Design& des, Clock& clock, size_t job_sz, const std::string& prefix) : AXIS_Control(des, clock, job_sz, prefix), lastComplete(0), interval(0) { if (job_sz < 1) { throw std::invalid_argument("Job size must be greater than 0."); } } + +void M_AXIS_Control::writeBack() { + this->port_rdy->write_back(); +} diff --git a/finn_xsi/finn_xsi/src/Clock.cpp b/finn_xsi/finn_xsi/src/Clock.cpp index 6814b11161..1b5e329e93 100644 --- a/finn_xsi/finn_xsi/src/Clock.cpp +++ b/finn_xsi/finn_xsi/src/Clock.cpp @@ -16,20 +16,53 @@ Clock::Clock(xsi::Design& des) : design(des) { break; } } - cycle = clk2x ? std::function([&des, &clk, clk2x](bool const up) mutable { - clk.set(up).write_back(); + clkHigh = clk2x ? std::function([&des, &clk, clk2x]() mutable { + des.run(1); + clk.set(1).write_back(); clk2x->set(1).write_back(); - des.run(5); + des.run(1); + }) : std::function([&des, &clk]() mutable { + des.run(1); + clk.set(1).write_back(); + des.run(1); + }); + clkLow = clk2x ? std::function([&des, &clk, clk2x]() mutable { + des.run(2499); clk2x->set(0).write_back(); - des.run(5); - }) - : std::function([&des, &clk](bool const up) mutable { - clk.set(up).write_back(); - des.run(5); - }); + des.run(2500); + clk.set(0).write_back(); + clk2x->set(1).write_back(); + des.run(2500); + clk2x->set(0).write_back(); + des.run(2499); + + }) : std::function([&des, &clk]() mutable { + des.run(4999); + clk.set(0).write_back(); + des.run(4999); + }); + // cycle = clk2x ? std::function([&des, &clk, clk2x](bool const up) mutable { + // clk.set(up).write_back(); + // clk2x->set(1).write_back(); + // des.run(5000); + // clk2x->set(0).write_back(); + // des.run(5000); + // }) + // : std::function([&des, &clk](bool const up) mutable { + // clk.set(up).write_back(); + // des.run(5000); + // }); } void Clock::toggleClk() noexcept { - cycle(1); - cycle(0); + clkHigh(); + clkLow(); +} + +void Clock::clockHigh() noexcept { + clkHigh(); +} + +void Clock::clockLow() noexcept { + clkLow(); } diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 7aca8ac069..d23f62efea 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -31,7 +31,7 @@ # Hardware BRAM FIFOs lose entries to internal pipeline registers compared to the software FIFO # model (which has exact capacity). This constant accounts for that overhead so that the # minimization algorithm finds depths that are safe to deploy on hardware. -BRAM_FIFO_PIPELINE_OVERHEAD = 2 +BRAM_FIFO_PIPELINE_OVERHEAD = 3 def _count_bram_sub_fifos(depth: int, max_qsrl_depth: int) -> int: @@ -158,6 +158,42 @@ def run( timeout_result = False fifo_depths: dict[str, list[int]] = {} fifo_cycles_until_first_valid_results: dict[str, list[int]] = {} + input_job_size_results: dict[str, list[int]] = {} + output_job_size_results: dict[str, list[int]] = {} + + def _store_result(result: tuple | None) -> None: + """Unpack one _run_binary result into the accumulator dicts. + + Safe to call from both the FIRST_COMPLETED loop and the shutdown + cleanup loop: the `sim_name not in fifo_results` guard prevents a + result that was already stored by the first loop from being + overwritten by the second. + """ + nonlocal timeout_result + if result is None: + return + ( + sim_name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + fifo_cycles_until_first_valid, + input_job_size, + output_job_size, + ) = result + if sim_name not in fifo_results: + fifo_results[sim_name] = fifo_util + fifo_depths[sim_name] = fifo_depth + cycles_results[sim_name] = cycles + samples_results[sim_name] = samps + intervals_results[sim_name] = intervals + fifo_cycles_until_first_valid_results[sim_name] = fifo_cycles_until_first_valid + input_job_size_results[sim_name] = input_job_size + output_job_size_results[sim_name] = output_job_size + timeout_result = timeout_result or timeout # Clean up any existing shared memory resources before starting self._cleanup_shm_resources() @@ -205,26 +241,7 @@ def run( for future in done: try: result = future.result() # This will raise if there was an exception - if result is not None: - ( - sim_name, - fifo_util, - cycles, - samps, - intervals, - timeout, - fifo_depth, - fifo_cycles_until_first_valid, - ) = result - fifo_depths[sim_name] = fifo_depth - fifo_results[sim_name] = fifo_util - cycles_results[sim_name] = cycles - samples_results[sim_name] = samps - intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[sim_name] = ( - fifo_cycles_until_first_valid - ) - timeout_result = timeout_result or timeout + _store_result(result) except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") # Set stop flag and break @@ -245,28 +262,7 @@ def run( continue try: result = future.result() - if result is not None: - ( - sim_name, - fifo_util, - cycles, - samps, - intervals, - timeout, - fifo_depth, - fifo_cycles_until_first_valid, - ) = result - # Only update if not already collected - if sim_name not in fifo_results: - fifo_cycles_until_first_valid_results[sim_name] = ( - fifo_cycles_until_first_valid - ) - fifo_depths[sim_name] = fifo_depth - fifo_results[sim_name] = fifo_util - cycles_results[sim_name] = cycles - samples_results[sim_name] = samps - intervals_results[sim_name] = intervals - timeout_result = timeout_result or timeout + _store_result(result) # guard inside: skips already-collected names except Exception as e: self.console.log(f"Error collecting result: {e}") @@ -305,6 +301,8 @@ def run( "fifo_cycles_until_first_valid": fifo_cycles_until_first_valid_results.get( name, [] ), + "input_job_size": input_job_size_results.get(name, []), + "output_job_size": output_job_size_results.get(name, []), } for name in self.names ], @@ -325,7 +323,10 @@ def _run_binary( is_special_for_display: bool = False, max_cycles: int | None = None, fifo_first_valid_cycles: list[int] | None = None, - ) -> tuple[str, list[int], int, int, list[int], bool, list[int], list[int]] | None: + ) -> ( + tuple[str, list[int], int, int, list[int], bool, list[int], list[int], list[int], list[int]] + | None + ): """Run the specified simulation binary in a new subprocess and communicate with it. Args: @@ -340,7 +341,7 @@ def _run_binary( Returns: Tuple of (simulation_name, fifo_utilization, cycles, samples, intervals, timeout, - fifo_depth, fifo_cycles_until_first_valid) on success, + fifo_depth, fifo_cycles_until_first_valid, input_job_size, output_job_size) on success, None on failure. """ cwd = binary.parent @@ -413,6 +414,8 @@ def _print(msg: str, color: str = "green") -> None: fifo_util: list[int] = [] fifo_depth: list[int] = [] fifo_cycles_until_first_valid: list[int] = [] + input_job_size: list[int] = [] + output_job_size: list[int] = [] # Poll for status updates while True: @@ -434,6 +437,8 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = stop_response.get( "fifo_cycles_until_first_valid", [] ) + input_job_size = stop_response.get("input_job_size", []) + output_job_size = stop_response.get("output_job_size", []) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") return ( @@ -445,6 +450,8 @@ def _print(msg: str, color: str = "green") -> None: timeout, fifo_depth, fifo_cycles_until_first_valid, + input_job_size, + output_job_size, ) time.sleep(self.poll_interval) @@ -468,6 +475,8 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = response.get( "fifo_cycles_until_first_valid", [] ) + input_job_size = response.get("input_job_size", []) + output_job_size = response.get("output_job_size", []) with self.stop_lock: self.should_stop = True break @@ -495,6 +504,8 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = stop_response.get( "fifo_cycles_until_first_valid", [] ) + input_job_size = stop_response.get("input_job_size", []) + output_job_size = stop_response.get("output_job_size", []) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") @@ -507,6 +518,8 @@ def _print(msg: str, color: str = "green") -> None: timeout, fifo_depth, fifo_cycles_until_first_valid, + input_job_size, + output_job_size, ) except Exception as e: @@ -597,6 +610,8 @@ def simulate( "samples": sim_entry["samples"], "intervals": sim_entry["intervals"], "fifo_cycles_until_first_valid": sim_entry["fifo_cycles_until_first_valid"], + "input_job_size": sim_entry.get("input_job_size", []), + "output_job_size": sim_entry.get("output_job_size", []), } ) json.dump(data, output_json.open("w"), indent=4) @@ -1004,6 +1019,10 @@ def _check_performance( "New simulation data has different number of streams than baseline." ) for idx in range(len(new["intervals"])): + if new["intervals"][idx] != 0: + print( + f"New intervals: {new['intervals'][idx]}, Initial intervals: {initial['intervals'][idx]}" + ) if new["intervals"][idx] > initial["intervals"][idx]: return True return False @@ -1037,6 +1056,11 @@ def _test_depth( Returns: Tuple of (success, timeout) where success means depth works without degradation """ + node_name = sim.model.graph.node[node_idx].name + peak_util = initial_fifo_depths[node_idx]["fifo_utilization"][fifo_idx] + print( + f"[{node_name}] FIFO {fifo_idx}: testing depth {test_depth} (peak util {peak_util}) ..." + ) test_depths = deepcopy(current_depths) test_depths[node_idx][fifo_idx] = test_depth @@ -1045,13 +1069,19 @@ def _test_depth( max_cycles=min( math.ceil(sim_cycles * 1.05), math.ceil(sim_cycles) + 10 * len(test_depths) ), - fifo_first_valid_cycles=fifo_first_valid_cycles, + # fifo_first_valid_cycles=fifo_first_valid_cycles, ) if timeout: + print(f"[{node_name}] FIFO {fifo_idx}: depth {test_depth} -> TIMEOUT.") return False, True + new_peak_util = new_simulation_data[node_idx]["fifo_utilization"][fifo_idx] performance_degraded = self._check_performance(new_simulation_data, initial_fifo_depths) + result_str = "SUCCESS" if not performance_degraded else "FAILED (perf degraded)" + print( + f"[{node_name}] FIFO {fifo_idx}: depth {test_depth} -> {result_str} (new peak util {new_peak_util})." + ) return not performance_degraded, False def _get_valid_block_counts(self, min_blocks: int, max_blocks: int, bitwidth: int) -> list[int]: @@ -1107,8 +1137,17 @@ def _minimize_fifo_depth( iterations = 0 original_size = current_depths[node_idx][fifo_idx] bw = bit_widths[node_idx][fifo_idx] + node_name = sim.model.graph.node[node_idx].name + peak_util = initial_fifo_depths[node_idx]["fifo_utilization"][fifo_idx] - log.debug(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") + log.debug( + f"[{node_name}] Minimizing node {node_idx} FIFO {fifo_idx}: " + f"original depth {original_size}, peak utilization {peak_util}, bitwidth {bw}" + ) + print( + f"[{node_name}] Minimizing node {node_idx} FIFO {fifo_idx}: " + f"original depth {original_size}, peak utilization {peak_util}, bitwidth {bw}" + ) # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs success, timeout = self._test_depth( @@ -1123,7 +1162,11 @@ def _minimize_fifo_depth( ) iterations += 1 if success: + print( + f"[{node_name}] FIFO {fifo_idx}: depth 32 works -> done in {iterations} iteration(s)." + ) return 32, iterations + print(f"[{node_name}] FIFO {fifo_idx}: depth 32 failed (timeout={timeout}).") if original_size <= self.max_qsrl_depth: upper_luts = calculate_srl16e_luts(original_size, bw) @@ -1132,6 +1175,9 @@ def _minimize_fifo_depth( # Binary search if there's room to search if upper_luts > lower_luts: + print( + f"[{node_name}] FIFO {fifo_idx}: original size {original_size} fits in LUTRAM; binary-searching SRL depths (LUTs {lower_luts}..{upper_luts})." + ) best_working_depth, bin_it = self._binary_search_srl_depth( node_idx, fifo_idx, @@ -1145,7 +1191,13 @@ def _minimize_fifo_depth( upper_luts=upper_luts, ) iterations += bin_it + print( + f"[{node_name}] FIFO {fifo_idx}: SRL binary search done -> depth {best_working_depth} in {bin_it} iteration(s)." + ) return best_working_depth, iterations + print( + f"[{node_name}] FIFO {fifo_idx}: no SRL search room (upper_luts == lower_luts); keeping original depth {original_size}." + ) return original_size, iterations # Try FIFO depth of 256 next (fits into LUTRAM) @@ -1167,6 +1219,9 @@ def _minimize_fifo_depth( # Binary search if there's room to search if upper_luts > lower_luts: + print( + f"[{node_name}] FIFO {fifo_idx}: max_qsrl_depth ({self.max_qsrl_depth}) works; binary-searching SRL depths (LUTs {lower_luts}..{upper_luts})." + ) best_working_depth, bin_it = self._binary_search_srl_depth( node_idx, fifo_idx, @@ -1180,21 +1235,38 @@ def _minimize_fifo_depth( upper_luts=upper_luts, ) iterations += bin_it + print( + f"[{node_name}] FIFO {fifo_idx}: SRL binary search done -> depth {best_working_depth} in {bin_it} iteration(s)." + ) return best_working_depth, iterations + print( + f"[{node_name}] FIFO {fifo_idx}: no SRL search room; using max_qsrl_depth {self.max_qsrl_depth}." + ) return self.max_qsrl_depth, iterations - + print( + f"[{node_name}] FIFO {fifo_idx}: max_qsrl_depth ({self.max_qsrl_depth}) failed -> escalating to BRAM search." + ) # We know 256 doesn't work, so we have to use BRAMs # Try one BRAM block less than current upper_blocks = calculate_bram_blocks(original_size, bw) # Get all valid block counts in the range valid_blocks = self._get_valid_block_counts(1, upper_blocks - 1, bw) + print( + f"[{node_name}] FIFO {fifo_idx}: upper_blocks={upper_blocks}, valid_blocks range=[{valid_blocks[0] if valid_blocks else 'none'}..{valid_blocks[-1] if valid_blocks else 'none'}] ({len(valid_blocks)} candidates)." + ) if not valid_blocks: # No valid configurations exist + print( + f"[{node_name}] FIFO {fifo_idx}: no valid BRAM configs below original -> keeping depth {original_size}." + ) return original_size, iterations # Test the maximum valid block count first # (largest depth below original, most likely to succeed) max_valid_blocks = valid_blocks[-1] _, max_d = calculate_bram_depth_range(max_valid_blocks, bw) + print( + f"[{node_name}] FIFO {fifo_idx}: testing max_valid_blocks={max_valid_blocks} -> depth {max_d}." + ) success, timeout = self._test_depth( max_d, @@ -1209,12 +1281,21 @@ def _minimize_fifo_depth( iterations += 1 if timeout or not success: + print( + f"[{node_name}] FIFO {fifo_idx}: depth {max_d} failed (timeout={timeout}) -> keeping original depth {original_size}." + ) return original_size, iterations + print( + f"[{node_name}] FIFO {fifo_idx}: depth {max_d} (blocks={max_valid_blocks}) works -> entering BRAM binary search." + ) best_working_depth = max_d # Binary search if there's room to search and multiple valid configs if len(valid_blocks) > 1: + print( + f"[{node_name}] FIFO {fifo_idx}: {len(valid_blocks)} valid BRAM configs -> exponential+binary search over blocks {valid_blocks[0]}..{valid_blocks[-1]}." + ) best_working_depth, bin_it = self._exponential_binary_search_depth( node_idx, fifo_idx, @@ -1227,6 +1308,13 @@ def _minimize_fifo_depth( valid_blocks=valid_blocks, ) iterations += bin_it + print( + f"[{node_name}] FIFO {fifo_idx}: BRAM search done -> depth {best_working_depth} in {bin_it} iteration(s)." + ) + else: + print( + f"[{node_name}] FIFO {fifo_idx}: only one valid BRAM config -> depth {best_working_depth}." + ) return best_working_depth, iterations @@ -1267,6 +1355,7 @@ def _exponential_binary_search_depth( iterations = 0 if not valid_blocks: raise FINNInternalError("valid_blocks list cannot be empty") + node_name = sim.model.graph.node[node_idx].name # Start with the largest valid block count (known to work from caller) _, max_d = calculate_bram_depth_range(valid_blocks[-1], bitwidth) @@ -1279,6 +1368,9 @@ def _exponential_binary_search_depth( exp_idx = 0 last_failed_idx = -1 + print( + f"[{node_name}] FIFO {fifo_idx}: exponential search phase over {len(valid_blocks)} valid blocks." + ) while exp_idx < upper_idx: blocks = valid_blocks[exp_idx] _, max_d = calculate_bram_depth_range(blocks, bitwidth) @@ -1300,12 +1392,21 @@ def _exponential_binary_search_depth( best_working_depth = max_d lower_idx = last_failed_idx + 1 upper_idx = exp_idx + print( + f"[{node_name}] FIFO {fifo_idx}: exp search: blocks={blocks} depth={max_d} SUCCESS -> narrowing to idx [{lower_idx}, {upper_idx}]." + ) break # This doesn't work, try exponentially larger index + print( + f"[{node_name}] FIFO {fifo_idx}: exp search: blocks={blocks} depth={max_d} FAILED -> advancing exp_idx." + ) last_failed_idx = exp_idx exp_idx = min(exp_idx * 2 if exp_idx > 0 else 1, upper_idx) # Binary search phase: refine the range + print( + f"[{node_name}] FIFO {fifo_idx}: binary search phase over idx [{lower_idx}, {upper_idx}] (blocks {valid_blocks[lower_idx]}..{valid_blocks[upper_idx]})." + ) while lower_idx < upper_idx: mid_idx = (lower_idx + upper_idx) // 2 blocks = valid_blocks[mid_idx] @@ -1327,10 +1428,19 @@ def _exponential_binary_search_depth( # This depth works, try smaller (lower indices) best_working_depth = max_d upper_idx = mid_idx + print( + f"[{node_name}] FIFO {fifo_idx}: bin search: blocks={blocks} depth={max_d} SUCCESS -> upper_idx={upper_idx}." + ) else: # This depth doesn't work, need larger (higher indices) lower_idx = mid_idx + 1 + print( + f"[{node_name}] FIFO {fifo_idx}: bin search: blocks={blocks} depth={max_d} FAILED -> lower_idx={lower_idx}." + ) + print( + f"[{node_name}] FIFO {fifo_idx}: exponential+binary search finished -> best depth {best_working_depth} in {iterations} iteration(s)." + ) return best_working_depth, iterations def _binary_search_srl_depth( @@ -1368,7 +1478,11 @@ def _binary_search_srl_depth( iterations = 0 _, max_d = calculate_srl16e_depth_range(upper_luts, bitwidth) best_working_depth = max_d + node_name = sim.model.graph.node[node_idx].name + print( + f"[{node_name}] FIFO {fifo_idx}: SRL binary search over LUTs [{lower_luts}, {upper_luts}], initial depth {best_working_depth}." + ) while lower_luts < upper_luts: mid_luts = (lower_luts + upper_luts) // 2 @@ -1383,6 +1497,9 @@ def _binary_search_srl_depth( if max_d == 0: # No valid configuration, try more LUTs + print( + f"[{node_name}] FIFO {fifo_idx}: SRL bin search: mid_luts={mid_luts} has no valid depth -> lower_luts={mid_luts + 1}." + ) lower_luts = mid_luts + 1 continue @@ -1402,10 +1519,19 @@ def _binary_search_srl_depth( # This depth works, try smaller best_working_depth = max_d upper_luts = mid_luts + print( + f"[{node_name}] FIFO {fifo_idx}: SRL bin search: luts={mid_luts} depth={max_d} SUCCESS -> upper_luts={upper_luts}." + ) else: # This depth doesn't work, need larger lower_luts = mid_luts + 1 + print( + f"[{node_name}] FIFO {fifo_idx}: SRL bin search: luts={mid_luts} depth={max_d} FAILED -> lower_luts={lower_luts}." + ) + print( + f"[{node_name}] FIFO {fifo_idx}: SRL binary search finished -> best depth {best_working_depth} in {iterations} iteration(s)." + ) return best_working_depth, iterations def _needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: From 0a52fb9291c8ef5be038433caa0d1b340c5199e1 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 12 Mar 2026 23:28:20 +0100 Subject: [PATCH 085/170] Fix FIFO sizing hopefully --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 1 + finn_xsi/finn_xsi/include/Simulation.hpp | 37 +-- finn_xsi/finn_xsi/src/FIFO.cpp | 4 +- .../fpgadataflow/simulation_connected.py | 222 ++++-------------- 4 files changed, 75 insertions(+), 189 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 54e862736a..314aa0a41a 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -110,6 +110,7 @@ class SimulationController { current_samples.store(sim.getCompletedMaps()); state = SimulationState::FINISHED; } + state = SimulationState::FINISHED; } catch (const std::exception& e) { std::lock_guard error_lock(state_mutex); std::cout << "Simulation error: " << e.what() << std::endl; diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 9b77b5dd28..64bcc304ea 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -107,8 +107,8 @@ struct CommData { // └──────────────────────────────────────┘ template class SingleNodeSimulation : public Simulation { - using ConsumingInterface = InterprocessCommunicationChannel; - using ProducingInterface = InterprocessCommunicationChannel; + using ConsumingInterface = InterprocessCommunicationChannel; + using ProducingInterface = InterprocessCommunicationChannel; std::array fromProducerInterface; std::array toConsumerInterface; std::size_t cyclesRun = 0; @@ -136,8 +136,9 @@ class SingleNodeSimulation : public Simulation sim - this->istreams[i].setValid(fromProducerInterface[i].receive_request(stoken).data); - fromProducerInterface[i].send_response(CommData{this->istreams[i].getInputReady()}); + bool istreamReady = this->istreams[i].getInputReady(); + bool fifoValid = fromProducerInterface[i].send_request(CommData{istreamReady}, stoken).data; + this->istreams[i].setValid(fifoValid); // deferred } } if constexpr (!LastNode) { @@ -145,11 +146,14 @@ class SingleNodeSimulation : public Simulation FIFO this->fifo[i].setInputValid(this->ostreams[i].getOutputValid(), stoken); // Interface FIFO <-> SHM - this->fifo[i].setOutputReady(toConsumerInterface[i].send_request(CommData{this->fifo[i].getOutputValid()}, stoken).data, stoken); - // FIFO -ready-> sim - this->ostreams[i].setReady(this->fifo[i].getInputReady()); + this->fifo[i].setOutputReady(toConsumerInterface[i].receive_request(stoken).data, stoken); + // Toggle FIFO clock ret |= this->fifo[i].toggleClock(); + bool fifoValid = this->fifo[i].getOutputValid(); + toConsumerInterface[i].send_response(CommData{fifoValid}); + // FIFO -ready-> sim + this->ostreams[i].setReady(this->fifo[i].getInputReady()); } } if constexpr (LastNode) { @@ -168,15 +172,22 @@ class SingleNodeSimulation : public Simulationclk.toggleClk(); - this->clk.clockHigh(); + // ── CLOCK HIGH ───────────────────────────────────────────────────────── + this->clk.clockHigh(); // run(1) [gap] → clk=1 → run(1) + + // ── WRITE (clock is high, commit deferred setValid / setReady) ───────── + // + // The deferred values were prepared at the end of the previous cycle's read + // phase (or are defaults for the first cycle). for (std::size_t i = 0; i < IStreamsSize; ++i) { this->istreams[i].writeBack(); - } + } for (std::size_t i = 0; i < OStreamsSize; ++i) { this->ostreams[i].writeBack(); } - this->clk.clockLow(); + + // ── CLOCK LOW ────────────────────────────────────────────────────────── + this->clk.clockLow(); // run(4999) → clk=0 → run(4999) ← sim settles return ret; } @@ -370,9 +381,7 @@ class SingleNodeSimulation : public Simulationostreams[i].stableState.get_ema(); // Fall back to the raw interval when the EMA has never been updated // (ema == 0.0 means no second job completion has occurred yet). - intervals[i] = (ema > 0.0) - ? static_cast(std::round(ema)) - : this->ostreams[i].interval; + intervals[i] = (ema > 0.0) ? static_cast(std::round(ema)) : this->ostreams[i].interval; } } return intervals; diff --git a/finn_xsi/finn_xsi/src/FIFO.cpp b/finn_xsi/finn_xsi/src/FIFO.cpp index 76f1144288..9cfa53aacc 100644 --- a/finn_xsi/finn_xsi/src/FIFO.cpp +++ b/finn_xsi/finn_xsi/src/FIFO.cpp @@ -36,7 +36,9 @@ bool FIFO::toggleClock() { } /// Return whether the FIFO can accept inputs (for the current utilization) -bool FIFO::getInputReady([[maybe_unused]] std::stop_token stoken) noexcept { return currentUtil < maxSize; } +/// Uses nextUtil (post-push state) so that ready correctly reflects capacity +/// after any push already committed this cycle, preventing AXI-S violations. +bool FIFO::getInputReady([[maybe_unused]] std::stop_token stoken) noexcept { return nextUtil < maxSize; } /// Return whether the FIFO can output values (for the current utilization) bool FIFO::getOutputValid([[maybe_unused]] std::stop_token stoken) noexcept { return currentUtil > 0; } diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index d23f62efea..7aca8ac069 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -31,7 +31,7 @@ # Hardware BRAM FIFOs lose entries to internal pipeline registers compared to the software FIFO # model (which has exact capacity). This constant accounts for that overhead so that the # minimization algorithm finds depths that are safe to deploy on hardware. -BRAM_FIFO_PIPELINE_OVERHEAD = 3 +BRAM_FIFO_PIPELINE_OVERHEAD = 2 def _count_bram_sub_fifos(depth: int, max_qsrl_depth: int) -> int: @@ -158,42 +158,6 @@ def run( timeout_result = False fifo_depths: dict[str, list[int]] = {} fifo_cycles_until_first_valid_results: dict[str, list[int]] = {} - input_job_size_results: dict[str, list[int]] = {} - output_job_size_results: dict[str, list[int]] = {} - - def _store_result(result: tuple | None) -> None: - """Unpack one _run_binary result into the accumulator dicts. - - Safe to call from both the FIRST_COMPLETED loop and the shutdown - cleanup loop: the `sim_name not in fifo_results` guard prevents a - result that was already stored by the first loop from being - overwritten by the second. - """ - nonlocal timeout_result - if result is None: - return - ( - sim_name, - fifo_util, - cycles, - samps, - intervals, - timeout, - fifo_depth, - fifo_cycles_until_first_valid, - input_job_size, - output_job_size, - ) = result - if sim_name not in fifo_results: - fifo_results[sim_name] = fifo_util - fifo_depths[sim_name] = fifo_depth - cycles_results[sim_name] = cycles - samples_results[sim_name] = samps - intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[sim_name] = fifo_cycles_until_first_valid - input_job_size_results[sim_name] = input_job_size - output_job_size_results[sim_name] = output_job_size - timeout_result = timeout_result or timeout # Clean up any existing shared memory resources before starting self._cleanup_shm_resources() @@ -241,7 +205,26 @@ def _store_result(result: tuple | None) -> None: for future in done: try: result = future.result() # This will raise if there was an exception - _store_result(result) + if result is not None: + ( + sim_name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + fifo_cycles_until_first_valid, + ) = result + fifo_depths[sim_name] = fifo_depth + fifo_results[sim_name] = fifo_util + cycles_results[sim_name] = cycles + samples_results[sim_name] = samps + intervals_results[sim_name] = intervals + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) + timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") # Set stop flag and break @@ -262,7 +245,28 @@ def _store_result(result: tuple | None) -> None: continue try: result = future.result() - _store_result(result) # guard inside: skips already-collected names + if result is not None: + ( + sim_name, + fifo_util, + cycles, + samps, + intervals, + timeout, + fifo_depth, + fifo_cycles_until_first_valid, + ) = result + # Only update if not already collected + if sim_name not in fifo_results: + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) + fifo_depths[sim_name] = fifo_depth + fifo_results[sim_name] = fifo_util + cycles_results[sim_name] = cycles + samples_results[sim_name] = samps + intervals_results[sim_name] = intervals + timeout_result = timeout_result or timeout except Exception as e: self.console.log(f"Error collecting result: {e}") @@ -301,8 +305,6 @@ def _store_result(result: tuple | None) -> None: "fifo_cycles_until_first_valid": fifo_cycles_until_first_valid_results.get( name, [] ), - "input_job_size": input_job_size_results.get(name, []), - "output_job_size": output_job_size_results.get(name, []), } for name in self.names ], @@ -323,10 +325,7 @@ def _run_binary( is_special_for_display: bool = False, max_cycles: int | None = None, fifo_first_valid_cycles: list[int] | None = None, - ) -> ( - tuple[str, list[int], int, int, list[int], bool, list[int], list[int], list[int], list[int]] - | None - ): + ) -> tuple[str, list[int], int, int, list[int], bool, list[int], list[int]] | None: """Run the specified simulation binary in a new subprocess and communicate with it. Args: @@ -341,7 +340,7 @@ def _run_binary( Returns: Tuple of (simulation_name, fifo_utilization, cycles, samples, intervals, timeout, - fifo_depth, fifo_cycles_until_first_valid, input_job_size, output_job_size) on success, + fifo_depth, fifo_cycles_until_first_valid) on success, None on failure. """ cwd = binary.parent @@ -414,8 +413,6 @@ def _print(msg: str, color: str = "green") -> None: fifo_util: list[int] = [] fifo_depth: list[int] = [] fifo_cycles_until_first_valid: list[int] = [] - input_job_size: list[int] = [] - output_job_size: list[int] = [] # Poll for status updates while True: @@ -437,8 +434,6 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = stop_response.get( "fifo_cycles_until_first_valid", [] ) - input_job_size = stop_response.get("input_job_size", []) - output_job_size = stop_response.get("output_job_size", []) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") return ( @@ -450,8 +445,6 @@ def _print(msg: str, color: str = "green") -> None: timeout, fifo_depth, fifo_cycles_until_first_valid, - input_job_size, - output_job_size, ) time.sleep(self.poll_interval) @@ -475,8 +468,6 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = response.get( "fifo_cycles_until_first_valid", [] ) - input_job_size = response.get("input_job_size", []) - output_job_size = response.get("output_job_size", []) with self.stop_lock: self.should_stop = True break @@ -504,8 +495,6 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = stop_response.get( "fifo_cycles_until_first_valid", [] ) - input_job_size = stop_response.get("input_job_size", []) - output_job_size = stop_response.get("output_job_size", []) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") @@ -518,8 +507,6 @@ def _print(msg: str, color: str = "green") -> None: timeout, fifo_depth, fifo_cycles_until_first_valid, - input_job_size, - output_job_size, ) except Exception as e: @@ -610,8 +597,6 @@ def simulate( "samples": sim_entry["samples"], "intervals": sim_entry["intervals"], "fifo_cycles_until_first_valid": sim_entry["fifo_cycles_until_first_valid"], - "input_job_size": sim_entry.get("input_job_size", []), - "output_job_size": sim_entry.get("output_job_size", []), } ) json.dump(data, output_json.open("w"), indent=4) @@ -1019,10 +1004,6 @@ def _check_performance( "New simulation data has different number of streams than baseline." ) for idx in range(len(new["intervals"])): - if new["intervals"][idx] != 0: - print( - f"New intervals: {new['intervals'][idx]}, Initial intervals: {initial['intervals'][idx]}" - ) if new["intervals"][idx] > initial["intervals"][idx]: return True return False @@ -1056,11 +1037,6 @@ def _test_depth( Returns: Tuple of (success, timeout) where success means depth works without degradation """ - node_name = sim.model.graph.node[node_idx].name - peak_util = initial_fifo_depths[node_idx]["fifo_utilization"][fifo_idx] - print( - f"[{node_name}] FIFO {fifo_idx}: testing depth {test_depth} (peak util {peak_util}) ..." - ) test_depths = deepcopy(current_depths) test_depths[node_idx][fifo_idx] = test_depth @@ -1069,19 +1045,13 @@ def _test_depth( max_cycles=min( math.ceil(sim_cycles * 1.05), math.ceil(sim_cycles) + 10 * len(test_depths) ), - # fifo_first_valid_cycles=fifo_first_valid_cycles, + fifo_first_valid_cycles=fifo_first_valid_cycles, ) if timeout: - print(f"[{node_name}] FIFO {fifo_idx}: depth {test_depth} -> TIMEOUT.") return False, True - new_peak_util = new_simulation_data[node_idx]["fifo_utilization"][fifo_idx] performance_degraded = self._check_performance(new_simulation_data, initial_fifo_depths) - result_str = "SUCCESS" if not performance_degraded else "FAILED (perf degraded)" - print( - f"[{node_name}] FIFO {fifo_idx}: depth {test_depth} -> {result_str} (new peak util {new_peak_util})." - ) return not performance_degraded, False def _get_valid_block_counts(self, min_blocks: int, max_blocks: int, bitwidth: int) -> list[int]: @@ -1137,17 +1107,8 @@ def _minimize_fifo_depth( iterations = 0 original_size = current_depths[node_idx][fifo_idx] bw = bit_widths[node_idx][fifo_idx] - node_name = sim.model.graph.node[node_idx].name - peak_util = initial_fifo_depths[node_idx]["fifo_utilization"][fifo_idx] - log.debug( - f"[{node_name}] Minimizing node {node_idx} FIFO {fifo_idx}: " - f"original depth {original_size}, peak utilization {peak_util}, bitwidth {bw}" - ) - print( - f"[{node_name}] Minimizing node {node_idx} FIFO {fifo_idx}: " - f"original depth {original_size}, peak utilization {peak_util}, bitwidth {bw}" - ) + log.debug(f"Minimizing Node {node_idx} FIFO {fifo_idx}: original depth {original_size}") # If FIFO depth of 32 works, use it because it fits into bw/2 LUTs success, timeout = self._test_depth( @@ -1162,11 +1123,7 @@ def _minimize_fifo_depth( ) iterations += 1 if success: - print( - f"[{node_name}] FIFO {fifo_idx}: depth 32 works -> done in {iterations} iteration(s)." - ) return 32, iterations - print(f"[{node_name}] FIFO {fifo_idx}: depth 32 failed (timeout={timeout}).") if original_size <= self.max_qsrl_depth: upper_luts = calculate_srl16e_luts(original_size, bw) @@ -1175,9 +1132,6 @@ def _minimize_fifo_depth( # Binary search if there's room to search if upper_luts > lower_luts: - print( - f"[{node_name}] FIFO {fifo_idx}: original size {original_size} fits in LUTRAM; binary-searching SRL depths (LUTs {lower_luts}..{upper_luts})." - ) best_working_depth, bin_it = self._binary_search_srl_depth( node_idx, fifo_idx, @@ -1191,13 +1145,7 @@ def _minimize_fifo_depth( upper_luts=upper_luts, ) iterations += bin_it - print( - f"[{node_name}] FIFO {fifo_idx}: SRL binary search done -> depth {best_working_depth} in {bin_it} iteration(s)." - ) return best_working_depth, iterations - print( - f"[{node_name}] FIFO {fifo_idx}: no SRL search room (upper_luts == lower_luts); keeping original depth {original_size}." - ) return original_size, iterations # Try FIFO depth of 256 next (fits into LUTRAM) @@ -1219,9 +1167,6 @@ def _minimize_fifo_depth( # Binary search if there's room to search if upper_luts > lower_luts: - print( - f"[{node_name}] FIFO {fifo_idx}: max_qsrl_depth ({self.max_qsrl_depth}) works; binary-searching SRL depths (LUTs {lower_luts}..{upper_luts})." - ) best_working_depth, bin_it = self._binary_search_srl_depth( node_idx, fifo_idx, @@ -1235,38 +1180,21 @@ def _minimize_fifo_depth( upper_luts=upper_luts, ) iterations += bin_it - print( - f"[{node_name}] FIFO {fifo_idx}: SRL binary search done -> depth {best_working_depth} in {bin_it} iteration(s)." - ) return best_working_depth, iterations - print( - f"[{node_name}] FIFO {fifo_idx}: no SRL search room; using max_qsrl_depth {self.max_qsrl_depth}." - ) return self.max_qsrl_depth, iterations - print( - f"[{node_name}] FIFO {fifo_idx}: max_qsrl_depth ({self.max_qsrl_depth}) failed -> escalating to BRAM search." - ) + # We know 256 doesn't work, so we have to use BRAMs # Try one BRAM block less than current upper_blocks = calculate_bram_blocks(original_size, bw) # Get all valid block counts in the range valid_blocks = self._get_valid_block_counts(1, upper_blocks - 1, bw) - print( - f"[{node_name}] FIFO {fifo_idx}: upper_blocks={upper_blocks}, valid_blocks range=[{valid_blocks[0] if valid_blocks else 'none'}..{valid_blocks[-1] if valid_blocks else 'none'}] ({len(valid_blocks)} candidates)." - ) if not valid_blocks: # No valid configurations exist - print( - f"[{node_name}] FIFO {fifo_idx}: no valid BRAM configs below original -> keeping depth {original_size}." - ) return original_size, iterations # Test the maximum valid block count first # (largest depth below original, most likely to succeed) max_valid_blocks = valid_blocks[-1] _, max_d = calculate_bram_depth_range(max_valid_blocks, bw) - print( - f"[{node_name}] FIFO {fifo_idx}: testing max_valid_blocks={max_valid_blocks} -> depth {max_d}." - ) success, timeout = self._test_depth( max_d, @@ -1281,21 +1209,12 @@ def _minimize_fifo_depth( iterations += 1 if timeout or not success: - print( - f"[{node_name}] FIFO {fifo_idx}: depth {max_d} failed (timeout={timeout}) -> keeping original depth {original_size}." - ) return original_size, iterations - print( - f"[{node_name}] FIFO {fifo_idx}: depth {max_d} (blocks={max_valid_blocks}) works -> entering BRAM binary search." - ) best_working_depth = max_d # Binary search if there's room to search and multiple valid configs if len(valid_blocks) > 1: - print( - f"[{node_name}] FIFO {fifo_idx}: {len(valid_blocks)} valid BRAM configs -> exponential+binary search over blocks {valid_blocks[0]}..{valid_blocks[-1]}." - ) best_working_depth, bin_it = self._exponential_binary_search_depth( node_idx, fifo_idx, @@ -1308,13 +1227,6 @@ def _minimize_fifo_depth( valid_blocks=valid_blocks, ) iterations += bin_it - print( - f"[{node_name}] FIFO {fifo_idx}: BRAM search done -> depth {best_working_depth} in {bin_it} iteration(s)." - ) - else: - print( - f"[{node_name}] FIFO {fifo_idx}: only one valid BRAM config -> depth {best_working_depth}." - ) return best_working_depth, iterations @@ -1355,7 +1267,6 @@ def _exponential_binary_search_depth( iterations = 0 if not valid_blocks: raise FINNInternalError("valid_blocks list cannot be empty") - node_name = sim.model.graph.node[node_idx].name # Start with the largest valid block count (known to work from caller) _, max_d = calculate_bram_depth_range(valid_blocks[-1], bitwidth) @@ -1368,9 +1279,6 @@ def _exponential_binary_search_depth( exp_idx = 0 last_failed_idx = -1 - print( - f"[{node_name}] FIFO {fifo_idx}: exponential search phase over {len(valid_blocks)} valid blocks." - ) while exp_idx < upper_idx: blocks = valid_blocks[exp_idx] _, max_d = calculate_bram_depth_range(blocks, bitwidth) @@ -1392,21 +1300,12 @@ def _exponential_binary_search_depth( best_working_depth = max_d lower_idx = last_failed_idx + 1 upper_idx = exp_idx - print( - f"[{node_name}] FIFO {fifo_idx}: exp search: blocks={blocks} depth={max_d} SUCCESS -> narrowing to idx [{lower_idx}, {upper_idx}]." - ) break # This doesn't work, try exponentially larger index - print( - f"[{node_name}] FIFO {fifo_idx}: exp search: blocks={blocks} depth={max_d} FAILED -> advancing exp_idx." - ) last_failed_idx = exp_idx exp_idx = min(exp_idx * 2 if exp_idx > 0 else 1, upper_idx) # Binary search phase: refine the range - print( - f"[{node_name}] FIFO {fifo_idx}: binary search phase over idx [{lower_idx}, {upper_idx}] (blocks {valid_blocks[lower_idx]}..{valid_blocks[upper_idx]})." - ) while lower_idx < upper_idx: mid_idx = (lower_idx + upper_idx) // 2 blocks = valid_blocks[mid_idx] @@ -1428,19 +1327,10 @@ def _exponential_binary_search_depth( # This depth works, try smaller (lower indices) best_working_depth = max_d upper_idx = mid_idx - print( - f"[{node_name}] FIFO {fifo_idx}: bin search: blocks={blocks} depth={max_d} SUCCESS -> upper_idx={upper_idx}." - ) else: # This depth doesn't work, need larger (higher indices) lower_idx = mid_idx + 1 - print( - f"[{node_name}] FIFO {fifo_idx}: bin search: blocks={blocks} depth={max_d} FAILED -> lower_idx={lower_idx}." - ) - print( - f"[{node_name}] FIFO {fifo_idx}: exponential+binary search finished -> best depth {best_working_depth} in {iterations} iteration(s)." - ) return best_working_depth, iterations def _binary_search_srl_depth( @@ -1478,11 +1368,7 @@ def _binary_search_srl_depth( iterations = 0 _, max_d = calculate_srl16e_depth_range(upper_luts, bitwidth) best_working_depth = max_d - node_name = sim.model.graph.node[node_idx].name - print( - f"[{node_name}] FIFO {fifo_idx}: SRL binary search over LUTs [{lower_luts}, {upper_luts}], initial depth {best_working_depth}." - ) while lower_luts < upper_luts: mid_luts = (lower_luts + upper_luts) // 2 @@ -1497,9 +1383,6 @@ def _binary_search_srl_depth( if max_d == 0: # No valid configuration, try more LUTs - print( - f"[{node_name}] FIFO {fifo_idx}: SRL bin search: mid_luts={mid_luts} has no valid depth -> lower_luts={mid_luts + 1}." - ) lower_luts = mid_luts + 1 continue @@ -1519,19 +1402,10 @@ def _binary_search_srl_depth( # This depth works, try smaller best_working_depth = max_d upper_luts = mid_luts - print( - f"[{node_name}] FIFO {fifo_idx}: SRL bin search: luts={mid_luts} depth={max_d} SUCCESS -> upper_luts={upper_luts}." - ) else: # This depth doesn't work, need larger lower_luts = mid_luts + 1 - print( - f"[{node_name}] FIFO {fifo_idx}: SRL bin search: luts={mid_luts} depth={max_d} FAILED -> lower_luts={lower_luts}." - ) - print( - f"[{node_name}] FIFO {fifo_idx}: SRL binary search finished -> best depth {best_working_depth} in {iterations} iteration(s)." - ) return best_working_depth, iterations def _needs_minimization(self, fifo_depth: int, bitwidth: int) -> bool: From 485ac7acd96189abd4d72fb905cc3a9033498a34 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Fri, 13 Mar 2026 16:12:37 +0100 Subject: [PATCH 086/170] Integrated the distributed simulation partly into the normal FINN flow --- src/finn/builder/build_dataflow_config.py | 1 + src/finn/builder/build_dataflow_steps.py | 9 +++++++++ src/finn/interface/run_finn.py | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index be5a631b29..75d4aa3468 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -97,6 +97,7 @@ class AutoFIFOSizingMethod(str, Enum): CHARACTERIZE = "characterize" LARGEFIFO_RTLSIM = "largefifo_rtlsim" + DISTRIBUTED_SIMULATION = "distributed_sim" class ShellFlowType(str, Enum): diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 592f510a5d..f9fd44d50b 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -69,6 +69,7 @@ DataflowOutputType, ShellFlowType, VerificationStepType, + AutoFIFOSizingMethod ) from finn.builder.passes import step_passes_frontend from finn.core.onnx_exec import execute_onnx @@ -684,6 +685,8 @@ def step_hw_ipgen(model: ModelWrapper, cfg: DataflowBuildConfig): # TODO: Both this and the step_size_... steps will be reworked before merging into dev +# TODO: These are also included in step_set_fifo_depths if the correct FIFO sizing method +# was selected def step_build_simulation(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Build the simulation binaries for isolated and connected simulations.""" from finn.transformation.fpgadataflow.simulation_build import BuildSimulation @@ -881,6 +884,12 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig): ) # InsertAndSetFIFODepths internally removes any shallow FIFOs # so no need to call RemoveShallowFIFOs here + elif cfg.auto_fifo_strategy == AutoFIFOSizingMethod.DISTRIBUTED_SIMULATION: + # TODO: When merging into dev, this should be finalized + model = step_build_simulation(model, cfg) + model = step_size_fifo_connected(model, cfg) + model = step_apply_fifosizes(model, cfg) + return model else: assert "Unsupported auto_fifo_strategy: " + cfg.auto_fifo_strategy else: diff --git a/src/finn/interface/run_finn.py b/src/finn/interface/run_finn.py index 0e6145063b..85708f3d11 100644 --- a/src/finn/interface/run_finn.py +++ b/src/finn/interface/run_finn.py @@ -934,7 +934,7 @@ def bench( # Late import because we need prepare_finn to setup remaining dependencies first from finn.benchmarking.bench import start_bench_run - exit_code = start_bench_run(bench_config) + exit_code = start_bench_run(str(bench_config)) sys.exit(exit_code) From 32bebff2881bb0e64dd84cdb2aa4a5a69730cd43 Mon Sep 17 00:00:00 2001 From: Linus Jungemann Date: Sat, 14 Mar 2026 11:46:17 +0100 Subject: [PATCH 087/170] Fixes --- src/finn/benchmarking/dut/resnet18.yml | 12 +- .../builder/custom_step_library/resnet.py | 245 +++++++++++++----- .../custom_step_library/transformer_adhoc.py | 4 +- .../fpgadataflow/set_folding.py | 4 +- 4 files changed, 199 insertions(+), 66 deletions(-) diff --git a/src/finn/benchmarking/dut/resnet18.yml b/src/finn/benchmarking/dut/resnet18.yml index f427c33e83..fb8a6589fe 100644 --- a/src/finn/benchmarking/dut/resnet18.yml +++ b/src/finn/benchmarking/dut/resnet18.yml @@ -1,3 +1,7 @@ +model_path: models/resnet18/resnet18_w3a3_cifar100.onnx +folding_config_file: models/resnet18/resnet18_folding_config.json +specialize_layers_config_file: models/resnet18/resnet18_specialize_layers.json + steps: - step_qonnx_to_finn - step_tidy_up @@ -11,13 +15,13 @@ steps: - step_apply_folding_config - step_minimize_bit_width - step_generate_estimate_reports - - step_build_simulation - - step_size_fifo_connected - - step_apply_fifosizes - - step_generate_estimate_reports + - step_set_fifo_depths - step_hw_codegen - step_hw_ipgen - step_create_stitched_ip - step_synthesize_bitfile - step_make_driver - step_deployment_package + +# Required to use RTL MVAUs +standalone_thresholds: true diff --git a/src/finn/builder/custom_step_library/resnet.py b/src/finn/builder/custom_step_library/resnet.py index 8163c5c5e5..1781bd8636 100644 --- a/src/finn/builder/custom_step_library/resnet.py +++ b/src/finn/builder/custom_step_library/resnet.py @@ -1,4 +1,3 @@ - # Copyright (C) 2020-2022, Xilinx, Inc. # Copyright (C) 2022-2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -35,54 +34,69 @@ hardware conversion. """ -from finn.transformation.qonnx.fold_quant_weights import FoldQuantWeights -from finn.transformation.qonnx.infer_quant_avg_pool_2d import ( - AvgPoolAndTruncv2ToQuantAvgPool, -) -from finn.transformation.qonnx.quant_act_to_multithreshold import ( - ConvertQuantActToMultiThreshold, - default_filter_function_generator, -) -from finn.transformation.streamline.streamline_plus import StreamlinePlus as Streamline -from finn.transformation.streamline.remove import RemoveIdentityReshape, RemoveIdentityTranspose +from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper +from qonnx.transformation.batchnorm_to_affine import BatchNormToAffine from qonnx.transformation.composed import ComposedTransformation from qonnx.transformation.double_to_single_float import DoubleToSingleFloat from qonnx.transformation.fold_constants import FoldConstants -from qonnx.transformation.extract_conv_bias import ExtractBiasFromConv -from qonnx.transformation.gemm_to_matmul import GemmToMatMul -from qonnx.transformation.infer_data_layouts import InferDataLayouts -from qonnx.transformation.infer_datatypes import InferDataTypes -from qonnx.transformation.quant_constant_folding import FoldTransposeIntoQuantInit -from qonnx.transformation.remove import RemoveIdentityOps from qonnx.transformation.general import ( + ConvertDivToMul, + ConvertSubToAdd, GiveReadableTensorNames, GiveUniqueNodeNames, GiveUniqueParameterTensors, + RemoveStaticGraphInputs, RemoveUnusedTensors, SortGraph, ) +from qonnx.transformation.infer_data_layouts import InferDataLayouts +from qonnx.transformation.infer_datatypes import InferDataTypes from qonnx.transformation.infer_shapes import InferShapes +from qonnx.transformation.insert_topk import InsertTopK from qonnx.transformation.lower_convs_to_matmul import LowerConvsToMatMul -from qonnx.util.cleanup import cleanup_model +from qonnx.transformation.remove import RemoveIdentityOps import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw -from finn.transformation.fpgadataflow.replicate_stream import InferReplicateStream from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.transformation.fpgadataflow.replicate_stream import InferReplicateStream from finn.transformation.move_reshape import RemoveCNVtoFCFlatten from finn.transformation.streamline.absorb import ( + Absorb1BitMulIntoConv, + Absorb1BitMulIntoMatMul, AbsorbAddIntoMultiThreshold, + AbsorbConsecutiveTransposes, + AbsorbMulIntoMultiThreshold, + AbsorbScalarMulAddIntoTopK, AbsorbSignBiasIntoMultiThreshold, AbsorbTransposeIntoMultiThreshold, + FactorOutMulSignMagnitude, +) +from finn.transformation.streamline.collapse_repeated import ( + CollapseRepeatedAdd, + CollapseRepeatedMul, ) from finn.transformation.streamline.remove import RemoveIdentityReshape, RemoveIdentityTranspose +# just for not linear # just for not linear from finn.transformation.streamline.reorder import ( + MoveAddPastConv, + MoveAddPastMul, + MoveLinearPastEltwiseAdd, + MoveLinearPastFork, + MoveMaxPoolPastMultiThreshold, MoveMulPastAdd, + MoveScalarAddPastMatMul, + MoveScalarLinearPastInvariants, + MoveScalarMulPastConv, + MoveScalarMulPastMatMul, + MoveTransposePastEltwise, + MoveTransposePastFork, + MoveTransposePastJoinAdd, ) -from finn.transformation.streamline.reorder import MoveMulPastAdd +from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds +from finn.transformation.streamline.sign_to_thres import ConvertSignToThres from finn.transformation.streamline.streamline_plus import StreamlinePlus as Streamline @@ -114,44 +128,6 @@ def step_resnet_tidy(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrap return model -# Temporary step function to replace ConvertQONNXtoFINN class, because qonnx version is to old to -# handle avgpool version parameter -def step_temp_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: # noqa: ARG001 - """Convert QONNX dialect to FINN ONNX dialect.""" - model = cleanup_model(model) - - model = model.transform(ExtractBiasFromConv()) - # Gemm operations are not supported by FINN, so we convert them to MatMul - model = model.transform(GemmToMatMul()) - model = model.transform(FoldTransposeIntoQuantInit()) - # Make sure the datatypes exist, these are required for folding the weights - model = model.transform(InferDataTypes()) - # Fold weights - model = model.transform(FoldQuantWeights()) - # Convert activations - - # Perform layout inference so that QuantActBaseHandler can set data_layout - # attribute of MT for use in later layout inference and NCHW->NHWC conversion - # in the InferThresholding transformation. - model = model.transform(InferDataLayouts()) - model = model.transform(InferShapes()) - model = model.transform( - ConvertQuantActToMultiThreshold( - filter_function=default_filter_function_generator(max_multithreshold_bit_width=8), - ) - ) - # Recompute datatypes - model = model.transform(InferDataTypes()) - model = model.transform(InferDataLayouts()) - model = model.transform(InferShapes()) - # Convert AvgPool -> Mul -> Trunc structure to QuantAvgPool2d - model = model.transform(AvgPoolAndTruncv2ToQuantAvgPool()) - # Remove empty padding if it exists - model = model.transform(RemoveIdentityOps()) - return model - - - def step_resnet_streamline( model: ModelWrapper, cfg: DataflowBuildConfig ) -> ModelWrapper: # noqa: ARG001 @@ -171,6 +147,7 @@ def step_resnet_streamline( model = model.transform(Streamline()) # model = model.transform(InsertTopK()) # model = model.transform(AbsorbScalarMulAddIntoTopK()) + return model @@ -201,3 +178,155 @@ def step_resnet_convert_to_hw( model = model.transform(RemoveUnusedTensors()) model = model.transform(SortGraph()) return model + + +# For backwards compatibility + + +def step_resnet50_tidy(model: ModelWrapper, cfg: DataflowBuildConfig): + """Tidy up ResNet-50 models (backwards-compatible legacy step). + + Applies shape and datatype inference, constant folding, unique naming, and + inserts a TopK layer at the output. + """ + model = model.transform(GiveUniqueParameterTensors()) + model = model.transform(InferShapes()) + model = model.transform(FoldConstants()) + model = model.transform(RemoveStaticGraphInputs()) + model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveReadableTensorNames()) + model = model.transform(InferDataTypes()) + model = model.transform(InsertTopK()) + model = model.transform(InferShapes()) + model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveReadableTensorNames()) + model = model.transform(InferDataTypes()) + return model + + +def step_resnet50_streamline_linear(model: ModelWrapper, cfg: DataflowBuildConfig): + """Apply linear streamlining transformations to a ResNet-50 model. + + Moves and absorbs scalar linear operations (mul, add) past convolutions and + matrix multiplications, collapses repeated operations, converts sign nodes + to thresholds, and absorbs values into multithreshold nodes. + """ + streamline_transformations = [ + AbsorbScalarMulAddIntoTopK(), # before MoveAddPastMul to avoid int->float + ConvertSubToAdd(), + ConvertDivToMul(), + RemoveIdentityOps(), + CollapseRepeatedMul(), + BatchNormToAffine(), + ConvertSignToThres(), + MoveAddPastMul(), + MoveScalarAddPastMatMul(), + MoveAddPastConv(), + MoveScalarMulPastMatMul(), + MoveScalarMulPastConv(), + MoveScalarLinearPastInvariants(), + MoveAddPastMul(), + CollapseRepeatedAdd(), + CollapseRepeatedMul(), + AbsorbAddIntoMultiThreshold(), + FactorOutMulSignMagnitude(), + MoveMaxPoolPastMultiThreshold(), + AbsorbMulIntoMultiThreshold(), + Absorb1BitMulIntoMatMul(), + Absorb1BitMulIntoConv(), + RoundAndClipThresholds(), + ] + for trn in streamline_transformations: + model = model.transform(trn) + model = model.transform(GiveUniqueNodeNames()) + return model + + +def step_resnet50_streamline_nonlinear(model: ModelWrapper, cfg: DataflowBuildConfig): + """Apply non-linear streamlining transformations to a ResNet-50 model. + + Moves linear operations past elementwise-add nodes and fork points to + enable further fusion in subsequent linear streamlining passes. + """ + streamline_transformations = [ + MoveLinearPastEltwiseAdd(), + MoveLinearPastFork(), + ] + for trn in streamline_transformations: + model = model.transform(trn) + model = model.transform(GiveUniqueNodeNames()) + return model + + +def step_resnet50_streamline(model: ModelWrapper, cfg: DataflowBuildConfig): + """Streamline a ResNet-50 model (backwards-compatible legacy step). + + Iterates linear and non-linear streamlining passes, then lowers convolutions + to matrix multiplications and absorbs the resulting transpose operations. + """ + for iter_id in range(4): + model = step_resnet50_streamline_linear(model, cfg) + model = step_resnet50_streamline_nonlinear(model, cfg) + + # big loop tidy up + model = model.transform(RemoveUnusedTensors()) + model = model.transform(GiveReadableTensorNames()) + model = model.transform(InferDataTypes()) + model = model.transform(SortGraph()) + + model = model.transform(DoubleToSingleFloat()) + + # Lower convolutions and streamline resulting transposes + model = model.transform(LowerConvsToMatMul()) + model = model.transform( + ComposedTransformation( + [ + MoveTransposePastJoinAdd(), + MoveTransposePastFork(), + MoveTransposePastEltwise(), + AbsorbConsecutiveTransposes(), + AbsorbTransposeIntoMultiThreshold(), + ] + ) + ) + return model + + +def step_resnet50_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig): + """Convert a ResNet-50 model to hardware-specific operations (backwards-compatible legacy step). + + Sets the input datatype to UINT8, then sequentially converts channelwise + linear layers, pooling, matrix-vector activations, thresholding, convolution + input generators, stream duplication/addition, and label selection to their + corresponding HLS hardware layer variants. + """ + model.set_tensor_datatype(model.graph.input[0].name, DataType["UINT8"]) + model = model.transform(InferDataLayouts()) + model = model.transform(DoubleToSingleFloat()) + model = model.transform(InferDataTypes()) + model = model.transform(SortGraph()) + + to_hw_transformations = [ + to_hw.InferChannelwiseLinearLayer, + to_hw.InferPool, + AbsorbConsecutiveTransposes, + RoundAndClipThresholds, + to_hw.InferQuantizedMatrixVectorActivation, + to_hw.InferThresholdingLayer, + to_hw.InferConvInpGen, + to_hw.InferDuplicateStreamsLayer, + to_hw.InferAddStreamsLayer, + to_hw.InferLabelSelectLayer, + ] + for trn in to_hw_transformations: + model = model.transform(trn()) + model = model.transform(InferDataLayouts()) + model = model.transform(GiveUniqueNodeNames()) + model = model.transform(InferDataTypes()) + + model = model.transform(RemoveCNVtoFCFlatten()) + model = model.transform(GiveReadableTensorNames()) + model = model.transform(RemoveUnusedTensors()) + model = model.transform(SortGraph()) + + return model diff --git a/src/finn/builder/custom_step_library/transformer_adhoc.py b/src/finn/builder/custom_step_library/transformer_adhoc.py index cbcd28a916..70aae9d59e 100644 --- a/src/finn/builder/custom_step_library/transformer_adhoc.py +++ b/src/finn/builder/custom_step_library/transformer_adhoc.py @@ -150,7 +150,7 @@ def _set_folding_attention(model: ModelWrapper, target_cycles_per_frame): # parallelism in steps following the common divisors the inputs. for fold in reversed(common_divisors([qkdim, vdim])): # Configure the folding attribute - inst.set_nodeattr("EmbFold", fold) + inst.set_nodeattr("EmbFold", int(fold)) # Check if this is sufficient to meet the cycles target if inst.get_exp_cycles() <= target_cycles_per_frame: break @@ -159,7 +159,7 @@ def _set_folding_attention(model: ModelWrapper, target_cycles_per_frame): # parallelism in steps divisors of the key and value sequence. for fold in reversed(common_divisors([kvlen])): # Configure the folding attribute - inst.set_nodeattr("SeqFold", fold) + inst.set_nodeattr("SeqFold", int(fold)) # Check if this is sufficient to meet the cycles target if inst.get_exp_cycles() <= target_cycles_per_frame: break diff --git a/src/finn/transformation/fpgadataflow/set_folding.py b/src/finn/transformation/fpgadataflow/set_folding.py index 6a23727bb3..dc069e9462 100644 --- a/src/finn/transformation/fpgadataflow/set_folding.py +++ b/src/finn/transformation/fpgadataflow/set_folding.py @@ -265,7 +265,7 @@ def apply(self, model): node_inst.set_nodeattr("SIMD", 1) channels_per_stream = node_inst.get_nodeattr("ChannelsPerStream") for simd_val in common_divisors(channels_per_stream): - node_inst.set_nodeattr("SIMD", simd_val) + node_inst.set_nodeattr("SIMD", int(simd_val)) cyc = node_inst.get_exp_cycles() if cyc < self.target_cycles_per_frame: break @@ -274,7 +274,7 @@ def apply(self, model): dim = int(node_inst.get_normal_input_shape()[-1]) for simd_val in divisors(dim): if dim // simd_val > 12: - node_inst.set_nodeattr("SIMD", simd_val) + node_inst.set_nodeattr("SIMD", int(simd_val)) cyc = node_inst.get_exp_cycles() if cyc < self.target_cycles_per_frame: break From d8890fc4a6a9f1b783cc733dec3164c283ae330f Mon Sep 17 00:00:00 2001 From: bwintermann Date: Tue, 17 Mar 2026 09:28:17 +0100 Subject: [PATCH 088/170] Missing import fixed --- src/finn/util/deprecated.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/finn/util/deprecated.py b/src/finn/util/deprecated.py index fa572b0e4b..8593aa8388 100644 --- a/src/finn/util/deprecated.py +++ b/src/finn/util/deprecated.py @@ -3,6 +3,7 @@ import warnings from collections.abc import Callable from typing import ParamSpec, TypeVar +from finn.util.logging import log rT = TypeVar("rT") # return type # noqa: N816 pT = ParamSpec("pT") # parameters type # noqa: N816 From 28ead3410a398d1fa1400ca519b5903a2b46ede2 Mon Sep 17 00:00:00 2001 From: bwintermann Date: Thu, 19 Mar 2026 14:59:23 +0100 Subject: [PATCH 089/170] Small changes to bench --- src/finn/benchmarking/bench.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/finn/benchmarking/bench.py b/src/finn/benchmarking/bench.py index 3e596add9d..499fa721cb 100644 --- a/src/finn/benchmarking/bench.py +++ b/src/finn/benchmarking/bench.py @@ -102,9 +102,12 @@ def get_default_session_options_new(): is_followup = True save_dir = save_dir + "_followup" else: - config_path = os.path.join("ci", "cfg", config_name + ".yml") + if config_name.endswith(".yaml") or config_name.endswith(".yml"): + config_path = config_name + else: + config_path = os.path.join("ci", "cfg", config_name + ".yml") print("Job launched with SLURM ID: %d" % (job_id)) - except KeyError: + except KeyError as e: # Launched without SLURM, assume test run on local machine job_id = 0 experiment_dir = "bench_output/" + time.strftime("%d_%H_%M") From 94477b7d4f8e7b8c380f15eb5c7948b42c0140af Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:31:34 +0100 Subject: [PATCH 090/170] Fixes --- src/finn/transformation/fpgadataflow/insert_fifo.py | 2 ++ .../transformation/fpgadataflow/set_fifo_depths.py | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/insert_fifo.py b/src/finn/transformation/fpgadataflow/insert_fifo.py index 94f2c29605..653d863283 100644 --- a/src/finn/transformation/fpgadataflow/insert_fifo.py +++ b/src/finn/transformation/fpgadataflow/insert_fifo.py @@ -133,6 +133,8 @@ def apply(self, model): # check if outFIFOdepths attribute of first node # and inFIFOdepths attribute of consumer node is equal + idx_out = min(idx_out, len(n0.get_nodeattr("outFIFODepths")) - 1) + idx_inp = min(idx_inp, len(n1.get_nodeattr("inFIFODepths")) - 1) n0_depth = n0.get_nodeattr("outFIFODepths")[idx_out] n1_depth = n1.get_nodeattr("inFIFODepths")[idx_inp] diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index a035e5283f..a717354cb3 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -413,7 +413,11 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: throttle_cycles = 0 sim = xsi_fifosim( - model, self.cfg_n_inferences, max_iters=max_iters, throttle_cycles=int(throttle_cycles) + model, + self.cfg_n_inferences, + False, + max_iters=max_iters, + throttle_cycles=int(throttle_cycles), ) for ind, node in enumerate(fifo_nodes): @@ -459,9 +463,9 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: reset_implementation(node_inst) modified_fc_nodes.remove(node.name) - assert ( - len(modified_fc_nodes) == 0 and len(fifos.keys()) == 0 - ), "FIFO/FC nodes left untouched after model reconfiguration" + assert len(modified_fc_nodes) == 0 and len(fifos.keys()) == 0, ( + "FIFO/FC nodes left untouched after model reconfiguration" + ) # handle custom sizing for SWG FIFOs if desired if self.swg_exception: From b2be7c6a713a1c39e84ed298b8c6070608be39a9 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:03:44 +0200 Subject: [PATCH 091/170] Cleanup basic and hwcustomop --- src/finn/custom_op/fpgadataflow/hwcustomop.py | 184 +++-------------- src/finn/util/basic.py | 193 ++++++++++-------- 2 files changed, 136 insertions(+), 241 deletions(-) diff --git a/src/finn/custom_op/fpgadataflow/hwcustomop.py b/src/finn/custom_op/fpgadataflow/hwcustomop.py index 5b9a81aa69..099cf9ab42 100644 --- a/src/finn/custom_op/fpgadataflow/hwcustomop.py +++ b/src/finn/custom_op/fpgadataflow/hwcustomop.py @@ -34,7 +34,6 @@ import numpy as np import numpy.typing as npt -import os from abc import abstractmethod from collections.abc import Sequence from finn_xsi.sim_engine import SimEngine @@ -47,13 +46,12 @@ from finn import xsi from finn.util.basic import get_liveness_threshold_cycles, is_versal -from finn.util.deprecated import deprecated from finn.util.exception import FINNInternalError -from finn.util.logging import log from finn.util.settings import get_settings if TYPE_CHECKING: from qonnx.core.modelwrapper import ModelWrapper + from finn.transformation.fpgadataflow.loop_rolling import LoopBodyInputType finnxsi = xsi if xsi.is_available() else None @@ -197,8 +195,7 @@ def get_rtlsim(self) -> SimEngine: rtlsim_so = self.get_nodeattr("rtlsim_so") if type(rtlsim_so) is not str: raise FINNInternalError( - f"rtlsim_so attribute not set correctly in {self.onnx_node.name}, " - "cannot get rtlsim" + f"rtlsim_so attribute not set correctly in {self.onnx_node.name}, cannot get rtlsim" ) if not Path(rtlsim_so).is_file(): raise FINNInternalError( @@ -434,12 +431,12 @@ def calc_wmem(self) -> int: """Calculate and returns the WMEM.""" raise NotImplementedError() - def generate_hdl_memstream(self, fpgapart, pumped_memory=0): - """Helper function to generate verilog code for memstream component. + def generate_hdl_memstream(self, fpgapart: str, pumped_memory: int = 0) -> None: + """Generate verilog code for memstream component. Currently utilized by MVAU, VVAU and HLS Thresholding layer.""" ops = ["MVAU_hls", "MVAU_rtl", "VVAU_hls", "VVAU_rtl", "Thresholding_hls"] if self.onnx_node.op_type in ops or self.onnx_node.op_type.startswith("Elementwise"): - template_path = str( + template_path = ( Path(get_settings().finn_rtllib) / "memstream" / "hdl" @@ -455,9 +452,9 @@ def generate_hdl_memstream(self, fpgapart, pumped_memory=0): else: depth = self.calc_wmem() padded_width = self.get_instream_width_padded(1) - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) - ram_style = self.get_nodeattr("ram_style") + ram_style = cast("str", self.get_nodeattr("ram_style")) init_file = code_gen_dir + "/memblock.dat" if ram_style == "ultra" and not is_versal(fpgapart): init_file = "" @@ -471,48 +468,43 @@ def generate_hdl_memstream(self, fpgapart, pumped_memory=0): "$PUMPED_MEMORY$": [str(pumped_memory)], } # apply code generation to template - with open(template_path, "r") as f: + with template_path.open() as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' code_gen_line = "\n".join(code_gen_dict[key]) template_wrapper = template_wrapper.replace(key, code_gen_line) - with open( - os.path.join(code_gen_dir, mname + "_memstream_wrapper.v"), - "w", - ) as f: + with (Path(code_gen_dir) / (mname + "_memstream_wrapper.v")).open("w") as f: f.write(template_wrapper) else: pass - def generate_hdl_fetch_weights(self, fpgapart): - """Helper function to generate verilog code for fetch_weights component. + def generate_hdl_fetch_weights(self, fpgapart: str) -> None: # noqa: ARG002 + """Generate verilog code for fetch_weights component. Currently utilized by MVAU.""" ops = ["MVAU_hls", "MVAU_rtl"] if self.onnx_node.op_type in ops or self.onnx_node.op_type.startswith("Elementwise"): - template_path = os.path.join( - get_settings().finn_rtllib, "mlo", "fetch_weights_wrapper.v" - ) + template_path = Path(get_settings().finn_rtllib) / "mlo" / "fetch_weights_wrapper.v" mname = self.onnx_node.name wdt = self.get_input_datatype(1) if self.onnx_node.op_type in ops: - mw = self.get_nodeattr("MW") - mh = self.get_nodeattr("MH") - pe = self.get_nodeattr("PE") - simd = self.get_nodeattr("SIMD") - n_reps = np.prod(self.get_nodeattr("numInputVectors")) + mw = cast("int", self.get_nodeattr("MW")) + mh = cast("int", self.get_nodeattr("MH")) + pe = cast("int", self.get_nodeattr("PE")) + simd = cast("int", self.get_nodeattr("SIMD")) + n_reps = np.prod(cast("list[int]", self.get_nodeattr("numInputVectors"))) else: # Eltwise layers only have one parallelism parameter mw = 1 - mh = self.get_nodeattr("rhs_shape")[-1] - pe = self.get_nodeattr("PE") + mh = cast("list[int]", self.get_nodeattr("rhs_shape"))[-1] + pe = cast("int", self.get_nodeattr("PE")) simd = 1 # TODO use broadcast rhs shape here - n_reps = np.prod(self.get_nodeattr("rhs_shape")[:-1]) + n_reps = np.prod(cast("list[int]", self.get_nodeattr("rhs_shape"))[:-1]) layer_offs = mw * mh # upper bound on how many layers can be supported, set to 64 for now n_max_layers = 64 - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) code_gen_dict = { "$MODULE_NAME_AXI_WRAPPER$": [mname + "_fetch_weights_wrapper"], "$MW$": [str(mw)], @@ -525,16 +517,13 @@ def generate_hdl_fetch_weights(self, fpgapart): "$N_LAYERS$": [str(n_max_layers)], } # apply code generation to template - with open(template_path, "r") as f: + with template_path.open("r") as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' code_gen_line = "\n".join(code_gen_dict[key]) template_wrapper = template_wrapper.replace(key, code_gen_line) - with open( - os.path.join(code_gen_dir, mname + "_fetch_weights_wrapper.v"), - "w", - ) as f: + with (Path(code_gen_dir) / (mname + "_fetch_weights_wrapper.v")).open("w") as f: f.write(template_wrapper) else: pass @@ -545,7 +534,7 @@ def generate_hdl_dynload(self) -> None: Path(get_settings().finn_rtllib) / "dynload" / "hdl" / "dynamic_load_wrapper_template.v" ) mname = self.onnx_node.name - pe = self.get_nodeattr("PE") + pe = cast("int", self.get_nodeattr("PE")) simd = self.get_nodeattr("SIMD") mh = self.get_nodeattr("MH") mw = self.get_nodeattr("MW") @@ -577,125 +566,8 @@ def generate_hdl_dynload(self) -> None: with output_path.open("w") as f: f.write(template_wrapper) - @deprecated - def derive_characteristic_fxns( - self, period: int, override_rtlsim_dict: dict | None = None, pre_hook=None - ) -> None: - """Return the unconstrained characteristic functions for this node. - - Args: - period: The characterization period. - override_rtlsim_dict: Optional dictionary to override rtlsim settings. - - Raises: - ValueError: If period is too short to characterize the node. - - """ - # ensure rtlsim is ready - assert self.get_nodeattr("rtlsim_so") != "", "rtlsim not ready for " + self.onnx_node.name - if cast("int | float", self.get_nodeattr("io_chrc_period")) > 0: - log.warning(f"Skipping node {self.onnx_node.name}: already has FIFO characteristic") - return - exp_cycles = self.get_exp_cycles() - n_inps = np.prod(self.get_folded_input_shape()[:-1]) - n_outs = np.prod(self.get_folded_output_shape()[:-1]) - if exp_cycles == 0: - # try to come up with an optimistic estimate - exp_cycles = min(n_inps, n_outs) - if exp_cycles > period: - raise ValueError( - f"Period {period} too short to characterize {self.onnx_node.name} : " - f"expects min {n_inps} cycles" - ) - sim = self.get_rtlsim() - if override_rtlsim_dict is not None: - io_dict = override_rtlsim_dict - else: - io_dict = { - "inputs": { - "in0": list(range(n_inps)), - }, - "outputs": {"out0": []}, - } - - # extra dicts to keep track of cycle-by-cycle transaction behavior - # note that we restrict key names to filter out weight streams etc - txns_in = {key: [] for (key, value) in io_dict["inputs"].items() if "in" in key} - txns_out = {key: [] for (key, value) in io_dict["outputs"].items() if "out" in key} - # signal name, note no underscore at the end (new finnxsi behavior) - sname = "_V" - self.reset_rtlsim(sim) - if pre_hook is not None: - pre_hook(sim) - # create stream tracers for all input and output streams - for k in txns_in.keys(): - txns_in[k] = sim.trace_stream(k + sname) # type: ignore - for k in txns_out.keys(): - txns_out[k] = sim.trace_stream(k + sname) # type: ignore - # For characterization, use period as liveness threshold directly - total_cycle_count = finnxsi.rtlsim_multi_io( - sim, - io_dict, - num_out_values=self.get_number_output_values(), - sname=sname, - liveness_threshold=period, - ) - self.set_nodeattr("cycles_rtlsim", total_cycle_count) - assert ( - total_cycle_count <= period - ), f"""Total cycle count from rtl simulation is higher than - specified period, please set the period higher than {total_cycle_count}""" - self.set_nodeattr("io_chrc_period", period) - # call str() on stream tracers to get their outputs, and convert - # to list of ints - for k in txns_in.keys(): - txns_in[k] = [int(c) for c in str(txns_in[k])] - for k in txns_out.keys(): - txns_out[k] = [int(c) for c in str(txns_out[k])] - - def accumulate_char_fxn(chrc: list) -> npt.NDArray[np.int32]: - """Accumulate characteristic function over two periods.""" - p = len(chrc) - ret = [] - for t in range(2 * p): - if t == 0: - ret.append(chrc[0]) - else: - ret.append(ret[-1] + chrc[t % p]) - return np.asarray(ret, dtype=np.int32) - - all_txns_in = np.empty((len(txns_in.keys()), 2 * period), dtype=np.int32) - all_txns_out = np.empty((len(txns_out.keys()), 2 * period), dtype=np.int32) - all_pad_in = [] - all_pad_out = [] - for in_idx, in_strm_nm in enumerate(txns_in.keys()): - txn_in = txns_in[in_strm_nm] - pad_in = 0 - if len(txn_in) < period: - pad_in = period - len(txn_in) - txn_in += [0 for x in range(pad_in)] - txn_in = accumulate_char_fxn(txn_in) - all_txns_in[in_idx, :] = txn_in - all_pad_in.append(pad_in) - - for out_idx, out_strm_nm in enumerate(txns_out.keys()): - txn_out = txns_out[out_strm_nm] - pad_out = 0 - if len(txn_out) < period: - pad_out = period - len(txn_out) - txn_out += [0 for x in range(pad_out)] - txn_out = accumulate_char_fxn(txn_out) - all_txns_out[out_idx, :] = txn_out - all_pad_out.append(pad_out) - - self.set_nodeattr("io_chrc_in", all_txns_in) - self.set_nodeattr("io_chrc_out", all_txns_out) - self.set_nodeattr("io_chrc_pads_in", all_pad_in) - self.set_nodeattr("io_chrc_pads_out", all_pad_out) - - def adapt_for_loop_body(self, input_types): - """ - Called by LoopRolling transformation to allow operators to adapt their + def adapt_for_loop_body(self, input_types: "list[LoopBodyInputType]") -> None: + """Called by LoopRolling transformation to allow operators to adapt their attributes when being placed inside a loop body. This base implementation does nothing. Operators that need to modify @@ -710,5 +582,5 @@ def adapt_for_loop_body(self, input_types): If an operator has a parameter that becomes a streamed input in a loop context (PARAMETER type), it might need to change an attribute like `rhs_style` from "const" to "input". - """ - pass # Default: no adaptation needed + """ # noqa: D401 + # Default: no adaptation needed diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 0a62e2fd09..0a8efbfc4b 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -26,8 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" -Basic utility functions and classes for FINN. +"""Basic utility functions and classes for FINN. This module provides essential utility functions and classes used throughout the FINN framework, including: @@ -42,22 +41,23 @@ basic system operations, hardware abstraction, and build tool integration. """ -from __future__ import annotations - import os import subprocess import tempfile from pathlib import Path - from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.util.basic import gen_finn_dt_tensor -from typing import Dict +from typing import TYPE_CHECKING, cast from finn.util.data_packing import finnpy_to_packed_bytearray +from finn.util.exception import FINNInternalError from finn.util.logging import log from finn.util.settings import get_settings +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp + # test boards used for bnn pynq tests test_board_map = ["Pynq-Z1", "KV260_SOM", "ZCU104", "U55C"] @@ -112,7 +112,7 @@ part_map["V80"] = "xcv80-lsva4737-2MHP-e-s" -def get_rtlsim_trace_depth(): +def get_rtlsim_trace_depth() -> int: """Return the trace depth for rtlsim. Controllable via the RTLSIM_TRACE_DEPTH environment variable. If the env.var. is undefined, the default value of 1 is returned. A trace depth of 1 @@ -124,27 +124,12 @@ def get_rtlsim_trace_depth(): - level 2 shows per-layer input/output streams - level 3 shows per full-layer I/O including FIFO count signals """ - try: return int(os.environ["RTLSIM_TRACE_DEPTH"]) except KeyError: return 1 -def get_finn_root(): - """ - Deprecated function that should not be used anymore. - - This function was previously used to get the FINN root directory, - but has been deprecated and should not be called in new code. - - Raises: - Exception: Always raises an exception indicating the function - should not be used. - """ - raise Exception("get_finn_root() should not be used anymore.") - - def get_vivado_root() -> str: """Return the root directory that Vivado is installed into.""" try: @@ -199,8 +184,8 @@ def __str__(self) -> str: def launch_process_helper( - args, - proc_env=None, + args: list[str | Path] | list[str] | list[Path], + proc_env: dict[str, str] | None = None, cwd: str | Path | None = None, print_stdout: bool = True, print_stderr: bool = True, @@ -208,7 +193,9 @@ def launch_process_helper( """Launch a helper process in a way that facilitates logging stdout/stderr with Python loggers. Returns (cmd_out, cmd_err) if successful, raises CalledProcessError otherwise.""" - process = subprocess.run(args, capture_output=True, env=proc_env, cwd=cwd, text=True) + process = subprocess.run( + [str(arg) for arg in args], capture_output=True, env=proc_env, cwd=cwd, text=True + ) cmd_out = process.stdout.strip() cmd_err = process.stderr.strip() @@ -231,7 +218,7 @@ def launch_process_helper( log.error(cmd_err) # Log additional ERROR message - cmd = " ".join(args) if isinstance(args, list) else args + cmd = " ".join(str(arg) for arg in args) if isinstance(args, list) else str(args) log.error(f"Launched process returned non-zero exit code ({process.returncode}): {cmd}") # Raise CalledProcessError for non-zero return code, including captured output @@ -242,33 +229,32 @@ def launch_process_helper( return (cmd_out, cmd_err) -def which(program): - "Python equivalent of the shell cmd 'which'." +def which(program: str | Path) -> str | Path | None: + """Python equivalent of the shell cmd 'which'.""" # source: # https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python - def is_exe(fpath): - """ - Check if a file path points to an executable file. + def is_exe(fpath: str | Path) -> bool: + """Check if a file path points to an executable file. Tests whether the given file path exists and has execute permissions. This is a helper function used by the which() function. Args: - fpath (str): File path to check for executability. + fpath (str | Path): File path to check for executability. Returns: bool: True if the file exists and is executable, False otherwise. """ - return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + return Path(fpath).is_file() and os.access(fpath, os.X_OK) - fpath, fname = os.path.split(program) + fpath, _fname = os.path.split(program) if fpath: if is_exe(program): return program else: for path in os.environ["PATH"].split(os.pathsep): - exe_file = os.path.join(path, program) + exe_file = Path(path) / program if is_exe(exe_file): return exe_file @@ -279,9 +265,8 @@ class CppBuilder: """Builds the g++ compiler command to produces the executable of the c++ code in code_gen_dir which is passed to the function build() of this class.""" - def __init__(self): - """ - Initialize a new CppBuilder instance. + def __init__(self) -> None: + """Initialize a new CppBuilder instance. Sets up empty lists and variables for building C++ compilation commands. All instance variables are initialized to empty states and should be @@ -295,27 +280,27 @@ def __init__(self): compile_components (list): List of compilation command components compile_script (str): Generated compilation script content """ - self.include_paths = [] - self.cpp_files = [] - self.executable_path = "" - self.code_gen_dir = "" - self.compile_components = [] - self.compile_script = "" - - def append_includes(self, library_path): - """Adds given library path to include_paths list.""" + self.include_paths: list[str] = [] + self.cpp_files: list[str] = [] + self.executable_path: str = "" + self.code_gen_dir: str = "" + self.compile_components: list[str] = [] + self.compile_script: Path + + def append_includes(self, library_path: str) -> None: + """Add given library path to include_paths list.""" self.include_paths.append(library_path) - def append_sources(self, cpp_file): - """Adds given c++ file to cpp_files list.""" + def append_sources(self, cpp_file: str) -> None: + """Add given c++ file to cpp_files list.""" self.cpp_files.append(cpp_file) - def set_executable_path(self, path): - """Sets member variable "executable_path" to given path.""" + def set_executable_path(self, path: str) -> None: + """Set member variable "executable_path" to given path.""" self.executable_path = path - def build(self, code_gen_dir): - """Builds the g++ compiler command according to entries in include_paths + def build(self, code_gen_dir: str) -> None: + """Build the g++ compiler command according to entries in include_paths and cpp_files lists. Saves it in bash script in given folder and executes it.""" # raise error if includes are empty @@ -328,25 +313,24 @@ def build(self, code_gen_dir): bash_compile = "" for component in self.compile_components: bash_compile += str(component) + " " - self.compile_script = str(self.code_gen_dir) + "/compile.sh" - with open(self.compile_script, "w") as f: + self.compile_script = Path(self.code_gen_dir) / "compile.sh" + with self.compile_script.open("w") as f: f.write("#!/bin/bash \n") f.write(bash_compile + "\n") - bash_command = ["bash", self.compile_script] + bash_command = ["bash", str(self.compile_script)] launch_process_helper(bash_command, print_stdout=False) -def is_versal(fpgapart): - """Returns whether board is part of the Versal family""" +def is_versal(fpgapart: str) -> bool: + """Return whether board is part of the Versal family.""" return fpgapart[0:4] in ["xcvc", "xcve", "xcvp", "xcvm", "xqvc", "xqvm"] or fpgapart[0:5] in [ "xqrvc", "xcv80", ] -def get_dsp_block(fpgapart): - """ - Determine the DSP block type based on the FPGA part name. +def get_dsp_block(fpgapart: str) -> str: + """Determine the DSP block type based on the FPGA part name. Different FPGA families and generations use different DSP block types. This function maps FPGA part names to their corresponding DSP block @@ -363,49 +347,67 @@ def get_dsp_block(fpgapart): """ if is_versal(fpgapart): return "DSP58" - elif fpgapart[2] == "7": + if fpgapart[2] == "7": return "DSP48E1" - else: - return "DSP48E2" + return "DSP48E2" -def get_driver_shapes(model: ModelWrapper) -> Dict: +def get_driver_shapes(model: ModelWrapper) -> dict: """Get all the IO shapes for the driver.""" idt = [] idma_names = [] ishape_normal = [] ishape_folded = [] ishape_packed = [] - for idma_ind, graph_in in enumerate(model.graph.input): + for _idma_ind, graph_in in enumerate(model.graph.input): i_tensor_name = graph_in.name # get inp tensor properties i_tensor_dt = model.get_tensor_datatype(i_tensor_name) - i_tensor_shape_normal = tuple(model.get_tensor_shape(i_tensor_name)) + tensor_shape = model.get_tensor_shape(i_tensor_name) + if tensor_shape is None: + raise FINNInternalError( + f"Input tensor {i_tensor_name} has no " + "shape information when generating driver shapes." + ) + i_tensor_shape_normal = tuple(tensor_shape) # go down into dataflow partition to get folded shape info etc # TODO consider setting these as attributes during dataflow partitioning i_consumer = model.find_consumer(i_tensor_name) - assert ( - i_consumer.op_type == "StreamingDataflowPartition" - ), """ - Ensure CreateDataflowPartition called before driver creation.""" - first_df_model = ModelWrapper(getCustomOp(i_consumer).get_nodeattr("model")) + if i_consumer is None: + raise FINNInternalError( + f"Input tensor {i_tensor_name} has no consumer when generating driver shapes." + ) + if i_consumer.op_type != "StreamingDataflowPartition": + raise FINNInternalError("Ensure CreateDataflowPartition called before driver creation.") + first_df_model = ModelWrapper(cast("str", getCustomOp(i_consumer).get_nodeattr("model"))) assert ( first_df_model.graph.node[0].op_type == "IODMA_hls" ), "First partition must hold input IODMA" successors = model.find_direct_successors(i_consumer) + if successors is None or len(successors) == 0: + raise FINNInternalError( + f"Input tensor {i_tensor_name} has no successor when generating driver shapes." + ) successor_input_num = list(successors[0].input).index(i_consumer.output[0]) successor_sdp = getCustomOp(successors[0]) - successor_df_model = ModelWrapper(successor_sdp.get_nodeattr("model")) + successor_df_model = ModelWrapper(cast("str", successor_sdp.get_nodeattr("model"))) first_node = successor_df_model.find_consumer( successor_df_model.graph.input[successor_input_num].name ) - i_tensor_shape_folded = tuple(getCustomOp(first_node).get_folded_input_shape()) + if first_node is None: + raise FINNInternalError( + f"Input tensor {i_tensor_name} has no consumer in the " + "dataflow partition when generating driver shapes." + ) + i_tensor_shape_folded = tuple( + cast("HWCustomOp", getCustomOp(first_node)).get_folded_input_shape() + ) # generate dummy folded i/o tensors and their packed versions i_tensor_dummy_folded = gen_finn_dt_tensor(i_tensor_dt, i_tensor_shape_folded) i_tensor_dummy_packed = finnpy_to_packed_bytearray(i_tensor_dummy_folded, i_tensor_dt) i_tensor_shape_packed = i_tensor_dummy_packed.shape # append all input tensor info to relevant lists - idt.append("DataType['%s']" % i_tensor_dt.name) + idt.append(f"DataType['{i_tensor_dt.name}']") ishape_normal.append(i_tensor_shape_normal) ishape_folded.append(i_tensor_shape_folded) ishape_packed.append(i_tensor_shape_packed) @@ -416,33 +418,54 @@ def get_driver_shapes(model: ModelWrapper) -> Dict: oshape_normal = [] oshape_folded = [] oshape_packed = [] - for odma_ind, graph_out in enumerate(model.graph.output): + for _odma_ind, graph_out in enumerate(model.graph.output): o_tensor_name = graph_out.name # get inp tensor properties o_tensor_dt = model.get_tensor_datatype(o_tensor_name) - o_tensor_shape_normal = tuple(model.get_tensor_shape(o_tensor_name)) + tensor_shape = model.get_tensor_shape(o_tensor_name) + if tensor_shape is None: + raise FINNInternalError( + f"Output tensor {o_tensor_name} has no " + "shape information when generating driver shapes." + ) + o_tensor_shape_normal = tuple(tensor_shape) # go down into IODMA partition to get folded shape info etc # TODO consider setting these as attributes during dataflow partitioning o_producer = model.find_producer(o_tensor_name) - assert ( - o_producer.op_type == "StreamingDataflowPartition" - ), """ - Ensure CreateDataflowPartition called before driver creation.""" - df_model = ModelWrapper(getCustomOp(o_producer).get_nodeattr("model")) + if o_producer is None: + raise FINNInternalError( + f"Output tensor {o_tensor_name} has no producer when generating driver shapes." + ) + if o_producer.op_type != "StreamingDataflowPartition": + raise FINNInternalError( + f"Output tensor {o_tensor_name} is not part of a StreamingDataflowPartition." + ) + df_model = ModelWrapper(cast("str", getCustomOp(o_producer).get_nodeattr("model"))) assert df_model.graph.node[-1].op_type == "IODMA_hls", "Partition must hold output IODMA" predecessors = model.find_direct_predecessors(o_producer) + if predecessors is None or len(predecessors) == 0: + raise FINNInternalError( + f"Output tensor {o_tensor_name} has no predecessor when generating driver shapes." + ) predecessor_output_num = list(predecessors[0].output).index(o_producer.input[0]) predecessor_sdp = getCustomOp(predecessors[0]) - predecessor_df_model = ModelWrapper(predecessor_sdp.get_nodeattr("model")) + predecessor_df_model = ModelWrapper(cast("str", predecessor_sdp.get_nodeattr("model"))) last_node = predecessor_df_model.find_producer( predecessor_df_model.graph.output[predecessor_output_num].name ) - o_tensor_shape_folded = tuple(getCustomOp(last_node).get_folded_output_shape()) + if last_node is None: + raise FINNInternalError( + f"Output tensor {o_tensor_name} has no producer in the " + "dataflow partition when generating driver shapes." + ) + o_tensor_shape_folded = tuple( + cast("HWCustomOp", getCustomOp(last_node)).get_folded_output_shape() + ) o_tensor_dummy_folded = gen_finn_dt_tensor(o_tensor_dt, o_tensor_shape_folded) o_tensor_dummy_packed = finnpy_to_packed_bytearray(o_tensor_dummy_folded, o_tensor_dt) o_tensor_shape_packed = o_tensor_dummy_packed.shape # append all output tensor info to relevant lists - odt.append("DataType['%s']" % o_tensor_dt.name) + odt.append(f"DataType['{o_tensor_dt.name}']") oshape_normal.append(o_tensor_shape_normal) oshape_folded.append(o_tensor_shape_folded) oshape_packed.append(o_tensor_shape_packed) From 410744d449b738fb8673d063411191a030e5900f Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:47:13 +0200 Subject: [PATCH 092/170] Remove characterization based FIFO sizing --- src/finn/builder/build_dataflow_steps.py | 100 +++------ .../fpgadataflow/duplicatestreams.py | 10 - .../fpgadataflow/hls/thresholding_hls.py | 30 --- .../fpgadataflow/matrixvectoractivation.py | 22 -- .../rtl/elementwise_binary_rtl.py | 11 - .../custom_op/fpgadataflow/rtl/finn_loop.py | 4 - .../fpgadataflow/vectorvectoractivation.py | 21 -- .../fpgadataflow/derive_characteristic.py | 192 ------------------ 8 files changed, 27 insertions(+), 363 deletions(-) delete mode 100644 src/finn/transformation/fpgadataflow/derive_characteristic.py diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index ded697716d..272b20650c 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -27,6 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# ruff: noqa: SLF001 """Collection of default build steps for building and verifying a dataflow accelerator from an ONNX model. """ @@ -52,7 +53,6 @@ from qonnx.transformation.infer_datatypes import InferDataTypes from qonnx.transformation.infer_shapes import InferShapes from qonnx.transformation.lower_convs_to_matmul import LowerConvsToMatMul -from qonnx.util.basic import get_by_name from qonnx.util.cleanup import cleanup_model from shutil import copy, move @@ -79,10 +79,6 @@ from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.create_dataflow_partition import CreateDataflowPartition from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP -from finn.transformation.fpgadataflow.derive_characteristic import ( - DeriveCharacteristic, - DeriveFIFOSizes, -) from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.insert_dwc import InsertDWC from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO @@ -138,7 +134,7 @@ def verify_step( step_name: str, need_parent: bool, rtlsim_pre_hook=None, -): +) -> None: """Verify a build step by running simulation and comparing results. Args: @@ -420,8 +416,7 @@ def prepare_loop_ops_ipgen(node, cfg): def step_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig): - """ - This step will only execute if QONNX nodes are found. + """This step will only execute if QONNX nodes are found. These include the following op_types: "Quant" , "Trunc" and "BinaryQuant". If such nodes are found the step will run the tidy-up step from QONNX and then convert the QONNX model to the FINN-ONNX dialect. @@ -450,11 +445,10 @@ def step_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig): return model -def step_tidy_up(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_tidy_up(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Run the tidy-up step on given model. This includes shape and datatype inference, constant folding, and giving nodes and tensors better names. """ - model = model.transform(InferShapes()) model = model.transform(FoldConstants()) model = model.transform(GiveUniqueNodeNames()) @@ -468,14 +462,13 @@ def step_tidy_up(model: ModelWrapper, cfg: DataflowBuildConfig): return model -def step_streamline(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_streamline(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Run streamlining on given model. Streamlining involves moving floating point scale/shift parameters around, collapsing adjacent ones into a single parameter, then absorbing the scale/shift into the following `MultiThreshold` node. Streamlining requires careful topology design and cannot be applied to all topologies. """ - model = model.transform(absorb.AbsorbSignBiasIntoMultiThreshold()) model = model.transform(Streamline()) need_lowering = len(model.get_nodes_by_op_type("Conv")) > 0 @@ -498,7 +491,7 @@ def step_streamline(model: ModelWrapper, cfg: DataflowBuildConfig): return model -def step_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Convert eligible nodes to `HWCustomOp` subclasses that represent HW layers. Which nodes and particular configurations can be converted to HW is limited, see the source code of the `convert_to_hw` module for more. @@ -660,11 +653,10 @@ def apply_if_relevant(model, op_types, transform, desc=""): return model -def step_create_dataflow_partition(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_create_dataflow_partition(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Separate consecutive groups of HWCustomOp nodes into StreamingDataflowPartition nodes, which point to a separate ONNX file. Dataflow accelerator synthesis can only be performed on those HWCustomOp sub-graphs.""" - unmapped_layers = [ node.name for node in model.graph.node @@ -713,13 +705,12 @@ def step_create_dataflow_partition(model: ModelWrapper, cfg: DataflowBuildConfig return model -def step_specialize_layers(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_specialize_layers(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Convert HW nodes to either an HLS or RTL variant of the node. HW nodes get converted either based on pre-determined rules (details can be found in `specialize_layers` source code) or the user provides a configuration file which contains the desired setting. If the user preference cannot be fulfilled, a warning will be printed and the implementation style will be set to a default.""" - if cfg.specialize_layers_config_file is not None: model = model.transform(GiveUniqueNodeNames()) model = model.transform(ApplyConfig(cfg.specialize_layers_config_file)) @@ -730,13 +721,13 @@ def step_specialize_layers(model: ModelWrapper, cfg: DataflowBuildConfig): return model -def step_transpose_decomposition(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_transpose_decomposition(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Decomposes a Shuffle into a chain of InnerShuffle and OuterShuffles that can be specialised into hardware operators. This should be executed after the folding has been configured. """ # check if model contains a Shuffle node - has_shuffle = True if model.get_nodes_by_op_type("Shuffle") else False + has_shuffle = bool(model.get_nodes_by_op_type("Shuffle")) loop_nodes = model.get_nodes_by_op_type("FINNLoop") for node in loop_nodes: node_inst = getCustomOp(node) @@ -761,12 +752,11 @@ def step_transpose_decomposition(model: ModelWrapper, cfg: DataflowBuildConfig): return model -def step_target_fps_parallelization(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_target_fps_parallelization(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """If target_fps was specified, use the SetFolding transformation to determine parallelization attributes. The auto-generated config will be saved under auto_folding_config.json under the outputs, which can serve as a basis for customizing the folding factors further.""" - target_cycles_per_frame = cfg._resolve_cycles_per_frame() if target_cycles_per_frame is not None: model = model.transform( @@ -813,10 +803,9 @@ def step_target_fps_parallelization(model: ModelWrapper, cfg: DataflowBuildConfi return model -def step_apply_folding_config(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_apply_folding_config(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Apply the folding configuration file onto the model to set folding (parallelization) and other attributes, if config file is specified.""" - model = model.transform(GiveUniqueNodeNames()) loop_nodes = model.get_nodes_by_op_type("FINNLoop") for node in loop_nodes: @@ -832,9 +821,8 @@ def step_apply_folding_config(model: ModelWrapper, cfg: DataflowBuildConfig): return model -def step_generate_estimate_reports(model: ModelWrapper, cfg: DataflowBuildConfig): - "Generate per-layer resource and cycle estimates using analytical models." - +def step_generate_estimate_reports(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Generate per-layer resource and cycle estimates using analytical models.""" if DataflowOutputType.ESTIMATE_REPORTS in cfg.generate_outputs: report_dir = cfg.output_dir + "/report" os.makedirs(report_dir, exist_ok=True) @@ -907,7 +895,7 @@ def step_generate_estimate_reports(model: ModelWrapper, cfg: DataflowBuildConfig return model -def step_minimize_bit_width(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_minimize_bit_width(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Tighten the weight and accumulator bit widths for each layer.""" if cfg.minimize_bit_width: model = model.transform(MinimizeWeightBitWidth(), apply_to_subgraphs=True) @@ -949,10 +937,9 @@ def step_minimize_bit_width(model: ModelWrapper, cfg: DataflowBuildConfig): return model -def step_hw_codegen(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_hw_codegen(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Generate Vitis HLS code to prepare HLSBackend nodes for IP generation. And fills RTL templates for RTLBackend nodes.""" - model = model.transform(GiveUniqueNodeNames()) loop_nodes = model.get_nodes_by_op_type("FINNLoop") for node in loop_nodes: @@ -965,7 +952,7 @@ def step_hw_codegen(model: ModelWrapper, cfg: DataflowBuildConfig): return model -def step_hw_ipgen(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_hw_ipgen(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Run Vitis HLS synthesis on generated code for HLSBackend nodes, in order to generate IP blocks. For RTL nodes this step does not do anything.""" @@ -1013,8 +1000,8 @@ def step_build_simulation(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mode model = model.transform( BuildSimulation( - cfg._resolve_fpga_part(), # noqa - cfg._resolve_hls_clk_period(), # noqa + cfg._resolve_fpga_part(), + cfg._resolve_hls_clk_period(), cfg.functional_simulation, ) ) @@ -1029,8 +1016,8 @@ def step_size_fifo_isolated(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo model = model.transform( RunLayerIsolatedSimulation( - cfg._resolve_fpga_part(), # noqa - cfg._resolve_hls_clk_period(), # noqa + cfg._resolve_fpga_part(), + cfg._resolve_hls_clk_period(), cfg.functional_simulation, Path(cfg.output_dir), ) @@ -1043,9 +1030,7 @@ def step_size_fifo_connected(model: ModelWrapper, cfg: DataflowBuildConfig) -> M from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation model = model.transform( - RunLayerParallelSimulation( - cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg # noqa # noqa - ) + RunLayerParallelSimulation(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg) ) return model @@ -1059,15 +1044,14 @@ def step_apply_fifosizes(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model return model -def step_insert_dwc(model: ModelWrapper, cfg: DataflowBuildConfig): - """Inserts data width converters between layers where necessary.""" +def step_insert_dwc(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Insert data width converters between layers where necessary.""" model = model.transform(InsertDWC()) return model.transform(SpecializeLayers(cfg._resolve_fpga_part())) -def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig): - """ - Depending on the auto_fifo_depths setting, do one of the following: +def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Depending on the auto_fifo_depths setting, do one of the following: * if auto_fifo_depths=True: Run the appropriate auto-sizing transformation to attempt to determine the FIFO sizes that provide full throughput. May take a long time. @@ -1077,7 +1061,6 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig): Coherency with config file node naming is ensured by calling `GiveUniqueNodeNames`. """ - hw_attrs = [ "PE", "SIMD", @@ -1163,36 +1146,7 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig): if cfg.auto_fifo_depths: strategy = cfg.auto_fifo_strategy - if strategy == "characterize" or is_mlo(model): - model = model.transform(InsertDWC()) - model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) - model = model.transform(GiveUniqueNodeNames()) - model = model.transform( - PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period()) - ) - model = model.transform(HLSSynthIP(cfg._resolve_fpga_part())) - model = model.transform(PrepareRTLSim(behav=True)) - model = model.transform(AnnotateCycles()) - period = model.analysis(dataflow_performance)["max_cycles"] + 10 - model = model.transform(DeriveCharacteristic(period)) - model = model.transform(DeriveFIFOSizes()) - model = model.transform( - InsertFIFO( - vivado_ram_style=cfg.large_fifo_mem_style, - max_qsrl_depth=256, - create_shallow_fifos=True, - ) - ) - # Clean up characterization attributes after FIFO sizing - for node in model.graph.node: - for attr_name in ["io_chrc_period", "io_chrc_in", "io_chrc_out"]: - attr = get_by_name(node.attribute, attr_name) - if attr is not None: - node.attribute.remove(attr) - model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) - elif strategy == "largefifo_rtlsim": + if strategy == "largefifo_rtlsim": if cfg.fifosim_save_waveform: report_dir = cfg.output_dir + "/report" os.makedirs(report_dir, exist_ok=True) diff --git a/src/finn/custom_op/fpgadataflow/duplicatestreams.py b/src/finn/custom_op/fpgadataflow/duplicatestreams.py index d54069d7a0..264fffda6c 100644 --- a/src/finn/custom_op/fpgadataflow/duplicatestreams.py +++ b/src/finn/custom_op/fpgadataflow/duplicatestreams.py @@ -149,13 +149,3 @@ def execute_node(self, context, graph): output = np.asarray([output], dtype=np.float32).reshape(*exp_shape) for outp in node.output: context[outp] = output - - def derive_characteristic_fxns(self, period): - n_inps = np.prod(self.get_folded_input_shape()[:-1]) - io_dict = { - "inputs": { - "in0": [0 for i in range(n_inps)], - }, - "outputs": {"out0": [], "out1": []}, - } - super().derive_characteristic_fxns(period, override_rtlsim_dict=io_dict) diff --git a/src/finn/custom_op/fpgadataflow/hls/thresholding_hls.py b/src/finn/custom_op/fpgadataflow/hls/thresholding_hls.py index 16e1d97808..30864d08d2 100644 --- a/src/finn/custom_op/fpgadataflow/hls/thresholding_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/thresholding_hls.py @@ -774,36 +774,6 @@ def ipgen_extra_directives(self) -> list[str]: """ return ["config_compile -pipeline_style frp"] - def derive_characteristic_fxns( - self, period: int, override_rtlsim_dict: dict | None = None # noqa: ARG002 - ) -> None: - """Derive characteristic functions for performance estimation. - - Parameters - ---------- - period : int - Clock period in nanoseconds - override_rtlsim_dict : dict | None - Optional dictionary to override RTL simulation parameters. - - Returns - ------- - None - """ - n_inps = np.prod(self.get_folded_input_shape()[:-1]) - io_dict = { - "inputs": { - "in0": [0 for i in range(n_inps)], - }, - "outputs": {"out0": []}, - } - mem_mode = self.get_nodeattr("mem_mode") - if mem_mode in ["internal_decoupled", "external"]: - n_weight_inps = self.calc_tmem() - num_w_reps = np.prod(self.get_nodeattr("numInputVectors")) - io_dict["inputs"]["in1"] = [0 for i in range(num_w_reps * n_weight_inps)] - super().derive_characteristic_fxns(period, override_rtlsim_dict=io_dict) - def minimize_weight_bit_width(self, model): """Minimize threshold datatype, with HLS-specific adjustments. diff --git a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py index a234e8f6f8..69969395c0 100644 --- a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py @@ -1018,28 +1018,6 @@ def get_op_and_param_counts(self): ret_dict[thres_param_type] = thres_count return ret_dict - def derive_characteristic_fxns(self, period): - """Derive characteristic performance functions for this node. - - Parameters - ---------- - period : float - Clock period in nanoseconds - """ - n_inps = np.prod(self.get_folded_input_shape()[:-1]) - io_dict = { - "inputs": { - "in0": [0 for i in range(n_inps)], - }, - "outputs": {"out0": []}, - } - mem_mode = self.get_nodeattr("mem_mode") - if mem_mode in ["internal_decoupled", "external"]: - n_weight_inps = self.calc_wmem() - num_w_reps = np.prod(self.get_nodeattr("numInputVectors")) - io_dict["inputs"]["in1"] = [0 for i in range(num_w_reps * n_weight_inps)] - super().derive_characteristic_fxns(period, override_rtlsim_dict=io_dict) - def get_verilog_top_module_intf_names(self): """Get Verilog top module interface names for this node. diff --git a/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py index 6994c95c1c..0356ba8364 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py @@ -308,17 +308,6 @@ def instantiate_ip(self, cmd): "create_bd_cell -type hier -reference %s /%s/%s" % (top_module, node_name, node_name) ) - def derive_characteristic_fxns(self, period, override_rtlsim_dict=None, pre_hook=None): - n_inps = np.prod(self.get_folded_input_shape(0)[:-1]) - io_dict = { - "inputs": { - "in0": [i for i in range(n_inps)], - "in1": [i for i in range(n_inps)], - }, - "outputs": {"out0": []}, - } - super().derive_characteristic_fxns(period, override_rtlsim_dict=io_dict, pre_hook=pre_hook) - def execute_node(self, context, graph): mode = self.get_nodeattr("exec_mode") if mode == "rtlsim": diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index 0a9a1e1106..dfbe6ad11f 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -294,10 +294,6 @@ def prepare_rtlsim(self, behav=False): sim_base, sim_rel = rtlsim_so self.set_nodeattr("rtlsim_so", sim_base + "/" + sim_rel) - def derive_characteristic_fxns(self, period): - mlo_prehook = mlo_prehook_func_factory(self.onnx_node) - super().derive_characteristic_fxns(period, pre_hook=mlo_prehook) - def execute_node(self, context, graph): node = self.onnx_node inp_values = context[node.input[0]] diff --git a/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py b/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py index 454e21db6c..0753436fdb 100644 --- a/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py @@ -911,27 +911,6 @@ def get_op_and_param_counts(self): ret_dict[thres_param_type] = thres_count return ret_dict - def derive_characteristic_fxns(self, period): - """ - Derive characteristic functions for RTL simulation. - - Args: - period: Clock period for simulation - """ - n_inps = np.prod(self.get_folded_input_shape()[:-1]) - io_dict = { - "inputs": { - "in0": [0 for i in range(n_inps)], - }, - "outputs": {"out0": []}, - } - mem_mode = self.get_nodeattr("mem_mode") - if mem_mode in ["internal_decoupled", "external"]: - n_weight_inps = self.calc_wmem() - num_w_reps = np.prod(self.get_nodeattr("numInputVectors")) - io_dict["inputs"]["in1"] = [0 for i in range(num_w_reps * n_weight_inps)] - super().derive_characteristic_fxns(period, override_rtlsim_dict=io_dict) - def get_verilog_top_module_intf_names(self): """ Get Verilog top module interface names. diff --git a/src/finn/transformation/fpgadataflow/derive_characteristic.py b/src/finn/transformation/fpgadataflow/derive_characteristic.py deleted file mode 100644 index 0ee28ded8d..0000000000 --- a/src/finn/transformation/fpgadataflow/derive_characteristic.py +++ /dev/null @@ -1,192 +0,0 @@ -# Copyright (C) 2022, Xilinx, Inc. -# Copyright (C) 2024, Advanced Micro Devices, Inc. -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, this -# list of conditions and the following disclaimer. -# -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# -# * Neither the name of FINN nor the names of its -# contributors may be used to endorse or promote products derived from -# this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - -import qonnx.custom_op.registry as registry -from qonnx.core.modelwrapper import ModelWrapper -from qonnx.transformation.base import NodeLocalTransformation - -from finn.util.fpgadataflow import is_hls_node, is_rtl_node -from finn.util.logging import log - - -class DeriveCharacteristic(NodeLocalTransformation): - """For each node in the graph, run rtlsim to obtain the i/o - characteristic function for FIFO sizing and set the attribute. - It is assumed that the PrepareRTLSim transformation was already - called on the graph. - - This transformation performs rtlsim for each node, so it will run for - some time (minutes to hours depending on configuration). - - * period (int) desired period over which the characteristic function - will be derived. - - * num_workers (int or None) number of parallel workers, see documentation in - NodeLocalTransformation for more details. - """ - - def __init__(self, period, num_workers=None, manual_bypass=False): - super().__init__(num_workers=num_workers) - self.period = period - self.manual_bypass = manual_bypass - - def applyNodeLocal(self, node): - op_type = node.op_type - if is_hls_node(node) or is_rtl_node(node): - try: - # lookup op_type in registry of CustomOps - inst = registry.getCustomOp(node) - inst.derive_characteristic_fxns(period=self.period) - except KeyError: - # exception if op_type is not supported - raise Exception("Custom op_type %s is currently not supported." % op_type) - return (node, False) - - def apply(self, model: ModelWrapper): - (model, run_again) = super().apply(model) - if not self.manual_bypass: - return (model, run_again) - # apply manual fix for DuplicateStreams and ElementwiseAdd for - # simple residual reconvergent paths with bypass - addstrm_nodes = model.get_nodes_by_op_type("ElementwiseAdd_hls") - for addstrm_node in addstrm_nodes: - # skip ElementwiseAdd nodes that have constant inputs (not two streams) - addstrm_inst = registry.getCustomOp(addstrm_node) - if addstrm_inst.get_nodeattr("lhs_style") != "input": - continue - if addstrm_inst.get_nodeattr("rhs_style") != "input": - continue - # we currently only support the case where one branch is - # a bypass - b0 = model.find_producer(addstrm_node.input[0]) - b1 = model.find_producer(addstrm_node.input[1]) - if (b0 is None) or (b1 is None): - log.warning("Found unsupported ElementwiseAdd, skipping") - return (model, run_again) - b0_is_bypass = b0.op_type == "DuplicateStreams_hls" - b1_is_bypass = b1.op_type == "DuplicateStreams_hls" - if (not b0_is_bypass) and (not b1_is_bypass): - log.warning("Found unsupported ElementwiseAdd, skipping") - return (model, run_again) - ds_node = b0 if b0_is_bypass else b1 - comp_branch_last = b1 if b0_is_bypass else b0 - - ds_comp_bout = ds_node.output[0] if b0_is_bypass else ds_node.output[1] - comp_branch_first = model.find_consumer(ds_comp_bout) - if comp_branch_first is None or comp_branch_last is None: - log.warning("Found unsupported DuplicateStreams, skipping") - return (model, run_again) - comp_branch_last = registry.getCustomOp(comp_branch_last) - comp_branch_first = registry.getCustomOp(comp_branch_first) - # for DuplicateStreams, use comp_branch_first's input characterization - # for ElementwiseAdd, use comp_branch_last's output characterization - period = comp_branch_first.get_nodeattr("io_chrc_period") - comp_branch_first_f = comp_branch_first.get_nodeattr("io_characteristic")[: 2 * period] - comp_branch_last_f = comp_branch_last.get_nodeattr("io_characteristic")[2 * period :] - ds_node_inst = registry.getCustomOp(ds_node) - addstrm_node_inst = registry.getCustomOp(addstrm_node) - ds_node_inst.set_nodeattr("io_chrc_period", period) - ds_node_inst.set_nodeattr("io_characteristic", comp_branch_first_f * 2) - addstrm_node_inst.set_nodeattr("io_chrc_period", period) - addstrm_node_inst.set_nodeattr("io_characteristic", comp_branch_last_f * 2) - log.warning(f"Set {ds_node.name} chrc. from {comp_branch_first.onnx_node.name}") - log.warning(f"Set {addstrm_node.name} chrc. from {comp_branch_last.onnx_node.name}") - return (model, run_again) - - -class DeriveFIFOSizes(NodeLocalTransformation): - """Prerequisite: DeriveCharacteristic already called on graph. - For each node in the graph, use the accumulated I/O characteristic function - to perform FIFO sizing, setting the in/outFIFODepths attributes of HLSCustomOp - nodes. - - * num_workers (int or None) number of parallel workers, see documentation in - NodeLocalTransformation for more details. - """ - - def __init__(self, num_workers=None, io_fifo_depth=32): - super().__init__(num_workers=num_workers) - self.io_fifo_depth = io_fifo_depth - - def applyNodeLocal(self, node): - op_type = node.op_type - if is_hls_node(node) or is_rtl_node(node): - try: - # lookup op_type in registry of CustomOps - prod = registry.getCustomOp(node) - assert not (op_type.startswith("StreamingFIFO")), "Found existing FIFOs" - period = prod.get_nodeattr("io_chrc_period") - prod_chrc = prod.get_nodeattr("io_chrc_out")[0] - assert len(prod_chrc) == 2 * period, "Found unexpected characterization attribute" - if any([x > 2 for x in prod.get_nodeattr("outFIFODepths")]): - # FIFO depth already set, can skip this node - return (node, False) - - # find consumers - model = self.ref_input_model - out_fifo_depths = [] - for output_name in node.output: - cons_node = model.find_consumer(output_name) - if cons_node is None: - # could be final node, will be overridden if so - # need an entry in the list anyway - out_fifo_depths.append(self.io_fifo_depth) - continue - cons = registry.getCustomOp(cons_node) - cons_chrc = cons.get_nodeattr("io_chrc_in")[0] - # find minimum phase shift satisfying the constraint - pshift_min = period - 1 - for pshift_cand in range(period): - prod_chrc_part = prod_chrc[pshift_cand:period] - cons_chrc_part = cons_chrc[: period - pshift_cand] - if (prod_chrc_part >= cons_chrc_part).all(): - pshift_min = pshift_cand - break - prod_chrc_part = prod_chrc[pshift_min : (pshift_min + period)] - cons_chrc_part = cons_chrc[:period] - fifo_depth = int((prod_chrc_part - cons_chrc_part).max()) - out_fifo_depths.append(fifo_depth) - # set output FIFO depth for this (producing) node - # InsertFIFO looks at the max of (outFIFODepths, inFIFODepths) - # for each tensor - prod.set_nodeattr("outFIFODepths", out_fifo_depths) - - # finally, check node inputs to ensure FIFOs are added to - # any top-level inputs (at least self.io_fifo_depth deep) - in_fifo_depths = prod.get_nodeattr("inFIFODepths") - for i, input_name in enumerate(node.input): - if input_name in [x.name for x in model.graph.input]: - in_fifo_depths[i] = max(self.io_fifo_depth, in_fifo_depths[i]) - prod.set_nodeattr("inFIFODepths", in_fifo_depths) - - except KeyError: - # exception if op_type is not supported - raise Exception("Custom op_type %s is currently not supported." % op_type) - return (node, False) From 634bb54c45ad426940b390043e1cfd540e1ee7c7 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:07:24 +0200 Subject: [PATCH 093/170] Add support for subgraphs to FIFO Sizing --- finn-rtllib/mock_hbm/hdl/mock_template.v | 141 +++++ finn_xsi/finn_xsi/include/Simulation.hpp | 2 +- src/finn/benchmarking/bench.py | 2 +- src/finn/builder/build_dataflow_config.py | 10 +- src/finn/builder/build_dataflow_steps.py | 504 ++++++++++-------- .../custom_op/fpgadataflow/rtl/finn_loop.py | 45 +- src/finn/custom_op/fpgadataflow/rtlbackend.py | 2 +- .../fpgadataflow/create_stitched_ip.py | 9 +- .../fpgadataflow/loop_rolling.py | 19 +- .../fpgadataflow/simulation_build.py | 406 +++++++++----- .../fpgadataflow/simulation_connected.py | 12 +- .../qonnx/give_unique_node_names_recursive.py | 37 ++ .../qonnx/infer_quant_avg_pool_2d.py | 27 - src/finn/util/basic.py | 11 +- src/finn/util/deprecated.py | 1 - src/finn/util/logging.py | 1 - src/finn/xsi/setup.py | 8 +- tests/fpgadataflow/test_simulation_build.py | 415 ++++++++++++++ 18 files changed, 1227 insertions(+), 425 deletions(-) create mode 100644 finn-rtllib/mock_hbm/hdl/mock_template.v create mode 100644 src/finn/transformation/qonnx/give_unique_node_names_recursive.py create mode 100644 tests/fpgadataflow/test_simulation_build.py diff --git a/finn-rtllib/mock_hbm/hdl/mock_template.v b/finn-rtllib/mock_hbm/hdl/mock_template.v new file mode 100644 index 0000000000..edd4316e87 --- /dev/null +++ b/finn-rtllib/mock_hbm/hdl/mock_template.v @@ -0,0 +1,141 @@ +module $TOP_MODULE_NAME$( +//- Global Control ------------------ +(* X_INTERFACE_PARAMETER = "ASSOCIATED_RESET = ap_rst_n" *) +(* X_INTERFACE_INFO = "xilinx.com:signal:clock:1.0 ap_clk CLK" *) +input ap_clk, +(* X_INTERFACE_PARAMETER = "POLARITY ACTIVE_LOW" *) +input ap_rst_n, + +//- AXI4 Slave - Write Address ----- +input [$ADDR_WIDTH$-1:0] s_axi_awaddr, +input s_axi_awvalid, +output s_axi_awready, + +//- AXI4 Slave - Write Data -------- +input [$DATA_WIDTH$-1:0] s_axi_wdata, +input [$DATA_BYTES$-1:0] s_axi_wstrb, +input s_axi_wvalid, +input s_axi_wlast, +output s_axi_wready, + +//- AXI4 Slave - Write Response ---- +output reg [1:0] s_axi_bresp, +output reg s_axi_bvalid, +input s_axi_bready, + +//- AXI4 Slave - Read Address ------ +input [$ADDR_WIDTH$-1:0] s_axi_araddr, +input s_axi_arvalid, +output s_axi_arready, + +//- AXI4 Slave - Read Data --------- +output reg [$DATA_WIDTH$-1:0] s_axi_rdata, +output reg [1:0] s_axi_rresp, +output reg s_axi_rvalid, +output reg s_axi_rlast, +input s_axi_rready +); + +parameter integer LATENCY = 100; + +// Internal flags and counters +reg aw_received; +reg w_received; +reg ar_received; +reg [$clog2(LATENCY+1)-1:0] write_cnt; +reg [$clog2(LATENCY+1)-1:0] read_cnt; +reg busy_write; +reg busy_read; + +// Ready signals: accept new addr/data when not busy and not already pending +assign s_axi_awready = !busy_write && !aw_received; +assign s_axi_wready = !busy_write && !w_received; +assign s_axi_arready = !busy_read && !ar_received; + +// Default response values +always @(posedge ap_clk) begin + if (!ap_rst_n) begin + aw_received <= 1'b0; + w_received <= 1'b0; + ar_received <= 1'b0; + busy_write <= 1'b0; + busy_read <= 1'b0; + write_cnt <= {($clog2(LATENCY+1)){1'b0}}; + read_cnt <= {($clog2(LATENCY+1)){1'b0}}; + s_axi_bvalid <= 1'b0; + s_axi_bresp <= 2'b00; + s_axi_rvalid <= 1'b0; + s_axi_rresp <= 2'b00; + s_axi_rdata <= {${DATA_WIDTH}${1'b0}}; + s_axi_rlast <= 1'b0; + end else begin + // Capture write address + if (s_axi_awvalid && s_axi_awready) begin + aw_received <= 1'b1; + end + + // Capture write data (we only need to see WLAST to consider a complete write) + if (s_axi_wvalid && s_axi_wready) begin + if (s_axi_wlast) begin + w_received <= 1'b1; + end + end + + // Start write transaction when both address and data received + if (aw_received && w_received && !busy_write) begin + busy_write <= 1'b1; + write_cnt <= LATENCY - 1; // will count down + aw_received <= 1'b0; + w_received <= 1'b0; + end + + // Decrement write counter + if (busy_write) begin + if (write_cnt != 0) begin + write_cnt <= write_cnt - 1; + end else begin + busy_write <= 1'b0; + s_axi_bvalid <= 1'b1; // respond OKAY after latency + s_axi_bresp <= 2'b00; + end + end + + // B channel handshake + if (s_axi_bvalid && s_axi_bready) begin + s_axi_bvalid <= 1'b0; + end + + // Capture read address + if (s_axi_arvalid && s_axi_arready) begin + ar_received <= 1'b1; + end + + // Start read transaction when address captured + if (ar_received && !busy_read) begin + busy_read <= 1'b1; + read_cnt <= LATENCY - 1; + ar_received <= 1'b0; + end + + // Decrement read counter + if (busy_read) begin + if (read_cnt != 0) begin + read_cnt <= read_cnt - 1; + end else begin + busy_read <= 1'b0; + s_axi_rvalid <= 1'b1; + s_axi_rresp <= 2'b00; + s_axi_rdata <= {${DATA_WIDTH}${1'b0}}; // return zeros for reads + s_axi_rlast <= 1'b1; + end + end + + // R channel handshake + if (s_axi_rvalid && s_axi_rready) begin + s_axi_rvalid <= 1'b0; + s_axi_rlast <= 1'b0; + end + end +end + +endmodule diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 64bcc304ea..0014caa6b6 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -147,7 +147,7 @@ class SingleNodeSimulation : public Simulationfifo[i].setInputValid(this->ostreams[i].getOutputValid(), stoken); // Interface FIFO <-> SHM this->fifo[i].setOutputReady(toConsumerInterface[i].receive_request(stoken).data, stoken); - + // Toggle FIFO clock ret |= this->fifo[i].toggleClock(); bool fifoValid = this->fifo[i].getOutputValid(); diff --git a/src/finn/benchmarking/bench.py b/src/finn/benchmarking/bench.py index 499fa721cb..d22e8b6959 100644 --- a/src/finn/benchmarking/bench.py +++ b/src/finn/benchmarking/bench.py @@ -107,7 +107,7 @@ def get_default_session_options_new(): else: config_path = os.path.join("ci", "cfg", config_name + ".yml") print("Job launched with SLURM ID: %d" % (job_id)) - except KeyError as e: + except KeyError: # Launched without SLURM, assume test run on local machine job_id = 0 experiment_dir = "bench_output/" + time.strftime("%d_%H_%M") diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index 854111265e..743d1b99f4 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -56,7 +56,7 @@ from mashumaro.mixins.json import DataClassJSONMixin from mashumaro.mixins.yaml import DataClassYAMLMixin from pathlib import Path, PosixPath, PurePath -from typing import Any, List, Literal, Optional, cast +from typing import Any, Literal, Optional, cast from finn.util.basic import alveo_default_platform, part_map from finn.util.exception import FINNConfigurationError @@ -221,7 +221,7 @@ class DataflowBuildConfig(DataClassJSONMixin, DataClassYAMLMixin): """ class Config(mashumaro.config.BaseConfig): - """Config for (de)serialization of the dataflow builder class.""" # noqa + """Config for (de)serialization of the dataflow builder class.""" forbid_extra_keys = True @@ -276,7 +276,7 @@ def construct_from(cls, from_this: Path | DataflowBuildConfig) -> DataflowBuildC ) if dfbc.folding_config_file is not None and not dfbc.folding_config_file.exists(): raise FINNConfigurationError( - f"No folding config file could be found " f"at {dfbc.folding_config_file}." + f"No folding config file could be found at {dfbc.folding_config_file}." ) return dfbc @@ -591,13 +591,13 @@ def _fix_path(p: Path | None) -> Path | None: #: A List of strings that specify the PyTorch metadata hierarchy to #: be used for the loop body hierarchy. Each item in the list should #: be a string that represents a level in the hierarchy. - loop_body_hierarchy: Optional[List[List[str]]] = None + loop_body_hierarchy: Optional[list[list[str]]] = None #: A list of a start and an end node to mark the loop body subgraph #: For this node range, the PyTorch metadata hierarchy will be simulated #: TODO: this argument will be replaced or extended when there is a way #: to preserve node metadata from the PyTorch model (e.g. from dynamo exporter) - loop_body_range: Optional[List[Any]] = None + loop_body_range: Optional[list[Any]] = None #: (Only relevant if CPP_DRIVER output product is enabled) Selects C++ driver version. #: If set to "latest", newest version will be used. diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 272b20650c..04ce6ece32 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -36,15 +36,16 @@ import numpy as np import os import shutil +from collections.abc import Callable from copy import deepcopy from functools import partial +from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.bipolar_to_xnor import ConvertBipolarMatMulToXnorPopcount from qonnx.transformation.fold_constants import FoldConstants from qonnx.transformation.general import ( GiveReadableTensorNames, - GiveUniqueNodeNames, RemoveStaticGraphInputs, RemoveUnusedTensors, SortGraph, @@ -55,6 +56,7 @@ from qonnx.transformation.lower_convs_to_matmul import LowerConvsToMatMul from qonnx.util.cleanup import cleanup_model from shutil import copy, move +from typing import TYPE_CHECKING, cast import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw import finn.transformation.streamline.absorb as absorb @@ -105,6 +107,9 @@ ) from finn.transformation.fpgadataflow.set_folding import SetFolding from finn.transformation.fpgadataflow.set_loop_boundary import SetLoopBoundary +from finn.transformation.fpgadataflow.simulation import ApplyFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.fpgadataflow.synth_ooc import SynthOutOfContext from finn.transformation.fpgadataflow.transpose_decomposition import ( @@ -116,6 +121,7 @@ from finn.transformation.general import ApplyConfig from finn.transformation.move_reshape import RemoveCNVtoFCFlatten from finn.transformation.qonnx.convert_qonnx_to_finn import ConvertQONNXtoFINN +from finn.transformation.qonnx.give_unique_node_names_recursive import GiveUniqueNodeNamesRecursive from finn.transformation.qonnx.quant_act_to_multithreshold import default_filter_function_generator from finn.transformation.streamline import Streamline from finn.transformation.streamline.reorder import MakeMaxPoolNHWC @@ -127,6 +133,31 @@ from finn.util.logging import log from finn.util.mlo_sim import is_mlo, mlo_prehook_func_factory +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.rtl.finn_loop import FINNLoop + + +BuildDataflowStep = Callable[..., ModelWrapper] +build_dataflow_step_lookup: dict[str, BuildDataflowStep] = {} + + +def register_build_dataflow_step( + step_name: str | None = None, +) -> Callable[[BuildDataflowStep], BuildDataflowStep]: + """Register a dataflow build step. + + Uses the function name by default, unless step_name is explicitly provided. + """ + + def _decorator(step_fn: BuildDataflowStep) -> BuildDataflowStep: + key = step_name if step_name is not None else step_fn.__name__ + if key in build_dataflow_step_lookup: + raise ValueError(f"Duplicate build step registration: {key}") + build_dataflow_step_lookup[key] = step_fn + return step_fn + + return _decorator + def verify_step( model: ModelWrapper, @@ -149,7 +180,7 @@ def verify_step( intermediate_models_dir = cfg.output_dir + "/intermediate_models" # Ensure tensor names are sorted and readable for easier debugging model = model.transform(SortGraph()) - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) os.makedirs(verify_out_dir, exist_ok=True) (in_npy_all, exp_out_npy_all) = cfg._resolve_verification_io_pair() @@ -362,59 +393,205 @@ def prepare_for_stitched_ip_rtlsim(verify_model, cfg): return verify_model -def prepare_loop_ops_fifo_sizing(node, cfg): - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") - loop_model = loop_model.transform(GiveUniqueNodeNames(prefix=node.name + "_")) - # go first into subgraph to check if there are other loop ops - loop_nodes = loop_model.get_nodes_by_op_type("FINNLoop") - for loop_node in loop_nodes: - prepare_loop_ops_fifo_sizing(loop_node, cfg) - loop_model = loop_model.transform( - PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period()) +@register_build_dataflow_step() +def step_hw_codegen(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Generate Vitis HLS code to prepare HLSBackend nodes for IP generation. + And fills RTL templates for RTLBackend nodes.""" + model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform( + PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period()), + apply_to_subgraphs=True, + use_preorder_traversal=False, + ) + return model + + +@register_build_dataflow_step() +def step_hw_ipgen( + model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None +) -> ModelWrapper: + """Run Vitis HLS synthesis on generated code for HLSBackend nodes, + in order to generate IP blocks. For RTL nodes this step does not do anything.""" + model = model.transform(HLSSynthIP(cfg._resolve_fpga_part())) + model = model.transform(ReplaceVerilogRelPaths()) + + # Emit resource consumption reports + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) + estimate_layer_resources_hls = model.analysis(hls_synth_res_estimation) + estimate_layer_resources_hls["total"] = aggregate_dict_keys(estimate_layer_resources_hls) + filename = ( + "estimate_layer_resources_hls.json" + if parent_node is None + else f"estimate_layer_resources_hls_{parent_node}.json" ) - loop_model = loop_model.transform(HLSSynthIP(cfg._resolve_hls_clk_period())) - loop_model = loop_model.transform(ReplaceVerilogRelPaths()) + with (report_dir / filename).open("w") as f: + json.dump(estimate_layer_resources_hls, f, indent=2) + + # Optional verifification step using node by node rtl simulation + # (only supported for top level model) + if ( + VerificationStepType.NODE_BY_NODE_RTLSIM in cfg._resolve_verification_steps() + and parent_node is None + ): + if cfg.verify_save_rtlsim_waveforms: + verify_out_dir = Path(cfg.output_dir) / "verification_output" + waveform_dir = verify_out_dir / "node_by_node_rtlsim_waveforms" + waveform_dir.mkdir(parents=True, exist_ok=True) + abspath = waveform_dir.absolute() + # Set rtlsim_trace on each node BEFORE PrepareRTLSim so compilation uses debug=True + for node in model.graph.node: + node_inst = getCustomOp(node) + node_inst.set_nodeattr("rtlsim_trace", f"{abspath}/{node.name}_rtlsim.wdb") + model = model.transform(PrepareRTLSim()) + model = model.transform(SetExecMode("rtlsim")) + verify_step(model, cfg, "node_by_node_rtlsim", need_parent=True) + + return model + + +@register_build_dataflow_step() +def step_build_simulation( + model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None +) -> ModelWrapper: + """Build the simulation binaries for isolated and connected simulations.""" if cfg.fifosim_save_waveform: - report_dir = cfg.output_dir + "/report" - os.makedirs(report_dir, exist_ok=True) - loop_model.set_metadata_prop( - "rtlsim_trace", os.path.abspath(report_dir) + f"/{node.name}_fifosim_trace.wdb" + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) + tracefile = ( + f"{parent_node}_fifosim_trace.wdb" if parent_node is not None else "fifosim_trace.wdb" ) - loop_model = loop_model.transform( - InsertAndSetFIFODepths( + model.set_metadata_prop("rtlsim_trace", str(report_dir.absolute()) + tracefile) + + model = model.transform( + BuildSimulation( cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), - swg_exception=cfg.default_swg_exception, - vivado_ram_style=cfg.large_fifo_mem_style, - fifosim_input_throttle=cfg.fifosim_input_throttle, + cfg.functional_simulation, ) ) - loop_model = loop_model.transform(SplitLargeFIFOs()) - loop_model = loop_model.transform(RemoveShallowFIFOs()) - loop_model = loop_model.transform(GiveUniqueNodeNames(prefix=node.name + "_")) - loop_model = loop_model.transform(GiveReadableTensorNames()) - node_inst.set_nodeattr("body", loop_model.graph) - - -def prepare_loop_ops_ipgen(node, cfg): - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") - # go first into subgraph to check if there are other loop ops - loop_nodes = loop_model.get_nodes_by_op_type("FINNLoop") - for loop_node in loop_nodes: - prepare_loop_ops_ipgen(loop_node, cfg) - loop_model = loop_model.transform(HLSSynthIP(cfg._resolve_hls_clk_period())) - loop_model = loop_model.transform( - CreateStitchedIP( - cfg._resolve_fpga_part(), - cfg.synth_clk_period_ns, - vitis=False, - ) + return model + + +@register_build_dataflow_step() +def step_size_fifo_connected(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Simulate layers connected and use the observed behaviour to size the FIFOs accordingly.""" + model = model.transform( + RunLayerParallelSimulation(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg) ) - node_inst.set_nodeattr("body", loop_model.graph) + return model + + +@register_build_dataflow_step() +def step_apply_fifosizes(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Apply the previously found FIFO sizes to the model.""" + model = model.transform(ApplyFIFOSizes(cfg)) + model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) + model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveReadableTensorNames()) + return model + +@register_build_dataflow_step() +def step_generate_hardware( + model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None +) -> ModelWrapper: + """Generate the hardware IP of the model. This includes generating the code, IPs and sizing the + fifos for the model and all submodels.""" + model = model.transform(GiveUniqueNodeNamesRecursive()) + # Recursively call this step for all subgraphs + for node in model.get_nodes_by_op_type("FINNLoop"): + node_inst = cast("FINNLoop", getCustomOp(node)) + loop_model = cast("ModelWrapper", node_inst.get_nodeattr("body")) + loop_model.set_metadata_prop("parent_node", node.name) + loop_model.set_metadata_prop("is_mlo", "1") + # Recursion here + loop_model = step_generate_hardware(loop_model, cfg, parent_node=node.name) + # Pack subgraph with IPs and FIFOs into stitched IP + loop_model = loop_model.transform( + CreateStitchedIP( + cfg._resolve_fpga_part(), + cfg.synth_clk_period_ns, + vitis=False, + ) + ) + node_inst.set_nodeattr("body", loop_model.graph) + + # Codegen for the current model + model = step_hw_codegen(model, cfg) + + # IP Gen for the current model + model = step_hw_ipgen(model, cfg, parent_node=parent_node) + + # FIFO sizing for the current model + model = step_build_simulation(model, cfg, parent_node=parent_node) + model = step_size_fifo_connected(model, cfg) + model = step_apply_fifosizes(model, cfg) + + # Codegen for the inserted FIFOs + model = step_hw_codegen(model, cfg) + # IP Gen for the inserted FIFOs and any remaining + # IPs that needed to be re-gen after FIFO insertion + model = step_hw_ipgen(model, cfg, parent_node=parent_node) + model.save("afterfirstiteration.onnx") + + return model + +# def prepare_loop_ops_fifo_sizing(node, cfg): +# node_inst = getCustomOp(node) +# loop_model = node_inst.get_nodeattr("body") +# loop_model = loop_model.transform(GiveUniqueNodeNamesRecursive(prefix=node.name + "_")) +# # go first into subgraph to check if there are other loop ops +# loop_nodes = loop_model.get_nodes_by_op_type("FINNLoop") +# for loop_node in loop_nodes: +# prepare_loop_ops_fifo_sizing(loop_node, cfg) +# loop_model = loop_model.transform( +# PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period()) +# ) +# loop_model = loop_model.transform(HLSSynthIP(cfg._resolve_hls_clk_period())) +# loop_model = loop_model.transform(ReplaceVerilogRelPaths()) +# if cfg.fifosim_save_waveform: +# report_dir = cfg.output_dir + "/report" +# os.makedirs(report_dir, exist_ok=True) +# loop_model.set_metadata_prop( +# "rtlsim_trace", os.path.abspath(report_dir) + f"/{node.name}_fifosim_trace.wdb" +# ) +# loop_model = loop_model.transform( +# InsertAndSetFIFODepths( +# cfg._resolve_fpga_part(), +# cfg._resolve_hls_clk_period(), +# swg_exception=cfg.default_swg_exception, +# vivado_ram_style=cfg.large_fifo_mem_style, +# fifosim_input_throttle=cfg.fifosim_input_throttle, +# ) +# ) +# loop_model = loop_model.transform(SplitLargeFIFOs()) +# loop_model = loop_model.transform(RemoveShallowFIFOs()) +# loop_model = loop_model.transform(GiveUniqueNodeNamesRecursive(prefix=node.name + "_")) +# loop_model = loop_model.transform(GiveReadableTensorNames()) +# node_inst.set_nodeattr("body", loop_model.graph) + + +# def prepare_loop_ops_ipgen(node, cfg): +# node_inst = getCustomOp(node) +# loop_model = node_inst.get_nodeattr("body") +# # go first into subgraph to check if there are other loop ops +# loop_nodes = loop_model.get_nodes_by_op_type("FINNLoop") +# for loop_node in loop_nodes: +# prepare_loop_ops_ipgen(loop_node, cfg) +# loop_model = loop_model.transform(HLSSynthIP(cfg._resolve_hls_clk_period())) +# loop_model = loop_model.transform( +# CreateStitchedIP( +# cfg._resolve_fpga_part(), +# cfg.synth_clk_period_ns, +# vitis=False, +# ) +# ) +# node_inst.set_nodeattr("body", loop_model.graph) + + +@register_build_dataflow_step() def step_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig): """This step will only execute if QONNX nodes are found. These include the following op_types: "Quant" , "Trunc" and "BinaryQuant". @@ -445,13 +622,14 @@ def step_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig): return model +@register_build_dataflow_step() def step_tidy_up(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Run the tidy-up step on given model. This includes shape and datatype inference, constant folding, and giving nodes and tensors better names. """ model = model.transform(InferShapes()) model = model.transform(FoldConstants()) - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) model = model.transform(InferDataTypes()) model = model.transform(RemoveStaticGraphInputs()) @@ -462,6 +640,7 @@ def step_tidy_up(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: return model +@register_build_dataflow_step() def step_streamline(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Run streamlining on given model. Streamlining involves moving floating point scale/shift parameters around, collapsing adjacent ones into a single parameter, @@ -491,6 +670,7 @@ def step_streamline(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapp return model +@register_build_dataflow_step() def step_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Convert eligible nodes to `HWCustomOp` subclasses that represent HW layers. Which nodes and particular configurations can be converted to HW @@ -631,7 +811,7 @@ def apply_if_relevant(model, op_types, transform, desc=""): # Get rid of Transpose -> Transpose identity sequences model = model.transform(absorb.AbsorbConsecutiveTransposes()) model = model.transform(RemoveCNVtoFCFlatten()) - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(InferDataLayouts()) model = model.transform(InferDataTypes()) model = model.transform(InferShapes()) @@ -653,6 +833,7 @@ def apply_if_relevant(model, op_types, transform, desc=""): return model +@register_build_dataflow_step() def step_create_dataflow_partition(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Separate consecutive groups of HWCustomOp nodes into StreamingDataflowPartition nodes, which point to a separate ONNX file. Dataflow accelerator synthesis @@ -705,6 +886,7 @@ def step_create_dataflow_partition(model: ModelWrapper, cfg: DataflowBuildConfig return model +@register_build_dataflow_step() def step_specialize_layers(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Convert HW nodes to either an HLS or RTL variant of the node. HW nodes get converted either based on pre-determined rules (details can be found @@ -712,15 +894,16 @@ def step_specialize_layers(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mod which contains the desired setting. If the user preference cannot be fulfilled, a warning will be printed and the implementation style will be set to a default.""" if cfg.specialize_layers_config_file is not None: - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(ApplyConfig(cfg.specialize_layers_config_file)) model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(InferShapes()) model = model.transform(InferDataTypes()) return model +@register_build_dataflow_step() def step_transpose_decomposition(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Decomposes a Shuffle into a chain of InnerShuffle and OuterShuffles that can be specialised into hardware operators. @@ -740,18 +923,19 @@ def step_transpose_decomposition(model: ModelWrapper, cfg: DataflowBuildConfig) model = model.transform(SpecializeLayers(cfg._resolve_fpga_part()), apply_to_subgraphs=True) model = model.transform(InferShapes(), apply_to_subgraphs=True) model = model.transform(InferDataTypes(), apply_to_subgraphs=True) - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) loop_nodes = model.get_nodes_by_op_type("FINNLoop") for node in loop_nodes: node_inst = getCustomOp(node) loop_model = node_inst.get_nodeattr("body") - loop_model = loop_model.transform(GiveUniqueNodeNames(prefix=node.name + "_")) + loop_model = loop_model.transform(GiveUniqueNodeNamesRecursive(prefix=node.name + "_")) node_inst.set_nodeattr("body", loop_model.graph) else: log.info("Model doesn't contain any Shuffle nodes, skipping step_transpose_decomposition.") return model +@register_build_dataflow_step() def step_target_fps_parallelization(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """If target_fps was specified, use the SetFolding transformation to determine parallelization attributes. The auto-generated config will be saved under @@ -767,13 +951,7 @@ def step_target_fps_parallelization(model: ModelWrapper, cfg: DataflowBuildConfi ), apply_to_subgraphs=True, ) - model = model.transform(GiveUniqueNodeNames()) - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for node in loop_nodes: - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") - loop_model = loop_model.transform(GiveUniqueNodeNames(prefix=node.name + "_")) - node_inst.set_nodeattr("body", loop_model.graph) + model = model.transform(GiveUniqueNodeNamesRecursive()) # extract the suggested configuration and save it as json hw_attrs = [ "PE", @@ -803,16 +981,11 @@ def step_target_fps_parallelization(model: ModelWrapper, cfg: DataflowBuildConfi return model +@register_build_dataflow_step() def step_apply_folding_config(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Apply the folding configuration file onto the model to set folding (parallelization) and other attributes, if config file is specified.""" - model = model.transform(GiveUniqueNodeNames()) - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for node in loop_nodes: - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") - loop_model = loop_model.transform(GiveUniqueNodeNames(prefix=node.name + "_")) - node_inst.set_nodeattr("body", loop_model.graph) + model = model.transform(GiveUniqueNodeNamesRecursive()) if cfg.folding_config_file is not None: model = model.transform(ApplyConfig(cfg.folding_config_file), apply_to_subgraphs=True) else: @@ -821,6 +994,7 @@ def step_apply_folding_config(model: ModelWrapper, cfg: DataflowBuildConfig) -> return model +@register_build_dataflow_step() def step_generate_estimate_reports(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Generate per-layer resource and cycle estimates using analytical models.""" if DataflowOutputType.ESTIMATE_REPORTS in cfg.generate_outputs: @@ -895,6 +1069,7 @@ def step_generate_estimate_reports(model: ModelWrapper, cfg: DataflowBuildConfig return model +@register_build_dataflow_step() def step_minimize_bit_width(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Tighten the weight and accumulator bit widths for each layer.""" if cfg.minimize_bit_width: @@ -937,119 +1112,13 @@ def step_minimize_bit_width(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo return model -def step_hw_codegen(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Generate Vitis HLS code to prepare HLSBackend nodes for IP generation. - And fills RTL templates for RTLBackend nodes.""" - model = model.transform(GiveUniqueNodeNames()) - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for node in loop_nodes: - prepare_loop_ops_fifo_sizing(node, cfg) - model = model.transform( - PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period()), - apply_to_subgraphs=True, - use_preorder_traversal=False, - ) - return model - - -def step_hw_ipgen(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Run Vitis HLS synthesis on generated code for HLSBackend nodes, - in order to generate IP blocks. For RTL nodes this step does not do anything.""" - - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for node in loop_nodes: - prepare_loop_ops_ipgen(node, cfg) - model = model.transform(HLSSynthIP(cfg._resolve_fpga_part())) - model = model.transform(ReplaceVerilogRelPaths()) - report_dir = cfg.output_dir + "/report" - os.makedirs(report_dir, exist_ok=True) - estimate_layer_resources_hls = model.analysis(hls_synth_res_estimation) - estimate_layer_resources_hls["total"] = aggregate_dict_keys(estimate_layer_resources_hls) - with open(report_dir + "/estimate_layer_resources_hls.json", "w") as f: - json.dump(estimate_layer_resources_hls, f, indent=2) - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for node in loop_nodes: - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") - estimate_layer_resources_hls = loop_model.analysis(hls_synth_res_estimation) - with open(report_dir + f"/estimate_layer_resources_hls_{node.name}.json", "w") as f: - json.dump(estimate_layer_resources_hls, f, indent=2) - - if VerificationStepType.NODE_BY_NODE_RTLSIM in cfg._resolve_verification_steps(): - if cfg.verify_save_rtlsim_waveforms: - verify_out_dir = cfg.output_dir + "/verification_output" - waveform_dir = verify_out_dir + "/node_by_node_rtlsim_waveforms" - os.makedirs(waveform_dir, exist_ok=True) - abspath = os.path.abspath(waveform_dir) - # Set rtlsim_trace on each node BEFORE PrepareRTLSim so compilation uses debug=True - for node in model.graph.node: - node_inst = getCustomOp(node) - node_inst.set_nodeattr("rtlsim_trace", f"{abspath}/{node.name}_rtlsim.wdb") - model = model.transform(PrepareRTLSim()) - model = model.transform(SetExecMode("rtlsim")) - verify_step(model, cfg, "node_by_node_rtlsim", need_parent=True) - return model - - -# TODO: Both this and the step_size_... steps will be reworked before merging into dev -# TODO: These are also included in step_set_fifo_depths if the correct FIFO sizing method -# was selected -def step_build_simulation(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Build the simulation binaries for isolated and connected simulations.""" - from finn.transformation.fpgadataflow.simulation_build import BuildSimulation - - model = model.transform( - BuildSimulation( - cfg._resolve_fpga_part(), - cfg._resolve_hls_clk_period(), - cfg.functional_simulation, - ) - ) - return model - - -def step_size_fifo_isolated(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Simulate layers in isolation and use the observed behaviour to size the FIFOs accordingly.""" - from pathlib import Path - - from finn.transformation.fpgadataflow.simulation_isolated import RunLayerIsolatedSimulation - - model = model.transform( - RunLayerIsolatedSimulation( - cfg._resolve_fpga_part(), - cfg._resolve_hls_clk_period(), - cfg.functional_simulation, - Path(cfg.output_dir), - ) - ) - return model - - -def step_size_fifo_connected(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Simulate layers connected and use the observed behaviour to size the FIFOs accordingly.""" - from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation - - model = model.transform( - RunLayerParallelSimulation(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg) - ) - return model - - -def step_apply_fifosizes(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Apply the previously found FIFO sizes to the model.""" - from finn.transformation.fpgadataflow.simulation import ApplyFIFOSizes - - model = model.transform(ApplyFIFOSizes(cfg)) - model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) - return model - - def step_insert_dwc(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Insert data width converters between layers where necessary.""" model = model.transform(InsertDWC()) return model.transform(SpecializeLayers(cfg._resolve_fpga_part())) +@register_build_dataflow_step() def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Depending on the auto_fifo_depths setting, do one of the following: * if auto_fifo_depths=True: Run the appropriate auto-sizing transformation @@ -1059,7 +1128,7 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model sizes as well. Runs the `InsertFIFO` transformation, then `ApplyConfig(cfg.folding_config_file)`, and finally `RemoveShallowFIFOs`. Coherency with config file node naming is ensured by calling - `GiveUniqueNodeNames`. + `GiveUniqueNodeNamesRecursive`. """ hw_attrs = [ "PE", @@ -1092,11 +1161,11 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model # Clean up model model = model.transform(SortGraph()) - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) # save original folding config before potentially modifying it - cfg_path = cfg.output_dir + "/report/folding_config_before_lfs.json" + cfg_path = str(cfg.output_dir) + "/report/folding_config_before_lfs.json" extract_model_config_to_json(model, cfg_path, hw_attrs) model.set_metadata_prop("folding_config_before_lfs", cfg_path) @@ -1130,7 +1199,7 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model # Clean up model model = model.transform(SortGraph()) - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) # Set impl_style + ID attributes @@ -1138,9 +1207,9 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model # because the nodes will be wrapped in SDPs for node in model.get_nodes_by_op_type("StreamingFIFO_rtl"): node_inst = getCustomOp(node) - id = int(node.name.split("_")[-1]) + idf = int(node.name.split("_")[-1]) node_inst.set_nodeattr("impl_style", "virtual") - node_inst.set_nodeattr("fifo_id", id) + node_inst.set_nodeattr("fifo_id", idf) return model @@ -1148,10 +1217,10 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model strategy = cfg.auto_fifo_strategy if strategy == "largefifo_rtlsim": if cfg.fifosim_save_waveform: - report_dir = cfg.output_dir + "/report" - os.makedirs(report_dir, exist_ok=True) + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) model.set_metadata_prop( - "rtlsim_trace", os.path.abspath(report_dir) + "/fifosim_trace.wdb" + "rtlsim_trace", str(report_dir.resolve() / "fifosim_trace.wdb") ) model = model.transform( InsertAndSetFIFODepths( @@ -1163,13 +1232,7 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model cfg_n_inferences=cfg.fifosim_n_inferences, ) ) - model = model.transform(GiveUniqueNodeNames()) - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for loop_node in loop_nodes: - loop_inst = getCustomOp(loop_node) - loop_body = loop_inst.get_nodeattr("body") - loop_body = loop_body.transform(GiveUniqueNodeNames(prefix=loop_node.name + "_")) - loop_inst.set_nodeattr("body", loop_body.graph) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames(), apply_to_subgraphs=True) # InsertAndSetFIFODepths internally removes any shallow FIFOs # so no need to call RemoveShallowFIFOs here @@ -1190,7 +1253,7 @@ def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> Model # set by ApplyConfig, so create_shallow_fifos=True model = model.transform(InsertFIFO(create_shallow_fifos=True)) model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) - model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) if cfg.folding_config_file is not None: model = model.transform(ApplyConfig(cfg.folding_config_file)) @@ -1248,10 +1311,10 @@ def verify_mlo(model: ModelWrapper, cfg: DataflowBuildConfig, step: str): verify_step(model, cfg, "stitched_ip_rtlsim", need_parent=False, rtlsim_pre_hook=mlo_prehook) -def step_create_stitched_ip(model: ModelWrapper, cfg: DataflowBuildConfig): +@register_build_dataflow_step() +def step_create_stitched_ip(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Create stitched IP for a graph after all HLS IP blocks have been generated. Depends on the DataflowOutputType.STITCHED_IP output product.""" - # introduce tLAST marker, required for instrumentation if cfg.enable_instrumentation: if cfg.shell_flow_type == ShellFlowType.VITIS_ALVEO: @@ -1274,10 +1337,10 @@ def step_create_stitched_ip(model: ModelWrapper, cfg: DataflowBuildConfig): model = model.transform(PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period())) model = model.transform(HLSSynthIP()) - if DataflowOutputType.STITCHED_IP in cfg.generate_outputs: - report_dir = cfg.output_dir + "/report" - os.makedirs(report_dir, exist_ok=True) - stitched_ip_dir = cfg.output_dir + "/stitched_ip" + if DataflowOutputType.STITCHED_IP in cast("list[DataflowOutputType]", cfg.generate_outputs): + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) + stitched_ip_dir = Path(cfg.output_dir) / "stitched_ip" model = model.transform( CreateStitchedIP( cfg._resolve_fpga_part(), @@ -1288,21 +1351,23 @@ def step_create_stitched_ip(model: ModelWrapper, cfg: DataflowBuildConfig): ) # TODO copy all ip sources into output dir? as zip? shutil.copytree( - model.get_metadata_prop("vivado_stitch_proj"), stitched_ip_dir, dirs_exist_ok=True + cast("str", model.get_metadata_prop("vivado_stitch_proj")), + stitched_ip_dir, + dirs_exist_ok=True, ) log.info(f"Vivado stitched IP written into {stitched_ip_dir}") if cfg.stitched_ip_gen_dcp: copy( - model.get_metadata_prop("vivado_synth_rpt"), - report_dir + "/post_synth_resources_dcp.xml", + cast("str", model.get_metadata_prop("vivado_synth_rpt")), + report_dir / "post_synth_resources_dcp.xml", ) post_synth_resources = model.analysis(post_synth_res) - with open(report_dir + "/post_synth_resources_dcp.json", "w") as f: + with (report_dir / "post_synth_resources_dcp.json").open("w") as f: json.dump(post_synth_resources, f, indent=2) else: - log.info( + log.warning( """DataflowOutputType.STITCHED_IP not in requested outputs, skipping step_create_stitched_ip.""" ) @@ -1337,6 +1402,7 @@ def step_create_stitched_ip(model: ModelWrapper, cfg: DataflowBuildConfig): return model +@register_build_dataflow_step() def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfig): """Measure performance + latency of stitched-IP model in rtlsim (xsi). Depends on the DataflowOutputType.STITCHED_IP output product. @@ -1411,6 +1477,7 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi return model +@register_build_dataflow_step() def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): """Create a driver that can be used to interface the generated accelerator. Use DataflowBuildConfig to select PYNQ Python or C++ driver.""" @@ -1467,6 +1534,7 @@ def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): return model +@register_build_dataflow_step() def step_out_of_context_synthesis(model: ModelWrapper, cfg: DataflowBuildConfig): """Run out-of-context synthesis and generate reports. Depends on the DataflowOutputType.STITCHED_IP output product.""" @@ -1496,6 +1564,7 @@ def step_out_of_context_synthesis(model: ModelWrapper, cfg: DataflowBuildConfig) return model +@register_build_dataflow_step() def step_vivado_power_estimation(model: ModelWrapper, cfg: DataflowBuildConfig): """Run Vivado power estimation on the stitched IP after OOC synthesis.""" if DataflowOutputType.OOC_SYNTH not in cfg.generate_outputs: @@ -1513,6 +1582,7 @@ def step_vivado_power_estimation(model: ModelWrapper, cfg: DataflowBuildConfig): return model +@register_build_dataflow_step() def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig): """Synthesize a bitfile for the using the specified shell flow, using either Vivado or Vitis, to target the specified board.""" @@ -1595,6 +1665,7 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig): return model +@register_build_dataflow_step() def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig): """Create a deployment package including the driver and bitfile.""" @@ -1621,6 +1692,7 @@ def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig): return model +@register_build_dataflow_step() def step_loop_rolling(model, cfg): """Roll a repeating sequence of layers into a loop. PyTorch metadata node hierarchy is used to indicate the loop structure.""" @@ -1650,33 +1722,5 @@ def step_loop_rolling(model, cfg): return model -#: map step name strings to step functions -build_dataflow_step_lookup = { - "step_passes_frontend": step_passes_frontend, - "step_qonnx_to_finn": step_qonnx_to_finn, - "step_tidy_up": step_tidy_up, - "step_streamline": step_streamline, - "step_convert_to_hw": step_convert_to_hw, - "step_specialize_layers": step_specialize_layers, - "step_create_dataflow_partition": step_create_dataflow_partition, - "step_target_fps_parallelization": step_target_fps_parallelization, - "step_apply_folding_config": step_apply_folding_config, - "step_minimize_bit_width": step_minimize_bit_width, - "step_transpose_decomposition": step_transpose_decomposition, - "step_generate_estimate_reports": step_generate_estimate_reports, - "step_hw_codegen": step_hw_codegen, - "step_hw_ipgen": step_hw_ipgen, - "step_build_simulation": step_build_simulation, - "step_size_fifo_isolated": step_size_fifo_isolated, - "step_size_fifo_connected": step_size_fifo_connected, - "step_apply_fifosizes": step_apply_fifosizes, - "step_set_fifo_depths": step_set_fifo_depths, - "step_create_stitched_ip": step_create_stitched_ip, - "step_measure_rtlsim_performance": step_measure_rtlsim_performance, - "step_make_driver": step_make_driver, - "step_out_of_context_synthesis": step_out_of_context_synthesis, - "step_vivado_power_estimation": step_vivado_power_estimation, - "step_synthesize_bitfile": step_synthesize_bitfile, - "step_deployment_package": step_deployment_package, - "step_loop_rolling": step_loop_rolling, -} +#: register imported step functions (local steps are registered via decorators) +register_build_dataflow_step()(step_passes_frontend) diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index dfbe6ad11f..3d95800250 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -29,9 +29,11 @@ import copy import math import numpy as np +import numpy.typing as npt import os import shutil import subprocess +from onnx import GraphProto from pathlib import Path from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper @@ -48,6 +50,7 @@ from finn.util.basic import make_build_dir from finn.util.create import adjacency_list from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy +from finn.util.exception import FINNInternalError from finn.util.mlo_sim import mlo_prehook_func_factory from finn.util.settings import get_settings @@ -76,7 +79,7 @@ def collect_ip_dirs(model, ipstitch_path): return ip_dirs -class FINNLoop(HWCustomOp, RTLBackend): +class FINNLoop(RTLBackend, HWCustomOp): """Class that corresponds to the meta/container node FINN loop which is a placeholder for a group of fpgadataflow nodes that have been separated out into a FINN-ONNX model of its own and are meant to be executed in a loop.""" @@ -97,7 +100,9 @@ def get_nodeattr_types(self): my_attrs.update(RTLBackend.get_nodeattr_types(self)) return my_attrs - def get_nodeattr(self, name): + def get_nodeattr( + self, name + ) -> ModelWrapper | int | float | str | bool | npt.NDArray | list[str | int | float]: """Get a node attribute by name. Data is stored inside the ONNX node's AttributeProto container. Attribute must be part of get_nodeattr_types. Default value is returned if attribute is not set.""" @@ -126,7 +131,17 @@ def get_nodeattr(self, name): except KeyError: raise AttributeError("Op has no such attribute: " + name) - def set_nodeattr(self, name, value): + def set_nodeattr( + self, + name, + value: ModelWrapper + | GraphProto + | float + | str + | bool + | npt.NDArray + | list[str | int | float], + ): """Set a node attribute by name. Data is stored inside the ONNX node's AttributeProto container. Attribute must be part of get_nodeattr_types.""" try: @@ -136,6 +151,12 @@ def set_nodeattr(self, name, value): # dtype indicates which ONNX Attribute member to use # g : graph if dtype == "g": + if isinstance(value, ModelWrapper): + value = value.model.graph + if not isinstance(value, GraphProto): + raise FINNInternalError( + "Value for graph attribute must be a GraphProto or ModelWrapper" + ) attr.g.CopyFrom(value) else: super().set_nodeattr(name, value) @@ -708,14 +729,16 @@ def ipgen_singlenode_code(self, fpgapart=None): adj_list = adjacency_list( loop_body, - lambda node: node.op_type == "Thresholding_rtl" - or ( - node.op_type == "MVAU_rtl" - and any(attr.name == "mlo_max_iter" and attr.i > 0 for attr in node.attribute) - ) - or ( - node.op_type.startswith("Elementwise") - and any(attr.name == "mlo_max_iter" and attr.i > 0 for attr in node.attribute) + lambda node: ( + node.op_type == "Thresholding_rtl" + or ( + node.op_type == "MVAU_rtl" + and any(attr.name == "mlo_max_iter" and attr.i > 0 for attr in node.attribute) + ) + or ( + node.op_type.startswith("Elementwise") + and any(attr.name == "mlo_max_iter" and attr.i > 0 for attr in node.attribute) + ) ), ) diff --git a/src/finn/custom_op/fpgadataflow/rtlbackend.py b/src/finn/custom_op/fpgadataflow/rtlbackend.py index d8a5d210b9..b5c1c80016 100644 --- a/src/finn/custom_op/fpgadataflow/rtlbackend.py +++ b/src/finn/custom_op/fpgadataflow/rtlbackend.py @@ -43,8 +43,8 @@ from onnx import GraphProto from qonnx.core.modelwrapper import ModelWrapper -from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn import xsi +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.basic import make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy from finn.util.exception import FINNInternalError diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py index 9f4631fdf2..fc36dfdc5b 100644 --- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py +++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py @@ -457,8 +457,13 @@ def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: "cannot connect AXI interfaces." ) ip_dir_value = node_inst.get_nodeattr("ip_path") - if type(ip_dir_value) is not str or ip_dir_value == "": + if type(ip_dir_value) is not str: raise FINNInternalError(f"ip_path has the wrong type in node {node.name}.") + if ip_dir_value == "": + raise FINNInternalError( + f"ip_path is not set correctly in node {node.name}. " + "Try running PrepareIP and HLSSynthIP first." + ) if not Path(ip_dir_value).is_dir(): raise FINNInternalError( f"IP generation directory doesn't exist in node {node.name}." @@ -518,7 +523,7 @@ def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: if len(model.graph.node) <= 3: build_dir_prefix = "".join([node.name + "_" for node in model.graph.node]) vivado_stitch_proj_dir = make_build_dir(prefix=build_dir_prefix) - model.set_metadata_prop("vivado_stitch_proj", vivado_stitch_proj_dir) + model.set_metadata_prop("vivado_stitch_proj", str(vivado_stitch_proj_dir)) # start building the tcl script tcl = [] diff --git a/src/finn/transformation/fpgadataflow/loop_rolling.py b/src/finn/transformation/fpgadataflow/loop_rolling.py index cc11d03ef5..acb8f1ccfa 100644 --- a/src/finn/transformation/fpgadataflow/loop_rolling.py +++ b/src/finn/transformation/fpgadataflow/loop_rolling.py @@ -9,6 +9,7 @@ # All other copyright is held by AMD and is provided under BSD-3-Clause license. # ################################################################################### +# ruff: noqa: SLF001 import copy import numpy as np @@ -21,11 +22,14 @@ from qonnx.custom_op.registry import getCustomOp, is_custom_op from qonnx.transformation.base import Transformation from qonnx.transformation.fold_constants import FoldConstants -from typing import List, Tuple +from typing import TYPE_CHECKING, List, Tuple, cast from finn.util import onnxscript_helpers as osh from finn.util.logging import log +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.rtl.finn_loop import FINNLoop + def get_constant_from_value(value): """ @@ -362,8 +366,8 @@ def validate_loop_io_tensor_pair(tensor_a, tensor_b): tensor_a.meta["quant_parameter_tensor_names"]["finn_datatype"], tensor_b.meta["quant_parameter_tensor_names"]["finn_datatype"], ), f"""FINNLoop body activation input/output finn_datatype mismatch - {tensor_a.meta['quant_parameter_tensor_names']['finn_datatype']} != - {tensor_b.meta['quant_parameter_tensor_names']['finn_datatype']}""" + {tensor_a.meta["quant_parameter_tensor_names"]["finn_datatype"]} != + {tensor_b.meta["quant_parameter_tensor_names"]["finn_datatype"]}""" def validate_loop_io_tensors(loop_node: ir.Node): @@ -536,11 +540,13 @@ def apply(self, model: ModelWrapper) -> Tuple[ModelWrapper, bool]: # Indexable inputs will have different constant or none producers # Constant values broadcast to all nodes will have the same producer # Skip the (all) Activation inputs (have been swapped to beginning of the list) + parameter_names_inputs: set[str] = set() for index in range(activations, len(nodes[0].inputs)): inputs = [] for node in nodes: cinput = node.inputs[index] inputs.append(cinput) + parameter_names_inputs.add(inputs[0].name) if osh.same(inputs) or same_values(inputs): # Constant with Respect to Loop @@ -578,7 +584,12 @@ def apply(self, model: ModelWrapper) -> Tuple[ModelWrapper, bool]: # This must be done after serialization so we can work with protobuf nodes for loop_node in model_wrapper.get_nodes_by_op_type("FINNLoop"): - loop_body = getCustomOp(loop_node).get_nodeattr("body") + loop_body = cast( + "ModelWrapper", cast("FINNLoop", getCustomOp(loop_node)).get_nodeattr("body") + ) + loop_body.set_metadata_prop( + "mlo_input_parameter_names", str(list(parameter_names_inputs)) + ) for node in loop_body.graph.node: if not is_custom_op(node.domain): continue diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index a21102b8f0..29a3f419bc 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -1,5 +1,6 @@ """Build FINN Simulations.""" +import contextlib import finn_xsi.adapter as finnxsi import numpy as np import onnx @@ -16,11 +17,10 @@ from onnx import NodeProto, TensorProto, ValueInfoProto from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper -from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames from qonnx.transformation.infer_shapes import InferShapes -from qonnx.util.basic import get_by_name +from qonnx.util.basic import gen_finn_dt_tensor, get_by_name from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, cast @@ -30,7 +30,7 @@ from finn.transformation.fpgadataflow.insert_dwc import InsertDWC from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -from finn.util.basic import launch_process_helper, make_build_dir +from finn.util.basic import getHWCustomOp, launch_process_helper, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log @@ -38,10 +38,6 @@ from collections.abc import Sequence -# TODO: Fix that BuildSimulation has to return binaries for either SimulationType -# TODO: Just store the directory instead - since we build all targets anyways - - class SimulationType(str, Enum): """Type of simulation.""" @@ -61,6 +57,127 @@ def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: self.fpgapart = fpgapart self.clk_ns = clk_ns + def _create_existing_initializer_input( + self, + inp_name: str, + target_node: NodeProto, + ) -> tuple[TensorProto, ValueInfoProto]: + """Create tensor/valueinfo for an input that already has an initializer.""" + init_ret = self.model.get_initializer(inp_name, return_dtype=True) + info = self.model.get_tensor_valueinfo(inp_name) + if init_ret is None or info is None: + raise FINNInternalError( + f"Failed to get initializer for {inp_name} while isolating node {target_node.name}." + ) + vals, dtype = cast("tuple[np.ndarray, int]", init_ret) + init_tensor = onnx.helper.make_tensor(info.name, dtype, vals.shape, vals) + val_info = onnx.helper.make_tensor_value_info(info.name, dtype, vals.shape) + return init_tensor, val_info + + def _create_mlo_dummy_initializer_input( + self, + inp_name: str, + target_node: NodeProto, + ) -> tuple[TensorProto, ValueInfoProto]: + """Create dummy initializer tensor/valueinfo for an MLO parameter input.""" + info = self.model.get_tensor_valueinfo(inp_name) + if info is None: + raise FINNInternalError( + f"Failed to get value info for {inp_name} while isolating node {target_node.name}." + ) + + dtype = info.type.tensor_type.elem_type + if dtype == TensorProto.UNDEFINED: + dtype = TensorProto.FLOAT + tdt = self.model.get_tensor_datatype(inp_name) + tshape = self.model.get_tensor_shape(inp_name) + if tshape is None: + raise FINNInternalError( + f"Failed to get shape for {inp_name} while isolating node {target_node.name}." + ) + vals = gen_finn_dt_tensor(tdt, tuple(tshape)) + vals = np.sort(vals) + + init_tensor = onnx.helper.make_tensor(info.name, dtype, vals.shape, vals) + val_info = onnx.helper.make_tensor_value_info(info.name, dtype, vals.shape) + return init_tensor, val_info + + def _create_dynamic_input_with_dummy( + self, + inp_name: str, + input_index: int, + target_node: NodeProto, + target_op: HWCustomOp, + ) -> tuple[ValueInfoProto, ValueInfoProto, NodeProto]: + """Create graph input and dummy node for a non-initializer input.""" + info = self.model.get_tensor_valueinfo(inp_name) + if info is None: + raise FINNInternalError( + f"Failed to get value info for {inp_name} while isolating node {target_node.name}." + ) + + new_input_info = onnx.helper.make_tensor_value_info( + info.name, + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_input_shape(input_index)), + ) + new_input_dummy_info = onnx.helper.make_tensor_value_info( + info.name + "_dummy", + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_input_shape(input_index)), + ) + + dummy_node = onnx.helper.make_node( + "RemoveDataPath_rtl", + inputs=[new_input_info.name], + outputs=[new_input_dummy_info.name], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + folded_shape=target_op.get_folded_input_shape(input_index), + normal_shape=target_op.get_normal_input_shape(input_index), + dataType=target_op.get_input_datatype(input_index).name, + name=target_node.name + f"_input_dummy_{input_index}", + ) + return new_input_info, new_input_dummy_info, dummy_node + + def _create_output_with_dummy( + self, + out_name: str, + output_index: int, + target_node: NodeProto, + target_op: HWCustomOp, + ) -> tuple[ValueInfoProto, ValueInfoProto, NodeProto]: + """Create graph output and dummy node for an output tensor.""" + info = self.model.get_tensor_valueinfo(out_name) + if info is None: + raise FINNInternalError( + f"Failed to get value info for {out_name} while isolating node {target_node.name}." + ) + + new_output_info = onnx.helper.make_tensor_value_info( + info.name, + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_output_shape(output_index)), + ) + new_output_dummy_info = onnx.helper.make_tensor_value_info( + info.name + "_dummy", + TensorProto.FLOAT, + cast("Sequence[int]", target_op.get_normal_output_shape(output_index)), + ) + + dummy_node = onnx.helper.make_node( + "RemoveDataPath_rtl", + inputs=[new_output_dummy_info.name], + outputs=[new_output_info.name], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + folded_shape=target_op.get_folded_output_shape(output_index), + normal_shape=target_op.get_normal_output_shape(output_index), + dataType=target_op.get_output_datatype(output_index).name, + name=target_node.name + f"_output_dummy_{output_index}", + ) + return new_output_info, new_output_dummy_info, dummy_node + def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: """Return a modelwrapper that has only the specified node. @@ -81,10 +198,17 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ) index = by_node elif type(by_node) is str: - node_name = self.model.get_node_from_name(by_node) - if node_name is None: + node_obj = self.model.get_node_from_name(by_node) + if node_obj is None: raise FINNInternalError(f"Cannot isolate node {by_node}. No such node found.") - index = [n.name for n in self.model.graph.node].index(cast("str", node_name)) + try: + index = next( + i for i, node in enumerate(self.model.graph.node) if node.name == by_node + ) + except Exception as e: + raise FINNInternalError( + f"Cannot isolate node {by_node}. No such node found." + ) from e elif type(by_node) is NodeProto: try: index = self.model.graph.node.index(by_node) @@ -97,11 +221,31 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: f"(NodeProto)." ) - target_op = getCustomOp(self.model.graph.node[index]) - if not isinstance(target_op, HWCustomOp): - raise FINNInternalError( - f"Node {target_op.name} is not a HWCustomOp, cannot isolate for simulation." - ) + target_node = self.model.graph.node[index] + target_op = getHWCustomOp(target_node) + + is_mlo_node = False + mlo_flag = self.model.get_metadata_prop("is_mlo") + if mlo_flag is not None and mlo_flag == "1": + is_mlo_node = True + mlo_parameter_input_names = [] + if is_mlo_node: + if mlo_parameter_input_names is None: + raise FINNInternalError( + f"Node {target_node.name} is an MLO node, but no " + f"mlo_input_parameter_names metadata found in the model." + ) + mlo_param = self.model.get_metadata_prop("mlo_input_parameter_names") + mlo_parameter_input_names = literal_eval(mlo_param) if mlo_param is not None else None + if ( + mlo_parameter_input_names is None + or not isinstance(mlo_parameter_input_names, list) + or not all(isinstance(name, str) for name in mlo_parameter_input_names) + ): + raise FINNInternalError( + f"mlo_input_parameter_names metadata is not a" + f"list of strings: {mlo_parameter_input_names}" + ) initializers: list[TensorProto] = [] value_info_protos: list[ValueInfoProto] = [] @@ -111,8 +255,8 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: outputs_node: list[ValueInfoProto] = [] nodes_graph: list[NodeProto] = [] - preds_list: list | None = self.model.find_direct_predecessors(self.model.graph.node[index]) - succs_list: list | None = self.model.find_direct_successors(self.model.graph.node[index]) + preds_list: list | None = self.model.find_direct_predecessors(target_node) + succs_list: list | None = self.model.find_direct_successors(target_node) num_preds = len(preds_list) if preds_list is not None else 0 num_succs = len(succs_list) if succs_list is not None else 0 @@ -123,137 +267,99 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: # Set correct input/output count for input and output nodes, since they have no pred/succ. if num_preds == 0: inputs = self.model.graph.input - ret = get_by_name( - inputs, self.model.graph.node[index].input[0] - ) # Check that node is graph input - if ret is not None: - num_preds = 1 - input_node = True + for i in range(len(target_node.input)): + ret = get_by_name(inputs, target_node.input[i]) # Check that node is graph input + if ret is not None and ( + not is_mlo_node or target_node.input[i] not in mlo_parameter_input_names + ): + num_preds += 1 + input_node = True if num_succs == 0: outputs = self.model.graph.output - ret = get_by_name( - outputs, self.model.graph.node[index].output[0] - ) # Check that node is graph output - if ret is not None: - num_succs = 1 - output_node = True + for i in range(len(target_node.output)): + ret = get_by_name(outputs, target_node.output[i]) # Check that node is graph output + if ret is not None: + num_succs += 1 + output_node = True - num_inputs = len(self.model.graph.node[index].input) - num_outputs = len(self.model.graph.node[index].output) + num_inputs = len(target_node.input) + num_outputs = len(target_node.output) if num_outputs != num_succs: raise FINNInternalError( - f"Node {self.model.graph.node[index].name} has {num_outputs} outputs but " + f"Node {target_node.name} has {num_outputs} outputs but " f"{num_succs} successor nodes. This is not supported for isolation." ) - initializer_inputs_list = [ - self.model.graph.node[index].input[i] - for i in range(num_inputs) - if self.model.get_initializer(self.model.graph.node[index].input[i]) is not None - ] - - # Handle initializers of nodes - initializer_inputs = [] - for init in initializer_inputs_list: - ret = self.model.get_initializer(init, return_dtype=True) - info = self.model.get_tensor_valueinfo(init) - if ret is None or info is None: - raise FINNInternalError( - f"Failed to get initializer for {init} " - f"while isolating node {self.model.graph.node[index].name}." - ) - vals, dtype = cast("tuple[np.ndarray, int]", ret) - initializers.append(onnx.helper.make_tensor(info.name, dtype, vals.shape, vals)) - val_info = onnx.helper.make_tensor_value_info(info.name, dtype, vals.shape) - value_info_protos.append(val_info) - initializer_inputs.append(val_info) - + # Process each input exactly once: either keep as initializer input or isolate via dummy pred_count = 0 + converted_initializer_input_indices: list[int] = [] for i in range(num_inputs): - if self.model.graph.node[index].input[i] in initializer_inputs_list: - continue # This input is handled as an initializer, skip + inp_name = target_node.input[i] + is_mlo_parameter_input = is_mlo_node and inp_name in mlo_parameter_input_names + init_vals_only = self.model.get_initializer(inp_name) + if init_vals_only is not None or is_mlo_parameter_input: + if init_vals_only is not None: + init_tensor, val_info = self._create_existing_initializer_input( + inp_name, + target_node, + ) + else: + init_tensor, val_info = self._create_mlo_dummy_initializer_input( + inp_name, + target_node, + ) + initializers.append(init_tensor) + value_info_protos.append(val_info) + inputs_node.append(val_info) + converted_initializer_input_indices.append(i) + continue + pred_count += 1 - info = self.model.get_tensor_valueinfo(self.model.graph.node[index].input[i]) - if info is None: - raise FINNInternalError( - f"Failed to get value info for {self.model.graph.node[index].input[i]} " - f"while isolating node {self.model.graph.node[index].name}." - ) - # Setup new input tensors - new_input_info = onnx.helper.make_tensor_value_info( - info.name, - TensorProto.FLOAT, - cast("Sequence[int]", target_op.get_normal_input_shape(i)), - ) - new_input_dummy_info = onnx.helper.make_tensor_value_info( - info.name + "_dummy", - TensorProto.FLOAT, - cast("Sequence[int]", target_op.get_normal_input_shape(i)), + ( + new_input_info, + new_input_dummy_info, + dummy_node, + ) = self._create_dynamic_input_with_dummy( + inp_name, + i, + target_node, + target_op, ) - # value_info_protos.append(new_input_info) value_info_protos.append(new_input_dummy_info) inputs_graph.append(new_input_info) inputs_node.append(new_input_dummy_info) - - # Create new dummy node to remove data path for input i - dummy_node = onnx.helper.make_node( - "RemoveDataPath_rtl", - inputs=[new_input_info.name], - outputs=[new_input_dummy_info.name], - domain="finn.custom_op.fpgadataflow.rtl", - backend="fpgadataflow", - folded_shape=target_op.get_folded_input_shape(i), - normal_shape=target_op.get_normal_input_shape(i), - dataType=target_op.get_input_datatype(i).name, - name=self.model.graph.node[index].name + f"_input_dummy_{i}", - ) - nodes_graph.append(dummy_node) - inputs_node.extend(initializer_inputs) + if pred_count != num_preds: raise FINNInternalError( - f"Node {self.model.graph.node[index].name} has {num_preds} pred. nodes but only " + f"Node {target_node.name} has {num_preds} pred. nodes but " f"{pred_count} inputs have been handled." ) - for i in range(num_succs): - info = self.model.get_tensor_valueinfo(self.model.graph.node[index].output[i]) - if info is None: - raise FINNInternalError( - f"Failed to get value info for {self.model.graph.node[index].output[i]} " - f"while isolating node {self.model.graph.node[index].name}." - ) - # Setup new input tensors - new_output_info = onnx.helper.make_tensor_value_info( - info.name, - TensorProto.FLOAT, - cast("Sequence[int]", target_op.get_normal_output_shape(i)), - ) - new_output_dummy_info = onnx.helper.make_tensor_value_info( - info.name + "_dummy", - TensorProto.FLOAT, - cast("Sequence[int]", target_op.get_normal_output_shape(i)), + + # Process each output exactly once and isolate via dummy + succ_count = 0 + for i in range(num_outputs): + out_name = target_node.output[i] + new_output_info, new_output_dummy_info, dummy_node = self._create_output_with_dummy( + out_name, + i, + target_node, + target_op, ) - # value_info_protos.append(new_output_info) + succ_count += 1 value_info_protos.append(new_output_dummy_info) outputs_graph.append(new_output_info) outputs_node.append(new_output_dummy_info) + nodes_graph.append(dummy_node) - # Create new dummy node to remove data path for output i - dummy_node = onnx.helper.make_node( - "RemoveDataPath_rtl", - inputs=[new_output_dummy_info.name], - outputs=[new_output_info.name], - domain="finn.custom_op.fpgadataflow.rtl", - backend="fpgadataflow", - folded_shape=target_op.get_folded_output_shape(i), - normal_shape=target_op.get_normal_output_shape(i), - dataType=target_op.get_output_datatype(i).name, - name=self.model.graph.node[index].name + f"_output_dummy_{i}", + if succ_count != num_succs: + raise FINNInternalError( + f"Node {target_node.name} has {num_succs} succ. nodes but only " + f"{succ_count} outputs have been handled." ) - nodes_graph.append(dummy_node) - + # Copy the target node and create a new model with the target node and dummy nodes target_op_attrs = target_op.get_nodeattr_types() params = {} for attr in target_op_attrs.keys(): @@ -265,19 +371,47 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ): # Empty value, skip continue params[attr] = target_op.get_nodeattr(attr) + + params_changed = False + if len(converted_initializer_input_indices) > 0: + if target_node.op_type.startswith("Elementwise"): + if 0 in converted_initializer_input_indices: + params["lhs_style"] = "const" + params_changed = True + if 1 in converted_initializer_input_indices: + params["rhs_style"] = "const" + params_changed = True + if target_node.op_type.startswith("MVAU"): + params["mem_mode"] = "internal_decoupled" + params_changed = True + if "mlo_max_iter" in params: + del params["mlo_max_iter"] + params_changed = True + if params_changed: + params["code_gen_dir_ipgen"] = "" + params["ipgen_path"] = "" + params["ip_path"] = "" + + # Add support for hierachical models. FINN returns ModelWrapper, + # but onnx needs GraphProto for subgraphs. + # We need to convert any ModelWrapper parameters to GraphProto. + for i in params.keys(): + if isinstance(params[i], ModelWrapper): + params[i] = params[i].model.graph + new_node = onnx.helper.make_node( - self.model.graph.node[index].op_type, + target_node.op_type, inputs=[inp.name for inp in inputs_node], outputs=[outp.name for outp in outputs_node], - domain=self.model.graph.node[index].domain, - name=self.model.graph.node[index].name, + domain=target_node.domain, + name=target_node.name, **params, ) nodes_graph.append(new_node) graph = onnx.helper.make_graph( nodes_graph, - f"isolated_node_graph_{self.model.graph.node[index].name}", + f"isolated_node_graph_{target_node.name}", inputs_graph, outputs_graph, initializer=initializers, @@ -292,7 +426,7 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: node_model.set_metadata_prop("input_node", str(input_node).lower()) node_model.set_metadata_prop("output_node", str(output_node).lower()) - # node_model.save(f"isolated_node_model_{self.model.graph.node[index].name}.onnx") + node_model.save(f"isolated_node_{target_node.name}.onnx") return node_model @@ -312,14 +446,14 @@ def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, str]: first_node = model.find_consumer(iname) assert first_node is not None, "Failed to find consumer for " + iname top_ind = list(first_node.input).index(iname) - ishape_folded = getCustomOp(first_node).get_folded_input_shape(ind=top_ind) + ishape_folded = getHWCustomOp(first_node).get_folded_input_shape(ind=top_ind) instream_iters.append(int(np.prod(ishape_folded[:-1]))) for top_out in model.graph.output: oname = top_out.name last_node = model.find_producer(oname) assert last_node is not None, "Failed to find producer for " + oname top_ind = list(last_node.output).index(oname) - oshape_folded = getCustomOp(last_node).get_folded_output_shape(ind=top_ind) + oshape_folded = getHWCustomOp(last_node).get_folded_output_shape(ind=top_ind) outstream_iters.append(int(np.prod(oshape_folded[:-1]))) interface_names = model.get_metadata_prop("vivado_stitch_ifnames") @@ -618,6 +752,7 @@ def _build( nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(InferShapes()) nodemodel = nodemodel.transform(PrepareIP(self.fpgapart, self.clk_ns)) + nodemodel = nodemodel.transform(HLSSynthIP(self.fpgapart)) nodemodel = nodemodel.transform( CreateStitchedIP(self.fpgapart, self.clk_ns, functional_simulation=functional_sim) ) @@ -665,10 +800,10 @@ def _f(f: Future) -> None: synth_workers = max( 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 10) ) # 10GB per synthesis - if not functional_sim: - # When not having to do synthesis, the build is not memory bottlenecked and - # can be executed as parallel as possible - synth_workers = int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))) + # When not having to do synthesis, the build is not memory bottlenecked and + # can be executed as parallel as possible + num_workers = int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))) + synth_workers = num_workers if not functional_sim else min(synth_workers, num_workers) # Build (stitched IP, cmake, make) all sims in parallel and return paths to # the compiled executables @@ -760,7 +895,9 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: log.info("[BuildSimulation] Starting model preparation.") self._prepare_model() self.builder = SimulationBuilder(self.model, self.fpgapart, self.clk_ns) - sys.stdout = sys.stdout.console # type: ignore + with contextlib.suppress(AttributeError): + sys.stdout = sys.stdout.console # type: ignore + self.binaries = self.builder.build_simulation( with_live_display=False, functional_sim=self.functional_sim, @@ -782,7 +919,7 @@ def _compile(binary: Path) -> None: raise FINNUserError(f"Failed compilation in {binary}: {result.stderr}") # Since we dont need a rebuild, sim_binaries contains the paths to the binaries - sim_binaries = [Path(p) for p in sim_binaries] + sim_binaries = [Path(p) for p in cast("list[str]", sim_binaries)] total = len(sim_binaries) # Prepare compiling the binaries again @@ -824,7 +961,10 @@ def _prepare_model(self) -> None: self.model = self.model.transform(SpecializeLayers(self.fpgapart)) log.info("[BuildSimulation] Assigning unique and readable node and tensor names...") self.model = self.model.transform(GiveUniqueNodeNames()) + old_input_names = [i.name for i in self.model.graph.input] self.model = self.model.transform(GiveReadableTensorNames()) + for old_name, node in zip(old_input_names, self.model.graph.input, strict=True): + self.model.rename_tensor(node.name, old_name) log.info("[BuildSimulation] Preparing IPs...") self.model = self.model.transform(PrepareIP(self.fpgapart, self.clk_ns)) log.info("[BuildSimulation] Synthesizing IPs...") diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 7aca8ac069..557c39d526 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -221,9 +221,9 @@ def run( cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[sim_name] = ( - fifo_cycles_until_first_valid - ) + fifo_cycles_until_first_valid_results[ + sim_name + ] = fifo_cycles_until_first_valid timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -258,9 +258,9 @@ def run( ) = result # Only update if not already collected if sim_name not in fifo_results: - fifo_cycles_until_first_valid_results[sim_name] = ( - fifo_cycles_until_first_valid - ) + fifo_cycles_until_first_valid_results[ + sim_name + ] = fifo_cycles_until_first_valid fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles diff --git a/src/finn/transformation/qonnx/give_unique_node_names_recursive.py b/src/finn/transformation/qonnx/give_unique_node_names_recursive.py new file mode 100644 index 0000000000..caaa57a323 --- /dev/null +++ b/src/finn/transformation/qonnx/give_unique_node_names_recursive.py @@ -0,0 +1,37 @@ +"""Implementation of the GiveUniqueNodeNamesRecursive transformation, +which assigns unique names to each node in the graph and its subgraphs (e.g., loop bodies) +using enumeration with an optional prefix.""" + +from qonnx.core.modelwrapper import ModelWrapper +from qonnx.custom_op.registry import getCustomOp +from qonnx.transformation.base import Transformation +from typing import TYPE_CHECKING, Literal, cast + +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.rtl.finn_loop import FINNLoop + + +class GiveUniqueNodeNamesRecursive(Transformation): + """Give unique names to each node in the graph using enumeration, starting + with given prefix (if specified in the constructor).""" + + def __init__(self, prefix: str = "") -> None: + """Initialize the transformation with an optional prefix for node names.""" + super().__init__() + self.prefix = prefix + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: + """Apply the transformation to the given model and all of its submodels.""" + optype_count = {} + for n in model.graph.node: + if n.op_type not in optype_count.keys(): + optype_count[n.op_type] = 0 + n.name = f"{self.prefix}{n.op_type}_{optype_count[n.op_type]}" + optype_count[n.op_type] += 1 + if n.op_type == "FINNLoop": + loop_inst = cast("FINNLoop", getCustomOp(n)) + loop_body = cast("ModelWrapper", loop_inst.get_nodeattr("body")) + loop_body = loop_body.transform(GiveUniqueNodeNamesRecursive(prefix=n.name + "_")) + loop_inst.set_nodeattr("body", loop_body.graph) + # return model_was_changed = False as single iteration is always enough + return (model, False) diff --git a/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py b/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py index e7c88a7766..385f4bb3c1 100644 --- a/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py +++ b/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py @@ -137,33 +137,6 @@ def apply(self, model): ) -class AvgPoolAndTruncv1ToQuantAvgPool(Transformation): - """Convert a section of nodes of the pattern: - AveragePool -> Mul (scalar) -> Trunc (v1) - To the FINN op: Div -> QuantAvgPool2d -> Mul. - """ - - def apply(self, model): - opset_imports = model.get_opset_imports() - if "qonnx.custom_op.general" in opset_imports: - trunc_opset = opset_imports["qonnx.custom_op.general"] - elif "onnx.brevitas" in opset_imports: - trunc_opset = opset_imports["onnx.brevitas"] - else: - trunc_opset = 1 # Default to v1 if no opset found - if trunc_opset == 1: - model = model.transform(AvgPoolAndTruncv1ToQuantAvgPool()) - return model, False - elif trunc_opset == 2: - model = model.transform(AvgPoolAndTruncv2ToQuantAvgPool()) - return model, False - else: - raise NotImplementedError( - f"AvgPoolAndTruncToQuantAvgPool not implemented for " - f"Trunc opset version {trunc_opset}." - ) - - class AvgPoolAndTruncv1ToQuantAvgPool(Transformation): """ Convert a section of nodes of the pattern: diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 0a8efbfc4b..872ed4888a 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -50,13 +50,14 @@ from qonnx.util.basic import gen_finn_dt_tensor from typing import TYPE_CHECKING, cast +from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.data_packing import finnpy_to_packed_bytearray from finn.util.exception import FINNInternalError from finn.util.logging import log from finn.util.settings import get_settings if TYPE_CHECKING: - from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp + from onnx import NodeProto # test boards used for bnn pynq tests test_board_map = ["Pynq-Z1", "KV260_SOM", "ZCU104", "U55C"] @@ -112,6 +113,14 @@ part_map["V80"] = "xcv80-lsva4737-2MHP-e-s" +def getHWCustomOp(node: "NodeProto") -> "HWCustomOp": # noqa: N802 + """Get the HWCustomOp from a node. Throws an error if the node is not an HWCustomOp.""" + n = getCustomOp(node) + if not isinstance(n, HWCustomOp): + raise FINNInternalError(f"Node {node.name} is not an HWCustomOp") + return n + + def get_rtlsim_trace_depth() -> int: """Return the trace depth for rtlsim. Controllable via the RTLSIM_TRACE_DEPTH environment variable. If the env.var. is diff --git a/src/finn/util/deprecated.py b/src/finn/util/deprecated.py index 8593aa8388..27ee216007 100644 --- a/src/finn/util/deprecated.py +++ b/src/finn/util/deprecated.py @@ -1,6 +1,5 @@ """Implements a decorator to mark functions as deprecated.""" import functools -import warnings from collections.abc import Callable from typing import ParamSpec, TypeVar from finn.util.logging import log diff --git a/src/finn/util/logging.py b/src/finn/util/logging.py index 50c73ef0ea..b0266de63b 100644 --- a/src/finn/util/logging.py +++ b/src/finn/util/logging.py @@ -35,7 +35,6 @@ def set_console(console: Console) -> None: _RICH_CONSOLE = console - class LogDisabledConsole: """Use to get a console to use for Rich formatting without logging enabled.""" diff --git a/src/finn/xsi/setup.py b/src/finn/xsi/setup.py index 3aa6c91523..cef1658f63 100644 --- a/src/finn/xsi/setup.py +++ b/src/finn/xsi/setup.py @@ -147,7 +147,13 @@ def build_xsi(force: bool = False, verbose: bool = True) -> bool: include_dirs, compiler, compile_args = get_build_paths() # Source files - source_files = ["xsi_bind.cpp", "src/Port.cpp", "src/Design.cpp", "src/Kernel.cpp", "src/SharedLibrary.cpp"] + source_files = [ + "xsi_bind.cpp", + "src/Port.cpp", + "src/Design.cpp", + "src/Kernel.cpp", + "src/SharedLibrary.cpp", + ] # Build command cmd = [compiler] + compile_args diff --git a/tests/fpgadataflow/test_simulation_build.py b/tests/fpgadataflow/test_simulation_build.py new file mode 100644 index 0000000000..29cf92b174 --- /dev/null +++ b/tests/fpgadataflow/test_simulation_build.py @@ -0,0 +1,415 @@ +"""Unit tests for SimulationBuilder isolated node model generation.""" + +from __future__ import annotations + +import pytest + +import numpy as np +import os +import sys +import types +from onnx import GraphProto, NodeProto, TensorProto, ValueInfoProto, helper +from pathlib import Path +from qonnx.core.modelwrapper import ModelWrapper +from qonnx.util.basic import qonnx_make_model +from typing import Protocol + + +class _SimulationBuilderProtocol(Protocol): + def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: + ... + + def _isolated_node_model(self, by_node: int | str) -> ModelWrapper: + ... + + +def _import_simulation_build_types() -> tuple[type[_SimulationBuilderProtocol], type[Exception]]: + finn_xsi_stub_dir = Path("/tmp/finn_xsi_stub") + finn_xsi_stub_dir.mkdir(parents=True, exist_ok=True) + (finn_xsi_stub_dir / "xsi.so").touch(exist_ok=True) + os.environ.setdefault("FINN_XSI", str(finn_xsi_stub_dir)) + + finn_xsi_module = types.ModuleType("finn_xsi") + finn_xsi_module.__path__ = [] + finn_xsi_adapter_module = types.ModuleType("finn_xsi.adapter") + + def _get_simkernel_so() -> str: + return "" + + finn_xsi_adapter_module.__dict__["get_simkernel_so"] = _get_simkernel_so + finn_xsi_sim_engine_module = types.ModuleType("finn_xsi.sim_engine") + + class _SimEngine: + pass + + finn_xsi_sim_engine_module.__dict__["SimEngine"] = _SimEngine + finn_xsi_module.__dict__["adapter"] = finn_xsi_adapter_module + finn_xsi_module.__dict__["sim_engine"] = finn_xsi_sim_engine_module + sys.modules.setdefault("finn_xsi", finn_xsi_module) + sys.modules.setdefault("finn_xsi.adapter", finn_xsi_adapter_module) + sys.modules.setdefault("finn_xsi.sim_engine", finn_xsi_sim_engine_module) + + scipy_module = types.ModuleType("scipy") + scipy_special_module = types.ModuleType("scipy.special") + + def _softmax(x: np.ndarray, axis: int | None = None) -> np.ndarray: + exp_x = np.exp(x) + return exp_x / np.sum(exp_x, axis=axis, keepdims=True) + + scipy_special_module.__dict__["softmax"] = _softmax + scipy_module.__dict__["special"] = scipy_special_module + sys.modules.setdefault("scipy", scipy_module) + sys.modules.setdefault("scipy.special", scipy_special_module) + + from finn.transformation.fpgadataflow.simulation_build import SimulationBuilder + from finn.util.exception import FINNInternalError + + return SimulationBuilder, FINNInternalError + + +def _vi(name: str, shape: list[int]) -> ValueInfoProto: + return helper.make_tensor_value_info(name, TensorProto.FLOAT, shape) + + +def _make_dwc(name: str, inp: str, out: str, shape: list[int]) -> NodeProto: + return helper.make_node( + "StreamingDataWidthConverter", + [inp], + [out], + domain="finn.custom_op.fpgadataflow", + backend="fpgadataflow", + inShape=list(shape), + outShape=list(shape), + inWidth=8, + outWidth=8, + dataType="INT8", + preferred_impl_style="rtl", + name=name, + ) + + +def _make_add_hls(name: str, lhs: str, rhs: str, out: str, shape: list[int]) -> NodeProto: + return helper.make_node( + "ElementwiseAdd_hls", + [lhs, rhs], + [out], + domain="finn.custom_op.fpgadataflow.hls", + backend="fpgadataflow", + numInputVectors=[1], + lhs_shape=list(shape), + rhs_shape=list(shape), + out_shape=list(shape), + lhs_dtype="INT8", + rhs_dtype="INT8", + out_dtype="INT9", + lhs_style="input", + rhs_style="input", + PE=1, + name=name, + ) + + +def _make_mvau_rtl(name: str, inp: str, weights: str, out: str) -> NodeProto: + return helper.make_node( + "MVAU_rtl", + [inp, weights], + [out], + domain="finn.custom_op.fpgadataflow.rtl", + backend="fpgadataflow", + MW=4, + MH=4, + SIMD=1, + PE=1, + inputDataType="INT8", + weightDataType="INT8", + outputDataType="INT32", + ActVal=0, + binaryXnorMode=0, + noActivation=1, + name=name, + ) + + +def _wrap_model(graph: GraphProto) -> ModelWrapper: + return ModelWrapper(qonnx_make_model(graph, producer_name="simulation-build-test")) + + +def _build_unary_target_model(pre_binary: bool = False, succ_binary: bool = False) -> ModelWrapper: + shape = [1, 4] + nodes = [] + graph_inputs = [] + graph_outputs = [_vi("graph_out", shape)] + value_info = [_vi("target_in", shape), _vi("target_out", shape)] + + if pre_binary: + graph_inputs.extend([_vi("pre_in0", shape), _vi("pre_in1", shape)]) + nodes.append(_make_add_hls("pre_add", "pre_in0", "pre_in1", "target_in", shape)) + else: + graph_inputs.append(_vi("pre_in0", shape)) + nodes.append(_make_dwc("pre_dwc", "pre_in0", "target_in", shape)) + + nodes.append(_make_dwc("target_dwc", "target_in", "target_out", shape)) + + if succ_binary: + graph_inputs.append(_vi("succ_in1", shape)) + nodes.append(_make_add_hls("succ_add", "target_out", "succ_in1", "graph_out", shape)) + else: + nodes.append(_make_dwc("succ_dwc", "target_out", "graph_out", shape)) + + graph = helper.make_graph( + nodes=nodes, + name="unary_target_graph", + inputs=graph_inputs, + outputs=graph_outputs, + value_info=value_info, + ) + return _wrap_model(graph) + + +def _build_binary_target_model( + initializer_side: str | None = None, mlo: bool = False +) -> ModelWrapper: + shape = [1, 4] + lhs_name = "lhs_in" + rhs_name = "rhs_in" + nodes = [_make_add_hls("target_add", lhs_name, rhs_name, "target_out", shape)] + nodes.append(_make_dwc("succ_dwc", "target_out", "graph_out", shape)) + + graph_inputs = [] + if initializer_side != "lhs": + graph_inputs.append(_vi(lhs_name, shape)) + if initializer_side != "rhs": + graph_inputs.append(_vi(rhs_name, shape)) + + value_info = [_vi("target_out", shape)] + if initializer_side == "lhs": + value_info.append(_vi(lhs_name, shape)) + if initializer_side == "rhs": + value_info.append(_vi(rhs_name, shape)) + + graph = helper.make_graph( + nodes=nodes, + name="binary_target_graph", + inputs=graph_inputs, + outputs=[_vi("graph_out", shape)], + value_info=value_info, + ) + model = _wrap_model(graph) + + if initializer_side is not None: + init_name = lhs_name if initializer_side == "lhs" else rhs_name + model.set_initializer(init_name, np.ones(shape, dtype=np.float32)) + + if mlo: + model.set_metadata_prop("is_mlo", "1") + mlo_inputs = [rhs_name] if initializer_side == "rhs" else [lhs_name] + model.set_metadata_prop("mlo_input_parameter_names", str(mlo_inputs)) + + return model + + +def _build_mvau_target_model(mlo: bool = False) -> ModelWrapper: + shape_ifm = [1, 1, 1, 4] + shape_out = [1, 1, 1, 4] + shape_w = [4, 4] + + graph = helper.make_graph( + nodes=[ + _make_mvau_rtl("target_mvau", "ifm", "weights", "mvau_out"), + _make_dwc("succ_dwc", "mvau_out", "graph_out", shape_out), + ], + name="mvau_target_graph", + inputs=[_vi("ifm", shape_ifm), _vi("weights", shape_w)], + outputs=[_vi("graph_out", shape_out)], + value_info=[_vi("mvau_out", shape_out)], + ) + model = _wrap_model(graph) + + if mlo: + model.set_metadata_prop("is_mlo", "1") + model.set_metadata_prop("mlo_input_parameter_names", str(["weights"])) + + return model + + +def _assert_isolated_model( + isolated_model: ModelWrapper, + target_name: str, + expected_graph_inputs: list[str], + expected_graph_outputs: list[str], + expected_initializer_inputs: list[str], + expected_input_node_flag: bool, + expected_target_inputs: list[str], + expected_target_outputs: list[str], +) -> None: + graph = isolated_model.graph + graph_input_names = [x.name for x in graph.input] + graph_output_names = [x.name for x in graph.output] + + assert graph_input_names == expected_graph_inputs + assert graph_output_names == expected_graph_outputs + + input_dummy_nodes = [ + n for n in graph.node if n.op_type == "RemoveDataPath_rtl" and "_input_dummy_" in n.name + ] + output_dummy_nodes = [ + n for n in graph.node if n.op_type == "RemoveDataPath_rtl" and "_output_dummy_" in n.name + ] + target_nodes = [n for n in graph.node if n.name == target_name] + + assert len(target_nodes) == 1 + assert len(input_dummy_nodes) == len(expected_graph_inputs) + assert len(output_dummy_nodes) == 1 + + initializer_names = [x.name for x in graph.initializer] + assert initializer_names == expected_initializer_inputs + + target_node = target_nodes[0] + assert list(target_node.input) == expected_target_inputs + assert list(target_node.output) == expected_target_outputs + target_dummy_inputs = [inp for inp in target_node.input if inp.endswith("_dummy")] + target_initializer_inputs = [inp for inp in target_node.input if inp in initializer_names] + assert len(target_dummy_inputs) == len(expected_graph_inputs) + assert target_initializer_inputs == expected_initializer_inputs + + assert isolated_model.get_metadata_prop("predecessors") == str(expected_graph_inputs) + assert isolated_model.get_metadata_prop("successors") == str(graph_output_names) + assert isolated_model.get_metadata_prop("input_node") == str(expected_input_node_flag).lower() + assert isolated_model.get_metadata_prop("output_node") == "false" + + +def _isolate_node_model(builder: _SimulationBuilderProtocol, by_node: int | str) -> ModelWrapper: + return builder._isolated_node_model(by_node) # noqa: SLF001 + + +@pytest.mark.parametrize( + "pre_binary,succ_binary", + [ + (False, False), + (True, True), + ], +) +def test_isolated_node_model_unary_target_with_varied_other_node_inputs( + pre_binary: bool, succ_binary: bool +) -> None: + """Isolate unary target with unary/binary surrounding nodes.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_unary_target_model(pre_binary=pre_binary, succ_binary=succ_binary) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + isolated = _isolate_node_model(builder, 1) + + _assert_isolated_model( + isolated_model=isolated, + target_name="target_dwc", + expected_graph_inputs=["target_in"], + expected_graph_outputs=["target_out"], + expected_initializer_inputs=[], + expected_input_node_flag=False, + expected_target_inputs=["target_in_dummy"], + expected_target_outputs=["target_out_dummy"], + ) + + +def test_isolated_node_model_select_by_name() -> None: + """Selecting node by name returns the correct isolated model.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_unary_target_model(pre_binary=False, succ_binary=False) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + isolated = _isolate_node_model(builder, "target_dwc") + + _assert_isolated_model( + isolated_model=isolated, + target_name="target_dwc", + expected_graph_inputs=["target_in"], + expected_graph_outputs=["target_out"], + expected_initializer_inputs=[], + expected_input_node_flag=False, + expected_target_inputs=["target_in_dummy"], + expected_target_outputs=["target_out_dummy"], + ) + + +@pytest.mark.parametrize( + "initializer_side,mlo,expected_graph_inputs,expected_initializer_inputs,expected_target_inputs", + [ + (None, False, ["lhs_in", "rhs_in"], [], ["lhs_in_dummy", "rhs_in_dummy"]), + ("rhs", False, ["lhs_in"], ["rhs_in"], ["lhs_in_dummy", "rhs_in"]), + ("lhs", False, ["rhs_in"], ["lhs_in"], ["lhs_in", "rhs_in_dummy"]), + ("rhs", True, ["lhs_in"], ["rhs_in"], ["lhs_in_dummy", "rhs_in"]), + (None, True, ["rhs_in"], ["lhs_in"], ["lhs_in", "rhs_in_dummy"]), + ], +) +def test_isolated_node_model_binary_target_with_dynamic_and_fixed_inputs( + initializer_side: str | None, + mlo: bool, + expected_graph_inputs: list[str], + expected_initializer_inputs: list[str], + expected_target_inputs: list[str], +) -> None: + """Isolate binary target for dynamic/fixed lhs-rhs and MLO/non-MLO cases.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_binary_target_model(initializer_side=initializer_side, mlo=mlo) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + isolated = _isolate_node_model(builder, 0) + + _assert_isolated_model( + isolated_model=isolated, + target_name="target_add", + expected_graph_inputs=expected_graph_inputs, + expected_graph_outputs=["target_out"], + expected_initializer_inputs=expected_initializer_inputs, + expected_input_node_flag=True, + expected_target_inputs=expected_target_inputs, + expected_target_outputs=["target_out_dummy"], + ) + + +def test_isolated_node_model_elementwise_sets_const_style_for_mlo_initializer() -> None: + """Elementwise ops set lhs_style/rhs_style=const for remapped MLO initializer inputs.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_binary_target_model(initializer_side=None, mlo=True) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + isolated = _isolate_node_model(builder, 0) + target_node = next(n for n in isolated.graph.node if n.name == "target_add") + attrs = {attr.name: helper.get_attribute_value(attr) for attr in target_node.attribute} + lhs_style = ( + attrs["lhs_style"].decode() if isinstance(attrs["lhs_style"], bytes) else attrs["lhs_style"] + ) + rhs_style = ( + attrs["rhs_style"].decode() if isinstance(attrs["rhs_style"], bytes) else attrs["rhs_style"] + ) + + assert lhs_style == "const" + assert rhs_style == "input" + + +def test_isolated_node_model_mvau_sets_internal_decoupled_for_initializer_input() -> None: + """MVAU ops set mem_mode=internal_decoupled when an input is remapped to initializer.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_mvau_target_model(mlo=True) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + isolated = _isolate_node_model(builder, 0) + target_node = next(n for n in isolated.graph.node if n.name == "target_mvau") + attrs = {attr.name: helper.get_attribute_value(attr) for attr in target_node.attribute} + mem_mode = ( + attrs["mem_mode"].decode() if isinstance(attrs["mem_mode"], bytes) else attrs["mem_mode"] + ) + + assert mem_mode == "internal_decoupled" + + +def test_isolated_node_model_rejects_bad_mlo_metadata() -> None: + """Reject invalid mlo_input_parameter_names metadata values.""" + simulation_builder_cls, finn_internal_error_cls = _import_simulation_build_types() + model = _build_binary_target_model(initializer_side="rhs", mlo=False) + model.set_metadata_prop("is_mlo", "1") + model.set_metadata_prop("mlo_input_parameter_names", "42") + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + with pytest.raises(finn_internal_error_cls, match="mlo_input_parameter_names"): + _isolate_node_model(builder, 0) From 1a690c4d2b52521f1ef93f5a0c9fcf2b7dd31798 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:15:02 +0200 Subject: [PATCH 094/170] Remove core pinning completely and add slurm support --- .../fpgadataflow/simulation_build.py | 81 +++++++++++++++++-- .../fpgadataflow/simulation_connected.py | 11 +-- .../fpgadataflow/simulation_controller.py | 8 +- 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index a21102b8f0..ad885eb6f4 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -5,6 +5,7 @@ import onnx import os import psutil +import re import shlex import subprocess import sys @@ -662,13 +663,79 @@ def _f(f: Future) -> None: return _f # Build sims in parallel - synth_workers = max( - 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 10) - ) # 10GB per synthesis - if not functional_sim: - # When not having to do synthesis, the build is not memory bottlenecked and - # can be executed as parallel as possible - synth_workers = int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))) + def _try_int(value: str | None) -> int | None: + if value is None: + return None + try: + parsed = int(value) + except ValueError: + return None + return parsed if parsed > 0 else None + + def _parse_slurm_job_cpus_per_node(value: str | None) -> int | None: + if value is None: + return None + # Example values: "16", "16(x2)", "16(x2),8" + first_chunk = value.split(",")[0].strip() + match = re.match(r"^(\d+)", first_chunk) + if match is None: + return None + parsed = int(match.group(1)) + return parsed if parsed > 0 else None + + def _get_slurm_cpus() -> int | None: + cpus_per_task = _try_int(os.environ.get("SLURM_CPUS_PER_TASK")) + if cpus_per_task is not None: + return cpus_per_task + + cpus_on_node = _try_int(os.environ.get("SLURM_CPUS_ON_NODE")) + if cpus_on_node is not None: + return cpus_on_node + + return _parse_slurm_job_cpus_per_node(os.environ.get("SLURM_JOB_CPUS_PER_NODE")) + + def _get_slurm_mem_workers(cpus_alloc: int | None) -> int | None: + # SLURM memory env vars are in MB. + mem_per_node_mb = _try_int(os.environ.get("SLURM_MEM_PER_NODE")) + if mem_per_node_mb is not None: + return max(1, mem_per_node_mb // (10 * 1024)) # 10GB per synthesis + + mem_per_cpu_mb = _try_int(os.environ.get("SLURM_MEM_PER_CPU")) + if mem_per_cpu_mb is not None and cpus_alloc is not None: + return max(1, (mem_per_cpu_mb * cpus_alloc) // (10 * 1024)) + + return None + + slurm_detected = os.environ.get("SLURM_JOB_ID") is not None + if slurm_detected: + cpus_alloc = _get_slurm_cpus() + if cpus_alloc is None: + cpus_alloc = 1 + + if functional_sim: + mem_workers = _get_slurm_mem_workers(cpus_alloc) + if mem_workers is not None: + synth_workers = max(1, min(cpus_alloc, mem_workers)) + else: + synth_workers = max(1, cpus_alloc) + else: + synth_workers = max(1, cpus_alloc) + + synth_workers = min(synth_workers, len(self.model.graph.node)) + log.info( + "[BuildSimulation] SLURM job detected, using " + f"{synth_workers} workers based on allocation." + ) + else: + synth_workers = max( + 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 10) + ) # 10GB per synthesis + if not functional_sim: + # When not having to do synthesis, the build is not memory bottlenecked and + # can be executed as parallel as possible + synth_workers = int( + os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node)) + ) # Build (stitched IP, cmake, make) all sims in parallel and return paths to # the compiled executables diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 7aca8ac069..ea96ebb16f 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -177,13 +177,6 @@ def run( self._run_binary, binary, name, - i % len(os.sched_getaffinity(0)) - if len(os.sched_getaffinity(0)) < len(self.names) - else -1, # sched_getaffinity needed, because - # cpu_count does not handle well with workload schedulers. - # We only pin the core if we have more simulations than cores to avoid - # simulations moving around too much and hurting performance. If we have - # more cores than simulations, we leave it to the OS to schedule. depth[i] if depth is not None else None, is_last_node, # Only last node has no output FIFOs is_special_for_display, # First and last get special coloring @@ -319,7 +312,6 @@ def _run_binary( self, binary: Path, name: str | None, - _cpu: int | None, depth: list[int] | None = None, is_last_node: bool = False, is_special_for_display: bool = False, @@ -331,7 +323,6 @@ def _run_binary( Args: binary: Path to simulation binary name: Name of simulation node - _cpu: CPU affinity (unused) depth: List of FIFO depths for this node's output FIFOs is_last_node: True if this is the last node (no output FIFOs to configure) is_special_for_display: True if this node should get special color in logs @@ -368,7 +359,7 @@ def _print(msg: str, color: str = "green") -> None: try: # Start the simulation process with socket communication proc_idx = self._start_process( - binary, process_index, cpu=_cpu if _cpu is not None else -1 + binary, process_index ) # Send configuration commands diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index feaff91bad..ca2111a456 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -1,7 +1,6 @@ """Control (node based) simulations via unix sockets.""" import json -import os import socket import subprocess import threading @@ -64,13 +63,12 @@ def __init__( self.should_stop = False self.stop_lock = Lock() - def _start_process(self, binary: Path, process_id: int, cpu: int = -1) -> int: + def _start_process(self, binary: Path, process_id: int) -> int: """Start a single C++ simulation process with its own Unix socket. Args: binary: Path to the simulation executable process_id: Unique identifier for this process - cpu: CPU core to bind to (if -1, no binding) Returns: Index of the started process @@ -99,10 +97,8 @@ def _start_process(self, binary: Path, process_id: int, cpu: int = -1) -> int: # Start C++ process - redirect stdout/stderr to files cwd = binary.parent - # Set CPU affinity if a specific core is requested - preexec_fn = (lambda: os.sched_setaffinity(0, {cpu})) if cpu != -1 else None proc = subprocess.Popen( - cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd, preexec_fn=preexec_fn + cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd ) # Check if process started successfully From 7a1ff98411bab141e85175965800b636cb5ce364 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:21:57 +0200 Subject: [PATCH 095/170] Add it so that the synth workers respect the num_workers setting --- .../transformation/fpgadataflow/simulation_build.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 03cc2066d8..36d33566e4 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -856,14 +856,19 @@ def _get_slurm_mem_workers(cpus_alloc: int | None) -> int | None: else: synth_workers = max(1, cpus_alloc) - synth_workers = min(synth_workers, len(self.model.graph.node)) + synth_workers = min( + synth_workers, + len(self.model.graph.node), + int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))), + ) log.info( "[BuildSimulation] SLURM job detected, using " f"{synth_workers} workers based on allocation." ) else: - synth_workers = max( - 1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 10) + synth_workers = min( + max(1, cast("int", (psutil.virtual_memory().free / 1024 / 1024 / 1024) // 10)), + int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))), ) # 10GB per synthesis if not functional_sim: # When not having to do synthesis, the build is not memory bottlenecked and From d71b71e94c7d0da7061b317ff5cbfeb1a79a924e Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:32:51 +0200 Subject: [PATCH 096/170] Clean up --- .../fpgadataflow/simulation_connected.py | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 4e878921cc..f195e45b93 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -1,9 +1,7 @@ """Node connected parallel simulations.""" -import glob import json import math -import os import pandas as pd import time import traceback @@ -12,7 +10,6 @@ from enum import Enum from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper -from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from rich.console import Console from threading import Barrier @@ -23,7 +20,7 @@ from finn.transformation.fpgadataflow.set_fifo_depths import get_fifo_split_configs from finn.transformation.fpgadataflow.simulation import Simulation, SimulationType, store_fifo_data from finn.transformation.fpgadataflow.simulation_controller import SimulationController -from finn.util.basic import make_build_dir +from finn.util.basic import getHWCustomOp, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log @@ -110,20 +107,14 @@ def __init__( def _cleanup_shm_resources(self) -> None: """Remove any existing shared memory segments and semaphores from /dev/shm.""" try: - # Collect potential shared memory and semaphore names based on node names - shm_patterns = [] - # Pattern for shared memory segments (e.g., /nodename_0, /nodename_1) - shm_patterns.append("/dev/shm/*") - removed_count = 0 - for pattern in shm_patterns: - for filepath in glob.glob(pattern): - try: - Path(filepath).unlink() - removed_count += 1 - except (FileNotFoundError, PermissionError): # noqa: PERF203 - # File might already be removed or we don't have permission - pass + for filepath in Path("/dev/shm").glob("*"): + try: + filepath.unlink() + removed_count += 1 + except (FileNotFoundError, PermissionError): # noqa: PERF203 + # File might already be removed or we don't have permission + pass if removed_count > 0: log.info(f"Cleaned up {removed_count} existing shared memory resources") @@ -192,7 +183,8 @@ def run( all_futures = list(futures) # Keep track of all futures while futures: - done, futures = wait(futures, return_when=FIRST_COMPLETED) + done, futures_s = wait(futures, return_when=FIRST_COMPLETED) + futures = list(futures_s) # Remaining futures that are still running # Check if any completed task indicates we should stop for future in done: @@ -214,9 +206,9 @@ def run( cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[ - sim_name - ] = fifo_cycles_until_first_valid + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -251,9 +243,9 @@ def run( ) = result # Only update if not already collected if sim_name not in fifo_results: - fifo_cycles_until_first_valid_results[ - sim_name - ] = fifo_cycles_until_first_valid + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles @@ -358,9 +350,7 @@ def _print(msg: str, color: str = "green") -> None: try: # Start the simulation process with socket communication - proc_idx = self._start_process( - binary, process_index - ) + proc_idx = self._start_process(binary, process_index) # Send configuration commands # Last node has no output FIFOs, so don't configure FIFO depths @@ -511,6 +501,8 @@ def _print(msg: str, color: str = "green") -> None: class NodeConnectedSimulation(Simulation): + """Run node-connected simulations for all layers in parallel.""" + def __init__( self, model: ModelWrapper, @@ -521,6 +513,7 @@ def __init__( workers: int | None = None, max_qsrl_depth: int = 256, ) -> None: + """Initialize node-connected simulation.""" super().__init__(model, simulation_type, fpgapart, clk_ns, functional_sim, workers) self.max_qsrl_depth = max_qsrl_depth @@ -670,7 +663,7 @@ def get_minimization_order_indices( ): diffs: list[tuple[int, int]] = [] # (index, diff) for i in range(len(model.graph.node)): - hw: HWCustomOp = getCustomOp(model.graph.node[i]) + hw: HWCustomOp = getHWCustomOp(model.graph.node[i]) in_width = max( [hw.get_instream_width(j) for j in range(len(model.graph.node[i].input))] ) @@ -757,7 +750,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: bit_widths = [] for node_idx in range(len(fifo_depths)): bit_widths.append([]) - hw_node = getCustomOp(model.graph.node[node_idx]) + hw_node = getHWCustomOp(model.graph.node[node_idx]) if isinstance(hw_node, HWCustomOp): for fifo_idx in range(len(fifo_depths[node_idx])): bit_widths[node_idx].append(hw_node.get_outstream_width(fifo_idx)) From 2a7cb13adc5e0fe544574e56816e1015ec96cfef Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:37:26 +0200 Subject: [PATCH 097/170] Fix linting --- .../fpgadataflow/simulation_connected.py | 12 ++++++------ .../fpgadataflow/simulation_controller.py | 4 +--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index f195e45b93..0e3973391e 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -206,9 +206,9 @@ def run( cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[sim_name] = ( - fifo_cycles_until_first_valid - ) + fifo_cycles_until_first_valid_results[ + sim_name + ] = fifo_cycles_until_first_valid timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -243,9 +243,9 @@ def run( ) = result # Only update if not already collected if sim_name not in fifo_results: - fifo_cycles_until_first_valid_results[sim_name] = ( - fifo_cycles_until_first_valid - ) + fifo_cycles_until_first_valid_results[ + sim_name + ] = fifo_cycles_until_first_valid fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py index ca2111a456..b06b56e3b1 100644 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ b/src/finn/transformation/fpgadataflow/simulation_controller.py @@ -97,9 +97,7 @@ def _start_process(self, binary: Path, process_id: int) -> int: # Start C++ process - redirect stdout/stderr to files cwd = binary.parent - proc = subprocess.Popen( - cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd - ) + proc = subprocess.Popen(cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd) # Check if process started successfully time.sleep(0.2) # Give process time to fail if there's an immediate error From 7d05e46ea418ac02b054fb2e1ba69bf300f2acf5 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 28 Apr 2026 17:15:53 +0200 Subject: [PATCH 098/170] Autofix all possible ruff warnings --- .../fpgadataflow/exp_cycles_per_layer.py | 12 +- .../analysis/fpgadataflow/floorplan_params.py | 14 +- .../fpgadataflow/hls_synth_res_estimation.py | 19 ++- .../fpgadataflow/op_and_param_counts.py | 1 - .../analysis/fpgadataflow/post_synth_res.py | 4 +- .../analysis/fpgadataflow/res_estimation.py | 2 - .../fpgadataflow/unsupported_layers.py | 19 ++- src/finn/analysis/verify_custom_nodes.py | 9 +- src/finn/benchmarking/bench.py | 16 +- src/finn/benchmarking/bench_base.py | 38 ++--- src/finn/benchmarking/dut/mvau.py | 16 +- .../benchmarking/dut/synthetic_nonlinear.py | 24 +-- src/finn/benchmarking/util.py | 4 +- src/finn/builder/build_dataflow_steps.py | 5 - .../builder/custom_step_library/resnet.py | 4 +- src/finn/builder/passes.py | 4 +- src/finn/core/onnx_exec.py | 17 +- src/finn/core/rtlsim_exec.py | 8 +- src/finn/core/throughput_test.py | 1 - .../custom_op/fpgadataflow/attention_heads.py | 13 +- .../fpgadataflow/convolutioninputgenerator.py | 7 +- src/finn/custom_op/fpgadataflow/crop.py | 2 +- .../fpgadataflow/elementwise_binary.py | 54 +++--- src/finn/custom_op/fpgadataflow/fmpadding.py | 2 +- .../custom_op/fpgadataflow/fmpadding_pixel.py | 2 +- .../fpgadataflow/hls/attention_heads_hls.py | 24 +-- .../fpgadataflow/hls/attention_hls.py | 8 +- .../fpgadataflow/hls/checksum_hls.py | 23 ++- .../custom_op/fpgadataflow/hls/concat_hls.py | 2 +- .../fpgadataflow/hls/duplicatestreams_hls.py | 2 +- .../hls/elementwise_binary_hls.py | 4 +- .../fpgadataflow/hls/fmpadding_pixel_hls.py | 6 +- .../fpgadataflow/hls/globalaccpool_hls.py | 8 +- .../custom_op/fpgadataflow/hls/iodma_hls.py | 54 +++--- .../custom_op/fpgadataflow/hls/lookup_hls.py | 9 +- .../hls/matrixvectoractivation_hls.py | 63 +++---- .../fpgadataflow/hls/outer_shuffle_hls.py | 8 +- .../custom_op/fpgadataflow/hls/pool_hls.py | 13 +- .../fpgadataflow/hls/replicate_stream_hls.py | 8 +- .../custom_op/fpgadataflow/hls/requant_hls.py | 2 +- .../custom_op/fpgadataflow/hls/split_hls.py | 2 +- .../custom_op/fpgadataflow/hls/squeeze_hls.py | 2 +- .../hls/streamingdatawidthconverter_hls.py | 8 +- .../fpgadataflow/hls/streamingfifo_hls.py | 24 +-- .../fpgadataflow/hls/unsqueeze_hls.py | 2 +- .../fpgadataflow/hls/upsampler_hls.py | 13 +- .../hls/vectorvectoractivation_hls.py | 59 +++---- src/finn/custom_op/fpgadataflow/hlsbackend.py | 62 +++---- .../custom_op/fpgadataflow/inner_shuffle.py | 2 +- src/finn/custom_op/fpgadataflow/layernorm.py | 3 +- src/finn/custom_op/fpgadataflow/lookup.py | 10 +- .../fpgadataflow/matrixvectoractivation.py | 38 ++--- .../custom_op/fpgadataflow/outer_shuffle.py | 7 +- src/finn/custom_op/fpgadataflow/pool.py | 5 +- .../fpgadataflow/replicate_stream.py | 5 +- src/finn/custom_op/fpgadataflow/requant.py | 13 +- src/finn/custom_op/fpgadataflow/reshape.py | 2 +- .../rtl/convolutioninputgenerator_rtl.py | 42 ++--- .../rtl/elementwise_binary_rtl.py | 8 +- .../custom_op/fpgadataflow/rtl/finn_loop.py | 52 +++--- .../fpgadataflow/rtl/fmpadding_rtl.py | 2 +- .../fpgadataflow/rtl/inner_shuffle_rtl.py | 10 +- .../fpgadataflow/rtl/layernorm_rtl.py | 2 +- .../rtl/matrixvectoractivation_rtl.py | 28 ++-- .../custom_op/fpgadataflow/rtl/requant_rtl.py | 11 +- .../custom_op/fpgadataflow/rtl/reshape_rtl.py | 3 +- .../rtl/streamingdatawidthconverter_rtl.py | 2 +- .../fpgadataflow/rtl/streamingfifo_rtl.py | 19 +-- .../fpgadataflow/rtl/thresholding_rtl.py | 18 +- .../rtl/vectorvectoractivation_rtl.py | 28 ++-- src/finn/custom_op/fpgadataflow/rtlbackend.py | 2 +- src/finn/custom_op/fpgadataflow/shuffle.py | 5 +- .../streamingdataflowpartition.py | 7 +- .../custom_op/fpgadataflow/streamingfifo.py | 15 +- .../custom_op/fpgadataflow/thresholding.py | 1 - .../fpgadataflow/vectorvectoractivation.py | 92 ++++------ src/finn/interface/manage_deps.py | 10 +- src/finn/interface/run_finn.py | 10 +- src/finn/templates/python_driver/driver.py | 34 ++-- .../templates/validate/imagenet/validate.py | 11 +- .../templates/validate/radioml/validate.py | 6 +- .../templates/validate/unswnb15/validate.py | 2 +- .../fpgadataflow/attention_heads.py | 4 +- .../fpgadataflow/convert_to_hw_layers.py | 40 ++--- .../fpgadataflow/create_dataflow_partition.py | 17 +- .../fpgadataflow/externalize_params.py | 23 ++- .../transformation/fpgadataflow/floorplan.py | 7 +- .../fpgadataflow/hlssynth_ip.py | 2 +- .../infer_pixel_padding_deconv.py | 5 +- .../transformation/fpgadataflow/insert_dwc.py | 9 +- .../fpgadataflow/insert_fifo.py | 8 +- .../fpgadataflow/insert_hook.py | 8 +- .../fpgadataflow/insert_iodma.py | 158 +++++++++--------- .../fpgadataflow/instrumentation.py | 4 +- .../fpgadataflow/loop_rolling.py | 24 ++- .../fpgadataflow/make_driver.py | 36 ++-- .../fpgadataflow/make_zynq_proj.py | 4 +- .../fpgadataflow/prepare_cppsim.py | 1 - .../transformation/fpgadataflow/prepare_ip.py | 1 - .../fpgadataflow/raise_scalar_to_rank1.py | 2 +- .../fpgadataflow/replace_verilog_relpaths.py | 2 +- .../fpgadataflow/set_loop_boundary.py | 3 +- .../fpgadataflow/simulation_isolated.py | 4 +- .../fpgadataflow/specialize_layers.py | 146 +++++++--------- .../fpgadataflow/transpose_decomposition.py | 77 ++++----- .../fpgadataflow/vitis_build.py | 12 +- .../fpgadataflow/vivado_power_estimation.py | 2 +- .../qonnx/fold_quant_weights.py | 7 +- .../qonnx/infer_quant_avg_pool_2d.py | 14 +- .../qonnx/qonnx_activation_handlers.py | 112 ++++++------- .../qonnx/quant_act_to_multithreshold.py | 3 +- src/finn/transformation/squeeze.py | 9 +- src/finn/transformation/streamline/absorb.py | 2 +- src/finn/transformation/streamline/remove.py | 2 +- src/finn/transformation/streamline/reorder.py | 23 +-- src/finn/transformation/util.py | 3 +- src/finn/util/config.py | 4 +- src/finn/util/create.py | 1 - src/finn/util/data_packing.py | 27 +-- src/finn/util/execution.py | 9 +- src/finn/util/mlo_sim.py | 5 +- src/finn/util/onnxscript_helpers.py | 63 +++---- src/finn/util/platforms.py | 20 +-- src/finn/xsi/__init__.py | 11 +- 124 files changed, 891 insertions(+), 1231 deletions(-) diff --git a/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py b/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py index 50585720fe..52d4e43a5f 100644 --- a/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py +++ b/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py @@ -27,23 +27,25 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import qonnx.custom_op.registry as registry - from finn.util.fpgadataflow import is_hls_node, is_rtl_node +from finn.util.basic import getHWCustomOp +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from qonnx.core.modelwrapper import ModelWrapper -def exp_cycles_per_layer(model): +def exp_cycles_per_layer(model:"ModelWrapper") -> dict[str, int]: """Estimates the number of cycles per sample for dataflow layers in the given model. Ensure that all nodes have unique names (by calling the GiveUniqueNodeNames transformation) prior to calling this analysis pass to ensure all nodes are visible in the results. Returns {node name : cycle estimation}.""" - cycle_dict = {} for node in model.graph.node: if is_hls_node(node) or is_rtl_node(node): - inst = registry.getCustomOp(node) + inst = getHWCustomOp(node) cycle_dict[node.name] = int(inst.get_exp_cycles()) return cycle_dict diff --git a/src/finn/analysis/fpgadataflow/floorplan_params.py b/src/finn/analysis/fpgadataflow/floorplan_params.py index be03966fb9..0f814a60bf 100644 --- a/src/finn/analysis/fpgadataflow/floorplan_params.py +++ b/src/finn/analysis/fpgadataflow/floorplan_params.py @@ -28,15 +28,17 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from qonnx.custom_op.registry import getCustomOp - from finn.util.fpgadataflow import is_fpgadataflow_node +from typing import TYPE_CHECKING, cast + +if TYPE_CHECKING: + from qonnx.core.modelwrapper import ModelWrapper -def floorplan_params(model): +def floorplan_params(model:"ModelWrapper"): """Gathers SLR and partition IDs from nodes. Returns {node name : {slr, device id, partition id, memory port}}.""" - ret_dict = { "Defaults": { "slr": [-1, ["all"]], @@ -48,9 +50,9 @@ def floorplan_params(model): for node in model.graph.node: if is_fpgadataflow_node(node): node_inst = getCustomOp(node) - node_slr = node_inst.get_nodeattr("slr") - node_pid = node_inst.get_nodeattr("partition_id") - node_mport = node_inst.get_nodeattr("mem_port") + node_slr = cast("int", node_inst.get_nodeattr("slr")) + node_pid = cast("int", node_inst.get_nodeattr("partition_id")) + node_mport = cast("str", node_inst.get_nodeattr("mem_port")) ret_dict[node.name] = { "slr": node_slr, "partition_id": node_pid, diff --git a/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py b/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py index a3bf713cc1..6bae3f66bf 100644 --- a/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py +++ b/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py @@ -25,28 +25,31 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os +from pathlib import Path import qonnx.custom_op.registry as registry import xml.etree.ElementTree as ET from finn.util.fpgadataflow import is_hls_node from finn.util.logging import log +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from qonnx.core.modelwrapper import ModelWrapper -def hls_synth_res_estimation(model): - """Extracts the FPGA resource results from the Vitis HLS synthesis estimates. + +def hls_synth_res_estimation(model:"ModelWrapper") -> dict[str, dict[str, int]]: + """Extract the FPGA resource results from the Vitis HLS synthesis estimates. Note that this analysis pass only works on nodes that have an HLS backend. Ensure that all nodes have unique names (by calling the GiveUniqueNodeNames transformation) prior to calling this analysis pass to ensure all nodes are visible in the results. Returns {node name : resources_dict}.""" - res_dict = {} for node in model.graph.node: if is_hls_node(node): # init values to zero - res_dict[node.name] = dict() + res_dict[node.name] = {} res_dict[node.name]["BRAM_18K"] = 0 res_dict[node.name]["FF"] = 0 res_dict[node.name]["LUT"] = 0 @@ -61,11 +64,11 @@ def hls_synth_res_estimation(model): "HLSSynthIP" first to generate the report files""" ) else: - xmlfile = "{}/project_{}/sol1/syn/report/{}_csynth.xml".format( - code_gen_dir, node.name, node.name + xmlfile = ( + f"{code_gen_dir}/project_{node.name}/sol1/syn/report/{node.name}_csynth.xml" ) - if os.path.isfile(xmlfile): + if Path(xmlfile).is_file(): tree = ET.parse(xmlfile) root = tree.getroot() for item in root.findall("AreaEstimates/Resources"): diff --git a/src/finn/analysis/fpgadataflow/op_and_param_counts.py b/src/finn/analysis/fpgadataflow/op_and_param_counts.py index bee98c46cf..885eb8994f 100644 --- a/src/finn/analysis/fpgadataflow/op_and_param_counts.py +++ b/src/finn/analysis/fpgadataflow/op_and_param_counts.py @@ -47,7 +47,6 @@ def aggregate_dict_keys(res_dict): def op_and_param_counts(model): """Return per-node and aggregate op counts per inference.""" - ret_dict = {} for node in model.graph.node: if registry.is_custom_op(node.domain): diff --git a/src/finn/analysis/fpgadataflow/post_synth_res.py b/src/finn/analysis/fpgadataflow/post_synth_res.py index f7a3e6e2ba..4874de1503 100644 --- a/src/finn/analysis/fpgadataflow/post_synth_res.py +++ b/src/finn/analysis/fpgadataflow/post_synth_res.py @@ -42,7 +42,6 @@ def post_synth_res(model, override_synth_report_filename=None): visible in the results. Returns {node name : resources_dict}.""" - res_dict = {} if override_synth_report_filename is not None: synth_report_filename = override_synth_report_filename @@ -112,8 +111,7 @@ def get_instance_stats(inst_name): for restype, ind in restype_to_ind.items(): node_dict[restype] = int(row[ind].attrib["contents"]) return node_dict - else: - return None + return None # global (top-level) stats, including shell etc. top_dict = get_instance_stats("(top)") diff --git a/src/finn/analysis/fpgadataflow/res_estimation.py b/src/finn/analysis/fpgadataflow/res_estimation.py index fb12eed837..b0ad9f6f1f 100644 --- a/src/finn/analysis/fpgadataflow/res_estimation.py +++ b/src/finn/analysis/fpgadataflow/res_estimation.py @@ -38,7 +38,6 @@ def res_estimation(model, fpgapart): visible in the results. Returns {node name : resource estimation}.""" - res_dict = {} for node in model.graph.node: if is_hls_node(node) or is_rtl_node(node): @@ -56,7 +55,6 @@ def res_estimation_complete(model, fpgapart): visible in the results. Returns {node name : [resource estimation(s)]}.""" - res_dict = {} for node in model.graph.node: if is_hls_node(node) or is_rtl_node(node): diff --git a/src/finn/analysis/fpgadataflow/unsupported_layers.py b/src/finn/analysis/fpgadataflow/unsupported_layers.py index e2f9f296ac..09d4a6ce26 100644 --- a/src/finn/analysis/fpgadataflow/unsupported_layers.py +++ b/src/finn/analysis/fpgadataflow/unsupported_layers.py @@ -7,21 +7,26 @@ from collections import deque from qonnx.core.modelwrapper import ModelWrapper +from typing import TYPE_CHECKING, Literal +if TYPE_CHECKING: + from onnx import NodeProto -def unsupported_layers(model: ModelWrapper): - """ - Check if all sink nodes are only reachable by paths with at most one + +def unsupported_layers( + model: ModelWrapper, +) -> tuple[Literal[False], "NodeProto"] | tuple[Literal[True], None]: + """Check if all sink nodes are only reachable by paths with at most one connected section of nodes which are supported by the FPGA. """ - def is_supported_node(node): + def is_supported_node(node: "NodeProto") -> bool: """Check if a node is supported by (= mapped to) the FPGA backend.""" return node.domain.startswith("finn.custom_op.fpgadataflow") # Find source and sink nodes in the model - source_nodes = [] - sink_nodes = [] + source_nodes: list[NodeProto] = [] + sink_nodes: list[NodeProto | None] = [] inputs = model.graph.input for inp in inputs: @@ -37,7 +42,7 @@ def is_supported_node(node): sink_nodes.append(n) # BFS to check paths - queue = deque() + queue : deque[tuple[NodeProto, bool, bool]] = deque() # Track (node_id, in_green_section, has_seen_complete_green_section) visited = [] diff --git a/src/finn/analysis/verify_custom_nodes.py b/src/finn/analysis/verify_custom_nodes.py index f7b65a0abf..ce4bef1cc6 100644 --- a/src/finn/analysis/verify_custom_nodes.py +++ b/src/finn/analysis/verify_custom_nodes.py @@ -27,17 +27,20 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import qonnx.custom_op.registry as registry +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from qonnx.core.modelwrapper import ModelWrapper -def verify_nodes(model): - """Checks if custom ops in graph are correctly built, with all attributes + +def verify_nodes(model: "ModelWrapper") -> dict[str, list[str]]: + """Check if custom ops in graph are correctly built, with all attributes and inputs. Please note that many FINN CustomOps don't yet implement the verify_node function required for this analysis pass to work correctly. Returns {node op_type : info_messages} * info_messages: is list of strings about the result of the verification.""" - verification_dict = {} for node in model.graph.node: if registry.is_custom_op(node.domain): diff --git a/src/finn/benchmarking/bench.py b/src/finn/benchmarking/bench.py index d22e8b6959..ac7c8482eb 100644 --- a/src/finn/benchmarking/bench.py +++ b/src/finn/benchmarking/bench.py @@ -1,5 +1,4 @@ -""" -FINN benchmarking execution framework. +"""FINN benchmarking execution framework. This module provides the main entry point for running FINN benchmarks, supporting both SLURM-based cluster execution and local testing. It handles configuration @@ -28,7 +27,7 @@ dut["synthetic_nonlinear"] = bench_synthetic_nonlinear -class PrefixPrinter(object): +class PrefixPrinter: """Custom stream handler that adds a prefix to console output for run identification.""" def __init__(self, prefix, originalstream): @@ -48,8 +47,7 @@ def flush(self): def start_bench_run(config_name): - """ - Start a benchmarking run with the specified configuration. + """Start a benchmarking run with the specified configuration. This function handles both SLURM cluster execution and local testing, loading configuration files, expanding parameter combinations, and @@ -148,11 +146,11 @@ def get_default_session_options_new(): # Load config print("Loading config %s" % (config_path)) if os.path.exists(config_path): - with open(config_path, "r") as f: + with open(config_path) as f: config = yaml.load(f, Loader=yaml.SafeLoader) else: print("ERROR: config file not found") - return + return None # Expand all specified config combinations (gridsearch) config_expanded = [] @@ -175,7 +173,7 @@ def get_default_session_options_new(): if task_id < total_runs: selected_runs = [task_id] else: - return + return None else: selected_runs = [] idx = task_id @@ -251,7 +249,7 @@ def get_default_session_options_new(): # we could also fail the pipeline if functional verification fails (TODO) builder_log_path = os.path.join(bench_object.report_dir, "metadata_builder.json") if os.path.isfile(builder_log_path): - with open(builder_log_path, "r") as f: + with open(builder_log_path) as f: builder_log = json.load(f) if builder_log["status"] == "failed": print("BENCH RUN %d FAILED (BUILDER REPORTED FAILURE)" % run_id) diff --git a/src/finn/benchmarking/bench_base.py b/src/finn/benchmarking/bench_base.py index 3d0edccd92..320b4b883d 100644 --- a/src/finn/benchmarking/bench_base.py +++ b/src/finn/benchmarking/bench_base.py @@ -1,5 +1,4 @@ -""" -Base class for FINN benchmarking framework. +"""Base class for FINN benchmarking framework. This module provides the foundational `bench` class for running automated benchmarks of FINN dataflow builds. It wraps the existing FINN builder and handles configuration @@ -26,8 +25,7 @@ class bench: - """ - Base class for FINN benchmarking operations. + """Base class for FINN benchmarking operations. This class provides the foundational framework for running automated benchmarks of FINN dataflow builds. It manages the complete lifecycle from configuration @@ -41,8 +39,7 @@ class bench: """ def __init__(self, params, task_id, run_id, work_dir, artifacts_dir, save_dir, debug=True): - """ - Initialize a new benchmark instance that manages a single FINN build. + """Initialize a new benchmark instance that manages a single FINN build. Args: params (dict): Parameters for the FINN builder and the bench instance itself @@ -144,7 +141,7 @@ def __init__(self, params, task_id, run_id, work_dir, artifacts_dir, save_dir, d dut_yaml_name = self._params["dut"] + ".yml" dut_path = os.path.join(os.path.dirname(__file__), "dut", dut_yaml_name) if os.path.isfile(dut_path): - with open(dut_path, "r") as f: + with open(dut_path) as f: dut_cfg = yaml.load(f, Loader=yaml.SafeLoader) for key in dut_cfg: if key in custom_params: @@ -197,8 +194,7 @@ def __init__(self, params, task_id, run_id, work_dir, artifacts_dir, save_dir, d self._artifacts_collection.append(("deploy", os.path.join(self._build_dir, "deploy"), True)) def _save_artifact(self, target_path, source_path, archive=False): - """ - Save a single artifact from source to target location. + """Save a single artifact from source to target location. Args: target_path (str): Destination path where artifact will be saved @@ -223,8 +219,7 @@ def _save_artifact(self, target_path, source_path, archive=False): shcopy(source_path, target_path) def save_artifacts_collection(self): - """ - Save all collected pipeline artifacts. + """Save all collected pipeline artifacts. This method should be called upon successful or failed completion of a run. It processes all artifacts in the artifacts_collection list and saves them @@ -241,8 +236,7 @@ def save_artifacts_collection(self): self._save_artifact(target_path, source_path, archive) def save_local_artifacts_collection(self): - """ - Save all collected local artifacts for debugging. + """Save all collected local artifacts for debugging. This method should be called upon successful or failed completion of a run. It processes all artifacts in the local_artifacts_collection list and saves @@ -257,8 +251,7 @@ def save_local_artifacts_collection(self): self._save_artifact(target_path, source_path, archive) def _step_export_onnx(self): - """ - Export or generate ONNX model for benchmarking. + """Export or generate ONNX model for benchmarking. This method must be implemented by subclasses to provide the ONNX model that will be processed by the FINN build flow. @@ -272,11 +265,9 @@ def _step_export_onnx(self): This is an abstract method that must be overridden by concrete benchmark implementations. """ - pass def _step_build_setup(self): - """ - Initialize the DataflowBuildConfig for this benchmark. + """Initialize the DataflowBuildConfig for this benchmark. This method can be overridden by subclasses if the setup is too complex for YAML definition. The default implementation loads configuration from @@ -294,14 +285,13 @@ def _step_build_setup(self): dut_yaml_name = self._params["dut"] + ".yml" dut_path = os.path.join(os.path.dirname(__file__), "dut", dut_yaml_name) if os.path.isfile(dut_path): - with open(dut_path, "r") as f: + with open(dut_path) as f: return DataflowBuildConfig.from_yaml(f) else: raise Exception("No DUT-specific YAML build definition found") def run(self): - """ - Execute the benchmark run. + """Execute the benchmark run. This method defaults to running the complete FINN build flow but may be overridden by subclasses to implement custom benchmark sequences. @@ -313,8 +303,7 @@ def run(self): return self._steps_full_build_flow() def _step_parse_builder_output(self, build_dir): - """ - Parse and analyze the output from the FINN builder. + """Parse and analyze the output from the FINN builder. Args: build_dir (str): Path to the build output directory @@ -341,8 +330,7 @@ def _step_parse_builder_output(self, build_dir): # TODO: mark job as failed if verification fails? def _steps_full_build_flow(self): - """ - Execute the complete FINN dataflow build sequence. + """Execute the complete FINN dataflow build sequence. This method implements the default step sequence for benchmarking a full FINN builder flow, including: diff --git a/src/finn/benchmarking/dut/mvau.py b/src/finn/benchmarking/dut/mvau.py index df9e1e2b35..6e77f1f2a9 100644 --- a/src/finn/benchmarking/dut/mvau.py +++ b/src/finn/benchmarking/dut/mvau.py @@ -1,5 +1,4 @@ -""" -MVAU (Matrix Vector Activation Unit) benchmarking module for FINN. +"""MVAU (Matrix Vector Activation Unit) benchmarking module for FINN. This module provides micro-benchmarking capabilities for FINN's MVAU operator. The module supports both HLS and RTL backend implementations with configurable @@ -38,8 +37,7 @@ class bench_mvau(bench): - """ - Specialized benchmark class for FINN Matrix Vector Activation Unit (MVAU) operations. + """Specialized benchmark class for FINN Matrix Vector Activation Unit (MVAU) operations. This class extends the base benchmark class to provide MVAU-specific model generation and benchmarking capabilities. It supports synthetic model creation with configurable @@ -76,8 +74,7 @@ def _make_single_mvau_model( ram_style_thresholds="auto", backend="hls", ): - """ - Create a single MVAU ONNX model with specified parameters. + """Create a single MVAU ONNX model with specified parameters. This method constructs a complete ONNX model containing a single MVAU node with the given weight matrix, data types, and configuration parameters. @@ -207,8 +204,7 @@ def _make_single_mvau_model( return model def _step_export_onnx(self, onnx_export_path): - """ - Generate and export a synthetic MVAU ONNX model for benchmarking. + """Generate and export a synthetic MVAU ONNX model for benchmarking. This method creates a synthetic MVAU model based on the benchmark parameters, including matrix dimensions, data types, sparsity patterns, and folding configuration. @@ -460,10 +456,10 @@ def _step_export_onnx(self, onnx_export_path): # TODO: also generate golden I/O pair for further verification steps model.save(onnx_export_path) + return None def _step_build_setup(self): - """ - Configure the dataflow build pipeline for MVAU microbenchmarks. + """Configure the dataflow build pipeline for MVAU microbenchmarks. This method sets up a comprehensive build configuration specifically optimized for MVAU microbenchmark evaluation. The configuration includes all necessary diff --git a/src/finn/benchmarking/dut/synthetic_nonlinear.py b/src/finn/benchmarking/dut/synthetic_nonlinear.py index 62d8f8cefc..d27b4d3b6c 100644 --- a/src/finn/benchmarking/dut/synthetic_nonlinear.py +++ b/src/finn/benchmarking/dut/synthetic_nonlinear.py @@ -1,5 +1,4 @@ -""" -Synthetic nonlinear CNN benchmarking module for FINN. +"""Synthetic nonlinear CNN benchmarking module for FINN. This module provides capabilities for generating and benchmarking synthetic nonlinear convolutional neural network models. This is used mostly to test FIFO sizing. @@ -41,8 +40,7 @@ def _generate_random_threshold_values( data_type, num_input_channels, num_steps, narrow=False, per_tensor=False ): - """ - Generate random threshold values for quantization operations. + """Generate random threshold values for quantization operations. This helper function creates random threshold arrays used in multi-threshold operators. @@ -77,8 +75,7 @@ def _generate_random_threshold_values( def _sort_thresholds_increasing(thresholds): - """ - Sort threshold arrays in ascending order along the last axis. + """Sort threshold arrays in ascending order along the last axis. This helper function ensures that threshold values are in monotonic increasing order, which is required for proper activation quantization. Each channel's thresholds @@ -96,8 +93,7 @@ def _sort_thresholds_increasing(thresholds): def _make_conv_building_block(ifm_dim, ch, kernel_size, simd, pe, parallel_window=0): - """ - Create a single convolutional building block for synthetic CNN models. + """Create a single convolutional building block for synthetic CNN models. This function generates a complete convolutional processing block consisting of: 1. Feature map padding to maintain spatial dimensions @@ -244,8 +240,7 @@ def _make_conv_building_block(ifm_dim, ch, kernel_size, simd, pe, parallel_windo def _combine_blocks(lb, rb, ifm_dim, ch, pe): - """ - Combine two processing branches into a nonlinear topology with split-merge pattern. + """Combine two processing branches into a nonlinear topology with split-merge pattern. This function creates a nonlinear CNN architecture by combining two separate processing branches (left and right) into a unified model. The topology @@ -363,8 +358,7 @@ def _combine_blocks(lb, rb, ifm_dim, ch, pe): class bench_synthetic_nonlinear(bench): - """ - Specialized benchmark class for synthetic nonlinear CNN topologies. + """Specialized benchmark class for synthetic nonlinear CNN topologies. This class extends the base benchmark framework to generate and evaluate complex synthetic CNN models with branching topologies. @@ -376,8 +370,7 @@ class bench_synthetic_nonlinear(bench): """ def _step_export_onnx(self, onnx_export_path): - """ - Generate and export a synthetic nonlinear CNN model for benchmarking. + """Generate and export a synthetic nonlinear CNN model for benchmarking. This method creates a synthetic CNN with branching topology by: 1. Building configurable left and right processing branches @@ -439,8 +432,7 @@ def _step_export_onnx(self, onnx_export_path): model.save(onnx_export_path) def _step_build_setup(self): - """ - Configure a minimal dataflow build config for synthetic nonlinear CNN benchmarks. + """Configure a minimal dataflow build config for synthetic nonlinear CNN benchmarks. Returns: DataflowBuildConfig: Configured build pipeline for nonlinear CNN benchmarking diff --git a/src/finn/benchmarking/util.py b/src/finn/benchmarking/util.py index fd90f2f867..6f9fcb470f 100644 --- a/src/finn/benchmarking/util.py +++ b/src/finn/benchmarking/util.py @@ -126,9 +126,9 @@ def merge_logs(log_a, log_b, log_out): """Merge log files.""" # merges json log (list of nested dicts) b into a, not vice versa (TODO) - with open(log_a, "r") as f: + with open(log_a) as f: a = json.load(f) - with open(log_b, "r") as f: + with open(log_b) as f: b = json.load(f) for idx, run_a in enumerate(a): diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 04ce6ece32..9efcb3bb0d 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -1407,7 +1407,6 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi """Measure performance + latency of stitched-IP model in rtlsim (xsi). Depends on the DataflowOutputType.STITCHED_IP output product. """ - if DataflowOutputType.RTLSIM_PERFORMANCE in cfg.generate_outputs and not is_mlo(model): assert ( DataflowOutputType.STITCHED_IP in cfg.generate_outputs @@ -1481,7 +1480,6 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): """Create a driver that can be used to interface the generated accelerator. Use DataflowBuildConfig to select PYNQ Python or C++ driver.""" - driver_dir = os.path.join(cfg.output_dir, "driver") if DataflowOutputType.PYNQ_DRIVER in cfg.generate_outputs: # determine drivertype @@ -1586,7 +1584,6 @@ def step_vivado_power_estimation(model: ModelWrapper, cfg: DataflowBuildConfig): def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig): """Synthesize a bitfile for the using the specified shell flow, using either Vivado or Vitis, to target the specified board.""" - if DataflowOutputType.BITFILE in cfg.generate_outputs: bitfile_dir = cfg.output_dir + "/bitfile" os.makedirs(bitfile_dir, exist_ok=True) @@ -1668,7 +1665,6 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig): @register_build_dataflow_step() def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig): """Create a deployment package including the driver and bitfile.""" - if DataflowOutputType.DEPLOYMENT_PACKAGE in cfg.generate_outputs: deploy_dir = cfg.output_dir + "/deploy" bitfile_dir = cfg.output_dir + "/bitfile" @@ -1696,7 +1692,6 @@ def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig): def step_loop_rolling(model, cfg): """Roll a repeating sequence of layers into a loop. PyTorch metadata node hierarchy is used to indicate the loop structure.""" - if cfg.mlo: if cfg.loop_body_range is not None: # set node metadata like loop rolling would expect diff --git a/src/finn/builder/custom_step_library/resnet.py b/src/finn/builder/custom_step_library/resnet.py index 1781bd8636..5303823d26 100644 --- a/src/finn/builder/custom_step_library/resnet.py +++ b/src/finn/builder/custom_step_library/resnet.py @@ -130,7 +130,7 @@ def step_resnet_tidy(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrap def step_resnet_streamline( model: ModelWrapper, cfg: DataflowBuildConfig -) -> ModelWrapper: # noqa: ARG001 +) -> ModelWrapper: """Streamline ResNet models.""" transform = ComposedTransformation( [ @@ -153,7 +153,7 @@ def step_resnet_streamline( def step_resnet_convert_to_hw( model: ModelWrapper, cfg: DataflowBuildConfig -) -> ModelWrapper: # noqa: ARG001 +) -> ModelWrapper: """Convert ResNet models to hardware-specific operations.""" # Convert Squeeze and Unsqueeze operators to hardware operations model = model.transform(InferDataLayouts()) diff --git a/src/finn/builder/passes.py b/src/finn/builder/passes.py index 7a0e87243f..d3fa15e34d 100644 --- a/src/finn/builder/passes.py +++ b/src/finn/builder/passes.py @@ -19,7 +19,7 @@ from onnx_passes.ops import inject_custom_ops # Make custom Im2Col operator available for convolution lowering -from onnx_passes.ops.im2col import Im2Col # noqa: Used indirectly via registry +from onnx_passes.ops.im2col import Im2Col # noqa: Used indirectly via registry # noqa: F401 from onnx_passes.ops.qonnx import DOMAIN as QONNX_DOMAIN # Collects named passes from the ONNX Passes registry @@ -42,7 +42,7 @@ # Makes custom QONNX import and inlining passes available import onnx_passes.passes.imports.qonnx # isort:skip # noqa: Used indirectly via registry -import onnx_passes.passes.inline.qonnx # isort:skip # noqa: Used indirectly via registry +import onnx_passes.passes.inline.qonnx # isort:skip # noqa: Used indirectly via registry # noqa: F401 def _make_pass_config(cfg: DataflowBuildConfig): diff --git a/src/finn/core/onnx_exec.py b/src/finn/core/onnx_exec.py index 93412d8112..24264f2abf 100644 --- a/src/finn/core/onnx_exec.py +++ b/src/finn/core/onnx_exec.py @@ -57,14 +57,13 @@ def execute_onnx(model, input_dict, return_full_exec_context=False, start_node=N If they are set to particular ONNX nodes, only the subgraph between (and including) those nodes is executed. """ - # check if model has an execution mode set # if None, execute model node using the QONNX-provided execute_onnx impl # if set to "rtlsim" execute model using xsi model_exec_mode = model.get_metadata_prop("exec_mode") if (model_exec_mode is None) or (model_exec_mode == ""): return execute_onnx_base(model, input_dict, return_full_exec_context, start_node, end_node) - elif model_exec_mode == "rtlsim": + if model_exec_mode == "rtlsim": # check sanity of model and then use stitched IP for rtlsim if not model.check_all_tensor_shapes_specified(): raise Exception("Found unspecified tensor shapes, try infer_shapes") @@ -106,13 +105,12 @@ def execute_onnx(model, input_dict, return_full_exec_context=False, start_node=N if return_full_exec_context: return execution_context - else: - # provide outputs as dict - output_dict = dict() - for out_tensor in graph.output: - out_name = out_tensor.name - output_dict[out_name] = execution_context[out_name] - return output_dict + # provide outputs as dict + output_dict = dict() + for out_tensor in graph.output: + out_name = out_tensor.name + output_dict[out_name] = execution_context[out_name] + return output_dict def execute_onnx_and_make_model(model, input_dict): @@ -120,7 +118,6 @@ def execute_onnx_and_make_model(model, input_dict): ModelWrapper where an initializer is provided for each tensor as taken from the execution. This new model is useful for debugging, since it contains all the intermediate activation values.""" - # retrieve the full execution context execution_context = execute_onnx(model, input_dict, True) new_model = copy.deepcopy(model) diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index e611f5cd78..fb7cf707b2 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -187,7 +187,7 @@ def rtlsim_exec_cppxsi( top_module_name = top_module_file_name.strip(".v") if (rtlsim_so is None) or (not os.path.isfile(rtlsim_so)): vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") - with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f: + with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt") as f: all_verilog_srcs = f.read().split() rtlsim_name = model.graph.node[0].name if is_single_node else top_module_name single_src_dir = make_build_dir("rtlsim_" + rtlsim_name + "_") @@ -242,9 +242,7 @@ def rtlsim_exec_cppxsi( # retrieve the number of inputs from execution_context n_inferences = execution_context[model.get_first_global_in()] ifnames = model.get_metadata_prop("vivado_stitch_ifnames") - assert not ( - ifnames is None - ), "Couldn't find stitched-IP interface names, did you run IP stitching first?" + assert ifnames is not None, "Couldn't find stitched-IP interface names, did you run IP stitching first?" ifnames = eval(ifnames) if "aximm" in ifnames.keys() and ifnames["aximm"] != []: assert ( @@ -371,7 +369,7 @@ def rtlsim_exec_finnxsi(model, execution_context, pre_hook=None, post_hook=None) rtlsim_so = model.get_metadata_prop("rtlsim_so") if (rtlsim_so is None) or (not os.path.isfile(rtlsim_so)): vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") - with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f: + with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt") as f: all_verilog_srcs = f.read().split() top_module_file_name = file_to_basename(model.get_metadata_prop("wrapper_filename")) top_module_name = top_module_file_name.strip(".v") diff --git a/src/finn/core/throughput_test.py b/src/finn/core/throughput_test.py index 9f632cf42a..b762c91ae7 100644 --- a/src/finn/core/throughput_test.py +++ b/src/finn/core/throughput_test.py @@ -35,7 +35,6 @@ def throughput_test_rtlsim(model, clk_ns, batchsize=100): """Runs a throughput test for the given IP-stitched model. When combined with tracing, useful to determine bottlenecks and required FIFO sizes.""" - assert ( model.get_metadata_prop("exec_mode") == "rtlsim" ), """Top-level exec_mode diff --git a/src/finn/custom_op/fpgadataflow/attention_heads.py b/src/finn/custom_op/fpgadataflow/attention_heads.py index c64f26211d..7e11a5c4ac 100644 --- a/src/finn/custom_op/fpgadataflow/attention_heads.py +++ b/src/finn/custom_op/fpgadataflow/attention_heads.py @@ -116,8 +116,7 @@ def num_inputs(self): def make_shape_compatible_op(self, model: ModelWrapper): # noqa - """ - Make an operation compatible with the output shape for shape inference + """Make an operation compatible with the output shape for shape inference Note: Propagates shape forward, i.e., never asks for the shape of the output, even if it seems easier. """ @@ -157,7 +156,7 @@ def make_shape_compatible_op(self, model: ModelWrapper): # noqa def infer_node_datatype(self, model: ModelWrapper): # noqa """Infer the datatype of the node output.""" - # Get the node wrapped by this custom op # noqa Duplicate + # Get the node wrapped by this custom op node = self.onnx_node # Test for changing input datatype if model.get_tensor_datatype(node.input[0]) != self.dtype: @@ -217,7 +216,7 @@ def _execute_node_cppsim(self, context, graph): # noqa: graph unused def _execute_node_rtlsim(self, context, graph): # noqa: graph unused """Execute node in RTL simulation mode.""" - # Get the node wrapped by this custom op # noqa Duplicate + # Get the node wrapped by this custom op node = self.onnx_node # Input data is stored in numpy files in the code generation dictionary code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") @@ -358,8 +357,7 @@ def get_number_output_values(self): num_outputs_per_stream = np.prod(self.get_folded_output_shape()[:-1]) if self.heads > 1: return {f"out{i}": num_outputs_per_stream for i in range(self.heads)} - else: - return num_outputs_per_stream + return num_outputs_per_stream def get_exp_cycles(self): """Derive the expected cycles of the operator given the folding configuration.""" @@ -370,6 +368,7 @@ def get_exp_cycles(self): class MergeMultiHeads(HWCustomOp): """Merging of attention heads (before output projections) custom operator.""" + # Initializes the operator given an onnx graph node def __init__(self, onnx_node, **kwargs): """Initialize the operator.""" @@ -473,7 +472,7 @@ def make_shape_compatible_op(self, model: ModelWrapper): # noqa def infer_node_datatype(self, model: ModelWrapper): # noqa """Infer the datatype of the node output.""" # Get the node wrapped by this custom op - node = self.onnx_node # noqa Duplicate + node = self.onnx_node # Test for changing input datatype if model.get_tensor_datatype(node.input[0]) != self.dtype: # Get the new datatype diff --git a/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py b/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py index d7c110d42b..9d4d50e9a8 100644 --- a/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py +++ b/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py @@ -179,9 +179,8 @@ def get_outstream_width(self, ind=0): # feed all window pixels in parallel k_h, k_w = self.get_nodeattr("ConvKernelDim") return self.get_instream_width() * k_h * k_w - else: - # if parallel variant not in use: same width for output and input stream - return self.get_instream_width() + # if parallel variant not in use: same width for output and input stream + return self.get_instream_width() def get_1d_conv_attrs_normalized(self): # support both (1, D) and (D, 1) cases transparently: @@ -242,7 +241,7 @@ def execute_node(self, context, graph): stride=[s[0], s[1]], kernel_size=[k[0], k[1]], dilations=[d[0], d[1]], - input_shape="(1,{},{},{})".format(ifm_dim[0], ifm_dim[1], ifm_ch), + input_shape=f"(1,{ifm_dim[0]},{ifm_dim[1]},{ifm_ch})", ) graph_im2col = helper.make_graph( nodes=[im2col_node], diff --git a/src/finn/custom_op/fpgadataflow/crop.py b/src/finn/custom_op/fpgadataflow/crop.py index ff4631e730..5e0f971d65 100644 --- a/src/finn/custom_op/fpgadataflow/crop.py +++ b/src/finn/custom_op/fpgadataflow/crop.py @@ -91,7 +91,7 @@ def infer_node_datatype(self, model): dt = model.get_tensor_datatype(node.input[0]) if dt != self.get_input_datatype(): warn_str = ( - f"data_type changing for {node.name}: {str(self.get_input_datatype())} -> {str(dt)}" + f"data_type changing for {node.name}: {self.get_input_datatype()!s} -> {dt!s}" ) log.warning(warn_str) self.set_nodeattr("DataType", dt.name) diff --git a/src/finn/custom_op/fpgadataflow/elementwise_binary.py b/src/finn/custom_op/fpgadataflow/elementwise_binary.py index 1508206073..1eb2d22eab 100644 --- a/src/finn/custom_op/fpgadataflow/elementwise_binary.py +++ b/src/finn/custom_op/fpgadataflow/elementwise_binary.py @@ -200,7 +200,7 @@ def infer_node_datatype(self, model: ModelWrapper): # Get the new datatype new_dtype = model.get_tensor_datatype(node.input[0]) # Issue a warning message - log.warning(f"{node.name}: lhs_dtype changing from" f" {self.lhs_dtype} to {new_dtype}") + log.warning(f"{node.name}: lhs_dtype changing from {self.lhs_dtype} to {new_dtype}") # Set the new datatype attribute self.set_nodeattr("lhs_dtype", new_dtype.name) # Test for changing right-hand-side input datatype @@ -208,7 +208,7 @@ def infer_node_datatype(self, model: ModelWrapper): # Get the new datatype new_dtype = model.get_tensor_datatype(node.input[1]) # Issue a warning message - log.warning(f"{node.name}: rhs_dtype changing from" f" {self.rhs_dtype} to {new_dtype}") + log.warning(f"{node.name}: rhs_dtype changing from {self.rhs_dtype} to {new_dtype}") # Set the new datatype attribute self.set_nodeattr("rhs_dtype", new_dtype.name) # Force the output data type stored as a node attribute @@ -878,30 +878,28 @@ def _derive_out_dtype(self, model: ModelWrapper): # if any of the inputs are float, make the output float as well max_bitwidth = max(self.lhs_dtype.bitwidth(), self.rhs_dtype.bitwidth()) return DataType[f"FLOAT{max_bitwidth}"] + all_ints = all([self.lhs_dtype.is_integer(), self.rhs_dtype.is_integer()]) + # Get the width of the data types of the inputs # noqa: Duplicate + lhs_width = self.lhs_dtype.bitwidth() + rhs_width = self.rhs_dtype.bitwidth() + if all_ints: + # output will be signed if both inputs are signed + signed = all([self.lhs_dtype.signed(), self.rhs_dtype.signed()]) + # use the greater of the two input bitwidths for the output + out_width = max(lhs_width, rhs_width) + return DataType[f"INT{out_width}" if signed else f"UINT{out_width}"] + # use fixed point with max of intbits and fracbits from both sides + # to make sure an output coming from either input is representable + lhs_fracbits = self.lhs_dtype.frac_bits() if self.lhs_dtype.is_fixed_point() else 0 + rhs_fracbits = self.rhs_dtype.frac_bits() if self.rhs_dtype.is_fixed_point() else 0 + out_fracbits = max(lhs_fracbits, rhs_fracbits) + if self.lhs_dtype.is_fixed_point(): + lhs_intbits = self.lhs_dtype.int_bits() + else: + lhs_intbits = self.lhs_dtype.bitwidth() + if self.rhs_dtype.is_fixed_point(): + rhs_intbits = self.rhs_dtype.int_bits() else: - all_ints = all([self.lhs_dtype.is_integer(), self.rhs_dtype.is_integer()]) - # Get the width of the data types of the inputs # noqa: Duplicate - lhs_width = self.lhs_dtype.bitwidth() - rhs_width = self.rhs_dtype.bitwidth() - if all_ints: - # output will be signed if both inputs are signed - signed = all([self.lhs_dtype.signed(), self.rhs_dtype.signed()]) - # use the greater of the two input bitwidths for the output - out_width = max(lhs_width, rhs_width) - return DataType[f"INT{out_width}" if signed else f"UINT{out_width}"] - else: - # use fixed point with max of intbits and fracbits from both sides - # to make sure an output coming from either input is representable - lhs_fracbits = self.lhs_dtype.frac_bits() if self.lhs_dtype.is_fixed_point() else 0 - rhs_fracbits = self.rhs_dtype.frac_bits() if self.rhs_dtype.is_fixed_point() else 0 - out_fracbits = max(lhs_fracbits, rhs_fracbits) - if self.lhs_dtype.is_fixed_point(): - lhs_intbits = self.lhs_dtype.int_bits() - else: - lhs_intbits = self.lhs_dtype.bitwidth() - if self.rhs_dtype.is_fixed_point(): - rhs_intbits = self.rhs_dtype.int_bits() - else: - rhs_intbits = self.rhs_dtype.bitwidth() - out_intbits = max(lhs_intbits, rhs_intbits) - return DataType[f"FIXED<{out_fracbits+out_intbits},{out_intbits}>"] + rhs_intbits = self.rhs_dtype.bitwidth() + out_intbits = max(lhs_intbits, rhs_intbits) + return DataType[f"FIXED<{out_fracbits+out_intbits},{out_intbits}>"] diff --git a/src/finn/custom_op/fpgadataflow/fmpadding.py b/src/finn/custom_op/fpgadataflow/fmpadding.py index a508e98046..098a413931 100644 --- a/src/finn/custom_op/fpgadataflow/fmpadding.py +++ b/src/finn/custom_op/fpgadataflow/fmpadding.py @@ -63,7 +63,7 @@ def get_nodeattr_types(self): return my_attrs def get_padded_odim(self): - "Return the padded spatial size of the output." + """Return the padded spatial size of the output.""" idim_h, idim_w = self.get_nodeattr("ImgDim") pad = self.get_nodeattr("Padding") pad_h = pad[0] + pad[2] diff --git a/src/finn/custom_op/fpgadataflow/fmpadding_pixel.py b/src/finn/custom_op/fpgadataflow/fmpadding_pixel.py index 107a2f38e8..a36eabe949 100644 --- a/src/finn/custom_op/fpgadataflow/fmpadding_pixel.py +++ b/src/finn/custom_op/fpgadataflow/fmpadding_pixel.py @@ -56,7 +56,7 @@ def get_nodeattr_types(self): return my_attrs def get_padded_odim(self): - "Return the padded spatial size of the output." + """Return the padded spatial size of the output.""" idim_h, idim_w = self.get_nodeattr("ImgDim") stride_h, stride_w = self.get_nodeattr("Stride") odim_h = idim_h + (idim_h - 1) * (stride_h - 1) diff --git a/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py b/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py index aec1727881..675e42c9da 100644 --- a/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py @@ -11,7 +11,7 @@ # Base class for specializing HW operators as implemented via HLS from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend # The generic HW custom operator version of the operator as a base class -from finn.custom_op.fpgadataflow.attention_heads import ( # noqa +from finn.custom_op.fpgadataflow.attention_heads import ( MergeMultiHeads, SplitMultiHeads ) @@ -33,8 +33,8 @@ def get_nodeattr_types(self): # Executes multi-head splitting in C++ simulation def _execute_node_cppsim(self, context, graph): # noqa: graph unused - # Get the node wrapped by this custom op # noqa Duplicate - node = self.onnx_node # noqa Duplicate + # Get the node wrapped by this custom op + node = self.onnx_node # Input data is stored in numpy files in the code generation dictionary code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") # Get the input out of the execution context @@ -66,7 +66,7 @@ def get_ap_int_max_w(self): # Find the widths of the widest output # Note: there is one output per head o_bits_max = max( - (self.get_outstream_width(ind) for ind in range(self.heads)) + self.get_outstream_width(ind) for ind in range(self.heads) ) # Find the biggest of the inputs/outputs return max([i_bits_max, o_bits_max]) @@ -151,13 +151,13 @@ def out(i): # output elements per head and write into the corresponding stream *(f"{out(i)}.write(x{split(i)});" for i in range(self.heads)), # End of for-loop over repetitions body - f"}}" # noqa: f-string symmetry + "}" # noqa: f-string symmetry ] # Generates C++ code for reading the output stream and converting back to # numpy format for testing in C++ simulation def dataoutstrm(self): - # Output data will be stored in numpy files in the # noqa Duplicate + # Output data will be stored in numpy files in the # code generation dictionary code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") # Get the expected shape of the folded output array formatted as a C++ @@ -165,7 +165,7 @@ def dataoutstrm(self): # Note: Valid formatting relies on correct placement of curly braces # and line breaks: Open/close all three braces on the same line of code # to avoid '\n' to be inserted into the string - shape = f"""{{{','.join((str(i) for i in self.get_folded_output_shape()))}}}""" + shape = f"""{{{','.join(str(i) for i in self.get_folded_output_shape())}}}""" # Start collecting function calls to write the output data stream self.code_gen_dict["$DATAOUTSTREAM$"] = [] @@ -233,9 +233,9 @@ def pragmas(self): # Returns the names of input and output interfaces grouped by protocol def get_verilog_top_module_intf_names(self): - # Start collecting interface names in a dictionary # noqa Duplicate + # Start collecting interface names in a dictionary # starting with clock and reset - intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # noqa + intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # AXI stream input interfaces intf_names["s_axis"] = [ # Just one input stream @@ -308,7 +308,7 @@ def get_ap_int_max_w(self): # Find the widths of the widest output # Note: there is one output per head o_bits_max = max( - (self.get_outstream_width(ind) for ind in range(self.heads)) + self.get_outstream_width(ind) for ind in range(self.heads) ) # Find the biggest of the inputs/outputs return max([i_bits_max, o_bits_max]) @@ -405,7 +405,7 @@ def dataoutstrm(self): # Note: Valid formatting relies on correct placement of curly braces # and line breaks: Open/close all three braces on the same line of code # to avoid '\n' to be inserted into the string - shape = f"""{{{','.join((str(i) for i in self.get_folded_output_shape()))}}}""" + shape = f"""{{{','.join(str(i) for i in self.get_folded_output_shape())}}}""" # Generate function call for reading from the output stream into the # output file self.code_gen_dict["$DATAOUTSTREAM$"] = [ @@ -466,7 +466,7 @@ def pragmas(self): def get_verilog_top_module_intf_names(self): # Start collecting interface names in a dictionary starting with clock # and reset - intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # noqa + intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # AXI stream input interfaces intf_names["s_axis"] = [ # One input stream per head diff --git a/src/finn/custom_op/fpgadataflow/hls/attention_hls.py b/src/finn/custom_op/fpgadataflow/hls/attention_hls.py index 8f63f4952b..2518ca3a6d 100644 --- a/src/finn/custom_op/fpgadataflow/hls/attention_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/attention_hls.py @@ -52,9 +52,9 @@ def get_ap_int_max_w(self): type needed for HLS synthesis. """ # Find the widths of the widest input - i_bits_max = max((self.get_instream_width(ind) for ind in range(3))) + i_bits_max = max(self.get_instream_width(ind) for ind in range(3)) # Find the widths of the widest output - o_bits_max = max((self.get_outstream_width(ind) for ind in range(1))) + o_bits_max = max(self.get_outstream_width(ind) for ind in range(1)) # Assume no bits to represent the mask, if there is no mask m_bits = 0 # A mask received as input has a bit-width as well @@ -153,7 +153,7 @@ def prepare_thresholds(ts, length, fold, dtype): """ # Number of thresholds is given as the last dimension of the # threshold tensor, first dimension is covering all output elements - num = ts.shape[-1] # noqa + num = ts.shape[-1] # Explicitly broadcast thresholds from per-tensor to per-channel ts = np.broadcast_to(ts, (length, num)) # Partition the thresholds along the length into folds of parallel @@ -653,7 +653,7 @@ def get_verilog_top_module_intf_names(self): """ # Start collecting interface names in a dictionary starting with clock # and reset - intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # noqa + intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # AXI stream input interfaces # TODO: support mask input? intf_names["s_axis"] = [ diff --git a/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py b/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py index 72bc3bd973..31d48c51fa 100644 --- a/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py @@ -126,16 +126,15 @@ def get_normal_output_shape(self, ind=0): # same shape as input return self.get_normal_input_shape() # second output is scalar checksum output - elif ind == 1: + if ind == 1: return tuple([1]) - else: - raise Exception("Undefined input ind for this layer type") + raise Exception("Undefined input ind for this layer type") def npy_to_dynamic_output(self, context): super().npy_to_dynamic_output(context) node = self.onnx_node code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") - output_checksum = np.load("{}/output_1.npy".format(code_gen_dir)) + output_checksum = np.load(f"{code_gen_dir}/output_1.npy") context[node.output[1]] = output_checksum def execute_node(self, context, graph): @@ -149,9 +148,9 @@ def defines(self, var): words_per_frame = self.get_nodeattr("words_per_frame") word_size = self.get_instream_width() my_defines = [] - my_defines.append("#define WORDS_PER_FRAME {}".format(words_per_frame)) - my_defines.append("#define ITEMS_PER_WORD {}".format(items_per_word)) - my_defines.append("#define WORD_SIZE {}".format(word_size)) + my_defines.append(f"#define WORDS_PER_FRAME {words_per_frame}") + my_defines.append(f"#define ITEMS_PER_WORD {items_per_word}") + my_defines.append(f"#define WORD_SIZE {word_size}") self.code_gen_dict["$DEFINES$"] = my_defines def read_npy_data(self): @@ -179,10 +178,10 @@ def read_npy_data(self): def strm_decl(self): self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in0_V ("in0_V");'.format(self.get_instream_width()) + f'hls::stream> in0_V ("in0_V");' ) self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> out0_V ("out0_V");'.format(self.get_outstream_width()) + f'hls::stream> out0_V ("out0_V");' ) self.code_gen_dict["$STREAMDECLARATIONS$"].append("ap_uint<32> chk;") # set drain = false for cppsim @@ -226,10 +225,8 @@ def dataoutstrm(self): def blackboxfunction(self): self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """using T = ap_uint;\n void {}(hls::stream &in0_V, - hls::stream &out0_V, ap_uint<32> &chk, ap_uint<1> &drain)""".format( - self.onnx_node.name - ) + f"""using T = ap_uint;\n void {self.onnx_node.name}(hls::stream &in0_V, + hls::stream &out0_V, ap_uint<32> &chk, ap_uint<1> &drain)""" ] def pragmas(self): diff --git a/src/finn/custom_op/fpgadataflow/hls/concat_hls.py b/src/finn/custom_op/fpgadataflow/hls/concat_hls.py index d673a5a1ac..2c24762eb1 100644 --- a/src/finn/custom_op/fpgadataflow/hls/concat_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/concat_hls.py @@ -63,7 +63,7 @@ def docompute(self): in_streams.append("in%d_V" % i) in_stream_names = ", ".join(in_streams) in_stream_folds = ", ".join(input_folds) - comp_call = "StreamingConcat<{}>(out0_V, {});".format(in_stream_folds, in_stream_names) + comp_call = f"StreamingConcat<{in_stream_folds}>(out0_V, {in_stream_names});" self.code_gen_dict["$DOCOMPUTE$"] = [comp_call] def blackboxfunction(self): diff --git a/src/finn/custom_op/fpgadataflow/hls/duplicatestreams_hls.py b/src/finn/custom_op/fpgadataflow/hls/duplicatestreams_hls.py index 8e655acd38..3aeb2b818b 100644 --- a/src/finn/custom_op/fpgadataflow/hls/duplicatestreams_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/duplicatestreams_hls.py @@ -115,7 +115,7 @@ def timeout_condition(self): condition = [] n_outputs = self.get_nodeattr("NumOutputStreams") for i in range(n_outputs): - condition.append("out{}_V.empty()".format(i)) + condition.append(f"out{i}_V.empty()") condition = " && ".join(condition) self.code_gen_dict["$TIMEOUT_CONDITION$"] = [condition] diff --git a/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py b/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py index 5277e2d61c..b3571f390b 100644 --- a/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py @@ -103,8 +103,7 @@ def get_ap_int_max_w(self): return max([i_bits_max, o_bits_max]) def adapt_for_loop_body(self, input_types): - """ - Adapt elementwise binary operator for loop body execution. + """Adapt elementwise binary operator for loop body execution. When an elementwise operator is placed inside a loop, parameters that are indexed per iteration (PARAMETER type) need to be received as @@ -1212,4 +1211,3 @@ class ElementwiseMax_hls( ): """HLS Implementation of the elementwise max operation.""" - pass diff --git a/src/finn/custom_op/fpgadataflow/hls/fmpadding_pixel_hls.py b/src/finn/custom_op/fpgadataflow/hls/fmpadding_pixel_hls.py index 036780836d..b85120172d 100644 --- a/src/finn/custom_op/fpgadataflow/hls/fmpadding_pixel_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/fmpadding_pixel_hls.py @@ -70,10 +70,8 @@ def docompute(self): stride_h, stride_w = self.get_nodeattr("Stride") hls_call = "FMPadding_Pixel_Nonsquare" self.code_gen_dict["$DOCOMPUTE$"] = [ - """{} (in0_V, out0_V);""".format( - hls_call, in_t - ) + f"""{hls_call} (in0_V, out0_V);""" ] def blackboxfunction(self): diff --git a/src/finn/custom_op/fpgadataflow/hls/globalaccpool_hls.py b/src/finn/custom_op/fpgadataflow/hls/globalaccpool_hls.py index ff0cb549b6..369d89cb30 100644 --- a/src/finn/custom_op/fpgadataflow/hls/globalaccpool_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/globalaccpool_hls.py @@ -91,10 +91,6 @@ def docompute(self): def blackboxfunction(self): self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """void {}(hls::stream> &in0_V, - hls::stream> &out0_V)""".format( - self.onnx_node.name, - self.get_instream_width(), - self.get_outstream_width(), - ) + f"""void {self.onnx_node.name}(hls::stream> &in0_V, + hls::stream> &out0_V)""" ] diff --git a/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py b/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py index 6fb54ddcff..2d2617f695 100644 --- a/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py @@ -115,32 +115,30 @@ def get_normal_output_shape(self, ind=0): def get_folded_input_shape(self, ind=0): if self.get_nodeattr("direction") == "in": raise ValueError("Folded input shape not defined for input IODMA") - else: - shape = list(self.get_normal_input_shape()) - itype_bits = self.get_input_datatype().bitwidth() - intfw = self.get_nodeattr("streamWidth") - assert intfw % itype_bits == 0, "Input stream width must be a multiple of datatype bits" - elems_per_word = intfw // itype_bits - assert shape[-1] % elems_per_word == 0, "Fold depth must be integer" - fold_depth = shape[-1] // elems_per_word - shape[-1] = fold_depth - shape.append(elems_per_word) - return tuple(shape) + shape = list(self.get_normal_input_shape()) + itype_bits = self.get_input_datatype().bitwidth() + intfw = self.get_nodeattr("streamWidth") + assert intfw % itype_bits == 0, "Input stream width must be a multiple of datatype bits" + elems_per_word = intfw // itype_bits + assert shape[-1] % elems_per_word == 0, "Fold depth must be integer" + fold_depth = shape[-1] // elems_per_word + shape[-1] = fold_depth + shape.append(elems_per_word) + return tuple(shape) def get_folded_output_shape(self, ind=0): if self.get_nodeattr("direction") == "out": raise ValueError("Folded output shape not defined for output IODMA") - else: - shape = list(self.get_normal_output_shape()) - itype_bits = self.get_output_datatype().bitwidth() - intfw = self.get_nodeattr("streamWidth") - assert intfw % itype_bits == 0, "Input stream width must be a multiple of datatype bits" - elems_per_word = intfw // itype_bits - assert shape[-1] % elems_per_word == 0, "Fold depth must be integer" - fold_depth = shape[-1] // elems_per_word - shape[-1] = fold_depth - shape.append(elems_per_word) - return tuple(shape) + shape = list(self.get_normal_output_shape()) + itype_bits = self.get_output_datatype().bitwidth() + intfw = self.get_nodeattr("streamWidth") + assert intfw % itype_bits == 0, "Input stream width must be a multiple of datatype bits" + elems_per_word = intfw // itype_bits + assert shape[-1] % elems_per_word == 0, "Fold depth must be integer" + fold_depth = shape[-1] // elems_per_word + shape[-1] = fold_depth + shape.append(elems_per_word) + return tuple(shape) def infer_node_datatype(self, model): node = self.onnx_node @@ -166,18 +164,16 @@ def get_output_datatype(self, ind=0): def get_instream_width(self, ind=0): if self.get_nodeattr("direction") == "in": return self.get_nodeattr("intfWidth") - elif self.get_nodeattr("direction") == "out": + if self.get_nodeattr("direction") == "out": return self.get_nodeattr("streamWidth") - else: - raise ValueError("Invalid IODMA direction, please set to in or out") + raise ValueError("Invalid IODMA direction, please set to in or out") def get_outstream_width(self, ind=0): if self.get_nodeattr("direction") == "out": return self.get_nodeattr("intfWidth") - elif self.get_nodeattr("direction") == "in": + if self.get_nodeattr("direction") == "in": return self.get_nodeattr("streamWidth") - else: - raise ValueError("Invalid IODMA direction, please set to in or out") + raise ValueError("Invalid IODMA direction, please set to in or out") def get_number_output_values(self): oshape = self.get_normal_output_shape() @@ -205,7 +201,7 @@ def defines(self, var): ] def get_ap_int_max_w(self): - "Return the maximum width of any ap_int used in this module." + """Return the maximum width of any ap_int used in this module.""" instream = self.get_instream_width() outstream = self.get_outstream_width() width_lcm = (instream * outstream) // math.gcd(instream, outstream) diff --git a/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py b/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py index 3c5a14b75c..02e8c8b35d 100644 --- a/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py @@ -36,7 +36,7 @@ class Lookup_hls(Lookup, HLSBackend): - "Streaming elementwise HLS lookup, mapping indices to values." + """Streaming elementwise HLS lookup, mapping indices to values.""" def __init__(self, onnx_node, **kwargs): super().__init__(onnx_node, **kwargs) @@ -168,7 +168,7 @@ def generate_params(self, model, path): embeddings = model.get_initializer(self.onnx_node.input[1]) if mem_mode == "internal_embedded": code_gen_dir = path - weight_filename = "{}/embeddings.hpp".format(code_gen_dir) + weight_filename = f"{code_gen_dir}/embeddings.hpp" edt = DataType[self.get_nodeattr("EmbeddingType")] # obits = self.get_outstream_width() # packed_output_hls_type = "ap_uint<%d>" % obits @@ -187,7 +187,7 @@ def generate_params(self, model, path): ext_mem_width = self.get_nodeattr("ext_mem_width") assert edt.bitwidth() == 8, ( "Lookup with mem_mode=external " - + "only works with 8-bit embeddings but found " + "only works with 8-bit embeddings but found " + str(edt) ) emb_dim = self.get_nodeattr("EmbeddingDim") @@ -224,5 +224,4 @@ def get_ap_int_max_w(self): ext_mem_width = self.get_nodeattr("ext_mem_width") if mem_mode == "external": return max(ext_mem_width, parent_max) - else: - return parent_max + return parent_max diff --git a/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py b/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py index de73a54464..b8e7c60fe2 100644 --- a/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py @@ -242,7 +242,7 @@ def defines(self, var): or self.get_nodeattr("mlo_max_iter") ): wdt = self.get_input_datatype(1) - self.code_gen_dict["$DEFINES$"].append("#define WP1 {}\n".format(wdt.bitwidth())) + self.code_gen_dict["$DEFINES$"].append(f"#define WP1 {wdt.bitwidth()}\n") def read_npy_data(self): code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") @@ -300,10 +300,10 @@ def strm_decl(self): mem_mode = self.get_nodeattr("mem_mode") self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in0_V ("in0_V");'.format(self.get_instream_width(0)) + f'hls::stream> in0_V ("in0_V");' ) self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> out0_V ("out0_V");'.format(self.get_outstream_width()) + f'hls::stream> out0_V ("out0_V");' ) if ( @@ -315,7 +315,7 @@ def strm_decl(self): if self.get_nodeattr("dynamic_input"): iwidth = iwidth * self.get_nodeattr("SIMD") self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in1_V ("in1_V");'.format(iwidth) + f'hls::stream> in1_V ("in1_V");' ) def docompute(self): @@ -406,13 +406,9 @@ def blackboxfunction(self): mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "internal_embedded": self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """void {}(hls::stream> &in0_V, - hls::stream> &out0_V - )""".format( - self.onnx_node.name, - self.get_instream_width(0), - self.get_outstream_width(), - ) + f"""void {self.onnx_node.name}(hls::stream> &in0_V, + hls::stream> &out0_V + )""" ] elif ( mem_mode == "internal_decoupled" @@ -423,16 +419,11 @@ def blackboxfunction(self): if self.get_nodeattr("dynamic_input"): wwidth = wwidth * self.get_nodeattr("SIMD") self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """void {}( - hls::stream> &in0_V, - hls::stream> &in1_V, - hls::stream> &out0_V - )""".format( - self.onnx_node.name, - self.get_instream_width(0), - wwidth, - self.get_outstream_width(), - ) + f"""void {self.onnx_node.name}( + hls::stream> &in0_V, + hls::stream> &in1_V, + hls::stream> &out0_V + )""" ] else: @@ -453,7 +444,7 @@ def pragmas(self): # the weight tensor is ap_uint [PE][WMEM] # partition for parallel access along the PE dimension (dim 1) self.code_gen_dict["$PRAGMAS$"].append( - ("#pragma HLS ARRAY_PARTITION variable=weights.m_weights " "complete dim=1") + "#pragma HLS ARRAY_PARTITION variable=weights.m_weights complete dim=1" ) elif ( mem_mode == "internal_decoupled" @@ -474,19 +465,19 @@ def pragmas(self): if self.calc_tmem() != 0: # TODO find a better way of checking for no pregenerated thresholds self.code_gen_dict["$PRAGMAS$"].append( - ("#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds " "complete dim=1") + "#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds complete dim=1" ) self.code_gen_dict["$PRAGMAS$"].append( - ("#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds " "complete dim=3") + "#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds complete dim=3" ) # add resource pragma for thresholds if set if ram_style_thresholds == "distributed": self.code_gen_dict["$PRAGMAS$"].append( - ("#pragma HLS RESOURCE variable=threshs.m_thresholds " "core=ROM_2P_LUTRAM") + "#pragma HLS RESOURCE variable=threshs.m_thresholds core=ROM_2P_LUTRAM" ) elif ram_style_thresholds == "block": self.code_gen_dict["$PRAGMAS$"].append( - ("#pragma HLS RESOURCE variable=threshs.m_thresholds " "core=ROM_2P_BRAM") + "#pragma HLS RESOURCE variable=threshs.m_thresholds core=ROM_2P_BRAM" ) elif ram_style_thresholds == "auto": # no pragma needed @@ -520,10 +511,8 @@ def execute_node(self, context, graph): code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) # create a npy file fore each input of the node (in_ind is input index) @@ -556,7 +545,7 @@ def execute_node(self, context, graph): if dynamic_input: reshaped_input = context[inputs].reshape(-1, context[inputs].shape[-1]) self.make_weight_file( - reshaped_input, "decoupled_npy", "{}/input_1.npy".format(code_gen_dir) + reshaped_input, "decoupled_npy", f"{code_gen_dir}/input_1.npy" ) if mode == "cppsim": @@ -575,7 +564,7 @@ def execute_node(self, context, graph): elif mode == "rtlsim": sim = self.get_rtlsim() nbits = self.get_instream_width(0) - inp = npy_to_rtlsim_input("{}/input_0.npy".format(code_gen_dir), export_idt, nbits) + inp = npy_to_rtlsim_input(f"{code_gen_dir}/input_0.npy", export_idt, nbits) self.reset_rtlsim(sim) if ( @@ -593,7 +582,7 @@ def execute_node(self, context, graph): if self.get_input_datatype(1) == DataType["BIPOLAR"]: export_wdt = DataType["BINARY"] - wei = npy_to_rtlsim_input("{}/input_1.npy".format(code_gen_dir), export_wdt, wnbits) + wei = npy_to_rtlsim_input(f"{code_gen_dir}/input_1.npy", export_wdt, wnbits) num_w_reps = np.prod(self.get_nodeattr("numInputVectors")) io_dict = { @@ -612,7 +601,7 @@ def execute_node(self, context, graph): odt = self.get_output_datatype() target_bits = odt.bitwidth() packed_bits = self.get_outstream_width() - out_npy_path = "{}/output_0.npy".format(code_gen_dir) + out_npy_path = f"{code_gen_dir}/output_0.npy" out_shape = self.get_folded_output_shape() rtlsim_output_to_npy(output, out_npy_path, odt, out_shape, packed_bits, target_bits) @@ -623,10 +612,8 @@ def execute_node(self, context, graph): context[node.output[0]] = output else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) def minimize_weight_bit_width(self, model): diff --git a/src/finn/custom_op/fpgadataflow/hls/outer_shuffle_hls.py b/src/finn/custom_op/fpgadataflow/hls/outer_shuffle_hls.py index eda773cbec..b1f2781a76 100644 --- a/src/finn/custom_op/fpgadataflow/hls/outer_shuffle_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/outer_shuffle_hls.py @@ -9,15 +9,13 @@ import math import numpy as np -from typing import Optional from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.outer_shuffle import OuterShuffle -def auto_size_simd(I_dim: int, SIMD: int) -> Optional[int]: - """ - Return the smallest divisor d of I_dim such that d > SIMD. +def auto_size_simd(I_dim: int, SIMD: int) -> int | None: + """Return the smallest divisor d of I_dim such that d > SIMD. if no such divisor exists, return None. """ if I_dim <= 0: @@ -26,7 +24,7 @@ def auto_size_simd(I_dim: int, SIMD: int) -> Optional[int]: raise ValueError("SIMD must be a non-negative integer") candidates = [] - limit = int(math.isqrt(I_dim)) + limit = math.isqrt(I_dim) for a in range(1, limit + 1): if I_dim % a == 0: b = I_dim // a diff --git a/src/finn/custom_op/fpgadataflow/hls/pool_hls.py b/src/finn/custom_op/fpgadataflow/hls/pool_hls.py index 6315c4652e..ba77ecc518 100644 --- a/src/finn/custom_op/fpgadataflow/hls/pool_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/pool_hls.py @@ -42,7 +42,6 @@ class Pool_hls(Pool, HLSBackend): Output shape (BatchSize,OutImgDim,OutImgDim,Channels) Notes: - * The input shape was chosen to be compatible with im2col (only true when there is not folding). * The actual data layout produced by the hlslib kernels is different @@ -70,8 +69,8 @@ def defines(self, var): cf = int(self.get_nodeattr("Channels") / self.get_nodeattr("PE")) osz = np.prod(self.get_nodeattr("OutImgDims")) self.code_gen_dict["$DEFINES$"] = [ - "constexpr unsigned ISIZE = {};".format(osz * cf * k), - "constexpr unsigned K = {};".format(k), + f"constexpr unsigned ISIZE = {osz * cf * k};", + f"constexpr unsigned K = {k};", ] def docompute(self): @@ -84,9 +83,9 @@ def docompute(self): self.code_gen_dict["$DOCOMPUTE$"] = [] if fxn == "MaxPool": - self.code_gen_dict["$DOCOMPUTE$"] += ["MaxPoolFunction<{}> pool_fxn;".format(o_hls_dt)] + self.code_gen_dict["$DOCOMPUTE$"] += [f"MaxPoolFunction<{o_hls_dt}> pool_fxn;"] elif fxn == "AccPool": - self.code_gen_dict["$DOCOMPUTE$"] += ["AccPoolFunction<{}> pool_fxn;".format(o_hls_dt)] + self.code_gen_dict["$DOCOMPUTE$"] += [f"AccPoolFunction<{o_hls_dt}> pool_fxn;"] elif fxn == "AvgPool": n = np.prod(self.get_nodeattr("KernelSize")) accum_bits = self.get_nodeattr("AccumBits") @@ -96,7 +95,7 @@ def docompute(self): pe, ) self.code_gen_dict["$DOCOMPUTE$"] += [ - "AvgPoolFunction<{},{},{}> pool_fxn;".format(o_hls_dt, act_hls_dt, n) + f"AvgPoolFunction<{o_hls_dt},{act_hls_dt},{n}> pool_fxn;" ] elif fxn == "QuantAvgPool": shift = self.get_nodeattr("Size") @@ -107,7 +106,7 @@ def docompute(self): pe, ) self.code_gen_dict["$DOCOMPUTE$"] += [ - "QuantAvgPoolFunction<{},{},{}> pool_fxn;".format(o_hls_dt, act_hls_dt, shift) + f"QuantAvgPoolFunction<{o_hls_dt},{act_hls_dt},{shift}> pool_fxn;" ] else: raise Exception("Pool_Batch doesn't currently support " + fxn) diff --git a/src/finn/custom_op/fpgadataflow/hls/replicate_stream_hls.py b/src/finn/custom_op/fpgadataflow/hls/replicate_stream_hls.py index 101afffe19..72c8315eb8 100644 --- a/src/finn/custom_op/fpgadataflow/hls/replicate_stream_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/replicate_stream_hls.py @@ -34,7 +34,7 @@ def get_ap_int_max_w(self): # Find the widths of the widest output # Note: there is one output per replica o_bits_max = max( - (self.get_outstream_width(ind) for ind in range(self.num)) + self.get_outstream_width(ind) for ind in range(self.num) ) # Find the biggest of the inputs/outputs return max([i_bits_max, o_bits_max]) @@ -92,7 +92,7 @@ def out(i): # Write the same input element into each output stream *(f"{out(i)}.write(x);" for i in range(self.num)), # End of for-loop over repetitions body - f"}}" # noqa: f-string symmetry + "}" # noqa: f-string symmetry ] # Generates essentially the head of the C++ function from which the IP block @@ -136,9 +136,9 @@ def pragmas(self): # Returns the names of input and output interfaces grouped by protocol def get_verilog_top_module_intf_names(self): - # Start collecting interface names in a dictionary # noqa Duplicate + # Start collecting interface names in a dictionary # starting with clock and reset - intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # noqa + intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # AXI stream input interfaces intf_names["s_axis"] = [ # Just one input stream diff --git a/src/finn/custom_op/fpgadataflow/hls/requant_hls.py b/src/finn/custom_op/fpgadataflow/hls/requant_hls.py index 7d5ce571c0..a8c90fb452 100644 --- a/src/finn/custom_op/fpgadataflow/hls/requant_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/requant_hls.py @@ -226,7 +226,7 @@ def strm_decl(self): def dataoutstrm(self): """Generate code for writing output data to .npy file.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") - shape = f"{{{','.join((str(i) for i in self.get_folded_output_shape(0)))}}}" + shape = f"{{{','.join(str(i) for i in self.get_folded_output_shape(0))}}}" odt = self.get_output_datatype() elem_hls_type = odt.get_hls_datatype_str() npy_type = "half" if elem_hls_type == "half" else "float" diff --git a/src/finn/custom_op/fpgadataflow/hls/split_hls.py b/src/finn/custom_op/fpgadataflow/hls/split_hls.py index 12507aad68..6bdae1f304 100644 --- a/src/finn/custom_op/fpgadataflow/hls/split_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/split_hls.py @@ -88,7 +88,7 @@ def pragmas(self): def timeout_condition(self): condition = [] for i in range(self.get_n_outputs()): - condition.append("out{}_V.empty()".format(i)) + condition.append(f"out{i}_V.empty()") condition = " && ".join(condition) self.code_gen_dict["$TIMEOUT_CONDITION$"] = [condition] diff --git a/src/finn/custom_op/fpgadataflow/hls/squeeze_hls.py b/src/finn/custom_op/fpgadataflow/hls/squeeze_hls.py index 19c4f4b9c3..e21a810137 100644 --- a/src/finn/custom_op/fpgadataflow/hls/squeeze_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/squeeze_hls.py @@ -21,7 +21,7 @@ # HLS Backend specialization of the squeeze operator @register_custom_op -class Squeeze_hls(Squeeze, HLSBackend): # noqa: N801 +class Squeeze_hls(Squeeze, HLSBackend): """HLS backend implementation of the Squeeze operator. Removes single-dimension entries from the shape of a tensor using HLS synthesis. diff --git a/src/finn/custom_op/fpgadataflow/hls/streamingdatawidthconverter_hls.py b/src/finn/custom_op/fpgadataflow/hls/streamingdatawidthconverter_hls.py index 12085423ef..b1ea6ab510 100644 --- a/src/finn/custom_op/fpgadataflow/hls/streamingdatawidthconverter_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/streamingdatawidthconverter_hls.py @@ -69,10 +69,10 @@ def defines(self, var): def strm_decl(self): self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in0_V ("in0_V");'.format(self.get_instream_width()) + f'hls::stream> in0_V ("in0_V");' ) self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> out0_V ("out0_V");'.format(self.get_outstream_width()) + f'hls::stream> out0_V ("out0_V");' ) def docompute(self): @@ -80,9 +80,7 @@ def docompute(self): op = "StreamingDataWidthConverter_Batch" if self.needs_lcm(): self.code_gen_dict["$DOCOMPUTE$"] = [ - 'hls::stream> intermediate ("intermediate");'.format( - self.get_iowidth_lcm() - ), + f'hls::stream> intermediate ("intermediate");', "%s(in0_V, intermediate, numReps);" % op, "%s(intermediate, out0_V, numReps);" % op, ] diff --git a/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py b/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py index e772fdef0a..7c32da57b5 100644 --- a/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py @@ -61,14 +61,10 @@ def defines(self, var): def strm_decl(self): self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in0_{} ("in0_{}");'.format( - self.get_instream_width(), self.hls_sname(), self.hls_sname() - ) + f'hls::stream> in0_{self.hls_sname()} ("in0_{self.hls_sname()}");' ) self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> out0_{} ("out0_{}");'.format( - self.get_outstream_width(), self.hls_sname(), self.hls_sname() - ) + f'hls::stream> out0_{self.hls_sname()} ("out0_{self.hls_sname()}");' ) def docompute(self): @@ -142,10 +138,8 @@ def execute_node(self, context, graph): code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) inp = context[node.input[0]] @@ -173,14 +167,14 @@ def execute_node(self, context, graph): sim = self.get_rtlsim() nbits = self.get_instream_width() rtlsim_inp = npy_to_rtlsim_input( - "{}/input_0.npy".format(code_gen_dir), export_idt, nbits + f"{code_gen_dir}/input_0.npy", export_idt, nbits ) super().reset_rtlsim(sim) rtlsim_output = self.rtlsim(sim, rtlsim_inp) odt = export_idt target_bits = odt.bitwidth() packed_bits = self.get_outstream_width() - out_npy_path = "{}/output.npy".format(code_gen_dir) + out_npy_path = f"{code_gen_dir}/output.npy" out_shape = self.get_folded_output_shape() rtlsim_output_to_npy( rtlsim_output, out_npy_path, odt, out_shape, packed_bits, target_bits @@ -191,10 +185,8 @@ def execute_node(self, context, graph): context[node.output[0]] = output else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to "rtlsim" """.format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to "rtlsim" """ ) # binary -> bipolar if needed if self.get_output_datatype() == DataType["BIPOLAR"]: diff --git a/src/finn/custom_op/fpgadataflow/hls/unsqueeze_hls.py b/src/finn/custom_op/fpgadataflow/hls/unsqueeze_hls.py index 5ef6f8fb56..4ed5590801 100644 --- a/src/finn/custom_op/fpgadataflow/hls/unsqueeze_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/unsqueeze_hls.py @@ -21,7 +21,7 @@ # HLS Backend specialization of the unsqueeze operator @register_custom_op -class Unsqueeze_hls(Unsqueeze, HLSBackend): # noqa: N801 +class Unsqueeze_hls(Unsqueeze, HLSBackend): """HLS backend implementation of the Unsqueeze operator. Inserts single-dimension entries into the shape of a tensor using HLS synthesis. diff --git a/src/finn/custom_op/fpgadataflow/hls/upsampler_hls.py b/src/finn/custom_op/fpgadataflow/hls/upsampler_hls.py index f1b991501d..53dca68359 100644 --- a/src/finn/custom_op/fpgadataflow/hls/upsampler_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/upsampler_hls.py @@ -31,8 +31,7 @@ class UpsampleNearestNeighbour_hls(UpsampleNearestNeighbour, HLSBackend): - """ - Corresponds to finn-hlslib UpsampleNearestNeighbour function. + """Corresponds to finn-hlslib UpsampleNearestNeighbour function. Upsampling is done with the Nearest Neighbour algorithm. The layer expects square feature maps for the in and output. """ @@ -53,21 +52,21 @@ def defines(self, var): self.code_gen_dict["$DEFINES$"] = [] HI = self.get_nodeattr("HI") - self.code_gen_dict["$DEFINES$"] += ["#define HI {}".format(HI)] + self.code_gen_dict["$DEFINES$"] += [f"#define HI {HI}"] WI = self.get_nodeattr("WI") - self.code_gen_dict["$DEFINES$"] += ["#define WI {}".format(WI)] + self.code_gen_dict["$DEFINES$"] += [f"#define WI {WI}"] HO = self.get_nodeattr("HO") - self.code_gen_dict["$DEFINES$"] += ["#define HO {}".format(HO)] + self.code_gen_dict["$DEFINES$"] += [f"#define HO {HO}"] WO = self.get_nodeattr("WO") - self.code_gen_dict["$DEFINES$"] += ["#define WO {}".format(WO)] + self.code_gen_dict["$DEFINES$"] += [f"#define WO {WO}"] SIMD = self.get_nodeattr("SIMD") CF = self.get_nodeattr("NumChannels") // SIMD - self.code_gen_dict["$DEFINES$"] += ["#define CF {}".format(CF)] + self.code_gen_dict["$DEFINES$"] += [f"#define CF {CF}"] def docompute(self): self.code_gen_dict["$DOCOMPUTE$"] = [ diff --git a/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py b/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py index 3b6a6ffab7..89914015bd 100644 --- a/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py @@ -139,10 +139,8 @@ def execute_node(self, context, graph): code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) # create a npy file fore each input of the node (in_ind is input index) @@ -167,7 +165,7 @@ def execute_node(self, context, graph): # make copy before saving the array reshaped_input = reshaped_input.copy() np.save( - os.path.join(code_gen_dir, "input_{}.npy".format(in_ind)), + os.path.join(code_gen_dir, f"input_{in_ind}.npy"), reshaped_input, ) elif in_ind > 2: @@ -190,7 +188,7 @@ def execute_node(self, context, graph): elif mode == "rtlsim": sim = self.get_rtlsim() nbits = self.get_instream_width(0) - inp = npy_to_rtlsim_input("{}/input_0.npy".format(code_gen_dir), export_idt, nbits) + inp = npy_to_rtlsim_input(f"{code_gen_dir}/input_0.npy", export_idt, nbits) super().reset_rtlsim(sim) if mem_mode == "external" or mem_mode == "internal_decoupled": @@ -200,7 +198,7 @@ def execute_node(self, context, graph): # so use it as such for weight generation if self.get_input_datatype(1) == DataType["BIPOLAR"]: export_wdt = DataType["BINARY"] - wei = npy_to_rtlsim_input("{}/weights.npy".format(code_gen_dir), export_wdt, wnbits) + wei = npy_to_rtlsim_input(f"{code_gen_dir}/weights.npy", export_wdt, wnbits) dim_h, dim_w = self.get_nodeattr("Dim") num_w_reps = dim_h * dim_w @@ -219,7 +217,7 @@ def execute_node(self, context, graph): odt = self.get_output_datatype() target_bits = odt.bitwidth() packed_bits = self.get_outstream_width() - out_npy_path = "{}/output_0.npy".format(code_gen_dir) + out_npy_path = f"{code_gen_dir}/output_0.npy" out_shape = self.get_folded_output_shape() rtlsim_output_to_npy(output, out_npy_path, odt, out_shape, packed_bits, target_bits) @@ -230,10 +228,8 @@ def execute_node(self, context, graph): context[node.output[0]] = output else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) def code_generation_ipgen(self, model, fpgapart, clk): @@ -319,7 +315,7 @@ def defines(self, var): ] if mem_mode == "internal_decoupled" or mem_mode == "external": wdt = self.get_input_datatype(1) - self.code_gen_dict["$DEFINES$"].append("#define WP1 {}\n".format(wdt.bitwidth())) + self.code_gen_dict["$DEFINES$"].append(f"#define WP1 {wdt.bitwidth()}\n") def read_npy_data(self): code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") @@ -371,14 +367,14 @@ def strm_decl(self): mem_mode = self.get_nodeattr("mem_mode") self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in0_V ("in0_V");'.format(self.get_instream_width(0)) + f'hls::stream> in0_V ("in0_V");' ) self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> out0_V ("out0_V");'.format(self.get_outstream_width()) + f'hls::stream> out0_V ("out0_V");' ) if mem_mode == "internal_decoupled" or mem_mode == "external": self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in1_V ("in1_V");'.format(self.get_instream_width(1)) + f'hls::stream> in1_V ("in1_V");' ) def docompute(self): @@ -466,26 +462,17 @@ def blackboxfunction(self): mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "internal_embedded": self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """void {}(hls::stream> &in0_V, - hls::stream> &out0_V - )""".format( - self.onnx_node.name, - self.get_instream_width(0), - self.get_outstream_width(), - ) + f"""void {self.onnx_node.name}(hls::stream> &in0_V, + hls::stream> &out0_V + )""" ] elif mem_mode == "internal_decoupled" or mem_mode == "external": self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """void {}( - hls::stream> &in0_V, - hls::stream> &in1_V, - hls::stream> &out0_V - )""".format( - self.onnx_node.name, - self.get_instream_width(0), - self.get_instream_width(1), - self.get_outstream_width(), - ) + f"""void {self.onnx_node.name}( + hls::stream> &in0_V, + hls::stream> &in1_V, + hls::stream> &out0_V + )""" ] else: raise Exception( @@ -504,7 +491,7 @@ def pragmas(self): # the weight tensor is ap_uint [PE][WMEM] # partition for parallel access along the PE dimension (dim 1) self.code_gen_dict["$PRAGMAS$"].append( - ("#pragma HLS ARRAY_PARTITION variable=weights.m_weights " "complete dim=1") + "#pragma HLS ARRAY_PARTITION variable=weights.m_weights complete dim=1" ) elif mem_mode == "internal_decoupled" or mem_mode == "external": self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE axis port=in1_V") @@ -517,10 +504,10 @@ def pragmas(self): if self.calc_tmem() != 0: # TODO find a better way of checking for no pregenerated thresholds self.code_gen_dict["$PRAGMAS$"].append( - ("#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds " "complete dim=1") + "#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds complete dim=1" ) self.code_gen_dict["$PRAGMAS$"].append( - ("#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds " "complete dim=3") + "#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds complete dim=3" ) def minimize_weight_bit_width(self, model): diff --git a/src/finn/custom_op/fpgadataflow/hlsbackend.py b/src/finn/custom_op/fpgadataflow/hlsbackend.py index 09992523cd..f52f62554f 100644 --- a/src/finn/custom_op/fpgadataflow/hlsbackend.py +++ b/src/finn/custom_op/fpgadataflow/hlsbackend.py @@ -72,16 +72,13 @@ def get_nodeattr_types(self): def get_all_verilog_paths(self): """Return list of all folders containing Verilog code for this node.""" - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") assert ( code_gen_dir != "" ), """Node attribute "code_gen_dir_ipgen" is not set. Please run HLSSynthIP first.""" - verilog_path = "{}/project_{}/sol1/impl/verilog/".format(code_gen_dir, self.onnx_node.name) - subcore_verilog_path = "{}/project_{}/sol1/impl/ip/hdl/ip/".format( - code_gen_dir, self.onnx_node.name - ) + verilog_path = f"{code_gen_dir}/project_{self.onnx_node.name}/sol1/impl/verilog/" + subcore_verilog_path = f"{code_gen_dir}/project_{self.onnx_node.name}/sol1/impl/ip/hdl/ip/" # default impl only returns the HLS verilog codegen dir and subcore (impl/ip/hdl/ip) dir # if it exists ret = [verilog_path] @@ -91,7 +88,6 @@ def get_all_verilog_paths(self): def get_all_verilog_filenames(self, abspath=False): """Return list of all Verilog files used for this node.""" - verilog_files = [] verilog_paths = self.get_all_verilog_paths() for verilog_path in verilog_paths: @@ -106,7 +102,6 @@ def get_all_verilog_filenames(self, abspath=False): def prepare_rtlsim(self, behav=False): """Creates a xsi emulation library for the RTL code generated for this node, sets the rtlsim_so attribute to its path.""" - verilog_files = self.get_all_verilog_filenames(abspath=True) single_src_dir = make_build_dir("rtlsim_" + self.onnx_node.name + "_") trace_file = self.get_nodeattr("rtlsim_trace") @@ -138,7 +133,7 @@ def code_generation_ipgen(self, model, fpgapart, clk): code_gen_line = "\n".join(self.code_gen_dict[key]) template = template.replace(key, code_gen_line) code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - f = open(os.path.join(code_gen_dir, "top_{}.cpp".format(node.name)), "w") + f = open(os.path.join(code_gen_dir, f"top_{node.name}.cpp"), "w") f.write(template) f.close() self.code_gen_dict.clear() @@ -151,7 +146,7 @@ def code_generation_ipgen(self, model, fpgapart, clk): ) # generate tcl script for ip generation - self.code_gen_dict["$PROJECTNAME$"] = ["project_{}".format(node.name)] + self.code_gen_dict["$PROJECTNAME$"] = [f"project_{node.name}"] self.code_gen_dict["$HWSRCDIR$"] = [code_gen_dir] self.code_gen_dict["$FPGAPART$"] = [fpgapart] self.code_gen_dict["$TOPFXN$"] = [node.name] @@ -170,14 +165,13 @@ def code_generation_ipgen(self, model, fpgapart, clk): code_gen_line = "\n".join(self.code_gen_dict[key]) template = template.replace(key, code_gen_line) code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - f = open(os.path.join(code_gen_dir, "hls_syn_{}.tcl".format(node.name)), "w") + f = open(os.path.join(code_gen_dir, f"hls_syn_{node.name}.tcl"), "w") f.write(template) f.close() self.code_gen_dict.clear() def ipgen_default_directives(self): """Return list of default HLS synthesis directives.""" - default_directives = [ "set_param hls.enable_hidden_option_error false", "config_compile -disable_unroll_code_size_check -pipeline_style flp", @@ -216,7 +210,7 @@ def ipgen_singlenode_code(self, fpgapart=None): is_port_conflict = False xcd_log_path = os.path.join(ipgen_path, "sol1", ".autopilot", "xcd.log") if os.path.isfile(xcd_log_path): - with open(xcd_log_path, "r") as xcd_log: + with open(xcd_log_path) as xcd_log: for line in xcd_log: if "Address already in use" in line: is_port_conflict = True @@ -264,7 +258,7 @@ def code_generation_cppsim(self, model): code_gen_line = "\n".join(self.code_gen_dict[key]) template = template.replace(key, code_gen_line) code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") - f = open(os.path.join(code_gen_dir, "execute_{}.cpp".format(node.op_type)), "w") + f = open(os.path.join(code_gen_dir, f"execute_{node.op_type}.cpp"), "w") f.write(template) f.close() self.code_gen_dict.clear() @@ -310,7 +304,7 @@ def npy_to_dynamic_output(self, context): node = self.onnx_node code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") for o, outp in enumerate(node.output): - output = np.load("{}/output_{}.npy".format(code_gen_dir, o)) + output = np.load(f"{code_gen_dir}/output_{o}.npy") exp_shape = self.get_normal_output_shape(o) context[outp] = output.reshape(exp_shape) @@ -347,10 +341,8 @@ def execute_node(self, context, graph): code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) inputs = {} for i, inp in enumerate(node.input): @@ -387,7 +379,7 @@ def execute_node(self, context, graph): np.save(os.path.join(code_gen_dir, "input_%s.npy" % i), reshaped_input) # The rtlsim will instead operate on a flattened int sequence from an "io_dict" rtlsim_inp = npy_to_rtlsim_input( - "{}/input_{}.npy".format(code_gen_dir, i), export_idt, nbits + f"{code_gen_dir}/input_{i}.npy", export_idt, nbits ) inputs["in%s" % i] = rtlsim_inp @@ -422,7 +414,7 @@ def execute_node(self, context, graph): odt = self.get_output_datatype(o) target_bits = odt.bitwidth() packed_bits = self.get_outstream_width(o) - out_npy_path = "{}/output_{}.npy".format(code_gen_dir, o) + out_npy_path = f"{code_gen_dir}/output_{o}.npy" out_shape = self.get_folded_output_shape(o) rtlsim_output_to_npy( rtlsim_output, out_npy_path, odt, out_shape, packed_bits, target_bits @@ -439,10 +431,8 @@ def execute_node(self, context, graph): else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) @abstractmethod @@ -450,7 +440,6 @@ def global_includes(self): """Function to set the global includes for c++ code that has to be generated for cppsim or rtlsim, is member function of HLSBackend class but has to be filled by every node.""" - pass @abstractmethod def defines(self, var): @@ -461,7 +450,6 @@ def defines(self, var): var: makes it possible to reuse the function for different c++ code generation. I.e. if set to "ipgen" in MatrixVectorActivation additional PRAGMA defines are added.""" - pass def read_npy_data(self): """Generate commands for reading data from .npy file in C++. @@ -521,16 +509,12 @@ def strm_decl(self): for i, inp in enumerate(node.input): if self.get_instream_width(i): self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in{}_V ("in{}_V");'.format( - self.get_instream_width(i), i, i - ) + f'hls::stream> in{i}_V ("in{i}_V");' ) for o, outp in enumerate(node.output): if self.get_outstream_width(o): self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> out{}_V ("out{}_V");'.format( - self.get_outstream_width(o), o, o - ) + f'hls::stream> out{o}_V ("out{o}_V");' ) else: for i, inp in enumerate(node.input): @@ -542,9 +526,7 @@ def strm_decl(self): elem_input_hls_type = dtype.get_hls_datatype_str() self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> in{}_V ("in{}_V");'.format( - elem_input_hls_type, self.get_folded_input_shape(i)[-1], i, i - ) + f'hls::stream> in{i}_V ("in{i}_V");' ) for o, outp in enumerate(node.output): @@ -556,18 +538,14 @@ def strm_decl(self): elem_output_hls_type = dtype.get_hls_datatype_str() self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> out{}_V ("out{}_V");'.format( - elem_output_hls_type, self.get_folded_output_shape(o)[-1], o, o - ) + f'hls::stream> out{o}_V ("out{o}_V");' ) if self.get_nodeattr("hls_style") == "freerunning": for o, outp in enumerate(node.output): if self.get_outstream_width(o): self.code_gen_dict["$STREAMDECLARATIONS$"].append( - 'hls::stream> strm{} ("strm{}");'.format( - elem_output_hls_type, self.get_folded_output_shape(o)[-1], o, o - ) + f'hls::stream> strm{o} ("strm{o}");' ) @abstractmethod @@ -575,7 +553,6 @@ def docompute(self): """Function to generate the commands for the computational part of the c++ code, is member function of HLSBackend class but has to be filled by every node.""" - pass def dataoutstrm(self): """Generate commands for reading out data from C++ and converting to npy format. @@ -639,7 +616,6 @@ def blackboxfunction(self): """Function to generate a blackbock function in c++ from which an IP block will be generated, is member function of HLSBackend class but has to be filled by every node.""" - pass def pragmas(self): """Generate pragma commands in C++. diff --git a/src/finn/custom_op/fpgadataflow/inner_shuffle.py b/src/finn/custom_op/fpgadataflow/inner_shuffle.py index 921e401d11..02bc9ef03b 100644 --- a/src/finn/custom_op/fpgadataflow/inner_shuffle.py +++ b/src/finn/custom_op/fpgadataflow/inner_shuffle.py @@ -56,7 +56,7 @@ def infer_node_datatype(self, model): dt = model.get_tensor_datatype(node.input[0]) if dt != self.get_input_datatype(): warn_str = ( - f"data_type changing for {node.name}: {str(self.get_input_datatype())} -> {str(dt)}" + f"data_type changing for {node.name}: {self.get_input_datatype()!s} -> {dt!s}" ) log.warning(warn_str) self.set_nodeattr("data_type", dt.name) diff --git a/src/finn/custom_op/fpgadataflow/layernorm.py b/src/finn/custom_op/fpgadataflow/layernorm.py index caead4e8f0..dc66a7bbe1 100644 --- a/src/finn/custom_op/fpgadataflow/layernorm.py +++ b/src/finn/custom_op/fpgadataflow/layernorm.py @@ -73,8 +73,7 @@ def get_input_datatype(self, ind=0): """Returns FINN DataType of input.""" if ind == 0: return DataType[self.get_nodeattr("inputDataType")] - else: - raise Exception("Undefined input ind for this layer type") + raise Exception("Undefined input ind for this layer type") def get_output_datatype(self, ind=0): """Returns FINN DataType of output.""" diff --git a/src/finn/custom_op/fpgadataflow/lookup.py b/src/finn/custom_op/fpgadataflow/lookup.py index d36feb6d3d..3394a2b5bf 100644 --- a/src/finn/custom_op/fpgadataflow/lookup.py +++ b/src/finn/custom_op/fpgadataflow/lookup.py @@ -75,10 +75,9 @@ def get_exp_cycles(self): def get_normal_input_shape(self, ind=0): if ind == 0: return self.get_nodeattr("InputShape") - elif ind == 1: + if ind == 1: return tuple([self.get_nodeattr("NumEmbeddings"), self.get_nodeattr("EmbeddingDim")]) - else: - raise Exception("Undefined input ind for this layer type") + raise Exception("Undefined input ind for this layer type") def get_normal_output_shape(self, ind=0): ishape = self.get_normal_input_shape() @@ -196,9 +195,8 @@ def bram_estimation(self): width_factor = ceil(self.get_outstream_width() / 16) depth_factor = ceil(self.get_nodeattr("NumEmbeddings") / 1024) return width_factor * depth_factor - else: - # TODO can we estimate BRAMs for the DMA engine? - return 0 + # TODO can we estimate BRAMs for the DMA engine? + return 0 def bram_efficiency_estimation(self): bram16_est = self.bram_estimation() diff --git a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py index 69969395c0..e51d5b276c 100644 --- a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py @@ -256,10 +256,8 @@ def verify_node(self): ) else: info_messages.append( - """noActivation attribute contains {} should - be 0 or 1""".format( - no_act - ) + f"""noActivation attribute contains {no_act} should + be 0 or 1""" ) return info_messages @@ -291,10 +289,9 @@ def get_input_datatype(self, ind=0): # parameter can be > 0 (referring to the weights) so handle that here if ind == 0: return DataType[self.get_nodeattr("inputDataType")] - elif ind == 1: + if ind == 1: return DataType[self.get_nodeattr("weightDataType")] - else: - raise Exception("Undefined input ind for this layer type") + raise Exception("Undefined input ind for this layer type") def get_accumulator_datatype(self): """Returns FINN DataType of accumulator""" @@ -480,10 +477,9 @@ def calc_tmem(self): """Calculates and returns TMEM.""" if self.get_nodeattr("noActivation") == 1: return 0 - else: - mh = self.get_nodeattr("MH") - pe = self.get_nodeattr("PE") - return mh // pe + mh = self.get_nodeattr("MH") + pe = self.get_nodeattr("PE") + return mh // pe def uram_estimation(self): """Estimate UltraRAM (URAM) resource usage. @@ -545,16 +541,15 @@ def bram_estimation(self): # which is more efficient than internal_embedded (HLS) if mem_width == 1: return math.ceil(omega / 16384) - elif mem_width == 2: + if mem_width == 2: return math.ceil(omega / 8192) - elif mem_width <= 4: + if mem_width <= 4: return (math.ceil(omega / 4096)) * (math.ceil(mem_width / 4)) - elif mem_width <= 9: + if mem_width <= 9: return (math.ceil(omega / 2048)) * (math.ceil(mem_width / 9)) - elif mem_width <= 18 or omega > 512: + if mem_width <= 18 or omega > 512: return (math.ceil(omega / 1024)) * (math.ceil(mem_width / 18)) - else: - return (math.ceil(omega / 512)) * (math.ceil(mem_width / 36)) + return (math.ceil(omega / 512)) * (math.ceil(mem_width / 36)) def bram_efficiency_estimation(self): """Estimate BRAM utilization efficiency. @@ -783,7 +778,6 @@ def make_weight_file(self, weights, weight_file_mode, weight_file_name): of weights. Arguments: - * weights : numpy array with weights to be put into the file * weight_file_mode : one of {hls_header, decoupled_verilog_dat, decoupled_runtime} @@ -917,16 +911,16 @@ def generate_params(self, model, path): if weights is not None: if mem_mode == "internal_embedded": # save hlslib-compatible weights in params.h - weight_filename = "{}/params.h".format(code_gen_dir) + weight_filename = f"{code_gen_dir}/params.h" self.make_weight_file(weights, "hls_header", weight_filename) elif mem_mode == "internal_decoupled" or mem_mode == "external": - weight_filename_sim = "{}/input_1.npy".format(code_gen_dir) + weight_filename_sim = f"{code_gen_dir}/input_1.npy" # save internal_decoupled weights for cppsim self.make_weight_file(weights, "decoupled_npy", weight_filename_sim) if mem_mode == "internal_decoupled": # also save weights as Verilog .dat file # This file will be ignored when synthesizing UltraScale memory. - weight_filename_rtl = "{}/memblock.dat".format(code_gen_dir) + weight_filename_rtl = f"{code_gen_dir}/memblock.dat" self.make_weight_file(weights, "decoupled_verilog_dat", weight_filename_rtl) else: if not ( @@ -965,7 +959,7 @@ def generate_params(self, model, path): threshold_tensor, tdt, "thresholds", False, True ) # write thresholds into thresh.h - f_thresh = open("{}/thresh.h".format(code_gen_dir), "w") + f_thresh = open(f"{code_gen_dir}/thresh.h", "w") tdt_hls = tdt.get_hls_datatype_str() # use binary to export bipolar activations export_odt = self.get_output_datatype() diff --git a/src/finn/custom_op/fpgadataflow/outer_shuffle.py b/src/finn/custom_op/fpgadataflow/outer_shuffle.py index 4ae003ffd4..82afef146c 100644 --- a/src/finn/custom_op/fpgadataflow/outer_shuffle.py +++ b/src/finn/custom_op/fpgadataflow/outer_shuffle.py @@ -54,9 +54,8 @@ def tick(self): fp_inc = self.W - self._fp_rewind self.cnt = self.N - 2 return rp_inc, fp_inc, True - else: - self.cnt -= 1 - return rp_inc, fp_inc, False + self.cnt -= 1 + return rp_inc, fp_inc, False return rp_inc, fp_inc, False @@ -107,7 +106,7 @@ def infer_node_datatype(self, model): dt = model.get_tensor_datatype(node.input[0]) if dt != self.get_input_datatype(): warn_str = ( - f"data_type changing for {node.name}: {str(self.get_input_datatype())} -> {str(dt)}" + f"data_type changing for {node.name}: {self.get_input_datatype()!s} -> {dt!s}" ) log.warning(warn_str) self.set_nodeattr("data_type", dt.name) diff --git a/src/finn/custom_op/fpgadataflow/pool.py b/src/finn/custom_op/fpgadataflow/pool.py index b83b0a3626..11c795cf19 100644 --- a/src/finn/custom_op/fpgadataflow/pool.py +++ b/src/finn/custom_op/fpgadataflow/pool.py @@ -41,7 +41,6 @@ class Pool(HWCustomOp): Output shape (BatchSize,OutImgDim,OutImgDim,Channels) Notes: - * The input shape was chosen to be compatible with im2col (only true when there is not folding). * The actual data layout produced by the hlslib kernels is different @@ -85,9 +84,7 @@ def get_output_datatype(self, ind=0): # Same as input idt = DataType[self.get_nodeattr("InputDataType")] assert odt == idt, "In datatype must be equal to out datatype for Maxpool" - elif fxn == "AccPool": - pass - elif fxn == "AvgPool": + elif fxn == "AccPool" or fxn == "AvgPool": pass elif fxn == "QuantAvgPool": idt = DataType[self.get_nodeattr("InputDataType")] diff --git a/src/finn/custom_op/fpgadataflow/replicate_stream.py b/src/finn/custom_op/fpgadataflow/replicate_stream.py index 86f2eebf0e..eeadf3d09f 100644 --- a/src/finn/custom_op/fpgadataflow/replicate_stream.py +++ b/src/finn/custom_op/fpgadataflow/replicate_stream.py @@ -111,7 +111,7 @@ def make_shape_compatible_op(self, model: ModelWrapper): # noqa # Infers the datatype of the node output def infer_node_datatype(self, model: ModelWrapper): # noqa - # Get the node wrapped by this custom op # noqa Duplicate + # Get the node wrapped by this custom op node = self.onnx_node # Test for changing input datatype if model.get_tensor_datatype(node.input[0]) != self.dtype: @@ -229,8 +229,7 @@ def get_number_output_values(self): num_outputs_per_stream = np.prod(self.get_folded_output_shape()[:-1]) if self.num > 1: return {f"out{i}": num_outputs_per_stream for i in range(self.num)} - else: - return num_outputs_per_stream + return num_outputs_per_stream # Derives the expected cycles for the stream replication operation given the # folding configuration diff --git a/src/finn/custom_op/fpgadataflow/requant.py b/src/finn/custom_op/fpgadataflow/requant.py index 5c77145d7f..4d26bb0f96 100644 --- a/src/finn/custom_op/fpgadataflow/requant.py +++ b/src/finn/custom_op/fpgadataflow/requant.py @@ -96,9 +96,8 @@ def get_input_datatype(self, ind=0): """Returns FINN DataType of input.""" if ind == 0: return DataType[self.get_nodeattr("inputDataType")] - else: - # Scale and bias are float - return DataType["FLOAT32"] + # Scale and bias are float + return DataType["FLOAT32"] def get_output_datatype(self, ind=0): """Returns FINN DataType of output.""" @@ -122,8 +121,7 @@ def get_folded_input_shape(self, ind=0): num_channels = self.get_nodeattr("NumChannels") fold = num_channels // pe return tuple(list(normal_shape[:-1]) + [fold, pe]) - else: - return self.get_normal_input_shape(ind) + return self.get_normal_input_shape(ind) def get_folded_output_shape(self, ind=0): """Returns folded output shape.""" @@ -172,9 +170,8 @@ def get_instream_width(self, ind=0): pe = self.get_nodeattr("PE") idt = self.get_input_datatype(0) return pe * idt.bitwidth() - else: - # Scale and bias (inputs 1, 2) are embedded, not streamed - return 0 + # Scale and bias (inputs 1, 2) are embedded, not streamed + return 0 def get_outstream_width(self, ind=0): """Returns output stream width.""" diff --git a/src/finn/custom_op/fpgadataflow/reshape.py b/src/finn/custom_op/fpgadataflow/reshape.py index 0bced6f594..24cffc5f64 100644 --- a/src/finn/custom_op/fpgadataflow/reshape.py +++ b/src/finn/custom_op/fpgadataflow/reshape.py @@ -133,7 +133,7 @@ def infer_node_datatype(self, model: ModelWrapper): # Get the new datatype new_dtype = model.get_tensor_datatype(node.input[0]) # Issue a warning message - log.warning(f"{node.name}: inp_dtype changing from" f" {self.dtype} to {new_dtype}") + log.warning(f"{node.name}: inp_dtype changing from {self.dtype} to {new_dtype}") # Set the new datatype attribute self.set_nodeattr("dtype", new_dtype.name) # Force the output data type stored as a node attribute diff --git a/src/finn/custom_op/fpgadataflow/rtl/convolutioninputgenerator_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/convolutioninputgenerator_rtl.py index 19c12138c0..24681ca1a4 100755 --- a/src/finn/custom_op/fpgadataflow/rtl/convolutioninputgenerator_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/convolutioninputgenerator_rtl.py @@ -280,8 +280,7 @@ def bram_estimation(self): cascade_savings = ram_cascade_width - remainder_cascade_width return int((ram_cascade_depth * ram_cascade_width - cascade_savings) * buffer_count) - else: - return 0 + return 0 def lut_estimation(self): """Estimate LUT resource usage. @@ -333,8 +332,7 @@ def uram_estimation(self): ram_cascade_depth = math.ceil(buffer_depth / ram_depth) ram_cascade_width = math.ceil(buffer_width / ram_width) return int(ram_cascade_depth * ram_cascade_width * buffer_count) - else: - return 0 + return 0 def execute_node(self, context, graph): """Execute this ConvolutionInputGenerator node. @@ -769,15 +767,9 @@ def prepare_codegen_parallel(self): for fifo_idx, access_idx in enumerate(reg_fifo): if access_idx != -1: code_gen_dict["$GENERATE_OUTPUT_MAPPING$"].append( - """assign data_out[OUT_ELEM_WIDTH*{out_idx}+:OUT_ELEM_WIDTH] - = reg_fifo_{fifo_id}[{access_idx}*{mmv}*OUT_ELEM_WIDTH+ - OUT_ELEM_WIDTH*{mmv_idx}+:OUT_ELEM_WIDTH];""".format( - out_idx=out_idx, - fifo_id=fifo_id, - access_idx=len(reg_fifo) - 1 - int((max(reg_fifo) - access_idx) / M), - mmv_idx=(max(reg_fifo) - access_idx) % M, - mmv=M, - ) + f"""assign data_out[OUT_ELEM_WIDTH*{out_idx}+:OUT_ELEM_WIDTH] + = reg_fifo_{fifo_id}[{len(reg_fifo) - 1 - int((max(reg_fifo) - access_idx) / M)}*{M}*OUT_ELEM_WIDTH+ + OUT_ELEM_WIDTH*{(max(reg_fifo) - access_idx) % M}+:OUT_ELEM_WIDTH];""" ) # reversal: out_idx=0 -> oldest buffer element -> highest access_idx out_idx = out_idx - 1 @@ -788,26 +780,20 @@ def prepare_codegen_parallel(self): if i == 0: # first FIFO containing newest elements -> input comes from input reg code_gen_dict["$GENERATE_BUFFER_CONNECTION$"].append( - """assign reg_fifo_{fifo_id}_in = data_in;""".format( - fifo_id=i, - ) + f"""assign reg_fifo_{i}_in = data_in;""" ) else: # other REG FIFOs -> input comes from connected BRAM FIFO (line buffer) input_fifo_id = i - 1 code_gen_dict["$GENERATE_BUFFER_CONNECTION$"].append( - """assign reg_fifo_{fifo_id}_in = bram_fifo_{input_fifo_id}_out; - """.format( - fifo_id=i, input_fifo_id=input_fifo_id - ) + f"""assign reg_fifo_{i}_in = bram_fifo_{input_fifo_id}_out; + """ ) for i in range(len(bram_fifos_depth)): input_fifo_id = i code_gen_dict["$GENERATE_BUFFER_CONNECTION$"].append( - """assign bram_fifo_{fifo_id}_in = reg_fifo_{input_fifo_id}_out; - """.format( - fifo_id=i, input_fifo_id=input_fifo_id - ) + f"""assign bram_fifo_{i}_in = reg_fifo_{input_fifo_id}_out; + """ ) return template_path, code_gen_dict @@ -889,19 +875,19 @@ def generate_hdl(self, model, fpgapart, clk): str(roundup_to_integer_multiple(self.get_outstream_width(), 8)) ] ram_style = self.get_nodeattr("ram_style") - code_gen_dict["$RAM_STYLE$"] = ['"{}"'.format(ram_style)] + code_gen_dict["$RAM_STYLE$"] = [f'"{ram_style}"'] # apply code generation to templates code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - with open(template_path, "r") as f: + with open(template_path) as f: template = f.read() if self.get_nodeattr("dynamic_mode"): template_select = "swg/swg_template_wrapper_dynamic.v" else: template_select = "swg/swg_template_wrapper.v" - with open(os.path.join(get_settings().finn_rtllib, template_select), "r") as f: + with open(os.path.join(get_settings().finn_rtllib, template_select)) as f: template_wrapper = f.read() - with open(os.path.join(get_settings().finn_rtllib, "swg/swg_template_axilite.v"), "r") as f: + with open(os.path.join(get_settings().finn_rtllib, "swg/swg_template_axilite.v")) as f: template_axilite = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' diff --git a/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py index 0356ba8364..fb8dcc1982 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py @@ -47,8 +47,7 @@ def get_nodeattr_types(self): return my_attrs def adapt_for_loop_body(self, input_types): - """ - Adapt elementwise binary operator for loop body execution. + """Adapt elementwise binary operator for loop body execution. When an elementwise operator is placed inside a loop, parameters that are indexed per iteration (PARAMETER type) need to be received as @@ -104,7 +103,7 @@ def generate_hdl(self, model, fpgapart, clk): "STREAM_BITS": pe * 32, } - with open(template_path, "r") as f: + with open(template_path) as f: template = f.read() for key_name in code_gen_dict: template = template.replace(f"${key_name}$", str(code_gen_dict[key_name])) @@ -138,8 +137,7 @@ def get_rtl_file_list(self, abspath=False): ] def get_verilog_top_module_intf_names(self): - """ - Return the interface names for the Verilog top module. + """Return the interface names for the Verilog top module. For RTL elementwise operations, this includes handling for MLO mode where the rhs parameter may be streamed as an input. diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index 3d95800250..14b6df92d0 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -116,18 +116,15 @@ def get_nodeattr( ret = attr.__getattribute__(dtype) ret = ModelWrapper(qonnx_make_model(ret)) return ret - else: - return super().get_nodeattr(name) - else: - if req: - raise Exception( - """Required attribute %s unspecified in + return super().get_nodeattr(name) + if req: + raise Exception( + """Required attribute %s unspecified in a %s node""" - % (name, self.onnx_node.op_type) - ) - else: - # not set, return default value - return def_val + % (name, self.onnx_node.op_type) + ) + # not set, return default value + return def_val except KeyError: raise AttributeError("Op has no such attribute: " + name) @@ -299,9 +296,8 @@ def get_number_output_values(self): def prepare_rtlsim(self, behav=False): """Creates a xsi emulation library for the RTL code generated for this node, sets the rtlsim_so attribute to its path.""" - vivado_stitch_proj_dir = self.get_nodeattr("code_gen_dir_ipgen") - with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f: + with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt") as f: all_verilog_srcs = f.read().split() top_module_file_name = os.path.basename(os.path.realpath(self.get_nodeattr("ipgen_path"))) top_module_name = top_module_file_name.strip(".v") @@ -406,7 +402,7 @@ def generate_hdl(self, model, fpgapart, clk): ] # need to get correct value template_path = os.path.join(get_settings().finn_rtllib, "mlo", "loop_control_wrapper.v") - with open(template_path, "r") as f: + with open(template_path) as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' @@ -434,8 +430,8 @@ def generate_params(self, model, path): loop_body.set_tensor_datatype(loop_tensor, param_dtype) inst = getCustomOp(param_node) inst.generate_params(loop_body, path) - param_file = "{}/memblock.dat".format(path) - new_param_file = "{}/{}_memblock_{}.dat".format(path, param_node.op_type, iter) + param_file = f"{path}/memblock.dat" + new_param_file = f"{path}/{param_node.op_type}_memblock_{iter}.dat" if param_node.op_type.startswith("MVAU") or param_node.op_type.startswith( "Elementwise" ): @@ -471,13 +467,11 @@ def generate_params(self, model, path): "Elementwise" ): # concatinate all .dat files together - param_file = "{}/memblock_{}_id_{}.dat".format(path, param_node.op_type, i + 1) + param_file = f"{path}/memblock_{param_node.op_type}_id_{i + 1}.dat" with open(param_file, "w") as outfile: for iter in range(iteration): - memblock_file = "{}/{}_memblock_{}.dat".format( - path, param_node.op_type, iter - ) - with open(memblock_file, "r") as infile: + memblock_file = f"{path}/{param_node.op_type}_memblock_{iter}.dat" + with open(memblock_file) as infile: for line in infile: outfile.write(line) os.remove(memblock_file) @@ -491,7 +485,7 @@ def generate_params(self, model, path): for fname in files: if fname.endswith("_memstream_wrapper.v"): fpath = os.path.join(dname, fname) - with open(fpath, "r") as f: + with open(fpath) as f: s = f.read() old = "%s/memblock.dat" % ipgen_path new = "%s/memblock_%s_id_%s.dat" % ( @@ -516,10 +510,8 @@ def generate_params(self, model, path): ) with open(param_file, "w") as outfile: for iter in range(iteration): - iter_file = "{}/{}_threshs_{}_{}_i{}.dat".format( - path, param_node.name, pe_value, stage, iter - ) - with open(iter_file, "r") as infile: + iter_file = f"{path}/{param_node.name}_threshs_{pe_value}_{stage}_i{iter}.dat" + with open(iter_file) as infile: cnt = 0 for line in infile: if cnt == 0: @@ -545,7 +537,7 @@ def generate_params(self, model, path): for fname in files: if fname.endswith(".v"): fpath = os.path.join(dname, fname) - with open(fpath, "r") as f: + with open(fpath) as f: s = f.read() old = "./%s" % param_node.name new = "%s/Thresholding_id_%s" % (path, i + 1) @@ -582,7 +574,7 @@ def generate_hdl_stream_tap(self): "$TAP_REP$": [str(tap_rep)], } # apply code generation to template - with open(template_path, "r") as f: + with open(template_path) as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' @@ -1106,9 +1098,9 @@ def ipgen_singlenode_code(self, fpgapart=None): working_dir = os.environ["PWD"] with open(make_project_sh, "w") as f: f.write("#!/bin/bash \n") - f.write("cd {}\n".format(vivado_stitch_proj_dir)) + f.write(f"cd {vivado_stitch_proj_dir}\n") f.write("vivado -mode batch -source make_loop_ip.tcl\n") - f.write("cd {}\n".format(working_dir)) + f.write(f"cd {working_dir}\n") bash_command = ["bash", make_project_sh] process_compile = subprocess.Popen(bash_command, stdout=subprocess.PIPE) process_compile.communicate() diff --git a/src/finn/custom_op/fpgadataflow/rtl/fmpadding_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/fmpadding_rtl.py index 1dcbac4200..d5f069da91 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/fmpadding_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/fmpadding_rtl.py @@ -187,7 +187,7 @@ def generate_hdl(self, model, fpgapart, clk): # apply code generation to templates code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - with open(template_path, "r") as f: + with open(template_path) as f: template = f.read() for key_name in code_gen_dict: key = "$%s$" % key_name diff --git a/src/finn/custom_op/fpgadataflow/rtl/inner_shuffle_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/inner_shuffle_rtl.py index 0179010e14..5f1392cdf8 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/inner_shuffle_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/inner_shuffle_rtl.py @@ -10,16 +10,14 @@ import os import shutil from qonnx.core.datatype import DataType -from typing import Optional from finn.custom_op.fpgadataflow.inner_shuffle import InnerShuffle from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.util.settings import get_settings -def auto_size_simd(I_dim: int, SIMD: int) -> Optional[int]: - """ - Return the smallest divisor d of I_dim such that d > SIMD. +def auto_size_simd(I_dim: int, SIMD: int) -> int | None: + """Return the smallest divisor d of I_dim such that d > SIMD. if no such divisor exists, return None. """ if I_dim <= 0: @@ -28,7 +26,7 @@ def auto_size_simd(I_dim: int, SIMD: int) -> Optional[int]: raise ValueError("SIMD must be a non-negative integer") candidates = [] - limit = int(math.isqrt(I_dim)) + limit = math.isqrt(I_dim) for a in range(1, limit + 1): if I_dim % a == 0: b = I_dim // a @@ -90,7 +88,7 @@ def generate_hdl(self, model, fpgapart, clk): "WIDTH": dt.bitwidth(), "STREAM_BITS": simd * dt.bitwidth(), } - with open(template_path, "r") as f: + with open(template_path) as f: template = f.read() for key_name in code_gen_dict: key = f"${key_name}$" diff --git a/src/finn/custom_op/fpgadataflow/rtl/layernorm_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/layernorm_rtl.py index c2d3fc1a7e..36d4b47e0c 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/layernorm_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/layernorm_rtl.py @@ -56,7 +56,7 @@ def generate_hdl(self, model, fpgapart, clk): # apply code generation to templates code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - with open(template_path, "r") as f: + with open(template_path) as f: template = f.read() for key in code_gen_dict: template = template.replace(key, str(code_gen_dict[key])) diff --git a/src/finn/custom_op/fpgadataflow/rtl/matrixvectoractivation_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/matrixvectoractivation_rtl.py index 96f05b3c24..2731683a02 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/matrixvectoractivation_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/matrixvectoractivation_rtl.py @@ -131,12 +131,12 @@ def execute_node(self, context, graph): if dynamic_input or self.get_nodeattr("mlo_max_iter"): reshaped_input = context[inputs].reshape(-1, context[inputs].shape[-1]) self.make_weight_file( - reshaped_input, "decoupled_npy", "{}/input_1.npy".format(code_gen_dir) + reshaped_input, "decoupled_npy", f"{code_gen_dir}/input_1.npy" ) sim = self.get_rtlsim() nbits = self.get_instream_width() - inp = npy_to_rtlsim_input("{}/input_0.npy".format(code_gen_dir), export_idt, nbits) + inp = npy_to_rtlsim_input(f"{code_gen_dir}/input_0.npy", export_idt, nbits) super().reset_rtlsim(sim) if ( dynamic_input @@ -148,7 +148,7 @@ def execute_node(self, context, graph): wnbits = wnbits * self.get_nodeattr("SIMD") export_wdt = self.get_input_datatype(1) - wei = npy_to_rtlsim_input("{}/input_1.npy".format(code_gen_dir), export_wdt, wnbits) + wei = npy_to_rtlsim_input(f"{code_gen_dir}/input_1.npy", export_wdt, wnbits) num_w_reps = np.prod(self.get_nodeattr("numInputVectors")) io_dict = { @@ -166,7 +166,7 @@ def execute_node(self, context, graph): odt = self.get_output_datatype() target_bits = odt.bitwidth() packed_bits = self.get_outstream_width() - out_npy_path = "{}/output.npy".format(code_gen_dir) + out_npy_path = f"{code_gen_dir}/output.npy" out_shape = self.get_folded_output_shape() rtlsim_output_to_npy(output, out_npy_path, odt, out_shape, packed_bits, target_bits) @@ -177,10 +177,8 @@ def execute_node(self, context, graph): context[node.output[0]] = output else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) def lut_estimation(self): @@ -318,10 +316,8 @@ def _resolve_segment_len(self, clk): assert ( ref_clk > 0.741 - ), """Infeasible clk target of {} ns has been set, - consider lowering the targeted clock frequency!""".format( - ref_clk - ) + ), f"""Infeasible clk target of {ref_clk} ns has been set, + consider lowering the targeted clock frequency!""" critical_path_dsps = np.floor((ref_clk - 0.741) / 0.605 + 1) max_chain_len = np.ceil(self.get_nodeattr("SIMD") / simd_factor) dsp_chain_len = critical_path_dsps if critical_path_dsps < max_chain_len else max_chain_len @@ -346,10 +342,8 @@ def _resolve_dsp_version(self, dsp_block): # supported RTL compute core assert ( self.get_nodeattr("resType") != "lut" - ), """LUT-based RTL-MVU implementation currently not supported! - Please change resType for {} to 'dsp' or consider switching to HLS-based MVAU!""".format( - self.onnx_node.name - ) + ), f"""LUT-based RTL-MVU implementation currently not supported! + Please change resType for {self.onnx_node.name} to 'dsp' or consider switching to HLS-based MVAU!""" match dsp_block: case "DSP58": @@ -395,7 +389,7 @@ def generate_hdl(self, model, fpgapart, clk): self.set_nodeattr("gen_top_module", self.get_verilog_top_module_name()) # apply code generation to template - with open(template_path, "r") as f: + with open(template_path) as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' diff --git a/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py index 92d4132afa..43badbbcb3 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py @@ -92,7 +92,7 @@ def format_sv_array(arr): # Generate SystemVerilog implementation module (with _impl suffix) sv_template_path = rtllib_dir + "requant_wrapper_template.sv" - with open(sv_template_path, "r") as f: + with open(sv_template_path) as f: sv_template = f.read() sv_code = sv_template @@ -113,7 +113,7 @@ def format_sv_array(arr): # Generate Verilog stub wrapper (for IP packaging - must be .v) v_template_path = rtllib_dir + "requant_wrapper_template.v" - with open(v_template_path, "r") as f: + with open(v_template_path) as f: v_template = f.read() v_code = v_template @@ -152,8 +152,7 @@ def get_rtl_file_list(self, abspath=False): if abspath: return rtl_files - else: - return [os.path.basename(f) for f in rtl_files] + return [os.path.basename(f) for f in rtl_files] def code_generation_ipi(self): sourcefiles = self.get_rtl_file_list(abspath=True) @@ -189,7 +188,7 @@ def execute_node(self, context, graph): np.save(os.path.join(code_gen_dir, "input_0.npy"), reshaped_input) nbits = self.get_instream_width(0) rtlsim_inp = npy_to_rtlsim_input( - "{}/input_0.npy".format(code_gen_dir), export_idt, nbits + f"{code_gen_dir}/input_0.npy", export_idt, nbits ) io_dict = { @@ -207,7 +206,7 @@ def execute_node(self, context, graph): odt = self.get_output_datatype(0) target_bits = odt.bitwidth() packed_bits = self.get_outstream_width(0) - out_npy_path = "{}/output.npy".format(code_gen_dir) + out_npy_path = f"{code_gen_dir}/output.npy" out_shape = self.get_folded_output_shape(0) rtlsim_output_to_npy( rtlsim_output, out_npy_path, odt, out_shape, packed_bits, target_bits diff --git a/src/finn/custom_op/fpgadataflow/rtl/reshape_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/reshape_rtl.py index c6a81f3d32..569168f80c 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/reshape_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/reshape_rtl.py @@ -44,7 +44,6 @@ def execute_node(self, context, graph): def generate_hdl(self, model, fpgapart, clk): """Generate HLD code by filling in the verilog template.""" - # Path to RTL sources implementing the AXI pass-through operator # Note: Implements AXI pass-through via the data width converter, which, # for identical input and output width, reduces to a no-op. @@ -70,7 +69,7 @@ def generate_hdl(self, model, fpgapart, clk): code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") # Load the code template and fill in the parameter values from the dict - with open(template, "r") as f: + with open(template) as f: template = f.read() for placeholder, value in code_gen_dict.items(): template = template.replace(f"${placeholder}$", str(value)) diff --git a/src/finn/custom_op/fpgadataflow/rtl/streamingdatawidthconverter_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/streamingdatawidthconverter_rtl.py index 0e731a8275..482651f3ad 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/streamingdatawidthconverter_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/streamingdatawidthconverter_rtl.py @@ -111,7 +111,7 @@ def generate_hdl(self, model, fpgapart, clk): # apply code generation to templates code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - with open(template_path, "r") as f: + with open(template_path) as f: template = f.read() for key_name in code_gen_dict: key = "$%s$" % key_name diff --git a/src/finn/custom_op/fpgadataflow/rtl/streamingfifo_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/streamingfifo_rtl.py index ad056bb26d..a051c7ab29 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/streamingfifo_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/streamingfifo_rtl.py @@ -177,14 +177,14 @@ def generate_hdl(self, model, fpgapart, clk): count_width = int(self.get_nodeattr("depth")).bit_length() depth = int(self.get_nodeattr("depth")) code_gen_dict["$COUNT_WIDTH$"] = f"{count_width}" - code_gen_dict["$COUNT_RANGE$"] = "[{}:0]".format(count_width - 1) - code_gen_dict["$IN_RANGE$"] = "[{}:0]".format(in_width - 1) - code_gen_dict["$OUT_RANGE$"] = "[{}:0]".format(in_width - 1) + code_gen_dict["$COUNT_RANGE$"] = f"[{count_width - 1}:0]" + code_gen_dict["$IN_RANGE$"] = f"[{in_width - 1}:0]" + code_gen_dict["$OUT_RANGE$"] = f"[{in_width - 1}:0]" code_gen_dict["$WIDTH$"] = str(in_width) code_gen_dict["$DEPTH$"] = str(depth) # apply code generation to templates code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - with open(template_path, "r") as f: + with open(template_path) as f: template = f.read() for key_name in code_gen_dict: key = "%s" % key_name @@ -230,7 +230,7 @@ def code_generation_ipi(self): % (self.get_nodeattr("gen_top_module"), self.onnx_node.name) ] return cmd - elif impl_style == "vivado": + if impl_style == "vivado": cmd = [] node_name = self.onnx_node.name depth = self.get_adjusted_depth() @@ -285,7 +285,7 @@ def code_generation_ipi(self): "[get_bd_pins %s/fifo/s_axis_aclk]" % (node_name, clk_name, node_name) ) return cmd - elif impl_style == "virtual": + if impl_style == "virtual": code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") sourcefiles = self.get_rtl_file_list(abspath=True) fifo_name = self.onnx_node.name @@ -301,10 +301,9 @@ def code_generation_ipi(self): cmd += [f"set_property CONFIG.DATA_WIDTH {width} [get_bd_cells {fifo_name}]"] cmd += [f"set_property CONFIG.FM_SIZE {fm_size} [get_bd_cells {fifo_name}]"] return cmd - else: - raise Exception( - "FIFO implementation style %s not supported, please use rtl or vivado" % impl_style - ) + raise Exception( + "FIFO implementation style %s not supported, please use rtl or vivado" % impl_style + ) def get_rtl_file_list(self, abspath=False): """Get list of RTL files required for this node. diff --git a/src/finn/custom_op/fpgadataflow/rtl/thresholding_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/thresholding_rtl.py index ebcf95059a..ef6072a8ff 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/thresholding_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/thresholding_rtl.py @@ -210,7 +210,7 @@ def prepare_codegen_rtl_values(self, model): "Fixed-point thresholds have more fractional bits than input. " "Run RoundAndClipThresholds to reduce threshold fractional bits." ) - elif wdt.scale_factor() > idt.scale_factor(): + if wdt.scale_factor() > idt.scale_factor(): raise ValueError( "Fixed-point inputs and with more fractional bits " "than thresholds are not supported." @@ -310,7 +310,7 @@ def generate_hdl(self, model, fpgapart, clk): axi_dir = os.path.join(get_settings().finn_rtllib, "axi/hdl/") rtlsrc = os.path.join(get_settings().finn_rtllib, "thresholding/hdl") template_path = rtlsrc + "/thresholding_template_wrapper.v" - with open(template_path, "r") as f: + with open(template_path) as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' @@ -376,7 +376,7 @@ def execute_node(self, context, graph): # make copy before saving the array reshaped_input = reshaped_input.copy() np.save( - os.path.join(code_gen_dir, "input_{}.npy".format(in_ind)), + os.path.join(code_gen_dir, f"input_{in_ind}.npy"), reshaped_input, ) elif in_ind > 2: @@ -386,7 +386,7 @@ def execute_node(self, context, graph): sim = self.get_rtlsim() nbits = self.get_instream_width() rtlsim_inp = npy_to_rtlsim_input( - "{}/input_0.npy".format(code_gen_dir), export_idt, nbits + f"{code_gen_dir}/input_0.npy", export_idt, nbits ) io_dict = { "inputs": {"in0": rtlsim_inp}, @@ -401,7 +401,7 @@ def execute_node(self, context, graph): odt = self.get_output_datatype() target_bits = odt.bitwidth() packed_bits = self.get_outstream_width() - out_npy_path = "{}/output.npy".format(code_gen_dir) + out_npy_path = f"{code_gen_dir}/output.npy" out_shape = self.get_folded_output_shape() rtlsim_output_to_npy( @@ -415,10 +415,8 @@ def execute_node(self, context, graph): context[node.output[0]] = output else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) def code_generation_ipi(self): @@ -471,7 +469,7 @@ def generate_params(self, model, path): """ thresholds = model.get_initializer(self.onnx_node.input[1]) rt_weights = self.get_nodeattr("runtime_writeable_weights") - file_name = "{}/memblock.dat".format(path) + file_name = f"{path}/memblock.dat" if rt_weights: self.make_weight_file(thresholds, "decoupled_runtime", file_name) self.make_weight_file(thresholds, "internal_embedded", file_name) diff --git a/src/finn/custom_op/fpgadataflow/rtl/vectorvectoractivation_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/vectorvectoractivation_rtl.py index 33f2a554d0..ceddb71a50 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/vectorvectoractivation_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/vectorvectoractivation_rtl.py @@ -119,7 +119,7 @@ def execute_node(self, context, graph): # make copy before saving the array reshaped_input = reshaped_input.copy() np.save( - os.path.join(code_gen_dir, "input_{}.npy".format(in_ind)), + os.path.join(code_gen_dir, f"input_{in_ind}.npy"), reshaped_input, ) elif in_ind > 2: @@ -128,7 +128,7 @@ def execute_node(self, context, graph): sim = self.get_rtlsim() nbits = self.get_instream_width(0) - inp = npy_to_rtlsim_input("{}/input_0.npy".format(code_gen_dir), export_idt, nbits) + inp = npy_to_rtlsim_input(f"{code_gen_dir}/input_0.npy", export_idt, nbits) super().reset_rtlsim(sim) if mem_mode in ["external", "internal_decoupled"]: @@ -139,7 +139,7 @@ def execute_node(self, context, graph): if self.get_input_datatype(1) == DataType["BIPOLAR"]: export_wdt = DataType["BINARY"] wei = npy_to_rtlsim_input( - "{}/weights.npy".format(code_gen_dir), export_wdt, wnbits + f"{code_gen_dir}/weights.npy", export_wdt, wnbits ) dim_h, dim_w = self.get_nodeattr("Dim") num_w_reps = dim_h * dim_w @@ -159,7 +159,7 @@ def execute_node(self, context, graph): odt = self.get_output_datatype() target_bits = odt.bitwidth() packed_bits = self.get_outstream_width() - out_npy_path = "{}/output.npy".format(code_gen_dir) + out_npy_path = f"{code_gen_dir}/output.npy" out_shape = self.get_folded_output_shape() rtlsim_output_to_npy(output, out_npy_path, odt, out_shape, packed_bits, target_bits) @@ -170,10 +170,8 @@ def execute_node(self, context, graph): context[node.output[0]] = output else: raise Exception( - """Invalid value for attribute exec_mode! Is currently set to: {} - has to be set to one of the following value ("cppsim", "rtlsim")""".format( - mode - ) + f"""Invalid value for attribute exec_mode! Is currently set to: {mode} + has to be set to one of the following value ("cppsim", "rtlsim")""" ) def lut_estimation(self): @@ -284,7 +282,7 @@ def generate_hdl(self, model, fpgapart, clk): self.set_nodeattr("gen_top_module", self.get_verilog_top_module_name()) # apply code generation to template - with open(template_path, "r") as f: + with open(template_path) as f: template_wrapper = f.read() for key in code_gen_dict: # transform list into long string separated by '\n' @@ -329,10 +327,8 @@ def _resolve_segment_len(self, clk): # clk >= (critical_path_dsps - 1) * 0.605 + 0.741 assert ( clk > 0.741 - ), """Infeasible clk target of {} ns has been set, - consider lowering the targeted clock frequency!""".format( - clk - ) + ), f"""Infeasible clk target of {clk} ns has been set, + consider lowering the targeted clock frequency!""" critical_path_dsps = np.floor((clk - 0.741) / 0.605 + 1) max_chain_len = np.ceil(self.get_nodeattr("SIMD") / 3) dsp_chain_len = critical_path_dsps if critical_path_dsps < max_chain_len else max_chain_len @@ -360,10 +356,8 @@ def _resolve_dsp_version(self, fpgapart): # supported RTL compute core assert ( self.get_nodeattr("resType") != "lut" - ), """LUT-based RTL-VVU implementation currently not supported! - Please change resType for {} to 'dsp' or consider switching to HLS-based VVAU!""".format( - self.onnx_node.name - ) + ), f"""LUT-based RTL-VVU implementation currently not supported! + Please change resType for {self.onnx_node.name} to 'dsp' or consider switching to HLS-based VVAU!""" is_versal_family = is_versal(fpgapart) assert ( is_versal_family diff --git a/src/finn/custom_op/fpgadataflow/rtlbackend.py b/src/finn/custom_op/fpgadataflow/rtlbackend.py index b5c1c80016..ecf410f433 100644 --- a/src/finn/custom_op/fpgadataflow/rtlbackend.py +++ b/src/finn/custom_op/fpgadataflow/rtlbackend.py @@ -163,7 +163,7 @@ def code_generation_ipgen(self, model: "ModelWrapper", fpgapart: str, clk: str) def execute_node( self, context: dict[str, npt.NDArray], graph: "GraphProto" - ) -> None: # noqa: ARG002 + ) -> None: """Execute this node's RTL simulation. Args: diff --git a/src/finn/custom_op/fpgadataflow/shuffle.py b/src/finn/custom_op/fpgadataflow/shuffle.py index 5ff61c3b33..fd77db4281 100644 --- a/src/finn/custom_op/fpgadataflow/shuffle.py +++ b/src/finn/custom_op/fpgadataflow/shuffle.py @@ -25,8 +25,7 @@ def __init__(self, onnx_node, **kwargs): super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): - """ - The attributes for the Shuffle node capture the + """The attributes for the Shuffle node capture the optional reshapes either side of the transpose. Below is a diagram indicating what tensors the attribute names are referring to. @@ -94,7 +93,7 @@ def infer_node_datatype(self, model): dt = model.get_tensor_datatype(node.input[0]) if dt != self.get_input_datatype(): warn_str = ( - f"data_type changing for {node.name}: {str(self.get_input_datatype())} -> {str(dt)}" + f"data_type changing for {node.name}: {self.get_input_datatype()!s} -> {dt!s}" ) log.warning(warn_str) self.set_nodeattr("data_type", dt.name) diff --git a/src/finn/custom_op/fpgadataflow/streamingdataflowpartition.py b/src/finn/custom_op/fpgadataflow/streamingdataflowpartition.py index 2ae6d92b88..0aa4439d35 100644 --- a/src/finn/custom_op/fpgadataflow/streamingdataflowpartition.py +++ b/src/finn/custom_op/fpgadataflow/streamingdataflowpartition.py @@ -81,7 +81,6 @@ def execute_node(self, context, graph): for tname in ret.keys(): if tname not in [x.name for x in model.graph.output]: context[node.name + "_" + tname] = ret[tname] - pass def verify_node(self): info_messages = [] @@ -92,10 +91,8 @@ def verify_node(self): info_messages.append("The number of attributes is correct") else: info_messages.append( - """The number of attributes is incorrect, - {} should have {} attributes""".format( - self.onnx_node.op_type, num_of_attr - ) + f"""The number of attributes is incorrect, + {self.onnx_node.op_type} should have {num_of_attr} attributes""" ) # verify that all necessary attributes exist try: diff --git a/src/finn/custom_op/fpgadataflow/streamingfifo.py b/src/finn/custom_op/fpgadataflow/streamingfifo.py index d35ece8147..b7238fa0e5 100644 --- a/src/finn/custom_op/fpgadataflow/streamingfifo.py +++ b/src/finn/custom_op/fpgadataflow/streamingfifo.py @@ -168,20 +168,18 @@ def bram_estimation(self): if W == 1: return math.ceil(depth / 16384) - elif W == 2: + if W == 2: return math.ceil(depth / 8192) - elif W <= 4: + if W <= 4: return (math.ceil(depth / 4096)) * (math.ceil(W / 4)) - elif W <= 9: + if W <= 9: return (math.ceil(depth / 2048)) * (math.ceil(W / 9)) - elif W <= 18 or depth > 512: + if W <= 18 or depth > 512: return (math.ceil(depth / 1024)) * (math.ceil(W / 18)) - else: - return (math.ceil(depth / 512)) * (math.ceil(W / 36)) + return (math.ceil(depth / 512)) * (math.ceil(W / 36)) def uram_estimation(self): """Calculates resource estimation for URAM""" - try: impl = self.get_nodeattr("impl_style") == "rtl" except AttributeError: @@ -200,8 +198,7 @@ def uram_estimation(self): if impl == "rtl" or (impl == "vivado" and ram_type != "ultra"): # Non-BRAM based implementation return 0 - else: - return (math.ceil(depth / 4096)) * (math.ceil(W / 72)) + return (math.ceil(depth / 4096)) * (math.ceil(W / 72)) def bram_efficiency_estimation(self): try: diff --git a/src/finn/custom_op/fpgadataflow/thresholding.py b/src/finn/custom_op/fpgadataflow/thresholding.py index ceddc47222..aac98e691c 100644 --- a/src/finn/custom_op/fpgadataflow/thresholding.py +++ b/src/finn/custom_op/fpgadataflow/thresholding.py @@ -157,7 +157,6 @@ def minimize_weight_bit_width(self, model): """Minimize threshold datatype bitwidth based on actual threshold values. This function should not round or clip the threshold values, that is done in RoundAndClipThresholds.""" - thresholds = model.get_initializer(self.onnx_node.input[1]) if self.get_nodeattr("runtime_writeable_weights") or self.get_nodeattr("mlo_max_iter"): return DataType[self.get_nodeattr("weightDataType")] diff --git a/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py b/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py index 0753436fdb..4e4a748ea2 100644 --- a/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py @@ -26,8 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" -Vector-Vector Activation Unit (VVAU) implementation for FPGA dataflow. +"""Vector-Vector Activation Unit (VVAU) implementation for FPGA dataflow. This module contains the VVAU class which provides hardware abstraction for vector-vector activation layers in FPGA implementations. The VVAU performs @@ -58,8 +57,7 @@ class VVAU(MemStreamSupport, HWCustomOp): """Abstraction layer for HW implementation of VectorVectorActivation layers.""" def __init__(self, onnx_node, **kwargs): - """ - Initialize the VVAU (Vector-Vector Activation Unit) instance. + """Initialize the VVAU (Vector-Vector Activation Unit) instance. Args: onnx_node: ONNX node representing the VVAU operation @@ -68,8 +66,7 @@ def __init__(self, onnx_node, **kwargs): super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): - """ - Get the dictionary of node attribute types for VVAU. + """Get the dictionary of node attribute types for VVAU. Returns: dict: Dictionary mapping attribute names to their types and constraints @@ -130,8 +127,7 @@ def get_nodeattr_types(self): return my_attrs def _infer_sparse_weight_tensor(self, W_conv, k_h, k_w, channels): - """ - Convert dense convolution weights to sparse weight tensor format. + """Convert dense convolution weights to sparse weight tensor format. Args: W_conv: Dense convolution weight tensor @@ -152,8 +148,7 @@ def _infer_sparse_weight_tensor(self, W_conv, k_h, k_w, channels): return W_matmul def execute_node(self, context, graph): - """ - Execute the VVAU node operation. + """Execute the VVAU node operation. Performs the vector-vector activation computation including matrix multiplication and optional thresholding activation. @@ -210,8 +205,7 @@ def execute_node(self, context, graph): context[node.output[0]] = result def infer_node_datatype(self, model): - """ - Infer and set the node's data types based on the model. + """Infer and set the node's data types based on the model. Args: model: FINN model containing the node @@ -236,10 +230,9 @@ def get_input_datatype(self, ind=0): # parameter can be > 0 (referring to the weights) so handle that here if ind == 0: return DataType[self.get_nodeattr("inputDataType")] - elif ind == 1: + if ind == 1: return DataType[self.get_nodeattr("weightDataType")] - else: - raise Exception("Undefined input ind for this layer type") + raise Exception("Undefined input ind for this layer type") def get_accumulator_datatype(self): """Returns FINN DataType of accumulator""" @@ -250,8 +243,7 @@ def get_output_datatype(self, ind=0): return DataType[self.get_nodeattr("outputDataType")] def get_instream_width(self, ind=0): - """ - Get the input stream width for the specified input. + """Get the input stream width for the specified input. Args: ind: Input index (0 for activations, 1 for weights, 2 for thresholds) @@ -293,8 +285,7 @@ def get_instream_width(self, ind=0): return width def get_outstream_width(self, ind=0): - """ - Get the output stream width. + """Get the output stream width. Args: ind: Output index (default 0) @@ -307,8 +298,7 @@ def get_outstream_width(self, ind=0): return out_width def get_folded_input_shape(self, ind=0): - """ - Get the folded input shape for hardware implementation. + """Get the folded input shape for hardware implementation. Args: ind: Input index (0 for activations, 1 for weights) @@ -342,8 +332,7 @@ def get_folded_input_shape(self, ind=0): return folded_input_shape def get_folded_output_shape(self, ind=0): - """ - Get the folded output shape for hardware implementation. + """Get the folded output shape for hardware implementation. Args: ind: Output index (default 0) @@ -359,8 +348,7 @@ def get_folded_output_shape(self, ind=0): return folded_output_shape def get_normal_input_shape(self, ind=0): - """ - Get the normal (unfolded) input shape. + """Get the normal (unfolded) input shape. Args: ind: Input index (default 0) @@ -375,8 +363,7 @@ def get_normal_input_shape(self, ind=0): return normal_input_shape def get_normal_output_shape(self, ind=0): - """ - Get the normal (unfolded) output shape. + """Get the normal (unfolded) output shape. Args: ind: Output index (default 0) @@ -402,14 +389,12 @@ def calc_tmem(self): """Calculates and returns TMEM.""" if self.get_nodeattr("noActivation") == 1: return 0 - else: - ch = self.get_nodeattr("Channels") - pe = self.get_nodeattr("PE") - return ch // pe + ch = self.get_nodeattr("Channels") + pe = self.get_nodeattr("PE") + return ch // pe def uram_estimation(self): - """ - Estimate UltraRAM (URAM) usage for this layer. + """Estimate UltraRAM (URAM) usage for this layer. Returns: int: Number of URAMs required @@ -456,20 +441,18 @@ def bram_estimation(self): if mem_width == 1: return math.ceil(omega / 16384) - elif mem_width == 2: + if mem_width == 2: return math.ceil(omega / 8192) - elif mem_width <= 4: + if mem_width <= 4: return (math.ceil(omega / 4096)) * (math.ceil(mem_width / 4)) - elif mem_width <= 9: + if mem_width <= 9: return (math.ceil(omega / 2048)) * (math.ceil(mem_width / 8)) - elif mem_width <= 18 or omega > 512: + if mem_width <= 18 or omega > 512: return (math.ceil(omega / 1024)) * (math.ceil(mem_width / 16)) - else: - return (math.ceil(omega / 512)) * (math.ceil(mem_width / 32)) + return (math.ceil(omega / 512)) * (math.ceil(mem_width / 32)) def bram_efficiency_estimation(self): - """ - Estimate BRAM efficiency (utilization) for this layer. + """Estimate BRAM efficiency (utilization) for this layer. Returns: float: BRAM efficiency ratio (actual usage / allocated capacity) @@ -500,8 +483,7 @@ def uram_efficiency_estimation(self): return wbits / uram_est_capacity def get_exp_cycles(self): - """ - Get the expected number of execution cycles for this layer. + """Get the expected number of execution cycles for this layer. Returns: int: Expected number of clock cycles for execution @@ -651,8 +633,7 @@ def get_hw_compatible_threshold_tensor(self, orig_thres_matrix): return ret.reshape(1, pe, tmem, n_thres_steps) def get_hw_compatible_weight_tensor(self, orig_weight_matrix): - """ - Convert weight matrix to hardware-compatible format. + """Convert weight matrix to hardware-compatible format. Args: orig_weight_matrix: Original weight matrix @@ -688,7 +669,6 @@ def make_weight_file(self, weights, weight_file_mode, weight_file_name): of weights. Arguments: - * weights : numpy array with weights to be put into the file * weight_file_mode : one of {hls_header, decoupled_verilog_dat, decoupled_runtime} @@ -802,8 +782,7 @@ def make_weight_file(self, weights, weight_file_mode, weight_file_name): raise Exception("Unknown weight_file_mode") def generate_params(self, model, path): - """ - Generate parameter files for hardware implementation. + """Generate parameter files for hardware implementation. Args: model: FINN model containing the node @@ -815,16 +794,16 @@ def generate_params(self, model, path): weights = model.get_initializer(self.onnx_node.input[1]) if mem_mode == "internal_embedded": # save hlslib-compatible weights in params.h - weight_filename = "{}/params.h".format(code_gen_dir) + weight_filename = f"{code_gen_dir}/params.h" self.make_weight_file(weights, "hls_header", weight_filename) elif mem_mode == "internal_decoupled" or mem_mode == "external": - weight_filename_sim = "{}/weights.npy".format(code_gen_dir) + weight_filename_sim = f"{code_gen_dir}/weights.npy" # save internal_decoupled weights for cppsim self.make_weight_file(weights, "decoupled_npy", weight_filename_sim) if mem_mode == "internal_decoupled": # also save weights as Verilog .dat file # This file will be ignored when synthesizing UltraScale memory. - weight_filename_rtl = "{}/memblock.dat".format(code_gen_dir) + weight_filename_rtl = f"{code_gen_dir}/memblock.dat" self.make_weight_file(weights, "decoupled_verilog_dat", weight_filename_rtl) else: raise Exception( @@ -859,7 +838,7 @@ def generate_params(self, model, path): threshold_tensor, tdt, "thresholds", False, True ) # write thresholds into thresh.h - f_thresh = open("{}/thresh.h".format(code_gen_dir), "w") + f_thresh = open(f"{code_gen_dir}/thresh.h", "w") tdt_hls = tdt.get_hls_datatype_str() # use binary to export bipolar activations export_odt = self.get_output_datatype() @@ -882,8 +861,7 @@ def generate_params(self, model, path): f_thresh.close() def get_op_and_param_counts(self): - """ - Get operation and parameter counts for this layer. + """Get operation and parameter counts for this layer. Returns: dict: Dictionary containing operation and parameter counts by type @@ -912,8 +890,7 @@ def get_op_and_param_counts(self): return ret_dict def get_verilog_top_module_intf_names(self): - """ - Get Verilog top module interface names. + """Get Verilog top module interface names. Returns: dict: Dictionary mapping interface types to their names @@ -930,8 +907,7 @@ def get_verilog_top_module_intf_names(self): return intf_names def code_generation_ipi(self): - """ - Generate IP integrator (IPI) commands for hardware synthesis. + """Generate IP integrator (IPI) commands for hardware synthesis. Returns: list: List of TCL commands for IP integrator diff --git a/src/finn/interface/manage_deps.py b/src/finn/interface/manage_deps.py index a118b32bd9..d01c919be8 100644 --- a/src/finn/interface/manage_deps.py +++ b/src/finn/interface/manage_deps.py @@ -169,7 +169,7 @@ def get_dependency_data(self, package_name: str) -> Dependency | None: def get_fields(self, package_name: str, *field_names: str) -> tuple: """Return a tuple with all required fields from the data. If one of the fields does not - exist, raise an exception.""" # noqa + exist, raise an exception.""" self.assert_unique_dependency_names() dep_data = self.get_dependency_data(package_name) if dep_data is None: @@ -218,7 +218,7 @@ def update_status(self, name: str, status: str, color: str) -> None: def _generate_renderable(self) -> Table: """Generate a renderable for rich to display in a live context.""" if self.non_interactive: - return + return None with self.datalock: table = Table( title="Dependency Updates", @@ -331,7 +331,7 @@ def _run_silent(self, cmd: str, cwd: Path | None = None, timeout: float | None = def _git_clone(self, url: str, commit: str, target: Path) -> bool: """Try to clone and checkout the git url to the given target directory. If something - went wrong return False, True otherwise.""" # noqa + went wrong return False, True otherwise.""" clone_result = sp.run( shlex.split(f"git clone {url} {target.absolute()}"), timeout=self.git_timeout, @@ -354,7 +354,7 @@ def _git_clone(self, url: str, commit: str, target: Path) -> bool: def _get_git_hash(self, package_name: str) -> str | None: """Return the hash of the given package_name dependency. - If there is no such package return None.""" # noqa + If there is no such package return None.""" if package_name in self.deps.git_deps: target = self.dep_location / package_name elif package_name in self.deps.boardfile_deps: @@ -605,7 +605,7 @@ def is_outdated(self, package_name: str, installed: bool = False) -> bool: def get_outdated_dependencies(self) -> list[str]: """Return a list of the names of all outdated packages. For Git dependencies this means - an outdated commit hash, for the others a different URL or target directory.""" # noqa + an outdated commit hash, for the others a different URL or target directory.""" return list( map( str, diff --git a/src/finn/interface/run_finn.py b/src/finn/interface/run_finn.py index dba20e3531..562cde35c8 100644 --- a/src/finn/interface/run_finn.py +++ b/src/finn/interface/run_finn.py @@ -57,13 +57,13 @@ def edit_file(p: Path) -> None: def output(f: Callable) -> Callable[..., Any]: """Add a click parameter named --output (-o) that defaults to - None if the param is empty, and a path otherwise.""" # noqa + None if the param is empty, and a path otherwise.""" return click.option("--output", "-o", "output", default="", type=NullablePath())(f) def finn_deps(f: Callable) -> Callable[..., Any]: """Add a click parameter named --dependency-path (-d) (finn_deps) that defaults to - None if the param is empty, and a path otherwise.""" # noqa + None if the param is empty, and a path otherwise.""" return click.option("--dependency-path", "-d", "finn_deps", default="", type=NullablePath())(f) @@ -77,7 +77,7 @@ def finn_deps_definitions(f: Callable) -> Callable[..., Any]: def finn_build_dir(f: Callable) -> Callable[..., Any]: """Add a click parameter named --build-path (-b) (finn_build_dir) that defaults to - None if the param is empty, and a path otherwise.""" # noqa + None if the param is empty, and a path otherwise.""" return click.option( "--build-path", "-b", @@ -604,7 +604,7 @@ def _build( else: model = mp status( - f"Starting FINN build with config {flow_config.name} and model " f"{model.name}!" + f"Starting FINN build with config {flow_config.name} and model {model.name}!" ) # type: ignore if finn_build_dir is not None: finn_build_dir = finn_build_dir.expanduser().absolute() @@ -644,7 +644,7 @@ def _build( ) sys.exit(1) except FileNotFoundError: - error(f"The flow configuration file could not be found at " f"{flow_config}.") + error(f"The flow configuration file could not be found at {flow_config}.") sys.exit(1) if dfbc is None: diff --git a/src/finn/templates/python_driver/driver.py b/src/finn/templates/python_driver/driver.py index e0c6b646cf..ec1340e481 100644 --- a/src/finn/templates/python_driver/driver.py +++ b/src/finn/templates/python_driver/driver.py @@ -99,7 +99,6 @@ def load_external_weights(self): is specified as the class member ``runtime_weight_dir``. External (DRAM) weights are one .npy file per layer. """ - self.external_weights = [] w_filenames = [] if not os.path.isdir(self.runtime_weight_dir): @@ -139,8 +138,8 @@ def load_external_weights(self): hw_ext_weights = self.io_shape_dict["number_of_external_weights"] assert len(self.external_weights) == hw_ext_weights, ( "Number of hardware external weights and number of external " - + "weight tensors available do not match. \n" - + "Is runtime_weight_dir pointing to the correct folder?" + "weight tensors available do not match. \n" + "Is runtime_weight_dir pointing to the correct folder?" ) def load_runtime_weights(self, flush_accel=True, verify=True): @@ -166,7 +165,7 @@ def load_runtime_weights(self, flush_accel=True, verify=True): rt_weight_dict = {} for w_filename in w_filenames: if w_filename.endswith(".dat"): - with open(self.runtime_weight_dir + "/" + w_filename, "r") as f: + with open(self.runtime_weight_dir + "/" + w_filename) as f: dat = f.read() else: continue @@ -411,7 +410,7 @@ def execute_on_buffers(self, asynch=False, batch_size=None): self.wait_until_finished() def wait_until_finished(self): - "Block until all output DMAs have finished writing." + """Block until all output DMAs have finished writing.""" if self.platform == "zynq-iodma": # check if output IODMA is finished via register reads for o in range(self.num_outputs): @@ -431,7 +430,7 @@ def execute(self, input_npy): packing and copying to device buffers, execute on accelerator, then unpack output and return output numpy array from accelerator.""" # if single input, convert to list to normalize how we process the input - if not type(input_npy) is list: + if type(input_npy) is not list: input_npy = [input_npy] assert self.num_inputs == len(input_npy), "Not all accelerator inputs are specified." for i in range(self.num_inputs): @@ -447,8 +446,7 @@ def execute(self, input_npy): outputs.append(obuf_normal) if self.num_outputs == 1: return outputs[0] - else: - return outputs + return outputs def throughput_test(self, **kwargs): """Run accelerator with empty inputs to measure throughput and other metrics. @@ -602,8 +600,7 @@ def reset_accelerator(self): ) def start_accelerator(self, throttle_interval=0): - """ - Start the accelerator. Input is throttled to the specified interval (in cycles) + """Start the accelerator. Input is throttled to the specified interval (in cycles) by pausing after each FM transmission. A throttle_interval of 0 means no throttling. """ # Set seed @@ -779,8 +776,7 @@ def ctrl_set_depth(self, fifo_id, depth=2): self.ctrl_read(check_success=True) def configure_fifos_bounded(self, depths): - """ - Configure all FIFOs with bounded depths. + """Configure all FIFOs with bounded depths. Caller can supply a list of depths or a single depth for all FIFOs. """ if isinstance(depths, list): @@ -789,7 +785,7 @@ def configure_fifos_bounded(self, depths): fifo_depths = [depths] * self.num_fifos # Set depth for each FIFO - for i in range(0, self.num_fifos): + for i in range(self.num_fifos): self.ctrl_set_depth(i, fifo_depths[i]) # Issue RUN_BOUNDED instruction once all depths have been set @@ -838,7 +834,7 @@ def run_paced(self, throttle_interval=0, runtime_s=1): # Collect maximum occupancy of all FIFOs by issuing READ_FILL instructions max_occupancy = [] - for i in range(0, self.num_fifos): + for i in range(self.num_fifos): max_occupancy.append(self.ctrl_read(opcode=0x0C, fifo_id=i)) return max_occupancy, latency @@ -1471,7 +1467,7 @@ def parse_kv(ctx, self, value): multiple=True, callback=parse_kv, nargs=2, - help=("Keyword argument for the class instance: " "... -ck key1=val1 TYPE -ck key2=val2 TYPE"), + help=("Keyword argument for the class instance: ... -ck key1=val1 TYPE -ck key2=val2 TYPE"), ) @click.option( "--fkwarg", @@ -1479,11 +1475,10 @@ def parse_kv(ctx, self, value): multiple=True, callback=parse_kv, nargs=2, - help=("Keyword argument for the called function: " "... -fk key1=val1 TYPE -fk key2=val2 TYPE"), + help=("Keyword argument for the called function: ... -fk key1=val1 TYPE -fk key2=val2 TYPE"), ) def driver_cli(bitfile_name, settings, function, ckwarg, fkwarg): - """ - CLI tool to instantiate driver and execute functions. + """CLI tool to instantiate driver and execute functions. Instantiates a driver class and executes a member function. The instantiation implicitly loads a bitstream to the FPGA. @@ -1494,8 +1489,7 @@ def driver_cli(bitfile_name, settings, function, ckwarg, fkwarg): via --ckwarg or --fkwarg options respectively. Class Kwargs take precedence over settings.json Kwargs. """ - - with open(settings, "r", encoding="utf-8") as f: + with open(settings, encoding="utf-8") as f: driver_settings = json.load(f)["driver_information"] if ckwarg is None: diff --git a/src/finn/templates/validate/imagenet/validate.py b/src/finn/templates/validate/imagenet/validate.py index 9eb19c0590..6dab8565ec 100644 --- a/src/finn/templates/validate/imagenet/validate.py +++ b/src/finn/templates/validate/imagenet/validate.py @@ -15,10 +15,9 @@ def img_resize(img, size): ow = size oh = int(size * h / w) return img.resize((ow, oh), Image.BILINEAR) - else: - oh = size - ow = int(size * w / h) - return img.resize((ow, oh), Image.BILINEAR) + oh = size + ow = int(size * w / h) + return img.resize((ow, oh), Image.BILINEAR) def img_center_crop(img, size): @@ -41,7 +40,7 @@ def pre_process(img_np): def setup_dataloader(val_path, label_file_path=None, batch_size=100, n_images=50000): """Create an image queue for streaming ImageNet validation images.""" - files = ["ILSVRC2012_val_{:08d}.JPEG".format(i) for i in range(1, n_images + 1)] + files = [f"ILSVRC2012_val_{i:08d}.JPEG" for i in range(1, n_images + 1)] labels = np.loadtxt(label_file_path, dtype=int, usecols=1) file_queue = FileQueue() file_queue.load_epochs(list(zip(files, labels)), shuffle=False) @@ -80,7 +79,7 @@ def validate(cls_inst, *args, **kwargs): total = 50000 acc = 100.0 * ok / (total) - print("Final top-1 accuracy: {}%".format(acc)) + print(f"Final top-1 accuracy: {acc}%") # write report to file report = { diff --git a/src/finn/templates/validate/radioml/validate.py b/src/finn/templates/validate/radioml/validate.py index 01e7a63aaa..8e9a69d964 100644 --- a/src/finn/templates/validate/radioml/validate.py +++ b/src/finn/templates/validate/radioml/validate.py @@ -32,8 +32,8 @@ def validate(cls_inst, *args, **kwargs): # do not pre-load large dataset into memory np.random.seed(2018) test_indices = [] - for mod in range(0, 24): # all modulations (0 to 23) - for snr_idx in range(0, 26): # all SNRs (0 to 25 = -20dB to +30dB) + for mod in range(24): # all modulations (0 to 23) + for snr_idx in range(26): # all SNRs (0 to 25 = -20dB to +30dB) start_idx = 26 * 4096 * mod + 4096 * snr_idx indices_subclass = list(range(start_idx, start_idx + 4096)) @@ -69,7 +69,7 @@ def validate(cls_inst, *args, **kwargs): print("batch %d : total OK %d NOK %d" % (i_batch, ok, nok)) acc = 100.0 * ok / (total) - print("Measured top-1 accuracy: {}%".format(acc)) + print(f"Measured top-1 accuracy: {acc}%") # write report to file report = { diff --git a/src/finn/templates/validate/unswnb15/validate.py b/src/finn/templates/validate/unswnb15/validate.py index 6810e41184..3ba94e4e0b 100644 --- a/src/finn/templates/validate/unswnb15/validate.py +++ b/src/finn/templates/validate/unswnb15/validate.py @@ -38,7 +38,7 @@ def validate(cls_inst, *args, **kwargs): print("batch %d / %d : total OK %d NOK %d" % (i + 1, n_batches, ok, nok)) acc = 100.0 * ok / (total) - print("Final accuracy: {:.2f}%".format(acc)) + print(f"Final accuracy: {acc:.2f}%") # write report to file report = { diff --git a/src/finn/transformation/fpgadataflow/attention_heads.py b/src/finn/transformation/fpgadataflow/attention_heads.py index ecf7d348af..6ad8ad349a 100644 --- a/src/finn/transformation/fpgadataflow/attention_heads.py +++ b/src/finn/transformation/fpgadataflow/attention_heads.py @@ -272,7 +272,7 @@ def apply(self, model: ModelWrapper): # noqa # The output of the reshape must be the same as specified as the # second input to the reshape operation - assert (out_shape # noqa + assert (out_shape == model.get_initializer(reshape.input[1])).all() # The final output shape must match the expectation of @@ -499,7 +499,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: continue # Now we know there is only one consumer operation following the # slice node - thresholds_node = model.find_direct_successors(node)[0] # noqa + thresholds_node = model.find_direct_successors(node)[0] # Successor must actually be a MultiThresholds for this # transform to apply if thresholds_node.op_type != "MultiThreshold": diff --git a/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py b/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py index 85d3002eb0..c1cd348b33 100644 --- a/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py +++ b/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py @@ -766,8 +766,7 @@ def apply(self, model): class InferAddStreamsLayer(Transformation): - """ - DEPRECATED: This transformation is deprecated and now redirects to + """DEPRECATED: This transformation is deprecated and now redirects to InferElementwiseBinaryOperation. AddStreams functionality is now covered by ElementwiseAdd operations @@ -937,8 +936,7 @@ def apply(self, model): class InferChannelwiseLinearLayer(Transformation): - """ - DEPRECATED: This transformation is deprecated and now redirects to + """DEPRECATED: This transformation is deprecated and now redirects to InferElementwiseBinaryOperation. ChannelwiseOp functionality is now covered by ElementwiseBinary operations @@ -1237,7 +1235,7 @@ def apply(self, model): else: raise Exception( - "pad_value and pool_fxn not configured for {}".format(node.op_type) + f"pad_value and pool_fxn not configured for {node.op_type}" ) # format input tensor @@ -1251,7 +1249,7 @@ def apply(self, model): pad_amount=pad, pad_value=pad_value, depthwise=1, - input_shape="(1,{},{},{})".format(ifm_h, ifm_w, ifm_ch), + input_shape=f"(1,{ifm_h},{ifm_w},{ifm_ch})", name="Im2Col_" + node.name, ) @@ -1309,7 +1307,6 @@ class InferPoolFromReduce(Transformation): def apply(self, model: ModelWrapper): """Apply transformation to convert lowered pooling to hardware.""" - # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -1666,8 +1663,7 @@ def apply(self, model): class InferStreamingEltwise(Transformation): - """ - DEPRECATED: This transformation is deprecated and now redirects to + """DEPRECATED: This transformation is deprecated and now redirects to InferElementwiseBinaryOperation. StreamingEltwise functionality is now covered by ElementwiseSub and @@ -2131,14 +2127,12 @@ def apply(self, model): class InferHWSoftmax(Transformation): - """ - Infers a regular softmax node without merging the multithreshold + """Infers a regular softmax node without merging the multithreshold and setting the softmax to perform the quantisation. """ def __init__(self): - """ - Infers a regular softmax node without merging the multithreshold + """Infers a regular softmax node without merging the multithreshold and setting the softmax to perform the quantisation. """ super().__init__() @@ -2181,8 +2175,7 @@ def skip_first_node_transpose(model, node): class InferShuffle(Transformation): - """ - Find transpose layers with (optionally) reshape layers around them + """Find transpose layers with (optionally) reshape layers around them and convert them into a shuffle operator """ @@ -2192,8 +2185,7 @@ def __init__(self, _filter=skip_first_node_transpose): self._filter = _filter def _is_streaming_ptranspose(self, perm, shape): - """ - Check if the permutation represents a streaming InnerShuffle case. + """Check if the permutation represents a streaming InnerShuffle case. A streaming InnerShuffle works when the last two dimensions are swapped, regardless of how many outer dimensions there are. """ @@ -2500,8 +2492,7 @@ def dtype_ok(tname): or dt in [DataType["FLOAT32"], DataType["FLOAT16"]] ): return True - else: - return False + return False return all([dtype_ok(tname) for tname in list(node.input) + list(node.output)]) @@ -2659,20 +2650,17 @@ def elements_are_consecutive(indices): """Are elements consecutive (max diff. 1 between all adjacent elements)?""" if indices.size == 1: return True - else: - indices.sort() - return np.all(np.diff(indices) == 1) + indices.sort() + return np.all(np.diff(indices) == 1) class InferCrop(Transformation): - """ - Find gather layers that can be converted into a Crop layer + """Find gather layers that can be converted into a Crop layer and replace them with a Crop layer """ def __init__(self): - """ - Find gather layers that can be converted into a Crop layer + """Find gather layers that can be converted into a Crop layer and replace them with a Crop layer """ super().__init__() diff --git a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py index f34c6b90af..ff6eed2f18 100644 --- a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py +++ b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py @@ -65,16 +65,13 @@ def filter_fc_extw(x): def assign_partition_id(node): if node.op_type in ["GenericPartition", "StreamingDataflowPartition"]: return -1 - else: - backend = get_by_name(node.attribute, "backend") - if backend is not None and backend.s.decode("UTF-8") == "fpgadataflow": - assigned_partition = get_by_name(node.attribute, "partition_id") - if assigned_partition is not None: - return assigned_partition.i - else: - return 0 - else: - return -1 + backend = get_by_name(node.attribute, "backend") + if backend is not None and backend.s.decode("UTF-8") == "fpgadataflow": + assigned_partition = get_by_name(node.attribute, "partition_id") + if assigned_partition is not None: + return assigned_partition.i + return 0 + return -1 # first, use the generic partitioning functionality to split up the graph parent_model = model.transform( diff --git a/src/finn/transformation/fpgadataflow/externalize_params.py b/src/finn/transformation/fpgadataflow/externalize_params.py index 5e21d8cb2a..d18a137a5e 100644 --- a/src/finn/transformation/fpgadataflow/externalize_params.py +++ b/src/finn/transformation/fpgadataflow/externalize_params.py @@ -55,17 +55,16 @@ def filter_fc_extw(x): extw_tensor_name_out = dma_extw.output[0] if extw_tensor_name in [x.name for x in model.graph.input]: continue - else: - extw_vi = model.get_tensor_valueinfo(extw_tensor_name) - assert extw_vi is not None - model.graph.value_info.remove(extw_vi) - model.graph.input.append(extw_vi) - iodma_init = model.get_initializer(extw_vi.name) - assert iodma_init is not None - # remove output-side initializer to get correct dataflow partitioning - model.graph.initializer.remove( - [x for x in model.graph.initializer if x.name == extw_tensor_name_out][0] - ) - graph_modified = True + extw_vi = model.get_tensor_valueinfo(extw_tensor_name) + assert extw_vi is not None + model.graph.value_info.remove(extw_vi) + model.graph.input.append(extw_vi) + iodma_init = model.get_initializer(extw_vi.name) + assert iodma_init is not None + # remove output-side initializer to get correct dataflow partitioning + model.graph.initializer.remove( + [x for x in model.graph.initializer if x.name == extw_tensor_name_out][0] + ) + graph_modified = True return (model, graph_modified) diff --git a/src/finn/transformation/fpgadataflow/floorplan.py b/src/finn/transformation/fpgadataflow/floorplan.py index 55249f3216..4a8332ff1a 100644 --- a/src/finn/transformation/fpgadataflow/floorplan.py +++ b/src/finn/transformation/fpgadataflow/floorplan.py @@ -152,7 +152,7 @@ def apply(self, model): node_inst.set_nodeattr("partition_id", partition_cnt) partition_cnt += 1 continue - elif not ( + if not ( node.op_type.startswith("MVAU") and node_inst.get_nodeattr("mem_mode") is not None and node_inst.get_nodeattr("mem_mode") == "external" @@ -188,9 +188,8 @@ def apply(self, model): partition_id = pre_inst.get_nodeattr("partition_id") node_inst.set_nodeattr("partition_id", partition_id) break - else: - # SLR mismatch with predecessor, can't assign same partition - slr_mismatch_count += 1 + # SLR mismatch with predecessor, can't assign same partition + slr_mismatch_count += 1 if slr_mismatch_count == len(pre_nodes): # SLR mismatch with ALL predecessors -> start new partition diff --git a/src/finn/transformation/fpgadataflow/hlssynth_ip.py b/src/finn/transformation/fpgadataflow/hlssynth_ip.py index c6def0c171..9f109d4c91 100644 --- a/src/finn/transformation/fpgadataflow/hlssynth_ip.py +++ b/src/finn/transformation/fpgadataflow/hlssynth_ip.py @@ -69,7 +69,7 @@ def applyNodeLocal(self, node): if not ( os.path.isdir(inst.get_nodeattr("ipgen_path")) or os.path.isfile(inst.get_nodeattr("ipgen_path")) - ) or not inst.get_nodeattr("code_gen_dir_ipgen") in inst.get_nodeattr("ipgen_path"): + ) or inst.get_nodeattr("code_gen_dir_ipgen") not in inst.get_nodeattr("ipgen_path"): # call the compilation function for this node inst.ipgen_singlenode_code(self.fpgapart) else: diff --git a/src/finn/transformation/fpgadataflow/infer_pixel_padding_deconv.py b/src/finn/transformation/fpgadataflow/infer_pixel_padding_deconv.py index bb29858eca..e0c3cf8e40 100644 --- a/src/finn/transformation/fpgadataflow/infer_pixel_padding_deconv.py +++ b/src/finn/transformation/fpgadataflow/infer_pixel_padding_deconv.py @@ -7,8 +7,7 @@ class InferPixelPaddingDeconv(Transformation): - """ - Lowering and conversion of ConvTranspose (NCHW) nodes to + """Lowering and conversion of ConvTranspose (NCHW) nodes to FMPadding_Pixel + Im2Col + MatMul (NHWC) surrounded by Transpose nodes note: this transformation produces a mix of hw layers and non hw layers to implement this on an FPGA the Im2Col and MatMul nodes need to be converted to hw layers @@ -177,7 +176,7 @@ def apply(self, model): stride=[1, 1], kernel_size=[k_h, k_w], pad_amount=conv_padding, - input_shape="(1,{},{},{})".format(padded_odim_h, padded_odim_w, ifm_ch), + input_shape=f"(1,{padded_odim_h},{padded_odim_w},{ifm_ch})", depthwise=False, dilations=dilation, ) diff --git a/src/finn/transformation/fpgadataflow/insert_dwc.py b/src/finn/transformation/fpgadataflow/insert_dwc.py index 011bd381b7..1b157c083c 100644 --- a/src/finn/transformation/fpgadataflow/insert_dwc.py +++ b/src/finn/transformation/fpgadataflow/insert_dwc.py @@ -43,15 +43,12 @@ def _suitable_node(node): if _is_dwc_node(node): # no DWC for DWCs return False - elif node.op_type == "IODMA_hls": + if node.op_type == "IODMA_hls": # IODMA data shapes/widths need special handling return False - else: - return True - else: - return False - else: + return True return False + return False class InsertDWC(Transformation): diff --git a/src/finn/transformation/fpgadataflow/insert_fifo.py b/src/finn/transformation/fpgadataflow/insert_fifo.py index fe1f60270a..67ce858f2c 100644 --- a/src/finn/transformation/fpgadataflow/insert_fifo.py +++ b/src/finn/transformation/fpgadataflow/insert_fifo.py @@ -39,8 +39,7 @@ def _is_fifo_node(node): if node.op_type.startswith("StreamingFIFO"): return True - else: - return False + return False def _suitable_node(node): @@ -48,12 +47,9 @@ def _suitable_node(node): if is_fpgadataflow_node(node): if not _is_fifo_node(node): return True - else: - return False - else: return False - else: return False + return False def _suitable_folded_shapes(ishape, oshape): diff --git a/src/finn/transformation/fpgadataflow/insert_hook.py b/src/finn/transformation/fpgadataflow/insert_hook.py index 843a32a73e..3895b9cf58 100644 --- a/src/finn/transformation/fpgadataflow/insert_hook.py +++ b/src/finn/transformation/fpgadataflow/insert_hook.py @@ -40,8 +40,7 @@ def _is_hook_node(node): if node.op_type in ["CheckSum_hls"]: return True - else: - return False + return False def _suitable_node(node): @@ -49,12 +48,9 @@ def _suitable_node(node): if is_hls_node(node) or is_rtl_node(node): if not _is_hook_node(node): return True - else: - return False - else: return False - else: return False + return False class InsertHook(Transformation): diff --git a/src/finn/transformation/fpgadataflow/insert_iodma.py b/src/finn/transformation/fpgadataflow/insert_iodma.py index b29049fa67..353fbb29b6 100644 --- a/src/finn/transformation/fpgadataflow/insert_iodma.py +++ b/src/finn/transformation/fpgadataflow/insert_iodma.py @@ -55,8 +55,7 @@ def __init__( self.max_intfwidth = max_intfwidth def get_mem_init(self, weights, pe, simd): - """ - Returns matrix ready for pack_innermost_dim_as_hex_string with + """Returns matrix ready for pack_innermost_dim_as_hex_string with reverse=False (finn.util.data_packing) to return the memory init file little endian packed. That is, get_mem_init returns: @@ -65,7 +64,6 @@ def get_mem_init(self, weights, pe, simd): addr = 1: [(pe-1,simd*2-1),.......(0,simd+1),(0,simd)] . """ - # TODO: refactor this into matrixvectoractivation.py, could go into # make_weight_file except it doesn't write a file but returns a npy # array instead @@ -109,45 +107,44 @@ def apply(self, model): if first_node.op_type == "IODMA_hls": # IODMA already inserted for this input continue - else: - in_shape = model.get_tensor_shape(graph_in_name) - in_dtype = model.get_tensor_datatype(graph_in_name) - first_node_inst = getCustomOp(first_node) - in_folded_shape = first_node_inst.get_folded_input_shape() - # take advantage of AXI stream width padding for DMA alignment - # (AXI streams are always padded to 8 bits) - # this is the width of stream output expected from the DMA - padded_instream_width = first_node_inst.get_instream_width_padded() - padded_instream_bytes = padded_instream_width // 8 - # determine the feasible interface width - transfer_bits = padded_instream_width * np.prod(in_folded_shape[:-1]) - intfwidth = math.gcd(transfer_bits, self.max_intfwidth) - assert intfwidth % 8 == 0, "No feasible interface width for transfer size" - # make new buffer - first_node_in = oh.make_tensor_value_info( - model.make_new_valueinfo_name(), TensorProto.FLOAT, in_shape - ) - model.graph.value_info.append(first_node_in) - model.set_tensor_datatype(first_node_in.name, in_dtype) - # reroute first node input - # FIXME: currently always using 8-bit dtypes to work around the - # padding problems for i/o DMA - first_node.input[0] = first_node_in.name - dma_node = oh.make_node( - "IODMA_hls", - [graph_in_name], - [first_node_in.name], - numInputVectors=in_folded_shape[:-1], - NumChannels=padded_instream_bytes, - dataType="UINT8", - intfWidth=intfwidth, - streamWidth=padded_instream_width, - direction="in", - domain="finn.custom_op.fpgadataflow.hls", - backend="fpgadataflow", - ) - model.graph.node.insert(0, dma_node) - modified = True + in_shape = model.get_tensor_shape(graph_in_name) + in_dtype = model.get_tensor_datatype(graph_in_name) + first_node_inst = getCustomOp(first_node) + in_folded_shape = first_node_inst.get_folded_input_shape() + # take advantage of AXI stream width padding for DMA alignment + # (AXI streams are always padded to 8 bits) + # this is the width of stream output expected from the DMA + padded_instream_width = first_node_inst.get_instream_width_padded() + padded_instream_bytes = padded_instream_width // 8 + # determine the feasible interface width + transfer_bits = padded_instream_width * np.prod(in_folded_shape[:-1]) + intfwidth = math.gcd(transfer_bits, self.max_intfwidth) + assert intfwidth % 8 == 0, "No feasible interface width for transfer size" + # make new buffer + first_node_in = oh.make_tensor_value_info( + model.make_new_valueinfo_name(), TensorProto.FLOAT, in_shape + ) + model.graph.value_info.append(first_node_in) + model.set_tensor_datatype(first_node_in.name, in_dtype) + # reroute first node input + # FIXME: currently always using 8-bit dtypes to work around the + # padding problems for i/o DMA + first_node.input[0] = first_node_in.name + dma_node = oh.make_node( + "IODMA_hls", + [graph_in_name], + [first_node_in.name], + numInputVectors=in_folded_shape[:-1], + NumChannels=padded_instream_bytes, + dataType="UINT8", + intfWidth=intfwidth, + streamWidth=padded_instream_width, + direction="in", + domain="finn.custom_op.fpgadataflow.hls", + backend="fpgadataflow", + ) + model.graph.node.insert(0, dma_node) + modified = True # insert IODMAs for graph outputs if self.insert_output: graph_out_names = [x.name for x in model.graph.output] @@ -155,45 +152,44 @@ def apply(self, model): final_node = model.find_producer(graph_out_name) if final_node.op_type == "IODMA_hls": continue - else: - out_shape = model.get_tensor_shape(graph_out_name) - out_dtype = model.get_tensor_datatype(graph_out_name) - final_node_inst = getCustomOp(final_node) - out_folded_shape = final_node_inst.get_folded_output_shape() - # take advantage of AXI stream width padding for DMA alignment - # (AXI streams are always padded to 8 bits) - # this is the width of stream input to DMA - padded_outstream_width = final_node_inst.get_outstream_width_padded() - padded_outstream_bytes = padded_outstream_width // 8 - # determine the feasible interface width - transfer_bits = padded_outstream_width * np.prod(out_folded_shape[:-1]) - intfwidth = math.gcd(transfer_bits, self.max_intfwidth) - assert intfwidth % 8 == 0, "No feasible interface width for transfer size" - # make new buffer - final_node_out = oh.make_tensor_value_info( - model.make_new_valueinfo_name(), TensorProto.FLOAT, out_shape - ) - model.graph.value_info.append(final_node_out) - model.set_tensor_datatype(final_node_out.name, out_dtype) - # reroute final node output to final_node_out_name - final_node.output[0] = final_node_out.name - # FIXME: currently always using 8-bit dtypes to work around the - # padding problems for i/o DMA - dma_node = oh.make_node( - "IODMA_hls", - [final_node_out.name], - [graph_out_name], - numInputVectors=out_folded_shape[:-1], - NumChannels=padded_outstream_bytes, - dataType="UINT8", - intfWidth=intfwidth, - streamWidth=padded_outstream_width, - direction="out", - domain="finn.custom_op.fpgadataflow.hls", - backend="fpgadataflow", - ) - model.graph.node.append(dma_node) - modified = True + out_shape = model.get_tensor_shape(graph_out_name) + out_dtype = model.get_tensor_datatype(graph_out_name) + final_node_inst = getCustomOp(final_node) + out_folded_shape = final_node_inst.get_folded_output_shape() + # take advantage of AXI stream width padding for DMA alignment + # (AXI streams are always padded to 8 bits) + # this is the width of stream input to DMA + padded_outstream_width = final_node_inst.get_outstream_width_padded() + padded_outstream_bytes = padded_outstream_width // 8 + # determine the feasible interface width + transfer_bits = padded_outstream_width * np.prod(out_folded_shape[:-1]) + intfwidth = math.gcd(transfer_bits, self.max_intfwidth) + assert intfwidth % 8 == 0, "No feasible interface width for transfer size" + # make new buffer + final_node_out = oh.make_tensor_value_info( + model.make_new_valueinfo_name(), TensorProto.FLOAT, out_shape + ) + model.graph.value_info.append(final_node_out) + model.set_tensor_datatype(final_node_out.name, out_dtype) + # reroute final node output to final_node_out_name + final_node.output[0] = final_node_out.name + # FIXME: currently always using 8-bit dtypes to work around the + # padding problems for i/o DMA + dma_node = oh.make_node( + "IODMA_hls", + [final_node_out.name], + [graph_out_name], + numInputVectors=out_folded_shape[:-1], + NumChannels=padded_outstream_bytes, + dataType="UINT8", + intfWidth=intfwidth, + streamWidth=padded_outstream_width, + direction="out", + domain="finn.custom_op.fpgadataflow.hls", + backend="fpgadataflow", + ) + model.graph.node.append(dma_node) + modified = True if self.insert_extmemw: # parse matrixvectoractivation layers looking for external weights with no # attached IODMA diff --git a/src/finn/transformation/fpgadataflow/instrumentation.py b/src/finn/transformation/fpgadataflow/instrumentation.py index ed0c6a9e68..619acd59e1 100644 --- a/src/finn/transformation/fpgadataflow/instrumentation.py +++ b/src/finn/transformation/fpgadataflow/instrumentation.py @@ -82,7 +82,7 @@ def apply(self, model): ko = out_shape_folded[-1] # fill out instrumentation wrapper template with open( - os.path.join(get_settings().finn_custom_hls, "instrumentation.template.cpp"), "r" + os.path.join(get_settings().finn_custom_hls, "instrumentation.template.cpp") ) as f: instrwrp_cpp = f.read() instrwrp_cpp = instrwrp_cpp.replace("@PENDING@", str(pending)) @@ -172,7 +172,6 @@ def apply(self, model): # fill in testbench template with open( os.path.join(get_settings().finn_custom_hls, "instrumentation_tb.template.sv"), - "r", ) as f: testbench_sv = f.read() with open(sim_output_dir + "/instrwrap_testbench.sv", "w") as f: @@ -180,7 +179,6 @@ def apply(self, model): # fill in testbench project creator template with open( os.path.join(get_settings().finn_custom_hls, "instrumentation_sim.template.tcl"), - "r", ) as f: testbench_tcl = f.read() diff --git a/src/finn/transformation/fpgadataflow/loop_rolling.py b/src/finn/transformation/fpgadataflow/loop_rolling.py index acb8f1ccfa..7f7718194d 100644 --- a/src/finn/transformation/fpgadataflow/loop_rolling.py +++ b/src/finn/transformation/fpgadataflow/loop_rolling.py @@ -22,7 +22,7 @@ from qonnx.custom_op.registry import getCustomOp, is_custom_op from qonnx.transformation.base import Transformation from qonnx.transformation.fold_constants import FoldConstants -from typing import TYPE_CHECKING, List, Tuple, cast +from typing import TYPE_CHECKING, cast from finn.util import onnxscript_helpers as osh from finn.util.logging import log @@ -32,19 +32,17 @@ def get_constant_from_value(value): - """ - Get the constant value of a tensor. + """Get the constant value of a tensor. """ # Handle input and/or inititalizer values if value.producer() is None: return value.const_value.numpy() - elif value.producer().op_type == "Constant": + if value.producer().op_type == "Constant": return value.producer().attributes["value"].value.numpy() def same_values(inputs): - """ - Check if all inputs have the same constant value. + """Check if all inputs have the same constant value. """ if not inputs: return False @@ -74,16 +72,16 @@ def build_loop_replace_pattern(graph, LoopBody): for node in nodes: if node.inputs[i].shape != g_shape: log.warning( - ( + f"LoopRolling: Index {i} expected shape {g_shape}, " f"got {node.inputs[i].shape}." - ) + ) raise Exception( - ( + "LoopRolling: all loop-body initializers of the same index " "must have the same shape." - ) + ) # Build Concat Node @@ -219,7 +217,7 @@ def build_loop_replace_pattern(graph, LoopBody): class LoopExtraction(Transformation): - def __init__(self, hierarchy_list: List[List[str]]): + def __init__(self, hierarchy_list: list[list[str]]): super().__init__() assert isinstance(hierarchy_list, list), "Hierarchy list must be a list of strings" @@ -231,7 +229,7 @@ def __init__(self, hierarchy_list: List[List[str]]): self.hierarchy_list = hierarchy_list self.loop_body_template = None - def apply(self, model: ModelWrapper) -> Tuple[ModelWrapper, bool]: + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Apply the loop extraction transformation # Extract the Loop Body from ONNX metadata model_ir = onnxscript.ir.serde.deserialize_model(model.model) @@ -472,7 +470,7 @@ def __init__(self, loop_body_template): super().__init__() self.loop_body_template = loop_body_template - def apply(self, model: ModelWrapper) -> Tuple[ModelWrapper, bool]: + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: model_ir = onnxscript.ir.serde.deserialize_model(model.model) graph = model_ir.graph LoopBody = self.loop_body_template diff --git a/src/finn/transformation/fpgadataflow/make_driver.py b/src/finn/transformation/fpgadataflow/make_driver.py index e60c4d3259..c489cc883f 100644 --- a/src/finn/transformation/fpgadataflow/make_driver.py +++ b/src/finn/transformation/fpgadataflow/make_driver.py @@ -40,7 +40,6 @@ from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from string import Template -from typing import Dict, List, Optional, Tuple import finn.util from finn.builder.build_dataflow_config import FpgaMemoryType @@ -52,8 +51,7 @@ def update_bitfile_path_after_copy(bitfile_path: str, json_path: str) -> None: - """ - Update the xclbinPath in the JSON configuration to point to the new bitfile location. + """Update the xclbinPath in the JSON configuration to point to the new bitfile location. Args: json_path (str): Path to the JSON configuration file @@ -67,7 +65,7 @@ def update_bitfile_path_after_copy(bitfile_path: str, json_path: str) -> None: raise FINNInternalError("Provided path is not a JSON file.") # Read the current JSON configuration - with open(json_path, "r") as f: + with open(json_path) as f: data = json.load(f) # Update the xclbinPath for each device in the configuration @@ -107,16 +105,15 @@ def resolve_dt_name(s: str) -> str: s = s.replace("DataType[", "").replace("]", "") if s in ["BINARY", "TERNARY", "BIPOLAR"]: return "Datatype" + s[0] + s[1:].lower() - elif s.startswith("U"): + if s.startswith("U"): return "DatatypeUInt<" + s.replace("UINT", "") + ">" - elif s.startswith("I"): + if s.startswith("I"): return "DatatypeInt<" + s.replace("INT", "") + ">" - elif "FLOAT" in s: + if "FLOAT" in s: return "DatatypeFloat<" + s.replace("FLOAT", "") + ">" - elif "FIXED" in s: + if "FIXED" in s: return "DatatypeFixed" + s.replace("FIXED", "") - else: - raise FINNInternalError(f"Unknown datatype for C++ Driver:{s}") + raise FINNInternalError(f"Unknown datatype for C++ Driver:{s}") def __init__( self, @@ -155,7 +152,7 @@ def __init__( else: self.host_memory = False - def apply(self, model: ModelWrapper) -> Tuple[ModelWrapper, bool]: + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Apply the MakeCPPDriver transformation to generate C++ driver code. Args: @@ -164,7 +161,7 @@ def apply(self, model: ModelWrapper) -> Tuple[ModelWrapper, bool]: Returns: Tuple of (modified model, transformation success flag) """ - driver_shapes: Dict = get_driver_shapes(model) + driver_shapes: dict = get_driver_shapes(model) ext_weight_dma_cnt: int # noqa weights_dir: str # noqa # TODO: Enable weight file generation @@ -232,7 +229,6 @@ def run_command(command, cwd=None, debug=False): os.path.join( cpp_driver_dir, "src", "FINNCppDriver", "config", "FinnDriverUsedDatatypes.h.in" ), - "r", ) as f_in: header = f_in.read() template_handler = Template(header) @@ -343,7 +339,7 @@ def configure_cmake( source_dir: str, # Directory containing CMakeLists.txt build_dir: str, # Directory where build files will be generated # Additional CMake arguments as string - cmake_args: Optional[str] = None, + cmake_args: str | None = None, # Command to invoke CMake cmake_executable: str = f"{sys.executable} -m cmake", ): @@ -381,9 +377,9 @@ def build_cmake( # Build tool to use (default: make) cmake_executable: str = "make", # Specific target to build (if any) - build_target: Optional[str] = None, + build_target: str | None = None, # Additional build arguments - build_args: Optional[List[str]] = None, + build_args: list[str] | None = None, ): """Build the configured CMake project. @@ -452,7 +448,7 @@ def check_finn_types(bin_dir: str, expectedInputType: str, expectedOutputType: s """ # Run the built finnhpc executable with the --check flag to output datatype information result = subprocess.run( - "./finnhpc --check".split(), cwd=bin_dir, capture_output=True, text=True + ["./finnhpc", "--check"], cwd=bin_dir, capture_output=True, text=True ) if result.returncode != 0: log.critical(f"Running datatype check failed with error:\n{result.stderr}") @@ -714,7 +710,7 @@ def _write_fifo_widths(self, model): # so that the driver can generate a final cfg with live fifo sizes applied folding_path = model.get_metadata_prop("folding_config_before_lfs") if folding_path: - with open(folding_path, "r") as f: + with open(folding_path) as f: folding_cfg = json.load(f) settings["folding_config_before_lfs"] = folding_cfg @@ -740,13 +736,13 @@ def apply(self, model): experiment_information = {} if self.experiment_info is not None: - with open(self.experiment_info, "r") as f: + with open(self.experiment_info) as f: experiment_information = json.load(f) driver_information["driver_type"] = self.driver_type if self.driver_type in ["FINNDMAOverlay", "FINNDMAInstrumentationOverlay"]: external_weights_dict, runtime_weights = self._generate_weight_files(model) - driver_shapes: Dict = get_driver_shapes(model) + driver_shapes: dict = get_driver_shapes(model) driver_information["io_shape_dict"] = driver_shapes driver_information["io_shape_dict"]["num_inputs"] = len(driver_shapes["idma_names"]) driver_information["io_shape_dict"]["num_outputs"] = len(driver_shapes["odma_names"]) diff --git a/src/finn/transformation/fpgadataflow/make_zynq_proj.py b/src/finn/transformation/fpgadataflow/make_zynq_proj.py index 1cffbaa26a..cdecb71a13 100644 --- a/src/finn/transformation/fpgadataflow/make_zynq_proj.py +++ b/src/finn/transformation/fpgadataflow/make_zynq_proj.py @@ -566,9 +566,9 @@ def apply(self, model): working_dir = os.getcwd() with open(synth_project_sh, "w") as f: f.write("#!/bin/bash \n") - f.write("cd {}\n".format(vivado_pynq_proj_dir)) + f.write(f"cd {vivado_pynq_proj_dir}\n") f.write("vivado -mode batch -source %s\n" % ipcfg) - f.write("cd {}\n".format(working_dir)) + f.write(f"cd {working_dir}\n") # call the synthesis script bash_command = ["bash", synth_project_sh] diff --git a/src/finn/transformation/fpgadataflow/prepare_cppsim.py b/src/finn/transformation/fpgadataflow/prepare_cppsim.py index d4cc6dcc99..8ad9b55d53 100644 --- a/src/finn/transformation/fpgadataflow/prepare_cppsim.py +++ b/src/finn/transformation/fpgadataflow/prepare_cppsim.py @@ -41,7 +41,6 @@ def _codegen_single_node(node, model): """Calls C++ code generation for one node. Resulting code can be used to simulate node using cppsim.""" - op_type = node.op_type try: # lookup op_type in registry of CustomOps diff --git a/src/finn/transformation/fpgadataflow/prepare_ip.py b/src/finn/transformation/fpgadataflow/prepare_ip.py index 57539c9afc..03d4f34937 100644 --- a/src/finn/transformation/fpgadataflow/prepare_ip.py +++ b/src/finn/transformation/fpgadataflow/prepare_ip.py @@ -39,7 +39,6 @@ def _codegen_single_node(node, model, fpgapart, clk): """Calls C++ code generation for one node. Resulting code can be used to generate a Vivado IP block for the node.""" - op_type = node.op_type try: # lookup op_type in registry of CustomOps diff --git a/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py b/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py index 06b0c9c003..a136feda37 100644 --- a/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py +++ b/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py @@ -31,7 +31,7 @@ from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation -from typing import Iterable +from collections.abc import Iterable import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw diff --git a/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py b/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py index de13166e73..99bfe8bfc0 100644 --- a/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py +++ b/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py @@ -53,7 +53,7 @@ def apply(self, model): for fname in files: if fname.endswith(".v"): fpath = os.path.join(dname, fname) - with open(fpath, "r") as f: + with open(fpath) as f: s = f.read() old = '$readmemh(".' new = '$readmemh("%s' % dname diff --git a/src/finn/transformation/fpgadataflow/set_loop_boundary.py b/src/finn/transformation/fpgadataflow/set_loop_boundary.py index ee2c9572f3..5f300f8b4b 100644 --- a/src/finn/transformation/fpgadataflow/set_loop_boundary.py +++ b/src/finn/transformation/fpgadataflow/set_loop_boundary.py @@ -11,8 +11,7 @@ class SetLoopBoundary(Transformation): - """ - Sets metadata attributes to nodes between defined node or tensor ranges in an ONNX model. + """Sets metadata attributes to nodes between defined node or tensor ranges in an ONNX model. :param node_metadata: Dictionary containing metadata attributes to set on the nodes. :param node_range: Tuple containing start and end node names (start_node, end_node). diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index a597918c45..1e5de21135 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -158,7 +158,7 @@ def write_log(msg: str) -> None: response = self._send_and_receive(proc_idx, "start", {}) if response is None: write_log( - "No answer for the clients 'start' " "command received. Timeout or disconnect." + "No answer for the clients 'start' command received. Timeout or disconnect." ) return None write_log(f"Start response: {response}") @@ -600,7 +600,7 @@ def get_index(a: Any, values: Any) -> int | None: node.name ][ key - ] # noqa + ] log.info( f"Incoming FIFO {node.name}[{key}/{consumer_idx}] " f"-> outgoing FIFO {predecessor.name}[{producer_idx}]" diff --git a/src/finn/transformation/fpgadataflow/specialize_layers.py b/src/finn/transformation/fpgadataflow/specialize_layers.py index 92cd0f2ea6..002c589eeb 100644 --- a/src/finn/transformation/fpgadataflow/specialize_layers.py +++ b/src/finn/transformation/fpgadataflow/specialize_layers.py @@ -74,51 +74,43 @@ def _determine_impl_style(node, fpgapart, model): weight_width_fit = wdt.bitwidth() >= 4 if inp_width_fit and weight_width_fit and _mvu_rtl_possible(node, fpgapart, model): return "rtl" - else: - return "hls" - elif optype == "VVAU": + return "hls" + if optype == "VVAU": idt = node_inst.get_input_datatype(0) wdt = node_inst.get_input_datatype(1) inp_width_fit = idt.bitwidth() >= 4 weight_width_fit = wdt.bitwidth() >= 4 if inp_width_fit and weight_width_fit and _vvu_rtl_possible(node, fpgapart): return "rtl" - else: - return "hls" - elif optype in ["ElementwiseAdd", "ElementwiseSub", "ElementwiseMul"]: + return "hls" + if optype in ["ElementwiseAdd", "ElementwiseSub", "ElementwiseMul"]: if _elementwise_rtl_possible(node, fpgapart): return "rtl" - else: - return "hls" - elif optype == "LayerNorm": + return "hls" + if optype == "LayerNorm": if _layernorm_rtl_possible(node, fpgapart): return "rtl" - else: - return "hls" - elif optype == "Requant": + return "hls" + if optype == "Requant": if _requant_rtl_possible(node, fpgapart): return "rtl" - else: - return "hls" + return "hls" return "rtl" # but if no rtl variant, set impl_style to hls - elif hls_variant: + if hls_variant: return "hls" # if there is neither an rtl nor hls variant # throw error - else: - raise Exception( - """Node {} with optype {} has no hw implementation variant)""".format( - node.name, optype - ) - ) + raise Exception( + f"""Node {node.name} with optype {optype} has no hw implementation variant)""" + ) # check if user setting can be fulfilled # otherwise change impl_style - elif impl_style == "hls": + if impl_style == "hls": if hls_variant: return "hls" - elif rtl_variant: + if rtl_variant: warn_str = """There is no HLS variant of %s. Node %s will automatically be set to RTL variant.""" % ( node.op_type, @@ -126,13 +118,10 @@ def _determine_impl_style(node, fpgapart, model): ) log.warning(warn_str) return "rtl" - else: - raise Exception( - """Node {} with optype {} has no hw implementation variant)""".format( - node.name, optype - ) - ) - elif impl_style == "rtl": + raise Exception( + f"""Node {node.name} with optype {optype} has no hw implementation variant)""" + ) + if impl_style == "rtl": # rtl dwc does not support every inWidth to outWidth ratio if optype == "StreamingDataWidthConverter": if _dwc_determine_impl_style(node) != "rtl": @@ -144,70 +133,64 @@ def _determine_impl_style(node, fpgapart, model): ) log.warning(warn_str) return "hls" - else: - # user setting can be fulfilled - return "rtl" - elif optype == "MVAU": + # user setting can be fulfilled + return "rtl" + if optype == "MVAU": if _mvu_rtl_possible(node, fpgapart, model): return "rtl" - else: - warn_str = """There is no RTL variant for %s. The node will automatically be + warn_str = """There is no RTL variant for %s. The node will automatically be set to HLS variant. Please check the bit-widths to be <= 8 and ensure the thresholds are implemented as standalone layer""" % ( - node.name, - ) - log.warning(warn_str) - return "hls" - elif optype == "VVAU": + node.name, + ) + log.warning(warn_str) + return "hls" + if optype == "VVAU": if _vvu_rtl_possible(node, fpgapart): return "rtl" - else: - warn_str = """There is no RTL variant for %s. The node will automatically be + warn_str = """There is no RTL variant for %s. The node will automatically be set to HLS variant. Please check the bit-widths to be <= 8 and ensure the thresholds are implemented as standalone layer. Note that the RTL-variant of this layer is only supported on Versal boards""" % ( - node.name, - ) - log.warning(warn_str) - return "hls" + node.name, + ) + log.warning(warn_str) + return "hls" - elif optype == "LayerNorm": + if optype == "LayerNorm": if _layernorm_rtl_possible(node, fpgapart): return "rtl" - else: - warn_str = """There is no RTL variant for %s. The node will automatically be + warn_str = """There is no RTL variant for %s. The node will automatically be set to HLS variant. The RTL Layernorm layer currently only supports float32 inputs and uses DSP58, so only versal devices supported.""" % ( - node.name, - ) - log.warning(warn_str) - return "hls" - elif optype in ["ElementwiseAdd", "ElementwiseSub", "ElementwiseMul"]: + node.name, + ) + log.warning(warn_str) + return "hls" + if optype in ["ElementwiseAdd", "ElementwiseSub", "ElementwiseMul"]: if _elementwise_rtl_possible(node, fpgapart): return "rtl" - else: - warn_str = """There is no RTL variant for %s. The node will automatically be + warn_str = """There is no RTL variant for %s. The node will automatically be set to HLS variant. The RTL Elementwise layers currently only supports float32 inputs and use DSP58, so only versal devices supported.""" % ( - node.name, - ) - log.warning(warn_str) - return "hls" - elif optype == "Requant": + node.name, + ) + log.warning(warn_str) + return "hls" + if optype == "Requant": if _requant_rtl_possible(node, fpgapart): return "rtl" - else: - warn_str = """There is no RTL variant for %s. The node will automatically be + warn_str = """There is no RTL variant for %s. The node will automatically be set to HLS variant. The RTL Requant layers currently only supports integer inputs, unsigned outputs and non-narrow quantization.""" % ( - node.name, - ) - log.warning(warn_str) - return "hls" + node.name, + ) + log.warning(warn_str) + return "hls" if rtl_variant: return "rtl" - elif hls_variant: + if hls_variant: warn_str = """There is no RTL variant of %s. Node %s will automatically be set to HLS variant.""" % ( node.op_type, @@ -215,19 +198,13 @@ def _determine_impl_style(node, fpgapart, model): ) log.warning(warn_str) return "hls" - else: - raise Exception( - """Node {} with optype {} has no hw implementation variant)""".format( - node.name, optype - ) - ) - else: raise Exception( - """Invalid value for attribute preferred_impl_style! Is currently set to: {} - has to be set to one of the following value ("hls", "rtl")""".format( - impl_style - ) + f"""Node {node.name} with optype {optype} has no hw implementation variant)""" ) + raise Exception( + f"""Invalid value for attribute preferred_impl_style! Is currently set to: {impl_style} + has to be set to one of the following value ("hls", "rtl")""" + ) def _dwc_determine_impl_style(node): @@ -243,8 +220,7 @@ def _dwc_determine_impl_style(node): owidth_d = dwc_out_width % dwc_in_width == 0 if iwidth_d or owidth_d: return "rtl" - else: - return "hls" + return "hls" def _mvu_rtl_possible(n, fpgapart, model): @@ -349,8 +325,7 @@ def _elementwise_rtl_possible(n, fpgapart): if dim_c != 1 and dim_c != dim_o: return False return True - else: - return False + return False def _layernorm_rtl_possible(n, fpgapart): @@ -362,8 +337,7 @@ def _layernorm_rtl_possible(n, fpgapart): idt = node_inst.get_input_datatype(0) if idt != "FLOAT32": return False - else: - return True + return True def _requant_rtl_possible(n, fpgapart): diff --git a/src/finn/transformation/fpgadataflow/transpose_decomposition.py b/src/finn/transformation/fpgadataflow/transpose_decomposition.py index d3d7fb5f1a..db8aa42e93 100644 --- a/src/finn/transformation/fpgadataflow/transpose_decomposition.py +++ b/src/finn/transformation/fpgadataflow/transpose_decomposition.py @@ -14,13 +14,11 @@ from qonnx.transformation.base import Transformation from qonnx.transformation.infer_datatypes import InferDataTypes from qonnx.transformation.infer_shapes import InferShapes -from typing import List, Optional, Tuple from finn.util.logging import log def shuffle_perfect_loopnest_coeffs(shape: tuple[int], perm: tuple[int]) -> tuple[int]: - """ - Given an input shape and permutation matrix calculate the + """Given an input shape and permutation matrix calculate the coefficients for the perfect loop nest for HLS generation. """ adjusted_shape = list(shape) + [1] @@ -30,10 +28,9 @@ def shuffle_perfect_loopnest_coeffs(shape: tuple[int], perm: tuple[int]) -> tupl def apply_inner_shuffle_operation( - perm: List[int], shape: List[int] = None, simd: int = 1 -) -> List[int]: - """ - Apply inner_shuffle operation: swap the last two positions + perm: list[int], shape: list[int] = None, simd: int = 1 +) -> list[int]: + """Apply inner_shuffle operation: swap the last two positions (..., a, b) -> (..., b, a) """ if len(perm) < 2: @@ -52,10 +49,9 @@ def apply_inner_shuffle_operation( def apply_outer_shuffle_operation( - perm: List[int], i: int, j: int, shape: List[int] = None, simd: int = 1 -) -> Optional[List[int]]: - """ - Apply outer_shuffle operation: swap positions i and j + perm: list[int], i: int, j: int, shape: list[int] = None, simd: int = 1 +) -> list[int] | None: + """Apply outer_shuffle operation: swap positions i and j Constraint: cannot move the very last dimension """ n = len(perm) @@ -83,10 +79,9 @@ def apply_outer_shuffle_operation( def get_all_possible_moves( - perm: List[int], shape: List[int] = None, simd: int = 1 -) -> List[Tuple[List[int], str, Optional[Tuple[int, int]]]]: - """ - Get all possible moves from current permutation. + perm: list[int], shape: list[int] = None, simd: int = 1 +) -> list[tuple[list[int], str, tuple[int, int] | None]]: + """Get all possible moves from current permutation. Returns list of (new_permutation, operation_type, operation_params) tuples. Each outer_shuffle move represents a single pairwise swap that doesn't @@ -114,9 +109,8 @@ def get_all_possible_moves( return moves -def is_valid_hardware_permutation(perm_array: List[int]) -> bool: - """ - Check if a permutation array represents a valid hardware operation. +def is_valid_hardware_permutation(perm_array: list[int]) -> bool: + """Check if a permutation array represents a valid hardware operation. Valid operations are: - inner_shuffle: swap last two elements - outer_shuffle: any permutation that doesn't move the last element @@ -150,13 +144,12 @@ def is_valid_hardware_permutation(perm_array: List[int]) -> bool: def find_minimal_operation_sequence( - start_perm: List[int], - target_perm: List[int], - shape: List[int] = None, + start_perm: list[int], + target_perm: list[int], + shape: list[int] = None, simd: int = 1, -) -> Optional[List[Tuple[str, Optional[Tuple[int, int]]]]]: - """ - Find minimal sequence of operations to transform start_perm into target_perm. +) -> list[tuple[str, tuple[int, int] | None]] | None: + """Find minimal sequence of operations to transform start_perm into target_perm. Uses BFS to find shortest path, ensuring all intermediate permutations are hardware-valid. Returns list of (operation_type, operation_params) tuples. @@ -194,13 +187,12 @@ def find_minimal_operation_sequence( def convert_operations_to_permutations( - start_perm: List[int], - operations: List[Tuple[str, Optional[Tuple[int, int]]]], - shape: List[int] = None, + start_perm: list[int], + operations: list[tuple[str, tuple[int, int] | None]], + shape: list[int] = None, simd: int = 1, -) -> List[List[int]]: - """ - Convert a sequence of operations to a list of permutation arrays. +) -> list[list[int]]: + """Convert a sequence of operations to a list of permutation arrays. Each permutation represents the transformation for that step. """ current_perm = start_perm[:] @@ -229,10 +221,9 @@ def convert_operations_to_permutations( def can_be_single_operation( - target_perm: List[int], shape: List[int] = None, simd: int = 1 -) -> Optional[Tuple[str, Optional[Tuple[int, int]]]]: - """ - Check if the target permutation can be achieved with a single operation. + target_perm: list[int], shape: list[int] = None, simd: int = 1 +) -> tuple[str, tuple[int, int] | None] | None: + """Check if the target permutation can be achieved with a single operation. i.e. no decomposition is required. Returns (operation_type, operation_params) or None if not possible. """ @@ -260,10 +251,9 @@ def can_be_single_operation( def decompose_transpose_with_constraints( - target_perm: List[int], shape: List[int] = None, simd: int = 1 -) -> Tuple[List[List[int]], List[str]]: - """ - Decompose a target permutation into a sequence of hardware-constrained + target_perm: list[int], shape: list[int] = None, simd: int = 1 +) -> tuple[list[list[int]], list[str]]: + """Decompose a target permutation into a sequence of hardware-constrained operations. inner_shuffle: swaps the last two dimensions @@ -306,8 +296,7 @@ def decompose_transpose_with_constraints( class ShuffleDecomposition(Transformation): - """ - Transformation that decomposes Shuffle nodes into + """Transformation that decomposes Shuffle nodes into a chain of Shuffle ops that can map to InnerShuffle and OuterShuffle nodes. """ @@ -321,7 +310,7 @@ def _unique(self, base): self._name_counter += 1 return f"{base}_{self._name_counter}" - def get_perm(self, node) -> List[int]: + def get_perm(self, node) -> list[int]: for a in node.attribute: if a.name == "perm": return list(a.ints) @@ -420,8 +409,7 @@ def apply(self, model): def _is_inner_shuffle(perm, shape): - """ - Check if the permutation represents a streaming InnerShuffle case. + """Check if the permutation represents a streaming InnerShuffle case. A streaming InnerShuffle is only possible when only the last two dimensions are swapped, regardless of how many outer dimensions there are. """ @@ -434,8 +422,7 @@ def _is_inner_shuffle(perm, shape): class InferInnerOuterShuffles(Transformation): - """ - Infers Inner and Outer Shuffles from Shuffle operators. + """Infers Inner and Outer Shuffles from Shuffle operators. This should run after the ShuffleDecomposition transformation. """ diff --git a/src/finn/transformation/fpgadataflow/vitis_build.py b/src/finn/transformation/fpgadataflow/vitis_build.py index c6ef183563..9a75dc3860 100644 --- a/src/finn/transformation/fpgadataflow/vitis_build.py +++ b/src/finn/transformation/fpgadataflow/vitis_build.py @@ -146,9 +146,9 @@ def apply(self, model): with open(package_xo_sh, "w") as f: f.write("#!/bin/bash \n") f.write("set -e\n") - f.write("cd {}\n".format(vivado_proj_dir)) + f.write(f"cd {vivado_proj_dir}\n") f.write("vivado -mode batch -source gen_xo.tcl\n") - f.write("cd {}\n".format(working_dir)) + f.write(f"cd {working_dir}\n") bash_command = ["bash", package_xo_sh] try: launch_process_helper(bash_command, print_stdout=False) @@ -324,7 +324,7 @@ def apply(self, model): with open(script, "w") as f: f.write("#!/bin/bash \n") f.write("set -e\n") - f.write("cd {}\n".format(link_dir)) + f.write(f"cd {link_dir}\n") f.write( "v++ -t hw --platform %s --link %s" " --kernel_frequency %d --config config.txt --optimize %s" @@ -337,7 +337,7 @@ def apply(self, model): " ".join(debug_commands), ) ) - f.write("cd {}\n".format(working_dir)) + f.write(f"cd {working_dir}\n") bash_command = ["bash", script] try: @@ -362,10 +362,10 @@ def apply(self, model): working_dir = os.getcwd() with open(gen_rep_xml_sh, "w") as f: f.write("#!/bin/bash \n") - f.write("cd {}\n".format(link_dir)) + f.write(f"cd {link_dir}\n") f.write("set -e\n") f.write("vivado -mode batch -source %s\n" % (link_dir + "/gen_report_xml.tcl")) - f.write("cd {}\n".format(working_dir)) + f.write(f"cd {working_dir}\n") bash_command = ["bash", gen_rep_xml_sh] try: launch_process_helper(bash_command, print_stdout=False) diff --git a/src/finn/transformation/fpgadataflow/vivado_power_estimation.py b/src/finn/transformation/fpgadataflow/vivado_power_estimation.py index e7094247ef..7f78c2f10b 100644 --- a/src/finn/transformation/fpgadataflow/vivado_power_estimation.py +++ b/src/finn/transformation/fpgadataflow/vivado_power_estimation.py @@ -75,7 +75,7 @@ def apply(self, model): testbench = testbench.replace("$OUTSTREAM_WIDTH$", str(out_width)) testbench = testbench.replace("$DTYPE_WIDTH$", str(dtype_width)) testbench = testbench.replace( - "$RANDOM_FUNCTION$", "$urandom_range(0, {max})".format(max=2**dtype_width - 1) + "$RANDOM_FUNCTION$", f"$urandom_range(0, {2**dtype_width - 1})" ) with open(tmp_dir + "/switching_simulation_tb.v", "w") as tb_file: tb_file.write(testbench) diff --git a/src/finn/transformation/qonnx/fold_quant_weights.py b/src/finn/transformation/qonnx/fold_quant_weights.py index 2c26b7614e..f948f64bb2 100644 --- a/src/finn/transformation/qonnx/fold_quant_weights.py +++ b/src/finn/transformation/qonnx/fold_quant_weights.py @@ -61,7 +61,7 @@ def apply(self, model): # Check node validity if n.op_type == "Quant" and not model.get_initializer(n.input[2]) == 0: raise ValueError( - "Only Quant nodes with zero-point == 0 " "are currently supported." + "Only Quant nodes with zero-point == 0 are currently supported." ) if model.is_fork_node(n): raise ValueError( @@ -71,10 +71,9 @@ def apply(self, model): target_node = model.find_direct_successors(n) if target_node is None: raise RuntimeError( - "Weights quantized with the Quant node must have " "a successor node." + "Weights quantized with the Quant node must have a successor node." ) - else: - target_node = target_node[0] + target_node = target_node[0] # If there is a DebugMarker in the weight path, # then the DebugMarker needs to be removed before any further # action is taken. Because this node interferes diff --git a/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py b/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py index 385f4bb3c1..4b8b3d9f91 100644 --- a/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py +++ b/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py @@ -127,19 +127,17 @@ def apply(self, model): if trunc_opset == 1: model = model.transform(AvgPoolAndTruncv1ToQuantAvgPool()) return model, False - elif trunc_opset == 2: + if trunc_opset == 2: model = model.transform(AvgPoolAndTruncv2ToQuantAvgPool()) return model, False - else: - raise NotImplementedError( - f"AvgPoolAndTruncToQuantAvgPool not implemented for " - f"Trunc opset version {trunc_opset}." - ) + raise NotImplementedError( + f"AvgPoolAndTruncToQuantAvgPool not implemented for " + f"Trunc opset version {trunc_opset}." + ) class AvgPoolAndTruncv1ToQuantAvgPool(Transformation): - """ - Convert a section of nodes of the pattern: + """Convert a section of nodes of the pattern: AveragePool -> Mul (scalar) -> Trunc (v1) To the FINN op: Div -> QuantAvgPool2d -> Mul """ diff --git a/src/finn/transformation/qonnx/qonnx_activation_handlers.py b/src/finn/transformation/qonnx/qonnx_activation_handlers.py index b623fc66a1..bf78abf9ae 100644 --- a/src/finn/transformation/qonnx/qonnx_activation_handlers.py +++ b/src/finn/transformation/qonnx/qonnx_activation_handlers.py @@ -113,7 +113,6 @@ def calculate_node_parameters(self): def replace_quant_node(self): """Replace the given QONNX style activation with a FINN style one.""" - # Check that we actually support what the user is trying to do self._check_compatibility() @@ -406,7 +405,7 @@ def _calculate_thresholds(self): # Get the shape of the input (should also be the output) tensor # Note: Querying the input is more safe as we do not want to # propagate shapes backwards by accident. - shape = self._model.get_tensor_shape(self._q_node.input[0]) # noqa + shape = self._model.get_tensor_shape(self._q_node.input[0]) # First try to consider the tensor layout of the input for # determining the number of output channels layout = self._model.get_tensor_layout(self._q_node.input[0]) @@ -458,7 +457,7 @@ def _remove_activation_node(self, multi_threshold_node): act_node = self._model.find_direct_predecessors(self._q_node) if act_node is None: raise RuntimeError( - "For handling of Relu activations a predecessor to " "the Quant node must exist." + "For handling of Relu activations a predecessor to the Quant node must exist." ) act_node = act_node[0] if act_node.op_type not in self.valid_predecessor_op_types(): @@ -557,67 +556,66 @@ def _calculate_thresholds(self): thresholds = np.empty([1, 1], dtype=np_default_dtype) thresholds[0] = 0 return thresholds + if narrow: + num_distinct_values = 2**bit_width - 1 else: - if narrow: - num_distinct_values = 2**bit_width - 1 - else: - num_distinct_values = 2**bit_width + num_distinct_values = 2**bit_width - num_thresholds = int(num_distinct_values - 1) - flat_scale = quant_scale.flatten() - num_scale_channels = flat_scale.shape[0] - step = np.abs(flat_scale) - half_step = step / 2.0 - thresholds = np.empty((num_scale_channels, num_thresholds), dtype=np_default_dtype) - # compute the value of the smallest threshold, we'll neg-bias all - # generated thresholds by this much - min_threshold = -half_step - step * ((num_thresholds // 2) - 1) - if not narrow: - min_threshold -= step - for c in range(num_scale_channels): - for t in range(num_thresholds): - thresholds[c][t] = min_threshold[c] + step[c] * t + num_thresholds = int(num_distinct_values - 1) + flat_scale = quant_scale.flatten() + num_scale_channels = flat_scale.shape[0] + step = np.abs(flat_scale) + half_step = step / 2.0 + thresholds = np.empty((num_scale_channels, num_thresholds), dtype=np_default_dtype) + # compute the value of the smallest threshold, we'll neg-bias all + # generated thresholds by this much + min_threshold = -half_step - step * ((num_thresholds // 2) - 1) + if not narrow: + min_threshold -= step + for c in range(num_scale_channels): + for t in range(num_thresholds): + thresholds[c][t] = min_threshold[c] + step[c] * t - # Get the shape of the input (should also be the output) tensor - # Note: Querying the input is more safe as we do not want to - # propagate shapes backwards by accident. - shape = self._model.get_tensor_shape(self._q_node.input[0]) - # First try to consider the tensor layout of the input for - # determining the number of output channels - layout = self._model.get_tensor_layout(self._q_node.input[0]) # noqa - # If there is no layout annotation, guess based on rank of the - # tensor - # TODO: No support for Rank >= 5 - if layout is None and len(shape) < 5: - # Maps tensor rank to layout annotation - rank_to_layout = {0: None, 1: "C", 2: "NC", 3: "NWC", 4: "NCHW"} - # Lookup the layout required by this input shape - layout = rank_to_layout[len(shape)] - # If there is a layout annotation, use this to determine the index - # of the channel dimension - if layout is not None and "C" in layout: # noqa: Duplicate - # Lookup the index in list - cdim = layout.index("C") - # If no layout has been annotated or there is no channel dimension, - # fall back to the previous default assumption - else: - # Assume the channels to be in axis 1 - cdim = 1 - # Issue a warning to the user, so they are aware of this - log.warning( - f"No layout annotations for {self._q_node.input[0]}:" - f" Assuming channel dimension at index {cdim}" - ) + # Get the shape of the input (should also be the output) tensor + # Note: Querying the input is more safe as we do not want to + # propagate shapes backwards by accident. + shape = self._model.get_tensor_shape(self._q_node.input[0]) + # First try to consider the tensor layout of the input for + # determining the number of output channels + layout = self._model.get_tensor_layout(self._q_node.input[0]) + # If there is no layout annotation, guess based on rank of the + # tensor + # TODO: No support for Rank >= 5 + if layout is None and len(shape) < 5: + # Maps tensor rank to layout annotation + rank_to_layout = {0: None, 1: "C", 2: "NC", 3: "NWC", 4: "NCHW"} + # Lookup the layout required by this input shape + layout = rank_to_layout[len(shape)] + # If there is a layout annotation, use this to determine the index + # of the channel dimension + if layout is not None and "C" in layout: # noqa: Duplicate + # Lookup the index in list + cdim = layout.index("C") + # If no layout has been annotated or there is no channel dimension, + # fall back to the previous default assumption + else: + # Assume the channels to be in axis 1 + cdim = 1 + # Issue a warning to the user, so they are aware of this + log.warning( + f"No layout annotations for {self._q_node.input[0]}:" + f" Assuming channel dimension at index {cdim}" + ) - # ToDo: The index 1 needs to be changed to -1 for the channels last format - num_output_channels = self._model.get_tensor_shape(self._q_node.output[0])[cdim] + # ToDo: The index 1 needs to be changed to -1 for the channels last format + num_output_channels = self._model.get_tensor_shape(self._q_node.output[0])[cdim] - assert ( - thresholds.shape[0] == 1 or thresholds.shape[0] == num_output_channels - ), """Quant node cannot be converted to MultiThreshold because only + assert ( + thresholds.shape[0] == 1 or thresholds.shape[0] == num_output_channels + ), """Quant node cannot be converted to MultiThreshold because only per tensor or per channel quantization supported.""" - return thresholds + return thresholds def _calculate_act_scale(self): # Gather parameters diff --git a/src/finn/transformation/qonnx/quant_act_to_multithreshold.py b/src/finn/transformation/qonnx/quant_act_to_multithreshold.py index 3c6c4a1b5a..0998bca073 100644 --- a/src/finn/transformation/qonnx/quant_act_to_multithreshold.py +++ b/src/finn/transformation/qonnx/quant_act_to_multithreshold.py @@ -36,8 +36,7 @@ def default_filter_function_generator(max_multithreshold_bit_width=8): - """ - This function generates the default filter function for the + """This function generates the default filter function for the ConvertQuantActToMultiThreshold transformation. Per default the returned function disables the conversion of Quant nodes which have a bit width above 8 bit. diff --git a/src/finn/transformation/squeeze.py b/src/finn/transformation/squeeze.py index a982d3e856..d2dd7018ec 100644 --- a/src/finn/transformation/squeeze.py +++ b/src/finn/transformation/squeeze.py @@ -36,8 +36,7 @@ class Squeeze(Transformation): - """ - Squeezes, i.e., removes, dimensions of size 1 + """Squeezes, i.e., removes, dimensions of size 1 Note: Use this transformation with great care, it currently serves only the purpose of turning the not well-supported 3d data layouts encountered in transformer models with batch dimension of size 1 into 2d data layouts where @@ -149,7 +148,7 @@ def apply(self, model: ModelWrapper): # noqa # Need to squeeze the number of inputs to multi-head splitting if node.op_type == "SplitMultiHeads": # Get number of input feature maps to the merging operation - num_inputs = get_by_name(node.attribute, "num_inputs") # noqa + num_inputs = get_by_name(node.attribute, "num_inputs") # Squeeze all dimensions of size 1 new_num_inputs = [size for size in num_inputs.ints if size != 1] # Update the attribute by removing and reinserting @@ -191,7 +190,7 @@ def apply(self, model: ModelWrapper): # noqa # Set squeezed mode attribute node.attribute.append(oh.make_attribute("squeezed", True)) # Get number of input feature maps to the merging operation - num_inputs = get_by_name(node.attribute, "num_inputs") # noqa + num_inputs = get_by_name(node.attribute, "num_inputs") # Squeeze all dimensions of size 1 new_num_inputs = [size for size in num_inputs.ints if size != 1] # Update the attribute by removing and reinserting @@ -438,7 +437,7 @@ def apply(self, model: ModelWrapper): # noqa # model graph model = model.transform(InferShapes()) model = model.transform(InferDataTypes()) - model = model.transform((InferDataLayouts())) + model = model.transform(InferDataLayouts()) # Return the transformed model and indicate whether this transformation # needs to be repeated # Note: Never repeat this transformation as it might break when diff --git a/src/finn/transformation/streamline/absorb.py b/src/finn/transformation/streamline/absorb.py index 77fddb03da..817c5db43a 100644 --- a/src/finn/transformation/streamline/absorb.py +++ b/src/finn/transformation/streamline/absorb.py @@ -508,7 +508,7 @@ def apply(self, model): please set tensor data layout.""" ) continue - elif data_layout == DataLayout.NCHW: + if data_layout == DataLayout.NCHW: (b, c, h, w) = model.get_tensor_shape(prod.input[0]) # if h=w=1 the transposition can be absorbed, otherwise # the absorption would lead to an error in the behavior diff --git a/src/finn/transformation/streamline/remove.py b/src/finn/transformation/streamline/remove.py index a392f9a4ef..e9b25691fb 100644 --- a/src/finn/transformation/streamline/remove.py +++ b/src/finn/transformation/streamline/remove.py @@ -40,7 +40,7 @@ def apply(self, model: ModelWrapper): # noqa inp = model.get_tensor_shape(node.input[0]) # If input and target shape are the same, this is an # identity operation - if len(shape) == len(inp) and (shape == inp).all(): # noqa + if len(shape) == len(inp) and (shape == inp).all(): # Remove and rewire this node remove_node_and_rewire(model, node) # Track whether the graph has been modified, never diff --git a/src/finn/transformation/streamline/reorder.py b/src/finn/transformation/streamline/reorder.py index 66ba2ee417..d36adee2b6 100644 --- a/src/finn/transformation/streamline/reorder.py +++ b/src/finn/transformation/streamline/reorder.py @@ -799,8 +799,7 @@ def apply(self, model): class MakeScaleResizeNHWC(Transformation): - """ - Converts the inputs and outputs for all scales Resize and Upsample nodes + """Converts the inputs and outputs for all scales Resize and Upsample nodes from NCHW to NHWC. """ @@ -1001,8 +1000,7 @@ def permute_shape(shape, perm): class MoveScalarLinearPastSplit(Transformation): - """ - Move scalar Mul and Add nodes past channel split operation. + """Move scalar Mul and Add nodes past channel split operation. """ def __init__(self): @@ -1135,7 +1133,7 @@ def apply(self, model): T = model.get_initializer(consumer.input[1]) T_sorted = np.sort(T, axis=1) assert ( - T == T_sorted + T_sorted == T ).all(), "MultiThreshold must have non-decreasing thresholds" mt_inst = getCustomOp(consumer) if mt_inst.get_nodeattr("out_scale") < 0: @@ -1375,8 +1373,7 @@ def apply(self, model): class MoveIdenticalOpPastJoinOp(Transformation): - """ - Move multiple identical operations on different branches past the common join node. + """Move multiple identical operations on different branches past the common join node. It assumes the shape to be preserved by the join op in the default move_node() method """ @@ -1386,8 +1383,7 @@ def __init__(self, identical_op_list, join_node_list): self.join_node_op = join_node_list def move_node(self, model, n, producers): - """ - Should be overwritten for some operations + """Should be overwritten for some operations Returns: bool: whether moving the node was successful @@ -1414,8 +1410,7 @@ def move_node(self, model, n, producers): return True def are_producers_identical(self, model, producers): - """ - Checks only op_types + """Checks only op_types Should be overwritten for additional checks """ op_types = [prod.op_type for prod in producers] @@ -1516,8 +1511,7 @@ def are_producers_identical(self, model, producers): return True def move_node(self, model, n, producers): - """ - We use the base move_node method to move the first producer + """We use the base move_node method to move the first producer past the join node (and delete the rest) """ add_inits = [model.get_initializer(producer.input[1]) for producer in producers] @@ -1574,8 +1568,7 @@ def move_node(self, model, n, producers): class MoveAffinePastJoinConcat(MoveIdenticalOpPastJoinOp): - """ - Applies to scalar linear or channelwise affine ops with the same parameter value + """Applies to scalar linear or channelwise affine ops with the same parameter value """ def __init__(self, linear_ops=["Mul", "Add"]): diff --git a/src/finn/transformation/util.py b/src/finn/transformation/util.py index 284712c3b2..0694121768 100644 --- a/src/finn/transformation/util.py +++ b/src/finn/transformation/util.py @@ -143,8 +143,7 @@ def is_transpose_reshape(node: NodeProto, model: ModelWrapper): # noqa def group_inputs_by_category(node: NodeProto, model: ModelWrapper): # noqa - """ - Group inputs by categories, i.e., groups dynamic inputs first, followed by + """Group inputs by categories, i.e., groups dynamic inputs first, followed by initializers. Keep order of inputs in each category. """ # First select all dynamic inputs, which are those without initializer diff --git a/src/finn/util/config.py b/src/finn/util/config.py index b5a24acb97..7055f54493 100644 --- a/src/finn/util/config.py +++ b/src/finn/util/config.py @@ -28,7 +28,6 @@ def extract_model_config(model, subgraph_hier, attr_names_to_extract): Nodes in subgraphs are prefixed with their parent hierarchy using '_' as separator. For example, a node 'Conv_0' inside a subgraph of node 'IfNode_0' will be exported as 'IfNode_0_Conv_0' in the config.""" - cfg = dict() cfg["Defaults"] = dict() for n in model.graph.node: @@ -70,7 +69,6 @@ def extract_model_config_to_json(model, json_filename, attr_names_to_extract): """Create a json file with layer name -> attribute mappings extracted from the model. The created json file can be later applied on a model with finn.transform.general.ApplyConfig.""" - with open(json_filename, "w") as f: json.dump( extract_model_config( @@ -85,7 +83,7 @@ def extract_model_config_consolidate_shuffles(model, output_file, hw_attrs): """Export flow that takes into consideration how Shuffle operations have been decomposed""" extract_model_config_to_json(model, output_file, hw_attrs) - with open(output_file, "r") as f: + with open(output_file) as f: config = json.load(f) shuffle_configs = {} diff --git a/src/finn/util/create.py b/src/finn/util/create.py index db769da2d3..93f555fe75 100644 --- a/src/finn/util/create.py +++ b/src/finn/util/create.py @@ -79,7 +79,6 @@ def hls_random_mlp_maker(layer_spec): def hls_mlp_maker(layer_spec): """Create an MLP of given specification using HLSCustomOp instances.""" - current_in_name = "" current_out_name = "" i = 0 diff --git a/src/finn/util/data_packing.py b/src/finn/util/data_packing.py index bc65f589f1..6f15bd5b29 100644 --- a/src/finn/util/data_packing.py +++ b/src/finn/util/data_packing.py @@ -37,8 +37,7 @@ def array2hexstring(array, dtype, pad_to_nbits, prefix="0x", reverse=False): - """ - Pack given one-dimensional NumPy array with FINN DataType dtype into a hex + """Pack given one-dimensional NumPy array with FINN DataType dtype into a hex string. Any BIPOLAR values will be converted to a single bit with a 0 representing -1. @@ -48,7 +47,6 @@ def array2hexstring(array, dtype, pad_to_nbits, prefix="0x", reverse=False): packing. Examples: - array2hexstring([1, 1, 1, 0], DataType["BINARY"], 4) = "0xe" array2hexstring([1, 1, 1, 0], DataType["BINARY"], 8) = "0x0e" @@ -103,7 +101,6 @@ def hexstring2npbytearray(hexstring, remove_prefix="0x"): """Convert a hex string into a NumPy array of dtype uint8. Example: - hexstring2npbytearray("0f01") = array([15, 1], dtype=uint8) """ # remove prefix if found @@ -118,7 +115,6 @@ def npbytearray2hexstring(npbytearray, prefix="0x"): """Convert a NumPy array of uint8 dtype into a hex string. Example: - npbytearray2hexstring(array([15, 1], dtype=uint8)) = "0x0f01" """ return prefix + binascii.hexlify(bytearray(npbytearray)).decode("utf-8") @@ -131,7 +127,6 @@ def pack_innermost_dim_as_hex_string( strings using array2hexstring. Examples: - A = [[1, 1, 1, 0], [0, 1, 1, 0]] eA = ["0e", "06"] @@ -144,7 +139,6 @@ def pack_innermost_dim_as_hex_string( pack_innermost_dim_as_hex_string(B, DataType["UINT2"], 8) == eB """ - if type(ndarray) is not np.ndarray or ndarray.dtype not in [np.float32, np.float16]: # try to convert to a float numpy array (container dtype is float) ndarray = np.asarray(ndarray, dtype=np.float32) @@ -162,7 +156,6 @@ def unpack_innermost_dim_from_hex_string( the hex strings into the specified data type. out_shape can be specified such that any padding in the packing dimension is removed. If reverse_inner is set, the innermost unpacked dimension will be reversed.""" - if type(ndarray) is not np.ndarray: raise Exception( """unpack_innermost_dim_from_hex_string needs ndarray @@ -267,13 +260,11 @@ def numpy_to_hls_code(ndarray, dtype, hls_var_name, pack_innermost_dim=True, no_ def elem2str(x): if type(x) is str or type(x) is np.str_: return '%s("%s", 16)' % (hls_dtype, x) - elif type(x) is np.float32: + if type(x) is np.float32: if dtype.is_integer(): return str(int(x)) - else: - return str(x) - else: - raise Exception("Unsupported type for numpy_to_hls_code") + return str(x) + raise Exception("Unsupported type for numpy_to_hls_code") strarr = np.array2string(ndarray, separator=", ", formatter={"all": elem2str}) np.set_printoptions(**orig_printops) @@ -322,7 +313,6 @@ def rtlsim_output_to_npy(output, path, dtype, shape, packedBits, targetBits, rev integer is assumed to be a packed array of targetBits-bit elements, which will be unpacked as the innermost dimension of the NumPy array. If path is not None it will also be saved as a npy file.""" - # TODO should have its own testbench? output = np.asarray([hex(int(x)) for x in output]) out_array = unpack_innermost_dim_from_hex_string( @@ -350,7 +340,6 @@ def finnpy_to_packed_bytearray( * ndarray -> 1-bit and total bits % 8 == 0 This mode is currently not well-tested, use at your own risk! """ - # handle fast_mode cases (currently only called from driver): if issubclass(type(ndarray), np.ndarray) and fast_mode: inp_is_byte = ndarray.dtype in [np.uint8, np.int8] @@ -404,14 +393,12 @@ def fn(x): def packed_bytearray_to_finnpy( packed_bytearray, dtype, output_shape, reverse_inner=False, reverse_endian=False ): - """ - Given a packed numpy uint8 ndarray, unpack it into a FINN array of + """Given a packed numpy uint8 ndarray, unpack it into a FINN array of given DataType. output_shape must be specified to remove padding from the packed dimension """ - if (not issubclass(type(packed_bytearray), np.ndarray)) or packed_bytearray.dtype != np.uint8: raise Exception("packed_bytearray_to_finnpy needs NumPy uint8 arrays") if packed_bytearray.ndim == 0: @@ -564,8 +551,7 @@ def data_prepared_to_finnpy_int(data_prepared, dtype): if signed: unpacked_data = unsiged_array_to_signed(data_prepared, target_bits) return unpacked_data.astype(np.float32) - else: - return data_prepared.astype(np.float32) + return data_prepared.astype(np.float32) def packed_bytearray_to_finnpy_float( @@ -584,7 +570,6 @@ def packed_bytearray_to_finnpy_float( def to_external_tensor(init, w_dtype): """Return an appropriately formatted and packed numpy byte array for given external parameter tensor.""" - weight_width = init.shape[1] * w_dtype.bitwidth() weight_width_padded = roundup_to_integer_multiple(weight_width, 4) hex_init = pack_innermost_dim_as_hex_string(init, w_dtype, weight_width_padded, prefix="0x") diff --git a/src/finn/util/execution.py b/src/finn/util/execution.py index 00b6f333e1..3dbb7bfcc6 100644 --- a/src/finn/util/execution.py +++ b/src/finn/util/execution.py @@ -26,8 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -""" -Utility functions for executing ONNX models in FINN. +"""Utility functions for executing ONNX models in FINN. This module contains functions for executing parent models containing StreamingDataflowPartition nodes and other execution-related utilities. @@ -55,8 +54,7 @@ def load_model_checkpoint(filename): if os.path.isfile(filename): model = ModelWrapper(filename) return model - else: - raise FileNotFoundError(f"Model file {filename} not found") + raise FileNotFoundError(f"Model file {filename} not found") def execute_parent(parent_path, child_path, input_tensor_npy, return_full_ctx=False): @@ -83,5 +81,4 @@ def execute_parent(parent_path, child_path, input_tensor_npy, return_full_ctx=Fa ret = execute_onnx(parent_model, {iname: input_tensor_npy}, True) if return_full_ctx: return ret - else: - return ret[oname] + return ret[oname] diff --git a/src/finn/util/mlo_sim.py b/src/finn/util/mlo_sim.py index 9f906767f3..d5985576a0 100644 --- a/src/finn/util/mlo_sim.py +++ b/src/finn/util/mlo_sim.py @@ -33,7 +33,7 @@ import numpy as np from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp -from typing import Callable +from collections.abc import Callable from finn import xsi @@ -51,7 +51,7 @@ def is_mlo(model: ModelWrapper) -> bool: def dat_file_to_numpy_array(file_path): byte_values = [] - with open(file_path, "r") as file: + with open(file_path) as file: for line in file: hex_string = line.strip() for i in range(len(hex_string) - 2, -1, -2): @@ -68,7 +68,6 @@ def mlo_prehook_func_factory(node) -> Callable[[SimEngine], None]: """Factory that will construct a prehook function to setup the axi memory mapped interfaces for MLO validation. """ - # Get the FINNLoop finnloop_op = getCustomOp(node) diff --git a/src/finn/util/onnxscript_helpers.py b/src/finn/util/onnxscript_helpers.py index 1654c06b48..dd51aa950e 100644 --- a/src/finn/util/onnxscript_helpers.py +++ b/src/finn/util/onnxscript_helpers.py @@ -11,7 +11,6 @@ pattern_builder, ) from qonnx.custom_op.registry import is_custom_op -from typing import List, Optional class SubGraphView(ir.GraphView): @@ -45,9 +44,7 @@ def _identify_inputs(self, nodes): inputs = set() for node in nodes: for input in node.inputs: - if input.is_graph_input(): - inputs.add(input) - elif input.producer() not in nodes: + if input.is_graph_input() or input.producer() not in nodes: inputs.add(input) return list(inputs) @@ -99,26 +96,22 @@ def check_node_metadata_exists(self): and "pkg.torch.onnx.class_hierarchy" in self._node.metadata_props ): return True - else: - return False + return False def is_last_level(self, level): if len(self.instance_metadata) - 1 == level: return True - else: - return False + return False def get_instance_name(self, depth=0): if depth >= len(self.instance_metadata): return None - else: - return self.instance_metadata[depth] + return self.instance_metadata[depth] def get_class_name(self, depth=0): if depth >= len(self.instance_metadata): return None - else: - return self.class_metadata[depth] + return self.class_metadata[depth] class PytorchHierarchyNode: @@ -153,7 +146,7 @@ def __init__(self): self.children = [] self.nodes = [] - def print_hierarchy(self, instance_hierarchy: Optional[List[str]] = None): + def print_hierarchy(self, instance_hierarchy: list[str] | None = None): if instance_hierarchy is None: instance_hierarchy = [] if self.instance_name is not None: @@ -174,7 +167,7 @@ def get_unwrapped_nodes(self): # Checks if the search hierarchy matches the instance hierarchy def hierarchy_matches( - self, search_hierarchy: List[str], instance_hierarchy: Optional[List[str]] = None + self, search_hierarchy: list[str], instance_hierarchy: list[str] | None = None ): if instance_hierarchy is None: instance_hierarchy = [] @@ -186,7 +179,7 @@ def hierarchy_matches( # Return all nodes from the given name hierarchy on down def get_nodes( - self, search_hierarchy: List[str], instance_hierarchy: Optional[List[str]] = None + self, search_hierarchy: list[str], instance_hierarchy: list[str] | None = None ): if instance_hierarchy is None: instance_hierarchy = [] @@ -231,17 +224,16 @@ def add_node(self, node, level=0): if node.is_last_level(level): self.nodes.append(node) return True - else: - for child in self.children: - if child.instance_name == node.get_instance_name(level + 1): - return child.add_node(node, level + 1) + for child in self.children: + if child.instance_name == node.get_instance_name(level + 1): + return child.add_node(node, level + 1) - # if no child matches the next level of the hierarchy, create a new child node - new_child = PytorchHierarchyNode() - new_child.instance_name = node.get_instance_name(level + 1) - new_child.module_type = node.get_class_name(level + 1) - self.children.append(new_child) - return new_child.add_node(node, level + 1) + # if no child matches the next level of the hierarchy, create a new child node + new_child = PytorchHierarchyNode() + new_child.instance_name = node.get_instance_name(level + 1) + new_child.module_type = node.get_class_name(level + 1) + self.children.append(new_child) + return new_child.add_node(node, level + 1) def direct_convert_ir_graph_to_pattern(graph): @@ -410,23 +402,22 @@ def build_reshape_node(inp, reshape_shape): def tensor_type_to_finn_datatype_string(tensor_type): if tensor_type == ir.TensorType(_enums.DataType.FLOAT): return "FLOAT32" - elif tensor_type == ir.TensorType(_enums.DataType.INT8): + if tensor_type == ir.TensorType(_enums.DataType.INT8): return "INT8" - elif tensor_type == ir.TensorType(_enums.DataType.INT16): + if tensor_type == ir.TensorType(_enums.DataType.INT16): return "INT16" - elif tensor_type == ir.TensorType(_enums.DataType.INT32): + if tensor_type == ir.TensorType(_enums.DataType.INT32): return "INT32" - elif tensor_type == ir.TensorType(_enums.DataType.INT64): + if tensor_type == ir.TensorType(_enums.DataType.INT64): return "INT64" - elif tensor_type == ir.TensorType(_enums.DataType.UINT8): + if tensor_type == ir.TensorType(_enums.DataType.UINT8): return "UINT8" - elif tensor_type == ir.TensorType(_enums.DataType.UINT16): + if tensor_type == ir.TensorType(_enums.DataType.UINT16): return "UINT16" - elif tensor_type == ir.TensorType(_enums.DataType.UINT32): + if tensor_type == ir.TensorType(_enums.DataType.UINT32): return "UINT32" - elif tensor_type == ir.TensorType(_enums.DataType.UINT64): + if tensor_type == ir.TensorType(_enums.DataType.UINT64): return "UINT64" - elif tensor_type == ir.TensorType(_enums.DataType.BOOL): + if tensor_type == ir.TensorType(_enums.DataType.BOOL): return "BOOL" - else: - raise ValueError(f"Unsupported tensor type: {tensor_type}") + raise ValueError(f"Unsupported tensor type: {tensor_type}") diff --git a/src/finn/util/platforms.py b/src/finn/util/platforms.py index 8856ce0ab8..2f7caf3d31 100644 --- a/src/finn/util/platforms.py +++ b/src/finn/util/platforms.py @@ -220,7 +220,7 @@ def __init__( limits=DEFAULT_RES_LIMITS, avg_constraints=DEFAULT_AVG_CONSTRAINTS, ): - super(Zynq7020_Platform, self).__init__( + super().__init__( nslr=1, ndevices=ndevices, sll_count=[[0]], @@ -243,7 +243,7 @@ def __init__( limits=DEFAULT_RES_LIMITS, avg_constraints=DEFAULT_AVG_CONSTRAINTS, ): - super(ZU3EG_Platform, self).__init__( + super().__init__( nslr=1, ndevices=ndevices, sll_count=[[0]], @@ -266,7 +266,7 @@ def __init__( limits=DEFAULT_RES_LIMITS, avg_constraints=DEFAULT_AVG_CONSTRAINTS, ): - super(ZU7EV_Platform, self).__init__( + super().__init__( nslr=1, ndevices=ndevices, sll_count=[[0]], @@ -289,7 +289,7 @@ def __init__( limits=DEFAULT_RES_LIMITS, avg_constraints=DEFAULT_AVG_CONSTRAINTS, ): - super(ZU9EG_Platform, self).__init__( + super().__init__( nslr=1, ndevices=ndevices, sll_count=[[0]], @@ -312,7 +312,7 @@ def __init__( limits=DEFAULT_RES_LIMITS, avg_constraints=DEFAULT_AVG_CONSTRAINTS, ): - super(ZU28DR_Platform, self).__init__( + super().__init__( nslr=1, ndevices=ndevices, sll_count=[[0]], @@ -337,7 +337,7 @@ def __init__( ): # according to Vivado: 23040 SLR0 <-> SLR1 sll_counts = [[0, 5000], [5000, 0]] - super(Alveo_NxU50_Platform, self).__init__( + super().__init__( nslr=2, ndevices=ndevices, sll_count=sll_counts, @@ -369,7 +369,7 @@ def __init__( avg_constraints=DEFAULT_AVG_CONSTRAINTS, ): sll_counts = [[0, 5000, 0], [5000, 0, 5000], [0, 5000, 0]] - super(Alveo_NxU200_Platform, self).__init__( + super().__init__( nslr=3, ndevices=ndevices, sll_count=sll_counts, @@ -407,7 +407,7 @@ def __init__( [0, 5000, 0, 5000], [0, 0, 5000, 0], ] - super(Alveo_NxU250_Platform, self).__init__( + super().__init__( nslr=4, ndevices=ndevices, sll_count=sll_counts, @@ -435,7 +435,7 @@ def __init__( avg_constraints=DEFAULT_AVG_CONSTRAINTS, ): sll_counts = [[0, 5000, 0], [5000, 0, 5000], [0, 5000, 0]] - super(Alveo_NxU280_Platform, self).__init__( + super().__init__( nslr=3, ndevices=ndevices, sll_count=sll_counts, @@ -469,7 +469,7 @@ def __init__( avg_constraints=DEFAULT_AVG_CONSTRAINTS, ): sll_counts = [[0, 5000, 0], [5000, 0, 5000], [0, 5000, 0]] - super(Alveo_NxU55C_Platform, self).__init__( + super().__init__( nslr=3, ndevices=ndevices, sll_count=sll_counts, diff --git a/src/finn/xsi/__init__.py b/src/finn/xsi/__init__.py index 54a2854be8..d09d775eed 100644 --- a/src/finn/xsi/__init__.py +++ b/src/finn/xsi/__init__.py @@ -29,9 +29,9 @@ _auto_install_attempted = False # Cache for loaded modules -_adapter_module: Optional[Any] = None -_sim_engine_module: Optional[Any] = None -_xsi_module: Optional[Any] = None +_adapter_module: Any | None = None +_sim_engine_module: Any | None = None +_xsi_module: Any | None = None def is_available() -> bool: @@ -85,9 +85,8 @@ def _attempt_auto_install() -> bool: if result == 0: print("✓ XSI installation completed successfully!") return True - else: - print("✗ XSI installation failed. Run 'python -m finn.xsi.setup' for details.") - return False + print("✗ XSI installation failed. Run 'python -m finn.xsi.setup' for details.") + return False finally: sys.argv = original_argv From 3deccaa46211a910296ee853db09c2a8ce3c745c Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 8 May 2026 11:56:49 +0200 Subject: [PATCH 099/170] Fix FIFO sizing for FINN loop operator --- finn-rtllib/cdma/cdma_u/axi_dma_wr_u.sv | 6 + finn-rtllib/mlo/fetch_weights.sv | 18 ++ finn-rtllib/mock_hbm/hdl/mock_template.v | 252 ++++++++++++------ src/finn/builder/build_dataflow_steps.py | 69 +---- .../fpgadataflow/elementwise_binary.py | 3 +- .../fpgadataflow/matrixvectoractivation.py | 3 +- src/finn/custom_op/fpgadataflow/memstream.py | 71 ----- .../custom_op/fpgadataflow/rtl/finn_loop.py | 173 +++++++----- .../fpgadataflow/rtl/removedatapath_rtl.py | 22 +- src/finn/custom_op/fpgadataflow/rtlbackend.py | 6 +- .../custom_op/fpgadataflow/thresholding.py | 3 +- .../fpgadataflow/vectorvectoractivation.py | 3 +- .../fpgadataflow/create_stitched_ip.py | 22 ++ .../fpgadataflow/hlssynth_ip.py | 54 ++-- .../transformation/fpgadataflow/prepare_ip.py | 42 ++- src/finn/util/basic.py | 3 +- src/finn/util/hbm_mock.py | 72 +++++ 17 files changed, 488 insertions(+), 334 deletions(-) delete mode 100644 src/finn/custom_op/fpgadataflow/memstream.py create mode 100644 src/finn/util/hbm_mock.py diff --git a/finn-rtllib/cdma/cdma_u/axi_dma_wr_u.sv b/finn-rtllib/cdma/cdma_u/axi_dma_wr_u.sv index d906286eff..ebd30e9589 100644 --- a/finn-rtllib/cdma/cdma_u/axi_dma_wr_u.sv +++ b/finn-rtllib/cdma/cdma_u/axi_dma_wr_u.sv @@ -579,6 +579,12 @@ always_ff @(posedge aclk) begin m_axi_wvalid_reg <= 1'b0; m_axi_wready_int_reg <= 1'b0; temp_m_axi_wvalid_reg <= 1'b0; + m_axi_wdata_reg <= {AXI_DATA_WIDTH{1'b0}}; + m_axi_wstrb_reg <= {AXI_STRB_WIDTH{1'b0}}; + m_axi_wlast_reg <= 1'b0; + temp_m_axi_wdata_reg <= {AXI_DATA_WIDTH{1'b0}}; + temp_m_axi_wstrb_reg <= {AXI_STRB_WIDTH{1'b0}}; + temp_m_axi_wlast_reg <= 1'b0; end else begin m_axi_wvalid_reg <= m_axi_wvalid_next; m_axi_wready_int_reg <= m_axi_wready_int_early; diff --git a/finn-rtllib/mlo/fetch_weights.sv b/finn-rtllib/mlo/fetch_weights.sv index fda40e45d8..b98dd689be 100644 --- a/finn-rtllib/mlo/fetch_weights.sv +++ b/finn-rtllib/mlo/fetch_weights.sv @@ -223,4 +223,22 @@ end else begin assign m_axis_tdata = axis_lwb_tdata; end +// Tie off outputs that are not used/driven in this module +assign m_axi_ddr_awaddr = '0; +assign m_axi_ddr_awburst = '0; +assign m_axi_ddr_awcache = '0; +assign m_axi_ddr_awid = '0; +assign m_axi_ddr_awlen = '0; +assign m_axi_ddr_awlock = '0; +assign m_axi_ddr_awprot = '0; +assign m_axi_ddr_awsize = '0; +assign m_axi_ddr_awvalid = 1'b0; + +assign m_axi_ddr_wdata = '0; +assign m_axi_ddr_wlast = 1'b0; +assign m_axi_ddr_wstrb = '0; +assign m_axi_ddr_wvalid = 1'b0; + +assign m_axi_ddr_bready = 1'b0; + endmodule diff --git a/finn-rtllib/mock_hbm/hdl/mock_template.v b/finn-rtllib/mock_hbm/hdl/mock_template.v index edd4316e87..47d9cc437b 100644 --- a/finn-rtllib/mock_hbm/hdl/mock_template.v +++ b/finn-rtllib/mock_hbm/hdl/mock_template.v @@ -1,4 +1,4 @@ -module $TOP_MODULE_NAME$( +module {{ TOP_MODULE_NAME }}( //- Global Control ------------------ (* X_INTERFACE_PARAMETER = "ASSOCIATED_RESET = ap_rst_n" *) (* X_INTERFACE_INFO = "xilinx.com:signal:clock:1.0 ap_clk CLK" *) @@ -7,134 +7,226 @@ input ap_clk, input ap_rst_n, //- AXI4 Slave - Write Address ----- -input [$ADDR_WIDTH$-1:0] s_axi_awaddr, +input [{{ ADDR_WIDTH }}-1:0] s_axi_awaddr, +input [1:0] s_axi_awburst, +input [3:0] s_axi_awcache, +input [1:0] s_axi_awid, +input [7:0] s_axi_awlen, +input s_axi_awlock, +input [2:0] s_axi_awprot, +input [2:0] s_axi_awsize, input s_axi_awvalid, output s_axi_awready, //- AXI4 Slave - Write Data -------- -input [$DATA_WIDTH$-1:0] s_axi_wdata, -input [$DATA_BYTES$-1:0] s_axi_wstrb, +input [{{ DATA_WIDTH }}-1:0] s_axi_wdata, +input [{{ DATA_BYTES }}-1:0] s_axi_wstrb, input s_axi_wvalid, input s_axi_wlast, output s_axi_wready, //- AXI4 Slave - Write Response ---- +output reg [1:0] s_axi_bid, output reg [1:0] s_axi_bresp, output reg s_axi_bvalid, input s_axi_bready, //- AXI4 Slave - Read Address ------ -input [$ADDR_WIDTH$-1:0] s_axi_araddr, +input [{{ ADDR_WIDTH }}-1:0] s_axi_araddr, +input [1:0] s_axi_arburst, +input [3:0] s_axi_arcache, +input [1:0] s_axi_arid, +input [7:0] s_axi_arlen, +input s_axi_arlock, +input [2:0] s_axi_arprot, +input [2:0] s_axi_arsize, input s_axi_arvalid, output s_axi_arready, //- AXI4 Slave - Read Data --------- -output reg [$DATA_WIDTH$-1:0] s_axi_rdata, +output reg [1:0] s_axi_rid, +output reg [{{ DATA_WIDTH }}-1:0] s_axi_rdata, output reg [1:0] s_axi_rresp, output reg s_axi_rvalid, output reg s_axi_rlast, input s_axi_rready ); -parameter integer LATENCY = 100; - -// Internal flags and counters -reg aw_received; -reg w_received; -reg ar_received; -reg [$clog2(LATENCY+1)-1:0] write_cnt; -reg [$clog2(LATENCY+1)-1:0] read_cnt; -reg busy_write; -reg busy_read; +parameter integer LATENCY = 1; + +localparam integer SHIFT_W = ({{ DATA_WIDTH }} <= 1) ? 1 : $clog2({{ DATA_WIDTH }} / 8); +localparam integer LAT_CNT_W = (LATENCY <= 1) ? 1 : $clog2(LATENCY + 1); + +reg write_active; +reg write_aw_captured; +reg [LAT_CNT_W-1:0] write_latency_cnt; +reg [7:0] write_len; +reg [7:0] write_beat_idx; +reg write_all_beats_received; + +reg read_active; +reg [LAT_CNT_W-1:0] read_latency_cnt; +reg [1:0] read_id; +reg [{{ ADDR_WIDTH }}-1:0] read_addr; +reg [7:0] read_len; +reg [2:0] read_size; +reg [1:0] read_burst; +reg [7:0] read_beat_idx; + +function automatic [{{ ADDR_WIDTH }}-1:0] calc_next_addr; + input [{{ ADDR_WIDTH }}-1:0] curr_addr; + input [2:0] size; + input [1:0] burst; + input [7:0] len; + reg [31:0] beat_bytes; + reg [31:0] total_bytes; + reg [{{ ADDR_WIDTH }}-1:0] base_addr; + reg [{{ ADDR_WIDTH }}-1:0] next_a; +begin + beat_bytes = (1 << size); + if (beat_bytes == 0) beat_bytes = 1; + case (burst) + 2'b00: begin // FIXED + next_a = curr_addr; + end + 2'b01: begin // INCR + next_a = curr_addr + beat_bytes; + end + 2'b10: begin // WRAP + total_bytes = beat_bytes * (len + 1); + if (total_bytes == 0) total_bytes = beat_bytes; + base_addr = (curr_addr / total_bytes) * total_bytes; + next_a = curr_addr + beat_bytes; + if (next_a >= (base_addr + total_bytes)) begin + next_a = base_addr; + end + end + default: begin // reserved burst type + next_a = curr_addr; + end + endcase + calc_next_addr = next_a; +end +endfunction -// Ready signals: accept new addr/data when not busy and not already pending -assign s_axi_awready = !busy_write && !aw_received; -assign s_axi_wready = !busy_write && !w_received; -assign s_axi_arready = !busy_read && !ar_received; +assign s_axi_awready = !write_active && !write_aw_captured; +assign s_axi_wready = write_aw_captured && write_active; +assign s_axi_arready = !read_active; -// Default response values always @(posedge ap_clk) begin if (!ap_rst_n) begin - aw_received <= 1'b0; - w_received <= 1'b0; - ar_received <= 1'b0; - busy_write <= 1'b0; - busy_read <= 1'b0; - write_cnt <= {($clog2(LATENCY+1)){1'b0}}; - read_cnt <= {($clog2(LATENCY+1)){1'b0}}; + write_active <= 1'b0; + write_aw_captured <= 1'b0; + write_latency_cnt <= {LAT_CNT_W{1'b0}}; + write_len <= 8'd0; + write_beat_idx <= 8'd0; + write_all_beats_received <= 1'b0; + + read_active <= 1'b0; + read_latency_cnt <= {LAT_CNT_W{1'b0}}; + read_id <= 2'b00; + read_addr <= {({{ ADDR_WIDTH }}){1'b0}}; + read_len <= 8'd0; + read_size <= 3'd0; + read_burst <= 2'b01; + read_beat_idx <= 8'd0; + + s_axi_bid <= 2'b00; + s_axi_bresp <= 2'b00; // OKAY s_axi_bvalid <= 1'b0; - s_axi_bresp <= 2'b00; + s_axi_rid <= 2'b00; + s_axi_rdata <= {({{ DATA_WIDTH }}){1'b0}}; + s_axi_rresp <= 2'b00; // OKAY s_axi_rvalid <= 1'b0; - s_axi_rresp <= 2'b00; - s_axi_rdata <= {${DATA_WIDTH}${1'b0}}; s_axi_rlast <= 1'b0; end else begin - // Capture write address - if (s_axi_awvalid && s_axi_awready) begin - aw_received <= 1'b1; + // Clear response channels on handshake - ALSO clear write state + if (s_axi_bvalid && s_axi_bready) begin + s_axi_bvalid <= 1'b0; + write_active <= 1'b0; + write_aw_captured <= 1'b0; + write_all_beats_received <= 1'b0; end - // Capture write data (we only need to see WLAST to consider a complete write) - if (s_axi_wvalid && s_axi_wready) begin - if (s_axi_wlast) begin - w_received <= 1'b1; - end + if (s_axi_rvalid && s_axi_rready) begin + s_axi_rvalid <= 1'b0; + s_axi_rlast <= 1'b0; end - // Start write transaction when both address and data received - if (aw_received && w_received && !busy_write) begin - busy_write <= 1'b1; - write_cnt <= LATENCY - 1; // will count down - aw_received <= 1'b0; - w_received <= 1'b0; + // Capture AW + if (s_axi_awvalid && s_axi_awready) begin + write_aw_captured <= 1'b1; + write_active <= 1'b1; + write_len <= s_axi_awlen; + write_beat_idx <= 8'd0; + write_all_beats_received <= 1'b0; + write_latency_cnt <= LATENCY - 1; end - // Decrement write counter - if (busy_write) begin - if (write_cnt != 0) begin - write_cnt <= write_cnt - 1; - end else begin - busy_write <= 1'b0; - s_axi_bvalid <= 1'b1; // respond OKAY after latency - s_axi_bresp <= 2'b00; + // Write data beats - count and validate WLAST + if (s_axi_wvalid && s_axi_wready) begin + // Validate WLAST arrives at correct beat + // Expected: WLAST high only on beat index == AWLEN + if (s_axi_wlast != ((write_beat_idx + 8'd1) == (write_len + 8'd1))) begin + // WLAST mismatch - protocol error detected + // Continue anyway (lenient slave behavior) + end + + // Always increment beat counter each write beat + write_beat_idx <= write_beat_idx + 8'd1; + + // Track when all expected beats received + if ((write_beat_idx + 8'd1) == (write_len + 8'd1)) begin + write_all_beats_received <= 1'b1; end end - // B channel handshake - if (s_axi_bvalid && s_axi_bready) begin - s_axi_bvalid <= 1'b0; + // Write latency countdown and B response generation + if (write_active) begin + if (write_latency_cnt != 0) begin + write_latency_cnt <= write_latency_cnt - 1; + end else begin + // Latency expired - check if all beats received and response not pending + if (write_aw_captured && write_all_beats_received && !s_axi_bvalid) begin + // Issue B response + s_axi_bvalid <= 1'b1; + // NOTE: Do NOT clear write_active/write_aw_captured here! + // They clear only after BREADY handshake (see above) + end + end end - // Capture read address + // Capture AR if (s_axi_arvalid && s_axi_arready) begin - ar_received <= 1'b1; - end - - // Start read transaction when address captured - if (ar_received && !busy_read) begin - busy_read <= 1'b1; - read_cnt <= LATENCY - 1; - ar_received <= 1'b0; + read_active <= 1'b1; + read_id <= s_axi_arid; + read_addr <= s_axi_araddr; + read_len <= s_axi_arlen; + read_size <= s_axi_arsize; + read_burst <= s_axi_arburst; + read_beat_idx <= 8'd0; + read_latency_cnt <= LATENCY - 1; end - // Decrement read counter - if (busy_read) begin - if (read_cnt != 0) begin - read_cnt <= read_cnt - 1; - end else begin - busy_read <= 1'b0; + // Read first-word latency and data beat generation + if (read_active) begin + if (read_latency_cnt != 0) begin + read_latency_cnt <= read_latency_cnt - 1; + end else if (!s_axi_rvalid || (s_axi_rvalid && s_axi_rready)) begin + s_axi_rid <= read_id; s_axi_rvalid <= 1'b1; - s_axi_rresp <= 2'b00; - s_axi_rdata <= {${DATA_WIDTH}${1'b0}}; // return zeros for reads - s_axi_rlast <= 1'b1; + s_axi_rlast <= ((read_beat_idx + 8'd1) == (read_len + 8'd1)); + + if ((read_beat_idx + 8'd1) < (read_len + 8'd1)) begin + read_addr <= calc_next_addr(read_addr, read_size, read_burst, read_len); + read_beat_idx <= read_beat_idx + 8'd1; + end else begin + if (s_axi_rready || !s_axi_rvalid) begin + read_active <= 1'b0; + end + end end end - - // R channel handshake - if (s_axi_rvalid && s_axi_rready) begin - s_axi_rvalid <= 1'b0; - s_axi_rlast <= 1'b0; - end end end diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 9efcb3bb0d..7116b3da10 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -507,6 +507,16 @@ def step_generate_hardware( loop_model.set_metadata_prop("is_mlo", "1") # Recursion here loop_model = step_generate_hardware(loop_model, cfg, parent_node=node.name) + + node_inst.set_nodeattr("body", loop_model.graph) + + # Codegen for the current model + model = step_hw_codegen(model, cfg) + + # Stitch submodels + for node in model.get_nodes_by_op_type("FINNLoop"): + node_inst = cast("FINNLoop", getCustomOp(node)) + loop_model = cast("ModelWrapper", node_inst.get_nodeattr("body")) # Pack subgraph with IPs and FIFOs into stitched IP loop_model = loop_model.transform( CreateStitchedIP( @@ -517,9 +527,6 @@ def step_generate_hardware( ) node_inst.set_nodeattr("body", loop_model.graph) - # Codegen for the current model - model = step_hw_codegen(model, cfg) - # IP Gen for the current model model = step_hw_ipgen(model, cfg, parent_node=parent_node) @@ -533,67 +540,13 @@ def step_generate_hardware( # IP Gen for the inserted FIFOs and any remaining # IPs that needed to be re-gen after FIFO insertion model = step_hw_ipgen(model, cfg, parent_node=parent_node) - model.save("afterfirstiteration.onnx") return model -# def prepare_loop_ops_fifo_sizing(node, cfg): -# node_inst = getCustomOp(node) -# loop_model = node_inst.get_nodeattr("body") -# loop_model = loop_model.transform(GiveUniqueNodeNamesRecursive(prefix=node.name + "_")) -# # go first into subgraph to check if there are other loop ops -# loop_nodes = loop_model.get_nodes_by_op_type("FINNLoop") -# for loop_node in loop_nodes: -# prepare_loop_ops_fifo_sizing(loop_node, cfg) -# loop_model = loop_model.transform( -# PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period()) -# ) -# loop_model = loop_model.transform(HLSSynthIP(cfg._resolve_hls_clk_period())) -# loop_model = loop_model.transform(ReplaceVerilogRelPaths()) -# if cfg.fifosim_save_waveform: -# report_dir = cfg.output_dir + "/report" -# os.makedirs(report_dir, exist_ok=True) -# loop_model.set_metadata_prop( -# "rtlsim_trace", os.path.abspath(report_dir) + f"/{node.name}_fifosim_trace.wdb" -# ) -# loop_model = loop_model.transform( -# InsertAndSetFIFODepths( -# cfg._resolve_fpga_part(), -# cfg._resolve_hls_clk_period(), -# swg_exception=cfg.default_swg_exception, -# vivado_ram_style=cfg.large_fifo_mem_style, -# fifosim_input_throttle=cfg.fifosim_input_throttle, -# ) -# ) -# loop_model = loop_model.transform(SplitLargeFIFOs()) -# loop_model = loop_model.transform(RemoveShallowFIFOs()) -# loop_model = loop_model.transform(GiveUniqueNodeNamesRecursive(prefix=node.name + "_")) -# loop_model = loop_model.transform(GiveReadableTensorNames()) -# node_inst.set_nodeattr("body", loop_model.graph) - - -# def prepare_loop_ops_ipgen(node, cfg): -# node_inst = getCustomOp(node) -# loop_model = node_inst.get_nodeattr("body") -# # go first into subgraph to check if there are other loop ops -# loop_nodes = loop_model.get_nodes_by_op_type("FINNLoop") -# for loop_node in loop_nodes: -# prepare_loop_ops_ipgen(loop_node, cfg) -# loop_model = loop_model.transform(HLSSynthIP(cfg._resolve_hls_clk_period())) -# loop_model = loop_model.transform( -# CreateStitchedIP( -# cfg._resolve_fpga_part(), -# cfg.synth_clk_period_ns, -# vitis=False, -# ) -# ) -# node_inst.set_nodeattr("body", loop_model.graph) - - @register_build_dataflow_step() def step_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig): - """This step will only execute if QONNX nodes are found. + """Step will only execute if QONNX nodes are found. These include the following op_types: "Quant" , "Trunc" and "BinaryQuant". If such nodes are found the step will run the tidy-up step from QONNX and then convert the QONNX model to the FINN-ONNX dialect. diff --git a/src/finn/custom_op/fpgadataflow/elementwise_binary.py b/src/finn/custom_op/fpgadataflow/elementwise_binary.py index 1eb2d22eab..262d2f49c1 100644 --- a/src/finn/custom_op/fpgadataflow/elementwise_binary.py +++ b/src/finn/custom_op/fpgadataflow/elementwise_binary.py @@ -36,14 +36,13 @@ from finn.custom_op.fpgadataflow import register_custom_op from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.custom_op.fpgadataflow.memstream import MemStreamSupport # FINN logging from finn.util.logging import log # Generic implementation for elementwise binary operations -class ElementwiseBinaryOperation(MemStreamSupport, HWCustomOp): +class ElementwiseBinaryOperation(HWCustomOp): # Specifies the elementwise operation to be implemented # Format: (Identifier, Python, C++, RTL) _operation: tuple[str, np.ufunc, str, str] | None = None diff --git a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py index e51d5b276c..43603ec112 100644 --- a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py @@ -47,7 +47,6 @@ ) from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.custom_op.fpgadataflow.memstream import MemStreamSupport from finn.util.data_packing import numpy_to_hls_code, pack_innermost_dim_as_hex_string from finn.util.logging import log from finn.util.settings import get_settings @@ -60,7 +59,7 @@ # the ... here can be any shape (representing groups of vectors) -class MVAU(MemStreamSupport, HWCustomOp): +class MVAU(HWCustomOp): """Abstraction layer for HW implementation of MatrixVectorActivation layers.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/custom_op/fpgadataflow/memstream.py b/src/finn/custom_op/fpgadataflow/memstream.py deleted file mode 100644 index ee6305f26f..0000000000 --- a/src/finn/custom_op/fpgadataflow/memstream.py +++ /dev/null @@ -1,71 +0,0 @@ -"""Support for memory stream operations in FPGA dataflow.""" - -import os -from pathlib import Path -from typing import cast - -from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.util.basic import is_versal - - -class MemStreamSupport(HWCustomOp): - """Custom Op for memory stream operations in FPGA dataflow.""" - - def calc_tmem(self) -> int: - """Abstract method to calculate threshold memory size. - The default implementation raises NotImplementedError because - some subclasses dont implement calc_tmem.""" - raise NotImplementedError() - - def calc_wmem(self) -> int: - """Abstract method to calculate weight memory size. - The default implementation raises NotImplementedError because - some subclasses dont implement calc_wmem.""" - raise NotImplementedError() - - def generate_hdl_memstream(self, fpgapart: str, pumped_memory: int = 0) -> None: - """Generate verilog code for memstream component. - - Currently utilized by MVAU, VVAU and HLS Thresholding layer. - - Args: - fpgapart: Target FPGA part string. - pumped_memory: Whether to use pumped memory (default: 0). - - """ - ops = ["MVAU_hls", "MVAU_rtl", "VVAU_hls", "VVAU_rtl", "Thresholding_hls"] - if self.onnx_node.op_type in ops or self.onnx_node.op_type.startswith("Elementwise"): - template_path = ( - Path(os.environ["FINN_RTLLIB"]) / "memstream/hdl/memstream_wrapper_template.v" - ) - mname = self.onnx_node.name - if self.onnx_node.op_type.startswith("Thresholding"): - depth = self.calc_tmem() - else: - depth = self.calc_wmem() - padded_width = self.get_instream_width_padded(1) - code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) - - ram_style = cast("str", self.get_nodeattr("ram_style")) - init_file = str(Path(code_gen_dir) / "memblock.dat") - if ram_style == "ultra" and not is_versal(fpgapart): - init_file = "" - code_gen_dict = { - "$MODULE_NAME$": [mname], - "$SETS$": ["1"], - "$DEPTH$": [str(depth)], - "$WIDTH$": [str(padded_width)], - "$INIT_FILE$": [init_file], - "$RAM_STYLE$": [ram_style], - "$PUMPED_MEMORY$": [str(pumped_memory)], - } - # apply code generation to template - with template_path.open() as f: - template_wrapper = f.read() - for key in code_gen_dict: - # transform list into long string separated by '\n' - code_gen_line = "\n".join(code_gen_dict[key]) - template_wrapper = template_wrapper.replace(key, code_gen_line) - output_path = Path(code_gen_dir) / f"{mname}_memstream_wrapper.v" - with output_path.open("w") as f: - f.write(template_wrapper) diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index 14b6df92d0..3923e0fedd 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -28,6 +28,7 @@ import copy import math +import re import numpy as np import numpy.typing as npt import os @@ -39,6 +40,7 @@ from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp, is_custom_op from qonnx.util.basic import get_by_name, qonnx_make_model, roundup_to_integer_multiple +from typing import cast import finn.core.onnx_exec as oxe from finn import xsi @@ -47,10 +49,10 @@ from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.transformation.fpgadataflow.annotate_cycles import AnnotateCycles -from finn.util.basic import make_build_dir +from finn.util.basic import getHWCustomOp, make_build_dir from finn.util.create import adjacency_list from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy -from finn.util.exception import FINNInternalError +from finn.util.exception import FINNInternalError, FINNUserError from finn.util.mlo_sim import mlo_prehook_func_factory from finn.util.settings import get_settings @@ -64,9 +66,7 @@ def collect_ip_dirs(model, ipstitch_path): for node in model.graph.node: node_inst = getCustomOp(node) ip_dir_value = node_inst.get_nodeattr("ip_path") - assert os.path.isdir( - ip_dir_value - ), """The directory that should + assert os.path.isdir(ip_dir_value), """The directory that should contain the generated ip blocks doesn't exist.""" ip_dirs += [ip_dir_value] if node.op_type.startswith("MVAU") or node.op_type == "Thresholding_hls": @@ -414,24 +414,39 @@ def generate_hdl(self, model, fpgapart, clk): ) as f: f.write(template_wrapper) - def generate_params(self, model, path): - iteration = self.get_nodeattr("iteration") + def generate_params(self, model: "ModelWrapper", path: str) -> None: + """Generate .dat files for loop parameters and concatenate them together.""" + iteration = cast("int", self.get_nodeattr("iteration")) loop_node = self.onnx_node - loop_body = self.get_nodeattr("body") + loop_body = cast("ModelWrapper", self.get_nodeattr("body")) for i, inp in enumerate(loop_node.input[1:]): params = model.get_initializer(inp) param_dtype = model.get_tensor_datatype(inp) - assert params.shape[0] == iteration + if params is None or not isinstance(params, np.ndarray): + raise FINNUserError( + f"Expected initializer for loop parameter input {inp} " + f"not found or not an ndarray." + ) + if params.shape[0] != iteration: + raise FINNUserError( + f"Expected first dimension of loop parameter {inp} to " + f"be equal to iteration count {iteration}, but got {params.shape[0]}." + ) # get node that initializer is attached to loop_tensor = loop_body.graph.input[i + 1].name param_node = loop_body.find_consumer(loop_tensor) - for iter in range(iteration): - loop_body.set_initializer(loop_tensor, params[iter]) + if param_node is None: + raise FINNInternalError( + f"Could not find consumer of loop parameter tensor {loop_tensor} in loop body." + ) + inst = None + for it in range(iteration): + loop_body.set_initializer(loop_tensor, params[it]) loop_body.set_tensor_datatype(loop_tensor, param_dtype) - inst = getCustomOp(param_node) + inst = getHWCustomOp(param_node) inst.generate_params(loop_body, path) param_file = f"{path}/memblock.dat" - new_param_file = f"{path}/{param_node.op_type}_memblock_{iter}.dat" + new_param_file = f"{path}/{param_node.op_type}_memblock_{it}.dat" if param_node.op_type.startswith("MVAU") or param_node.op_type.startswith( "Elementwise" ): @@ -439,80 +454,85 @@ def generate_params(self, model, path): shutil.move(param_file, new_param_file) elif param_node.op_type.startswith("Thresholding"): # get all generated Thresholding dat files - pe = inst.get_nodeattr("PE") - output_data_type = inst.get_nodeattr("outputDataType") + pe = cast("int", inst.get_nodeattr("PE")) + output_data_type = cast("str", inst.get_nodeattr("outputDataType")) o_bitwidth = DataType[output_data_type].bitwidth() param_files = [] for stage in range(o_bitwidth): for pe_value in range(pe): param_files.append( - path - + "/%s_threshs_%s_%s.dat" - % ( - param_node.name, - pe_value, - stage, - ) + path + f"/{param_node.name}_threshs_{pe_value}_{stage}.dat" ) for param_file in param_files: param_path = Path(param_file) new_param_file = param_path.with_name( - param_path.stem + "_i" + str(iter) + param_path.suffix + param_path.stem + "_i" + str(it) + param_path.suffix ) shutil.move(param_path, new_param_file) else: - raise Exception + raise FINNUserError( + f"Node of type {param_node.op_type} not supported as loop node." + ) if param_node.op_type.startswith("MVAU") or param_node.op_type.startswith( "Elementwise" ): # concatinate all .dat files together - param_file = f"{path}/memblock_{param_node.op_type}_id_{i + 1}.dat" - with open(param_file, "w") as outfile: - for iter in range(iteration): - memblock_file = f"{path}/{param_node.op_type}_memblock_{iter}.dat" - with open(memblock_file) as infile: + param_file = Path(path) / f"memblock_{param_node.op_type}_id_{i + 1}.dat" + with param_file.open("w") as outfile: + for it in range(iteration): + memblock_file = Path(path) / f"{param_node.op_type}_memblock_{it}.dat" + with memblock_file.open("r") as infile: for line in infile: outfile.write(line) - os.remove(memblock_file) + memblock_file.unlink() # remove the per-iteration file after concatenation # Replace the path for the dat files in the ipgen files if Eltwise # Adapted from transformations.fpgadataflow.replace_verilog_relpaths if param_node.op_type.startswith("Elementwise"): param_customop = getCustomOp(param_node) - ipgen_path = param_customop.get_nodeattr("code_gen_dir_ipgen") - if ipgen_path is not None and os.path.isdir(ipgen_path): - for dname, dirs, files in os.walk(ipgen_path): - for fname in files: - if fname.endswith("_memstream_wrapper.v"): - fpath = os.path.join(dname, fname) - with open(fpath) as f: - s = f.read() - old = "%s/memblock.dat" % ipgen_path - new = "%s/memblock_%s_id_%s.dat" % ( - path, - param_node.op_type, - i + 1, - ) - s = s.replace(old, new) - with open(fpath, "w") as f: - f.write(s) + ipgen_path_str = param_customop.get_nodeattr("code_gen_dir_ipgen") + ipgen_path = Path(cast("str", ipgen_path_str)) + if ipgen_path.is_dir(): + init_file = Path(path) / f"memblock_{param_node.op_type}_id_{i + 1}.dat" + pattern = re.compile( + r'^(\s*parameter\s+INIT_FILE\s*=\s*")[^"]+(".*)$', + re.MULTILINE, + ) + for fpath in ipgen_path.rglob("*_memstream_wrapper.v"): + s = fpath.read_text() + updated, n = pattern.subn( + lambda m, init_file=init_file: ( + f"{m.group(1)}{init_file}{m.group(2)}" + ), + s, + count=1, + ) + if n: + print(f"Updating INIT_FILE in {fpath} -> {init_file}") + fpath.write_text(updated) elif param_node.op_type.startswith("Thresholding"): # concatinate all .dat files together - pe = inst.get_nodeattr("PE") - output_data_type = inst.get_nodeattr("outputDataType") + if inst is None: + raise FINNInternalError( + "Expected inst to be set after loop over iterations, but it was not." + ) + pe = cast("int", inst.get_nodeattr("PE")) + output_data_type = cast("str", inst.get_nodeattr("outputDataType")) o_bitwidth = DataType[output_data_type].bitwidth() for stage in range(o_bitwidth): for pe_value in range(pe): - param_file = path + "/Thresholding_id_%s_threshs_%s_%s.dat" % ( - i + 1, - pe_value, - stage, + param_file = ( + Path(path) / f"Thresholding_id_{i + 1}_threshs_{pe_value}_{stage}.dat" ) - with open(param_file, "w") as outfile: - for iter in range(iteration): - iter_file = f"{path}/{param_node.name}_threshs_{pe_value}_{stage}_i{iter}.dat" - with open(iter_file) as infile: + with param_file.open("w") as outfile: + for it in range(iteration): + iter_file = ( + Path(path) + / f"{param_node.name}_threshs_{pe_value}_{stage}_i{it}.dat" + ) + with iter_file.open("r") as infile: cnt = 0 + hex_len = 0 for line in infile: if cnt == 0: hex_len = len(line.strip()) @@ -526,24 +546,31 @@ def generate_params(self, model, path): for _ in range(next_pow2 - cnt): # write out as hex of len hex_len outfile.write(hex(pad_val)[2:].zfill(hex_len) + "\n") - os.remove(iter_file) + iter_file.unlink() # Replace the path for the dat files in the ipgen files # Adapted from transformations.fpgadataflow.replace_verilog_relpaths param_customop = getCustomOp(param_node) - ipgen_path = param_customop.get_nodeattr("ipgen_path") - if ipgen_path is not None and os.path.isdir(ipgen_path): - for dname, dirs, files in os.walk(ipgen_path): - for fname in files: - if fname.endswith(".v"): - fpath = os.path.join(dname, fname) - with open(fpath) as f: - s = f.read() - old = "./%s" % param_node.name - new = "%s/Thresholding_id_%s" % (path, i + 1) - s = s.replace(old, new) - with open(fpath, "w") as f: - f.write(s) + ipgen_p = cast("str", param_customop.get_nodeattr("ipgen_path")) + ipgen_path = Path(ipgen_p) + if ipgen_path.is_dir(): + threshold_path = f"{path}/Thresholding_id_{i + 1}_" + pattern = re.compile( + r'^(\s*parameter\s+THRESHOLDS_PATH\s*=\s*")[^"]+(".*)$', + re.MULTILINE, + ) + for fpath in ipgen_path.rglob("*.v"): + print(f"Checking {fpath} for THRESHOLDS_PATH to update...") + s = fpath.read_text() + updated, n = pattern.subn( + lambda m, threshold_path=threshold_path: ( + f"{m.group(1)}{threshold_path}{m.group(2)}" + ), + s, + count=1, + ) + if n: + fpath.write_text(updated) def generate_hdl_stream_tap(self): """Helper function to generate verilog code for stream tap components.""" @@ -1166,5 +1193,5 @@ def code_generation_ipi(self): cmd.append("create_bd_cell -type ip -vlnv %s %s" % (vlnv, self.onnx_node.name)) return cmd - def get_rtl_file_list(self, abspath=False): - pass + def get_rtl_file_list(self, abspath: bool = False): + return [] diff --git a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py index aa7a7c0004..956c355f1c 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py @@ -6,7 +6,6 @@ """ import numpy as np -import os from collections.abc import Sequence from numpy import ndarray from numpy import typing as npt @@ -18,6 +17,7 @@ from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.util.exception import FINNInternalError from finn.util.logging import log +from finn.util.settings import get_settings class RemoveDataPath_rtl(RTLBackend): @@ -104,7 +104,7 @@ def get_rtl_file_list(self, abspath: bool = False) -> list[Path]: ] return verilog_files - def generate_hdl(self, model: Any, fpgapart: str, clk: str) -> None: # noqa: ARG002 + def generate_hdl(self, model: Any, fpgapart: str, clk: float) -> None: # noqa: ARG002 """Generate the RTL code for this custom op. Args: @@ -116,7 +116,7 @@ def generate_hdl(self, model: Any, fpgapart: str, clk: str) -> None: # noqa: AR FINNInternalError: If code_gen_dir_ipgen attribute is invalid. """ - rtlsrc = Path(os.environ["FINN_RTLLIB"]) / "removedatapath" / "hdl" + rtlsrc = Path(get_settings().finn_rtllib) / "removedatapath" / "hdl" template_path = rtlsrc / "dummy_template.v" # save top module name so we can refer to it after this node has been renamed @@ -199,7 +199,7 @@ def get_normal_input_shape( f"normal_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get normal input shape" ) - return normal_shape + return cast("Sequence[int]|npt.NDArray[np.int_]", normal_shape) def get_normal_output_shape( self, ind: int = 0 # noqa: ARG002 @@ -290,7 +290,12 @@ def get_instream_width(self, ind: int = 0) -> int: # noqa: ARG002 f"folded_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get outstream width" ) - in_width = folded_shape[-1] * dtype.bitwidth() + if not isinstance(folded_shape[-1], int) or not isinstance(folded_shape[-1], np.integer): + raise FINNInternalError( + f"folded_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + in_width = cast("int|np.integer", folded_shape[-1]) * dtype.bitwidth() return in_width def get_outstream_width(self, ind: int = 0) -> int: # noqa: ARG002 @@ -323,7 +328,12 @@ def get_outstream_width(self, ind: int = 0) -> int: # noqa: ARG002 f"folded_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get outstream width" ) - in_width = folded_shape[-1] * dtype.bitwidth() + if not isinstance(folded_shape[-1], int) or not isinstance(folded_shape[-1], np.integer): + raise FINNInternalError( + f"folded_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + in_width = cast("int|np.integer", folded_shape[-1]) * dtype.bitwidth() return in_width def get_input_datatype(self, ind: int = 0) -> BaseDataType: # noqa: ARG002 diff --git a/src/finn/custom_op/fpgadataflow/rtlbackend.py b/src/finn/custom_op/fpgadataflow/rtlbackend.py index ecf410f433..889a2a4b2e 100644 --- a/src/finn/custom_op/fpgadataflow/rtlbackend.py +++ b/src/finn/custom_op/fpgadataflow/rtlbackend.py @@ -84,7 +84,7 @@ def get_nodeattr_types( return super_attrs @abstractmethod - def generate_hdl(self, model: "ModelWrapper", fpgapart: str, clk: str) -> None: + def generate_hdl(self, model: "ModelWrapper", fpgapart: str, clk: float) -> None: """Generate HDL code for this node. Args: @@ -147,7 +147,7 @@ def code_generation_ipi(self) -> list[str]: list[str]: List of TCL commands for IP Integrator """ - def code_generation_ipgen(self, model: "ModelWrapper", fpgapart: str, clk: str) -> None: + def code_generation_ipgen(self, model: "ModelWrapper", fpgapart: str, clk: float) -> None: """Generate HDL code for IP generation. Wrapper method that calls generate_hdl to produce the HDL code for this node. @@ -162,7 +162,7 @@ def code_generation_ipgen(self, model: "ModelWrapper", fpgapart: str, clk: str) self.generate_hdl(model, fpgapart, clk) def execute_node( - self, context: dict[str, npt.NDArray], graph: "GraphProto" + self, context: dict[str, npt.NDArray], graph: "GraphProto" # noqa: ARG002 ) -> None: """Execute this node's RTL simulation. diff --git a/src/finn/custom_op/fpgadataflow/thresholding.py b/src/finn/custom_op/fpgadataflow/thresholding.py index aac98e691c..18afd1d287 100644 --- a/src/finn/custom_op/fpgadataflow/thresholding.py +++ b/src/finn/custom_op/fpgadataflow/thresholding.py @@ -40,12 +40,11 @@ ) from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.custom_op.fpgadataflow.memstream import MemStreamSupport from finn.util.exception import FINNInternalError from finn.util.logging import log -class Thresholding(MemStreamSupport, HWCustomOp): +class Thresholding(HWCustomOp): """Abstraction layer for HW implementation of Thresholding.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py b/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py index 4e4a748ea2..75d48e63ac 100644 --- a/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/vectorvectoractivation.py @@ -47,13 +47,12 @@ ) from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.custom_op.fpgadataflow.memstream import MemStreamSupport from finn.util.data_packing import numpy_to_hls_code, pack_innermost_dim_as_hex_string from finn.util.logging import log from finn.util.settings import get_settings -class VVAU(MemStreamSupport, HWCustomOp): +class VVAU(HWCustomOp): """Abstraction layer for HW implementation of VectorVectorActivation layers.""" def __init__(self, onnx_node, **kwargs): diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py index fc36dfdc5b..be92460f3e 100644 --- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py +++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py @@ -56,6 +56,7 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.fpgadataflow import is_hls_node, is_rtl_node from finn.util.logging import log +from finn.util.hbm_mock import HBMDummy def is_external_input(model: ModelWrapper, node: "NodeProto", i: int) -> bool: @@ -240,6 +241,27 @@ def connect_axi(self, node: "NodeProto", model: "ModelWrapper") -> None: if not node_inst.get_nodeattr("mlo_max_iter"): if node.op_type == "FINNLoop": for mm_intf_name in aximm_intf_name: + if self.functional_simulation: + code_gen_dir = make_build_dir( + prefix="code_gen_ipgen_" + inst_name + "_" + mm_intf_name[0] + "_dummy_" + ) + dummy = HBMDummy( + inst_name + "_" + mm_intf_name[0] + "_dummy", + 64, + 256, + Path(code_gen_dir), + ) + dummy.generate_hdl() + self.create_cmds.extend(dummy.code_generation_ipi()) + self.connect_cmds.extend(dummy.code_clk_rst()) + self.connect_cmds.extend( + [ + f"connect_bd_intf_net " + f"[get_bd_intf_pins {inst_name}/{mm_intf_name[0]}] " + f"[get_bd_intf_pins {dummy.name}/s_axi]", + ] + ) + continue self.connect_cmds.extend( [ f"make_bd_intf_pins_external " diff --git a/src/finn/transformation/fpgadataflow/hlssynth_ip.py b/src/finn/transformation/fpgadataflow/hlssynth_ip.py index 9f109d4c91..3c0a2a300f 100644 --- a/src/finn/transformation/fpgadataflow/hlssynth_ip.py +++ b/src/finn/transformation/fpgadataflow/hlssynth_ip.py @@ -1,3 +1,4 @@ +"""HLSSynthIP transformation.""" # Copyright (C) 2020, Xilinx, Inc. # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -27,12 +28,18 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os import qonnx.custom_op.registry as registry from qonnx.transformation.base import NodeLocalTransformation +from typing import Literal, cast, TYPE_CHECKING +from pathlib import Path from finn.util.fpgadataflow import is_hls_node from finn.util.logging import log +from finn.util.exception import FINNUserError, FINNInternalError +from onnx import NodeProto + +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend class HLSSynthIP(NodeLocalTransformation): @@ -50,37 +57,44 @@ class HLSSynthIP(NodeLocalTransformation): NodeLocalTransformation for more details. """ - def __init__(self, fpgapart=None, num_workers=None): + def __init__(self, fpgapart: str | None = None, num_workers: int | None = None) -> None: + """Initialize the transformation with the given FPGA part and number of workers.""" self.fpgapart = fpgapart super().__init__(num_workers=num_workers) - def applyNodeLocal(self, node): + def applyNodeLocal(self, node: "NodeProto") -> tuple[NodeProto, Literal[False]]: # noqa: N802 + """Apply the transformation to a single node. + See documentation in NodeLocalTransformation for more details.""" op_type = node.op_type if is_hls_node(node) or node.op_type == "FINNLoop": try: # lookup op_type in registry of CustomOps - inst = registry.getCustomOp(node) + inst = cast("HLSBackend", registry.getCustomOp(node)) # ensure that code is generated - assert ( - inst.get_nodeattr("code_gen_dir_ipgen") != "" - ), """Node - attribute "code_gen_dir_ipgen" is empty. Please run - transformation PrepareIP first.""" - if not ( - os.path.isdir(inst.get_nodeattr("ipgen_path")) - or os.path.isfile(inst.get_nodeattr("ipgen_path")) - ) or inst.get_nodeattr("code_gen_dir_ipgen") not in inst.get_nodeattr("ipgen_path"): + if inst.get_nodeattr("code_gen_dir_ipgen") == "": + raise FINNUserError( + "Node attribute 'code_gen_dir_ipgen' is empty. " + "Please run transformation PrepareIP first." + ) + ip_path = cast("str", inst.get_nodeattr("ipgen_path")) + ip_path_p = Path(ip_path) + if ( + not (ip_path_p.is_dir() or ip_path_p.is_file()) + or cast("str", inst.get_nodeattr("code_gen_dir_ipgen")) not in ip_path + ): # call the compilation function for this node inst.ipgen_singlenode_code(self.fpgapart) else: - log.debug(f"Using pre-existing IP for {node.name}") + log.debug(f"Using cached IP for {node.name}") # ensure that executable path is now set - assert ( - inst.get_nodeattr("ipgen_path") != "" - ), """Transformation - HLSSynthIP was not successful. Node attribute "ipgen_path" - is empty.""" + if inst.get_nodeattr("ipgen_path") == "": + raise FINNInternalError( + "Transformation HLSSynthIP was not successful. " + "Node attribute 'ipgen_path' is empty." + ) except KeyError: # exception if op_type is not supported - raise Exception("Custom op_type %s is currently not supported." % op_type) + raise FINNUserError( + f"Custom op_type {op_type} is currently not supported." + ) from None return (node, False) diff --git a/src/finn/transformation/fpgadataflow/prepare_ip.py b/src/finn/transformation/fpgadataflow/prepare_ip.py index 03d4f34937..a16e2fa14d 100644 --- a/src/finn/transformation/fpgadataflow/prepare_ip.py +++ b/src/finn/transformation/fpgadataflow/prepare_ip.py @@ -27,35 +27,47 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os -import qonnx.custom_op.registry as registry +from pathlib import Path +from typing import Literal, cast, TYPE_CHECKING +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation from finn.util.basic import make_build_dir from finn.util.fpgadataflow import is_hls_node, is_rtl_node from finn.util.logging import log +from finn.util.exception import FINNUserError +from finn.util.basic import getHWCustomOp +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend + from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend + from onnx import NodeProto -def _codegen_single_node(node, model, fpgapart, clk): - """Calls C++ code generation for one node. Resulting code can be used + +def _codegen_single_node( + node: "NodeProto", model: "ModelWrapper", fpgapart: str, clk: float +) -> None: + """Call C++ code generation for one node. Resulting code can be used to generate a Vivado IP block for the node.""" op_type = node.op_type try: # lookup op_type in registry of CustomOps - inst = registry.getCustomOp(node) + inst = cast("RTLBackend|HLSBackend", getHWCustomOp(node)) # get the path of the code generation directory - code_gen_dir = inst.get_nodeattr("code_gen_dir_ipgen") + code_gen_dir = cast("str", inst.get_nodeattr("code_gen_dir_ipgen")) + print(f"Code generation directory for node {node.name}: {code_gen_dir}") # ensure that there is a directory - if code_gen_dir == "" or not os.path.isdir(code_gen_dir): + if code_gen_dir == "" or not Path(code_gen_dir).is_dir(): code_gen_dir = make_build_dir(prefix="code_gen_ipgen_" + str(node.name) + "_") - inst.set_nodeattr("code_gen_dir_ipgen", code_gen_dir) + inst.set_nodeattr("code_gen_dir_ipgen", str(code_gen_dir)) # ensure that there is generated code inside the dir inst.code_generation_ipgen(model, fpgapart, clk) else: - log.debug(f"Using pre-existing code for {node.name}") + log.debug(f"Using cached code for {node.name}") + print(f"Using cached code for node {node.name}...") except KeyError: # exception if op_type is not supported - raise Exception(f"Custom op_type {op_type} is currently not supported.") + raise FINNUserError(f"Custom op_type {op_type} is currently not supported.") from None class PrepareIP(Transformation): @@ -66,7 +78,7 @@ class PrepareIP(Transformation): * fpgapart (string) - * clk in ns (int) + * clk in ns (float) Any nodes that already have a code_gen_dir_ipgen attribute pointing to a valid path will be skipped. @@ -80,14 +92,16 @@ class PrepareIP(Transformation): * For RTL layers: filled template verilog files that can be used to instantiate as module during IP stitching. - """ + """ # noqa: D400, D415 - def __init__(self, fpgapart, clk): + def __init__(self, fpgapart: str, clk: float) -> None: + """Initialize the transformation with the given FPGA part and clock period.""" super().__init__() self.fpgapart = fpgapart self.clk = clk - def apply(self, model): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: + """Apply the transformation to the model.""" for node in model.graph.node: if is_hls_node(node) or is_rtl_node(node): _codegen_single_node(node, model, self.fpgapart, self.clk) diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 872ed4888a..e68e197d54 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -50,13 +50,13 @@ from qonnx.util.basic import gen_finn_dt_tensor from typing import TYPE_CHECKING, cast -from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.data_packing import finnpy_to_packed_bytearray from finn.util.exception import FINNInternalError from finn.util.logging import log from finn.util.settings import get_settings if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from onnx import NodeProto # test boards used for bnn pynq tests @@ -115,6 +115,7 @@ def getHWCustomOp(node: "NodeProto") -> "HWCustomOp": # noqa: N802 """Get the HWCustomOp from a node. Throws an error if the node is not an HWCustomOp.""" + from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp n = getCustomOp(node) if not isinstance(n, HWCustomOp): raise FINNInternalError(f"Node {node.name} is not an HWCustomOp") diff --git a/src/finn/util/hbm_mock.py b/src/finn/util/hbm_mock.py new file mode 100644 index 0000000000..6f2b5c1dff --- /dev/null +++ b/src/finn/util/hbm_mock.py @@ -0,0 +1,72 @@ +"""Dummy class to mock the HBM interface for simulation purposes.""" + +from pathlib import Path + +from jinja2 import Environment + +from finn.util.settings import get_settings + + +class HBMDummy: + """Dummy class to mock the HBM interface for simulation purposes.""" + + def __init__(self, name: str, addr_width: int, data_width: int, codegen_dir: Path) -> None: + """Initialize the dummy HBM interface. + + Parameters + ---------- + name : str + Name of the HBM interface + addr_width : int + Width of the address bus in bits + data_width : int + Width of the data bus in bits + codegen_dir : Path + Directory where the generated HDL code should be saved + """ + self.name = name + self.addr_width = addr_width + self.data_width = data_width + self.data_bytes = data_width // 8 + self.codegen_dir = codegen_dir + + def generate_hdl(self) -> None: + """Render the mock HBM HDL template into the code generation directory.""" + rtlsrc = Path(get_settings().finn_rtllib) / "mock_hbm" / "hdl" + template_path = rtlsrc / "mock_template.v" + + self.codegen_dir.mkdir(parents=True, exist_ok=True) + + with template_path.open() as f: + template = f.read() + + template_dict = { + "TOP_MODULE_NAME": self.name, + "ADDR_WIDTH": self.addr_width, + "DATA_WIDTH": self.data_width, + "DATA_BYTES": self.data_bytes, + } + + env = Environment() + rendered_hdl = env.from_string(template).render(**template_dict) + output_path = self.codegen_dir / f"{self.name}.v" + output_path.write_text(rendered_hdl) + + def code_generation_ipi(self) -> list[str]: + """Code generation for IP integration.""" + f = self.codegen_dir / f"{self.name}.v" + return [ + f"add_files -norecurse {f}", + f"create_bd_cell -type module -reference {self.name} {self.name}", + ] + + def code_clk_rst(self) -> list[str]: + """Code generation for clock and reset signals.""" + return [ + # f"make_bd_pins_external [get_bd_pins {self.name}/ap_clk]", + # "set_property name ap_clk [get_bd_ports ap_clk_0]", + # f"make_bd_pins_external [get_bd_pins {self.name}/ap_rst_n]", + # "set_property name ap_rst_n [get_bd_ports ap_rst_n_0]", + f"connect_bd_net [get_bd_ports ap_rst_n] [get_bd_pins {self.name}/ap_rst_n]", + f"connect_bd_net [get_bd_ports ap_clk] [get_bd_pins {self.name}/ap_clk]", + ] From e4358a21508e623dec851564186e8e254a7c686d Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 8 May 2026 14:26:34 +0200 Subject: [PATCH 100/170] Fix linting --- finn-rtllib/mock_hbm/hdl/mock_template.v | 4 +- .../fpgadataflow/exp_cycles_per_layer.py | 7 +- .../analysis/fpgadataflow/floorplan_params.py | 5 +- .../fpgadataflow/hls_synth_res_estimation.py | 6 +- .../fpgadataflow/unsupported_layers.py | 2 +- .../builder/custom_step_library/resnet.py | 8 +- src/finn/core/rtlsim_exec.py | 4 +- .../fpgadataflow/hls/attention_heads_hls.py | 7 +- .../hls/elementwise_binary_hls.py | 1 - .../custom_op/fpgadataflow/hls/lookup_hls.py | 3 +- .../hls/matrixvectoractivation_hls.py | 11 +- .../fpgadataflow/hls/streamingfifo_hls.py | 34 +-- .../hls/vectorvectoractivation_hls.py | 13 +- src/finn/custom_op/fpgadataflow/hlsbackend.py | 263 +++++++++--------- src/finn/custom_op/fpgadataflow/hwcustomop.py | 4 +- .../fpgadataflow/matrixvectoractivation.py | 41 ++- .../rtl/convolutioninputgenerator_rtl.py | 9 +- .../custom_op/fpgadataflow/rtl/finn_loop.py | 6 +- .../rtl/matrixvectoractivation_rtl.py | 45 ++- .../custom_op/fpgadataflow/rtl/requant_rtl.py | 4 +- .../fpgadataflow/rtl/thresholding_rtl.py | 4 +- .../rtl/vectorvectoractivation_rtl.py | 33 ++- src/finn/templates/python_driver/driver.py | 4 +- .../fpgadataflow/convert_to_hw_layers.py | 4 +- .../fpgadataflow/create_stitched_ip.py | 2 +- .../fpgadataflow/hlssynth_ip.py | 8 +- .../fpgadataflow/loop_rolling.py | 18 +- .../transformation/fpgadataflow/prepare_ip.py | 10 +- .../fpgadataflow/raise_scalar_to_rank1.py | 2 +- .../fpgadataflow/simulation_isolated.py | 4 +- src/finn/transformation/streamline/reorder.py | 6 +- src/finn/util/basic.py | 4 +- src/finn/util/hbm_mock.py | 3 +- src/finn/util/mlo_sim.py | 2 +- src/finn/util/onnxscript_helpers.py | 4 +- src/finn/xsi/__init__.py | 13 +- 36 files changed, 306 insertions(+), 292 deletions(-) diff --git a/finn-rtllib/mock_hbm/hdl/mock_template.v b/finn-rtllib/mock_hbm/hdl/mock_template.v index 47d9cc437b..4e07e3a18e 100644 --- a/finn-rtllib/mock_hbm/hdl/mock_template.v +++ b/finn-rtllib/mock_hbm/hdl/mock_template.v @@ -171,10 +171,10 @@ always @(posedge ap_clk) begin // WLAST mismatch - protocol error detected // Continue anyway (lenient slave behavior) end - + // Always increment beat counter each write beat write_beat_idx <= write_beat_idx + 8'd1; - + // Track when all expected beats received if ((write_beat_idx + 8'd1) == (write_len + 8'd1)) begin write_all_beats_received <= 1'b1; diff --git a/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py b/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py index 52d4e43a5f..715b80a296 100644 --- a/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py +++ b/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py @@ -27,15 +27,16 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from finn.util.fpgadataflow import is_hls_node, is_rtl_node -from finn.util.basic import getHWCustomOp from typing import TYPE_CHECKING +from finn.util.basic import getHWCustomOp +from finn.util.fpgadataflow import is_hls_node, is_rtl_node + if TYPE_CHECKING: from qonnx.core.modelwrapper import ModelWrapper -def exp_cycles_per_layer(model:"ModelWrapper") -> dict[str, int]: +def exp_cycles_per_layer(model: "ModelWrapper") -> dict[str, int]: """Estimates the number of cycles per sample for dataflow layers in the given model. Ensure that all nodes have unique names (by calling the GiveUniqueNodeNames transformation) prior to calling this analysis pass to ensure all nodes are diff --git a/src/finn/analysis/fpgadataflow/floorplan_params.py b/src/finn/analysis/fpgadataflow/floorplan_params.py index 0f814a60bf..f047aeaa0d 100644 --- a/src/finn/analysis/fpgadataflow/floorplan_params.py +++ b/src/finn/analysis/fpgadataflow/floorplan_params.py @@ -28,14 +28,15 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from qonnx.custom_op.registry import getCustomOp -from finn.util.fpgadataflow import is_fpgadataflow_node from typing import TYPE_CHECKING, cast +from finn.util.fpgadataflow import is_fpgadataflow_node + if TYPE_CHECKING: from qonnx.core.modelwrapper import ModelWrapper -def floorplan_params(model:"ModelWrapper"): +def floorplan_params(model: "ModelWrapper"): """Gathers SLR and partition IDs from nodes. Returns {node name : {slr, device id, partition id, memory port}}.""" diff --git a/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py b/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py index 6bae3f66bf..96b45c7dc6 100644 --- a/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py +++ b/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py @@ -25,19 +25,19 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from pathlib import Path import qonnx.custom_op.registry as registry import xml.etree.ElementTree as ET +from pathlib import Path +from typing import TYPE_CHECKING from finn.util.fpgadataflow import is_hls_node from finn.util.logging import log -from typing import TYPE_CHECKING if TYPE_CHECKING: from qonnx.core.modelwrapper import ModelWrapper -def hls_synth_res_estimation(model:"ModelWrapper") -> dict[str, dict[str, int]]: +def hls_synth_res_estimation(model: "ModelWrapper") -> dict[str, dict[str, int]]: """Extract the FPGA resource results from the Vitis HLS synthesis estimates. Note that this analysis pass only works on nodes that have an HLS backend. Ensure that all nodes have unique names (by calling the GiveUniqueNodeNames diff --git a/src/finn/analysis/fpgadataflow/unsupported_layers.py b/src/finn/analysis/fpgadataflow/unsupported_layers.py index 09d4a6ce26..62236ac899 100644 --- a/src/finn/analysis/fpgadataflow/unsupported_layers.py +++ b/src/finn/analysis/fpgadataflow/unsupported_layers.py @@ -42,7 +42,7 @@ def is_supported_node(node: "NodeProto") -> bool: sink_nodes.append(n) # BFS to check paths - queue : deque[tuple[NodeProto, bool, bool]] = deque() + queue: deque[tuple[NodeProto, bool, bool]] = deque() # Track (node_id, in_green_section, has_seen_complete_green_section) visited = [] diff --git a/src/finn/builder/custom_step_library/resnet.py b/src/finn/builder/custom_step_library/resnet.py index 5303823d26..d24af9722e 100644 --- a/src/finn/builder/custom_step_library/resnet.py +++ b/src/finn/builder/custom_step_library/resnet.py @@ -128,9 +128,7 @@ def step_resnet_tidy(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrap return model -def step_resnet_streamline( - model: ModelWrapper, cfg: DataflowBuildConfig -) -> ModelWrapper: +def step_resnet_streamline(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Streamline ResNet models.""" transform = ComposedTransformation( [ @@ -151,9 +149,7 @@ def step_resnet_streamline( return model -def step_resnet_convert_to_hw( - model: ModelWrapper, cfg: DataflowBuildConfig -) -> ModelWrapper: +def step_resnet_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Convert ResNet models to hardware-specific operations.""" # Convert Squeeze and Unsqueeze operators to hardware operations model = model.transform(InferDataLayouts()) diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index fb7cf707b2..3597299dec 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -242,7 +242,9 @@ def rtlsim_exec_cppxsi( # retrieve the number of inputs from execution_context n_inferences = execution_context[model.get_first_global_in()] ifnames = model.get_metadata_prop("vivado_stitch_ifnames") - assert ifnames is not None, "Couldn't find stitched-IP interface names, did you run IP stitching first?" + assert ( + ifnames is not None + ), "Couldn't find stitched-IP interface names, did you run IP stitching first?" ifnames = eval(ifnames) if "aximm" in ifnames.keys() and ifnames["aximm"] != []: assert ( diff --git a/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py b/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py index 675e42c9da..429959f41e 100644 --- a/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py @@ -8,12 +8,11 @@ # Operating system stuff, e.g. paths import os +# The generic HW custom operator version of the operator as a base class +from finn.custom_op.fpgadataflow.attention_heads import MergeMultiHeads, SplitMultiHeads + # Base class for specializing HW operators as implemented via HLS from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend -# The generic HW custom operator version of the operator as a base class -from finn.custom_op.fpgadataflow.attention_heads import ( - MergeMultiHeads, SplitMultiHeads -) # HLS Backend specialization of the multi-head attention splitting operator diff --git a/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py b/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py index b3571f390b..6372ac4af1 100644 --- a/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py @@ -1210,4 +1210,3 @@ class ElementwiseMax_hls( elementwise_binary.ElementwiseMax, ): """HLS Implementation of the elementwise max operation.""" - diff --git a/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py b/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py index 02e8c8b35d..f55a90c3c8 100644 --- a/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py @@ -187,8 +187,7 @@ def generate_params(self, model, path): ext_mem_width = self.get_nodeattr("ext_mem_width") assert edt.bitwidth() == 8, ( "Lookup with mem_mode=external " - "only works with 8-bit embeddings but found " - + str(edt) + "only works with 8-bit embeddings but found " + str(edt) ) emb_dim = self.get_nodeattr("EmbeddingDim") # need to zero-pad embeddings in external mode for burst alignment diff --git a/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py b/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py index b8e7c60fe2..73573ca770 100644 --- a/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py @@ -29,13 +29,17 @@ import math import numpy as np import os -from qonnx.core.datatype import DataType +from qonnx.core.datatype import BaseDataType, DataType +from typing import TYPE_CHECKING from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.matrixvectoractivation import MVAU from finn.util.basic import is_versal from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy +if TYPE_CHECKING: + from qonnx.core.modelwrapper import ModelWrapper + # ONNX i/o tensor shape assumptions for MatrixVectorActivation_hls: # input 0 is the input tensor, shape (.., i_size) = (..., MW) # input 1 is the weight tensor, shape (i_size, o_size) = (MW, MH) @@ -406,7 +410,8 @@ def blackboxfunction(self): mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "internal_embedded": self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - f"""void {self.onnx_node.name}(hls::stream> &in0_V, + f"""void {self.onnx_node.name}( + hls::stream> &in0_V, hls::stream> &out0_V )""" ] @@ -616,7 +621,7 @@ def execute_node(self, context, graph): has to be set to one of the following value ("cppsim", "rtlsim")""" ) - def minimize_weight_bit_width(self, model): + def minimize_weight_bit_width(self, model: "ModelWrapper") -> BaseDataType: """Minimize weight and threshold datatypes, with HLS-specific adjustments. The HLS implementation uses the threshold datatype for comparisons. diff --git a/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py b/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py index 7c32da57b5..62210ec733 100644 --- a/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py @@ -47,29 +47,32 @@ def get_nodeattr_types(self): my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs - def global_includes(self): + def global_includes(self) -> None: + """Add global include for virtual FIFO implementation.""" self.code_gen_dict["$GLOBALS$"] = ['#include "virtual_fifo.hpp"'] - def defines(self, var): + def defines(self, var) -> None: numReps = 1 width = self.get_instream_width() self.code_gen_dict["$DEFINES$"] = [ - "#define Width %d " % width, - "#define numReps %d" % numReps, + f"#define Width {width} ", + f"#define numReps {numReps}", ] def strm_decl(self): self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( - f'hls::stream> in0_{self.hls_sname()} ("in0_{self.hls_sname()}");' + f"hls::stream> " + f'in0_{self.hls_sname()} ("in0_{self.hls_sname()}");' ) self.code_gen_dict["$STREAMDECLARATIONS$"].append( - f'hls::stream> out0_{self.hls_sname()} ("out0_{self.hls_sname()}");' + f"hls::stream> " + f'out0_{self.hls_sname()} ("out0_{self.hls_sname()}");' ) def docompute(self): self.code_gen_dict["$DOCOMPUTE$"] = [ - """ + f""" #pragma HLS dataflow disable_start_propagation static hls::stream> in_fifo; @@ -78,24 +81,25 @@ def docompute(self): #pragma HLS stream variable=out_fifo depth=2 // AXI-Stream -> FIFO - move(in0_%s, in_fifo); + move(in0_{self.hls_sname()}, in_fifo); // Main VirtualFIFO(in_fifo, out_fifo, mode, depth, occupancy, max_occupancy); // FIFO -> AXI-Stream - move(out_fifo, out0_%s); + move(out_fifo, out0_{self.hls_sname()}); """ - % (self.hls_sname(), self.hls_sname()) ] def blackboxfunction(self): in_packed_bits = self.get_instream_width() - in_packed_hls_type = "ap_uint<%d>" % in_packed_bits + in_packed_hls_type = f"ap_uint<{in_packed_bits}>" out_packed_bits = self.get_outstream_width() - out_packed_hls_type = "ap_uint<%d>" % out_packed_bits + out_packed_hls_type = f"ap_uint<{out_packed_bits}>" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """void %s(hls::stream<%s > &in0_%s, hls::stream<%s > &out0_%s, ap_uint<32> mode, + f"""void {self.onnx_node.name}( + hls::stream<{in_packed_hls_type} > &in0_{self.hls_sname()}, + hls::stream<{out_packed_hls_type} > &out0_{self.hls_sname()}, ap_uint<32> mode, ap_uint<32> depth, ap_uint<32> &occupancy, ap_uint<32> &max_occupancy)""" % ( self.onnx_node.name, @@ -166,9 +170,7 @@ def execute_node(self, context, graph): elif mode == "rtlsim": sim = self.get_rtlsim() nbits = self.get_instream_width() - rtlsim_inp = npy_to_rtlsim_input( - f"{code_gen_dir}/input_0.npy", export_idt, nbits - ) + rtlsim_inp = npy_to_rtlsim_input(f"{code_gen_dir}/input_0.npy", export_idt, nbits) super().reset_rtlsim(sim) rtlsim_output = self.rtlsim(sim, rtlsim_inp) odt = export_idt diff --git a/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py b/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py index 89914015bd..67f671dc39 100644 --- a/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py @@ -29,7 +29,7 @@ import math import numpy as np import os -from qonnx.core.datatype import DataType +from qonnx.core.datatype import BaseDataType, DataType from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.vectorvectoractivation import VVAU @@ -455,14 +455,15 @@ def dataoutstrm(self): ) ] - def save_as_npy(self): + def save_as_npy(self) -> None: self.code_gen_dict["$SAVEASCNPY$"] = [] - def blackboxfunction(self): + def blackboxfunction(self) -> None: mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "internal_embedded": self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - f"""void {self.onnx_node.name}(hls::stream> &in0_V, + f"""void {self.onnx_node.name}( + hls::stream> &in0_V, hls::stream> &out0_V )""" ] @@ -480,7 +481,7 @@ def blackboxfunction(self): currently no other parameter value is supported!""" ) - def pragmas(self): + def pragmas(self) -> None: mem_mode = self.get_nodeattr("mem_mode") self.code_gen_dict["$PRAGMAS$"] = ["#pragma HLS INTERFACE axis port=in0_V"] self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE axis port=out0_V") @@ -510,7 +511,7 @@ def pragmas(self): "#pragma HLS ARRAY_PARTITION variable=threshs.m_thresholds complete dim=3" ) - def minimize_weight_bit_width(self, model): + def minimize_weight_bit_width(self, model) -> BaseDataType: """Minimize weight and threshold datatypes, with HLS-specific adjustments. The HLS implementation uses the threshold datatype for comparisons. diff --git a/src/finn/custom_op/fpgadataflow/hlsbackend.py b/src/finn/custom_op/fpgadataflow/hlsbackend.py index f52f62554f..a706bced13 100644 --- a/src/finn/custom_op/fpgadataflow/hlsbackend.py +++ b/src/finn/custom_op/fpgadataflow/hlsbackend.py @@ -29,23 +29,27 @@ """HLS backend implementation for FINN custom operations.""" import numpy as np +import numpy.typing as npt import os from abc import ABC, abstractmethod from pathlib import Path from qonnx.core.datatype import DataType +from qonnx.core.modelwrapper import ModelWrapper +from typing import TYPE_CHECKING, Literal, cast -from finn import xsi +from finn import xsi as finnxsi from finn.custom_op.fpgadataflow import templates from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.templates import get_templates_folder from finn.util.basic import CppBuilder, launch_process_helper, make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy -from finn.util.exception import FINNError, FINNUserError +from finn.util.exception import FINNInternalError, FINNUserError from finn.util.hls import CallHLS from finn.util.logging import log from finn.util.settings import get_settings -finnxsi = xsi if xsi.is_available() else None +if TYPE_CHECKING: + from onnx import GraphProto class HLSBackend(HWCustomOp, ABC): @@ -54,7 +58,13 @@ class HLSBackend(HWCustomOp, ABC): custom node should have. Some as abstract methods, these have to be filled when writing a new HLS custom op node.""" - def get_nodeattr_types(self): + def get_nodeattr_types( + self, + ) -> dict[ + str, + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], + ]: """Return dictionary of node attribute types and properties.""" super_types = super().get_nodeattr_types() super_types.update( @@ -70,37 +80,38 @@ def get_nodeattr_types(self): ) return super_types - def get_all_verilog_paths(self): + def get_all_verilog_paths(self) -> list[str]: """Return list of all folders containing Verilog code for this node.""" - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - assert ( - code_gen_dir != "" - ), """Node attribute "code_gen_dir_ipgen" is - not set. Please run HLSSynthIP first.""" + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) + if code_gen_dir == "": + raise FINNUserError( + f"""Node attribute "code_gen_dir_ipgen" is + not set for node {self.onnx_node.name}. Please run HLSSynthIP first.""" + ) verilog_path = f"{code_gen_dir}/project_{self.onnx_node.name}/sol1/impl/verilog/" subcore_verilog_path = f"{code_gen_dir}/project_{self.onnx_node.name}/sol1/impl/ip/hdl/ip/" # default impl only returns the HLS verilog codegen dir and subcore (impl/ip/hdl/ip) dir # if it exists ret = [verilog_path] - if os.path.isdir(subcore_verilog_path): + if Path(subcore_verilog_path).is_dir(): ret += [subcore_verilog_path] return ret - def get_all_verilog_filenames(self, abspath=False): + def get_all_verilog_filenames(self, abspath: bool = False) -> list[str]: """Return list of all Verilog files used for this node.""" - verilog_files = [] + verilog_files: list[str] = [] verilog_paths = self.get_all_verilog_paths() for verilog_path in verilog_paths: - for f in os.listdir(verilog_path): - if f.endswith(".v"): + for f in Path(verilog_path).iterdir(): + if f.is_file() and f.suffix == ".v": if abspath: - verilog_files += [verilog_path + "/" + f] + verilog_files += [f.absolute().as_posix()] else: - verilog_files += [f] + verilog_files += [f.relative_to(verilog_path).as_posix()] return verilog_files - def prepare_rtlsim(self, behav=False): - """Creates a xsi emulation library for the RTL code generated + def prepare_rtlsim(self, behav: bool = False) -> None: + """Create a xsi emulation library for the RTL code generated for this node, sets the rtlsim_so attribute to its path.""" verilog_files = self.get_all_verilog_filenames(abspath=True) single_src_dir = make_build_dir("rtlsim_" + self.onnx_node.name + "_") @@ -112,12 +123,12 @@ def prepare_rtlsim(self, behav=False): # save generated lib filename in attribute self.set_nodeattr("rtlsim_so", ret[0] + "/" + ret[1]) - def code_generation_ipgen(self, model, fpgapart, clk): + def code_generation_ipgen(self, model: "ModelWrapper", fpgapart: str, clk: float) -> None: """Generate C++ code and TCL script for IP generation.""" node = self.onnx_node # generate top cpp file for ip generation - path = self.get_nodeattr("code_gen_dir_ipgen") + path = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) self.code_gen_dict["$AP_INT_MAX_W$"] = [str(self.get_ap_int_max_w())] self.generate_params(model, path) self.global_includes() @@ -132,10 +143,9 @@ def code_generation_ipgen(self, model, fpgapart, clk): # transform list into long string separated by '\n' code_gen_line = "\n".join(self.code_gen_dict[key]) template = template.replace(key, code_gen_line) - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - f = open(os.path.join(code_gen_dir, f"top_{node.name}.cpp"), "w") - f.write(template) - f.close() + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) + f = Path(code_gen_dir) / f"top_{node.name}.cpp" + f.open("w").write(template) self.code_gen_dict.clear() if node.name in ["", None]: @@ -164,13 +174,12 @@ def code_generation_ipgen(self, model, fpgapart, clk): # transform list into long string separated by '\n' code_gen_line = "\n".join(self.code_gen_dict[key]) template = template.replace(key, code_gen_line) - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") - f = open(os.path.join(code_gen_dir, f"hls_syn_{node.name}.tcl"), "w") - f.write(template) - f.close() + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) + f = Path(code_gen_dir) / f"hls_syn_{node.name}.tcl" + f.open("w").write(template) self.code_gen_dict.clear() - def ipgen_default_directives(self): + def ipgen_default_directives(self) -> list[str]: """Return list of default HLS synthesis directives.""" default_directives = [ "set_param hls.enable_hidden_option_error false", @@ -181,20 +190,21 @@ def ipgen_default_directives(self): ] return default_directives - def ipgen_extra_directives(self): + def ipgen_extra_directives(self) -> list[str]: """Return a list of extra TCL directives for HLS synthesis.""" return [] - def ipgen_singlenode_code(self, fpgapart=None): + def ipgen_singlenode_code(self, fpgapart: str | None = None) -> None: # noqa: ARG002 """Build the bash script for IP generation using the CallHLS utility.""" node = self.onnx_node - code_gen_dir = Path(self.get_nodeattr("code_gen_dir_ipgen")) + code_gen_dir = Path(cast("str", self.get_nodeattr("code_gen_dir_ipgen"))) builder = CallHLS( tcl_script=code_gen_dir / f"hls_syn_{node.name}.tcl", code_gen_dir=code_gen_dir, ipgen_path=code_gen_dir / f"project_{node.name}", ) success = False + ip_path = None while not success: builder.build() if not builder.ipgen_path.is_dir(): @@ -204,13 +214,13 @@ def ipgen_singlenode_code(self, fpgapart=None): ) ipgen_path = str(builder.ipgen_path) self.set_nodeattr("ipgen_path", ipgen_path) - ip_path = ipgen_path + "/sol1/impl/ip" - if not os.path.isdir(ip_path): + ip_path = builder.ipgen_path / "sol1" / "impl" / "ip" + if not ip_path.is_dir(): # Workaround for possible race condition between Vitis HLS instances is_port_conflict = False - xcd_log_path = os.path.join(ipgen_path, "sol1", ".autopilot", "xcd.log") - if os.path.isfile(xcd_log_path): - with open(xcd_log_path) as xcd_log: + xcd_log_path = builder.ipgen_path / "sol1" / ".autopilot" / "xcd.log" + if xcd_log_path.is_file(): + with xcd_log_path.open() as xcd_log: for line in xcd_log: if "Address already in use" in line: is_port_conflict = True @@ -220,20 +230,20 @@ def ipgen_singlenode_code(self, fpgapart=None): "(XCD server port conflict). Retrying..." ) else: - raise FINNError( + raise FINNInternalError( f"IPGen failed: {ip_path} not found. Check log under {code_gen_dir}" ) else: success = True - self.set_nodeattr("ip_path", ip_path) - vlnv = "xilinx.com:hls:%s:1.0" % node.name + self.set_nodeattr("ip_path", str(ip_path)) + vlnv = f"xilinx.com:hls:{node.name}:1.0" self.set_nodeattr("ip_vlnv", vlnv) - def code_generation_cppsim(self, model): + def code_generation_cppsim(self, model: ModelWrapper) -> None: """Generate C++ code for simulation (cppsim).""" node = self.onnx_node - path = self.get_nodeattr("code_gen_dir_cppsim") + path = cast("str", self.get_nodeattr("code_gen_dir_cppsim")) self.code_gen_dict["$AP_INT_MAX_W$"] = [str(self.get_ap_int_max_w())] self.generate_params(model, path) self.global_includes() @@ -257,21 +267,20 @@ def code_generation_cppsim(self, model): # transform list into long string separated by '\n' code_gen_line = "\n".join(self.code_gen_dict[key]) template = template.replace(key, code_gen_line) - code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") - f = open(os.path.join(code_gen_dir, f"execute_{node.op_type}.cpp"), "w") - f.write(template) - f.close() + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_cppsim")) + f = Path(code_gen_dir) / f"execute_{node.op_type}.cpp" + f.open("w").write(template) self.code_gen_dict.clear() - def code_generation_ipi(self): + def code_generation_ipi(self) -> list[str]: """Construct and return the TCL for node instantiation in Vivado IPI.""" vlnv = self.get_nodeattr("ip_vlnv") - cmd = ["create_bd_cell -type ip -vlnv %s %s" % (vlnv, self.onnx_node.name)] + cmd = [f"create_bd_cell -type ip -vlnv {vlnv} {self.onnx_node.name}"] return cmd - def compile_singlenode_code(self): + def compile_singlenode_code(self) -> None: """Build bash script for compilation using CppBuilder and execute to produce executable.""" - code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_cppsim")) hls_path = os.environ.get("XILINX_HLS") builder = CppBuilder() # to enable additional debug features please uncommand the next line @@ -299,7 +308,7 @@ def compile_singlenode_code(self): builder.build(code_gen_dir) self.set_nodeattr("executable_path", builder.executable_path) - def npy_to_dynamic_output(self, context): + def npy_to_dynamic_output(self, context: dict[str, np.ndarray]) -> None: """Read output.npy file generated from cppsim and place into context dictionary.""" node = self.onnx_node code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") @@ -308,39 +317,41 @@ def npy_to_dynamic_output(self, context): exp_shape = self.get_normal_output_shape(o) context[outp] = output.reshape(exp_shape) - def exec_precompiled_singlenode_model(self): + def exec_precompiled_singlenode_model(self) -> None: """Execute precompiled executable.""" - executable_path = self.get_nodeattr("executable_path") + executable_path = cast("str", self.get_nodeattr("executable_path")) if executable_path == "": - raise Exception( + raise FINNUserError( """ Found no executable for this node, did you run the codegen and compilation transformations? """ ) - launch_process_helper(executable_path, print_stdout=False) + launch_process_helper([executable_path], print_stdout=False) # TODO: Should have been removed by refactoring (PR #1318) # However, it is still used by some CustomOps, namely: # SplitMultiHeads, MergeMultiHeads, ScaledDotProductAttention, # ReplicateStream, StreamingConcat - def hls_sname(self): + def hls_sname(self) -> Literal["V"]: """Get the naming convention used by Vitis HLS for stream signals Example: the TDATA for a stream called "out" would be out_V_TDATA. """ return "V" - def execute_node(self, context, graph): + def execute_node( + self, context: dict[str, np.ndarray], graph: "GraphProto" + ) -> None: # noqa: ARG002 """Execute node in specified mode (cppsim or rtlsim).""" mode = self.get_nodeattr("exec_mode") node = self.onnx_node if mode == "cppsim": - code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_cppsim")) elif mode == "rtlsim": - code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + code_gen_dir = cast("str", self.get_nodeattr("code_gen_dir_ipgen")) else: - raise Exception( + raise FINNInternalError( f"""Invalid value for attribute exec_mode! Is currently set to: {mode} has to be set to one of the following value ("cppsim", "rtlsim")""" ) @@ -376,12 +387,10 @@ def execute_node(self, context, graph): reshaped_input = inp_val.reshape(folded_ishape) reshaped_input = reshaped_input.copy() # This npy file will be read by the cppsim executable - np.save(os.path.join(code_gen_dir, "input_%s.npy" % i), reshaped_input) + np.save(Path(code_gen_dir) / f"input_{i}.npy", reshaped_input) # The rtlsim will instead operate on a flattened int sequence from an "io_dict" - rtlsim_inp = npy_to_rtlsim_input( - f"{code_gen_dir}/input_{i}.npy", export_idt, nbits - ) - inputs["in%s" % i] = rtlsim_inp + rtlsim_inp = npy_to_rtlsim_input(f"{code_gen_dir}/input_{i}.npy", export_idt, nbits) + inputs[f"in{i}"] = rtlsim_inp if mode == "cppsim": # execute the precompiled model @@ -400,8 +409,8 @@ def execute_node(self, context, graph): context[outp] = out elif mode == "rtlsim": outputs = {} - for o, outp in enumerate(node.output): - outputs["out%s" % o] = [] + for o, _outp in enumerate(node.output): + outputs[f"out{o}"] = [] # assembled execution context io_dict = {"inputs": inputs, "outputs": outputs} @@ -410,7 +419,7 @@ def execute_node(self, context, graph): self.rtlsim_multi_io(sim, io_dict) self.close_rtlsim(sim) for o, outp in enumerate(node.output): - rtlsim_output = io_dict["outputs"]["out%s" % o] + rtlsim_output = io_dict["outputs"][f"out{o}"] odt = self.get_output_datatype(o) target_bits = odt.bitwidth() packed_bits = self.get_outstream_width(o) @@ -436,14 +445,14 @@ def execute_node(self, context, graph): ) @abstractmethod - def global_includes(self): - """Function to set the global includes for c++ code that has to be generated + def global_includes(self) -> None: + """Set the global includes for c++ code that has to be generated for cppsim or rtlsim, is member function of HLSBackend class but has to be filled by every node.""" @abstractmethod - def defines(self, var): - """Function to set the define commands for c++ code that has to be generated + def defines(self, var: str) -> None: + """Set the define commands for c++ code that has to be generated for cppsim or rtlsim, is member function of HLSBackend class but has to be filled by every node. @@ -451,21 +460,21 @@ def defines(self, var): I.e. if set to "ipgen" in MatrixVectorActivation additional PRAGMA defines are added.""" - def read_npy_data(self): + def read_npy_data(self) -> None: """Generate commands for reading data from .npy file in C++. Might need to be overwritten depending on CustomOp.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") self.code_gen_dict["$READNPYDATA$"] = [] cpp_interface = self.get_nodeattr("cpp_interface") - for i, inp in enumerate(self.onnx_node.input): + for i, _inp in enumerate(self.onnx_node.input): dtype = self.get_input_datatype(i) if dtype == DataType["BIPOLAR"]: # use binary for bipolar storage dtype = DataType["BINARY"] elem_hls_type = dtype.get_hls_datatype_str() npy_type = "half" if elem_hls_type == "half" else "float" - npy_in = "%s/input_%s.npy" % (code_gen_dir, i) + npy_in = f"{code_gen_dir}/input_{i}.npy" iwidth = self.get_instream_width(i) # if the stream is not exposed, it has 0 width and no npy file will be created @@ -474,50 +483,38 @@ def read_npy_data(self): if cpp_interface == "packed": elem_bits = dtype.bitwidth() packed_bits = iwidth - packed_hls_type = "ap_uint<%d>" % packed_bits + packed_hls_type = f"ap_uint<{packed_bits}>" self.code_gen_dict["$READNPYDATA$"].append( - 'npy2apintstream<%s, %s, %d, %s>("%s", in%s_V);' - % ( - packed_hls_type, - elem_hls_type, - elem_bits, - npy_type, - npy_in, - i, - ) + f"npy2apintstream<{packed_hls_type}, {elem_hls_type}, {elem_bits}, {npy_type}>" + f'("{npy_in}", in{i}_V);' ) else: folded_shape = self.get_folded_input_shape() self.code_gen_dict["$READNPYDATA$"].append( - 'npy2vectorstream<%s, %s, %d>("%s", in%s_V, false);' - % ( - elem_hls_type, - npy_type, - folded_shape[-1], - npy_in, - i, - ) + f"npy2vectorstream<{elem_hls_type}, {npy_type}, {folded_shape[-1]}>" + f'("{npy_in}", in{i}_V, false);' ) - def strm_decl(self): + def strm_decl(self) -> None: """Generate commands for stream declaration in C++. Might need to be overwritten depending on CustomOp.""" node = self.onnx_node cpp_interface = self.get_nodeattr("cpp_interface") self.code_gen_dict["$STREAMDECLARATIONS$"] = [] if cpp_interface == "packed": - for i, inp in enumerate(node.input): + for i, _inp in enumerate(node.input): if self.get_instream_width(i): self.code_gen_dict["$STREAMDECLARATIONS$"].append( f'hls::stream> in{i}_V ("in{i}_V");' ) - for o, outp in enumerate(node.output): + for o, _outp in enumerate(node.output): if self.get_outstream_width(o): self.code_gen_dict["$STREAMDECLARATIONS$"].append( - f'hls::stream> out{o}_V ("out{o}_V");' + f"hls::stream> " + f'out{o}_V ("out{o}_V");' ) else: - for i, inp in enumerate(node.input): + for i, _inp in enumerate(node.input): if self.get_instream_width(i): dtype = self.get_input_datatype(i) if dtype == DataType["BIPOLAR"]: @@ -526,10 +523,12 @@ def strm_decl(self): elem_input_hls_type = dtype.get_hls_datatype_str() self.code_gen_dict["$STREAMDECLARATIONS$"].append( - f'hls::stream> in{i}_V ("in{i}_V");' + f"hls::stream> in{i}_V ("in{i}_V");' ) - for o, outp in enumerate(node.output): + elem_output_hls_type = None + for o, _outp in enumerate(node.output): if self.get_outstream_width(o): dtype = self.get_output_datatype(o) if dtype == DataType["BIPOLAR"]: @@ -538,36 +537,38 @@ def strm_decl(self): elem_output_hls_type = dtype.get_hls_datatype_str() self.code_gen_dict["$STREAMDECLARATIONS$"].append( - f'hls::stream> out{o}_V ("out{o}_V");' + f"hls::stream> out{o}_V ("out{o}_V");' ) if self.get_nodeattr("hls_style") == "freerunning": - for o, outp in enumerate(node.output): + for o, _outp in enumerate(node.output): if self.get_outstream_width(o): self.code_gen_dict["$STREAMDECLARATIONS$"].append( - f'hls::stream> strm{o} ("strm{o}");' + f"hls::stream> strm{o} ("strm{o}");' ) @abstractmethod - def docompute(self): - """Function to generate the commands for the computational part of the + def docompute(self) -> None: + """Generate the commands for the computational part of the c++ code, is member function of HLSBackend class but has to be filled by every node.""" - def dataoutstrm(self): + def dataoutstrm(self) -> None: """Generate commands for reading out data from C++ and converting to npy format. Might need to be overwritten depending on CustomOp.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") self.code_gen_dict["$DATAOUTSTREAM$"] = [] - for o, outp in enumerate(self.onnx_node.output): + for o, _outp in enumerate(self.onnx_node.output): dtype = self.get_output_datatype(o) if dtype == DataType["BIPOLAR"]: # use binary for bipolar storage dtype = DataType["BINARY"] elem_hls_type = dtype.get_hls_datatype_str() npy_type = "half" if elem_hls_type == "half" else "float" - npy_out = "%s/output_%s.npy" % (code_gen_dir, o) + npy_out = f"{code_gen_dir}/output_{o}.npy" oshape = self.get_folded_output_shape(o) oshape_cpp_str = str(oshape).replace("(", "{").replace(")", "}") @@ -576,19 +577,11 @@ def dataoutstrm(self): if cpp_interface == "packed": elem_bits = dtype.bitwidth() packed_bits = self.get_outstream_width(o) - packed_hls_type = "ap_uint<%d>" % packed_bits + packed_hls_type = f"ap_uint<{packed_bits}>" self.code_gen_dict["$DATAOUTSTREAM$"].append( - 'apintstream2npy<%s, %s, %d, %s>(out%s_V, %s, "%s");' - % ( - packed_hls_type, - elem_hls_type, - elem_bits, - npy_type, - o, - oshape_cpp_str, - npy_out, - ) + f"apintstream2npy<{packed_hls_type}, {elem_hls_type}, {elem_bits}, {npy_type}>" + f'(out{o}_V, {oshape_cpp_str}, "{npy_out}");' ) else: folded_shape = self.get_folded_output_shape(o) @@ -596,28 +589,21 @@ def dataoutstrm(self): f"strm{o}" if self.get_nodeattr("hls_style") == "freerunning" else f"out{o}_V" ) self.code_gen_dict["$DATAOUTSTREAM$"].append( - 'vectorstream2npy<%s, %s, %d>(%s, %s, "%s");' - % ( - elem_hls_type, - npy_type, - folded_shape[-1], - out_vector, - oshape_cpp_str, - npy_out, - ) + f"vectorstream2npy<{elem_hls_type}, {npy_type}, {folded_shape[-1]}>" + f'({out_vector}, {oshape_cpp_str}, "{npy_out}");' ) - def save_as_npy(self): + def save_as_npy(self) -> None: """Generate commands for saving data in .npy file in C++.""" self.code_gen_dict["$SAVEASCNPY$"] = [] @abstractmethod - def blackboxfunction(self): - """Function to generate a blackbock function in c++ from which an IP block + def blackboxfunction(self) -> None: + """Generate a blackbock function in c++ from which an IP block will be generated, is member function of HLSBackend class but has to be filled by every node.""" - def pragmas(self): + def pragmas(self) -> None: """Generate pragma commands in C++. Might need to be overwritten depending on CustomOp.""" # TODO: make this loop over all inputs/outputs so we don't need as much @@ -626,23 +612,24 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE axis port=out0_V") self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE ap_ctrl_none port=return") - def get_ap_int_max_w(self): + def get_ap_int_max_w(self) -> int: """Return the maximum width of any ap_int used in this module. Used to set the AP_INT_MAX_W definition for HLS.""" instream = self.get_instream_width() outstream = self.get_outstream_width() ret = max([instream, outstream]) - assert ret <= 8191, "AP_INT_MAX_W=%d is larger than allowed maximum of 8191" % ret + if ret > 8191: + raise FINNInternalError(f"AP_INT_MAX_W={ret} is larger than allowed maximum of 8191") return ret - def timeout_value(self): + def timeout_value(self) -> None: """Set timeout value for HLS functions defined for one clock cycle.""" self.code_gen_dict["$TIMEOUT_VALUE$"] = ["1000"] - def timeout_condition(self): + def timeout_condition(self) -> None: """Set timeout condition for HLS functions defined for one clock cycle.""" self.code_gen_dict["$TIMEOUT_CONDITION$"] = ["out0_V.empty()"] - def timeout_read_stream(self): + def timeout_read_stream(self) -> None: """Set reading output stream procedure for HLS functions defined for one clock cycle.""" self.code_gen_dict["$TIMEOUT_READ_STREAM$"] = ["strm0 << out0_V.read();"] diff --git a/src/finn/custom_op/fpgadataflow/hwcustomop.py b/src/finn/custom_op/fpgadataflow/hwcustomop.py index 099cf9ab42..4b9d9b0784 100644 --- a/src/finn/custom_op/fpgadataflow/hwcustomop.py +++ b/src/finn/custom_op/fpgadataflow/hwcustomop.py @@ -63,7 +63,7 @@ class HWCustomOp(CustomOp): when writing a new fpgadataflow custom op node. """ - def __init__(self, onnx_node: NodeProto, **kwargs: Any) -> None: + def __init__(self, onnx_node: NodeProto, **kwargs: int) -> None: """Initialize HWCustomOp with an ONNX node. Args: @@ -339,7 +339,7 @@ def verify_node(self) -> None: are there and that particular attributes are set correctly. Can also check if the number of inputs is equal to the expected number.""" - def generate_params(self, model: Any, path: str) -> None: + def generate_params(self, model: "ModelWrapper", path: str | Path) -> None: """Generate parameters (i.e. weights and thresholds). Member function of HWCustomOp class that must be implemented by every node diff --git a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py index 43603ec112..678c1c3fa0 100644 --- a/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py +++ b/src/finn/custom_op/fpgadataflow/matrixvectoractivation.py @@ -35,6 +35,7 @@ import math import numpy as np +import numpy.typing as npt import os import qonnx.custom_op.general.xnorpopcount as xp import textwrap @@ -45,12 +46,16 @@ interleave_matrix_outer_dim_from_partitions, roundup_to_integer_multiple, ) +from typing import TYPE_CHECKING, cast from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.data_packing import numpy_to_hls_code, pack_innermost_dim_as_hex_string from finn.util.logging import log from finn.util.settings import get_settings +if TYPE_CHECKING: + from onnx import NodeProto + # ONNX i/o tensor shape assumptions for MatrixVectorActivation: # input 0 is the input tensor, shape (.., i_size) = (..., MW) # input 1 is the weight tensor, shape (i_size, o_size) = (MW, MH) @@ -62,7 +67,7 @@ class MVAU(HWCustomOp): """Abstraction layer for HW implementation of MatrixVectorActivation layers.""" - def __init__(self, onnx_node, **kwargs): + def __init__(self, onnx_node: "NodeProto", **kwargs: int) -> None: """Initialize the MVAU custom operation. Parameters @@ -74,7 +79,13 @@ def __init__(self, onnx_node, **kwargs): """ super().__init__(onnx_node, **kwargs) - def get_nodeattr_types(self): + def get_nodeattr_types( + self, + ) -> dict[ + str, + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], + ]: """Get dictionary of attribute names and their types for this node. Returns @@ -82,7 +93,11 @@ def get_nodeattr_types(self): dict Dictionary mapping attribute names to type specifications """ - my_attrs = { + my_attrs: dict[ + str, + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], + ] = { "PE": ("i", True, 0), "SIMD": ("i", True, 0), "MW": ("i", True, 0), @@ -509,23 +524,23 @@ def uram_estimation(self): depth_multiplier = math.ceil(omega / 4096) return width_multiplier * depth_multiplier - def bram_estimation(self): - """Calculates resource estimation for BRAM based on: + def bram_estimation(self) -> int: + """Calculate resource estimation for BRAM based on: - FINN-R: An End-to-End Deep-Learning Framework for Fast Exploration of Quantized Neural Networks - M. Blott, T. B. Preusser, N. J. Fraser, G. Gambardella, K. O'Brien, Y. Umuroglu, M. Leeser and K. Vissers - - 12. Sep 2018 + - 12. Sep 2018. """ # TODO add in/out FIFO contributions - P = self.get_nodeattr("PE") - Q = self.get_nodeattr("SIMD") + p = cast("int", self.get_nodeattr("PE")) + q = cast("int", self.get_nodeattr("SIMD")) wdt = self.get_input_datatype(1) - W = wdt.bitwidth() - D_in = self.get_nodeattr("MW") - D_out = self.get_nodeattr("MH") - omega = (D_in * D_out) / (Q * P) - mem_width = Q * W * P + w = wdt.bitwidth() + d_in = cast("int", self.get_nodeattr("MW")) + d_out = cast("int", self.get_nodeattr("MH")) + omega = (d_in * d_out) / (q * p) + mem_width = q * w * p mmode = self.get_nodeattr("mem_mode") mstyle = self.get_nodeattr("ram_style") if ( diff --git a/src/finn/custom_op/fpgadataflow/rtl/convolutioninputgenerator_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/convolutioninputgenerator_rtl.py index 24681ca1a4..e90e0525e0 100755 --- a/src/finn/custom_op/fpgadataflow/rtl/convolutioninputgenerator_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/convolutioninputgenerator_rtl.py @@ -41,6 +41,7 @@ from qonnx.custom_op.general.im2col import compute_conv_output_dim from qonnx.custom_op.registry import getCustomOp from qonnx.util.basic import roundup_to_integer_multiple +from typing import Literal from finn.custom_op.fpgadataflow.convolutioninputgenerator import ConvolutionInputGenerator from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend @@ -764,11 +765,13 @@ def prepare_codegen_parallel(self): code_gen_dict["$GENERATE_OUTPUT_MAPPING$"] = [] out_idx = mmv_out - 1 for fifo_id, reg_fifo in enumerate(reg_fifos): - for fifo_idx, access_idx in enumerate(reg_fifo): + for _fifo_idx, access_idx in enumerate(reg_fifo): if access_idx != -1: code_gen_dict["$GENERATE_OUTPUT_MAPPING$"].append( f"""assign data_out[OUT_ELEM_WIDTH*{out_idx}+:OUT_ELEM_WIDTH] - = reg_fifo_{fifo_id}[{len(reg_fifo) - 1 - int((max(reg_fifo) - access_idx) / M)}*{M}*OUT_ELEM_WIDTH+ + = reg_fifo_{fifo_id}[ + {len(reg_fifo) - 1 - int((max(reg_fifo) - access_idx) / M)} + *{M}*OUT_ELEM_WIDTH+ OUT_ELEM_WIDTH*{(max(reg_fifo) - access_idx) % M}+:OUT_ELEM_WIDTH];""" ) # reversal: out_idx=0 -> oldest buffer element -> highest access_idx @@ -798,7 +801,7 @@ def prepare_codegen_parallel(self): return template_path, code_gen_dict - def select_impl_style(self): + def select_impl_style(self) -> Literal["parallel", "default"]: """Select implementation style based on folding configuration.""" simd = self.get_nodeattr("SIMD") M = self.get_nodeattr("M") diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index 3923e0fedd..25652d7ca7 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -28,10 +28,10 @@ import copy import math -import re import numpy as np import numpy.typing as npt import os +import re import shutil import subprocess from onnx import GraphProto @@ -66,7 +66,9 @@ def collect_ip_dirs(model, ipstitch_path): for node in model.graph.node: node_inst = getCustomOp(node) ip_dir_value = node_inst.get_nodeattr("ip_path") - assert os.path.isdir(ip_dir_value), """The directory that should + assert os.path.isdir( + ip_dir_value + ), """The directory that should contain the generated ip blocks doesn't exist.""" ip_dirs += [ip_dir_value] if node.op_type.startswith("MVAU") or node.op_type == "Thresholding_hls": diff --git a/src/finn/custom_op/fpgadataflow/rtl/matrixvectoractivation_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/matrixvectoractivation_rtl.py index 2731683a02..5613975bdc 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/matrixvectoractivation_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/matrixvectoractivation_rtl.py @@ -34,14 +34,20 @@ """ import numpy as np +import numpy.typing as npt import os +from typing import TYPE_CHECKING, Literal from finn.custom_op.fpgadataflow.matrixvectoractivation import MVAU from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.util.basic import get_dsp_block, is_versal from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy +from finn.util.exception import FINNUserError from finn.util.settings import get_settings +if TYPE_CHECKING: + from onnx import NodeProto + # ONNX i/o tensor shape assumptions for MatrixVectorActivation_rtl: # input 0 is the input tensor, shape (.., i_size) = (..., MW) # input 1 is the weight tensor, shape (i_size, o_size) = (MW, MH) @@ -52,7 +58,7 @@ class MVAU_rtl(MVAU, RTLBackend): """Class that corresponds to finn-rtl Matrix Vector Unit.""" - def __init__(self, onnx_node, **kwargs): + def __init__(self, onnx_node: "NodeProto", **kwargs: int) -> None: """Initialize the RTL Matrix Vector Activation Unit. Parameters @@ -64,7 +70,13 @@ def __init__(self, onnx_node, **kwargs): """ super().__init__(onnx_node, **kwargs) - def get_nodeattr_types(self): + def get_nodeattr_types( + self, + ) -> dict[ + str, + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], + ]: """Get dictionary of attribute names and their types for this node. Returns @@ -73,7 +85,11 @@ def get_nodeattr_types(self): Dictionary mapping attribute names to type specifications, including pumpedCompute for double-pumped DSP operation """ - my_attrs = { + my_attrs: dict[ + str, + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], + ] = { # Double-pumped DSPs enabled "pumpedCompute": ("i", False, 0, {0, 1}), } @@ -127,12 +143,11 @@ def execute_node(self, context, graph): reshaped_input, ) - if in_ind == 1: - if dynamic_input or self.get_nodeattr("mlo_max_iter"): - reshaped_input = context[inputs].reshape(-1, context[inputs].shape[-1]) - self.make_weight_file( - reshaped_input, "decoupled_npy", f"{code_gen_dir}/input_1.npy" - ) + if in_ind == 1 and (dynamic_input or self.get_nodeattr("mlo_max_iter")): + reshaped_input = context[inputs].reshape(-1, context[inputs].shape[-1]) + self.make_weight_file( + reshaped_input, "decoupled_npy", f"{code_gen_dir}/input_1.npy" + ) sim = self.get_rtlsim() nbits = self.get_instream_width() @@ -323,7 +338,7 @@ def _resolve_segment_len(self, clk): dsp_chain_len = critical_path_dsps if critical_path_dsps < max_chain_len else max_chain_len return dsp_chain_len - def _resolve_dsp_version(self, dsp_block): + def _resolve_dsp_version(self, dsp_block: str) -> Literal[3, 2, 1]: """Resolve DSP version based on target FPGA device. Selects the appropriate RTL compute core version for the target DSP type. @@ -340,10 +355,12 @@ def _resolve_dsp_version(self, dsp_block): """ # Based on target device and activation/weight-width, choose the # supported RTL compute core - assert ( - self.get_nodeattr("resType") != "lut" - ), f"""LUT-based RTL-MVU implementation currently not supported! - Please change resType for {self.onnx_node.name} to 'dsp' or consider switching to HLS-based MVAU!""" + if self.get_nodeattr("resType") == "lut": + raise FINNUserError( + f"LUT-based RTL-MVU implementation currently not supported!" + f"Please change resType for {self.onnx_node.name} to 'dsp' " + f"or consider switching to HLS-based MVAU!" + ) match dsp_block: case "DSP58": diff --git a/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py index 43badbbcb3..c3b7ff7564 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py @@ -187,9 +187,7 @@ def execute_node(self, context, graph): reshaped_input = inp_val.reshape(folded_ishape) np.save(os.path.join(code_gen_dir, "input_0.npy"), reshaped_input) nbits = self.get_instream_width(0) - rtlsim_inp = npy_to_rtlsim_input( - f"{code_gen_dir}/input_0.npy", export_idt, nbits - ) + rtlsim_inp = npy_to_rtlsim_input(f"{code_gen_dir}/input_0.npy", export_idt, nbits) io_dict = { "inputs": {"in0": rtlsim_inp}, diff --git a/src/finn/custom_op/fpgadataflow/rtl/thresholding_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/thresholding_rtl.py index ef6072a8ff..946f0114ba 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/thresholding_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/thresholding_rtl.py @@ -385,9 +385,7 @@ def execute_node(self, context, graph): sim = self.get_rtlsim() nbits = self.get_instream_width() - rtlsim_inp = npy_to_rtlsim_input( - f"{code_gen_dir}/input_0.npy", export_idt, nbits - ) + rtlsim_inp = npy_to_rtlsim_input(f"{code_gen_dir}/input_0.npy", export_idt, nbits) io_dict = { "inputs": {"in0": rtlsim_inp}, "outputs": {"out0": []}, diff --git a/src/finn/custom_op/fpgadataflow/rtl/vectorvectoractivation_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/vectorvectoractivation_rtl.py index ceddb71a50..c49dd2606e 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/vectorvectoractivation_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/vectorvectoractivation_rtl.py @@ -36,11 +36,13 @@ import numpy as np import os from qonnx.core.datatype import DataType +from typing import Literal from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.custom_op.fpgadataflow.vectorvectoractivation import VVAU from finn.util.basic import is_versal from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy +from finn.util.exception import FINNUserError from finn.util.settings import get_settings @@ -138,9 +140,7 @@ def execute_node(self, context, graph): # so use it as such for weight generation if self.get_input_datatype(1) == DataType["BIPOLAR"]: export_wdt = DataType["BINARY"] - wei = npy_to_rtlsim_input( - f"{code_gen_dir}/weights.npy", export_wdt, wnbits - ) + wei = npy_to_rtlsim_input(f"{code_gen_dir}/weights.npy", export_wdt, wnbits) dim_h, dim_w = self.get_nodeattr("Dim") num_w_reps = dim_h * dim_w @@ -174,7 +174,7 @@ def execute_node(self, context, graph): has to be set to one of the following value ("cppsim", "rtlsim")""" ) - def lut_estimation(self): + def lut_estimation(self) -> Literal[0]: """Estimate LUT utilization for this VVAU node. Returns @@ -184,7 +184,7 @@ def lut_estimation(self): """ return 0 - def dsp_estimation(self, fpgapart): + def dsp_estimation(self, fpgapart: str) -> int: """Estimate DSP utilization for this VVAU node. Parameters @@ -201,7 +201,7 @@ def dsp_estimation(self, fpgapart): Q = self.get_nodeattr("SIMD") return int(P * np.ceil(Q / 3)) - def instantiate_ip(self, cmd): + def instantiate_ip(self, cmd) -> None: """Add RTL IP instantiation commands to Vivado script. Parameters @@ -334,7 +334,7 @@ def _resolve_segment_len(self, clk): dsp_chain_len = critical_path_dsps if critical_path_dsps < max_chain_len else max_chain_len return dsp_chain_len - def _resolve_dsp_version(self, fpgapart): + def _resolve_dsp_version(self, fpgapart: str) -> Literal[3]: """Resolve DSP version based on target FPGA part. Parameters @@ -349,19 +349,22 @@ def _resolve_dsp_version(self, fpgapart): Raises ------ - AssertionError + FINNUserError If LUT-based compute or non-Versal device is targeted """ # Based on target device and activation/weight-width, choose the # supported RTL compute core - assert ( - self.get_nodeattr("resType") != "lut" - ), f"""LUT-based RTL-VVU implementation currently not supported! - Please change resType for {self.onnx_node.name} to 'dsp' or consider switching to HLS-based VVAU!""" + if self.get_nodeattr("resType") == "lut": + raise FINNUserError( + f"""LUT-based RTL-VVU implementation currently not supported! + Please change resType for {self.onnx_node.name} " + f"to 'dsp' or consider switching to HLS-based VVAU!""" + ) is_versal_family = is_versal(fpgapart) - assert ( - is_versal_family - ), "DSP-based (RTL) VVU currently only supported on Versal (DSP58) devices" + if not is_versal_family: + raise FINNUserError( + "DSP-based (RTL) VVU currently only supported on Versal (DSP58) devices" + ) return 3 diff --git a/src/finn/templates/python_driver/driver.py b/src/finn/templates/python_driver/driver.py index ec1340e481..2d1a1121a8 100644 --- a/src/finn/templates/python_driver/driver.py +++ b/src/finn/templates/python_driver/driver.py @@ -138,8 +138,8 @@ def load_external_weights(self): hw_ext_weights = self.io_shape_dict["number_of_external_weights"] assert len(self.external_weights) == hw_ext_weights, ( "Number of hardware external weights and number of external " - "weight tensors available do not match. \n" - "Is runtime_weight_dir pointing to the correct folder?" + "weight tensors available do not match. \n" + "Is runtime_weight_dir pointing to the correct folder?" ) def load_runtime_weights(self, flush_accel=True, verify=True): diff --git a/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py b/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py index c1cd348b33..7ba8da374d 100644 --- a/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py +++ b/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py @@ -1234,9 +1234,7 @@ def apply(self, model): accum_bits = inst.get_accum_size() else: - raise Exception( - f"pad_value and pool_fxn not configured for {node.op_type}" - ) + raise Exception(f"pad_value and pool_fxn not configured for {node.op_type}") # format input tensor im2col_node = helper.make_node( diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py index be92460f3e..c7f4f2da0b 100644 --- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py +++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py @@ -55,8 +55,8 @@ from finn.util.basic import launch_process_helper, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.fpgadataflow import is_hls_node, is_rtl_node -from finn.util.logging import log from finn.util.hbm_mock import HBMDummy +from finn.util.logging import log def is_external_input(model: ModelWrapper, node: "NodeProto", i: int) -> bool: diff --git a/src/finn/transformation/fpgadataflow/hlssynth_ip.py b/src/finn/transformation/fpgadataflow/hlssynth_ip.py index 3c0a2a300f..82d8c1d5ef 100644 --- a/src/finn/transformation/fpgadataflow/hlssynth_ip.py +++ b/src/finn/transformation/fpgadataflow/hlssynth_ip.py @@ -29,14 +29,14 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import qonnx.custom_op.registry as registry -from qonnx.transformation.base import NodeLocalTransformation -from typing import Literal, cast, TYPE_CHECKING +from onnx import NodeProto from pathlib import Path +from qonnx.transformation.base import NodeLocalTransformation +from typing import TYPE_CHECKING, Literal, cast +from finn.util.exception import FINNInternalError, FINNUserError from finn.util.fpgadataflow import is_hls_node from finn.util.logging import log -from finn.util.exception import FINNUserError, FINNInternalError -from onnx import NodeProto if TYPE_CHECKING: from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend diff --git a/src/finn/transformation/fpgadataflow/loop_rolling.py b/src/finn/transformation/fpgadataflow/loop_rolling.py index 7f7718194d..93b5d09330 100644 --- a/src/finn/transformation/fpgadataflow/loop_rolling.py +++ b/src/finn/transformation/fpgadataflow/loop_rolling.py @@ -32,8 +32,7 @@ def get_constant_from_value(value): - """Get the constant value of a tensor. - """ + """Get the constant value of a tensor.""" # Handle input and/or inititalizer values if value.producer() is None: return value.const_value.numpy() @@ -42,8 +41,7 @@ def get_constant_from_value(value): def same_values(inputs): - """Check if all inputs have the same constant value. - """ + """Check if all inputs have the same constant value.""" if not inputs: return False @@ -72,16 +70,12 @@ def build_loop_replace_pattern(graph, LoopBody): for node in nodes: if node.inputs[i].shape != g_shape: log.warning( - - f"LoopRolling: Index {i} expected shape {g_shape}, " - f"got {node.inputs[i].shape}." - + f"LoopRolling: Index {i} expected shape {g_shape}, " + f"got {node.inputs[i].shape}." ) raise Exception( - - "LoopRolling: all loop-body initializers of the same index " - "must have the same shape." - + "LoopRolling: all loop-body initializers of the same index " + "must have the same shape." ) # Build Concat Node diff --git a/src/finn/transformation/fpgadataflow/prepare_ip.py b/src/finn/transformation/fpgadataflow/prepare_ip.py index a16e2fa14d..43d95e4345 100644 --- a/src/finn/transformation/fpgadataflow/prepare_ip.py +++ b/src/finn/transformation/fpgadataflow/prepare_ip.py @@ -28,20 +28,20 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from pathlib import Path -from typing import Literal, cast, TYPE_CHECKING from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation +from typing import TYPE_CHECKING, Literal, cast -from finn.util.basic import make_build_dir +from finn.util.basic import getHWCustomOp, make_build_dir +from finn.util.exception import FINNUserError from finn.util.fpgadataflow import is_hls_node, is_rtl_node from finn.util.logging import log -from finn.util.exception import FINNUserError -from finn.util.basic import getHWCustomOp if TYPE_CHECKING: + from onnx import NodeProto + from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend - from onnx import NodeProto def _codegen_single_node( diff --git a/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py b/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py index a136feda37..1a146370a1 100644 --- a/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py +++ b/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py @@ -29,9 +29,9 @@ from __future__ import annotations +from collections.abc import Iterable from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation -from collections.abc import Iterable import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 1e5de21135..60f0ec975d 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -598,9 +598,7 @@ def get_index(a: Any, values: Any) -> int | None: # TODO: Tests edited_bounds[predecessor.name][producer_idx] = in_fifo_upper_bound[ node.name - ][ - key - ] + ][key] log.info( f"Incoming FIFO {node.name}[{key}/{consumer_idx}] " f"-> outgoing FIFO {predecessor.name}[{producer_idx}]" diff --git a/src/finn/transformation/streamline/reorder.py b/src/finn/transformation/streamline/reorder.py index d36adee2b6..c53f16ff78 100644 --- a/src/finn/transformation/streamline/reorder.py +++ b/src/finn/transformation/streamline/reorder.py @@ -1000,8 +1000,7 @@ def permute_shape(shape, perm): class MoveScalarLinearPastSplit(Transformation): - """Move scalar Mul and Add nodes past channel split operation. - """ + """Move scalar Mul and Add nodes past channel split operation.""" def __init__(self): super().__init__() @@ -1568,8 +1567,7 @@ def move_node(self, model, n, producers): class MoveAffinePastJoinConcat(MoveIdenticalOpPastJoinOp): - """Applies to scalar linear or channelwise affine ops with the same parameter value - """ + """Applies to scalar linear or channelwise affine ops with the same parameter value""" def __init__(self, linear_ops=["Mul", "Add"]): super().__init__(linear_ops, ["Concat"]) diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index e68e197d54..398aa527e6 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -56,9 +56,10 @@ from finn.util.settings import get_settings if TYPE_CHECKING: - from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from onnx import NodeProto + from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp + # test boards used for bnn pynq tests test_board_map = ["Pynq-Z1", "KV260_SOM", "ZCU104", "U55C"] @@ -116,6 +117,7 @@ def getHWCustomOp(node: "NodeProto") -> "HWCustomOp": # noqa: N802 """Get the HWCustomOp from a node. Throws an error if the node is not an HWCustomOp.""" from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp + n = getCustomOp(node) if not isinstance(n, HWCustomOp): raise FINNInternalError(f"Node {node.name} is not an HWCustomOp") diff --git a/src/finn/util/hbm_mock.py b/src/finn/util/hbm_mock.py index 6f2b5c1dff..3c0199f860 100644 --- a/src/finn/util/hbm_mock.py +++ b/src/finn/util/hbm_mock.py @@ -1,8 +1,7 @@ """Dummy class to mock the HBM interface for simulation purposes.""" -from pathlib import Path - from jinja2 import Environment +from pathlib import Path from finn.util.settings import get_settings diff --git a/src/finn/util/mlo_sim.py b/src/finn/util/mlo_sim.py index d5985576a0..d01911cb31 100644 --- a/src/finn/util/mlo_sim.py +++ b/src/finn/util/mlo_sim.py @@ -31,9 +31,9 @@ # aximm simulation tasks for handling the aximm interfaces. import numpy as np +from collections.abc import Callable from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp -from collections.abc import Callable from finn import xsi diff --git a/src/finn/util/onnxscript_helpers.py b/src/finn/util/onnxscript_helpers.py index dd51aa950e..d517ec88ca 100644 --- a/src/finn/util/onnxscript_helpers.py +++ b/src/finn/util/onnxscript_helpers.py @@ -178,9 +178,7 @@ def hierarchy_matches( return True # Return all nodes from the given name hierarchy on down - def get_nodes( - self, search_hierarchy: list[str], instance_hierarchy: list[str] | None = None - ): + def get_nodes(self, search_hierarchy: list[str], instance_hierarchy: list[str] | None = None): if instance_hierarchy is None: instance_hierarchy = [] diff --git a/src/finn/xsi/__init__.py b/src/finn/xsi/__init__.py index d09d775eed..ce389d13dd 100644 --- a/src/finn/xsi/__init__.py +++ b/src/finn/xsi/__init__.py @@ -6,7 +6,7 @@ # SPDX-License-Identifier: BSD-3-Clause # # ########################################################################## -"""FINN XSI (Xilinx Simulation Interface) support module +"""FINN XSI (Xilinx Simulation Interface) support module. This module provides utilities for RTL simulation support via finn_xsi. The finn_xsi extension must be built separately using the setup command. @@ -18,10 +18,11 @@ import finn_xsi.adapter """ +import contextlib import os import sys from pathlib import Path -from typing import Any, Optional +from typing import Any from finn.util.logging import log @@ -137,10 +138,8 @@ def _load_modules() -> bool: finally: # Remove from path if we added it if path_added and str(xsi_path) in sys.path: - try: + with contextlib.suppress(ValueError): sys.path.remove(str(xsi_path)) - except ValueError: - pass # Path was already removed somehow # List of functions to wrap from finn_xsi.adapter @@ -174,13 +173,13 @@ def _wrapper(*args, **kwargs): class SimEngine: """Wrapper for finn_xsi.sim_engine.SimEngine.""" - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: """Create a new SimEngine.""" if not _load_modules(): raise ImportError("finn_xsi not available. Run: python -m finn.xsi.setup") self._engine = _sim_engine_module.SimEngine(*args, **kwargs) - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: """Get attribute of the given name.""" return getattr(self._engine, name) From 6a463dc21d2d8d6d14860bf83835d5d923c42f9a Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 8 May 2026 14:30:37 +0200 Subject: [PATCH 101/170] Fix the noqa placement in hlsbackend --- src/finn/custom_op/fpgadataflow/hlsbackend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/finn/custom_op/fpgadataflow/hlsbackend.py b/src/finn/custom_op/fpgadataflow/hlsbackend.py index a706bced13..39eda40e6e 100644 --- a/src/finn/custom_op/fpgadataflow/hlsbackend.py +++ b/src/finn/custom_op/fpgadataflow/hlsbackend.py @@ -340,8 +340,8 @@ def hls_sname(self) -> Literal["V"]: return "V" def execute_node( - self, context: dict[str, np.ndarray], graph: "GraphProto" - ) -> None: # noqa: ARG002 + self, context: dict[str, np.ndarray], graph: "GraphProto" # noqa: ARG002 + ) -> None: """Execute node in specified mode (cppsim or rtlsim).""" mode = self.get_nodeattr("exec_mode") node = self.onnx_node From f2f9df1a3957bcf5fb94bc34ca113e085f02c29d Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 8 May 2026 16:39:06 +0200 Subject: [PATCH 102/170] Remove old FIFO sizing --- src/finn/builder/build_dataflow_config.py | 37 +- src/finn/builder/build_dataflow_steps.py | 422 +++++----- .../fpgadataflow/insert_fifo.py | 144 ++-- .../fpgadataflow/set_fifo_depths.py | 752 +++--------------- .../transformation/fpgadataflow/simulation.py | 36 +- 5 files changed, 439 insertions(+), 952 deletions(-) diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index 743d1b99f4..517a5d8a02 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -95,8 +95,7 @@ def to_logging_level(level: LogLevel) -> int: class AutoFIFOSizingMethod(str, Enum): """Select the type of automatic FIFO sizing strategy.""" - CHARACTERIZE = "characterize" - LARGEFIFO_RTLSIM = "largefifo_rtlsim" + LIVE_FIFO = "live_fifo" DISTRIBUTED_SIMULATION = "distributed_sim" @@ -239,7 +238,7 @@ def construct_from(cls, from_this: Path | DataflowBuildConfig) -> DataflowBuildC Returns: The completed config object """ # noqa - dfbc: DataflowBuildConfig + dfbc: DataflowBuildConfig = DataflowBuildConfig() # Read the config if type(from_this) in [Path, PosixPath, PurePath]: @@ -325,7 +324,7 @@ def _fix_path(p: Path | None) -> Path | None: #: Which output(s) to generate from the build flow. See documentation of #: DataflowOutputType for available options. - generate_outputs: Optional[list[DataflowOutputType]] = field( + generate_outputs: list[DataflowOutputType] = field( default_factory=lambda: [ DataflowOutputType.STITCHED_IP, DataflowOutputType.ESTIMATE_REPORTS, @@ -347,7 +346,7 @@ def _fix_path(p: Path | None) -> Path | None: specialize_layers_config_file: Path | None = None #: (Optional) Path to configuration JSON file. May include parallelization, - #: FIFO sizes, RAM and implementation style attributes and so on. + #: RAM and implementation style attributes and so on. #: If the parallelization attributes (PE, SIMD) are part of the config, #: this will override the automatically generated parallelization #: attributes inferred from target_fps (if any) @@ -358,6 +357,13 @@ def _fix_path(p: Path | None) -> Path | None: #: conversion (permutation) for global model inputs and outputs. layouts_config_file: Path | None = None + #: (Optional) Path to configuration JSON file in which user can specify + #: FIFO sizes for each FIFO in the design. This is only needed if + #: auto_fifo_depths is set to False. + #: This file is usually generated by a previous run of build_dataflow with + #: auto_fifo_depths enabled, but can also be manually edited to set custom FIFO sizes. + fifo_config_file: Path | None = None + #: (Optional) Target inference performance in frames per second. #: Note that target may not be achievable due to specific layer constraints, #: or due to resource limitations of the FPGA. @@ -459,42 +465,25 @@ def _fix_path(p: Path | None) -> Path | None: #: for each FIFO. auto_fifo_depths: bool = True - #: Enables experimental live FIFO sizing on the FPGA. - live_fifo_sizing: bool = False - #: Whether to use functional simulation when available. Takes some time #: to synthesize, but results in much faster simulations. functional_simulation: bool = True #: Whether FIFO nodes with depth larger than 32768 will be split. #: Allow to configure very large FIFOs in the folding_config_file. - split_large_fifos: bool = False + split_large_fifos: bool = True #: (Only relevant when auto_fifo_depths is enabled) #: Select which method will be used for setting the FIFO sizes. - auto_fifo_strategy: AutoFIFOSizingMethod = AutoFIFOSizingMethod.LARGEFIFO_RTLSIM + auto_fifo_strategy: AutoFIFOSizingMethod = AutoFIFOSizingMethod.DISTRIBUTED_SIMULATION #: (Only relevant when auto_fifo_depths is enabled) #: Memory resource type for large FIFOs. large_fifo_mem_style: LargeFIFOMemStyle = LargeFIFOMemStyle.AUTO - #: (Only relevant if auto_fifo_strategy = LARGEFIFO_RTLSIM) - #: Enable input throttling for simulation-based FIFO sizing. - fifosim_input_throttle: bool = True - - #: (Only relevant if auto_fifo_strategy = LARGEFIFO_RTLSIM) - #: Manually specify the number of inferences for simulation-based FIFO sizing - fifosim_n_inferences: int = 2 - - #: (Only relevant if auto_fifo_strategy = LARGEFIFO_RTLSIM) #: Enable saving waveforms from simulation-based FIFO sizing. fifosim_save_waveform: bool = False - #: (Only relevant if auto_fifo_strategy = LARGEFIFO_RTLSIM) - #: Call CapConvolutionFIFODepths in InsertAndSetFIFODepths transform - #: to make convolution FIFOs smaller where appropriate. - default_swg_exception: bool = False - #: (Optional) Target clock frequency (in nanoseconds) for Vitis HLS synthesis. #: e.g. `hls_clk_period_ns=5.0` will target a 200 MHz clock. #: If not specified it will default to synth_clk_period_ns diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 7116b3da10..d707709b17 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -33,6 +33,7 @@ """ import json +import math import numpy as np import os import shutil @@ -100,14 +101,12 @@ from finn.transformation.fpgadataflow.replace_verilog_relpaths import ReplaceVerilogRelPaths from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode from finn.transformation.fpgadataflow.set_fifo_depths import ( - InsertAndSetFIFODepths, - RemoveShallowFIFOs, + ApplyFIFODepthsFromFile, SplitLargeFIFOs, - xsi_fifosim, ) from finn.transformation.fpgadataflow.set_folding import SetFolding from finn.transformation.fpgadataflow.set_loop_boundary import SetLoopBoundary -from finn.transformation.fpgadataflow.simulation import ApplyFIFOSizes +from finn.transformation.fpgadataflow.simulation import ApplySimulatedFIFOSizes from finn.transformation.fpgadataflow.simulation_build import BuildSimulation from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers @@ -126,12 +125,13 @@ from finn.transformation.streamline import Streamline from finn.transformation.streamline.reorder import MakeMaxPoolNHWC from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds -from finn.util.basic import get_liveness_threshold_cycles, get_rtlsim_trace_depth -from finn.util.config import extract_model_config_consolidate_shuffles, extract_model_config_to_json +from finn.util.basic import get_liveness_threshold_cycles, get_rtlsim_trace_depth, getHWCustomOp +from finn.util.config import extract_model_config_to_json from finn.util.exception import FINNUserError from finn.util.execution import execute_parent from finn.util.logging import log from finn.util.mlo_sim import is_mlo, mlo_prehook_func_factory +from qonnx.transformation.base import Transformation if TYPE_CHECKING: from finn.custom_op.fpgadataflow.rtl.finn_loop import FINNLoop @@ -348,7 +348,9 @@ def verify_step( log.info(f"Verification for {step_name} : {res_to_str[all_res]}") -def prepare_for_stitched_ip_rtlsim(verify_model, cfg): +def prepare_for_stitched_ip_rtlsim( + verify_model: ModelWrapper, cfg: DataflowBuildConfig +) -> ModelWrapper: """Prepare model for stitched IP RTL simulation. Switches implementation styles from Vivado components to RTL where needed @@ -485,13 +487,166 @@ def step_size_fifo_connected(model: ModelWrapper, cfg: DataflowBuildConfig) -> M @register_build_dataflow_step() def step_apply_fifosizes(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Apply the previously found FIFO sizes to the model.""" - model = model.transform(ApplyFIFOSizes(cfg)) - model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + if cfg.split_large_fifos: + model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) return model +@register_build_dataflow_step() +def step_set_fifo_depths( + model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None +) -> ModelWrapper: + """Depending on the auto_fifo_depths setting, do one of the following: + * if auto_fifo_depths=True: Run the appropriate auto-sizing transformation + to attempt to determine the FIFO sizes that provide full throughput. + May take a long time. + * if auto_fifo_depths=False: Load the FIFO sizes from the folding config file and apply them. + Coherency with config file node naming is ensured by calling + `GiveUniqueNodeNamesRecursive`. + """ + if cfg.auto_fifo_depths: + if cfg.fifosim_save_waveform: + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) + model.set_metadata_prop("rtlsim_trace", str(report_dir.resolve() / "fifosim_trace.wdb")) + if cfg.auto_fifo_strategy == AutoFIFOSizingMethod.DISTRIBUTED_SIMULATION: + model = step_build_simulation(model, cfg, parent_node=parent_node) + model = step_size_fifo_connected(model, cfg) + model = step_apply_fifosizes(model, cfg) + elif cfg.auto_fifo_strategy == AutoFIFOSizingMethod.LIVE_FIFO: + hw_attrs = [ + "PE", + "SIMD", + "EmbFold", + "SeqFold", + "parallel_window", + "ram_style", + "ram_style_thresholds", + "ram_style_mask", + "depth", + "impl_style", + "resType", + "mac_resource", + "mem_mode", + "runtime_writeable_weights", + "inFIFODepths", + "outFIFODepths", + "depth_trigger_uram", + "depth_trigger_bram", + ] + # Create all DWCs and FIFOs normally + model = model.transform(InsertDWC()) + model = model.transform( + InsertFIFO(vivado_ram_style=cfg.large_fifo_mem_style, create_shallow_fifos=True) + ) + + # Clean up model + model = model.transform(SortGraph()) + model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveReadableTensorNames()) + + # save original folding config before potentially modifying it + cfg_path = str(cfg.output_dir) + "/report/folding_config_before_lfs.json" + extract_model_config_to_json(model, cfg_path, hw_attrs) + model.set_metadata_prop("folding_config_before_lfs", cfg_path) + + # Disable runtime-writable weights, external weights, and dynamic mode + for node in model.graph.node: + if node.domain.startswith("finn.custom_op.fpgadataflow"): + node_inst = getCustomOp(node) + try: + if node_inst.get_nodeattr("runtime_writeable_weights") == 1: + node_inst.set_nodeattr("runtime_writeable_weights", 0) + if node_inst.get_nodeattr("ram_style") == "ultra": + node_inst.set_nodeattr("ram_style", "block") + except AttributeError: + pass + try: + if node_inst.get_nodeattr("mem_mode") == "external": + node_inst.set_nodeattr("mem_mode", "internal_decoupled") + except AttributeError: + pass + try: + if node_inst.get_nodeattr("dynamic_mode") == 1: + node_inst.set_nodeattr("dynamic_mode", 0) + except AttributeError: + pass + + # Specialize FIFOs to RTL back-end + for node in model.get_nodes_by_op_type("StreamingFIFO"): + node_inst = getCustomOp(node) + node_inst.set_nodeattr("preferred_impl_style", "rtl") + model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) + + # Clean up model + model = model.transform(SortGraph()) + model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveReadableTensorNames()) + + # Set impl_style + ID attributes + # We can't infer ID from the unique node name at IP instantiation, + # because the nodes will be wrapped in SDPs + for node in model.get_nodes_by_op_type("StreamingFIFO_rtl"): + node_inst = getCustomOp(node) + idf = int(node.name.split("_")[-1]) + node_inst.set_nodeattr("impl_style", "virtual") + node_inst.set_nodeattr("fifo_id", idf) + + return model + else: + raise FINNUserError("Unsupported auto_fifo_strategy: " + cfg.auto_fifo_strategy) + + # generate a dedicated report about final FIFO sizes + fifo_info = {} + fifo_info["fifo_depths"] = {} + fifo_info["fifo_sizes"] = {} + fifo_info["impl_style"] = {} + fifo_info["ram_style"] = {} + total_fifo_size = 0 + for node in model.get_nodes_by_op_type("StreamingFIFO_rtl"): + node_inst = getHWCustomOp(node) + fifo_info["fifo_depths"][node.name] = node_inst.get_nodeattr("depth") + fifo_info["fifo_sizes"][node.name] = ( + node_inst.get_instream_width() + * math.ceil(cast("int", node_inst.get_nodeattr("depth")) / 32) + * 32 + ) # Round up to nearest multiple of 32 to reflect actual hardware usage + fifo_info["impl_style"][node.name] = node_inst.get_nodeattr("impl_style") + fifo_info["ram_style"][node.name] = node_inst.get_nodeattr("ram_style") + total_fifo_size += fifo_info["fifo_sizes"][node.name] + fifo_info["total_fifo_size_kiB"] = int(total_fifo_size / 8.0 / 1024.0) + + with (Path(cfg.output_dir) / "report" / "fifo_sizing.json").open("w") as f: + json.dump(fifo_info, f, indent=2) + else: + if cfg.fifo_config_file is None: + raise FINNUserError("auto_fifo_depths is set to False but no fifo_config_file provided") + log.info( + f"auto_fifo_depths is set to False, applying FIFO sizes from {cfg.fifo_config_file}" + ) + # insert DWCs, FIFOs and run ApplyConfig once more + model = model.transform(InsertDWC()) + # need to make sure all FIFOs are created so that their depth can be + # set by ApplyConfig, so create_shallow_fifos=True + model = model.transform(InsertFIFO(create_shallow_fifos=True)) + model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) + model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveReadableTensorNames()) + model = model.transform(ApplyFIFODepthsFromFile(cfg.fifo_config_file)) + + # after FIFOs are ready to go, call PrepareIP and HLSSynthIP again + # this will only run for the new nodes (e.g. FIFOs and DWCs) + # Codegen for the inserted FIFOs + model = step_hw_codegen(model, cfg) + # IP Gen for the inserted FIFOs and any remaining + # IPs that needed to be re-gen after FIFO insertion + model = step_hw_ipgen(model, cfg, parent_node=parent_node) + return model + + @register_build_dataflow_step() def step_generate_hardware( model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None @@ -531,21 +686,13 @@ def step_generate_hardware( model = step_hw_ipgen(model, cfg, parent_node=parent_node) # FIFO sizing for the current model - model = step_build_simulation(model, cfg, parent_node=parent_node) - model = step_size_fifo_connected(model, cfg) - model = step_apply_fifosizes(model, cfg) - - # Codegen for the inserted FIFOs - model = step_hw_codegen(model, cfg) - # IP Gen for the inserted FIFOs and any remaining - # IPs that needed to be re-gen after FIFO insertion - model = step_hw_ipgen(model, cfg, parent_node=parent_node) + model = step_set_fifo_depths(model, cfg, parent_node=parent_node) return model @register_build_dataflow_step() -def step_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_qonnx_to_finn(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Step will only execute if QONNX nodes are found. These include the following op_types: "Quant" , "Trunc" and "BinaryQuant". If such nodes are found the step will run the tidy-up step from QONNX @@ -632,7 +779,9 @@ def step_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWr preferred implementation styles for each node.""" # Helper function to conditionally apply transformation - def apply_if_relevant(model, op_types, transform, desc=""): + def apply_if_relevant( + model: ModelWrapper, op_types: list[str], transform: Transformation, desc: str = "" + ) -> ModelWrapper: # Check if any of the relevant op types exist in the model if any(len(model.get_nodes_by_op_type(op_type)) > 0 for op_type in op_types): if desc: @@ -951,64 +1100,69 @@ def step_apply_folding_config(model: ModelWrapper, cfg: DataflowBuildConfig) -> def step_generate_estimate_reports(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Generate per-layer resource and cycle estimates using analytical models.""" if DataflowOutputType.ESTIMATE_REPORTS in cfg.generate_outputs: - report_dir = cfg.output_dir + "/report" - os.makedirs(report_dir, exist_ok=True) + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) ops_and_params = model.analysis(op_and_param_counts) - with open(report_dir + "/op_and_param_counts.json", "w") as f: + with (report_dir / "op_and_param_counts.json").open("w") as f: json.dump(ops_and_params, f, indent=2) estimate_layer_cycles = model.analysis(exp_cycles_per_layer) - with open(report_dir + "/estimate_layer_cycles.json", "w") as f: + with (report_dir / "estimate_layer_cycles.json").open("w") as f: json.dump(estimate_layer_cycles, f, indent=2) estimate_layer_resources = model.analysis( partial(res_estimation, fpgapart=cfg._resolve_fpga_part()) ) estimate_layer_resources["total"] = aggregate_dict_keys(estimate_layer_resources) - with open(report_dir + "/estimate_layer_resources.json", "w") as f: + with (report_dir / "estimate_layer_resources.json").open("w") as f: json.dump(estimate_layer_resources, f, indent=2) estimate_layer_resources_complete = model.analysis( partial(res_estimation_complete, fpgapart=cfg._resolve_fpga_part()) ) - with open(report_dir + "/estimate_layer_config_alternatives.json", "w") as f: + with (report_dir / "estimate_layer_config_alternatives.json").open("w") as f: json.dump(estimate_layer_resources_complete, f, indent=2) # generate reports for MLO nodes loop_nodes = model.get_nodes_by_op_type("FINNLoop") for node in loop_nodes: - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") + node_inst = cast("FINNLoop", getCustomOp(node)) + loop_model = cast("ModelWrapper", node_inst.get_nodeattr("body")) ops_and_params = loop_model.analysis(op_and_param_counts) - with open(report_dir + f"/op_and_param_counts_{node.name}.json", "w") as f: + with (report_dir / f"op_and_param_counts_{node.name}.json").open("w") as f: json.dump(ops_and_params, f, indent=2) estimate_layer_cycles = loop_model.analysis(exp_cycles_per_layer) - with open(report_dir + f"/estimate_layer_cycles_{node.name}.json", "w") as f: + with (report_dir / f"estimate_layer_cycles_{node.name}.json").open("w") as f: json.dump(estimate_layer_cycles, f, indent=2) estimate_layer_resources = loop_model.analysis( partial(res_estimation, fpgapart=cfg._resolve_fpga_part()) ) estimate_layer_resources["total"] = aggregate_dict_keys(estimate_layer_resources) - with open(report_dir + f"/estimate_layer_resources_{node.name}.json", "w") as f: + with (report_dir / f"estimate_layer_resources_{node.name}.json").open("w") as f: json.dump(estimate_layer_resources, f, indent=2) estimate_layer_resources_complete = loop_model.analysis( partial(res_estimation_complete, fpgapart=cfg._resolve_fpga_part()) ) - with open( - report_dir + f"/estimate_layer_config_alternatives_{node.name}.json", "w" + with (report_dir / f"estimate_layer_config_alternatives_{node.name}.json").open( + "w" ) as f: json.dump(estimate_layer_resources_complete, f, indent=2) if not is_mlo(model): # need to call AnnotateCycles before dataflow_performance model = model.transform(AnnotateCycles()) - estimate_network_performance = model.analysis(dataflow_performance) + estimate_network_performance: dict[str, str | int | float] = dict( + model.analysis(dataflow_performance) + ) # add some more metrics to estimated performance n_clock_cycles_per_sec = (10**9) / cfg.synth_clk_period_ns - est_fps = n_clock_cycles_per_sec / estimate_network_performance["max_cycles"] + est_fps = n_clock_cycles_per_sec / cast( + "int", estimate_network_performance["max_cycles"] + ) estimate_network_performance["estimated_throughput_fps"] = est_fps est_latency_ns = ( - estimate_network_performance["critical_path_cycles"] * cfg.synth_clk_period_ns + cast("int", estimate_network_performance["critical_path_cycles"]) + * cfg.synth_clk_period_ns ) estimate_network_performance["estimated_latency_ns"] = est_latency_ns - with open(report_dir + "/estimate_network_performance.json", "w") as f: + with (report_dir / "estimate_network_performance.json").open("w") as f: json.dump(estimate_network_performance, f, indent=2) else: log.warning( @@ -1071,192 +1225,6 @@ def step_insert_dwc(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapp return model.transform(SpecializeLayers(cfg._resolve_fpga_part())) -@register_build_dataflow_step() -def step_set_fifo_depths(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Depending on the auto_fifo_depths setting, do one of the following: - * if auto_fifo_depths=True: Run the appropriate auto-sizing transformation - to attempt to determine the FIFO sizes that provide full throughput. - May take a long time. - * if auto_fifo_depths=False: Assume the folding config file contains FIFO - sizes as well. Runs the `InsertFIFO` transformation, then - `ApplyConfig(cfg.folding_config_file)`, and finally `RemoveShallowFIFOs`. - Coherency with config file node naming is ensured by calling - `GiveUniqueNodeNamesRecursive`. - """ - hw_attrs = [ - "PE", - "SIMD", - "EmbFold", - "SeqFold", - "parallel_window", - "ram_style", - "ram_style_thresholds", - "ram_style_mask", - "depth", - "impl_style", - "resType", - "mac_resource", - "mem_mode", - "runtime_writeable_weights", - "inFIFODepths", - "outFIFODepths", - "depth_trigger_uram", - "depth_trigger_bram", - ] - - # Experimental live FIFO-sizing, overwrites all other FIFO-related behavior - if cfg.live_fifo_sizing: - # Create all DWCs and FIFOs normally - model = model.transform(InsertDWC()) - model = model.transform( - InsertFIFO(vivado_ram_style=cfg.large_fifo_mem_style, create_shallow_fifos=True) - ) - - # Clean up model - model = model.transform(SortGraph()) - model = model.transform(GiveUniqueNodeNamesRecursive()) - model = model.transform(GiveReadableTensorNames()) - - # save original folding config before potentially modifying it - cfg_path = str(cfg.output_dir) + "/report/folding_config_before_lfs.json" - extract_model_config_to_json(model, cfg_path, hw_attrs) - model.set_metadata_prop("folding_config_before_lfs", cfg_path) - - # Disable runtime-writable weights, external weights, and dynamic mode - for node in model.graph.node: - if node.domain.startswith("finn.custom_op.fpgadataflow"): - node_inst = getCustomOp(node) - try: - if node_inst.get_nodeattr("runtime_writeable_weights") == 1: - node_inst.set_nodeattr("runtime_writeable_weights", 0) - if node_inst.get_nodeattr("ram_style") == "ultra": - node_inst.set_nodeattr("ram_style", "block") - except AttributeError: - pass - try: - if node_inst.get_nodeattr("mem_mode") == "external": - node_inst.set_nodeattr("mem_mode", "internal_decoupled") - except AttributeError: - pass - try: - if node_inst.get_nodeattr("dynamic_mode") == 1: - node_inst.set_nodeattr("dynamic_mode", 0) - except AttributeError: - pass - - # Specialize FIFOs to RTL back-end - for node in model.get_nodes_by_op_type("StreamingFIFO"): - node_inst = getCustomOp(node) - node_inst.set_nodeattr("preferred_impl_style", "rtl") - model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) - - # Clean up model - model = model.transform(SortGraph()) - model = model.transform(GiveUniqueNodeNamesRecursive()) - model = model.transform(GiveReadableTensorNames()) - - # Set impl_style + ID attributes - # We can't infer ID from the unique node name at IP instantiation, - # because the nodes will be wrapped in SDPs - for node in model.get_nodes_by_op_type("StreamingFIFO_rtl"): - node_inst = getCustomOp(node) - idf = int(node.name.split("_")[-1]) - node_inst.set_nodeattr("impl_style", "virtual") - node_inst.set_nodeattr("fifo_id", idf) - - return model - - if cfg.auto_fifo_depths: - strategy = cfg.auto_fifo_strategy - if strategy == "largefifo_rtlsim": - if cfg.fifosim_save_waveform: - report_dir = Path(cfg.output_dir) / "report" - report_dir.mkdir(parents=True, exist_ok=True) - model.set_metadata_prop( - "rtlsim_trace", str(report_dir.resolve() / "fifosim_trace.wdb") - ) - model = model.transform( - InsertAndSetFIFODepths( - cfg._resolve_fpga_part(), - cfg._resolve_hls_clk_period(), - swg_exception=cfg.default_swg_exception, - vivado_ram_style=cfg.large_fifo_mem_style, - fifosim_input_throttle=cfg.fifosim_input_throttle, - cfg_n_inferences=cfg.fifosim_n_inferences, - ) - ) - model = model.transform(GiveUniqueNodeNamesRecursive()) - model = model.transform(GiveReadableTensorNames(), apply_to_subgraphs=True) - # InsertAndSetFIFODepths internally removes any shallow FIFOs - # so no need to call RemoveShallowFIFOs here - elif cfg.auto_fifo_strategy == AutoFIFOSizingMethod.DISTRIBUTED_SIMULATION: - # TODO: When merging into dev, this should be finalized - model = step_build_simulation(model, cfg) - model = step_size_fifo_connected(model, cfg) - model = step_apply_fifosizes(model, cfg) - return model - else: - assert "Unsupported auto_fifo_strategy: " + cfg.auto_fifo_strategy - else: - log.info("auto_fifo_depths is set to False, assume folding cfg json contains FIFO sizes.") - # assume folding cfg json contains FIFO sizes too - # insert DWCs, FIFOs and run ApplyConfig once more - model = model.transform(InsertDWC()) - # need to make sure all FIFOs are created so that their depth can be - # set by ApplyConfig, so create_shallow_fifos=True - model = model.transform(InsertFIFO(create_shallow_fifos=True)) - model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) - model = model.transform(GiveUniqueNodeNamesRecursive()) - model = model.transform(GiveReadableTensorNames()) - if cfg.folding_config_file is not None: - model = model.transform(ApplyConfig(cfg.folding_config_file)) - - # extract the final configuration and save it as json - if model.get_nodes_by_op_type("InnerShuffle_rtl") or model.get_nodes_by_op_type( - "OuterShuffle_hls" - ): - extract_model_config_consolidate_shuffles( - model, cfg.output_dir + "/report/final_hw_config.json", hw_attrs - ) - else: - extract_model_config_to_json( - model, cfg.output_dir + "/report/final_hw_config.json", hw_attrs - ) - - # perform FIFO splitting and shallow FIFO removal only after the final config - # json file has been written. otherwise, since these transforms may add/remove - # FIFOs, we get name mismatch problems when trying to reuse the final config. - if cfg.split_large_fifos: - model = model.transform(SplitLargeFIFOs()) - model = model.transform(RemoveShallowFIFOs()) - - # generate a dedicated report about final FIFO sizes - fifo_info = {} - fifo_info["fifo_depths"] = {} - fifo_info["fifo_sizes"] = {} - total_fifo_size = 0 - for node in model.get_nodes_by_op_type("StreamingFIFO_rtl"): - node_inst = getCustomOp(node) - fifo_info["fifo_depths"][node.name] = node_inst.get_nodeattr("depth") - fifo_info["fifo_sizes"][ - node.name - ] = node_inst.get_instream_width() * node_inst.get_nodeattr("depth") - total_fifo_size += fifo_info["fifo_sizes"][node.name] - fifo_info["total_fifo_size_kB"] = int(total_fifo_size / 8.0 / 1000.0) - - with open(cfg.output_dir + "/report/fifo_sizing.json", "w") as f: - json.dump(fifo_info, f, indent=2) - - # With this step moved before step_hw_codegen and step_hw_ipgen, the following - # could be removed, but we keep it for now for backwards compatibility: - - # after FIFOs are ready to go, call PrepareIP and HLSSynthIP again - # this will only run for the new nodes (e.g. FIFOs and DWCs) - model = model.transform(PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period())) - model = model.transform(HLSSynthIP(cfg._resolve_fpga_part())) - return model - - def verify_mlo(model: ModelWrapper, cfg: DataflowBuildConfig, step: str): finn_loop = model.get_nodes_by_op_type("FINNLoop") # TODO: allow for multiple FINNLoops @@ -1361,9 +1329,9 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi Depends on the DataflowOutputType.STITCHED_IP output product. """ if DataflowOutputType.RTLSIM_PERFORMANCE in cfg.generate_outputs and not is_mlo(model): - assert ( - DataflowOutputType.STITCHED_IP in cfg.generate_outputs - ), "rtlsim_perf needs stitched IP" + assert DataflowOutputType.STITCHED_IP in cfg.generate_outputs, ( + "rtlsim_perf needs stitched IP" + ) report_dir = cfg.output_dir + "/report" os.makedirs(report_dir, exist_ok=True) rtlsim_bs = int(cfg.rtlsim_batch_size) @@ -1440,7 +1408,7 @@ def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): driver_type = "FINNDMAInstrumentationOverlay" if cfg.instrumentation_no_dma: driver_type = "FINNInstrumentationOverlay" - if cfg.live_fifo_sizing: + if cfg.auto_fifo_strategy == AutoFIFOSizingMethod.LIVE_FIFO and cfg.auto_fifo_depths: driver_type = "FINNLiveFIFOOverlay" else: driver_type = "FINNDMAOverlay" diff --git a/src/finn/transformation/fpgadataflow/insert_fifo.py b/src/finn/transformation/fpgadataflow/insert_fifo.py index 67ce858f2c..737e3d1309 100644 --- a/src/finn/transformation/fpgadataflow/insert_fifo.py +++ b/src/finn/transformation/fpgadataflow/insert_fifo.py @@ -27,35 +27,25 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Insert FIFO nodes into fpgadataflow graphs. + +This transformation derives FIFO depths from adjacent node attributes and +inserts StreamingFIFO nodes where appropriate. +""" + import numpy as np +import numpy.typing as npt from onnx import helper as oh -from qonnx.custom_op.registry import getCustomOp +from onnx import NodeProto +from finn.util.basic import getHWCustomOp +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation from finn.util.fpgadataflow import is_fpgadataflow_node from finn.util.logging import log - - -def _is_fifo_node(node): - if node.op_type.startswith("StreamingFIFO"): - return True - return False - - -def _suitable_node(node): - if node is not None: - if is_fpgadataflow_node(node): - if not _is_fifo_node(node): - return True - return False - return False - return False - - -def _suitable_folded_shapes(ishape, oshape): - matching_stream_width = ishape[-1] == oshape[-1] - matching_size = np.prod(ishape) == np.prod(oshape) - return matching_stream_width and matching_size +from finn.util.exception import FINNInternalError +from typing import cast +from collections.abc import Sequence class InsertFIFO(Transformation): @@ -81,19 +71,48 @@ class InsertFIFO(Transformation): The other node attributes necessary to create a FIFO node are taken from the node the FIFO node is inserted after: 'folded_shape' and 'dtype'""" - def __init__(self, create_shallow_fifos=False, max_qsrl_depth=None, vivado_ram_style="auto"): + def __init__( + self, + create_shallow_fifos: bool = False, + max_qsrl_depth: int | None = None, + vivado_ram_style: str = "auto", + ) -> None: + """Initialize InsertFIFO transformation.""" super().__init__() self.create_shallow_fifos = create_shallow_fifos self.max_qsrl_depth = max_qsrl_depth self.vivado_ram_style = vivado_ram_style - def apply(self, model): + def _is_fifo_node(self, node: NodeProto) -> bool: + return bool(node.op_type.startswith("StreamingFIFO")) + + def _suitable_node(self, node: NodeProto) -> bool: + if node is not None: + if is_fpgadataflow_node(node): + return bool(not self._is_fifo_node(node)) + return False + return False + + def _suitable_folded_shapes( + self, + ishape: Sequence[int] | npt.NDArray[np.int_], + oshape: Sequence[int] | npt.NDArray[np.int_], + ) -> bool: + matching_stream_width = ishape[-1] == oshape[-1] + matching_size = np.prod(ishape) == np.prod(oshape) + return matching_stream_width and matching_size + + def _shape_to_onnx(self, shape: Sequence[int] | npt.NDArray[np.int_]) -> list[int]: + return [int(dim) for dim in shape] + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Apply the transformation to insert FIFOs in the model.""" graph = model.graph node_ind = -1 graph_modified = False for first_node in graph.node: node_ind += 1 - if _suitable_node(first_node): + if self._suitable_node(first_node): for idx_out, output_name in enumerate(first_node.output): consumers = model.find_consumers(output_name) if consumers == []: @@ -104,35 +123,50 @@ def apply(self, model): than 1 cannot be stitched" ) consumer = consumers[0] - if _suitable_node(consumer) is True: - n0 = getCustomOp(first_node) + if self._suitable_node(consumer) is True: + n0 = getHWCustomOp(first_node) # determine fifo node attributes fld_shape = n0.get_folded_output_shape() dtype = n0.get_output_datatype() n0_otensor = model.get_tensor_valueinfo(output_name) + if n0_otensor is None: + raise FINNInternalError( + f"Output tensor {output_name} not found in " + f"model for node {first_node.name} when inserting FIFO." + ) n0_tensor_dtype = n0_otensor.type.tensor_type.elem_type # check if folded_shape of output of first node and # input of the second node is equal - n1 = getCustomOp(consumer) + n1 = getHWCustomOp(consumer) + fld_shape_2 = None + idx_inp = None for idx, inp in enumerate(consumer.input): if inp == output_name: fld_shape_2 = n1.get_folded_input_shape(ind=idx) idx_inp = idx - assert _suitable_folded_shapes( - fld_shape, fld_shape_2 - ), """The - folded output shape of the first node is not the same as the - folded output shape of the second node. A streaming fifo can't - be implemented in between these nodes.""" + if ( + fld_shape_2 is None + or not self._suitable_folded_shapes(fld_shape, fld_shape_2) + or idx_inp is None + ): + raise FINNInternalError( + f"Folded output shape of node {first_node.name} is not the same as " + f"the folded input shape of node {consumer.name}. " + f"A streaming FIFO can't be implemented in between these nodes." + ) n_shape = n0.get_normal_output_shape() # check if outFIFOdepths attribute of first node # and inFIFOdepths attribute of consumer node is equal - idx_out = min(idx_out, len(n0.get_nodeattr("outFIFODepths")) - 1) - idx_inp = min(idx_inp, len(n1.get_nodeattr("inFIFODepths")) - 1) - n0_depth = n0.get_nodeattr("outFIFODepths")[idx_out] - n1_depth = n1.get_nodeattr("inFIFODepths")[idx_inp] + idx_out = min( + idx_out, len(cast("list", n0.get_nodeattr("outFIFODepths"))) - 1 + ) + idx_inp = min( + idx_inp, len(cast("list", n1.get_nodeattr("inFIFODepths"))) - 1 + ) + n0_depth = cast("list[int]", n0.get_nodeattr("outFIFODepths"))[idx_out] + n1_depth = cast("list[int]", n1.get_nodeattr("inFIFODepths"))[idx_inp] fifo_depth = max(n0_depth, n1_depth) @@ -145,7 +179,7 @@ def apply(self, model): fifo_output_tensor = oh.make_tensor_value_info( model.make_new_valueinfo_name(), n0_tensor_dtype, - n0.get_normal_output_shape(), + self._shape_to_onnx(n0.get_normal_output_shape()), ) graph.value_info.append(fifo_output_tensor) model.set_tensor_datatype(fifo_output_tensor.name, dtype) @@ -183,6 +217,10 @@ def apply(self, model): graph_in_names = [x.name for x in model.graph.input] for graph_in_name in graph_in_names: first_node = model.find_consumer(graph_in_name) + if first_node is None: + raise FINNInternalError( + f"Input tensor {graph_in_name} not found in model when inserting FIFO." + ) # insert FIFO as first node, except when first node is DMA if ( not first_node.op_type.startswith("StreamingFIFO") @@ -190,7 +228,7 @@ def apply(self, model): ): inp_ind = list(first_node.input).index(graph_in_name) n_input = first_node.input[inp_ind] - n0 = getCustomOp(first_node) + n0 = getHWCustomOp(first_node) if n0.get_nodeattr("mlo_max_iter") and inp_ind > 0: continue # determine fifo node attributes @@ -198,8 +236,13 @@ def apply(self, model): n_shape = n0.get_normal_input_shape(inp_ind) dtype = n0.get_input_datatype(inp_ind) n0_itensor = model.get_tensor_valueinfo(graph_in_name) + if n0_itensor is None: + raise FINNInternalError( + f"Input tensor {graph_in_name} not found in model for " + f"node {first_node.name} when inserting FIFO." + ) n0_tensor_dtype = n0_itensor.type.tensor_type.elem_type - fifo_depth = n0.get_nodeattr("inFIFODepths")[inp_ind] + fifo_depth = cast("list[int]", n0.get_nodeattr("inFIFODepths"))[inp_ind] if fifo_depth > 2 or self.create_shallow_fifos: # Ensure that create shallow fifo condition doesn't create depth=1 fifos @@ -208,7 +251,7 @@ def apply(self, model): fifo_output_tensor = oh.make_tensor_value_info( model.make_new_valueinfo_name(), n0_tensor_dtype, - n0.get_normal_input_shape(inp_ind), + self._shape_to_onnx(n0.get_normal_input_shape(inp_ind)), ) graph.value_info.append(fifo_output_tensor) model.set_tensor_datatype(fifo_output_tensor.name, dtype) @@ -245,6 +288,10 @@ def apply(self, model): graph_out_names = [x.name for x in model.graph.output] for graph_out_name in graph_out_names: final_node = model.find_producer(graph_out_name) + if final_node is None: + raise FINNInternalError( + f"Output tensor {graph_out_name} not found in model when inserting FIFO." + ) if ( not final_node.op_type.startswith("StreamingFIFO") and final_node.op_type != "IODMA_hls" @@ -253,15 +300,20 @@ def apply(self, model): final_node.op_type != "TLastMarker_hls" ), """Insert tlast marker should be done after inserting the FIFOs""" - n0 = getCustomOp(final_node) + n0 = getHWCustomOp(final_node) out_ind = list(final_node.output).index(graph_out_name) # determine fifo node attributes fld_shape = n0.get_folded_output_shape(out_ind) n_shape = n0.get_normal_output_shape(out_ind) dtype = n0.get_output_datatype(out_ind) n0_otensor = model.get_tensor_valueinfo(graph_out_name) + if n0_otensor is None: + raise FINNInternalError( + f"Output tensor {graph_out_name} not found in model for " + f"node {final_node.name} when inserting FIFO." + ) n0_tensor_dtype = n0_otensor.type.tensor_type.elem_type - fifo_depth = n0.get_nodeattr("outFIFODepths")[out_ind] + fifo_depth = cast("list[int]", n0.get_nodeattr("outFIFODepths"))[out_ind] if fifo_depth > 2 or self.create_shallow_fifos: # Ensure that create shallow fifo condition doesn't create depth=1 fifos @@ -270,7 +322,7 @@ def apply(self, model): fifo_input_tensor = oh.make_tensor_value_info( model.make_new_valueinfo_name(), n0_tensor_dtype, - n0.get_normal_output_shape(out_ind), + self._shape_to_onnx(n0.get_normal_output_shape(out_ind)), ) graph.value_info.append(fifo_input_tensor) model.set_tensor_datatype(fifo_input_tensor.name, dtype) diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index 19752175bb..3bd04d3825 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -27,659 +27,74 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -"""Transformations for inserting and sizing FIFOs in FINN dataflow graphs.""" +"""Transformations for inserting and setting the size of FIFOs in FINN dataflow graphs.""" -import numpy as np +from finn.util.exception import FINNUserError from onnx import TensorProto, helper from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper -from qonnx.custom_op.registry import getCustomOp +from finn.util.basic import getHWCustomOp from qonnx.transformation.base import Transformation -from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames, SortGraph -from qonnx.util.basic import gen_finn_dt_tensor -from typing import TYPE_CHECKING, Literal, Protocol, cast +from qonnx.transformation.general import GiveReadableTensorNames, SortGraph +from typing import Literal, cast +from pathlib import Path +import json -from finn.analysis.fpgadataflow.dataflow_performance import dataflow_performance -from finn.core.rtlsim_exec import rtlsim_exec_cppxsi -from finn.transformation.fpgadataflow.annotate_cycles import AnnotateCycles -from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP -from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP -from finn.transformation.fpgadataflow.insert_dwc import InsertDWC -from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO -from finn.transformation.fpgadataflow.prepare_ip import PrepareIP -from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -from finn.util.exception import FINNInternalError -from finn.util.fpgadataflow import is_hls_node, is_rtl_node from finn.util.logging import log -if TYPE_CHECKING: - from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +class ApplyFIFODepthsFromFile(Transformation): + """Apply FIFO depths from a JSON file generated by a previous run of build_dataflow + with auto_fifo_depths enabled.""" -# Only used for type checking -class _SimProtocol(Protocol): - """Protocol for simulation objects used in signal access helpers.""" - - inputs: list[tuple[str, ...]] - outputs: list[tuple[str, ...]] - io: dict[str, int] - - -def reset_implementation(node: "HWCustomOp") -> None: - """Reset IP generation attributes of a node to trigger re-synthesis.""" - node.set_nodeattr("code_gen_dir_ipgen", "") - node.set_nodeattr("ipgen_path", "") - node.set_nodeattr("ip_path", "") - - -def set_signal(sim: _SimProtocol, keyw: str, value: int) -> None: - """Set the first simulation input signal whose name contains keyw to value.""" - for i in range(len(sim.inputs)): - input_name = sim.inputs[i][0] - if keyw in input_name: - sim.io[input_name] = value - - -def get_signal(sim: _SimProtocol, keyw: str) -> int | None: - """Return the value of the first simulation output signal whose name contains keyw.""" - for i in range(len(sim.outputs)): - output_name = sim.outputs[i][0] - if keyw in output_name: - return sim.io[output_name] - return None - - -def optimize_depth(depth: int) -> int: - """Round depth to avoid resource-inefficient FIFO sizes.""" - if depth <= 2: - return 2 - if depth <= 32: - # Q_srl FIFOs do not benefit from size < 32 - # add some slack - return 32 - # otherwise leave as is - # will be rounded to nearest power of two for Vivado-style FIFO - return int(depth) - - -class RemoveShallowFIFOs(Transformation): - """Remove zero-depth FIFOs The threshold used to be 2 instead of 0, but - with increasing number of FINN RTL components 2-depth FIFOs are still - important for decoupling.. - """ - - # TODO add unit test - - def __init__(self, shallow_threshold: int = 0) -> None: - """Initialize RemoveShallowFIFOs with the given depth threshold.""" - self.shallow_threshold = shallow_threshold - - def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: - """Remove FIFOs at or below the shallow threshold depth.""" - shallow_fifos = [] - for node in model.graph.node: - if len(node.input) > 0: - is_first_node = model.find_producer(node.input[0]) is None - else: - is_first_node = True - if ( - node.op_type.startswith("StreamingFIFO") - and cast("int", getCustomOp(node).get_nodeattr("depth")) <= self.shallow_threshold - and (not is_first_node or getCustomOp(node).get_nodeattr("mlo_max_iter")) - ): - # bypass shallow fifos - shallow_fifos.append(node) - consumers = model.find_consumers(node.output[0]) - if consumers == []: - producer = model.find_producer(node.input[0]) - if producer is None: - raise FINNInternalError("Producer not found for FIFO input") - for idx, inp in enumerate(producer.output): - if inp == node.input[0]: - producer.output[idx] = node.output[0] - else: - assert len(consumers) == 1, "Fanout detected from FIFO output" - consumer = consumers[0] - # set fifo input tensor as new input tensor of second node - for idx, inp in enumerate(consumer.input): - if inp == node.output[0]: - consumer.input[idx] = node.input[0] - # now filter out - for node_to_remove in shallow_fifos: - model.graph.node.remove(node_to_remove) - - return (model, False) - - -class CapConvolutionFIFODepths(Transformation): - """Make the size of FIFOs for convolution layers smaller where possible. - Will be automatically called from InsertAndSetFIFODepths if the appropriate - constructor flag is set. - - Constructor arguments: - - :parameter max_qsrl_depth: FIFOs deeper than this will use Vivado IP - instead of Verilog FIFOs (Q_srl.v) - - Assumed input graph properties: - - - all nodes are fpgadataflow nodes - - FIFOs inserted with InsertAndSetFIFODepths - - Output: - - - graph with smaller-depth FIFOs for convolutions - - Background: - The simulation-based rtlsim_exec tends to overestimate the required depth - of FIFOs between the ConvolutionInputGenerator (here called SWG) and the - MatrixVectorActivation (here called MVAU). As the SWG has an internal buffer of 1 - image row, we use this as a rule of thumb to set FIFO depth to be no larger - than 1 row. - """ - - # TODO add unit test - - def __init__(self, max_qsrl_depth: int = 256) -> None: - """Initialize CapConvolutionFIFODepths with the given maximum SRL FIFO depth.""" - super().__init__() - self.max_qsrl_depth = max_qsrl_depth - - def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: - """Cap FIFO depths between ConvolutionInputGenerator and MVAU nodes.""" - # TODO move this to own transformation - for node in model.graph.node: - # look for following pattern: - # ConvolutionInputGenerator -> StreamingFIFO -> MatrixVectorActivation - if node.op_type.startswith("StreamingFIFO"): - fifo_prod = model.find_producer(node.input[0]) - fifo_cons = model.find_consumer(node.output[0]) - if fifo_prod is None: - continue - if not fifo_prod.op_type.startswith("ConvolutionInputGenerator"): - continue - if fifo_cons is None: - continue - if not fifo_cons.op_type.startswith("MVAU"): - continue - op_inst = cast("HWCustomOp", getCustomOp(node)) - depth = cast("int", op_inst.get_nodeattr("depth")) - # SWG has an internal buffer of 1 row, so we use this as a - # rule of thumb to set FIFO depth to be no larger than 1 row - (_bs, _h, w, ifold, _simd) = op_inst.get_folded_input_shape() - new_depth = optimize_depth(w * ifold) - new_depth = min(new_depth, depth) - op_inst.set_nodeattr("depth", new_depth) - # Set FIFO implementation/ram styles - if new_depth > self.max_qsrl_depth: - op_inst.set_nodeattr("impl_style", "vivado") - op_inst.set_nodeattr("ram_style", "auto") - else: - op_inst.set_nodeattr("impl_style", "rtl") - - return (model, False) - - -def xsi_fifosim( - model, - n_inferences, - is_single_node, - total_nodes: int = 1, - current_node_index: int | None = None, - previous_node_name: str | None = None, - max_iters=None, - throttle_cycles=0, -): - """Create a XSI model of stitched IP and use a simple C++ - driver to drive the input stream. Useful for FIFO sizing, latency - and throughput measurement. If max_iters is None, use the default - liveness threshold instead. throttle_cycles can be used for throttling - the input stream every time a frame is finished. - """ - iname = model.get_first_global_in() - first_node = model.find_consumer(iname) - oname = model.get_first_global_out() - last_node = model.find_producer(oname) - assert (first_node is not None) and (last_node is not None), "Failed to find first/last nodes" - # define execution context for dummy data mode: - # only number of transactions, no real data - ctx = {k.name: n_inferences for k in model.graph.input} - # run XSI sim - ret_dict = rtlsim_exec_cppxsi( - model, - ctx, - is_single_node, - total_nodes=total_nodes, - current_node_index=current_node_index, - previous_node_name=previous_node_name, - dummy_data_mode=True, - timeout_cycles=max_iters, - throttle_cycles=throttle_cycles, - ) - - return ret_dict - - -class InsertAndSetFIFODepths(Transformation): - """Insert appropriate-depth StreamingFIFOs through RTLSim that preserve - throughput in the created accelerator. - - Constructor arguments: - - :parameter clk_ns: clock period (used for IP preparation) - :parameter max_qsrl_depth: FIFOs deeper than this will use Vivado IP - instead of Verilog FIFOs (Q_srl.v) - :parameter max_depth: how deep the "max"-sized FIFOs initially inserted - will be. If set to None, use the tensor size as the depth - :parameter swg_exception: call CapConvolutionFIFODepths to make convolution FIFOs - smaller where appropriate - :parameter vivado_ram_style: the StreamingFIFO.ram_style attribute to be used - for large FIFOs implemented by Vivado afterwards - :parameter fifosim_input_throttle: use input throttling based on dataflow analysis - while doing simulation-based FIFO sizing - - Assumed input graph properties: - - - all nodes are fpgadataflow nodes - - no FIFOs inserted, - - (inFIFODepths/outFIFODepths attrs will be ignored) - - Output: - - - graph with appropriate-depth FIFOs inserted - - Background: - Even with all FINN HLS fpgadatflow layers appropriately parallelized, it is - necessary to insert FIFOs between them to prevent stalls due to bursty - behavior. The sizes of those FIFOs are hard to predict analytically, so - we do the following: - - - insert deep (=tensor size) FIFOs between all fpgadataflow nodes - - create stitched design - - run through rtlsim with stream of multiple random input images (to fill pipeline) - - keep track of observed maximum occupancy for each FIFO during rtlsim - - when sim finished, update each FIFO depth to maximum observed occupancy - and set inFIFODepths/outFIFODepths attrs to that depth as well - - """ - - def __init__( - self, - fpgapart: str, - clk_ns: float = 10.0, - max_qsrl_depth: int = 256, - max_depth: int | None = None, - swg_exception: bool = False, - vivado_ram_style: str = "auto", - fifosim_input_throttle: bool = True, - cfg_n_inferences: int = 2, - ) -> None: - """Initialize InsertAndSetFIFODepths with synthesis and simulation parameters.""" + def __init__(self, fifo_config_file: Path) -> None: + """Initialize ApplyFIFODepthsFromFile with the path to the FIFO configuration file.""" super().__init__() - self.fpgapart = fpgapart - self.clk_ns = clk_ns - self.max_qsrl_depth = max_qsrl_depth - self.max_depth = max_depth - self.swg_exception = swg_exception - self.vivado_ram_style = vivado_ram_style - self.fifosim_input_throttle = fifosim_input_throttle - self.cfg_n_inferences = cfg_n_inferences - self.mlo_max_iter = 0 - self.ind_map = {} - - def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: - """Insert and size StreamingFIFOs using RTL simulation.""" - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) - for x in model.graph.node: - if x.op_type == "FINNLoop": - reset_implementation(getCustomOp(x)) - return (model, False) - - # these optypes may potentially use external weights - # but don't have the param input exposed as a graph input - # we'll temporarily change them to use decoupled mode for FIFO sizing - extw_optypes = ["MVAU_hls", "MVAU_rtl", "VVAU_hls", "VVAU_rtl"] - modified_extw_nodes = [] + self.fifo_config_file = fifo_config_file + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Apply FIFO depths from the configuration file to the model.""" + with self.fifo_config_file.open() as f: + fifo_info = json.load(f) + + #JSON file format: + # { + # "fifo_depths": { + # "StreamingFIFO_rtl_0": 2, + # }, + # "fifo_sizes": { + # "StreamingFIFO_rtl_0": 64, + # }, + # "impl_style": { + # "StreamingFIFO_rtl_0": "rtl", + # }, + # "ram_style": { + # "StreamingFIFO_rtl_0": "block", + # }, + # "total_fifo_size_kiB": 31 + # } + fifo_nodes = model.get_nodes_by_op_type("StreamingFIFO_rtl") - # these optypes may potentially be param nodes in an mlo - # we'll temporarily change them to use external mode for FIFO sizing - mlo_optypes = [ - "MVAU_hls", - "MVAU_rtl", - "Thresholding_rtl", - "ElementwiseAdd_hls", - "ElementwiseMul_hls", - "ElementwiseAdd_rtl", - "ElementwiseMul_rtl", - "ElementwiseSub_rtl", - ] - modified_mlo_nodes = [] - for node in model.graph.node: - # verify assumptions - assert is_hls_node(node) or is_rtl_node(node), "Found non-fpgadataflow node: " + str( - node + if len(fifo_nodes) != len(fifo_info["fifo_depths"]): + raise FINNUserError( + f"Number of FIFO nodes in the model ({len(fifo_nodes)}) does not match the number " + f"of FIFO nodes in the configuration file ({len(fifo_info['fifo_depths'])})" ) - op_type = node.op_type - assert not op_type.startswith("StreamingFIFO"), "Found existing StreamingFIFO node" - node = cast("HWCustomOp", getCustomOp(node)) - ifd = cast("list", node.get_nodeattr("inFIFODepths")) - ofd = cast("list", node.get_nodeattr("outFIFODepths")) - if self.max_depth is not None: - ifd = [self.max_depth] * len(ifd) - ofd = [self.max_depth] * len(ofd) - else: - # set each FIFO to its tensor size - # (except stream width hence the :-1) - tensor_size = 1 - for i in range(len(ifd)): - # safe guard that for very small tensors depth is not set to 1 - tensor_size = np.prod(node.get_folded_input_shape(i)[:-1]) - ifd[i] = tensor_size if tensor_size > 1 else 2 - for o in range(len(ofd)): - # safe guard that for very small tensors depth is not set to 1 - depth = np.prod(node.get_folded_output_shape(o)[:-1]) - ofd[o] = tensor_size if tensor_size > 1 else 2 - # set node attribute and ensure that it gets saved as list of integers - node.set_nodeattr("inFIFODepths", [int(fifo) for fifo in ifd]) - node.set_nodeattr("outFIFODepths", [int(fifo) for fifo in ofd]) - # do necessary temporary settinggs for external weights nodes - if node.onnx_node.op_type in extw_optypes: - input_names_set = {inp.name for inp in model.graph.input} - mmode = node.get_nodeattr("mem_mode") - if mmode == "external" and node.onnx_node.input[1] not in input_names_set: - modified_extw_nodes.append(node.onnx_node.name) - node.set_nodeattr("mem_mode", "internal_decoupled") - reset_implementation(node) - log.warning( - "Changed mem_mode from external to internal_decoupled for " - + node.onnx_node.name - ) - # do necessary temporary settings for mlo nodes - if node.onnx_node.op_type in mlo_optypes: - mlo_max_iter = node.get_nodeattr("mlo_max_iter") - if mlo_max_iter: - modified_mlo_nodes.append(node.onnx_node.name) - node.set_nodeattr("mlo_max_iter", 0) - if node.onnx_node.op_type.startswith("MVAU"): - node.set_nodeattr("mem_mode", "external") - elif ( - node.onnx_node.op_type == "Thresholding_rtl" - or node.onnx_node.op_type.startswith("Elementwise") - ): - # set thresholding array to a dummy value - param_input = node.onnx_node.input[1] - # remember index of input - inputs = [x.name for x in model.graph.input] - ind = inputs.index(param_input) - tdt = model.get_tensor_datatype(param_input) - tshape = model.get_tensor_shape(param_input) - dummy_threshs = gen_finn_dt_tensor(tdt, tuple(tshape)) - if node.onnx_node.op_type == "Thresholding_rtl": - dummy_threshs = np.sort(dummy_threshs, axis=1) - model.set_initializer(param_input, dummy_threshs) - self.ind_map[node.onnx_node.name] = ind - # For elementwise ops, temporarily set rhs_style to const - # since we converted the parameter to an initializer - if node.onnx_node.op_type.startswith("Elementwise"): - node.set_nodeattr("rhs_style", "const") - self.mlo_max_iter = mlo_max_iter - reset_implementation(node) - # insert stream infrastructure (DWC/FIFO) - model = model.transform(InsertDWC()) - model = model.transform(InsertFIFO(create_shallow_fifos=True)) - model = model.transform(SpecializeLayers(self.fpgapart)) - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) - - # gather FIFO names, check they are of expected depth - fifos = {} - fifo_nodes = model.get_nodes_by_op_type("StreamingFIFO_rtl") + graph_modified = False for node in fifo_nodes: - fifos[node.name] = 0 - node = getCustomOp(node) - node.set_nodeattr("depth_monitor", 1) - node.set_nodeattr("impl_style", "rtl") - # check depths and fix as necessary - if (self.max_depth is not None) and (node.get_nodeattr("depth") != self.max_depth): - node.set_nodeattr("depth", self.max_depth) - - # insert FIFOs and do all transformations for RTLsim - model = model.transform(AnnotateCycles()) - perf = model.analysis(dataflow_performance) - latency = cast("int", perf["critical_path_cycles"]) - max_cycles = cast("int", perf["max_cycles"]) - model = model.transform(PrepareIP(self.fpgapart, self.clk_ns)) - model = model.transform(HLSSynthIP()) - model = model.transform(CreateStitchedIP(self.fpgapart, self.clk_ns)) - model.set_metadata_prop("exec_mode", "rtlsim") - - # do rtlsim in C++ for FIFO sizing - # use the critical_path_cycles estimate to set the timeout limit for FIFO sim - max_iters = latency * 1.1 + 50 - - # set up rate limit for input throttling - if self.fifosim_input_throttle: - first_node = cast("HWCustomOp", getCustomOp(model.graph.node[0])) - inp_fold = np.prod(first_node.get_folded_input_shape()[:-1]) - throttle_cycles = max(0, max_cycles - inp_fold) - else: - throttle_cycles = 0 - - sim = xsi_fifosim( - model, - self.cfg_n_inferences, - False, - max_iters=max_iters, - throttle_cycles=int(throttle_cycles), - ) - - for ind, node in enumerate(fifo_nodes): - maxcount_name = f"maxcount_{ind}" - if ind == 0: - maxcount_name = "maxcount" - fifos[node.name] = sim[maxcount_name] - - # Apply depths back into the model; - # also set in/outFIFODepths to zero for non-FIFO - # nodes, preventing further FIFO insertion - for node in model.graph.node: - # set FIFO depth, reset FIFO implementation, - # and set implementation/ram styles - if node.op_type.startswith("StreamingFIFO"): - assert node.name in fifos, "FIFO node not found in size dictionary" - # set depth of FIFO - depth = optimize_depth(fifos[node.name]) - node_inst = cast("HWCustomOp", getCustomOp(node)) - node_inst.set_nodeattr("depth", depth) - node_inst.set_nodeattr("depth_monitor", 0) - # exception for top-level IO FIFOs which cause a bug in simulation - # (top-level IOs should not have impl_style=vivado) - toplevel_in = node.input[0] in [x.name for x in model.graph.input] - toplevel_out = node.output[0] in [x.name for x in model.graph.output] - toplevel_style_exception = toplevel_in or toplevel_out - # Set FIFO implementation/ram styles - if (depth > self.max_qsrl_depth) and (not toplevel_style_exception): - node_inst.set_nodeattr("impl_style", "vivado") - node_inst.set_nodeattr("ram_style", self.vivado_ram_style) - else: - node_inst.set_nodeattr("impl_style", "rtl") - # reset implementation - reset_implementation(node_inst) - del fifos[node.name] - else: - # (removed setting of node FIFO size attributes to 0 here) - # for every extw node we changed from external to decoupled, - # change back and reset implementation - if node.op_type in extw_optypes and node.name in modified_extw_nodes: - node_inst = cast("HWCustomOp", getCustomOp(node)) - node_inst.set_nodeattr("mem_mode", "external") - reset_implementation(node_inst) - modified_extw_nodes.remove(node.name) - # do the same resetting for mlo nodes - if node.op_type in mlo_optypes: - if node.name in modified_mlo_nodes and node.op_type.startswith("MVAU"): - node_inst = getCustomOp(node) - node_inst.set_nodeattr("mlo_max_iter", self.mlo_max_iter) - node_inst.set_nodeattr("mem_mode", "internal_decoupled") - reset_implementation(node_inst) - modified_mlo_nodes.remove(node.name) - - sorted_ind_map = dict(sorted(self.ind_map.items(), key=lambda item: item[1])) - for k, v in sorted_ind_map.items(): - node = model.get_node_from_name(k) - node_inst = getCustomOp(node) - node_inst.set_nodeattr("mlo_max_iter", self.mlo_max_iter) - # remove initializer again - param_input = node.input[1] - param_input_vi = model.get_tensor_valueinfo(param_input) - model.del_initializer(param_input) - model.graph.input.insert(self.ind_map[node.name], param_input_vi) - model.graph.value_info.remove(param_input_vi) - if node.op_type.startswith("Elementwise"): - # Restore rhs_style to "input" (it must have been "input" for MLO nodes) - node_inst.set_nodeattr("rhs_style", "input") - reset_implementation(node_inst) - modified_mlo_nodes.remove(node.name) - - assert ( - len(modified_extw_nodes) == 0 and len(fifos.keys()) == 0 - ), "FIFO/FC nodes left untouched after model reconfiguration" - assert ( - len(modified_mlo_nodes) == 0 and len(fifos.keys()) == 0 - ), "FIFO/FC nodes left untouched after model reconfiguration" - - # handle custom sizing for SWG FIFOs if desired - if self.swg_exception: - model = model.transform(CapConvolutionFIFODepths(max_qsrl_depth=self.max_qsrl_depth)) - - # remove FIFOs from mlo parameter inputs - for op_type in mlo_optypes: - nodes = model.get_nodes_by_op_type(op_type) - for node in nodes: - node_inst = getCustomOp(node) - if node_inst.get_nodeattr("mlo_max_iter"): - # Check if there is a FIFO inserted at param input - fifo_node = model.find_producer(node.input[1]) - if fifo_node and fifo_node.op_type.startswith("StreamingFIFO"): - fifo_inst = getCustomOp(fifo_node) - fifo_inst.set_nodeattr("depth", 0) - fifo_inst.set_nodeattr("mlo_max_iter", 1) - - # remove shallow FIFOs - model = model.transform(RemoveShallowFIFOs()) - - # clean up references to stitched IP and rtlsim objects - # (the stitched IP needs to be re-done after FIFO sizing) - model.set_metadata_prop("rtlsim_trace", "") - model.set_metadata_prop("rtlsim_so", "") - model.set_metadata_prop("vivado_stitch_proj", "") - model.set_metadata_prop("wrapper_filename", "") - model.set_metadata_prop("vivado_stitch_vlnv", "") - model.set_metadata_prop("vivado_stitch_ifnames", "") - model.set_metadata_prop("exec_mode", "") - - # reflect final values in attributes - for node in model.graph.node: - if not node.op_type.startswith("StreamingFIFO"): - node_inst = cast("HWCustomOp", getCustomOp(node)) - fifodepth_in = [] - for node_inp in node.input: - prod = model.find_producer(node_inp) - if prod is None: - # no producer for this input - if node_inp in [x.name for x in model.graph.input]: - # top-level input with no FIFO - fifodepth_in.append(0) - else: - # FIFO depth attr applies only to dynamic attributes - pass - else: - # there is a producer for this input - if prod.op_type.startswith("StreamingFIFO"): - prod_inst = cast("HWCustomOp", getCustomOp(prod)) - fifodepth_in.append(prod_inst.get_nodeattr("depth")) - else: - # explicitly no FIFO on this dynamic input - fifodepth_in.append(0) - fifodepth_out = [] - for node_out in node.output: - cons = model.find_consumer(node_out) - if cons is None: - # no consumer for this output - if node_out in [x.name for x in model.graph.output]: - # top-level output with no FIFO - fifodepth_out.append(0) - else: - # FIFO depth attr applies only to dynamic attributes - pass - else: - # there is a consumer for this input - if cons.op_type.startswith("StreamingFIFO"): - cons_inst = getCustomOp(cons) - fifodepth_out.append(cons_inst.get_nodeattr("depth")) - else: - # explicitly no FIFO on this dynamic output - fifodepth_out.append(0) - # set node attribute and ensure that it gets saved as list of integers - node_inst.set_nodeattr("inFIFODepths", [int(fifo) for fifo in fifodepth_in]) - node_inst.set_nodeattr("outFIFODepths", [int(fifo) for fifo in fifodepth_out]) - - return (model, False) - - -def get_fifo_split_configs( - depth: int, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768 -) -> list[tuple[int, str]]: - """Break non-power-of-2 sized FIFO depths into several ones.""" - - def floor_pow2(x: int) -> int: - """Return the largest power of 2 less than or equal to x.""" - if (x & (x - 1) == 0) and x != 0: - return x - return 1 << ((x - 1).bit_length() - 1) - - def decompose_pow2(x: int) -> list[int]: - """Decompose x into a sum of powers of 2, - with each power of 2 no larger than max_qsrl_depth. - """ - if x <= max_qsrl_depth: - return [x] - r = floor_pow2(x) - if x == r: - return [x] - return [r, *decompose_pow2(x - r)] - - ret = [] - # trivial case: for small FIFOs, return as-is with rtl style - if depth <= max_qsrl_depth: - return [(depth, "rtl")] - # first pass: ensure max depth is respected - # (restricted by Vivado AXIS infra IP) - remainder = depth - while remainder != 0: - if remainder > max_vivado_depth: - ret.append(max_vivado_depth) - remainder -= max_vivado_depth - else: - ret.append(remainder) - remainder = 0 - # second pass: break non-power-of-2 sized FIFOs - # into several ones - - ret_pass2 = list(map(decompose_pow2, ret)) - # unpack list of lists - ret_pass2 = [x for dec_list in ret_pass2 for x in dec_list] - - # finally, add impl_style to each split FIFO - ret_final = [] - for cand_depth in ret_pass2: - if cand_depth <= max_qsrl_depth: - ret_final.append((max(2, cand_depth), "rtl")) - else: - ret_final.append((cand_depth, "vivado")) - - return ret_final + if node.name not in fifo_info["fifo_depths"]: + raise FINNUserError( + f"FIFO node {node.name} not found in configuration file" + ) + n_inst = getHWCustomOp(node) + depth = fifo_info["fifo_depths"][node.name] + impl_style = fifo_info["impl_style"][node.name] + ram_style = fifo_info["ram_style"][node.name] + n_inst.set_nodeattr("depth", depth) + n_inst.set_nodeattr("impl_style", impl_style) + n_inst.set_nodeattr("ram_style", ram_style) + graph_modified = True + return (model, graph_modified) class SplitLargeFIFOs(Transformation): @@ -701,15 +116,70 @@ def __init__(self, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768) -> self.max_qsrl_depth = max_qsrl_depth self.max_vivado_depth = max_vivado_depth + def get_fifo_split_configs( + self, depth: int, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768 + ) -> list[tuple[int, str]]: + """Break non-power-of-2 sized FIFO depths into several ones.""" + + def floor_pow2(x: int) -> int: + """Return the largest power of 2 less than or equal to x.""" + if (x & (x - 1) == 0) and x != 0: + return x + return 1 << ((x - 1).bit_length() - 1) + + def decompose_pow2(x: int) -> list[int]: + """Decompose x into a sum of powers of 2, + with each power of 2 no larger than max_qsrl_depth. + """ + if x <= max_qsrl_depth: + return [x] + r = floor_pow2(x) + if x == r: + return [x] + return [r, *decompose_pow2(x - r)] + + ret = [] + # trivial case: for small FIFOs, return as-is with rtl style + if depth <= max_qsrl_depth: + return [(depth, "rtl")] + # first pass: ensure max depth is respected + # (restricted by Vivado AXIS infra IP) + remainder = depth + while remainder != 0: + if remainder > max_vivado_depth: + ret.append(max_vivado_depth) + remainder -= max_vivado_depth + else: + ret.append(remainder) + remainder = 0 + # second pass: break non-power-of-2 sized FIFOs + # into several ones + + ret_pass2 = list(map(decompose_pow2, ret)) + # unpack list of lists + ret_pass2 = [x for dec_list in ret_pass2 for x in dec_list] + + # finally, add impl_style to each split FIFO + ret_final = [] + for cand_depth in ret_pass2: + if cand_depth <= max_qsrl_depth: + ret_final.append((max(2, cand_depth), "rtl")) + else: + ret_final.append((cand_depth, "vivado")) + + return ret_final + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: """Split large FIFOs into chains of smaller power-of-two FIFOs.""" graph = model.graph graph_modified = False for node_ind, node in enumerate(graph.node, 1): if node.op_type == ("StreamingFIFO_rtl"): - n_inst = cast("HWCustomOp", getCustomOp(node)) + n_inst = getHWCustomOp(node) depth = cast("int", n_inst.get_nodeattr("depth")) - cfgs = get_fifo_split_configs(depth, self.max_qsrl_depth, self.max_vivado_depth) + cfgs = self.get_fifo_split_configs( + depth, self.max_qsrl_depth, self.max_vivado_depth + ) if len(cfgs) > 1: fld_shape = n_inst.get_folded_output_shape() n_shape = n_inst.get_normal_output_shape() diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index e0d57197b4..4398bda830 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -78,7 +78,10 @@ def store_fifo_data( merged = data else: merged = pd.merge( - data, pd.read_csv(fifo_data_path), on=merge_on, how=merge_how # type: ignore + data, + pd.read_csv(fifo_data_path), + on=merge_on, + how=merge_how, # type: ignore ) merged = merged.sort_values(sort_on) merged.to_csv(fifo_data_path, index=False) @@ -115,20 +118,20 @@ def __init__( fpgapart: str, clk_ns: float, functional_sim: bool, - workers: int | None = None, + workers: int | None = None, # noqa: ARG002 ) -> None: """Create a new simulation instance. Read simulation binary paths from the simulation_binaries metadata prop field.""" self.simulation_type = simulation_type self.model = model - sim_binaries = self.model.get_metadata_prop("simulation_binaries") + sim_binaries_str = self.model.get_metadata_prop("simulation_binaries") - if sim_binaries is None: + if sim_binaries_str is None: raise FINNUserError( "No field simulation_binaries found in the model. Make " "sure to run the BuildSimulation transformation beforehand." ) - sim_binaries: list[Path] = [Path(p) for p in str(sim_binaries).split("\n")] + sim_binaries: list[Path] = [Path(p) for p in str(sim_binaries_str).split("\n")] if len(sim_binaries) != len(self.model.graph.node): raise FINNUserError( "The number of found simulation binaries does not match the number " @@ -139,9 +142,6 @@ def __init__( raise FINNUserError( "Simulation binary data points to invalid paths. Please rerun BuildSimulation." ) - # TODO: Currently we have to recompile even if we just - # TODO: called BuildSimulation in the step before - # (However this only compiles, it should NOT stitch the IPs again) self.model = self.model.transform(BuildSimulation(fpgapart, clk_ns, functional_sim)) self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} match simulation_type: @@ -164,10 +164,12 @@ def __init__( raise FINNInternalError("Errors occurred: \n" + "\n\t".join(errors)) def simulate(self) -> Any: + """Run the simulation and return the results. + The type of the results may differ based on the simulation type.""" raise NotImplementedError("Call simulate() on subclasses.") -class ApplyFIFOSizes(Transformation): +class ApplySimulatedFIFOSizes(Transformation): """Apply a FIFO sizing configuration to the model. If FIFOs already exist the step is skipped.""" @@ -223,9 +225,15 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: n = getCustomOp(node) if n is not None: if predecessors is not None: - n.set_nodeattr("inFIFODepths", [0] * len(predecessors)) + n.set_nodeattr( + "inFIFODepths", + cast("list[str | int | float]", [0] * len(predecessors)), + ) if successors is not None: - n.set_nodeattr("outFIFODepths", [0] * len(successors)) + n.set_nodeattr( + "outFIFODepths", + cast("list[str | int | float]", [0] * len(successors)), + ) # Set new outFIFODepths according to config graph = model.graph @@ -246,17 +254,17 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: "or due to changes in the model after the simulation was run. " "Consider re-running the entire flow from start to finish." ) - fifos = cast("list[int]", (self.fifo_depths[node_ind]["depths"])) + fifos = cast("list[str | int | float]", (self.fifo_depths[node_ind]["depths"])) n0.set_nodeattr("outFIFODepths", fifos) # Insert the FIFOs into the model model = model.transform(InsertFIFO(True, self.max_qsrl_depth, self.vivado_ram_style)) model = model.transform(GiveUniqueNodeNames()) - model: ModelWrapper = model.transform(GiveReadableTensorNames()) + model = model.transform(GiveReadableTensorNames()) model = model.transform(SpecializeLayers(self.cfg._resolve_fpga_part())) # noqa model = model.transform(GiveUniqueNodeNames()) - model: ModelWrapper = model.transform(GiveReadableTensorNames()) + model = model.transform(GiveReadableTensorNames()) # Sanity check to make sure fifos were inserted inserted_fifo_count = sum( From f3ac35ba74e73618fd8c746cf3ede218030f13d4 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 8 May 2026 17:36:59 +0200 Subject: [PATCH 103/170] Fix some errors --- .../fpgadataflow/duplicatestreams.py | 2 - .../fpgadataflow/rtl/removedatapath_rtl.py | 10 ++--- .../fpgadataflow/convert_to_hw_layers.py | 8 +--- .../transformation/fpgadataflow/prepare_ip.py | 3 +- .../fpgadataflow/set_fifo_depths.py | 44 +++++++++---------- .../fpgadataflow/simulation_build.py | 2 - 6 files changed, 28 insertions(+), 41 deletions(-) diff --git a/src/finn/custom_op/fpgadataflow/duplicatestreams.py b/src/finn/custom_op/fpgadataflow/duplicatestreams.py index 264fffda6c..e0b6053014 100644 --- a/src/finn/custom_op/fpgadataflow/duplicatestreams.py +++ b/src/finn/custom_op/fpgadataflow/duplicatestreams.py @@ -30,14 +30,12 @@ from qonnx.core.datatype import DataType from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp -from finn.util.deprecated import deprecated from finn.util.logging import log class DuplicateStreams(HWCustomOp): """Abstraction layer for HW implementation of DuplicateStreams""" - @deprecated def __init__(self, onnx_node, **kwargs): super().__init__(onnx_node, **kwargs) diff --git a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py index 956c355f1c..5b12c11de4 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/removedatapath_rtl.py @@ -194,7 +194,7 @@ def get_normal_input_shape( f"normal_shape attribute is empty in {self.onnx_node.name}, " "cannot get normal input shape" ) - if type(normal_shape[0]) is not int: + if not isinstance(normal_shape[0], int) and not isinstance(normal_shape[0], np.integer): raise FINNInternalError( f"normal_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get normal input shape" @@ -242,7 +242,7 @@ def get_folded_input_shape( f"folded_shape attribute is empty in {self.onnx_node.name}, " "cannot get folded input shape" ) - if type(folded_shape[0]) is not int: + if not isinstance(folded_shape[0], int) and not isinstance(folded_shape[0], np.integer): raise FINNInternalError( f"folded_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get folded input shape" @@ -290,7 +290,7 @@ def get_instream_width(self, ind: int = 0) -> int: # noqa: ARG002 f"folded_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get outstream width" ) - if not isinstance(folded_shape[-1], int) or not isinstance(folded_shape[-1], np.integer): + if not isinstance(folded_shape[-1], int) and not isinstance(folded_shape[-1], np.integer): raise FINNInternalError( f"folded_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get outstream width" @@ -328,10 +328,10 @@ def get_outstream_width(self, ind: int = 0) -> int: # noqa: ARG002 f"folded_shape attribute not set correctly in {self.onnx_node.name}, " "cannot get outstream width" ) - if not isinstance(folded_shape[-1], int) or not isinstance(folded_shape[-1], np.integer): + if not isinstance(folded_shape[-1], int) and not isinstance(folded_shape[-1], np.integer): raise FINNInternalError( f"folded_shape attribute not set correctly in {self.onnx_node.name}, " - "cannot get outstream width" + f"cannot get outstream width" ) in_width = cast("int|np.integer", folded_shape[-1]) * dtype.bitwidth() return in_width diff --git a/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py b/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py index 7ba8da374d..91e4fe4e0a 100644 --- a/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py +++ b/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py @@ -780,9 +780,7 @@ def apply(self, model): log.warning( "InferAddStreamsLayer is deprecated. " "Use InferElementwiseBinaryOperation instead. " - "AddStreams is being replaced by ElementwiseAdd operations.", - DeprecationWarning, - stacklevel=2, + "AddStreams is being replaced by ElementwiseAdd operations." ) # Delegate to the new transformation return InferElementwiseBinaryOperation().apply(model) @@ -951,8 +949,6 @@ def apply(self, model): "InferChannelwiseLinearLayer is deprecated. " "Use InferElementwiseBinaryOperation instead. " "ChannelwiseOp is being replaced by ElementwiseBinary operations.", - DeprecationWarning, - stacklevel=2, ) # Delegate to the new transformation return InferElementwiseBinaryOperation().apply(model) @@ -1677,8 +1673,6 @@ def apply(self, model): "InferStreamingEltwise is deprecated. " "Use InferElementwiseBinaryOperation instead. " "StreamingEltwise is being replaced by ElementwiseSub/ElementwiseAbsDiff.", - DeprecationWarning, - stacklevel=2, ) # Delegate to the new transformation return InferElementwiseBinaryOperation().apply(model) diff --git a/src/finn/transformation/fpgadataflow/prepare_ip.py b/src/finn/transformation/fpgadataflow/prepare_ip.py index 43d95e4345..0c4275f229 100644 --- a/src/finn/transformation/fpgadataflow/prepare_ip.py +++ b/src/finn/transformation/fpgadataflow/prepare_ip.py @@ -1,3 +1,4 @@ +"""Module which implements the PrepareIP transformation, which generates the code for each node.""" # Copyright (C) 2020, Xilinx, Inc. # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -55,7 +56,6 @@ def _codegen_single_node( inst = cast("RTLBackend|HLSBackend", getHWCustomOp(node)) # get the path of the code generation directory code_gen_dir = cast("str", inst.get_nodeattr("code_gen_dir_ipgen")) - print(f"Code generation directory for node {node.name}: {code_gen_dir}") # ensure that there is a directory if code_gen_dir == "" or not Path(code_gen_dir).is_dir(): code_gen_dir = make_build_dir(prefix="code_gen_ipgen_" + str(node.name) + "_") @@ -64,7 +64,6 @@ def _codegen_single_node( inst.code_generation_ipgen(model, fpgapart, clk) else: log.debug(f"Using cached code for {node.name}") - print(f"Using cached code for node {node.name}...") except KeyError: # exception if op_type is not supported raise FINNUserError(f"Custom op_type {op_type} is currently not supported.") from None diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index 3bd04d3825..008295120d 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -96,28 +96,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: graph_modified = True return (model, graph_modified) - -class SplitLargeFIFOs(Transformation): - """Split large FIFOs before implementation, for two reasons. - - - impl_style="vivado" supports a max depth of 32k. Any larger - FIFOs must be implemented as a sequence of smaller FIFOs. - - impl_style="vivado" requires power-of-two depths, which is - normally handled by rounding up to the nearest power-of-two. - So a FIFO of size 8196 normally gets rounded-up to a depth of - 16384 and wastes a lot of resources. Here, instead, we split - this up into two FIFOs of depth 8192 + 4. - - """ - - def __init__(self, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768) -> None: - """Initialize SplitLargeFIFOs with maximum FIFO depth constraints.""" - super().__init__() - self.max_qsrl_depth = max_qsrl_depth - self.max_vivado_depth = max_vivado_depth - - def get_fifo_split_configs( - self, depth: int, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768 +def get_fifo_split_configs(depth: int, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768 ) -> list[tuple[int, str]]: """Break non-power-of-2 sized FIFO depths into several ones.""" @@ -169,6 +148,25 @@ def decompose_pow2(x: int) -> list[int]: return ret_final +class SplitLargeFIFOs(Transformation): + """Split large FIFOs before implementation, for two reasons. + + - impl_style="vivado" supports a max depth of 32k. Any larger + FIFOs must be implemented as a sequence of smaller FIFOs. + - impl_style="vivado" requires power-of-two depths, which is + normally handled by rounding up to the nearest power-of-two. + So a FIFO of size 8196 normally gets rounded-up to a depth of + 16384 and wastes a lot of resources. Here, instead, we split + this up into two FIFOs of depth 8192 + 4. + + """ + + def __init__(self, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768) -> None: + """Initialize SplitLargeFIFOs with maximum FIFO depth constraints.""" + super().__init__() + self.max_qsrl_depth = max_qsrl_depth + self.max_vivado_depth = max_vivado_depth + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: """Split large FIFOs into chains of smaller power-of-two FIFOs.""" graph = model.graph @@ -177,7 +175,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: if node.op_type == ("StreamingFIFO_rtl"): n_inst = getHWCustomOp(node) depth = cast("int", n_inst.get_nodeattr("depth")) - cfgs = self.get_fifo_split_configs( + cfgs = get_fifo_split_configs( depth, self.max_qsrl_depth, self.max_vivado_depth ) if len(cfgs) > 1: diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 36d33566e4..6187827433 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -427,8 +427,6 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: node_model.set_metadata_prop("input_node", str(input_node).lower()) node_model.set_metadata_prop("output_node", str(output_node).lower()) - node_model.save(f"isolated_node_{target_node.name}.onnx") - return node_model def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, str]: From 05114dac9fadbc14c185e694be549a498af35dce Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 15 May 2026 14:35:31 +0200 Subject: [PATCH 104/170] Fix performance simulation --- finn-rtllib/mock_hbm/hdl/mock_template.v | 2 +- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 6 +- finn_xsi/finn_xsi/include/Simulation.hpp | 10 +- finn_xsi/finn_xsi/rtlsim_config.hpp.template | 40 +- src/finn/builder/build_dataflow_config.py | 7 - src/finn/builder/build_dataflow_steps.py | 269 +++---- .../fpgadataflow/elementwise_binary.py | 59 +- src/finn/custom_op/fpgadataflow/hwcustomop.py | 8 - .../fpgadataflow/create_stitched_ip.py | 2 +- .../fpgadataflow/insert_fifo.py | 10 +- .../fpgadataflow/make_driver.py | 19 +- .../fpgadataflow/set_fifo_depths.py | 144 ++-- .../transformation/fpgadataflow/simulation.py | 35 +- .../fpgadataflow/simulation_build.py | 166 +++- .../fpgadataflow/simulation_connected.py | 53 +- src/finn/util/config.py | 41 +- tests/fpgadataflow/test_simulation_build.py | 748 +++++++++++++++++- tests/util/test_config.py | 31 +- 18 files changed, 1217 insertions(+), 433 deletions(-) diff --git a/finn-rtllib/mock_hbm/hdl/mock_template.v b/finn-rtllib/mock_hbm/hdl/mock_template.v index 4e07e3a18e..1057cab750 100644 --- a/finn-rtllib/mock_hbm/hdl/mock_template.v +++ b/finn-rtllib/mock_hbm/hdl/mock_template.v @@ -52,7 +52,7 @@ output reg s_axi_rlast, input s_axi_rready ); -parameter integer LATENCY = 1; +parameter integer LATENCY = 100; localparam integer SHIFT_W = ({{ DATA_WIDTH }} <= 1) ? 1 : $clog2({{ DATA_WIDTH }} / 8); localparam integer LAT_CNT_W = (LATENCY <= 1) ? 1 : $clog2(LATENCY + 1); diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 314aa0a41a..3f9d8570a8 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -35,7 +35,7 @@ enum class SimulationState { IDLE, CONFIGURED, RUNNING, FINISHED, ERROR }; class SimulationController { private: SingleNodeSimulation& sim; + RTLSimConfig::IsOutputNode, RTLSimConfig::preciseTimeout>& sim; std::atomic state{SimulationState::IDLE}; std::atomic current_cycles{0}; std::atomic current_samples{0}; @@ -48,7 +48,7 @@ class SimulationController { public: explicit SimulationController(SingleNodeSimulation& simulation) + RTLSimConfig::IsInputNode, RTLSimConfig::IsOutputNode, RTLSimConfig::preciseTimeout>& simulation) : sim(simulation) {} void configure(const std::vector& depths, const std::vector& expected_first_valid_cycles, std::size_t maxCycles) { @@ -340,7 +340,7 @@ int main(int argc, const char* argv[]) { // Construct simulation SingleNodeSimulation + RTLSimConfig::IsOutputNode, RTLSimConfig::preciseTimeout> sim(RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, "xsim_log_file.txt", "trace_file.txt", RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs, RTLSimConfig::inputInterfaceNames, RTLSimConfig::outputInterfaceNames, 2); diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 0014caa6b6..42d3fe575a 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -105,7 +105,7 @@ struct CommData { // │ ready ready │ // │ (sim) │ // └──────────────────────────────────────┘ -template +template class SingleNodeSimulation : public Simulation { using ConsumingInterface = InterprocessCommunicationChannel; using ProducingInterface = InterprocessCommunicationChannel; @@ -275,9 +275,11 @@ class SingleNodeSimulation : public Simulationostreams.begin(), this->ostreams.end(), [](const M_AXIS_Control& stream) { return stream.stableState.is_stable(); }) & !stoken.stop_requested() & (cyclesRun <= max_cycles) & !timeout) { timeout |= runSingleCycle(stoken); - timeout |= runSingleCycle(stoken); - timeout |= runSingleCycle(stoken); - timeout |= runSingleCycle(stoken); + if constexpr (!PreciseTimeout) { + timeout |= runSingleCycle(stoken); + timeout |= runSingleCycle(stoken); + timeout |= runSingleCycle(stoken); + } } return timeout || cyclesRun > max_cycles; } diff --git a/finn_xsi/finn_xsi/rtlsim_config.hpp.template b/finn_xsi/finn_xsi/rtlsim_config.hpp.template index 6b442ade25..58ecb526f8 100644 --- a/finn_xsi/finn_xsi/rtlsim_config.hpp.template +++ b/finn_xsi/finn_xsi/rtlsim_config.hpp.template @@ -1,16 +1,3 @@ -/**************************************************************************** - * Copyright (C) 2025, Advanced Micro Devices, Inc. - * All rights reserved. - * - * SPDX-License-Identifier: BSD-3-Clause - * - * @brief Configuration template for C++ rtlsim driver for Verilog designs. - * @author Yaman Umuroğlu - * @note - * All code template arguments formatted like @TEMPLATE@ must be filled in - * prior to compilation. - ***************************************************************************/ - #include #include #include @@ -21,40 +8,41 @@ namespace RTLSimConfig { // Log during simulation. Turned off by default. Might increase runtime if used. constexpr bool LoggingEnabled = true; - constexpr bool IsInputNode = @IS_INPUT_NODE@; - constexpr bool IsOutputNode = @IS_OUTPUT_NODE@; + constexpr bool IsInputNode = {{ IS_INPUT_NODE }}; + constexpr bool IsOutputNode = {{ IS_OUTPUT_NODE }}; /**** General RTLSIM Configuration Parameters ****/ - constexpr std::array inputInterfaceNames { @INPUT_INTERFACE_NAMES@ }; - constexpr std::array outputInterfaceNames { @OUTPUT_INTERFACE_NAMES@ }; + constexpr std::array inputInterfaceNames { {{ INPUT_INTERFACE_NAMES }} }; + constexpr std::array outputInterfaceNames { {{ OUTPUT_INTERFACE_NAMES }} }; // Which index node this simulation executes // In a complete design simulation this is 0 - constexpr size_t NodeIndex = @NODE_INDEX@; + constexpr size_t NodeIndex = {{ NODE_INDEX }}; // Number of total nodes in the simulation (over all processes) // In a complete design simulation this is 1 - constexpr size_t TotalNodes = @TOTAL_NODES@; + constexpr size_t TotalNodes = {{ TOTAL_NODES }}; // sim kernel .so to use (depends on Vivado version) - static char const kernel_libname[] = "@SIMKERNEL_SO@"; + static char const kernel_libname[] = "{{ SIMKERNEL_SO }}"; // design library .so to use (important to use this relative path here, // due to how XSI looks for certain files) - static char const design_libname[] = "xsim.dir/@TOP_MODULE_NAME@/xsimk.so"; + static char const design_libname[] = "xsim.dir/{{ TOP_MODULE_NAME }}/xsimk.so"; // AXI stream descriptors {stream_name, transactions_per_inference} // input AXI stream descriptors - constexpr std::array istream_descs { @ISTREAM_DESC@ }; + constexpr std::array istream_descs { {{ ISTREAM_DESC }} }; // output AXI stream descriptors - constexpr std::array ostream_descs { @OSTREAM_DESC@ }; + constexpr std::array ostream_descs { {{ OSTREAM_DESC }} }; // max number of cycles to wait for output activity on any stream before timeout - constexpr unsigned max_iters = @TIMEOUT_CYCLES@; + constexpr unsigned max_iters = {{ TIMEOUT_CYCLES }}; + constexpr bool preciseTimeout = {{ PRECISE_TIMEOUT }}; // filename for trace and debug, if enabled. This needs xelab -debug option too. - static const std::optional trace_filename = @TRACE_FILE@; - static const std::string xsim_log_filename = @XSIM_LOG_FILE@; + static const std::optional trace_filename = {{ TRACE_FILE }}; + static const std::string xsim_log_filename = {{ XSIM_LOG_FILE }}; } diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index 517a5d8a02..a6a3d42ed7 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -566,13 +566,6 @@ def _fix_path(p: Path | None) -> Path | None: #: MultiThreshold nodes and a warning is raised instead. max_multithreshold_bit_width: int = 8 - #: Control the number of input frames for rtlsim performance measurement. - rtlsim_batch_size: int = 1 - - #: If set to True, FIFOs with impl_style=vivado will be kept during - #: rtlsim, otherwise they will be replaced by RTL implementations. - rtlsim_use_vivado_comps: bool = True - #: If set to True, the FINN compiler tries to create an MLO design based on #: loop_body_hierarchy and loop_body_range mlo: bool = False diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index d707709b17..fa2f9b7d39 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -43,6 +43,7 @@ from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp +from qonnx.transformation.base import Transformation from qonnx.transformation.bipolar_to_xnor import ConvertBipolarMatMulToXnorPopcount from qonnx.transformation.fold_constants import FoldConstants from qonnx.transformation.general import ( @@ -107,8 +108,11 @@ from finn.transformation.fpgadataflow.set_folding import SetFolding from finn.transformation.fpgadataflow.set_loop_boundary import SetLoopBoundary from finn.transformation.fpgadataflow.simulation import ApplySimulatedFIFOSizes -from finn.transformation.fpgadataflow.simulation_build import BuildSimulation -from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType +from finn.transformation.fpgadataflow.simulation_connected import ( + NodeConnectedSimulation, + RunLayerParallelSimulation, +) from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.fpgadataflow.synth_ooc import SynthOutOfContext from finn.transformation.fpgadataflow.transpose_decomposition import ( @@ -127,11 +131,10 @@ from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds from finn.util.basic import get_liveness_threshold_cycles, get_rtlsim_trace_depth, getHWCustomOp from finn.util.config import extract_model_config_to_json -from finn.util.exception import FINNUserError +from finn.util.exception import FINNUserError, FINNInternalError from finn.util.execution import execute_parent from finn.util.logging import log from finn.util.mlo_sim import is_mlo, mlo_prehook_func_factory -from qonnx.transformation.base import Transformation if TYPE_CHECKING: from finn.custom_op.fpgadataflow.rtl.finn_loop import FINNLoop @@ -348,53 +351,6 @@ def verify_step( log.info(f"Verification for {step_name} : {res_to_str[all_res]}") -def prepare_for_stitched_ip_rtlsim( - verify_model: ModelWrapper, cfg: DataflowBuildConfig -) -> ModelWrapper: - """Prepare model for stitched IP RTL simulation. - - Switches implementation styles from Vivado components to RTL where needed - and ensures proper configuration for RTL simulation. - - Args: - verify_model: The model to prepare for RTL simulation - cfg: Build configuration object - """ - if not cfg.rtlsim_use_vivado_comps: - need_restitch = False - # switch impl_style=vivado components to rtl - # StreamingFIFO must have impl_style=rtl - for fifo_layer in verify_model.get_nodes_by_op_type("StreamingFIFO_rtl"): - inst = getCustomOp(fifo_layer) - if inst.get_nodeattr("impl_style") != "rtl": - inst.set_nodeattr("impl_style", "rtl") - inst.set_nodeattr("code_gen_dir_ipgen", "") - inst.set_nodeattr("ipgen_path", "") - need_restitch = True - # if we've made alterations to the model, need to do some re-prep - if need_restitch: - log.info("Need to regen/re-stitch some IP for STITCHED_IP_RTLSIM") - verify_model = verify_model.transform( - PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period()) - ) - verify_model = verify_model.transform(HLSSynthIP(cfg._resolve_fpga_part())) - verify_model = verify_model.transform( - CreateStitchedIP( - cfg._resolve_fpga_part(), - cfg.synth_clk_period_ns, - vitis=False, - ) - ) - else: - log.info("rtlsim_use_vivado_comps is enabled, may yield incorrect results") - - # set top-level prop for stitched-ip rtlsim and launch - verify_model.set_metadata_prop("exec_mode", "rtlsim") - # TODO make configurable - # verify_model.set_metadata_prop("rtlsim_trace", "trace.vcd") - return verify_model - - @register_build_dataflow_step() def step_hw_codegen(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Generate Vitis HLS code to prepare HLSBackend nodes for IP generation. @@ -454,7 +410,10 @@ def step_hw_ipgen( @register_build_dataflow_step() def step_build_simulation( - model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None + model: ModelWrapper, + cfg: DataflowBuildConfig, + parent_node: str | None = None, + performance_sim: bool = False, ) -> ModelWrapper: """Build the simulation binaries for isolated and connected simulations.""" if cfg.fifosim_save_waveform: @@ -470,6 +429,7 @@ def step_build_simulation( cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg.functional_simulation, + performance_sim=performance_sim, ) ) return model @@ -515,7 +475,7 @@ def step_set_fifo_depths( if cfg.auto_fifo_strategy == AutoFIFOSizingMethod.DISTRIBUTED_SIMULATION: model = step_build_simulation(model, cfg, parent_node=parent_node) model = step_size_fifo_connected(model, cfg) - model = step_apply_fifosizes(model, cfg) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) elif cfg.auto_fifo_strategy == AutoFIFOSizingMethod.LIVE_FIFO: hw_attrs = [ "PE", @@ -549,9 +509,9 @@ def step_set_fifo_depths( model = model.transform(GiveReadableTensorNames()) # save original folding config before potentially modifying it - cfg_path = str(cfg.output_dir) + "/report/folding_config_before_lfs.json" + cfg_path = Path(cfg.output_dir) / "report" / "folding_config_before_lfs.json" extract_model_config_to_json(model, cfg_path, hw_attrs) - model.set_metadata_prop("folding_config_before_lfs", cfg_path) + model.set_metadata_prop("folding_config_before_lfs", str(cfg_path)) # Disable runtime-writable weights, external weights, and dynamic mode for node in model.graph.node: @@ -600,6 +560,7 @@ def step_set_fifo_depths( raise FINNUserError("Unsupported auto_fifo_strategy: " + cfg.auto_fifo_strategy) # generate a dedicated report about final FIFO sizes + # Report has to be generated before large FIFOs are split. fifo_info = {} fifo_info["fifo_depths"] = {} fifo_info["fifo_sizes"] = {} @@ -621,6 +582,11 @@ def step_set_fifo_depths( with (Path(cfg.output_dir) / "report" / "fifo_sizing.json").open("w") as f: json.dump(fifo_info, f, indent=2) + + if cfg.split_large_fifos: + model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) + model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveReadableTensorNames()) else: if cfg.fifo_config_file is None: raise FINNUserError("auto_fifo_depths is set to False but no fifo_config_file provided") @@ -636,6 +602,10 @@ def step_set_fifo_depths( model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) model = model.transform(ApplyFIFODepthsFromFile(cfg.fifo_config_file)) + if cfg.split_large_fifos: + model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) + model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveReadableTensorNames()) # after FIFOs are ready to go, call PrepareIP and HLSSynthIP again # this will only run for the new nodes (e.g. FIFOs and DWCs) @@ -948,7 +918,7 @@ def step_create_dataflow_partition(model: ModelWrapper, cfg: DataflowBuildConfig # Check if there are unsupported layers somewhere between supported layers # This would cause a "cyclic-free graph partitioning violated" error otherwise results = model.analysis(unsupported_layers) - if not results[0]: + if results[0] is False: raise FINNUserError( f"Unsupported/unmapped layer(s) found in between FINN operators, " f"starting with node {results[1].name}. " @@ -965,16 +935,16 @@ def step_create_dataflow_partition(model: ModelWrapper, cfg: DataflowBuildConfig parent_model = model.transform( CreateDataflowPartition( - partition_model_dir=cfg.output_dir + "/intermediate_models/supported_op_partitions" + partition_model_dir=str(cfg.output_dir) + "/intermediate_models/supported_op_partitions" ) ) sdp_nodes = parent_model.get_nodes_by_op_type("StreamingDataflowPartition") assert len(sdp_nodes) == 1, "Only a single StreamingDataflowPartition supported." sdp_node = sdp_nodes[0] sdp_node = getCustomOp(sdp_node) - dataflow_model_filename = sdp_node.get_nodeattr("model") + dataflow_model_filename = cast("str", sdp_node.get_nodeattr("model")) if cfg.save_intermediate_models: - parent_model.save(cfg.output_dir + "/intermediate_models/dataflow_parent.onnx") + parent_model.save(str(cfg.output_dir) + "/intermediate_models/dataflow_parent.onnx") model = ModelWrapper(dataflow_model_filename) # create a configuration json file that can be used to set the specialize layer config @@ -982,7 +952,7 @@ def step_create_dataflow_partition(model: ModelWrapper, cfg: DataflowBuildConfig "preferred_impl_style", ] extract_model_config_to_json( - model, cfg.output_dir + "/template_specialize_layers_config.json", attrs + model, Path(cfg.output_dir) / "template_specialize_layers_config.json", attrs ) return model @@ -1074,7 +1044,7 @@ def step_target_fps_parallelization(model: ModelWrapper, cfg: DataflowBuildConfi "depth_trigger_bram", ] extract_model_config_to_json( - model, cfg.output_dir + "/report/auto_folding_config.json", hw_attrs + model, Path(cfg.output_dir) / "report" / "auto_folding_config.json", hw_attrs ) else: @@ -1295,7 +1265,7 @@ def step_create_stitched_ip(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo if VerificationStepType.STITCHED_IP_RTLSIM in cfg._resolve_verification_steps(): # prepare ip-stitched rtlsim verify_model = deepcopy(model) - verify_model = prepare_for_stitched_ip_rtlsim(verify_model, cfg) + verify_model.set_metadata_prop("exec_mode", "rtlsim") # Use critical path estimate to set rtlsim liveness threshold # TODO: This is a heuristic which usually overestimates the maximum @@ -1324,84 +1294,87 @@ def step_create_stitched_ip(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo @register_build_dataflow_step() -def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfig): - """Measure performance + latency of stitched-IP model in rtlsim (xsi). - Depends on the DataflowOutputType.STITCHED_IP output product. - """ - if DataflowOutputType.RTLSIM_PERFORMANCE in cfg.generate_outputs and not is_mlo(model): - assert DataflowOutputType.STITCHED_IP in cfg.generate_outputs, ( - "rtlsim_perf needs stitched IP" - ) - report_dir = cfg.output_dir + "/report" - os.makedirs(report_dir, exist_ok=True) - rtlsim_bs = int(cfg.rtlsim_batch_size) - orig_rtlsim_trace_depth = get_rtlsim_trace_depth() - assert rtlsim_bs > 0, "rtlsim batch size must be >0" - if cfg.verify_save_rtlsim_waveforms: - # set depth to 3 for layer-by-layer visibility - os.environ["RTLSIM_TRACE_DEPTH"] = "3" - model.set_metadata_prop( - "rtlsim_trace", - "%s/rtlsim_perf_batch_%d.wdb" % (os.path.abspath(report_dir), rtlsim_bs), - ) +def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: + """Measure performance + latency of stitched-IP model in rtlsim (xsi).""" + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) - # Use critical path estimate to set the timeout limit for FIFO sim - # TODO: This is a heuristic which usually overestimates the maximum - # cycles (by a lot), but can actually also underestimate causing - # incorrect detection of timeouts. In these cases, this estimation can - # be overwritten by setting LIVENESS_THRESHOLD to a very large value. - model = model.transform(AnnotateCycles()) - liveness = get_liveness_threshold_cycles() - perf = model.analysis(dataflow_performance) - latency = perf["critical_path_cycles"] - max_iters = max(liveness, int(np.ceil(latency * 1.1 + 50))) - - rtlsim_perf_dict = xsi_fifosim(model, rtlsim_bs, max_iters=max_iters) - # keep keys consistent between the Python and C++-styles - cycles = rtlsim_perf_dict["cycles"] - clk_ns = cfg.synth_clk_period_ns - fclk_mhz = 1 / (clk_ns * 0.001) - runtime_s = (cycles * clk_ns) * (10**-9) - rtlsim_perf_dict["runtime[ms]"] = runtime_s * 1000 - rtlsim_perf_dict["throughput[images/s]"] = rtlsim_bs / runtime_s - rtlsim_perf_dict["fclk[mhz]"] = fclk_mhz - for key, val in rtlsim_perf_dict.items(): - if "max_count" in key: - del rtlsim_perf_dict[key] - # estimate stable-state throughput based on latency+throughput - if rtlsim_bs == 1: - rtlsim_perf_dict["stable_throughput[images/s]"] = rtlsim_perf_dict[ - "throughput[images/s]" - ] - else: - total_cycles = rtlsim_perf_dict["cycles"] - latency_cycles = rtlsim_perf_dict["latency_cycles"] - stablestate_cycles = total_cycles - latency_cycles - clk_ns = cfg.synth_clk_period_ns - fclk_mhz = 1 / (clk_ns * 0.001) - runtime_s = (stablestate_cycles * clk_ns) * (10**-9) - rtlsim_perf_dict["stable_throughput[images/s]"] = rtlsim_bs / runtime_s - - with open(report_dir + "/rtlsim_performance.json", "w") as f: - json.dump(rtlsim_perf_dict, f, indent=2) - if cfg.verify_save_rtlsim_waveforms: - # restore original trace depth - os.environ["RTLSIM_TRACE_DEPTH"] = str(orig_rtlsim_trace_depth) + orig_rtlsim_trace_depth = get_rtlsim_trace_depth() + + if cfg.verify_save_rtlsim_waveforms: + # set depth to 3 for layer-by-layer visibility + os.environ["RTLSIM_TRACE_DEPTH"] = "3" + model.set_metadata_prop("rtlsim_trace", str(report_dir.resolve() / "rtlsim_perf_trace.wdb")) + + # Use critical path estimate to set the timeout limit for FIFO sim + model = model.transform(AnnotateCycles()) + perf = model.analysis(dataflow_performance) + latency = cast("int", perf["critical_path_cycles"]) + max_iters = latency * 10 + # prepare simulation + # model = step_build_simulation(model, cfg, parent_node = None, performance_sim=True) + sim = NodeConnectedSimulation( + model, + SimulationType.NODE_BASED_CONNECTED, + cfg._resolve_fpga_part(), + cfg._resolve_hls_clk_period(), + cfg.functional_simulation, + max_qsrl_depth=256, + performance_sim=True, + ) - else: - log.info( - """DataflowOutputType.RTLSIM_PERFORMANCE not in requested outputs or model is MLO, - skipping step_measure_rtlsim_performance.""" - ) + nodes = [node for node in model.graph.node if "FIFO" not in node.op_type] + num_nodes = len(nodes) + fifo_depth: list[list[int]] = [[]] * num_nodes + + for i, node in enumerate(nodes): + hwnode = getHWCustomOp(node) + fifos = cast("list[int]", hwnode.get_nodeattr("outFIFODepths")) + num_successors = len(node.output) + if num_successors != len(fifos): + raise FINNUserError( + f"Number of successors ({num_successors}) doesn't match number of FIFO depths " + f"({len(fifos)}) for node {node.name}. " + f"Did you run the FIFO sizing step or supplied a valid FIFO config for the model?" + ) + if fifos[0] == 2: + log.warning( + f"Node {node.name} has FIFO depth of 2, which is the default unconfigured depth. " + "This might lead to deadlock in the simulation. " + "Please run the FIFO sizing step or supply a valid FIFO config for the model." + ) + fifo_depth[i] = fifos + + results = sim.simulate(fifo_depth, max_cycles=max_iters) + outputs: list[dict] = [] + for res in results[0]: + if res["samples"] != 0: + # Cleanup of Output + del res["fifo_utilization"] + del res["fifo_depth"] + del res["fifo_cycles_until_first_valid"] + cycle_per_sec = 1e9 / cfg.synth_clk_period_ns + res["throughput_fps"] = cycle_per_sec / res["intervals"][0] # type: ignore + # Attach entry to output + outputs.append(res) + + rtl_sim_perf_dir = report_dir / "rtlsim_performance.json" + with rtl_sim_perf_dir.open("w") as f: + json.dump(outputs, f, indent=2) + if cfg.verify_save_rtlsim_waveforms: + # restore original trace depth + os.environ["RTLSIM_TRACE_DEPTH"] = str(orig_rtlsim_trace_depth) + + log.info(f"RTLSim performance results written into {rtl_sim_perf_dir}") return model @register_build_dataflow_step() -def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Create a driver that can be used to interface the generated accelerator. Use DataflowBuildConfig to select PYNQ Python or C++ driver.""" - driver_dir = os.path.join(cfg.output_dir, "driver") + driver_dir = Path(cfg.output_dir) / "driver" if DataflowOutputType.PYNQ_DRIVER in cfg.generate_outputs: # determine drivertype if cfg.enable_instrumentation: @@ -1426,8 +1399,10 @@ def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): ) ) - shutil.copytree(model.get_metadata_prop("pynq_driver_dir"), driver_dir, dirs_exist_ok=True) - log.info("PYNQ Python driver written into " + driver_dir) + shutil.copytree( + cast("str", model.get_metadata_prop("pynq_driver_dir")), driver_dir, dirs_exist_ok=True + ) + log.info("PYNQ Python driver written into " + str(driver_dir)) elif DataflowOutputType.CPP_DRIVER in cfg.generate_outputs: # generate C++ Driver model = model.transform( @@ -1438,13 +1413,13 @@ def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): ) ) shutil.copytree( - model.get_metadata_prop("cpp_driver_dir"), + cast("str", model.get_metadata_prop("cpp_driver_dir")), driver_dir, dirs_exist_ok=True, copy_function=shutil.copyfile, ) - log.info("C++ driver written into " + driver_dir) + log.info("C++ driver written into " + str(driver_dir)) else: log.warning( """Neither DataflowOutputType.PYNQ_DRIVER nor DataflowOutputType.CPP_DRIVER @@ -1454,7 +1429,7 @@ def step_make_driver(model: ModelWrapper, cfg: DataflowBuildConfig): @register_build_dataflow_step() -def step_out_of_context_synthesis(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_out_of_context_synthesis(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Run out-of-context synthesis and generate reports. Depends on the DataflowOutputType.STITCHED_IP output product.""" if DataflowOutputType.OOC_SYNTH in cfg.generate_outputs: @@ -1484,7 +1459,7 @@ def step_out_of_context_synthesis(model: ModelWrapper, cfg: DataflowBuildConfig) @register_build_dataflow_step() -def step_vivado_power_estimation(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_vivado_power_estimation(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Run Vivado power estimation on the stitched IP after OOC synthesis.""" if DataflowOutputType.OOC_SYNTH not in cfg.generate_outputs: raise FINNUserError("Vivado power estimation needs OOC synth") @@ -1502,7 +1477,7 @@ def step_vivado_power_estimation(model: ModelWrapper, cfg: DataflowBuildConfig): @register_build_dataflow_step() -def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Synthesize a bitfile for the using the specified shell flow, using either Vivado or Vitis, to target the specified board.""" if DataflowOutputType.BITFILE in cfg.generate_outputs: @@ -1584,21 +1559,21 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig): @register_build_dataflow_step() -def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig): +def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Create a deployment package including the driver and bitfile.""" if DataflowOutputType.DEPLOYMENT_PACKAGE in cfg.generate_outputs: - deploy_dir = cfg.output_dir + "/deploy" - bitfile_dir = cfg.output_dir + "/bitfile" - driver_dir = cfg.output_dir + "/driver" - os.makedirs(deploy_dir, exist_ok=True) - shutil.copytree(bitfile_dir, deploy_dir + "/bitfile", dirs_exist_ok=True) + deploy_dir = Path(cfg.output_dir) / "deploy" + bitfile_dir = Path(cfg.output_dir) / "bitfile" + driver_dir = Path(cfg.output_dir) / "driver" + deploy_dir.mkdir(parents=True, exist_ok=True) + shutil.copytree(bitfile_dir, deploy_dir / "bitfile", dirs_exist_ok=True) shutil.copytree( - driver_dir, deploy_dir + "/driver", dirs_exist_ok=True, copy_function=shutil.copyfile + driver_dir, deploy_dir / "driver", dirs_exist_ok=True, copy_function=shutil.copyfile ) if DataflowOutputType.CPP_DRIVER in cfg.generate_outputs: update_bitfile_path_after_copy( - os.path.join(deploy_dir, "bitfile", "finn-accel.xclbin"), - os.path.join(deploy_dir, "driver", "acceleratorconfig.json"), + deploy_dir / "bitfile" / "finn-accel.xclbin", + deploy_dir / "driver" / "acceleratorconfig.json", ) else: diff --git a/src/finn/custom_op/fpgadataflow/elementwise_binary.py b/src/finn/custom_op/fpgadataflow/elementwise_binary.py index 262d2f49c1..04834215ba 100644 --- a/src/finn/custom_op/fpgadataflow/elementwise_binary.py +++ b/src/finn/custom_op/fpgadataflow/elementwise_binary.py @@ -27,15 +27,16 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import numpy as np - +from typing import cast # Helper for creating ONNX nodes -from onnx import helper as oh +from onnx import NodeProto, helper as oh from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper from finn.custom_op.fpgadataflow import register_custom_op from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.util.exception import FINNInternalError # FINN logging from finn.util.logging import log @@ -135,18 +136,18 @@ def out_dtype(self): # Shape attribute as property for convenience @property - def lhs_shape(self): - return self.get_nodeattr("lhs_shape") + def lhs_shape(self) ->np.ndarray: + return cast("np.ndarray", self.get_nodeattr("lhs_shape")) # Shape attribute as property for convenience @property - def rhs_shape(self): - return self.get_nodeattr("rhs_shape") + def rhs_shape(self) ->np.ndarray: + return cast("np.ndarray", self.get_nodeattr("rhs_shape")) # Shape attribute as property for convenience @property - def out_shape(self): - return self.get_nodeattr("out_shape") + def out_shape(self) ->np.ndarray: + return cast("np.ndarray", self.get_nodeattr("out_shape")) # Style attribute as property for convenience @property @@ -171,27 +172,35 @@ def broadcast_last_axis(self): # Makes an operation compatible with the output shape for shape inference # Note: Propagates shape forward, i.e., never asks for the shape of the # output, even if it seems easier. - def make_shape_compatible_op(self, model: ModelWrapper): + def make_shape_compatible_op(self, model: ModelWrapper) -> NodeProto: # Get the node wrapped by this custom op node = self.onnx_node # There must be exactly two inputs to the binary operation - assert len(node.input) == 2, f"Binary operation {node.name} requires exactly two inputs" + if len(node.input) != 2: + raise FINNInternalError(f"Binary operation {node.name} requires exactly two inputs") # Validate input shapes match what is stored as attributes - assert ( - model.get_tensor_shape(node.input[0]) == self.lhs_shape - ), f"Input shape mismatch: {node.name} {node.input[0]}" - assert ( - model.get_tensor_shape(node.input[1]) == self.rhs_shape - ), f"Input shape mismatch: {node.name} {node.input[1]}" + if model.get_tensor_shape(node.input[0]) != self.lhs_shape: + raise FINNInternalError( + f"Input shape mismatch: {node.name} {node.input[0]}. " + f"Expected {self.lhs_shape}, got {model.get_tensor_shape(node.input[0])}" + ) + if model.get_tensor_shape(node.input[1]) != self.rhs_shape: + raise FINNInternalError( + f"Input shape mismatch: {node.name} {node.input[1]}. " + f"Expected {self.rhs_shape}, got {model.get_tensor_shape(node.input[1])}" + ) # Validate broadcasting of inputs to the output shape - assert ( - list(np.broadcast_shapes(self.lhs_shape, self.rhs_shape)) == self.out_shape - ), f"Shape broadcast mismatch: {node.name}" + if list(np.broadcast_shapes(self.lhs_shape, self.rhs_shape)) != self.out_shape: + raise FINNInternalError( + f"Shape broadcast mismatch: {node.name}. " + f"Expected {self.out_shape}, got " + f"{list(np.broadcast_shapes(self.lhs_shape, self.rhs_shape))}" + ) # Simulate behavior via the standard ONNX add operation return oh.make_node("Add", node.input, node.output) # Infers the datatype of the node output - def infer_node_datatype(self, model: ModelWrapper): + def infer_node_datatype(self, model: ModelWrapper) -> None: # Get the node wrapped by this custom op node = self.onnx_node # Test for changing left-hand-side input datatype @@ -213,7 +222,7 @@ def infer_node_datatype(self, model: ModelWrapper): # Force the output data type stored as a node attribute model.set_tensor_datatype(node.output[0], self.out_dtype) - def execute_node(self, context, graph): + def execute_node(self, context, graph) -> None: # Get the node wrapped by this custom op node = self.onnx_node # Get the inputs out of the execution context @@ -326,9 +335,9 @@ def minimize_accumulator_width(self, model: ModelWrapper): if not all([self.lhs_dtype.is_integer(), self.rhs_dtype.is_integer()]): # Check the annotated tensor data type corresponds to the stored # attribute - assert ( - model.get_tensor_datatype(self.onnx_node.output[0]) == self.out_dtype - ), f"Output type mismatch for {self.onnx_node.name}" + assert model.get_tensor_datatype(self.onnx_node.output[0]) == self.out_dtype, ( + f"Output type mismatch for {self.onnx_node.name}" + ) # Exit here, returning the not-minimized data type return self.out_dtype # Call the output type derivation specialized by the concrete operator @@ -901,4 +910,4 @@ def _derive_out_dtype(self, model: ModelWrapper): else: rhs_intbits = self.rhs_dtype.bitwidth() out_intbits = max(lhs_intbits, rhs_intbits) - return DataType[f"FIXED<{out_fracbits+out_intbits},{out_intbits}>"] + return DataType[f"FIXED<{out_fracbits + out_intbits},{out_intbits}>"] diff --git a/src/finn/custom_op/fpgadataflow/hwcustomop.py b/src/finn/custom_op/fpgadataflow/hwcustomop.py index 4b9d9b0784..f4c0dd0ba4 100644 --- a/src/finn/custom_op/fpgadataflow/hwcustomop.py +++ b/src/finn/custom_op/fpgadataflow/hwcustomop.py @@ -121,14 +121,6 @@ def get_nodeattr_types( "inFIFODepths": ("ints", False, [2]), "outFIFODepths": ("ints", False, [2]), "output_hook": ("s", False, ""), - # accumulated characteristic function over two periods - "io_chrc_in": ("t", False, np.asarray([], dtype=np.int32)), - "io_chrc_out": ("t", False, np.asarray([], dtype=np.int32)), - # the period for which the characterization was run - "io_chrc_period": ("i", False, 0), - # amount of zero padding inserted during chrc. - "io_chrc_pads_in": ("ints", False, []), - "io_chrc_pads_out": ("ints", False, []), "mlo_max_iter": ("i", False, 0), } diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py index c7f4f2da0b..1513023cf6 100644 --- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py +++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py @@ -294,7 +294,7 @@ def connect_axi(self, node: "NodeProto", model: "ModelWrapper") -> None: self.connect_cmds.extend( [ f"make_bd_intf_pins_external " - f"[get_bd_intf_pins {inst_name}/{aximm_intf_name[0][0]}]" + f"[get_bd_intf_pins {inst_name}/{aximm_intf_name[0][0]}]", f"set_property name {ext_if_name} [get_bd_intf_ports m_axi_gmem_0]", "assign_bd_address", f"set_property offset 0 [get_bd_addr_segs {{{seg_name}}}]", diff --git a/src/finn/transformation/fpgadataflow/insert_fifo.py b/src/finn/transformation/fpgadataflow/insert_fifo.py index 737e3d1309..eac4897aaf 100644 --- a/src/finn/transformation/fpgadataflow/insert_fifo.py +++ b/src/finn/transformation/fpgadataflow/insert_fifo.py @@ -35,17 +35,17 @@ import numpy as np import numpy.typing as npt -from onnx import helper as oh +from collections.abc import Sequence from onnx import NodeProto -from finn.util.basic import getHWCustomOp +from onnx import helper as oh from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation +from typing import cast +from finn.util.basic import getHWCustomOp +from finn.util.exception import FINNInternalError from finn.util.fpgadataflow import is_fpgadataflow_node from finn.util.logging import log -from finn.util.exception import FINNInternalError -from typing import cast -from collections.abc import Sequence class InsertFIFO(Transformation): diff --git a/src/finn/transformation/fpgadataflow/make_driver.py b/src/finn/transformation/fpgadataflow/make_driver.py index c489cc883f..09ea587535 100644 --- a/src/finn/transformation/fpgadataflow/make_driver.py +++ b/src/finn/transformation/fpgadataflow/make_driver.py @@ -49,31 +49,32 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log +from pathlib import Path -def update_bitfile_path_after_copy(bitfile_path: str, json_path: str) -> None: +def update_bitfile_path_after_copy(bitfile_path: Path, json_path: Path) -> None: """Update the xclbinPath in the JSON configuration to point to the new bitfile location. Args: - json_path (str): Path to the JSON configuration file - bitfile_path (str): New path to the bitfile (.xclbin) + json_path (Path): Path to the JSON configuration file + bitfile_path (Path): New path to the bitfile (.xclbin) """ - if json_path is None or not os.path.exists(json_path): + if json_path is None or not json_path.exists(): raise FINNInternalError("JSON configuration file does not exist or is not specified.") - if bitfile_path is None or not os.path.exists(bitfile_path): + if bitfile_path is None or not bitfile_path.exists(): raise FINNInternalError("Bitfile path does not exist or is not specified.") - if not json_path.endswith(".json"): + if not json_path.suffix == ".json": raise FINNInternalError("Provided path is not a JSON file.") # Read the current JSON configuration - with open(json_path) as f: + with json_path.open() as f: data = json.load(f) # Update the xclbinPath for each device in the configuration for device_config in data: - device_config["xclbinPath"] = os.path.abspath(bitfile_path) + device_config["xclbinPath"] = bitfile_path.resolve().as_posix() # Write the updated configuration back to the file - with open(json_path, "w") as f: + with json_path.open("w") as f: json.dump(data, f, indent=4) diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index 008295120d..b3a9bbf371 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -29,19 +29,21 @@ """Transformations for inserting and setting the size of FIFOs in FINN dataflow graphs.""" -from finn.util.exception import FINNUserError +import json from onnx import TensorProto, helper +from pathlib import Path from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper -from finn.util.basic import getHWCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, SortGraph from typing import Literal, cast -from pathlib import Path -import json +from finn.util.basic import getHWCustomOp +from finn.util.exception import FINNUserError from finn.util.logging import log +from onnx import NodeProto + class ApplyFIFODepthsFromFile(Transformation): """Apply FIFO depths from a JSON file generated by a previous run of build_dataflow @@ -57,7 +59,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: with self.fifo_config_file.open() as f: fifo_info = json.load(f) - #JSON file format: + # JSON file format: # { # "fifo_depths": { # "StreamingFIFO_rtl_0": 2, @@ -83,70 +85,90 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: graph_modified = False for node in fifo_nodes: if node.name not in fifo_info["fifo_depths"]: - raise FINNUserError( - f"FIFO node {node.name} not found in configuration file" - ) + raise FINNUserError(f"FIFO node {node.name} not found in configuration file") n_inst = getHWCustomOp(node) depth = fifo_info["fifo_depths"][node.name] + old_depth = n_inst.get_nodeattr("depth") + if depth == old_depth: + continue impl_style = fifo_info["impl_style"][node.name] ram_style = fifo_info["ram_style"][node.name] n_inst.set_nodeattr("depth", depth) n_inst.set_nodeattr("impl_style", impl_style) n_inst.set_nodeattr("ram_style", ram_style) + + producer = model.find_producer(node.input[0]) + if producer is not None: + prod_node = getHWCustomOp(producer) + out_fifo_depths = cast("list[int]", prod_node.get_nodeattr("outFIFODepths")) + succ = cast("list[NodeProto]", model.find_direct_successors(producer)) + num_succ = len(succ) + if len(out_fifo_depths) != num_succ: + out_fifo_depths = [0] * num_succ + for i, s in enumerate(succ): + if s.name == node.name: + out_fifo_depths[i] = depth + prod_node.set_nodeattr( + "outFIFODepths", cast("list[int|str|float]", out_fifo_depths) + ) + graph_modified = True return (model, graph_modified) -def get_fifo_split_configs(depth: int, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768 - ) -> list[tuple[int, str]]: - """Break non-power-of-2 sized FIFO depths into several ones.""" - - def floor_pow2(x: int) -> int: - """Return the largest power of 2 less than or equal to x.""" - if (x & (x - 1) == 0) and x != 0: - return x - return 1 << ((x - 1).bit_length() - 1) - - def decompose_pow2(x: int) -> list[int]: - """Decompose x into a sum of powers of 2, - with each power of 2 no larger than max_qsrl_depth. - """ - if x <= max_qsrl_depth: - return [x] - r = floor_pow2(x) - if x == r: - return [x] - return [r, *decompose_pow2(x - r)] - - ret = [] - # trivial case: for small FIFOs, return as-is with rtl style - if depth <= max_qsrl_depth: - return [(depth, "rtl")] - # first pass: ensure max depth is respected - # (restricted by Vivado AXIS infra IP) - remainder = depth - while remainder != 0: - if remainder > max_vivado_depth: - ret.append(max_vivado_depth) - remainder -= max_vivado_depth - else: - ret.append(remainder) - remainder = 0 - # second pass: break non-power-of-2 sized FIFOs - # into several ones - - ret_pass2 = list(map(decompose_pow2, ret)) - # unpack list of lists - ret_pass2 = [x for dec_list in ret_pass2 for x in dec_list] - - # finally, add impl_style to each split FIFO - ret_final = [] - for cand_depth in ret_pass2: - if cand_depth <= max_qsrl_depth: - ret_final.append((max(2, cand_depth), "rtl")) - else: - ret_final.append((cand_depth, "vivado")) - - return ret_final + +def get_fifo_split_configs( + depth: int, max_qsrl_depth: int = 256, max_vivado_depth: int = 32768 +) -> list[tuple[int, str]]: + """Break non-power-of-2 sized FIFO depths into several ones.""" + + def floor_pow2(x: int) -> int: + """Return the largest power of 2 less than or equal to x.""" + if (x & (x - 1) == 0) and x != 0: + return x + return 1 << ((x - 1).bit_length() - 1) + + def decompose_pow2(x: int) -> list[int]: + """Decompose x into a sum of powers of 2, + with each power of 2 no larger than max_qsrl_depth. + """ + if x <= max_qsrl_depth: + return [x] + r = floor_pow2(x) + if x == r: + return [x] + return [r, *decompose_pow2(x - r)] + + ret = [] + # trivial case: for small FIFOs, return as-is with rtl style + if depth <= max_qsrl_depth: + return [(depth, "rtl")] + # first pass: ensure max depth is respected + # (restricted by Vivado AXIS infra IP) + remainder = depth + while remainder != 0: + if remainder > max_vivado_depth: + ret.append(max_vivado_depth) + remainder -= max_vivado_depth + else: + ret.append(remainder) + remainder = 0 + # second pass: break non-power-of-2 sized FIFOs + # into several ones + + ret_pass2 = list(map(decompose_pow2, ret)) + # unpack list of lists + ret_pass2 = [x for dec_list in ret_pass2 for x in dec_list] + + # finally, add impl_style to each split FIFO + ret_final = [] + for cand_depth in ret_pass2: + if cand_depth <= max_qsrl_depth: + ret_final.append((max(2, cand_depth), "rtl")) + else: + ret_final.append((cand_depth, "vivado")) + + return ret_final + class SplitLargeFIFOs(Transformation): """Split large FIFOs before implementation, for two reasons. @@ -175,9 +197,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: if node.op_type == ("StreamingFIFO_rtl"): n_inst = getHWCustomOp(node) depth = cast("int", n_inst.get_nodeattr("depth")) - cfgs = get_fifo_split_configs( - depth, self.max_qsrl_depth, self.max_vivado_depth - ) + cfgs = get_fifo_split_configs(depth, self.max_qsrl_depth, self.max_vivado_depth) if len(cfgs) > 1: fld_shape = n_inst.get_folded_output_shape() n_shape = n_inst.get_normal_output_shape() diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 4398bda830..b1ec469374 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -119,11 +119,15 @@ def __init__( clk_ns: float, functional_sim: bool, workers: int | None = None, # noqa: ARG002 + performance_sim: bool = False, ) -> None: """Create a new simulation instance. Read simulation binary paths from the simulation_binaries metadata prop field.""" self.simulation_type = simulation_type self.model = model + self.model = self.model.transform( + BuildSimulation(fpgapart, clk_ns, functional_sim, performance_sim) + ) sim_binaries_str = self.model.get_metadata_prop("simulation_binaries") if sim_binaries_str is None: @@ -132,7 +136,12 @@ def __init__( "sure to run the BuildSimulation transformation beforehand." ) sim_binaries: list[Path] = [Path(p) for p in str(sim_binaries_str).split("\n")] - if len(sim_binaries) != len(self.model.graph.node): + nodes = ( + [n for n in self.model.graph.node if "FIFO" not in n.op_type] + if performance_sim + else self.model.graph.node + ) + if len(sim_binaries) != len(nodes): raise FINNUserError( "The number of found simulation binaries does not match the number " "of nodes in the graph. Make sure to run BuildSimulation just " @@ -142,7 +151,6 @@ def __init__( raise FINNUserError( "Simulation binary data points to invalid paths. Please rerun BuildSimulation." ) - self.model = self.model.transform(BuildSimulation(fpgapart, clk_ns, functional_sim)) self.binaries: dict[int, Path] = {i: sim_binaries[i] for i in range(len(sim_binaries))} match simulation_type: case SimulationType.NODE_BASED_CONNECTED: @@ -186,14 +194,7 @@ def __init__( self.cfg = cfg self.max_qsrl_depth = max_qsrl_depth self.vivado_ram_style = vivado_ram_style - if fifo_config is None: - self.path = Path(cfg.output_dir) / "fifo_config.json" - else: - self.path = fifo_config - - self.fifo_depths: FIFODepthConfig = [] - with self.path.open() as f: - self.fifo_depths = cast("FIFODepthConfig", json.load(f)) + self.fifo_config = fifo_config def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Apply FIFO Simulation Depths to the model.""" @@ -204,6 +205,20 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: ) return model, False + if self.fifo_config is None: + p = model.get_metadata_prop("fifo_data") + if p == "" or p is None: + raise FINNInternalError( + "FIFO sizing simulation was not run before inserting simulated FIFO sizes!" + ) + self.path = Path(p) + else: + self.path = self.fifo_config + + self.fifo_depths: FIFODepthConfig = [] + with self.path.open() as f: + self.fifo_depths = cast("FIFODepthConfig", json.load(f)) + if len(model.graph.node) != len(self.fifo_depths): raise FINNUserError( "There are no StreamingFIFOs in the graph, yet the number " diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 6187827433..1f000d856b 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -34,6 +34,7 @@ from finn.util.basic import getHWCustomOp, launch_process_helper, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log +from jinja2 import Environment if TYPE_CHECKING: from collections.abc import Sequence @@ -52,11 +53,14 @@ class SimulationType(str, Enum): class SimulationBuilder: """Build simulations in FINN.""" - def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: + def __init__( + self, model: ModelWrapper, fpgapart: str, clk_ns: float, performance_sim: bool = False + ) -> None: """Create a new simulation instance.""" self.model = model self.fpgapart = fpgapart self.clk_ns = clk_ns + self.performance_sim = performance_sim def _create_existing_initializer_input( self, @@ -185,6 +189,9 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: Args: by_node: If int, used as the index of the specified node. If string, assumed to be the name of the node. + performance_sim: Specifies if the simulation is used for FIFO sizing or performance + simulation. In the case of a performance simulation, FIFOs are already + in the graph and need to be skipped. Returns: ModelWrapper: The isolated-node modelwrapper. @@ -256,29 +263,100 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: outputs_node: list[ValueInfoProto] = [] nodes_graph: list[NodeProto] = [] - preds_list: list | None = self.model.find_direct_predecessors(target_node) - succs_list: list | None = self.model.find_direct_successors(target_node) + def _find_first_non_fifo_pred(pred: NodeProto) -> NodeProto | None: + if "FIFO" in pred.op_type: + # Replace FIFOs with their predecessor + pred_fifo = self.model.find_direct_predecessors(pred) + if pred_fifo is not None: + if len(pred_fifo) > 1: + raise FINNInternalError( + f"FIFOs are expected to have exactly one predecessor, " + f"but found multiple: {pred_fifo}" + ) + if "FIFO" in pred_fifo[0].op_type: + return _find_first_non_fifo_pred(pred_fifo[0]) + return pred_fifo[0] + if get_by_name(self.model.graph.input, pred.input[0]) is not None: + return None # Reached the end of the graph, return None to indicate graph input + return pred + + def _find_first_non_fifo_succ(succ: NodeProto) -> NodeProto | None: + if "FIFO" in succ.op_type: + # Replace FIFOs with their successor + succ_fifo = self.model.find_direct_successors(succ) + if succ_fifo is not None: + if len(succ_fifo) > 1: + raise FINNInternalError( + f"FIFOs are expected to have exactly one successor, " + f"but found multiple: {succ_fifo}" + ) + if "FIFO" in succ_fifo[0].op_type: + return _find_first_non_fifo_succ(succ_fifo[0]) + return succ_fifo[0] + if get_by_name(self.model.graph.output, succ.output[0]) is not None: + return ( + None # Reached the end of the graph, return None to indicate graph output + ) + return succ + + preds_list: list[NodeProto] | None = self.model.find_direct_predecessors(target_node) + succs_list: list[NodeProto] | None = self.model.find_direct_successors(target_node) - num_preds = len(preds_list) if preds_list is not None else 0 - num_succs = len(succs_list) if succs_list is not None else 0 + preds = [] + if preds_list is not None: + for pred in preds_list: + preds.append(_find_first_non_fifo_pred(pred)) + succs = [] + if succs_list is not None: + for succ in succs_list: + succs.append(_find_first_non_fifo_succ(succ)) + preds_list = [x for x in preds if x is not None] + succs_list = [x for x in succs if x is not None] + + num_preds = len(preds_list) + num_succs = len(succs_list) input_node = False output_node = False + def _get_first_non_fifo_input(node: NodeProto, inp: str) -> str: + """Return the first output by name that is not produced by a FIFO. + If the input is not produced by a FIFO, return the input itself.""" + pred = self.model.find_direct_predecessors(node) + if pred is None: + return inp + for p in pred: + if inp in p.output and "FIFO" in p.op_type: + fifo_input = p.input[0] # FIFOs are expected to have exactly one input + return _get_first_non_fifo_input(p, fifo_input) + return inp + + def _get_first_non_fifo_output(node: NodeProto, outp: str) -> str: + """Return the first output by name that is not consumed by a FIFO. + If the output is not consumed by a FIFO, return the output itself.""" + succ = self.model.find_direct_successors(node) + if succ is None: + return outp + for s in succ: + if outp in s.input and "FIFO" in s.op_type: + fifo_output = s.output[0] # FIFOs are expected to have exactly one output + return _get_first_non_fifo_output(s, fifo_output) + return outp + # Set correct input/output count for input and output nodes, since they have no pred/succ. if num_preds == 0: inputs = self.model.graph.input for i in range(len(target_node.input)): - ret = get_by_name(inputs, target_node.input[i]) # Check that node is graph input - if ret is not None and ( - not is_mlo_node or target_node.input[i] not in mlo_parameter_input_names - ): + inp = _get_first_non_fifo_input(target_node, target_node.input[i]) + ret = get_by_name(inputs, inp) # Check that node is graph input + if ret is not None and (not is_mlo_node or inp not in mlo_parameter_input_names): num_preds += 1 input_node = True if num_succs == 0: outputs = self.model.graph.output for i in range(len(target_node.output)): - ret = get_by_name(outputs, target_node.output[i]) # Check that node is graph output + out = _get_first_non_fifo_output(target_node, target_node.output[i]) + ret = get_by_name(outputs, out) # Check that node is graph output if ret is not None: num_succs += 1 output_node = True @@ -297,6 +375,10 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: converted_initializer_input_indices: list[int] = [] for i in range(num_inputs): inp_name = target_node.input[i] + inp_name = _get_first_non_fifo_input( + target_node, inp_name + ) # Replace FIFO-produced inputs with their predecessor's input, + # this will skip all FIFOs in the isolation is_mlo_parameter_input = is_mlo_node and inp_name in mlo_parameter_input_names init_vals_only = self.model.get_initializer(inp_name) if init_vals_only is not None or is_mlo_parameter_input: @@ -388,6 +470,12 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: if "mlo_max_iter" in params: del params["mlo_max_iter"] params_changed = True + if "runtime_writeable_weights" in params and params["runtime_writeable_weights"] == 1: + params["runtime_writeable_weights"] = 0 + params_changed = True + if "dynamic_mode" in params and params["dynamic_mode"] == 1: + params["dynamic_mode"] = 0 + params_changed = False if params_changed: params["code_gen_dir_ipgen"] = "" params["ipgen_path"] = "" @@ -599,6 +687,7 @@ def _template_rtlsim_config( ) = self._get_stream_descriptions(model) template_dict = { "TIMEOUT_CYCLES": timeout_cycles, + "PRECISE_TIMEOUT": str(self.performance_sim).lower(), # name of the top-level HDL module "TOP_MODULE_NAME": top_module_name, # top-level AXI stream descriptors @@ -632,8 +721,8 @@ def _template_rtlsim_config( fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" fsim_config = fifosim_config_fname.read_text() - for key, val in template_dict.items(): - fsim_config = fsim_config.replace(f"@{key}@", str(val)) + env = Environment() + fsim_config = env.from_string(fsim_config).render(**template_dict) # Write the config to the simulation directory rtlsim_config = Path(sim_base) / "rtlsim_config.hpp" rtlsim_config.write_text(fsim_config) @@ -676,9 +765,6 @@ def build_single_node_simulation( Returns: Path: The path to the simulation binary (shell script). """ - # TODO: Check if something is an output node instead of checking the node index - # TODO: Requires changes in the C++ code as well - # Check that the relevant data exists wrapper_filename = node_model.get_metadata_prop("wrapper_filename") if wrapper_filename is None or not Path(wrapper_filename).exists(): @@ -771,7 +857,12 @@ def _build( silent=with_live_display, ) - total_nodes = len(self.model.graph.node) + nodes = ( + [n for n in self.model.graph.node if "FIFO" not in n.op_type] + if self.performance_sim + else self.model.graph.node + ) + total_nodes = len(nodes) log.info(f"[BuildSimulation] Preparing to build {total_nodes} nodes for the simulation.") futures: dict[int, Future] = {} built_nodes = 0 @@ -856,8 +947,8 @@ def _get_slurm_mem_workers(cpus_alloc: int | None) -> int | None: synth_workers = min( synth_workers, - len(self.model.graph.node), - int(os.environ.get("NUM_DEFAULT_WORKERS", len(self.model.graph.node))), + len(nodes), + int(os.environ.get("NUM_DEFAULT_WORKERS", len(nodes))), ) log.info( "[BuildSimulation] SLURM job detected, using " @@ -878,13 +969,22 @@ def _get_slurm_mem_workers(cpus_alloc: int | None) -> int | None: # Build (stitched IP, cmake, make) all sims in parallel and return paths to # the compiled executables log.info("[BuildSimulation] Starting the build process.") + fifos = self.model.get_nodes_by_op_type("StreamingFIFO") + fifos.extend(self.model.get_nodes_by_op_type("StreamingFIFO_rtl")) + if len(fifos) != 0 and not self.performance_sim: + raise FINNUserError( + "FIFOs detected in model during FIFO sizing. " + "Did you call the steps in the correct order?" + ) with ThreadPoolExecutor(max_workers=synth_workers) as pool: - for i in range(total_nodes): + for i in range(len(self.model.graph.node)): node_name = self.model.graph.node[i].name + if "FIFO" in node_name: + continue futures[i] = pool.submit( _build, i, - total_nodes - 1, + len(self.model.graph.node) - 1, Path(make_build_dir(f"rtlsim_{node_name}_")), ) futures[i].add_done_callback(_callback_progress(node_name)) @@ -923,15 +1023,13 @@ class BuildSimulation(Transformation): If simulation binaries already exist, enter their directory and only re-compile.""" def __init__( - self, - fpgapart: str, - clk_ns: float, - functional_sim: bool, + self, fpgapart: str, clk_ns: float, functional_sim: bool, performance_sim: bool = False ) -> None: """Create a new BuildSimulation transform.""" self.functional_sim = functional_sim self.fpgapart = fpgapart self.clk_ns = clk_ns + self.performance_sim = performance_sim def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Build / compile the model. Modifies the model.""" @@ -941,17 +1039,25 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: needs_rebuild = True sim_binaries = self.model.get_metadata_prop("simulation_binaries") + if self.performance_sim: + log.info("[BuildSimulation] Performance simulation mode enabled.") + # 1. Check if binary paths are saved in the model if sim_binaries is not None: sim_binaries = sim_binaries.split("\n") # 2. Check that the model size hasn't changed since creating the binaries. Otherwise # we should rebuild. - if len(sim_binaries) != len(self.model.graph.node): + nodes = ( + [n for n in self.model.graph.node if "FIFO" not in n.op_type] + if self.performance_sim + else self.model.graph.node + ) + if len(sim_binaries) != len(nodes): log.info( f"[BuildSimulation] Found existing binaries, but number ({len(sim_binaries)}) " f"does not match number of nodes in the graph " - f"({len(self.model.graph.node)}). Rebuilding..." + f"({len(nodes)}). Rebuilding..." ) else: log.info("Existing simulations found. Re-running only CMake/Make..") @@ -963,8 +1069,12 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # This creates both the isolated and connected binaries in one go. if needs_rebuild: log.info("[BuildSimulation] Starting model preparation.") - self._prepare_model() - self.builder = SimulationBuilder(self.model, self.fpgapart, self.clk_ns) + # For rtlsim performance, we assume, that we already have a complete model. + if not self.performance_sim: + self._prepare_model() + self.builder = SimulationBuilder( + self.model, self.fpgapart, self.clk_ns, self.performance_sim + ) with contextlib.suppress(AttributeError): sys.stdout = sys.stdout.console # type: ignore diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 0e3973391e..f0b779d1a9 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -206,9 +206,9 @@ def run( cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[ - sim_name - ] = fifo_cycles_until_first_valid + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -243,9 +243,9 @@ def run( ) = result # Only update if not already collected if sim_name not in fifo_results: - fifo_cycles_until_first_valid_results[ - sim_name - ] = fifo_cycles_until_first_valid + fifo_cycles_until_first_valid_results[sim_name] = ( + fifo_cycles_until_first_valid + ) fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles @@ -512,10 +512,14 @@ def __init__( functional_sim: bool, workers: int | None = None, max_qsrl_depth: int = 256, + performance_sim: bool = False, ) -> None: """Initialize node-connected simulation.""" - super().__init__(model, simulation_type, fpgapart, clk_ns, functional_sim, workers) + super().__init__( + model, simulation_type, fpgapart, clk_ns, functional_sim, workers, performance_sim + ) self.max_qsrl_depth = max_qsrl_depth + self.performance_sim = performance_sim def simulate( self, @@ -533,7 +537,11 @@ def simulate( f"does not match provided simulation type " f"{self.simulation_type}" ) - names = [node.name for node in self.model.graph.node] + names = ( + [node.name for node in self.model.graph.node if "FIFO" not in node.op_type] + if self.performance_sim + else [node.name for node in self.model.graph.node] + ) initial_depth: Any = [[depth]] * len(self.binaries) if isinstance(depth, int) else depth # For BRAM FIFOs (depth > max_qsrl_depth), hardware loses BRAM_FIFO_PIPELINE_OVERHEAD @@ -609,7 +617,6 @@ def __init__( if minimization_orders is not None: self.minimization_orders = minimization_orders else: - # TODO: Set to ALL search orders self.minimization_orders = [MinimizationOrder.NODE_ORDER] self.final_depths: dict[MinimizationOrder, list[list[int]] | None] = dict.fromkeys( @@ -692,6 +699,8 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: ) model = sim.model # TODO:clean up + work_folder = cast("Path", make_build_dir("fifo_results_", True)) + # Create empty table for datapoints that will be collected # First create as a nested dict, since not all data is avilable at the same time # It is then flattened when creating the dataframe, so that node and stream are columns too @@ -716,20 +725,14 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: df_data[node.name][-1][f"simulation_time_{min_order.name}"] = -1 df_data[node.name][-1][f"minimization_iterations_{min_order.name}"] = -1 - # TODO: The final depths contained a lot of -1 (default values). - # Did we need to write the initial depths into there? - # Or in case of minimization skip we likely need to write the values still. - # Running the initial simulation log.info("Running initial node-connected simulation.") initial_fifo_depths, _ = sim.simulate() # Store the initial sizes as a report - initial_sizes_path = ( - Path(self.cfg.output_dir) / "report" / "initial_fifo_sizes_sim_connected.json" - ) + initial_sizes_path = work_folder / "initial_fifo_sizes_sim_connected.json" initial_sizes_path.write_text(json.dumps(initial_fifo_depths, indent=4)) - log.info(f"Wrote initial sizes to: {initial_sizes_path}") + log.debug(f"Wrote initial sizes to: {initial_sizes_path}") # Store initial sizes in dataframe as well for layerdata in initial_fifo_depths: @@ -888,7 +891,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: model = store_fifo_data( model, df, - Path(self.cfg.output_dir) / "report" / "fifo_data.csv", + work_folder / "fifo_data.csv", delete_existing=False, store_html=True, ) @@ -922,19 +925,9 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: if fifo_depths[node_idx][fifo_idx] > self.max_qsrl_depth: bw = bit_widths[node_idx][fifo_idx] blocks = calculate_bram_blocks(fifo_depths[node_idx][fifo_idx], bw) - # if len(fifo_depths[i]) > 1: - # blocks_plus_one = self._get_valid_block_counts( - # blocks + 1, blocks + 1000, bw - # ) - # _, max_d = calculate_bram_depth_range(blocks_plus_one[0], bw) - # else: _, max_d = calculate_bram_depth_range(blocks, bw) fifo_depths[node_idx][fifo_idx] = max_d - log.info("Final FIFO depths:") - for node_idx in range(len(fifo_depths)): - log.info(f"{node_idx}: {fifo_depths[node_idx]}") - log.info("Running final end-to-end validation simulation with minimised FIFO depths...") validation_data, validation_timeout = sim.simulate( fifo_depths, @@ -959,14 +952,14 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: log.info("Final validation simulation passed - minimised depths are correct.") # Write back results. By default write to output_dir / "fifo_config.json" - writeback_path = Path(self.cfg.output_dir) / "fifo_config.json" - assert len(fifo_depths) == len(model.graph.node) + writeback_path = work_folder / "fifo_config.json" json_results = [] for node_idx, node in enumerate(model.graph.node): json_results.append({"node": node.name, "depths": fifo_depths[node_idx]}) with writeback_path.open("w") as f: json.dump(json_results, f) log.info(f"Wrote results back to {writeback_path}") + model.set_metadata_prop("fifo_data", str(writeback_path)) return model, False diff --git a/src/finn/util/config.py b/src/finn/util/config.py index 7055f54493..cb86cc2287 100644 --- a/src/finn/util/config.py +++ b/src/finn/util/config.py @@ -1,3 +1,4 @@ +"""Utility functions for extracting model configuration.""" ############################################################################ # Copyright (C) 2020-2022, Xilinx, Inc. # Copyright (C) 2025, Advanced Micro Devices, Inc. @@ -12,15 +13,24 @@ # https://github.com/fastmachinelearning/qonnx/blob/ # abb9eb12e0248014a805f505aacfaeb14d42409a/src/qonnx/util/config.py - +from numpy import typing as npt import json +from pathlib import Path import onnx from qonnx.custom_op.registry import getCustomOp, is_custom_op +from typing import TYPE_CHECKING +import contextlib + +if TYPE_CHECKING: + from qonnx.core.modelwrapper import ModelWrapper + # update this code to handle export configs from subgraphs # where the subgraph is found in a node's attribute as a graph type -def extract_model_config(model, subgraph_hier, attr_names_to_extract): +def extract_model_config( + model: "ModelWrapper", subgraph_hier: str | None, attr_names_to_extract: list[str] +) -> dict[str, dict[str, int | float | str | bool | npt.NDArray | list[str | int | float] | None]]: """Create a dictionary with layer name -> attribute mappings extracted from the model. The created dictionary can be later applied on a model with finn.transform.general.ApplyConfig. @@ -28,21 +38,20 @@ def extract_model_config(model, subgraph_hier, attr_names_to_extract): Nodes in subgraphs are prefixed with their parent hierarchy using '_' as separator. For example, a node 'Conv_0' inside a subgraph of node 'IfNode_0' will be exported as 'IfNode_0_Conv_0' in the config.""" - cfg = dict() - cfg["Defaults"] = dict() + cfg = {} + cfg["Defaults"] = {} for n in model.graph.node: new_hier = n.name if subgraph_hier is None else str(subgraph_hier) + "_" + n.name # Check if this is a custom op and prepare to extract attributes + layer_dict = {} is_custom = is_custom_op(n.domain, n.op_type) if is_custom: oi = getCustomOp(n) - layer_dict = dict() + layer_dict = {} for attr in attr_names_to_extract: - try: + with contextlib.suppress(AttributeError): layer_dict[attr] = oi.get_nodeattr(attr) - except AttributeError: - pass # Process node attributes - handle both subgraphs and extractable attributes for attr in n.attribute: @@ -65,11 +74,13 @@ def extract_model_config(model, subgraph_hier, attr_names_to_extract): return cfg -def extract_model_config_to_json(model, json_filename, attr_names_to_extract): +def extract_model_config_to_json( + model: "ModelWrapper", json_filename: Path, attr_names_to_extract: list[str] +) -> None: """Create a json file with layer name -> attribute mappings extracted from the model. The created json file can be later applied on a model with finn.transform.general.ApplyConfig.""" - with open(json_filename, "w") as f: + with json_filename.open("w") as f: json.dump( extract_model_config( model, subgraph_hier=None, attr_names_to_extract=attr_names_to_extract @@ -79,11 +90,13 @@ def extract_model_config_to_json(model, json_filename, attr_names_to_extract): ) -def extract_model_config_consolidate_shuffles(model, output_file, hw_attrs): - """Export flow that takes into consideration how Shuffle operations have been decomposed""" +def extract_model_config_consolidate_shuffles( + model: "ModelWrapper", output_file: Path, hw_attrs: list[str] +) -> None: + """Export flow that takes into consideration how Shuffle operations have been decomposed.""" extract_model_config_to_json(model, output_file, hw_attrs) - with open(output_file) as f: + with output_file.open() as f: config = json.load(f) shuffle_configs = {} @@ -108,5 +121,5 @@ def extract_model_config_consolidate_shuffles(model, output_file, hw_attrs): config.update(shuffle_configs) - with open(output_file, "w") as f: + with output_file.open("w") as f: json.dump(config, f, indent=2) diff --git a/tests/fpgadataflow/test_simulation_build.py b/tests/fpgadataflow/test_simulation_build.py index 29cf92b174..09618f8280 100644 --- a/tests/fpgadataflow/test_simulation_build.py +++ b/tests/fpgadataflow/test_simulation_build.py @@ -12,15 +12,13 @@ from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.util.basic import qonnx_make_model -from typing import Protocol +from typing import Protocol, cast class _SimulationBuilderProtocol(Protocol): - def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: - ... + def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: ... - def _isolated_node_model(self, by_node: int | str) -> ModelWrapper: - ... + def _isolated_node_model(self, by_node: int | str) -> ModelWrapper: ... def _import_simulation_build_types() -> tuple[type[_SimulationBuilderProtocol], type[Exception]]: @@ -71,6 +69,12 @@ def _vi(name: str, shape: list[int]) -> ValueInfoProto: return helper.make_tensor_value_info(name, TensorProto.FLOAT, shape) +def _add_value_info(value_info: list[ValueInfoProto], name: str, shape: list[int]) -> None: + if any(vi.name == name for vi in value_info): + return + value_info.append(_vi(name, shape)) + + def _make_dwc(name: str, inp: str, out: str, shape: list[int]) -> NodeProto: return helper.make_node( "StreamingDataWidthConverter", @@ -80,14 +84,31 @@ def _make_dwc(name: str, inp: str, out: str, shape: list[int]) -> NodeProto: backend="fpgadataflow", inShape=list(shape), outShape=list(shape), - inWidth=8, - outWidth=8, - dataType="INT8", + inWidth=9, + outWidth=9, + dataType="INT9", preferred_impl_style="rtl", name=name, ) +def _make_fifo(name: str, inp: str, out: str, shape: list[int]) -> NodeProto: + return helper.make_node( + "StreamingFIFO", + [inp], + [out], + domain="finn.custom_op.fpgadataflow", + backend="fpgadataflow", + depth=32, + folded_shape=shape, + normal_shape=shape, + dataType="INT9", + impl_style="rtl", + ram_style="block", + name=name, + ) + + def _make_add_hls(name: str, lhs: str, rhs: str, out: str, shape: list[int]) -> NodeProto: return helper.make_node( "ElementwiseAdd_hls", @@ -99,8 +120,8 @@ def _make_add_hls(name: str, lhs: str, rhs: str, out: str, shape: list[int]) -> lhs_shape=list(shape), rhs_shape=list(shape), out_shape=list(shape), - lhs_dtype="INT8", - rhs_dtype="INT8", + lhs_dtype="INT9", + rhs_dtype="INT9", out_dtype="INT9", lhs_style="input", rhs_style="input", @@ -130,31 +151,199 @@ def _make_mvau_rtl(name: str, inp: str, weights: str, out: str) -> NodeProto: ) +def _make_duplicate_stream( + name: str, + inp: str, + out_list: list[str], + shape: list[int], + num_outputs: int = 2, + num_channels: int = 4, + pe: int = 1, + data_type: str = "INT8", +) -> NodeProto: + return helper.make_node( + "DuplicateStreams", + [inp], + out_list, + domain="finn.custom_op.fpgadataflow", + backend="fpgadataflow", + NumChannels=num_channels, + NumOutputStreams=num_outputs, + PE=pe, + inputDataType=data_type, + numInputVectors=shape, + preferred_impl_style="hls", + cpp_interface="hls_vector", + hls_style="freerunning", + name=name, + ) + + def _wrap_model(graph: GraphProto) -> ModelWrapper: - return ModelWrapper(qonnx_make_model(graph, producer_name="simulation-build-test")) + model = ModelWrapper(qonnx_make_model(graph, producer_name="simulation-build-test")) + # model = model.transform(InferDataLayouts()) + # model = model.transform(InferDataTypes()) + # # model = model.transform(InferShapes()) + return model -def _build_unary_target_model(pre_binary: bool = False, succ_binary: bool = False) -> ModelWrapper: - shape = [1, 4] +def _build_unary_target_model( + pre_binary: bool = False, + succ_binary: bool = False, + fifos: bool = False, + include_succ_node: bool = True, + extra_post_nodes: int = 0, + fifo_pre: bool | None = None, + fifo_between: bool | None = None, + fifo_after: bool | None = None, + fifo_between_depth: int | None = None, +) -> ModelWrapper: + shape = [1, 32, 32, 3] + if ( + include_succ_node + and extra_post_nodes == 0 + and fifo_pre is None + and fifo_between is None + and fifo_after is None + and fifo_between_depth is None + ): + nodes = [] + graph_inputs = [] + value_info = [_vi("target_in", shape), _vi("target_out", shape)] + + # Add Node in front of dwc + if pre_binary: + graph_inputs.extend([_vi("pre_in0", shape), _vi("pre_in1", shape)]) + if fifos: + nodes.append(_make_fifo("FIFO_0", "pre_in0", "fifo_out0", shape)) + _add_value_info(value_info, "fifo_out0", shape) + node_in_0 = "fifo_out0" + nodes.append(_make_fifo("FIFO_1", "pre_in1", "fifo_out1", shape)) + _add_value_info(value_info, "fifo_out1", shape) + node_in_1 = "fifo_out1" + else: + node_in_0 = "pre_in0" + node_in_1 = "pre_in1" + nodes.append(_make_add_hls("pre_add", node_in_0, node_in_1, "target_in", shape)) + else: + graph_inputs.append(_vi("pre_in0", shape)) + if fifos: + nodes.append(_make_fifo("FIFO_0", "pre_in0", "fifo_out0", shape)) + _add_value_info(value_info, "fifo_out0", shape) + node_in_0 = "fifo_out0" + else: + node_in_0 = "pre_in0" + nodes.append(_make_dwc("pre_dwc", node_in_0, "target_in", shape)) + + nodes.append(_make_dwc("target_dwc", "target_in", "target_out", shape)) + + if fifos: + nodes.append(_make_fifo("FIFOOut_0", "target_out", "fifoOut_out0", shape)) + _add_value_info(value_info, "fifoOut_out0", shape) + node_out_0 = "fifoOut_out0" + else: + node_out_0 = "target_out" + + # Add Node after dwc + if succ_binary: + graph_inputs.append(_vi("succ_in1", shape)) + nodes.append(_make_add_hls("succ_add", node_out_0, "succ_in1", "graph_out", shape)) + _add_value_info(value_info, "graph_out", shape) + else: + nodes.append(_make_dwc("succ_dwc", node_out_0, "graph_out", shape)) + _add_value_info(value_info, "graph_out", shape) + + if fifos: + nodes.append(_make_fifo("FIFOOut_1", "graph_out", "fifoOut_out1", shape)) + _add_value_info(value_info, "fifoOut_out1", shape) + node_out_1 = "fifoOut_out1" + else: + node_out_1 = "graph_out" + + graph_outputs = [_vi(node_out_1, shape)] + + reserved_names = {vi.name for vi in graph_inputs} | {vi.name for vi in graph_outputs} + value_info = [vi for vi in value_info if vi.name not in reserved_names] + + graph = helper.make_graph( + nodes=nodes, + name="unary_target_graph", + inputs=graph_inputs, + outputs=graph_outputs, + value_info=value_info, + ) + return _wrap_model(graph) + nodes = [] graph_inputs = [] - graph_outputs = [_vi("graph_out", shape)] value_info = [_vi("target_in", shape), _vi("target_out", shape)] + use_fifo_pre = fifos if fifo_pre is None else fifo_pre + use_fifo_between = fifos if fifo_between is None else fifo_between + use_fifo_after = fifos if fifo_after is None else fifo_after + fifo_between_count = ( + fifo_between_depth if fifo_between_depth is not None else (1 if use_fifo_between else 0) + ) + fifo_index = 0 + + def _append_fifo(prefix: str, inp: str) -> str: + nonlocal fifo_index + out_name = f"{prefix}_out{fifo_index}" + nodes.append(_make_fifo(f"{prefix}_{fifo_index}", inp, out_name, shape)) + _add_value_info(value_info, out_name, shape) + fifo_index += 1 + return out_name + + def _append_fifo_chain(prefix: str, inp: str, count: int) -> str: + current = inp + for _ in range(count): + current = _append_fifo(prefix, current) + return current if pre_binary: graph_inputs.extend([_vi("pre_in0", shape), _vi("pre_in1", shape)]) - nodes.append(_make_add_hls("pre_add", "pre_in0", "pre_in1", "target_in", shape)) + node_in_0 = "pre_in0" + node_in_1 = "pre_in1" + if use_fifo_pre: + node_in_0 = _append_fifo("FIFO_pre", node_in_0) + node_in_1 = _append_fifo("FIFO_pre", node_in_1) + nodes.append(_make_add_hls("pre_add", node_in_0, node_in_1, "target_in", shape)) else: graph_inputs.append(_vi("pre_in0", shape)) - nodes.append(_make_dwc("pre_dwc", "pre_in0", "target_in", shape)) + node_in_0 = "pre_in0" + if use_fifo_pre: + node_in_0 = _append_fifo("FIFO_pre", node_in_0) + nodes.append(_make_dwc("pre_dwc", node_in_0, "target_in", shape)) nodes.append(_make_dwc("target_dwc", "target_in", "target_out", shape)) + current_out = "target_out" - if succ_binary: - graph_inputs.append(_vi("succ_in1", shape)) - nodes.append(_make_add_hls("succ_add", "target_out", "succ_in1", "graph_out", shape)) - else: - nodes.append(_make_dwc("succ_dwc", "target_out", "graph_out", shape)) + if fifo_between_count > 0: + current_out = _append_fifo_chain("FIFO_mid", current_out, fifo_between_count) + + if include_succ_node: + if succ_binary: + graph_inputs.append(_vi("succ_in1", shape)) + nodes.append(_make_add_hls("succ_add", current_out, "succ_in1", "post_0_out", shape)) + else: + nodes.append(_make_dwc("succ_dwc", current_out, "post_0_out", shape)) + _add_value_info(value_info, "post_0_out", shape) + current_out = "post_0_out" + + for idx in range(extra_post_nodes): + if fifo_between_count > 0: + current_out = _append_fifo_chain("FIFO_mid", current_out, fifo_between_count) + post_name = f"post_out_{idx}" + nodes.append(_make_dwc(f"post_dwc_{idx}", current_out, post_name, shape)) + _add_value_info(value_info, post_name, shape) + current_out = post_name + + if use_fifo_after: + current_out = _append_fifo("FIFO_out", current_out) + + graph_outputs = [_vi(current_out, shape)] + + reserved_names = {vi.name for vi in graph_inputs} | {vi.name for vi in graph_outputs} + value_info = [vi for vi in value_info if vi.name not in reserved_names] graph = helper.make_graph( nodes=nodes, @@ -167,31 +356,142 @@ def _build_unary_target_model(pre_binary: bool = False, succ_binary: bool = Fals def _build_binary_target_model( - initializer_side: str | None = None, mlo: bool = False + initializer_side: str | None = None, + mlo: bool = False, + fifos: bool = False, + include_succ_node: bool = True, + extra_post_nodes: int = 0, + fifo_pre: bool | None = None, + fifo_between: bool | None = None, + fifo_after: bool | None = None, + fifo_between_depth: int | None = None, ) -> ModelWrapper: - shape = [1, 4] + shape = [1, 32, 32, 3] + if ( + include_succ_node + and extra_post_nodes == 0 + and fifo_pre is None + and fifo_between is None + and fifo_after is None + and not fifos + and fifo_between_depth is None + ): + lhs_name = "lhs_in" + rhs_name = "rhs_in" + nodes = [_make_add_hls("target_add", lhs_name, rhs_name, "target_out", shape)] + nodes.append(_make_dwc("succ_dwc", "target_out", "graph_out", shape)) + + graph_outputs = [_vi("graph_out", shape)] + graph_inputs = [] + if initializer_side != "lhs": + graph_inputs.append(_vi(lhs_name, shape)) + if initializer_side != "rhs": + graph_inputs.append(_vi(rhs_name, shape)) + + value_info = [_vi("target_out", shape), _vi("graph_out", shape)] + if initializer_side == "lhs": + value_info.append(_vi(lhs_name, shape)) + if initializer_side == "rhs": + value_info.append(_vi(rhs_name, shape)) + + reserved_names = {vi.name for vi in graph_inputs} | {vi.name for vi in graph_outputs} + value_info = [vi for vi in value_info if vi.name not in reserved_names] + + graph = helper.make_graph( + nodes=nodes, + name="binary_target_graph", + inputs=graph_inputs, + outputs=graph_outputs, + value_info=value_info, + ) + model = _wrap_model(graph) + + if initializer_side is not None: + init_name = lhs_name if initializer_side == "lhs" else rhs_name + model.set_initializer(init_name, np.ones(shape, dtype=np.float32)) + + if mlo: + model.set_metadata_prop("is_mlo", "1") + mlo_inputs = [rhs_name] if initializer_side == "rhs" else [lhs_name] + model.set_metadata_prop("mlo_input_parameter_names", str(mlo_inputs)) + + return model + lhs_name = "lhs_in" rhs_name = "rhs_in" - nodes = [_make_add_hls("target_add", lhs_name, rhs_name, "target_out", shape)] - nodes.append(_make_dwc("succ_dwc", "target_out", "graph_out", shape)) - + nodes = [] graph_inputs = [] + value_info = [_vi("target_out", shape)] + use_fifo_pre = fifos if fifo_pre is None else fifo_pre + use_fifo_between = fifos if fifo_between is None else fifo_between + use_fifo_after = fifos if fifo_after is None else fifo_after + fifo_between_count = ( + fifo_between_depth if fifo_between_depth is not None else (1 if use_fifo_between else 0) + ) + fifo_index = 0 + + def _append_fifo(prefix: str, inp: str) -> str: + nonlocal fifo_index + out_name = f"{prefix}_out{fifo_index}" + nodes.append(_make_fifo(f"{prefix}_{fifo_index}", inp, out_name, shape)) + _add_value_info(value_info, out_name, shape) + fifo_index += 1 + return out_name + + def _append_fifo_chain(prefix: str, inp: str, count: int) -> str: + current = inp + for _ in range(count): + current = _append_fifo(prefix, current) + return current + + lhs_input = lhs_name + rhs_input = rhs_name if initializer_side != "lhs": graph_inputs.append(_vi(lhs_name, shape)) + if use_fifo_pre: + lhs_input = _append_fifo("FIFO_pre", lhs_input) + else: + value_info.append(_vi(lhs_name, shape)) + if initializer_side != "rhs": graph_inputs.append(_vi(rhs_name, shape)) - - value_info = [_vi("target_out", shape)] - if initializer_side == "lhs": - value_info.append(_vi(lhs_name, shape)) - if initializer_side == "rhs": + if use_fifo_pre: + rhs_input = _append_fifo("FIFO_pre", rhs_input) + else: value_info.append(_vi(rhs_name, shape)) + nodes.append(_make_add_hls("target_add", lhs_input, rhs_input, "target_out", shape)) + current_out = "target_out" + + if fifo_between_count > 0: + current_out = _append_fifo_chain("FIFO_mid", current_out, fifo_between_count) + + if include_succ_node: + nodes.append(_make_dwc("succ_dwc", current_out, "post_0_out", shape)) + _add_value_info(value_info, "post_0_out", shape) + current_out = "post_0_out" + + for idx in range(extra_post_nodes): + if fifo_between_count > 0: + current_out = _append_fifo_chain("FIFO_mid", current_out, fifo_between_count) + post_name = f"post_out_{idx}" + nodes.append(_make_dwc(f"post_dwc_{idx}", current_out, post_name, shape)) + _add_value_info(value_info, post_name, shape) + current_out = post_name + + if use_fifo_after: + current_out = _append_fifo("FIFO_out", current_out) + + graph_outputs = [_vi(current_out, shape)] + + reserved_names = {vi.name for vi in graph_inputs} | {vi.name for vi in graph_outputs} + value_info = [vi for vi in value_info if vi.name not in reserved_names] + graph = helper.make_graph( nodes=nodes, name="binary_target_graph", inputs=graph_inputs, - outputs=[_vi("graph_out", shape)], + outputs=graph_outputs, value_info=value_info, ) model = _wrap_model(graph) @@ -208,6 +508,77 @@ def _build_binary_target_model( return model +def _build_duplicate_target_model( + fifos: bool = False, + branch_nodes: bool = False, + fifo_pre: bool | None = None, + fifo_between: bool | None = None, + fifo_after: bool | None = None, + fifo_between_depth: int | None = None, +) -> ModelWrapper: + shape = [1, 32, 32, 3] + nodes = [] + graph_inputs = [_vi("dup_in", shape)] + value_info = [] + use_fifo_pre = fifos if fifo_pre is None else fifo_pre + use_fifo_between = fifos if fifo_between is None else fifo_between + use_fifo_after = fifos if fifo_after is None else fifo_after + fifo_between_count = ( + fifo_between_depth if fifo_between_depth is not None else (1 if use_fifo_between else 0) + ) + + current_in = "dup_in" + if use_fifo_pre: + nodes.append(_make_fifo("FIFO_pre_0", current_in, "fifo_pre_out0", shape)) + _add_value_info(value_info, "fifo_pre_out0", shape) + current_in = "fifo_pre_out0" + + dup_outs = ["dup_out0", "dup_out1"] + nodes.append(_make_duplicate_stream("dup_stream", current_in, dup_outs, shape, num_outputs=2)) + _add_value_info(value_info, dup_outs[0], shape) + _add_value_info(value_info, dup_outs[1], shape) + + branch_outputs = [] + for idx in range(2): + branch_in = dup_outs[idx] + if fifo_between_count > 0: + for chain_idx in range(fifo_between_count): + fifo_name = f"FIFO_branch_{idx}_{chain_idx}" + fifo_out = f"fifo_branch_{idx}_{chain_idx}" + nodes.append(_make_fifo(fifo_name, branch_in, fifo_out, shape)) + _add_value_info(value_info, fifo_out, shape) + branch_in = fifo_out + + if branch_nodes: + node_out = f"branch_out_{idx}" + nodes.append(_make_dwc(f"branch_dwc_{idx}", branch_in, node_out, shape)) + _add_value_info(value_info, node_out, shape) + branch_in = node_out + + if use_fifo_after: + fifo_name = f"FIFO_branch_{idx}_after" + fifo_out = f"fifo_branch_{idx}_after" + nodes.append(_make_fifo(fifo_name, branch_in, fifo_out, shape)) + _add_value_info(value_info, fifo_out, shape) + branch_in = fifo_out + + branch_outputs.append(branch_in) + + graph_outputs = [_vi(branch_outputs[0], shape), _vi(branch_outputs[1], shape)] + + reserved_names = {vi.name for vi in graph_inputs} | {vi.name for vi in graph_outputs} + value_info = [vi for vi in value_info if vi.name not in reserved_names] + + graph = helper.make_graph( + nodes=nodes, + name="duplicate_target_graph", + inputs=graph_inputs, + outputs=graph_outputs, + value_info=value_info, + ) + return _wrap_model(graph) + + def _build_mvau_target_model(mlo: bool = False) -> ModelWrapper: shape_ifm = [1, 1, 1, 4] shape_out = [1, 1, 1, 4] @@ -234,6 +605,7 @@ def _build_mvau_target_model(mlo: bool = False) -> ModelWrapper: def _assert_isolated_model( isolated_model: ModelWrapper, + source_model: ModelWrapper | None, target_name: str, expected_graph_inputs: list[str], expected_graph_outputs: list[str], @@ -241,13 +613,38 @@ def _assert_isolated_model( expected_input_node_flag: bool, expected_target_inputs: list[str], expected_target_outputs: list[str], + expected_output_node_flag: bool = False, ) -> None: + reference_model = isolated_model if source_model is None else source_model + + def _resolve_through_fifos(tensor_name: str) -> str: + name = tensor_name + if name.endswith("_dummy"): + name = name[: -len("_dummy")] + while True: + producer = reference_model.find_producer(name) + if producer is None: + return name + if producer.op_type == "StreamingFIFO" or "FIFO" in producer.name: + name = producer.input[0] + continue + return name + graph = isolated_model.graph graph_input_names = [x.name for x in graph.input] graph_output_names = [x.name for x in graph.output] - assert graph_input_names == expected_graph_inputs - assert graph_output_names == expected_graph_outputs + resolved_graph_inputs = [_resolve_through_fifos(name) for name in graph_input_names] + resolved_graph_outputs = [_resolve_through_fifos(name) for name in graph_output_names] + resolved_expected_graph_inputs = [ + _resolve_through_fifos(name) for name in expected_graph_inputs + ] + resolved_expected_graph_outputs = [ + _resolve_through_fifos(name) for name in expected_graph_outputs + ] + + assert resolved_graph_inputs == resolved_expected_graph_inputs + assert resolved_graph_outputs == resolved_expected_graph_outputs input_dummy_nodes = [ n for n in graph.node if n.op_type == "RemoveDataPath_rtl" and "_input_dummy_" in n.name @@ -259,14 +656,24 @@ def _assert_isolated_model( assert len(target_nodes) == 1 assert len(input_dummy_nodes) == len(expected_graph_inputs) - assert len(output_dummy_nodes) == 1 + assert len(output_dummy_nodes) == len(expected_graph_outputs) initializer_names = [x.name for x in graph.initializer] assert initializer_names == expected_initializer_inputs target_node = target_nodes[0] - assert list(target_node.input) == expected_target_inputs - assert list(target_node.output) == expected_target_outputs + resolved_target_inputs = [_resolve_through_fifos(inp) for inp in target_node.input] + resolved_target_outputs = [_resolve_through_fifos(outp) for outp in target_node.output] + resolved_expected_target_inputs = [ + _resolve_through_fifos(inp) for inp in expected_target_inputs + ] + resolved_expected_target_outputs = [ + _resolve_through_fifos(outp) for outp in expected_target_outputs + ] + + assert resolved_target_inputs == resolved_expected_target_inputs + assert resolved_target_outputs == resolved_expected_target_outputs + target_dummy_inputs = [inp for inp in target_node.input if inp.endswith("_dummy")] target_initializer_inputs = [inp for inp in target_node.input if inp in initializer_names] assert len(target_dummy_inputs) == len(expected_graph_inputs) @@ -275,13 +682,39 @@ def _assert_isolated_model( assert isolated_model.get_metadata_prop("predecessors") == str(expected_graph_inputs) assert isolated_model.get_metadata_prop("successors") == str(graph_output_names) assert isolated_model.get_metadata_prop("input_node") == str(expected_input_node_flag).lower() - assert isolated_model.get_metadata_prop("output_node") == "false" + assert isolated_model.get_metadata_prop("output_node") == str(expected_output_node_flag).lower() def _isolate_node_model(builder: _SimulationBuilderProtocol, by_node: int | str) -> ModelWrapper: return builder._isolated_node_model(by_node) # noqa: SLF001 +def _assert_isolated_models_match( + isolated_no_fifo: ModelWrapper, + isolated_fifo: ModelWrapper, + target_name: str, +) -> None: + assert [inp.name for inp in isolated_no_fifo.graph.input] == [ + inp.name for inp in isolated_fifo.graph.input + ] + assert [out.name for out in isolated_no_fifo.graph.output] == [ + out.name for out in isolated_fifo.graph.output + ] + + def _node_names(model: ModelWrapper, op_type: str) -> list[str]: + return [node.name for node in model.graph.node if node.op_type == op_type] + + assert _node_names(isolated_no_fifo, "RemoveDataPath_rtl") == _node_names( + isolated_fifo, "RemoveDataPath_rtl" + ) + + node_no_fifo = next(node for node in isolated_no_fifo.graph.node if node.name == target_name) + node_fifo = next(node for node in isolated_fifo.graph.node if node.name == target_name) + assert node_no_fifo.op_type == node_fifo.op_type + assert list(node_no_fifo.input) == list(node_fifo.input) + assert list(node_no_fifo.output) == list(node_fifo.output) + + @pytest.mark.parametrize( "pre_binary,succ_binary", [ @@ -301,6 +734,7 @@ def test_isolated_node_model_unary_target_with_varied_other_node_inputs( _assert_isolated_model( isolated_model=isolated, + source_model=None, target_name="target_dwc", expected_graph_inputs=["target_in"], expected_graph_outputs=["target_out"], @@ -321,6 +755,7 @@ def test_isolated_node_model_select_by_name() -> None: _assert_isolated_model( isolated_model=isolated, + source_model=None, target_name="target_dwc", expected_graph_inputs=["target_in"], expected_graph_outputs=["target_out"], @@ -357,6 +792,7 @@ def test_isolated_node_model_binary_target_with_dynamic_and_fixed_inputs( _assert_isolated_model( isolated_model=isolated, + source_model=None, target_name="target_add", expected_graph_inputs=expected_graph_inputs, expected_graph_outputs=["target_out"], @@ -367,6 +803,242 @@ def test_isolated_node_model_binary_target_with_dynamic_and_fixed_inputs( ) +@pytest.mark.parametrize("fifo_between_depth", [1, 2]) +def test_isolated_node_model_unary_succ_fifo_chain_transparency( + fifo_between_depth: int, +) -> None: + """FIFOs between target and successor are transparent for isolation checks.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_unary_target_model( + pre_binary=False, + succ_binary=False, + include_succ_node=True, + fifo_between=True, + fifo_between_depth=fifo_between_depth, + ) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + isolated = _isolate_node_model(builder, "succ_dwc") + + _assert_isolated_model( + isolated_model=isolated, + source_model=model, + target_name="succ_dwc", + expected_graph_inputs=["target_out"], + expected_graph_outputs=["post_0_out"], + expected_initializer_inputs=[], + expected_input_node_flag=False, + expected_target_inputs=["target_out_dummy"], + expected_target_outputs=["post_0_out_dummy"], + expected_output_node_flag=True, + ) + + +@pytest.mark.parametrize("fifo_between_depth", [1, 2]) +def test_isolated_node_model_binary_succ_fifo_chain_transparency( + fifo_between_depth: int, +) -> None: + """Binary successor nodes see FIFO chains as transparent.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_binary_target_model( + initializer_side=None, + mlo=False, + include_succ_node=True, + fifo_between=True, + fifo_between_depth=fifo_between_depth, + ) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + isolated = _isolate_node_model(builder, "succ_dwc") + + _assert_isolated_model( + isolated_model=isolated, + source_model=model, + target_name="succ_dwc", + expected_graph_inputs=["target_out"], + expected_graph_outputs=["post_0_out"], + expected_initializer_inputs=[], + expected_input_node_flag=False, + expected_target_inputs=["target_out_dummy"], + expected_target_outputs=["post_0_out_dummy"], + expected_output_node_flag=True, + ) + + +@pytest.mark.parametrize( + "initializer_side,expected_graph_inputs,expected_initializer_inputs,expected_target_inputs", + [ + (None, ["lhs_in", "rhs_in"], [], ["lhs_in_dummy", "rhs_in_dummy"]), + ("rhs", ["lhs_in"], ["rhs_in"], ["lhs_in_dummy", "rhs_in"]), + ], +) +def test_isolated_node_model_binary_target_fifo_pre_transparency( + initializer_side: str | None, + expected_graph_inputs: list[str], + expected_initializer_inputs: list[str], + expected_target_inputs: list[str], +) -> None: + """FIFO chains before the target are transparent for inputs.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_binary_target_model( + initializer_side=initializer_side, + mlo=False, + include_succ_node=True, + fifo_pre=True, + fifo_between=True, + fifo_between_depth=2, + ) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + model.save("/scratch/pc2-mitarbeiter/linusjun/finn-tmp/source_model.onnx") + + isolated = _isolate_node_model(builder, "target_add") + + _assert_isolated_model( + isolated_model=isolated, + source_model=model, + target_name="target_add", + expected_graph_inputs=expected_graph_inputs, + expected_graph_outputs=["target_out"], + expected_initializer_inputs=expected_initializer_inputs, + expected_input_node_flag=True, + expected_target_inputs=expected_target_inputs, + expected_target_outputs=["target_out_dummy"], + expected_output_node_flag=False, + ) + + isolated = _isolate_node_model(builder, "succ_dwc") + + _assert_isolated_model( + isolated_model=isolated, + source_model=model, + target_name="succ_dwc", + expected_graph_inputs=["target_out"], + expected_graph_outputs=["post_0_out"], + expected_initializer_inputs=[], + expected_input_node_flag=False, + expected_target_inputs=["target_out_dummy"], + expected_target_outputs=["post_0_out_dummy"], + expected_output_node_flag=True, + ) + + +@pytest.mark.parametrize( + "config", + [ + { + "fifos": False, + "branch_nodes": False, + "fifo_pre": False, + "fifo_between": False, + "fifo_after": False, + "fifo_between_depth": None, + "expected_input_node": True, + "expected_output_node": True, + }, + { + "fifos": False, + "branch_nodes": False, + "fifo_pre": True, + "fifo_between": True, + "fifo_after": False, + "fifo_between_depth": 2, + "expected_input_node": True, + "expected_output_node": True, + }, + { + "fifos": False, + "branch_nodes": True, + "fifo_pre": False, + "fifo_between": False, + "fifo_after": False, + "fifo_between_depth": None, + "expected_input_node": True, + "expected_output_node": False, + }, + { + "fifos": False, + "branch_nodes": True, + "fifo_pre": True, + "fifo_between": True, + "fifo_after": True, + "fifo_between_depth": 2, + "expected_input_node": True, + "expected_output_node": False, + }, + ], +) +def test_isolated_node_model_duplicate_stream_fifo_transparency( + config: dict[str, object], +) -> None: + """DuplicateStreams models behave identically with FIFO chains present.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model = _build_duplicate_target_model( + fifos=bool(config["fifos"]), + branch_nodes=bool(config["branch_nodes"]), + fifo_pre=bool(config["fifo_pre"]), + fifo_between=bool(config["fifo_between"]), + fifo_after=bool(config["fifo_after"]), + fifo_between_depth=cast("int | None", config["fifo_between_depth"]), + ) + builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + + isolated = _isolate_node_model(builder, "dup_stream") + + _assert_isolated_model( + isolated_model=isolated, + source_model=model, + target_name="dup_stream", + expected_graph_inputs=["dup_in"], + expected_graph_outputs=["dup_out0", "dup_out1"], + expected_initializer_inputs=[], + expected_input_node_flag=bool(config["expected_input_node"]), + expected_target_inputs=["dup_in_dummy"], + expected_target_outputs=["dup_out0_dummy", "dup_out1_dummy"], + expected_output_node_flag=bool(config["expected_output_node"]), + ) + + +def test_isolated_node_model_fifo_transparency_nodes() -> None: + """Compare isolated node inputs/outputs between FIFO and non-FIFO topologies for all nodes.""" + simulation_builder_cls, _ = _import_simulation_build_types() + model_no_fifo = _build_unary_target_model( + pre_binary=True, + succ_binary=False, + include_succ_node=True, + extra_post_nodes=1, + fifo_pre=False, + fifo_between=False, + fifo_after=False, + ) + model_fifo = _build_unary_target_model( + pre_binary=True, + succ_binary=False, + include_succ_node=True, + extra_post_nodes=1, + fifo_pre=True, + fifo_between=True, + fifo_between_depth=2, + fifo_after=True, + ) + + builder_no_fifo = simulation_builder_cls(model_no_fifo, "xc7z020clg400-1", 5.0) + builder_fifo = simulation_builder_cls(model_fifo, "xc7z020clg400-1", 5.0) + + node_names = [node.name for node in model_no_fifo.graph.node if node.op_type != "StreamingFIFO"] + + for node_name in node_names: + print(f"Comparing isolated models for node '{node_name}'...") + isolated_no_fifo = _isolate_node_model(builder_no_fifo, node_name) + isolated_fifo = _isolate_node_model(builder_fifo, node_name) + + _assert_isolated_models_match( + isolated_no_fifo=isolated_no_fifo, + isolated_fifo=isolated_fifo, + target_name=node_name, + ) + + def test_isolated_node_model_elementwise_sets_const_style_for_mlo_initializer() -> None: """Elementwise ops set lhs_style/rhs_style=const for remapped MLO initializer inputs.""" simulation_builder_cls, _ = _import_simulation_build_types() diff --git a/tests/util/test_config.py b/tests/util/test_config.py index 4d6e638e1b..5920096afe 100644 --- a/tests/util/test_config.py +++ b/tests/util/test_config.py @@ -1,3 +1,5 @@ +"""Tests for model config extraction and application utilities.""" + ############################################################################ # Copyright (C) 2025, Advanced Micro Devices, Inc. # All rights reserved. @@ -6,17 +8,18 @@ # ############################################################################ -import pytest +import os +from pathlib import Path +from typing import Any import onnx -import os +import pytest from onnxscript import BOOL, FLOAT from onnxscript import opset13 as op from onnxscript import script from onnxscript.values import Opset from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp -from typing import Any, Dict from finn.transformation.general import ApplyConfig from finn.util.config import extract_model_config, extract_model_config_to_json @@ -30,7 +33,7 @@ def main_graph_fn( main_inp: FLOAT[1, 28, 28, 1], condition: BOOL, nested_condition: BOOL ) -> FLOAT[1, 4, 4, 144]: - """Main graph with nested if statement in else branch.""" + """Build a main graph with a nested if statement in the else branch.""" im2col_0 = qops.Im2Col( main_inp, stride=[1, 1], @@ -80,7 +83,7 @@ def main_graph_fn( return main_out -def build_expected_config_from_node(node: onnx.NodeProto, prefix="") -> Dict[str, Any]: +def build_expected_config_from_node(node: onnx.NodeProto, prefix: str = "") -> dict[str, Any]: """Build expected config dictionary from a given ONNX node.""" custom_op = getCustomOp(node) attrs = {} @@ -89,9 +92,8 @@ def build_expected_config_from_node(node: onnx.NodeProto, prefix="") -> Dict[str return {prefix + node.name: attrs} -def make_im2col_test_model(): +def make_im2col_test_model() -> tuple[ModelWrapper, dict[Any, Any]]: """Create a simple ONNX model with a single Im2Col node.""" - model_proto = main_graph_fn.to_model_proto() im2col_node = model_proto.graph.node[0] @@ -136,9 +138,8 @@ def make_im2col_test_model(): @pytest.mark.util -def test_extract_model_config(): +def test_extract_model_config() -> None: """Test extraction of model config from models with and without subgraphs.""" - model, expected_config = make_im2col_test_model() attrs_to_extract = ["kernel_size", "stride", "pad_amount", "input_shape"] @@ -150,9 +151,9 @@ def test_extract_model_config(): @pytest.mark.util -def test_roundtrip_export_import(): +def test_roundtrip_export_import() -> None: """Test config extraction and re-application preserves node attributes.""" - model, expected_config = make_im2col_test_model() + model, _ = make_im2col_test_model() attrs_to_extract = ["kernel_size", "stride", "pad_amount", "input_shape"] # Extract config from model @@ -160,11 +161,11 @@ def test_roundtrip_export_import(): model, subgraph_hier=None, attr_names_to_extract=attrs_to_extract ) # Save in json - config_json_file = os.environ["FINN_BUILD_DIR"] + "/original_config.json" + config_json_file = Path(os.environ["FINN_BUILD_DIR"]) / "original_config.json" extract_model_config_to_json(model, config_json_file, attrs_to_extract) # Modify all Im2Col nodes to different values (recursively through subgraphs) - def modify_all_im2col_nodes(graph_proto): + def modify_all_im2col_nodes(graph_proto: onnx.GraphProto) -> None: for node in graph_proto.node: if node.op_type == "Im2Col": inst = getCustomOp(node) @@ -191,5 +192,5 @@ def modify_all_im2col_nodes(graph_proto): ) assert restored_config == original_config, "Config not properly restored after roundtrip" - if os.path.exists(config_json_file): - os.remove(config_json_file) + if config_json_file.exists(): + config_json_file.unlink() From 26e8e8ed8ce6424c435ce2fdb36d264ff450ffdf Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 15 May 2026 18:15:23 +0200 Subject: [PATCH 105/170] Start fixing tests --- finn_xsi/finn_xsi/adapter.py | 109 +- finn_xsi/finn_xsi/sim_engine.py | 1040 ++++++++++-------- finn_xsi/finn_xsi/xsi.pyi | 37 + finn_xsi/finn_xsi/xsi_bind.cpp | 3 + src/finn/builder/build_dataflow_steps.py | 73 +- src/finn/core/rtlsim_exec.py | 360 ++---- src/finn/xsi/__init__.py | 64 +- tests/fpgadataflow/test_fpgadataflow_mvau.py | 120 +- 8 files changed, 833 insertions(+), 973 deletions(-) create mode 100644 finn_xsi/finn_xsi/xsi.pyi diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index d2df8296d9..782c85eaed 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -1,3 +1,5 @@ +"""Interface adapter for FINN XSI.""" + ############################################################################# # Copyright (C) 2025, Advanced Micro Devices, Inc. # All rights reserved. @@ -10,39 +12,45 @@ import errno import os -import os.path import re from finn_xsi.sim_engine import SimEngine -from typing import Optional +from typing import Literal from finn.util.basic import launch_process_helper +from finn.util.exception import FINNInternalError, FINNUserError +from pathlib import Path -def locate_glbl() -> Optional[str]: - """ - Tries to determine the glbl.v file path from environment variables. + +def locate_glbl() -> Path | None: + """Try to determine the glbl.v file path from environment variables. Returns None if it cannot be found. """ # Get GLBL from the Vitis environment variable vivado_path = os.environ.get("XILINX_VIVADO") if vivado_path: - glbl_path = os.path.join(vivado_path, "data", "verilog", "src", "glbl.v") - if os.path.isfile(glbl_path): + glbl_path = Path(vivado_path) / "data" / "verilog" / "src" / "glbl.v" + if glbl_path.is_file(): return glbl_path return None -def compile_sim_obj(top_module_name, source_list, sim_out_dir, debug=False, behav=False): +def compile_sim_obj( + top_module_name: str, + source_list: list[str], + sim_out_dir: Path, + debug: bool = False, + behav: bool = False, +) -> tuple[Path, Path]: + """Compile the simulation object (.so) for the given top module and source files.""" # create a .prj file with the source files - with open(sim_out_dir + "/rtlsim.prj", "w") as f: + with (sim_out_dir / "rtlsim.prj").open("w") as f: glbl = locate_glbl() if glbl is not None: f.write(f"verilog work {glbl}\n") # extract (unique, by using a set) verilog headers for inclusion - verilog_headers = { - os.path.dirname(x) for x in source_list if x.endswith(".vh") or x.endswith(".svh") - } + verilog_headers = {str(Path(x).parent) for x in source_list if x.endswith((".vh", ".svh"))} verilog_header_incl_str = " ".join(["--include " + x for x in verilog_headers]) for src_line in source_list: @@ -53,11 +61,11 @@ def compile_sim_obj(top_module_name, source_list, sim_out_dir, debug=False, beha f.write(f"vhdl2008 work {src_line}\n") elif src_line.endswith(".sv"): f.write(f"sv work {verilog_header_incl_str} {src_line}\n") - elif src_line.endswith(".vh") or src_line.endswith(".svh"): + elif src_line.endswith((".vh", ".svh")): # skip adding Verilog headers directly (see verilog_header_incl_str) continue else: - raise Exception(f"Unknown extension for .prj file sources: {src_line}") + raise FINNInternalError(f"Unknown extension for .prj file sources: {src_line}") # now call xelab to generate the .so for the design to be simulated # list of libs for xelab retrieved from Vitis HLS cosim cmdline @@ -107,23 +115,31 @@ def compile_sim_obj(top_module_name, source_list, sim_out_dir, debug=False, beha if locate_glbl() is not None: cmd_xelab.insert(1, "work.glbl") - cmd_xvlog = "xvlog --incr --relax -prj rtlsim.prj".split() + cmd_xvlog = ["xvlog", "--incr", "--relax", "-prj", "rtlsim.prj"] launch_process_helper(cmd_xvlog, cwd=sim_out_dir, print_stdout=False) launch_process_helper(cmd_xelab, cwd=sim_out_dir, print_stdout=False) - out_so_relative_path = "xsim.dir/%s/xsimk.so" % top_module_name - out_so_full_path = sim_out_dir + "/" + out_so_relative_path + out_so_relative_path = Path(f"xsim.dir/{top_module_name}/xsimk.so") + out_so_full_path = sim_out_dir / out_so_relative_path - if not os.path.isfile(out_so_full_path): + if not out_so_full_path.is_file(): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), out_so_full_path) return (sim_out_dir, out_so_relative_path) -def get_simkernel_so(): +def get_simkernel_so() -> Literal["libxv_simulator_kernel.so", "librdi_simulator_kernel.so"]: + """Determine the correct XSI simulator kernel .so filename based on the Vivado version.""" vivado_path = os.environ.get("XILINX_VIVADO") + if vivado_path is None: + raise OSError( + "XILINX_VIVADO environment variable is not set. " + "Did you source the Vitis/Vivado settings script?" + ) # xsi kernel lib name depends on Vivado version (renamed in 2024.2) match = re.search(r"\b(20\d{2})\.(1|2)\b", vivado_path) + if match is None: + raise ValueError(f"Could not parse Vivado version from XILINX_VIVADO path: {vivado_path}") year, minor = int(match.group(1)), int(match.group(2)) if (year, minor) > (2024, 1): simkernel_so = "libxv_simulator_kernel.so" @@ -132,12 +148,18 @@ def get_simkernel_so(): return simkernel_so -def load_sim_obj(sim_out_dir, out_so_relative_path, tracefile=None, simkernel_so=None): +def load_sim_obj( + sim_out_dir: Path, + out_so_relative_path: Path, + tracefile: str | None = None, + simkernel_so: str | None = None, +) -> SimEngine: + """Load the compiled simulation object (.so) and return a SimEngine instance.""" if simkernel_so is None: simkernel_so = get_simkernel_so() - oldcwd = os.getcwd() + oldcwd = Path.cwd() os.chdir(sim_out_dir) - sim = SimEngine(simkernel_so, out_so_relative_path, "finnxsi_rtlsim.log", tracefile) + sim = SimEngine(simkernel_so, str(out_so_relative_path), "finnxsi_rtlsim.log", tracefile) if tracefile: sim.top.trace_all() os.chdir(oldcwd) @@ -145,31 +167,40 @@ def load_sim_obj(sim_out_dir, out_so_relative_path, tracefile=None, simkernel_so def reset_rtlsim( - sim, rst_name="ap_rst_n", active_low=True, clk_name="ap_clk", clk2x_name="ap_clk2x", n_cycles=16 -): + sim: SimEngine, + rst_name: str = "ap_rst_n", # noqa: ARG001 + active_low: bool = True, # noqa: ARG001 + clk_name: str = "ap_clk", # noqa: ARG001 + clk2x_name: str = "ap_clk2x", # noqa: ARG001 + n_cycles: int = 16, # noqa: ARG001 +) -> None: + """Reset the RTL simulation by toggling the reset signal for a specified number of cycles.""" sim.do_reset() sim.run() -def close_rtlsim(sim): +def close_rtlsim(sim: SimEngine) -> None: + """Close the RTL simulation, ensuring that any pending traces are flushed.""" del sim def rtlsim_multi_io( - sim, - io_dict, - num_out_values, - sname="_V_V", - liveness_threshold=10000, -): + sim: SimEngine, + io_dict: dict[str, dict[str, list[int]]], + num_out_values: int | dict[str, int], + sname: str = "_V_V", + liveness_threshold: int = 10000, +) -> int: + """Run the RTL simulation with multiple input and/or output streams.""" if len(io_dict["outputs"]) > 1: - assert isinstance( - num_out_values, dict - ), "num_out_values must be dict for multiple output streams" + if not isinstance(num_out_values, dict): + raise FINNInternalError("num_out_values must be dict for multiple output streams") else: # num_out_values is provided as integer (indicating the expected # outputs from the single output stream) - make into dict - oname = list(io_dict["outputs"].keys())[0] + if not isinstance(num_out_values, int): + raise FINNInternalError("num_out_values must be int for single output stream") + oname = next(iter(io_dict["outputs"].keys())) num_out_values = {oname: num_out_values} # FINN XSI expects hex strings, while rtlsim_multi_io uses @@ -179,7 +210,7 @@ def rtlsim_multi_io( # hex strings instead of arb-prec Python integers for inp in io_dict["inputs"]: arbprec_int_input = io_dict["inputs"][inp] - hexstring_input = map(lambda var: f"{var:0x}", arbprec_int_input) + hexstring_input = (f"{var:0x}" for var in arbprec_int_input) stream_name = inp + sname sim.stream_input(stream_name, hexstring_input) @@ -195,9 +226,11 @@ def rtlsim_multi_io( start_ticks = sim.ticks ret = sim.run() if len(ret) > 0: - assert False, f"RTL simulation watchdogs {str(ret)} timed out. Check rtlsim_trace if any." + raise FINNUserError( + f"RTL simulation watchdogs {ret!s} timed out. Check rtlsim_trace if any." + ) end_ticks = sim.ticks for out in io_dict["outputs"]: - io_dict["outputs"][out] = list(map(lambda var: int(var, base=16), hex_output_streams[out])) + io_dict["outputs"][out] = [int(var, base=16) for var in hex_output_streams[out]] return end_ticks - start_ticks diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py index 0d17e581af..2d24a35ad7 100644 --- a/finn_xsi/finn_xsi/sim_engine.py +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -8,15 +8,53 @@ # @author Thomas B. Preußer # @author Yaman Umuroglu ############################################################################# +"""Simulation engine utilities for FINN XSI-based hardware runs.""" + +from typing import Literal +from collections.abc import Generator, Iterator import numpy as np + +# provided via pybind11 import xsi class SimEngine: + """SimEngine abstraction for orchestrating XSI simulation tasks.""" + + # ------------------------------------------------------------------------ + # Classes + class Watchdog: + """Track simulation cycles and signal when a timeout is reached.""" + + def __init__(self, name: str, timeout: int) -> None: + """Create a watchdog with a label and timeout in cycles.""" + self.name = name + self.ticks = 0 + self.timeout = timeout + + def __bool__(self) -> bool: + """Return True while the watchdog has not timed out.""" + return self.ticks < self.timeout + + def __repr__(self) -> str: + """Return the watchdog name for debugging.""" + return self.name + + def __call__(self) -> None: + """Advance the watchdog by one tick.""" + self.ticks += 1 + + def reset(self) -> None: + """Reset the watchdog tick counter.""" + self.ticks = 0 + # ------------------------------------------------------------------------ # Life Cycle - def __init__(self, kernel, design, log=None, wdb=None): + def __init__( + self, kernel: str, design: str, log: str | None = None, wdb: str | None = None + ) -> None: + """Create a simulation engine bound to the given kernel and design.""" top = xsi.Design(xsi.Kernel(kernel), design, log, wdb) clk = top.getPort("ap_clk") clk2x = top.getPort("ap_clk2x") @@ -24,7 +62,7 @@ def __init__(self, kernel, design, log=None, wdb=None): if p.isInput(): p.clear().write_back() - def cycle(updates): + def cycle(updates: dict[xsi.Port, str]) -> None: # Rising Edge clk.set(1).write_back() if clk2x is not None: @@ -53,11 +91,12 @@ def cycle(updates): self.cycle = cycle self.ticks = 0 self.tasks = [] - self.watchdogs = [] + self.watchdogs: list[SimEngine.Watchdog] = [] # ------------------------------------------------------------------------ # Utility - def get_bus_port(self, bus, suffix): + def get_bus_port(self, bus: str, suffix: str) -> "xsi.Port": + """Return a port by bus name and suffix, trying lower/upper variants.""" port = self.top.getPort(bus + "_" + suffix.lower()) return port if port is not None else self.top.getPort(bus + "_" + suffix.upper()) @@ -65,40 +104,28 @@ def get_bus_port(self, bus, suffix): # Simulation Setup # Task Scheduling - def enlist(self, task): + def enlist( + self, + task: "SimEngine.Reset | SimEngine.InputStreamer | SimEngine.OutputCollector | SimEngine.Watchdog | SimEngine.StreamTracer | SimEngine.AxiLiteWriter | SimEngine.AxiLiteReader | SimEngine.AximmRoImage | SimEngine.AximmQueue", # noqa + ) -> None: + """Register a task to be driven by the simulation loop.""" self.tasks.append(task) # Watchdog Generation - def create_watchdog(self, name, timeout): - class Watchdog: - def __init__(self, name, timeout): - self.name = name - self.ticks = 0 - self.timeout = timeout - - def __bool__(self): - return self.ticks < self.timeout - - def __repr__(self): - return self.name - - def __call__(self): - self.ticks += 1 - - def reset(self): - self.ticks = 0 - - ret = Watchdog(name, timeout) + def create_watchdog(self, name: str, timeout: int) -> "SimEngine.Watchdog": + """Create and register a watchdog with the given timeout.""" + ret = SimEngine.Watchdog(name, timeout) self.watchdogs.append(ret) return ret - def remove_watchdog(self, watchdog): + def remove_watchdog(self, watchdog: "SimEngine.Watchdog") -> None: + """Remove a previously registered watchdog.""" self.watchdogs.remove(watchdog) # ------------------------------------------------------------------------ # Execution - def run(self, cycles=None): - "Run all tasks to completion or until a watchdog triggers." + def run(self, cycles: int | None = None) -> list[Watchdog]: + """Run all tasks to completion or until a watchdog triggers.""" timeout = None if cycles is None else self.create_watchdog("Run Timeout", cycles) woken = [] @@ -134,498 +161,539 @@ def run(self, cycles=None): # ------------------------------------------------------------------------ # Standard Tasks - def do_reset(self): - "Schedule a reset sequence." - - class Reset: - def __init__(self, top): - self.cnt = 0 - self.rst_n = top.getPort("ap_rst_n") - - def __call__(self, sim): - cnt = self.cnt - self.cnt = cnt + 1 - - if cnt == 0: - return {self.rst_n: "0"} - if cnt < 16: - return {} - if cnt == 16: - return {self.rst_n: "1"} - return None + class Reset: + """Drive the reset signal for a fixed number of cycles.""" + + def __init__(self, top: "xsi.Design") -> None: + """Bind to the design reset port.""" + self.cnt = 0 + self.rst_n: xsi.Port = top.getPort("ap_rst_n") + + def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG002 + """Return port updates to perform the reset sequence.""" + cnt = self.cnt + self.cnt = cnt + 1 + + if cnt == 0: + return {self.rst_n: "0"} + if cnt < 16: + return {} + if cnt == 16: + return {self.rst_n: "1"} + return None + + def do_reset(self) -> None: + """Schedule a reset sequence.""" + self.enlist(SimEngine.Reset(self.top)) + + class InputStreamer: + """Drive an AXI-Stream input from an iterator of values.""" + + def __init__( + self, top: "SimEngine", istream: str, values: Generator[str], throttle: tuple + ) -> None: + """Bind to the stream ports and configure throttling.""" + self.vld: xsi.Port = top.get_bus_port(istream, "tvalid") + self.rdy: xsi.Port = top.get_bus_port(istream, "tready") + self.dat: xsi.Port = top.get_bus_port(istream, "tdata") + self.values = values + + self.throttle = throttle + self.await_tick = 0 + self.count_txns = throttle[0] + + def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: + """Advance one cycle of input streaming.""" + vld = self.vld.as_bool() + if vld and not self.rdy.read().as_bool(): + return {} - self.enlist(Reset(self.top)) - - def stream_input(self, istream, values, throttle=(float("inf"), 0)): - "Stream all values from the passed iterator into the specified stream." - - class InputStreamer: - def __init__(self, top, istream, values, throttle): - self.vld = top.get_bus_port(istream, "tvalid") - self.rdy = top.get_bus_port(istream, "tready") - self.dat = top.get_bus_port(istream, "tdata") - self.values = values - - self.throttle = throttle - self.await_tick = 0 - self.count_txns = throttle[0] - - def __call__(self, sim): - vld = self.vld.as_bool() - if vld and not self.rdy.read().as_bool(): - return {} - - # Track Transaction Count - if vld: - self.count_txns += 1 - - # Proceed according to Throttling Rate - if self.count_txns < self.throttle[0] or not sim.ticks < self.await_tick: - # Try Feed - val = next(self.values, None) - if val is None: - # Unset vld, then exit - return {self.vld: "0", self.dat: "0"} if vld else None - - # Feed next Value - ret = {self.dat: val} - if not vld: - ret[self.vld] = "1" - if self.count_txns == self.throttle[0]: - self.count_txns = 0 - self.await_tick = sim.ticks + self.throttle[1] - return ret - - # Stall Feed - return {self.vld: "0", self.dat: "0"} if vld else {} - - self.enlist(InputStreamer(self, istream, values, throttle)) - - def collect_output(self, ostream, size, watchdog=None): - "Collect size outputs from the specified stream into the returned iterable buffer." - - class OutputCollector: - def __init__(self, top, ostream, size, watchdog): - self.size = size - self.vld = top.get_bus_port(ostream, "tvalid") - self.rdy = top.get_bus_port(ostream, "tready") - self.dat = top.get_bus_port(ostream, "tdata") - self.buf = [] - self.watchdog = watchdog - - def __iter__(self): - return iter(self.buf) - - def __call__(self, sim): - if self.rdy.as_bool(): - if self.vld.read().as_bool(): - # Have a n Output Transaction - if self.watchdog is not None: - self.watchdog.reset() - val = self.dat.read().as_hexstr() - self.buf.append(val) - if len(self.buf) == size: - return {self.rdy: "0"} - return {} - - if len(self.buf) < size: - return {self.rdy: "1"} - return None + # Track Transaction Count + if vld: + self.count_txns += 1 + + # Proceed according to Throttling Rate + if self.count_txns < self.throttle[0] or not sim.ticks < self.await_tick: + # Try Feed + val = next(self.values, None) + if val is None: + # Unset vld, then exit + return {self.vld: "0", self.dat: "0"} if vld else None + + # Feed next Value + ret = {self.dat: val} + if not vld: + ret[self.vld] = "1" + if self.count_txns == self.throttle[0]: + self.count_txns = 0 + self.await_tick = sim.ticks + self.throttle[1] + return ret - ret = OutputCollector(self, ostream, size, watchdog) + # Stall Feed + return {self.vld: "0", self.dat: "0"} if vld else {} + + def stream_input( + self, + istream: str, + values: Generator[str], + throttle: tuple[float, float] = (float("inf"), 0), + ) -> None: + """Stream all values from the passed iterator into the specified stream.""" + self.enlist(SimEngine.InputStreamer(self, istream, values, throttle)) + + class OutputCollector: + """Collect a fixed number of AXI-Stream output values.""" + + def __init__( + self, top: "SimEngine", ostream: str, size: int, watchdog: "SimEngine.Watchdog | None" + ) -> None: + """Bind to the stream ports and prepare a buffer.""" + self.size = size + self.vld = top.get_bus_port(ostream, "tvalid") + self.rdy = top.get_bus_port(ostream, "tready") + self.dat = top.get_bus_port(ostream, "tdata") + self.buf: list[str] = [] + self.watchdog = watchdog + + def __iter__(self) -> Iterator[str]: + """Iterate over collected output values.""" + return iter(self.buf) + + def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG002 + """Advance one cycle of output collection.""" + if self.rdy.as_bool(): + if self.vld.read().as_bool(): + # Have a n Output Transaction + if self.watchdog is not None: + self.watchdog.reset() + val = self.dat.read().as_hexstr() + self.buf.append(val) + if len(self.buf) == self.size: + return {self.rdy: "0"} + return {} + + if len(self.buf) < self.size: + return {self.rdy: "1"} + return None + + def collect_output( + self, ostream: str, size: int, watchdog: "SimEngine.Watchdog | None" = None + ) -> "SimEngine.OutputCollector": + """Collect size outputs from the specified stream into the returned iterable buffer.""" + ret = SimEngine.OutputCollector(self, ostream, size, watchdog) self.enlist(ret) return ret - def trace_stream(self, stream): - "Monitor an AXI-Stream and trace its transaction activity" + class StreamTracer: + """Trace AXI-Stream activity as a string of 0/1 tokens.""" - class StreamTracer: - def __init__(self, sim, stream): - self.vld = sim.get_bus_port(stream, "tvalid") - self.rdy = sim.get_bus_port(stream, "tready") - self.trace = "" + def __init__(self, sim: "SimEngine", stream: str) -> None: + """Bind to the stream ports to trace handshakes.""" + self.vld = sim.get_bus_port(stream, "tvalid") + self.rdy = sim.get_bus_port(stream, "tready") + self.trace = "" - def __call__(self, sim): - self.trace += ( - "1" if self.vld.read().as_bool() and self.rdy.read().as_bool() else "0" - ) - return {} + def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG002 + """Advance one cycle of trace collection.""" + self.trace += "1" if self.vld.read().as_bool() and self.rdy.read().as_bool() else "0" + return {} - def __bool__(self): - return False + def __bool__(self) -> Literal[False]: + """Report false to keep the task alive.""" + return False - def __str__(self): - return self.trace + def __str__(self) -> str: + """Return the collected trace string.""" + return self.trace - ret = StreamTracer(self, stream) + def trace_stream(self, stream: str) -> "SimEngine.StreamTracer": + """Monitor an AXI-Stream and trace its transaction activity.""" + ret = SimEngine.StreamTracer(self, stream) self.enlist(ret) return ret - def write_axilite(self, m_axilite, writes): - "Execute writes specified as a list of (addr, val)-tuples to AXI-lite interface" - - class AxiLiteWriter: - INIT = 0 - FEED = 1 - COOL = 2 - - def __init__(self, top, m_axilite, writes): - self.awready = top.get_bus_port(m_axilite, "awready") - self.awvalid = top.get_bus_port(m_axilite, "awvalid") - self.awaddr = top.get_bus_port(m_axilite, "awaddr") - self.wready = top.get_bus_port(m_axilite, "wready") - self.wvalid = top.get_bus_port(m_axilite, "wvalid") - self.wdata = top.get_bus_port(m_axilite, "wdata") - wstrb = top.get_bus_port(m_axilite, "wstrb") - wstrb.set_binstr("1" * wstrb.width()).write_back() - self.bready = top.get_bus_port(m_axilite, "bready") - self.bvalid = top.get_bus_port(m_axilite, "bvalid") - self.bresp = top.get_bus_port(m_axilite, "bresp") - self.writes = writes - self.state = self.INIT - self.pending = 0 - - def __call__(self, sim): - # Termination - if self.state == self.COOL and not self.bready.as_bool(): - return None + class AxiLiteWriter: + """Drive AXI-Lite writes from a list of address/value pairs.""" + + INIT = 0 + FEED = 1 + COOL = 2 + + def __init__( + self, top: "SimEngine", m_axilite: str, writes: Iterator[tuple[int, str]] + ) -> None: + """Bind to AXI-Lite write channels and store the write iterator.""" + self.awready = top.get_bus_port(m_axilite, "awready") + self.awvalid = top.get_bus_port(m_axilite, "awvalid") + self.awaddr = top.get_bus_port(m_axilite, "awaddr") + self.wready = top.get_bus_port(m_axilite, "wready") + self.wvalid = top.get_bus_port(m_axilite, "wvalid") + self.wdata = top.get_bus_port(m_axilite, "wdata") + wstrb = top.get_bus_port(m_axilite, "wstrb") + wstrb.set_binstr("1" * wstrb.width()).write_back() + self.bready = top.get_bus_port(m_axilite, "bready") + self.bvalid = top.get_bus_port(m_axilite, "bvalid") + self.bresp = top.get_bus_port(m_axilite, "bresp") + self.writes = writes + self.state = self.INIT + self.pending = 0 + + def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG002 + """Advance one cycle of AXI-Lite write transactions.""" + # Termination + if self.state == self.COOL and not self.bready.as_bool(): + return None - ret = {} + ret = {} - # Always Monitor Completions - if self.state == self.INIT: - ret[self.bready] = "1" - self.state = self.FEED + # Always Monitor Completions + if self.state == self.INIT: + ret[self.bready] = "1" + self.state = self.FEED - if self.bvalid.read().as_bool(): - if self.pending < 1: - print("Received spurious completion on", self.bresp.name()) + if self.bvalid.read().as_bool(): + if self.pending < 1: + print("Received spurious completion on", self.bresp.name()) + else: + self.pending -= 1 + if self.pending == 0 and self.state == self.COOL: + ret[self.bready] = "0" + + if self.bresp.read().as_unsigned() != 0: + print("Received error indication on", self.bresp.name()) + + # Transaction Feed + if self.state == self.FEED: + step = True + + # Check for busy address feed + avld = self.awvalid.as_bool() + aclr = False + if avld: + if self.awready.read().as_bool(): + aclr = True else: - self.pending -= 1 - if self.pending == 0 and self.state == self.COOL: - ret[self.bready] = "0" - - if self.bresp.read().as_unsigned() != 0: - print("Received error indication on", self.bresp.name()) - - # Transaction Feed - if self.state == self.FEED: - step = True - - # Check for busy address feed - avld = self.awvalid.as_bool() - aclr = False - if avld: - if self.awready.read().as_bool(): - aclr = True - else: - step = False - - # Check for busy data feed - wvld = self.wvalid.as_bool() - wclr = False - if wvld: - if self.wready.read().as_bool(): - wclr = True - else: - step = False - - # Proceed with next Write - if step: - addr, val = next(self.writes, (None, None)) - if addr is not None: - ret[self.awaddr] = f"{addr:x}" - ret[self.wdata] = val - if not avld: - ret[self.awvalid] = "1" - if not wvld: - ret[self.wvalid] = "1" - self.pending += 1 - return ret - if not self.pending: - ret[self.bready] = "0" - self.state = self.COOL - - # Deassert completed feed - if aclr: - ret[self.awvalid] = "0" - if wclr: - ret[self.wvalid] = "0" + step = False + + # Check for busy data feed + wvld = self.wvalid.as_bool() + wclr = False + if wvld: + if self.wready.read().as_bool(): + wclr = True + else: + step = False + + # Proceed with next Write + if step: + item = next(self.writes, None) + if item is not None: + addr, val = item + ret[self.awaddr] = f"{addr:x}" + ret[self.wdata] = val + if not avld: + ret[self.awvalid] = "1" + if not wvld: + ret[self.wvalid] = "1" + self.pending += 1 + return ret + if not self.pending: + ret[self.bready] = "0" + self.state = self.COOL + + # Deassert completed feed + if aclr: + ret[self.awvalid] = "0" + if wclr: + ret[self.wvalid] = "0" + + return ret + + class AxiLiteReader: + """Collect AXI-Lite reads for a list of addresses.""" + + def __init__(self, top: "SimEngine", m_axilite: str, reads: Iterator[int]) -> None: + """Bind to AXI-Lite read channels and store the address iterator.""" + self.arready = top.get_bus_port(m_axilite, "arready") + self.arvalid = top.get_bus_port(m_axilite, "arvalid") + self.araddr = top.get_bus_port(m_axilite, "araddr") + self.rready = top.get_bus_port(m_axilite, "rready") + self.rvalid = top.get_bus_port(m_axilite, "rvalid") + self.rdata = top.get_bus_port(m_axilite, "rdata") + self.reads = reads + self.pending = [] + self.draining = False + self.replies: dict[xsi.Port, str] = {} + + def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG002 + """Advance one cycle of AXI-Lite read transactions.""" + ret = {} + + # Address Stream Feed: assert self.draining when done + if not self.draining and (self.arready.read().as_bool() or not self.arvalid.as_bool()): + addr = next(self.reads, None) + if addr is None: + ret[self.arvalid] = "0" + self.draining = True + else: + ret[self.arvalid] = "1" + ret[self.araddr] = f"{addr:x}" + self.pending.append(addr) - return ret + # Reply Collection + if not self.rready.as_bool(): + # Termination + if self.draining: + return None + # Activation + ret[self.rready] = "1" + elif self.rvalid.read().as_bool(): + assert len(self.pending) > 0, "Spurious reply." + self.replies[self.pending.pop(0)] = self.rdata.read().as_hexstr() + if self.draining and len(self.pending) == 0: + ret[self.rready] = "0" + + return ret + + def __iter__(self) -> Iterator[xsi.Port]: + """Iterate over completed read replies.""" + return iter(self.replies) + + def __getitem__(self, addr: xsi.Port) -> str: + """Return the reply value for a specific address.""" + return self.replies[addr] + + def write_axilite(self, m_axilite: str, writes: Iterator[tuple[int, str]]) -> None: + """Execute writes specified as a list of (addr, val)-tuples to AXI-lite interface.""" + self.enlist(SimEngine.AxiLiteWriter(self, m_axilite, writes)) + + def read_axilite(self, m_axilite: str, reads: Iterator[int]) -> "SimEngine.AxiLiteReader": + """Execute reads specified as a list of addresses from AXI-lite interface.""" + ret = SimEngine.AxiLiteReader(self, m_axilite, reads) + self.enlist(ret) + return ret + + class AximmRoImage: + """Serve a read-only AXI memory image from a byte buffer.""" + + def __init__(self, top: "SimEngine", mm_axi: "str", base: int, img: list[str]) -> None: + """Bind to AXI memory ports and stage the image data.""" + self.mm_axi = mm_axi + self.rd_count = 0 + # Tie off Write Channels + for tie_off in ("awready", "wready", "bvalid"): + port = top.get_bus_port(mm_axi, tie_off) + if port is not None: + port.set(0).write_back() + + # Collect Ports of Read Channels + self.arready = top.get_bus_port(mm_axi, "arready") + self.arvalid = top.get_bus_port(mm_axi, "arvalid") + self.araddr = top.get_bus_port(mm_axi, "araddr") + self.arlen = top.get_bus_port(mm_axi, "arlen") + self.arburst = top.get_bus_port(mm_axi, "arburst") + self.arsize = top.get_bus_port(mm_axi, "arsize") + self.rready = top.get_bus_port(mm_axi, "rready") + self.rvalid = top.get_bus_port(mm_axi, "rvalid") + self.rdata = top.get_bus_port(mm_axi, "rdata") + self.rresp = top.get_bus_port(mm_axi, "rresp") + self.rlast = top.get_bus_port(mm_axi, "rlast") + + self.arready.set(1).write_back() + self.rvalid.set(0).write_back() + self.rresp.set(0).write_back() + + # Hold on to Image + self.base = base + self.img = [f"{_:02x}" for _ in np.array(img).astype(np.uint8)] + # This is a hack to account for the minimum DMA burst read size of 32 bytes. + for _i in range(32): + self.img.append("00") # Pad to 32 bytes + self.queue = [] + + def __bool__(self) -> Literal[False]: + """Report false to keep the task alive.""" + return False + + def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG002 + """Advance one cycle of read-only memory servicing.""" + ret = {} + + # Push out Read Replies + if self.rready.read().as_bool() or not self.rvalid.as_bool(): + if len(self.queue) > 0: + # Work on Head of Queue + addr, length, size = self.queue.pop(0) + data = "" + for _i in range(size): + data = self.img[addr] + data + addr += 1 + ret[self.rdata] = data + + if length > 1: + self.queue.insert(0, (addr, length - 1, size)) + ret[self.rlast] = "0" + else: + ret[self.rlast] = "1" + ret[self.rvalid] = "1" - self.enlist(AxiLiteWriter(self, m_axilite, writes)) - - def read_axilite(self, m_axilite, reads): - class AxiLiteReader: - def __init__(self, top, m_axilite, reads): - self.arready = top.get_bus_port(m_axilite, "arready") - self.arvalid = top.get_bus_port(m_axilite, "arvalid") - self.araddr = top.get_bus_port(m_axilite, "araddr") - self.rready = top.get_bus_port(m_axilite, "rready") - self.rvalid = top.get_bus_port(m_axilite, "rvalid") - self.rdata = top.get_bus_port(m_axilite, "rdata") - self.reads = reads - self.pending = [] - self.draining = False - self.replies = {} - - def __call__(self, sim): - ret = {} - - # Address Stream Feed: assert self.draining when done - if not self.draining: - if self.arready.read().as_bool() or not self.arvalid.as_bool(): - addr = next(self.reads, None) - if addr is None: - ret[self.arvalid] = "0" - self.draining = True - else: - ret[self.arvalid] = "1" - ret[self.araddr] = f"{addr:x}" - self.pending.append(addr) - - # Reply Collection - if not self.rready.as_bool(): - # Termination - if self.draining: - return None - # Activation - ret[self.rready] = "1" - elif self.rvalid.read().as_bool(): - assert len(self.pending) > 0, "Spurious reply." - self.replies[self.pending.pop(0)] = self.rdata.read().as_hexstr() - if self.draining and len(self.pending) == 0: - ret[self.rready] = "0" + elif self.rvalid.as_bool(): + # Silent Reply Interface + ret[self.rvalid] = "0" - return ret + # Queue up newly received Read Requests + if self.arvalid.read().as_bool(): + assert self.arburst.read().as_unsigned() == 1, "Only INCR bursts supported." - def __iter__(self): - return iter(self.replies) + addr = int(self.araddr.read().as_hexstr(), 16) + # addr = addr - 8*self.rd_count + # self.rd_count = self.rd_count + 2 - def __getitem__(self, addr): - return self.replies[addr] + assert self.base <= addr, "Read address out of range." + addr -= self.base - ret = AxiLiteReader(self, m_axilite, reads) - self.enlist(ret) - return ret + length = 1 + self.arlen.read().as_unsigned() + size = 2 ** self.arsize.read().as_unsigned() + if addr + (length * size) > len( + self.img + ): # account for minimum dma burst read size of 32 bytes + print(f"Range extends beyond range {addr=} {length=} {size=}") + # assert addr + length * size < len(self.img), "Read extends beyond range." - def aximm_ro_image(self, mm_axi, base, img): - class AximmRoImage: - def __init__(self, top, mm_axi, base, img): - self.mm_axi = mm_axi - self.rd_count = 0 - # Tie off Write Channels - for tie_off in ("awready", "wready", "bvalid"): - port = top.get_bus_port(mm_axi, tie_off) - if port is not None: - port.set(0).write_back() - - # Collect Ports of Read Channels - for name in ( - "arready", - "arvalid", - "araddr", - "arlen", - "arburst", - "arsize", - "rready", - "rvalid", - "rdata", - "rresp", - "rlast", - ): - self.__dict__[name] = top.get_bus_port(mm_axi, name) - self.arready.set(1).write_back() - self.rvalid.set(0).write_back() - self.rresp.set(0).write_back() - - # Hold on to Image - self.base = base - self.img = [f"{_:02x}" for _ in np.array(img).astype(np.uint8)] - # This is a hack to account for the minimum DMA burst read size of 32 bytes. - for i in range(32): - self.img.append("00") # Pad to 32 bytes - self.queue = [] - - def __bool__(self): - return False - - def __call__(self, sim): - ret = {} - - # Push out Read Replies - if self.rready.read().as_bool() or not self.rvalid.as_bool(): - if len(self.queue) > 0: - # Work on Head of Queue - addr, length, size = self.queue.pop(0) - data = "" - for i in range(size): - data = self.img[addr] + data - addr += 1 - ret[self.rdata] = data - - if length > 1: - self.queue.insert(0, (addr, length - 1, size)) - ret[self.rlast] = "0" - else: - ret[self.rlast] = "1" - ret[self.rvalid] = "1" - - elif self.rvalid.as_bool(): - # Silent Reply Interface - ret[self.rvalid] = "0" - - # Queue up newly received Read Requests - if self.arvalid.read().as_bool(): - assert self.arburst.read().as_unsigned() == 1, "Only INCR bursts supported." - - addr = int(self.araddr.read().as_hexstr(), 16) - # addr = addr - 8*self.rd_count - # self.rd_count = self.rd_count + 2 - - assert self.base <= addr, "Read address out of range." - addr -= self.base - - length = 1 + self.arlen.read().as_unsigned() - size = 2 ** self.arsize.read().as_unsigned() - if addr + (length * size) > len( - self.img - ): # account for minimum dma burst read size of 32 bytes - print(f"Range extends beyond range {addr=} {length=} {size=}") - # assert addr + length * size < len(self.img), "Read extends beyond range." - - self.queue.append((addr, length, size)) + self.queue.append((addr, length, size)) - return ret + return ret - ret = AximmRoImage(self, mm_axi, base, img) + def aximm_ro_image(self, mm_axi: "str", base: int, img: list[str]) -> "SimEngine.AximmRoImage": + """Register a read-only AXI memory image task.""" + ret = SimEngine.AximmRoImage(self, mm_axi, base, img) self.enlist(ret) return ret - def aximm_queue(self, mm_axi): - "Pick up all write requests to carry them over to complete" - " a later read request with the same address and size." - - class AximmQueue: - def __init__(self, top, mm_axi): - # Collect Ports of Read Channels - for name in ( - "awready", - "awvalid", - "awaddr", - "awlen", - "awburst", - "awsize", - "wready", - "wvalid", - "wdata", - "wlast", - "bready", - "bvalid", - "bdata", - "bresp", - "arready", - "arvalid", - "araddr", - "arlen", - "arburst", - "arsize", - "rready", - "rvalid", - "rdata", - "rresp", - "rlast", - ): - self.__dict__[name] = top.get_bus_port(mm_axi, name) - self.awready.set(1).write_back() - self.wready.set(1).write_back() - self.bvalid.set(0).write_back() - self.bresp.set(0).write_back() - self.arready.set(1).write_back() - self.rvalid.set(0).write_back() - self.rresp.set(0).write_back() - - # Hold on to Contents Map per transfer: addr -> data - self.map = {} # addr -> (data, size) - - # Queued transactions - self.wa_queue = [] # Write Addresses (addr, len, size) - self.wd_queue = [] # Write Data (data) - self.ra_queue = [] # Read Addresses (addr, len, size) - self.wr_completion_queue = [] # A queue to track the write completions - - def __bool__(self): - return False - - def __call__(self, sim): - ret = {} - - # Process Write Updates - while len(self.wa_queue) > 0: - addr, length, size = self.wa_queue.pop(0) - while length > 0: - if len(self.wd_queue) > 0: - self.map[addr] = (self.wd_queue.pop(0), size) - addr += size - length -= 1 - if length == 0: - self.wr_completion_queue.append((0, 1)) - else: - self.wa_queue.insert(0, (addr, length, size)) - break - if len(self.wd_queue) == 0: + class AximmQueue: + """Queue AXI-MM writes and replay them on reads.""" + + def __init__(self, top: "SimEngine", mm_axi: "str") -> None: + """Bind to AXI-MM channels and initialize queues.""" + # Collect Ports of Read Channels + self.awready = top.get_bus_port(mm_axi, "awready") + self.awvalid = top.get_bus_port(mm_axi, "awvalid") + self.awaddr = top.get_bus_port(mm_axi, "awaddr") + self.awlen = top.get_bus_port(mm_axi, "awlen") + self.awburst = top.get_bus_port(mm_axi, "awburst") + self.awsize = top.get_bus_port(mm_axi, "awsize") + self.wready = top.get_bus_port(mm_axi, "wready") + self.wvalid = top.get_bus_port(mm_axi, "wvalid") + self.wdata = top.get_bus_port(mm_axi, "wdata") + self.wlast = top.get_bus_port(mm_axi, "wlast") + self.bready = top.get_bus_port(mm_axi, "bready") + self.bvalid = top.get_bus_port(mm_axi, "bvalid") + self.bdata = top.get_bus_port(mm_axi, "bdata") + self.bresp = top.get_bus_port(mm_axi, "bresp") + self.arready = top.get_bus_port(mm_axi, "arready") + self.arvalid = top.get_bus_port(mm_axi, "arvalid") + self.araddr = top.get_bus_port(mm_axi, "araddr") + self.arlen = top.get_bus_port(mm_axi, "arlen") + self.arburst = top.get_bus_port(mm_axi, "arburst") + self.arsize = top.get_bus_port(mm_axi, "arsize") + self.rready = top.get_bus_port(mm_axi, "rready") + self.rvalid = top.get_bus_port(mm_axi, "rvalid") + self.rdata = top.get_bus_port(mm_axi, "rdata") + self.rresp = top.get_bus_port(mm_axi, "rresp") + self.rlast = top.get_bus_port(mm_axi, "rlast") + self.awready.set(1).write_back() + self.wready.set(1).write_back() + self.bvalid.set(0).write_back() + self.bresp.set(0).write_back() + self.arready.set(1).write_back() + self.rvalid.set(0).write_back() + self.rresp.set(0).write_back() + + # Hold on to Contents Map per transfer: addr -> data + self.map = {} # addr -> (data, size) + + # Queued transactions + self.wa_queue = [] # Write Addresses (addr, len, size) + self.wd_queue = [] # Write Data (data) + self.ra_queue = [] # Read Addresses (addr, len, size) + self.wr_completion_queue = [] # A queue to track the write completions + + def __bool__(self) -> Literal[False]: + """Report false to keep the task alive.""" + return False + + def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG002 + """Advance one cycle of AXI-MM queue servicing.""" + ret = {} + + # Process Write Updates + while len(self.wa_queue) > 0: + addr, length, size = self.wa_queue.pop(0) + while length > 0: + if len(self.wd_queue) > 0: + self.map[addr] = (self.wd_queue.pop(0), size) + addr += size + length -= 1 + if length == 0: + self.wr_completion_queue.append((0, 1)) + else: + self.wa_queue.insert(0, (addr, length, size)) break + if len(self.wd_queue) == 0: + break + + # Push out Read Replies + if self.rready.read().as_bool() or not self.rvalid.as_bool(): + if len(self.ra_queue) > 0: + # Work on Head of Queue + addr, length, size0 = self.ra_queue.pop(0) + assert addr in self.map, "Missing data entry" + data, size = self.map[addr] + assert size == size0, "Write and read size mismatch." + ret[self.rdata] = data + if length > 1: + self.ra_queue.insert(0, (addr + size, length - 1, size)) + ret[self.rlast] = "0" + else: + ret[self.rlast] = "1" + ret[self.rvalid] = "1" + elif self.rvalid.as_bool(): + # Silent Reply Interface + ret[self.rvalid] = "0" + + # Process write completion queue items + if len(self.wr_completion_queue) > 0: + if self.bready.read().as_bool(): + ret[self.bvalid] = "1" + _ = self.wr_completion_queue.pop(0) + else: + ret[self.bvalid] = "0" - # Push out Read Replies - if self.rready.read().as_bool() or not self.rvalid.as_bool(): - if len(self.ra_queue) > 0: - # Work on Head of Queue - addr, length, size0 = self.ra_queue.pop(0) - assert addr in self.map, "Missing data entry" - data, size = self.map[addr] - assert size == size0, "Write and read size mismatch." - ret[self.rdata] = data - if length > 1: - self.ra_queue.insert(0, (addr + size, length - 1, size)) - ret[self.rlast] = "0" - else: - ret[self.rlast] = "1" - ret[self.rvalid] = "1" - elif self.rvalid.as_bool(): - # Silent Reply Interface - ret[self.rvalid] = "0" - - # Process write completion queue items - if len(self.wr_completion_queue) > 0: - if self.bready.read().as_bool(): - ret[self.bvalid] = "1" - _ = self.wr_completion_queue.pop(0) - else: - ret[self.bvalid] = "0" - - # Queue new Write Address Requests - if self.awvalid.read().as_bool(): - assert self.awburst.read().as_unsigned() == 1, "Only INCR bursts supported." + # Queue new Write Address Requests + if self.awvalid.read().as_bool(): + assert self.awburst.read().as_unsigned() == 1, "Only INCR bursts supported." - addr = int(self.awaddr.read().as_hexstr(), 16) - length = 1 + self.awlen.read().as_unsigned() - size = 2 ** self.awsize.read().as_unsigned() - self.wa_queue.append((addr, length, size)) + addr = int(self.awaddr.read().as_hexstr(), 16) + length = 1 + self.awlen.read().as_unsigned() + size = 2 ** self.awsize.read().as_unsigned() + self.wa_queue.append((addr, length, size)) - # Queue received Write Data - if self.wvalid.read().as_bool(): - self.wd_queue.append(self.wdata.read().as_hexstr()) + # Queue received Write Data + if self.wvalid.read().as_bool(): + self.wd_queue.append(self.wdata.read().as_hexstr()) - # Queue new Read Requests - if self.arvalid.read().as_bool(): - assert self.arburst.read().as_unsigned() == 1, "Only INCR bursts supported." + # Queue new Read Requests + if self.arvalid.read().as_bool(): + assert self.arburst.read().as_unsigned() == 1, "Only INCR bursts supported." - addr = int(self.araddr.read().as_hexstr(), 16) - length = 1 + self.arlen.read().as_unsigned() - size = 2 ** self.arsize.read().as_unsigned() - self.ra_queue.append((addr, length, size)) + addr = int(self.araddr.read().as_hexstr(), 16) + length = 1 + self.arlen.read().as_unsigned() + size = 2 ** self.arsize.read().as_unsigned() + self.ra_queue.append((addr, length, size)) - return ret + return ret - self.enlist(AximmQueue(self, mm_axi)) + def aximm_queue(self, mm_axi: "str") -> None: + """Pick up all write requests to carry them over to complete + a later read request with the same address and size.""" + self.enlist(SimEngine.AximmQueue(self, mm_axi)) diff --git a/finn_xsi/finn_xsi/xsi.pyi b/finn_xsi/finn_xsi/xsi.pyi new file mode 100644 index 0000000000..78d5554765 --- /dev/null +++ b/finn_xsi/finn_xsi/xsi.pyi @@ -0,0 +1,37 @@ +from collections.abc import Iterator + +class Kernel: + def __init__(self, name: str) -> None: ... + +class Port: + def name(self) -> str: ... + def dir(self) -> int: ... + def width(self) -> int: ... + def isInput(self) -> bool: ... # noqa: N802 + def isOutput(self) -> bool: ... # noqa: N802 + def isInout(self) -> bool: ... # noqa: N802 + def read(self) -> Port: ... + def write_back(self) -> None: ... + def hasUnknown(self) -> bool: ... # noqa: N802 + def isZero(self) -> bool: ... # noqa: N802 + def as_bool(self) -> bool: ... + def as_unsigned(self) -> int: ... + def as_binstr(self) -> str: ... + def as_hexstr(self) -> str: ... + def clear(self) -> Port: ... + def set(self, value: int) -> Port: ... + def set_binstr(self, value: str) -> Port: ... + def set_hexstr(self, value: str) -> Port: ... + +class Design: + def __init__( + self, kernel: Kernel, design_lib: str, log_file: str | None, wdb_file: str | None + ) -> None: ... + def trace_all(self) -> None: ... + def run(self, cycles: int) -> None: ... + def restart(self) -> None: ... + def get_status(self) -> int: ... + def get_error_info(self) -> str: ... + def num_ports(self) -> int: ... + def getPort(self, name: str) -> Port: ... # noqa: N802 + def ports(self) -> Iterator[Port]: ... diff --git a/finn_xsi/finn_xsi/xsi_bind.cpp b/finn_xsi/finn_xsi/xsi_bind.cpp index 1edf80b01b..fb95353d7f 100644 --- a/finn_xsi/finn_xsi/xsi_bind.cpp +++ b/finn_xsi/finn_xsi/xsi_bind.cpp @@ -34,6 +34,9 @@ namespace { PYBIND11_MODULE(xsi, m) { + py::class_>(m, "Kernel") + .def(py::init()); + py::class_>(m, "Design") .def(py::init([]( std::shared_ptr const &kernel, diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index fa2f9b7d39..267ae01547 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -407,54 +407,6 @@ def step_hw_ipgen( return model - -@register_build_dataflow_step() -def step_build_simulation( - model: ModelWrapper, - cfg: DataflowBuildConfig, - parent_node: str | None = None, - performance_sim: bool = False, -) -> ModelWrapper: - """Build the simulation binaries for isolated and connected simulations.""" - if cfg.fifosim_save_waveform: - report_dir = Path(cfg.output_dir) / "report" - report_dir.mkdir(parents=True, exist_ok=True) - tracefile = ( - f"{parent_node}_fifosim_trace.wdb" if parent_node is not None else "fifosim_trace.wdb" - ) - model.set_metadata_prop("rtlsim_trace", str(report_dir.absolute()) + tracefile) - - model = model.transform( - BuildSimulation( - cfg._resolve_fpga_part(), - cfg._resolve_hls_clk_period(), - cfg.functional_simulation, - performance_sim=performance_sim, - ) - ) - return model - - -@register_build_dataflow_step() -def step_size_fifo_connected(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Simulate layers connected and use the observed behaviour to size the FIFOs accordingly.""" - model = model.transform( - RunLayerParallelSimulation(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg) - ) - return model - - -@register_build_dataflow_step() -def step_apply_fifosizes(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: - """Apply the previously found FIFO sizes to the model.""" - model = model.transform(ApplySimulatedFIFOSizes(cfg)) - if cfg.split_large_fifos: - model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) - model = model.transform(GiveUniqueNodeNamesRecursive()) - model = model.transform(GiveReadableTensorNames()) - return model - - @register_build_dataflow_step() def step_set_fifo_depths( model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None @@ -473,8 +425,29 @@ def step_set_fifo_depths( report_dir.mkdir(parents=True, exist_ok=True) model.set_metadata_prop("rtlsim_trace", str(report_dir.resolve() / "fifosim_trace.wdb")) if cfg.auto_fifo_strategy == AutoFIFOSizingMethod.DISTRIBUTED_SIMULATION: - model = step_build_simulation(model, cfg, parent_node=parent_node) - model = step_size_fifo_connected(model, cfg) + if cfg.fifosim_save_waveform: + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) + tracefile = ( + f"{parent_node}_fifosim_trace.wdb" + if parent_node is not None + else "fifosim_trace.wdb" + ) + model.set_metadata_prop("rtlsim_trace", str(report_dir.absolute()) + tracefile) + + model = model.transform( + BuildSimulation( + cfg._resolve_fpga_part(), + cfg._resolve_hls_clk_period(), + cfg.functional_simulation, + performance_sim=False, + ) + ) + model = model.transform( + RunLayerParallelSimulation( + cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period(), cfg + ) + ) model = model.transform(ApplySimulatedFIFOSizes(cfg)) elif cfg.auto_fifo_strategy == AutoFIFOSizingMethod.LIVE_FIFO: hw_attrs = [ diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index 3597299dec..8780626ddb 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -27,41 +27,65 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from collections.abc import Callable + +from finn_xsi.sim_engine import SimEngine import numpy as np -import os -import shlex -import subprocess -import sys from pathlib import Path -from qonnx.custom_op.registry import getCustomOp -from subprocess import CalledProcessError +from finn.util.basic import getHWCustomOp -from finn import xsi +from finn import xsi as finnxsi from finn.util.basic import ( get_liveness_threshold_cycles, - get_vivado_root, - launch_process_helper, make_build_dir, ) from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy -from finn.util.exception import FINNConfigurationError, FINNError, FINNInternalError, FINNUserError -from finn.util.logging import log -finnxsi = xsi if xsi.is_available() else None +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from qonnx.core.datatype import BaseDataType + from qonnx.core.modelwrapper import ModelWrapper + +from finn.util.exception import FINNUserError + +from ast import literal_eval -def prep_rtlsim_io_dict(model, execution_context): +def prep_rtlsim_io_dict( + model: "ModelWrapper", execution_context: dict[str, np.ndarray] +) -> tuple[ + dict[str, dict[str, list[int]]], + dict[str, list[tuple[str, int]] | list[str]], + dict[str, int] | int, + list[tuple[int, "BaseDataType", tuple[int, ...], tuple[int, ...]]], + int, +]: """Prepare the input/output dictionary for RTLSim execution.""" # extract i/o info to prepare io_dict io_dict = {"inputs": {}, "outputs": {}} - if_dict = eval(model.get_metadata_prop("vivado_stitch_ifnames")) + if_names = model.get_metadata_prop("vivado_stitch_ifnames") + if if_names is None: + raise FINNUserError( + "Vivado stitch interface names not found in model metadata. " + "Did you run step_create_stitched_ip first?" + ) + if_dict: dict[str, list[tuple[str, int]] | list[str]] = literal_eval(if_names) # go over and prepare inputs + batchsize = None + first_node = None + if_name = None for i, i_vi in enumerate(model.graph.input): i_name = i_vi.name i_tensor = execution_context[i_name] i_dt = model.get_tensor_datatype(i_name) first_node_onnx = model.find_consumer(i_name) - first_node = getCustomOp(first_node_onnx) + if first_node_onnx is None: + raise FINNUserError( + f"Input {i_name} has no consumer node in the model. " + f"Check that the inputs are all properly connected." + ) + first_node = getHWCustomOp(first_node_onnx) node_inp_ind = list(first_node_onnx.input).index(i_name) if node_inp_ind == 0: # default node input (input 0) @@ -90,8 +114,14 @@ def prep_rtlsim_io_dict(model, execution_context): if_name = if_dict["s_axis"][i][0] io_dict["inputs"][if_name] = packed_input # go over outputs to determine how many values will be produced - num_out_values = {} - o_tensor_info = [] + num_out_values: dict[str, int] | int = {} + o_tensor_info: list[tuple[int, BaseDataType, tuple[int, ...], tuple[int, ...]]] = [] + if first_node is None or batchsize is None or if_name is None: + raise FINNUserError( + "No consumer node found for first input. " + "Cannot determine output stream widths and number of output values. " + "Check that the inputs are all properly connected and consumed by a node." + ) for o, o_vi in enumerate(model.graph.output): # output in io_dict just needs an empty list if_name = if_dict["m_axis"][o][0] @@ -99,8 +129,20 @@ def prep_rtlsim_io_dict(model, execution_context): # extract output shape o_name = o_vi.name o_shape = model.get_tensor_shape(o_name) + if o_shape is None: + raise FINNUserError( + f"Shape of output {o_name} is not known. " + f"Cannot determine number of output values. " + f"Check that the model is properly inferred and shapes are known." + ) o_dt = model.get_tensor_datatype(o_name) - last_node = getCustomOp(model.find_producer(o_name)) + last_node_onnx = model.find_producer(o_name) + if last_node_onnx is None: + raise FINNUserError( + f"Output {o_name} has no producer node in the model. " + f"Check that the outputs are all properly connected." + ) + last_node = getHWCustomOp(last_node_onnx) o_folded_shape = last_node.get_folded_output_shape() # override batch size from actual input o_shape = list(o_shape) @@ -121,247 +163,37 @@ def prep_rtlsim_io_dict(model, execution_context): return io_dict, if_dict, num_out_values, o_tensor_info, batchsize -def file_to_basename(x: str | Path) -> str: - """Given a path return it's name (basename), without any symlinks.""" - # return str(Path(x).resolve()) - return os.path.basename(os.path.realpath(x)) - - -def rtlsim_exec_cppxsi( - model, - execution_context, - is_single_node: bool, - total_nodes: int = 1, - current_node_index: int | None = None, - previous_node_name: str | None = None, - dummy_data_mode=False, - timeout_cycles=None, - throttle_cycles=0, -): - """Use XSI C++ rtl simulation to execute given model with stitched IP. - The dummy_data_mode flag controls whether the simulation is driven by - dummy data or real data. The execution_context parameter must be formatted - according to whether dummy or real data is used. - Example with dummy_data = True: - execution_context = { - "inputs" : {"" : }, - "outputs" : {"" : }, - } - Example with dummy_data = False: - execution_context = { - "" : - }. - - If timeout_cycles is not None, the default value from get_liveness_threshold_cycles - will be used. - throttle_cycles will be used to pause the input stream every time an input frame is finished. - """ - # TODO: support running functional rtlsim with real I/O data - # TODO: support running with multiple inputs/outputs - if timeout_cycles is None: - timeout_cycles = get_liveness_threshold_cycles() - - assert dummy_data_mode, "Only dummy_data_mode=True is supported for now" - if finnxsi is None: - raise FINNConfigurationError("Cannot execute RTLSIM since finn_xsi is not available!") - - # ensure stitched ip project already exists - assert os.path.isfile( - model.get_metadata_prop("wrapper_filename") - ), """The - file name from metadata property "wrapper_filename" doesn't exist.""" - assert os.path.isdir( - model.get_metadata_prop("vivado_stitch_proj") - ), """The - directory from metadata property "vivado_stitch_proj" doesn't exist""" - trace_file = model.get_metadata_prop("rtlsim_trace") - if not dummy_data_mode: - # ignore last value which would be batchsize - io_dict, if_dict, num_out_values, o_tensor_info = prep_rtlsim_io_dict( - model, execution_context - )[:-1] - - # prepare rtlsim compiled object (unless it already exists) - rtlsim_so = model.get_metadata_prop("rtlsim_so") - top_module_file_name = file_to_basename(model.get_metadata_prop("wrapper_filename")) - top_module_name = top_module_file_name.strip(".v") - if (rtlsim_so is None) or (not os.path.isfile(rtlsim_so)): - vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") - with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt") as f: - all_verilog_srcs = f.read().split() - rtlsim_name = model.graph.node[0].name if is_single_node else top_module_name - single_src_dir = make_build_dir("rtlsim_" + rtlsim_name + "_") - debug = not (trace_file is None or trace_file == "") - rtlsim_so = finnxsi.compile_sim_obj( - top_module_name, all_verilog_srcs, single_src_dir, debug=debug, behav=True - ) - # save generated lib filename in attribute - model.set_metadata_prop("rtlsim_so", rtlsim_so[0] + "/" + rtlsim_so[1]) - sim_base, sim_rel = rtlsim_so - # pass in correct tracefile from attribute - if trace_file == "default": - trace_file = top_module_file_name + ".wdb" - else: - sim_base, sim_rel = rtlsim_so.split("xsim.dir") - sim_rel = "xsim.dir" + sim_rel - - # TODO: There has to be a better solution than using the relative path - - # 1. Assume we are in a git repository - finnxsi_dir = Path(__file__).parent.parent.parent.parent / "finn_xsi" / "finn_xsi" - fifosim_config_fname = finnxsi_dir / "rtlsim_config.hpp.template" - - # 2. We have to assume that we are in site-packages/finn/core - if not fifosim_config_fname.exists(): - finnxsi_dir = Path(__file__).parent.parent.parent / "finn_xsi" - fifosim_config_fname = finnxsi_dir / "rtlsim_config.hpp.template" - - # Where are we? - if not fifosim_config_fname.exists(): - raise FINNInternalError("The finn_xsi directory could not be found. Stopping here.") - - instream_iters = [] - outstream_iters = [] - for top_inp in model.graph.input: - iname = top_inp.name - first_node = model.find_consumer(iname) - assert first_node is not None, "Failed to find consumer for " + iname - fnode_inst = getCustomOp(first_node) - top_ind = list(first_node.input).index(iname) - ishape_folded = fnode_inst.get_folded_input_shape(ind=top_ind) - instream_iters.append(np.prod(ishape_folded[:-1])) - for top_out in model.graph.output: - oname = top_out.name - last_node = model.find_producer(oname) - assert last_node is not None, "Failed to find producer for " + oname - lnode_inst = getCustomOp(last_node) - top_ind = list(last_node.output).index(oname) - oshape_folded = lnode_inst.get_folded_output_shape(ind=top_ind) - outstream_iters.append(np.prod(oshape_folded[:-1])) - - # retrieve the number of inputs from execution_context - n_inferences = execution_context[model.get_first_global_in()] - ifnames = model.get_metadata_prop("vivado_stitch_ifnames") - assert ( - ifnames is not None - ), "Couldn't find stitched-IP interface names, did you run IP stitching first?" - ifnames = eval(ifnames) - if "aximm" in ifnames.keys() and ifnames["aximm"] != []: - assert ( - False - ), f"cppxsi sim doesn't know how to handle full AXI MM interfaces: {ifnames['aximm']}" - instream_names = [x[0] for x in ifnames["s_axis"]] - outstream_names = [x[0] for x in ifnames["m_axis"]] - instream_descrs = [ - (instream_names[i], instream_iters[i], instream_iters[i] + throttle_cycles) - for i in range(len(instream_names)) - ] - instream_descrs_str = str(instream_descrs).replace("[", "").replace("]", "") - instream_descrs_str = instream_descrs_str.replace("(", "{").replace(")", "}") - instream_descrs_str = instream_descrs_str.replace("'", '"') - - outstream_descrs = [ - (outstream_names[i], outstream_iters[i], outstream_iters[i]) - for i in range(len(outstream_names)) - ] - outstream_descrs_str = str(outstream_descrs).replace("[", "").replace("]", "") - outstream_descrs_str = outstream_descrs_str.replace("(", "{").replace(")", "}") - outstream_descrs_str = outstream_descrs_str.replace("'", '"') - - # fill in the template arguments for sim config - template_dict = { - # number of inferences - "N_INFERENCES": n_inferences, - # max number of cycles to wait for output activity before timeout - "TIMEOUT_CYCLES": timeout_cycles, - # name of the top-level HDL module - "TOP_MODULE_NAME": top_module_name, - # top-level AXI stream descriptors - "ISTREAM_DESC": instream_descrs_str, - "ISTREAM_LEN": len(instream_names), - "OSTREAM_DESC": outstream_descrs_str, - "OSTREAM_LEN": len(outstream_names), - # control tracing and trace filename - "TRACE_FILE": "nullptr" if trace_file is None else f'"{trace_file}"', - # sim kernel .so to use (depends on Vivado version) - "SIMKERNEL_SO": finnxsi.get_simkernel_so(), - # log file for xsi (not the sim driver) - "XSIM_LOG_FILE": '"xsi.log"', - # Node name in case of single-node simulation - "NODE_NAME": model.graph.node[0].name, - # Previous node name (for single node simulation) - "PREVIOUS_NODE_NAME": "std::nullopt" - if previous_node_name is None - else f'"{previous_node_name}"', - "NODE_INDEX": current_node_index if is_single_node else 0, - "TOTAL_NODES": total_nodes, - } - - fifosim_config_fname = Path(finnxsi_dir) / "rtlsim_config.hpp.template" - fsim_config = fifosim_config_fname.read_text() - for key, val in template_dict.items(): - fsim_config = fsim_config.replace(f"@{key}@", str(val)) - - # Write the config to the simulation directory - rtlsim_config = Path(sim_base) / "rtlsim_config.hpp" - rtlsim_config.write_text(fsim_config) - - # Building the whole simulation - # Running CMake first - cmake_call = f"{sys.executable} -m cmake -S {finnxsi_dir} -B {sim_base}" - log.info(f"Running cmake on RTLSIM Wrapper in {sim_base}") - try: - launch_process_helper( - shlex.split(cmake_call), cwd=finnxsi_dir, print_stdout=True, proc_env=os.environ.copy() - ) - except CalledProcessError as e: - raise FINNError(f"Failed to run cmake in {sim_base}") from e - - # Calling make to actually build the simulation - makefile = Path(sim_base) / "Makefile" - if not makefile.exists(): - raise FINNUserError(f"Failed to create Makefile in {sim_base}!") - try: - launch_process_helper(["make"], proc_env=os.environ.copy(), cwd=sim_base) - except CalledProcessError as e: - raise FINNUserError(f"Failed to create executable in {sim_base}!") from e - - # TODO: Fix name for general rtlsim - simulation_executable = Path(sim_base) / "LayerSimulationBackend" - assert simulation_executable.exists() - - # Prepare the script to run the simulation - # (important to specify LD_LIBRARY_PATH here for XSI to work correctly) - runsim = Path(sim_base) / "run_fifosim.sh" - ld_library_path = get_vivado_root() + "/lib/lnx64.o" - runsim.write_text(f"LD_LIBRARY_PATH={ld_library_path}:$LD_LIBRARY_PATH {simulation_executable}") - - # Actually run the simulation - subprocess.run( - ["bash", runsim.name], cwd=sim_base, stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - - # parse results file and return dict - # TODO - return {} - - -def rtlsim_exec_finnxsi(model, execution_context, pre_hook=None, post_hook=None): +def rtlsim_exec_finnxsi( + model: "ModelWrapper", + execution_context: dict[str, np.ndarray], + pre_hook: Callable[[SimEngine], None] | None = None, + post_hook: Callable[[SimEngine], None] | None = None, +) -> None: """Use finnxsi to execute given model with stitched IP. The execution context contains the input values. Hook functions can be optionally specified to observe/alter the state of the circuit - pre_hook : hook function to be called before sim start (after reset) - - post_hook : hook function to be called after sim end + - post_hook : hook function to be called after sim end. """ # ensure stitched ip project already exists - assert os.path.isfile( - model.get_metadata_prop("wrapper_filename") - ), """The - file name from metadata property "wrapper_filename" doesn't exist.""" - assert os.path.isdir( - model.get_metadata_prop("vivado_stitch_proj") - ), """The - directory from metadata property "vivado_stitch_proj" doesn't exist""" + wrapper_filename = model.get_metadata_prop("wrapper_filename") + if wrapper_filename is None: + wrapper_filename = "" + wrapper_filename = Path(wrapper_filename) + if not wrapper_filename.is_file(): + raise FINNUserError( + f"Wrapper file {wrapper_filename} doesn't exist. " + f"Did you run step_create_stitched_ip first?" + ) + vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") + if vivado_stitch_proj_dir is None: + vivado_stitch_proj_dir = "" + vivado_stitch_proj_dir = Path(vivado_stitch_proj_dir) + if not vivado_stitch_proj_dir.is_dir(): + raise FINNUserError( + f"Directory {vivado_stitch_proj_dir} doesn't exist. " + f"Did you run step_create_stitched_ip first?" + ) trace_file = model.get_metadata_prop("rtlsim_trace") io_dict, if_dict, num_out_values, o_tensor_info, batchsize = prep_rtlsim_io_dict( model, execution_context @@ -369,19 +201,18 @@ def rtlsim_exec_finnxsi(model, execution_context, pre_hook=None, post_hook=None) # prepare rtlsim model rtlsim_so = model.get_metadata_prop("rtlsim_so") - if (rtlsim_so is None) or (not os.path.isfile(rtlsim_so)): - vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") - with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt") as f: + if (rtlsim_so is None) or (not Path(rtlsim_so).is_file()): + with (vivado_stitch_proj_dir / "all_verilog_srcs.txt").open() as f: all_verilog_srcs = f.read().split() - top_module_file_name = file_to_basename(model.get_metadata_prop("wrapper_filename")) + top_module_file_name = wrapper_filename.name top_module_name = top_module_file_name.strip(".v") - single_src_dir = make_build_dir("rtlsim_" + top_module_name + "_") + single_src_dir = Path(make_build_dir("rtlsim_" + top_module_name + "_")) debug = not (trace_file is None or trace_file == "") rtlsim_so = finnxsi.compile_sim_obj( top_module_name, all_verilog_srcs, single_src_dir, debug=debug ) # save generated lib filename in attribute - model.set_metadata_prop("rtlsim_so", rtlsim_so[0] + "/" + rtlsim_so[1]) + model.set_metadata_prop("rtlsim_so", str(rtlsim_so[0] / rtlsim_so[1])) sim_base, sim_rel = rtlsim_so # pass in correct tracefile from attribute if trace_file == "default": @@ -390,7 +221,7 @@ def rtlsim_exec_finnxsi(model, execution_context, pre_hook=None, post_hook=None) else: sim_base, sim_rel = rtlsim_so.split("xsim.dir") sim_rel = "xsim.dir" + sim_rel - sim = finnxsi.load_sim_obj(sim_base, sim_rel, trace_file) + sim = finnxsi.load_sim_obj(Path(sim_base), Path(sim_rel), trace_file) # reset and call rtlsim, including any pre/post hooks finnxsi.reset_rtlsim(sim) @@ -422,12 +253,17 @@ def rtlsim_exec_finnxsi(model, execution_context, pre_hook=None, post_hook=None) model.set_metadata_prop("cycles_rtlsim", str(n_cycles)) -def rtlsim_exec(model, execution_context, pre_hook=None, post_hook=None): +def rtlsim_exec( + model: "ModelWrapper", + execution_context: dict[str, np.ndarray], + pre_hook: Callable[[SimEngine], None] | None = None, + post_hook: Callable[[SimEngine], None] | None = None, +) -> None: """Use XSI to execute given model with stitched IP. The execution context contains the input values. Hook functions can be optionally specified to observe/alter the state of the circuit, receiving the sim object as their first argument: - pre_hook : hook function to be called before sim start (after reset) - - post_hook : hook function to be called after sim end + - post_hook : hook function to be called after sim end. """ rtlsim_exec_finnxsi(model, execution_context, pre_hook, post_hook) diff --git a/src/finn/xsi/__init__.py b/src/finn/xsi/__init__.py index ce389d13dd..92effbbaaf 100644 --- a/src/finn/xsi/__init__.py +++ b/src/finn/xsi/__init__.py @@ -24,6 +24,7 @@ from pathlib import Path from typing import Any from finn.util.logging import log +from finn.util.exception import FINNUserError # Track if auto-install has been attempted @@ -32,7 +33,6 @@ # Cache for loaded modules _adapter_module: Any | None = None _sim_engine_module: Any | None = None -_xsi_module: Any | None = None def is_available() -> bool: @@ -98,7 +98,7 @@ def _attempt_auto_install() -> bool: def _load_modules() -> bool: """Load finn_xsi modules if available.""" - global _adapter_module, _sim_engine_module, _xsi_module + global _adapter_module, _sim_engine_module if _adapter_module is not None: return True @@ -118,9 +118,7 @@ def _load_modules() -> bool: try: import finn_xsi.adapter import finn_xsi.sim_engine - import xsi - _xsi_module = xsi _adapter_module = finn_xsi.adapter _sim_engine_module = finn_xsi.sim_engine @@ -142,47 +140,19 @@ def _load_modules() -> bool: sys.path.remove(str(xsi_path)) -# List of functions to wrap from finn_xsi.adapter -_ADAPTER_FUNCTIONS = [ - "locate_glbl", - "compile_sim_obj", - "get_simkernel_so", - "load_sim_obj", - "reset_rtlsim", - "close_rtlsim", - "rtlsim_multi_io", -] - - -def __getattr__(name: str) -> Any: - """Dynamically wrap finn_xsi.adapter functions.""" - if name in _ADAPTER_FUNCTIONS: - - def _wrapper(*args, **kwargs): - if not _load_modules(): - raise ImportError("finn_xsi not available. Run: python -m finn.xsi.setup") - return getattr(_adapter_module, name)(*args, **kwargs) - - _wrapper.__name__ = name - _wrapper.__doc__ = f"Wrapper for finn_xsi.adapter.{name}" - return _wrapper - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - -# SimEngine class wrapper -class SimEngine: - """Wrapper for finn_xsi.sim_engine.SimEngine.""" - - def __init__(self, *args, **kwargs) -> None: - """Create a new SimEngine.""" - if not _load_modules(): - raise ImportError("finn_xsi not available. Run: python -m finn.xsi.setup") - self._engine = _sim_engine_module.SimEngine(*args, **kwargs) - - def __getattr__(self, name: str) -> Any: - """Get attribute of the given name.""" - return getattr(self._engine, name) - - # Trigger auto-install at import time -is_available() +xsi_avail = is_available() + +if xsi_avail is False: + raise FINNUserError("XSI not available. Please run 'finn deps update' to install XSI.") + +from finn_xsi.sim_engine import SimEngine # noqa +from finn_xsi.adapter import ( #noqa + locate_glbl, + compile_sim_obj, + get_simkernel_so, + load_sim_obj, + reset_rtlsim, + close_rtlsim, + rtlsim_multi_io, +) diff --git a/tests/fpgadataflow/test_fpgadataflow_mvau.py b/tests/fpgadataflow/test_fpgadataflow_mvau.py index e4bf665612..93d3f11e6b 100644 --- a/tests/fpgadataflow/test_fpgadataflow_mvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_mvau.py @@ -26,6 +26,10 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from finn.builder.build_dataflow_config import DataflowBuildConfig +from finn.transformation.fpgadataflow.simulation import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation import pytest import numpy as np @@ -42,13 +46,12 @@ import finn.core.onnx_exec as oxe import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw -from finn import xsi +from finn import xsi as finnxsi from finn.analysis.fpgadataflow.exp_cycles_per_layer import exp_cycles_per_layer from finn.analysis.fpgadataflow.hls_synth_res_estimation import hls_synth_res_estimation from finn.core.rtlsim_exec import rtlsim_exec from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP -from finn.transformation.fpgadataflow.derive_characteristic import DeriveCharacteristic from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.minimize_accumulator_width import MinimizeAccumulatorWidth from finn.transformation.fpgadataflow.minimize_weight_bit_width import MinimizeWeightBitWidth @@ -56,14 +59,30 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.general import ApplyConfig from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds from finn.util.basic import is_versal -finnxsi = xsi if xsi.is_available() else None - +from finn.xsi import SimEngine + +def InsertAndSetFIFODepths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform( + RunLayerParallelSimulation( + fpga_part, clk_ns, cfg + ) + ) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model def make_single_fclayer_modelwrapper(W, pe, simd, wdt, idt, odt, T=None, tdt=None): mw = W.shape[0] @@ -596,7 +615,7 @@ def test_fpgadataflow_mvau_large_depth_decoupled_mode_rtlsim( assert exp_cycles != 0 # Run stitched-ip RTLsim to have memstream in the test loop - model = model.transform(InsertAndSetFIFODepths(part, clk_ns)) + model = InsertAndSetFIFODepths(model, part, clk_ns) model = model.transform(PrepareIP(part, clk_ns)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP(part, clk_ns)) @@ -620,9 +639,9 @@ def test_fpgadataflow_mvau_large_depth_decoupled_mode_rtlsim( weight_stream = list(weight_stream) # helper functions to write or read axilite - def write_weights(sim): + def write_weights(sim: SimEngine) -> None: addr = 0 - writes = [] + writes: list[tuple[int, str]] = [] for nw in weight_stream: # convert value to hex value and without '0x' prefix hex_val = format(nw, "x") @@ -634,7 +653,7 @@ def write_weights(sim): extracted_weight_stream = [] - def read_weights(sim): + def read_weights(sim: SimEngine) -> None: addr = 0 read_handles = [] addresses = [] @@ -657,85 +676,6 @@ def read_weights(sim): y_expected == output_mvau_rtl_stitch ).all(), "Output of ONNX model not matching output of stitched-IP RTL model!" - -# mem_mode: internal_embedded or internal_decoupled -@pytest.mark.parametrize("mem_mode", ["internal_decoupled", "internal_embedded"]) -# activation: None or DataType -@pytest.mark.parametrize("act", [None, DataType["INT4"]]) -# weight datatype -@pytest.mark.parametrize("wdt", [DataType["INT4"]]) -# input datatype -@pytest.mark.parametrize("idt", [DataType["INT4"]]) -# neuron folding, -1 is maximum possible -@pytest.mark.parametrize("nf", [8]) -# synapse folding, -1 is maximum possible -@pytest.mark.parametrize("sf", [8]) -# HLS matrix width (input features) -@pytest.mark.parametrize("mw", [32]) -# HLS matrix height (output features) -@pytest.mark.parametrize("mh", [32]) -# Backend -@pytest.mark.parametrize("preferred_impl_style", ["hls", "rtl"]) -@pytest.mark.fpgadataflow -@pytest.mark.vivado -def test_mvau_fifocharacterize_rtlsim( - mem_mode, idt, wdt, act, nf, sf, mw, mh, preferred_impl_style -): - if preferred_impl_style == "rtl" and (mem_mode == "internal_embedded" or act is not None): - pytest.skip("RTL-MVAU doesn't support const mem mode or embedded activations") - if nf == -1: - nf = mh - if sf == -1: - sf = mw - pe = mh // nf - simd = mw // sf - assert mh % pe == 0 - assert mw % sf == 0 - # generate weights - W = gen_finn_dt_tensor(wdt, (mw, mh)) - - # no activation, produce accumulators - T = None - tdt = None - if wdt == DataType["BIPOLAR"] and idt == DataType["BIPOLAR"]: - odt = DataType["UINT32"] - else: - odt = DataType["INT32"] - - model = make_single_fclayer_modelwrapper(W, pe, simd, wdt, idt, odt, T, tdt) - for node in model.graph.node: - # lookup op_type in registry of CustomOps - inst = getCustomOp(node) - inst.set_nodeattr("mem_mode", mem_mode) - inst.set_nodeattr("resType", "auto") - inst.set_nodeattr("preferred_impl_style", preferred_impl_style) - total_fold = nf * sf - exp_total_cycles = int(np.ceil(total_fold * 1.2)) - model = model.transform(SpecializeLayers("xczu7ev-ffvc1156-2-e")) - model = model.transform(MinimizeWeightBitWidth()) - model = model.transform(MinimizeAccumulatorWidth()) - model = model.transform(SetExecMode("rtlsim")) - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(PrepareIP("xczu7ev-ffvc1156-2-e", 5)) - model = model.transform(HLSSynthIP()) - model = model.transform(PrepareRTLSim()) - model = model.transform(DeriveCharacteristic(exp_total_cycles)) - node_inst = getCustomOp(model.graph.node[0]) - period_attr = node_inst.get_nodeattr("io_chrc_period") - assert period_attr == exp_total_cycles - chrc_in = node_inst.get_nodeattr("io_chrc_in") - chrc_out = node_inst.get_nodeattr("io_chrc_out") - if mem_mode == "internal_decoupled": - assert chrc_in.shape == (2, 2 * exp_total_cycles) - else: - assert chrc_in.shape == (1, 2 * exp_total_cycles) - assert chrc_out.shape == (1, 2 * exp_total_cycles) - # total number of transactions == 2*SF - assert chrc_in[0, -1] == 2 * sf - # all outputs should be produced within the exp n of cycles - assert chrc_out[0, exp_total_cycles] == nf - - @pytest.mark.parametrize("mh", [18]) @pytest.mark.parametrize("mw", [32]) @pytest.mark.parametrize("pe", [1, 9, 18]) @@ -836,7 +776,7 @@ def test_fpgadataflow_rtl_mvau( ).all(), "Output of ONNX model not matching output of node-by-node RTLsim!" # Run stitched-ip RTLsim - model = model.transform(InsertAndSetFIFODepths(part, clk_ns)) + model = InsertAndSetFIFODepths(model, part, clk_ns) model = model.transform(PrepareIP(part, clk_ns)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP(part, clk_ns)) @@ -947,7 +887,7 @@ def test_fpgadataflow_rtl_dynamic_mvau(mh, mw, n_vectors, pe, simd, idt_wdt, par ).all(), "Output of ONNX model not matching output of node-by-node RTLsim!" # Run stitched-ip RTLsim - model = model.transform(InsertAndSetFIFODepths(part, clk_ns)) + model = InsertAndSetFIFODepths(model, part, clk_ns) model = model.transform(SpecializeLayers(part)) model = model.transform(GiveUniqueNodeNames()) model = model.transform(PrepareIP(part, clk_ns)) From b4af93402132884a8641489ed2afaa61b976aa4b Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 18 May 2026 17:27:28 +0200 Subject: [PATCH 106/170] Fix unittests --- .gitignore | 1 + finn_xsi/finn_xsi/adapter.py | 2 +- finn_xsi/finn_xsi/sim_engine.py | 30 +++-- finn_xsi/finn_xsi/xsi_bind.cpp | 114 +++++++++--------- src/finn/builder/build_dataflow_config.py | 12 +- src/finn/custom_op/fpgadataflow/hlsbackend.py | 4 +- src/finn/custom_op/fpgadataflow/hwcustomop.py | 39 +++--- .../custom_op/fpgadataflow/rtl/finn_loop.py | 3 +- src/finn/custom_op/fpgadataflow/rtlbackend.py | 27 +++-- src/finn/interface/run_finn.py | 30 +---- src/finn/interface/settings.py | 2 + .../fpgadataflow/compile_cppsim.py | 42 ++++--- .../fpgadataflow/prepare_rtlsim.py | 38 ++++-- .../fpgadataflow/set_exec_mode.py | 26 ++-- .../fpgadataflow/simulation_build.py | 6 +- src/finn/xsi/setup.py | 48 +++++--- ...dataflow_convinputgenerator_rtl_dynamic.py | 3 +- .../test_fpgadataflow_thresholding.py | 25 +++- .../test_fpgadataflow_thresholding_runtime.py | 3 +- 19 files changed, 261 insertions(+), 194 deletions(-) diff --git a/.gitignore b/.gitignore index ad0f998716..e0f978fdff 100644 --- a/.gitignore +++ b/.gitignore @@ -68,6 +68,7 @@ poetry.lock **/_deps finn_xsi/finn_xsi/unittests/*.cmake finn_xsi/finn_xsi/unittests/Makefile +finn_xsi/finn_xsi/VERSION settings.yaml */.cache/* diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 782c85eaed..80a21def9b 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -93,7 +93,7 @@ def compile_sim_obj( cmd_xelab = [ "xelab", - "work." + "finn_design_wrapper", + "work." + top_module_name, "-relax", "-dll", "--O3", diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py index 2d24a35ad7..de85618dc0 100644 --- a/finn_xsi/finn_xsi/sim_engine.py +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -57,7 +57,13 @@ def __init__( """Create a simulation engine bound to the given kernel and design.""" top = xsi.Design(xsi.Kernel(kernel), design, log, wdb) clk = top.getPort("ap_clk") - clk2x = top.getPort("ap_clk2x") + for port in top.ports(): + print(port.name()) + # If clock pumping is disabled, set clk2x to None + try: + clk2x = top.getPort("ap_clk2x") + except RuntimeError: + clk2x = None for p in top.ports(): if p.isInput(): p.clear().write_back() @@ -97,7 +103,10 @@ def cycle(updates: dict[xsi.Port, str]) -> None: # Utility def get_bus_port(self, bus: str, suffix: str) -> "xsi.Port": """Return a port by bus name and suffix, trying lower/upper variants.""" - port = self.top.getPort(bus + "_" + suffix.lower()) + try: + port = self.top.getPort(bus + "_" + suffix.lower()) + except RuntimeError: + port = None return port if port is not None else self.top.getPort(bus + "_" + suffix.upper()) # ------------------------------------------------------------------------ @@ -136,7 +145,6 @@ def run(self, cycles: int | None = None) -> list[Watchdog]: # Execute Cycle self.ticks += 1 - print(f"Cycle {self.ticks}") strong = False for task in self.tasks: # Tasks read signals and derive updates to schedule for after the clock cycle @@ -193,9 +201,9 @@ def __init__( self, top: "SimEngine", istream: str, values: Generator[str], throttle: tuple ) -> None: """Bind to the stream ports and configure throttling.""" - self.vld: xsi.Port = top.get_bus_port(istream, "tvalid") - self.rdy: xsi.Port = top.get_bus_port(istream, "tready") - self.dat: xsi.Port = top.get_bus_port(istream, "tdata") + self.vld: xsi.Port = top.get_bus_port(istream, "TVALID") + self.rdy: xsi.Port = top.get_bus_port(istream, "TREADY") + self.dat: xsi.Port = top.get_bus_port(istream, "TDATA") self.values = values self.throttle = throttle @@ -249,9 +257,9 @@ def __init__( ) -> None: """Bind to the stream ports and prepare a buffer.""" self.size = size - self.vld = top.get_bus_port(ostream, "tvalid") - self.rdy = top.get_bus_port(ostream, "tready") - self.dat = top.get_bus_port(ostream, "tdata") + self.vld = top.get_bus_port(ostream, "TVALID") + self.rdy = top.get_bus_port(ostream, "TREADY") + self.dat = top.get_bus_port(ostream, "TDATA") self.buf: list[str] = [] self.watchdog = watchdog @@ -289,8 +297,8 @@ class StreamTracer: def __init__(self, sim: "SimEngine", stream: str) -> None: """Bind to the stream ports to trace handshakes.""" - self.vld = sim.get_bus_port(stream, "tvalid") - self.rdy = sim.get_bus_port(stream, "tready") + self.vld = sim.get_bus_port(stream, "TVALID") + self.rdy = sim.get_bus_port(stream, "TREADY") self.trace = "" def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG002 diff --git a/finn_xsi/finn_xsi/xsi_bind.cpp b/finn_xsi/finn_xsi/xsi_bind.cpp index fb95353d7f..a47a179bdd 100644 --- a/finn_xsi/finn_xsi/xsi_bind.cpp +++ b/finn_xsi/finn_xsi/xsi_bind.cpp @@ -8,76 +8,72 @@ * @author Thomas B. Preußer ***************************************************************************/ -#include #include #include - +#include #include -#include + #include +#include -namespace py = pybind11; +namespace py = pybind11; using namespace xsi; namespace { - std::mutex use_mutex; - std::map const> use_map; - struct DesignDeleter : public std::default_delete { - void operator()(Design *d) const { - std::default_delete::operator()(d); - std::lock_guard lock(use_mutex); - use_map.erase(use_map.find(d)); - } - }; -} + std::mutex use_mutex; + std::map const> use_map; + struct DesignDeleter : public std::default_delete { + void operator()(Design* d) const { + std::default_delete::operator()(d); + std::lock_guard lock(use_mutex); + use_map.erase(use_map.find(d)); + } + }; +} // namespace PYBIND11_MODULE(xsi, m) { + py::class_>(m, "Kernel").def(py::init()); - py::class_>(m, "Kernel") - .def(py::init()); - - py::class_>(m, "Design") - .def(py::init([]( - std::shared_ptr const &kernel, - std::string const &design_lib, - char const *const log_file, - char const *const wdb_file - ) { - std::unique_ptr d { new Design(*kernel, design_lib, log_file, wdb_file) }; - std::lock_guard lock(use_mutex); - use_map.emplace(d.get(), kernel); - return d; - })) - .def("trace_all", &Design::trace_all) - .def("run", &Design::run) - .def("restart", &Design::restart) - .def("get_status", &Design::get_status) - .def("get_error_info", &Design::get_error_info) - .def("num_ports", &Design::num_ports) - .def("getPort", static_cast(&Design::getPort)) - .def("ports", [](Design &d) { - auto const e = d.ports(); - return py::make_iterator(e.begin(), e.end()); - }); + py::class_>(m, "Design") + .def(py::init([](std::shared_ptr const& kernel, std::string const& design_lib, char const* const log_file, char const* const wdb_file) { + std::unique_ptr d{new Design(*kernel, design_lib, log_file, wdb_file)}; + std::lock_guard lock(use_mutex); + use_map.emplace(d.get(), kernel); + return d; + })) + .def("trace_all", &Design::trace_all) + .def("run", &Design::run) + .def("restart", &Design::restart) + .def("get_status", &Design::get_status) + .def("get_error_info", &Design::get_error_info) + .def("num_ports", &Design::num_ports) + .def("getPort", static_cast(&Design::getPort), py::return_value_policy::reference_internal) + .def( + "ports", + [](Design& d) { + auto const e = d.ports(); + return py::make_iterator(e.begin(), e.end(), py::return_value_policy::reference_internal); + }, + py::keep_alive<0, 1>()); - py::class_>(m, "Port") - .def("name", &Port::name) - .def("dir", &Port::dir) - .def("width", &Port::width) - .def("isInput", &Port::isInput) - .def("isOutput", &Port::isOutput) - .def("isInout", &Port::isInout) - .def("read", &Port::read) - .def("write_back", &Port::write_back) - .def("hasUnknown", &Port::hasUnknown) - .def("isZero", &Port::isZero) - .def("as_bool", &Port::as_bool) - .def("as_unsigned", &Port::as_unsigned) - .def("as_binstr", &Port::as_binstr) - .def("as_hexstr", &Port::as_hexstr) - .def("clear", &Port::clear) - .def("set", &Port::set) - .def("set_binstr", &Port::set_binstr) - .def("set_hexstr", &Port::set_hexstr); + py::class_>(m, "Port") + .def("name", &Port::name) + .def("dir", &Port::dir) + .def("width", &Port::width) + .def("isInput", &Port::isInput) + .def("isOutput", &Port::isOutput) + .def("isInout", &Port::isInout) + .def("read", &Port::read, py::return_value_policy::reference_internal) + .def("write_back", &Port::write_back) + .def("hasUnknown", &Port::hasUnknown) + .def("isZero", &Port::isZero) + .def("as_bool", &Port::as_bool) + .def("as_unsigned", &Port::as_unsigned) + .def("as_binstr", &Port::as_binstr) + .def("as_hexstr", &Port::as_hexstr) + .def("clear", &Port::clear, py::return_value_policy::reference_internal) + .def("set", &Port::set, py::return_value_policy::reference_internal) + .def("set_binstr", &Port::set_binstr, py::return_value_policy::reference_internal) + .def("set_hexstr", &Port::set_hexstr, py::return_value_policy::reference_internal); } diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index a6a3d42ed7..b965626fc8 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -622,13 +622,15 @@ def _resolve_driver_platform(self) -> Literal["zynq-iodma", "alveo"]: shell flows and "alveo" for Vitis Alveo shell flows. Raises: - Exception: If the shell flow type is not recognized or supported. + FINNConfigurationError: If the shell flow type is not recognized or supported. """ if self.shell_flow_type == ShellFlowType.VIVADO_ZYNQ: return "zynq-iodma" if self.shell_flow_type == ShellFlowType.VITIS_ALVEO: return "alveo" - raise Exception("Couldn't resolve driver platform for " + str(self.shell_flow_type)) + raise FINNConfigurationError( + "Couldn't resolve driver platform for " + str(self.shell_flow_type) + ) def _resolve_fpga_part(self) -> str: """Resolve the FPGA part identifier. @@ -641,7 +643,7 @@ def _resolve_fpga_part(self) -> str: str: The FPGA part identifier (e.g., "xc7z020clg400-1"). Raises: - Exception: If the FPGA part cannot be resolved from the board name or + FINNConfigurationError: If the FPGA part cannot be resolved from the board name or if the board is not found in the part map. """ if self.fpga_part is None: @@ -688,7 +690,7 @@ def _resolve_vitis_platform(self) -> str: str: The Vitis platform identifier (e.g., "xilinx_u250_xdma_201830_2"). Raises: - Exception: If neither vitis_platform nor board is specified, or if the + FINNConfigurationError: If neither vitis_platform nor board is specified, or if the platform cannot be resolved from the given information. """ if self.vitis_platform is not None: @@ -722,7 +724,7 @@ def _resolve_verification_io_pair(self) -> None | tuple[Any, Any]: if verification is enabled, None if verify_steps is None. Raises: - AssertionError: If either the input or expected output files cannot be found. + FINNConfigurationError: If either the input or expected output files cannot be found. """ if self.verify_steps is None: return None diff --git a/src/finn/custom_op/fpgadataflow/hlsbackend.py b/src/finn/custom_op/fpgadataflow/hlsbackend.py index 39eda40e6e..acae4ae281 100644 --- a/src/finn/custom_op/fpgadataflow/hlsbackend.py +++ b/src/finn/custom_op/fpgadataflow/hlsbackend.py @@ -114,14 +114,14 @@ def prepare_rtlsim(self, behav: bool = False) -> None: """Create a xsi emulation library for the RTL code generated for this node, sets the rtlsim_so attribute to its path.""" verilog_files = self.get_all_verilog_filenames(abspath=True) - single_src_dir = make_build_dir("rtlsim_" + self.onnx_node.name + "_") + single_src_dir = Path(make_build_dir("rtlsim_" + self.onnx_node.name + "_")) trace_file = self.get_nodeattr("rtlsim_trace") debug = not (trace_file is None or trace_file == "") ret = finnxsi.compile_sim_obj( self.get_verilog_top_module_name(), verilog_files, single_src_dir, debug, behav ) # save generated lib filename in attribute - self.set_nodeattr("rtlsim_so", ret[0] + "/" + ret[1]) + self.set_nodeattr("rtlsim_so", str(ret[0] / ret[1])) def code_generation_ipgen(self, model: "ModelWrapper", fpgapart: str, clk: float) -> None: """Generate C++ code and TCL script for IP generation.""" diff --git a/src/finn/custom_op/fpgadataflow/hwcustomop.py b/src/finn/custom_op/fpgadataflow/hwcustomop.py index f4c0dd0ba4..8bf152d468 100644 --- a/src/finn/custom_op/fpgadataflow/hwcustomop.py +++ b/src/finn/custom_op/fpgadataflow/hwcustomop.py @@ -36,7 +36,6 @@ import numpy.typing as npt from abc import abstractmethod from collections.abc import Sequence -from finn_xsi.sim_engine import SimEngine from onnx import NodeProto from pathlib import Path from qonnx.core.datatype import BaseDataType @@ -44,7 +43,6 @@ from qonnx.util.basic import roundup_to_integer_multiple from typing import TYPE_CHECKING, Any, cast -from finn import xsi from finn.util.basic import get_liveness_threshold_cycles, is_versal from finn.util.exception import FINNInternalError from finn.util.settings import get_settings @@ -53,7 +51,8 @@ from qonnx.core.modelwrapper import ModelWrapper from finn.transformation.fpgadataflow.loop_rolling import LoopBodyInputType -finnxsi = xsi if xsi.is_available() else None +import finn.xsi as finnxsi +from finn.xsi import SimEngine class HWCustomOp(CustomOp): @@ -180,25 +179,24 @@ def get_verilog_top_module_intf_names(self) -> dict[str, list[tuple[str, int]] | def get_rtlsim(self) -> SimEngine: """Return a xsi wrapper for the emulation library for this node.""" - import finn_xsi.adapter as finnxsi - - # without finnxsi dependency - - rtlsim_so = self.get_nodeattr("rtlsim_so") - if type(rtlsim_so) is not str: - raise FINNInternalError( - f"rtlsim_so attribute not set correctly in {self.onnx_node.name}, cannot get rtlsim" - ) - if not Path(rtlsim_so).is_file(): + rtlsim_so = Path(cast("str", self.get_nodeattr("rtlsim_so"))) + if not rtlsim_so.is_file(): raise FINNInternalError( f"rtlsim_so attribute points to non-existent file in {self.onnx_node.name}, " "cannot get rtlsim" ) - sim_base, sim_rel = rtlsim_so.split("xsim.dir") - sim_rel = "xsim.dir" + sim_rel + rtlsim_parts = rtlsim_so.parts + try: + xsim_idx = rtlsim_parts.index("xsim.dir") + except ValueError as exc: + raise FINNInternalError( + f"rtlsim_so path does not contain xsim.dir for {self.onnx_node.name}" + ) from exc + sim_base = Path(*rtlsim_parts[:xsim_idx]) + sim_rel = Path(*rtlsim_parts[xsim_idx:]) # pass in correct tracefile from attribute - tracefile = self.get_nodeattr("rtlsim_trace") + tracefile = cast("str", self.get_nodeattr("rtlsim_trace")) if tracefile == "default": tracefile = self.onnx_node.name + ".wdb" sim = finnxsi.load_sim_obj(sim_base, sim_rel, tracefile) @@ -212,9 +210,6 @@ def close_rtlsim(self, sim: SimEngine) -> None: sim: The RTL simulation object to close. """ - import finn_xsi.adapter as finnxsi - - # without finnxsi dependency finnxsi.close_rtlsim(sim) def node_res_estimation(self, fpgapart: str) -> dict[str, int | float]: @@ -301,16 +296,10 @@ def get_op_and_param_counts(self) -> dict[str, int]: def reset_rtlsim(self, sim: SimEngine) -> None: """Set reset input in finnxsi to zero, toggle the clock and set it back to one.""" - import finn_xsi.adapter as finnxsi - - # without finnxsi dependency finnxsi.reset_rtlsim(sim) def rtlsim_multi_io(self, sim: SimEngine, io_dict: dict[str, Any], sname: str = "_V") -> None: """Run rtlsim for this node, supports multiple i/o streams.""" - import finn_xsi.adapter as finnxsi - - # without finnxsi dependency num_out_values = self.get_number_output_values() # Use the larger of expected cycles or liveness threshold exp_cycles = self.get_exp_cycles() diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index 25652d7ca7..3c2a0e4916 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -43,7 +43,6 @@ from typing import cast import finn.core.onnx_exec as oxe -from finn import xsi from finn.analysis.fpgadataflow.dataflow_performance import dataflow_performance from finn.custom_op.fpgadataflow import templates from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp @@ -56,7 +55,7 @@ from finn.util.mlo_sim import mlo_prehook_func_factory from finn.util.settings import get_settings -finnxsi = xsi if xsi.is_available() else None +import finn.xsi as finnxsi def collect_ip_dirs(model, ipstitch_path): diff --git a/src/finn/custom_op/fpgadataflow/rtlbackend.py b/src/finn/custom_op/fpgadataflow/rtlbackend.py index 889a2a4b2e..ba9c9e62e1 100644 --- a/src/finn/custom_op/fpgadataflow/rtlbackend.py +++ b/src/finn/custom_op/fpgadataflow/rtlbackend.py @@ -43,14 +43,13 @@ from onnx import GraphProto from qonnx.core.modelwrapper import ModelWrapper -from finn import xsi from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.basic import make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy from finn.util.exception import FINNInternalError from finn.util.logging import log -finnxsi = xsi if xsi.is_available() else None +import finn.xsi as finnxsi class RTLBackend(HWCustomOp, ABC): @@ -96,24 +95,26 @@ def generate_hdl(self, model: "ModelWrapper", fpgapart: str, clk: float) -> None None """ - def prepare_rtlsim(self) -> None: + def prepare_rtlsim(self, behav: bool) -> None: """Create a xsi emulation library for the RTL code generated for this node. Sets the rtlsim_so attribute to the path of the generated library. Returns: None """ - import finn_xsi.adapter as finnxsi - verilog_files = self.get_rtl_file_list(abspath=True) - single_src_dir = make_build_dir("rtlsim_" + self.onnx_node.name + "_") + single_src_dir = Path(make_build_dir("rtlsim_" + self.onnx_node.name + "_")) trace_file = self.get_nodeattr("rtlsim_trace") debug = not (trace_file is None or trace_file == "") ret = finnxsi.compile_sim_obj( - self.get_verilog_top_module_name(), verilog_files, single_src_dir, debug + self.get_verilog_top_module_name(), + [str(f) for f in verilog_files], + single_src_dir, + debug, + behav, ) # save generated lib filename in attribute - self.set_nodeattr("rtlsim_so", ret[0] + "/" + ret[1]) + self.set_nodeattr("rtlsim_so", str(ret[0] / ret[1])) def get_verilog_paths(self) -> list[str]: """Return path to code gen directory. @@ -162,7 +163,9 @@ def code_generation_ipgen(self, model: "ModelWrapper", fpgapart: str, clk: float self.generate_hdl(model, fpgapart, clk) def execute_node( - self, context: dict[str, npt.NDArray], graph: "GraphProto" # noqa: ARG002 + self, + context: dict[str, npt.NDArray], + graph: "GraphProto", # noqa: ARG002 ) -> None: """Execute this node's RTL simulation. @@ -246,9 +249,9 @@ def execute_node( output = np.asarray([output], dtype=np.float32).reshape(*exp_oshape) context[outp] = output - assert ( - context[outp].shape == exp_oshape - ), "Output shape doesn't match expected shape." + assert context[outp].shape == exp_oshape, ( + "Output shape doesn't match expected shape." + ) else: raise Exception( diff --git a/src/finn/interface/run_finn.py b/src/finn/interface/run_finn.py index 562cde35c8..c471c6a22a 100644 --- a/src/finn/interface/run_finn.py +++ b/src/finn/interface/run_finn.py @@ -465,17 +465,6 @@ def prepare_finn( error(f"FINN ERROR: {e}") sys.exit(1) - # Even if we dont update deps, we still need to make xsi available - finn_xsi = Path(resolve_module_path("finn_xsi")) - os.environ["FINN_XSI"] = str(finn_xsi) - finn_xsi_so = finn_xsi / "xsi.so" - if not finn_xsi_so.exists(): - error(f"finn_xsi was not found at {finn_xsi}") - sys.exit(1) - status(f"Loading finn_xsi from {finn_xsi}") - os.environ["PYTHONPATH"] = f"{os.environ['PYTHONPATH']}:{finn_xsi.absolute()}" - sys.path.append(str(finn_xsi)) - # Check synthesis tools set_synthesis_tools_paths() @@ -485,9 +474,9 @@ def prepare_finn( if "LD_LIBRARY_PATH" not in os.environ.keys(): os.environ["LD_LIBRARY_PATH"] = f"/lib/x86_64-linux-gnu/:{vivado_path}/lib/lnx64.o" else: - os.environ[ - "LD_LIBRARY_PATH" - ] = f"/lib/x86_64-linux-gnu/:{vivado_path}/lib/lnx64.o:{os.environ['LD_LIBRARY_PATH']}" + os.environ["LD_LIBRARY_PATH"] = ( + f"/lib/x86_64-linux-gnu/:{vivado_path}/lib/lnx64.o:{os.environ['LD_LIBRARY_PATH']}" + ) # Automatically set XILINX_LOCAL_USER_DATA to avoid issues later on if "XILINX_LOCAL_USER_DATA" in os.environ and os.environ["XILINX_LOCAL_USER_DATA"] != "no": @@ -603,9 +592,7 @@ def _build( sys.exit(1) else: model = mp - status( - f"Starting FINN build with config {flow_config.name} and model {model.name}!" - ) # type: ignore + status(f"Starting FINN build with config {flow_config.name} and model {model.name}!") # type: ignore if finn_build_dir is not None: finn_build_dir = finn_build_dir.expanduser().absolute() finn_build_dir.mkdir(parents=True, exist_ok=True) @@ -1046,13 +1033,8 @@ def update( flow_config=Path(), **get_function_args(), ) - if force: - if settings.finn_deps.exists(): - shutil.rmtree(settings.finn_deps) - finnxsi = resolve_module_path("finn_xsi") - so = Path(finnxsi) / "xsi.so" - if so.exists(): - so.unlink() + if force and settings.finn_deps.exists(): + shutil.rmtree(settings.finn_deps) prepare_finn(settings, accept_defaults or batch, batch, create_build_dir=False) diff --git a/src/finn/interface/settings.py b/src/finn/interface/settings.py index 241566053b..4f44f0726a 100644 --- a/src/finn/interface/settings.py +++ b/src/finn/interface/settings.py @@ -94,6 +94,7 @@ class FINNSettings(BaseModel): finn_custom_hls: str = Field(default=resolve_module_path("custom_hls")) finn_notebooks: str = Field(default=resolve_module_path("notebooks")) finn_tests: str = Field(default=resolve_module_path("tests")) + finn_xsi: Path = Field(default=Path(resolve_module_path("finn_xsi"))) @computed_field @property @@ -339,6 +340,7 @@ def save(self, installation_independent: bool, path: Path | None = None) -> None del data["finn_custom_hls"] del data["finn_notebooks"] del data["finn_tests"] + del data["finn_xsi"] if self._num_default_workers == -1: # Dont save this if its set to automatic detection del data["num_default_workers"] diff --git a/src/finn/transformation/fpgadataflow/compile_cppsim.py b/src/finn/transformation/fpgadataflow/compile_cppsim.py index 6190560265..b0adef0ef4 100644 --- a/src/finn/transformation/fpgadataflow/compile_cppsim.py +++ b/src/finn/transformation/fpgadataflow/compile_cppsim.py @@ -1,3 +1,6 @@ +"""For every node: compile C++ code in node attribute "code_gen_dir_cppsim" +and save path to executables in node attribute "executable_path". +All nodes in the graph must have the fpgadataflow backend attribute.""" # Copyright (C) 2020, Xilinx, Inc. # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -27,10 +30,17 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from typing import cast, TYPE_CHECKING +from onnx import NodeProto + import qonnx.custom_op.registry as registry from qonnx.transformation.base import NodeLocalTransformation from finn.util.fpgadataflow import is_hls_node +from finn.util.exception import FINNUserError + +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend class CompileCppSim(NodeLocalTransformation): @@ -46,30 +56,34 @@ class CompileCppSim(NodeLocalTransformation): NodeLocalTransformation for more details. """ - def __init__(self, num_workers=None): + def __init__(self, num_workers: int | None = None) -> None: + """Initialize the transformation.""" super().__init__(num_workers=num_workers) - def applyNodeLocal(self, node): + def applyNodeLocal(self, node: NodeProto) -> tuple[NodeProto, bool]: # noqa: N802 + """Compile C++ code for a single node.""" op_type = node.op_type if is_hls_node(node): try: # lookup op_type in registry of CustomOps - inst = registry.getCustomOp(node) + inst = cast("HLSBackend", registry.getCustomOp(node)) # ensure that code is generated - assert ( - inst.get_nodeattr("code_gen_dir_cppsim") != "" - ), """Node - attribute "code_gen_dir_cppsim" is not set. Please run - Transformation PrepareCppSim first.""" + if inst.get_nodeattr("code_gen_dir_cppsim") == "": + raise FINNUserError( + "Node attribute 'code_gen_dir_cppsim' is not set. Please run " + "Transformation PrepareCppSim first." + ) # call the compilation function for this node inst.compile_singlenode_code() # ensure that executable path is now set - assert ( - inst.get_nodeattr("executable_path") != "" - ), """Transformation - compile was not successful, there is no path to executables set - in node attribute "executable_path".""" + if inst.get_nodeattr("executable_path") == "": + raise FINNUserError( + "Transformation compile was not successful, there is no path to " + "executables set in node attribute 'executable_path'." + ) except KeyError: # exception if op_type is not supported - raise Exception("Custom op_type %s is currently not supported." % op_type) + raise FINNUserError( + f"Custom op_type {op_type} is currently not supported." + ) from None return (node, False) diff --git a/src/finn/transformation/fpgadataflow/prepare_rtlsim.py b/src/finn/transformation/fpgadataflow/prepare_rtlsim.py index 79502b51bb..8639bcf90e 100644 --- a/src/finn/transformation/fpgadataflow/prepare_rtlsim.py +++ b/src/finn/transformation/fpgadataflow/prepare_rtlsim.py @@ -1,3 +1,8 @@ +"""For a graph with generated RTL sources (after HLSSynthIP), create an +emulation library for each node to prepare for rtlsim +execution and set the rtlsim_so property to the path to the generated +emulation library.""" + # Copyright (C) 2020, Xilinx, Inc. # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -27,12 +32,23 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import qonnx.custom_op.registry as registry +from typing import Literal, cast, TYPE_CHECKING + +from onnx import NodeProto + +from finn.util.basic import getHWCustomOp +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import NodeLocalTransformation from finn.transformation.fpgadataflow.replace_verilog_relpaths import ReplaceVerilogRelPaths from finn.util.fpgadataflow import is_hls_node, is_rtl_node +from finn.util.exception import FINNUserError + +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend + from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend + class PrepareRTLSim(NodeLocalTransformation): """For a graph with generated RTL sources (after HLSSynthIP), create an @@ -48,28 +64,32 @@ class PrepareRTLSim(NodeLocalTransformation): NodeLocalTransformation for more details. """ - def __init__(self, behav=False, num_workers=None): + def __init__(self, behav: bool = False, num_workers: int | None = None) -> None: + """Construct the transformation.""" super().__init__(num_workers=num_workers) self.behav = behav - def apply(self, model): + def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, bool]: + """Apply the transformation to the model.""" model = model.transform(ReplaceVerilogRelPaths()) return super().apply(model) - def applyNodeLocal(self, node): + def applyNodeLocal(self, node: NodeProto) -> tuple[NodeProto, Literal[False]]: # noqa: N802 + """Apply the transformation to a single node.""" op_type = node.op_type if is_hls_node(node) or is_rtl_node(node): try: # lookup op_type in registry of CustomOps - inst = registry.getCustomOp(node) + inst = cast("HLSBackend | RTLBackend", getHWCustomOp(node)) inst.prepare_rtlsim(self.behav) # ensure that executable path is now set - assert ( - inst.get_nodeattr("rtlsim_so") != "" - ), "Failed to prepare RTLSim, no rtlsim_so attribute found." + if inst.get_nodeattr("rtlsim_so") == "": + raise FINNUserError("Failed to prepare RTLSim, no rtlsim_so attribute found.") except KeyError: # exception if op_type is not supported - raise Exception("Custom op_type %s is currently not supported." % op_type) + raise FINNUserError( + f"Custom op_type {op_type} is currently not supported." + ) from None except NotImplementedError: # Some custom ops (Vivado StreamingFIFO) may gracefully skip rtlsim pass diff --git a/src/finn/transformation/fpgadataflow/set_exec_mode.py b/src/finn/transformation/fpgadataflow/set_exec_mode.py index 405ddb0c42..7a4451e433 100644 --- a/src/finn/transformation/fpgadataflow/set_exec_mode.py +++ b/src/finn/transformation/fpgadataflow/set_exec_mode.py @@ -1,3 +1,8 @@ +"""Set attribute exec_mode in all fpgadataflow nodes to specify which +kind of execution should be used ("cppsim" or "rtlsim"). +Note that RTL components do not support cppsim. When cppsim is selected +for RTL components, by default the execution of the HW op parent is +executed.""" # Copyright (C) 2020, Xilinx, Inc. # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -27,6 +32,10 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from typing import Literal + +from finn.util.exception import FINNUserError +from qonnx.core.modelwrapper import ModelWrapper import qonnx.custom_op.registry as registry from qonnx.transformation.base import Transformation @@ -40,11 +49,13 @@ class SetExecMode(Transformation): for RTL components, by default the execution of the HW op parent is executed.""" - def __init__(self, mode): + def __init__(self, mode:str) -> None: + """Construct the transformation.""" super().__init__() self.mode = mode - def apply(self, model): + def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: + """Apply the transformation to the model.""" for node in model.graph.node: op_type = node.op_type if is_hls_node(node) or is_rtl_node(node): @@ -54,11 +65,12 @@ def apply(self, model): # set sim_mode accordingly to argument mode inst.set_nodeattr("exec_mode", self.mode) # ensure that sim_mode is now set - assert ( - inst.get_nodeattr("exec_mode") != "" - ), """Transformation - was not successful. Node attribute "exec_mode" is not set""" + if inst.get_nodeattr("exec_mode") == "": + raise FINNUserError("""Transformation + was not successful. Node attribute "exec_mode" is not set""") except KeyError: # exception if op_type is not supported - raise Exception("Custom op_type %s is currently not supported." % op_type) + raise FINNUserError( + f"Custom op_type {op_type} is currently not supported." + ) from None return (model, False) diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 1f000d856b..40377da6cf 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -1,7 +1,7 @@ """Build FINN Simulations.""" import contextlib -import finn_xsi.adapter as finnxsi +import finn.xsi as finnxsi import numpy as np import onnx import os @@ -592,13 +592,13 @@ def _create_sim_so( all_verilog_srcs = ( (Path(vivado_stitched_proj) / "all_verilog_srcs.txt").read_text().split() ) - sim_dir = ( + sim_dir = Path( make_build_dir(f"rtlsim_{model.graph.node[0].name}_") if build_dir is None else build_dir ) sim_base, sim_rel = finnxsi.compile_sim_obj( - top_module_name, all_verilog_srcs, str(sim_dir), debug=debug + top_module_name, all_verilog_srcs, sim_dir, debug=debug ) rtlsim_so = Path(sim_base) / Path(sim_rel) model.set_metadata_prop("rtlsim_so", str(rtlsim_so)) diff --git a/src/finn/xsi/setup.py b/src/finn/xsi/setup.py index cef1658f63..05ad3b1c0c 100644 --- a/src/finn/xsi/setup.py +++ b/src/finn/xsi/setup.py @@ -22,12 +22,14 @@ import argparse import os +import re import shutil import subprocess import sys import sysconfig from pathlib import Path from typing import List, Tuple +from finn.util.settings import get_settings def get_build_paths() -> Tuple[List[str], str, List[str]]: @@ -115,29 +117,43 @@ def build_xsi(force: bool = False, verbose: bool = True) -> bool: Returns: bool: True if build successful """ - xsi_path = Path(os.environ["FINN_XSI"]) + xsi_path = get_settings().finn_xsi + + vivado_path = os.environ.get("XILINX_VIVADO") + if vivado_path is None: + raise EnvironmentError("XILINX_VIVADO environment variable not set. Please source Vivado settings.") + match = re.search(r"\b(20\d{2})\.(1|2)\b", vivado_path) + if not match: + raise ValueError(f"Could not parse Vivado version from XILINX_VIVADO path: {vivado_path}") + year, minor = int(match.group(1)), int(match.group(2)) if not xsi_path.exists(): print(f"Error: finn_xsi source not found at {xsi_path}") return False + version_file = xsi_path / "VERSION" # Check if already built if not force: xsi_so = xsi_path / "xsi.so" - if xsi_so.exists(): - # Try importing to see if it works - sys.path.insert(0, str(xsi_path)) - try: - import xsi - - sys.path.pop(0) - if verbose: - print("xsi.so is already built and working.") - return True - except ImportError: - sys.path.pop(0) + if xsi_so.exists() and version_file.exists(): + with version_file.open() as f: + version_info = f.read().strip() + if version_info == f"Vivado {year}.{minor}": if verbose: - print("xsi.so exists but failed to import, rebuilding...") + print("xsi.so is already built for the current Vivado version.") + # Try importing to see if it works + sys.path.insert(0, str(xsi_path)) + try: + import xsi + + sys.path.pop(0) + if verbose: + print("xsi.so is already built and working.") + return True + except ImportError: + sys.path.pop(0) + if verbose: + print("xsi.so exists but failed to import, rebuilding...") # else: Need to build if verbose: @@ -194,6 +210,10 @@ def build_xsi(force: bool = False, verbose: bool = True) -> bool: if verbose and result.stdout: print(result.stdout) + + # Write version info version_file = xsi_path / "VERSION" + with version_file.open("w") as f: + f.write(f"Vivado {year}.{minor}") if verbose: print("Build completed successfully.") diff --git a/tests/fpgadataflow/test_fpgadataflow_convinputgenerator_rtl_dynamic.py b/tests/fpgadataflow/test_fpgadataflow_convinputgenerator_rtl_dynamic.py index 4b4d356040..66a7a2c5e3 100644 --- a/tests/fpgadataflow/test_fpgadataflow_convinputgenerator_rtl_dynamic.py +++ b/tests/fpgadataflow/test_fpgadataflow_convinputgenerator_rtl_dynamic.py @@ -52,7 +52,6 @@ import finn.core.onnx_exec as oxe import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw import finn.transformation.streamline.absorb as absorb -from finn import xsi from finn.core.onnx_exec import execute_onnx from finn.core.rtlsim_exec import rtlsim_exec from finn.transformation.fpgadataflow.create_dataflow_partition import CreateDataflowPartition @@ -64,7 +63,7 @@ from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.basic import get_liveness_threshold_cycles -finnxsi = xsi if xsi.is_available() else None +import finn.xsi as finnxsi def create_conv_model(idim_h, idim_w, ifm, k, stride, ofm, idt, wdt, pad_mode, depthwise): diff --git a/tests/fpgadataflow/test_fpgadataflow_thresholding.py b/tests/fpgadataflow/test_fpgadataflow_thresholding.py index babe4d8e99..b12c90c6e3 100644 --- a/tests/fpgadataflow/test_fpgadataflow_thresholding.py +++ b/tests/fpgadataflow/test_fpgadataflow_thresholding.py @@ -26,6 +26,10 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from finn.builder.build_dataflow_config import DataflowBuildConfig +from finn.transformation.fpgadataflow.simulation import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation import pytest import numpy as np @@ -50,13 +54,30 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds test_fpga_part = "xczu3eg-sbva484-1-e" target_clk_ns = 5 +def InsertAndSetFIFODepths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform( + RunLayerParallelSimulation( + fpga_part, clk_ns, cfg + ) + ) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model + def generate_edge_threshold_values( data_type, num_input_channels, num_steps, narrow=False, per_tensor=False @@ -427,7 +448,7 @@ def test_fpgadataflow_thresholding_stitched_ip( model = model.transform(MinimizeWeightBitWidth()) model = model.transform(GiveUniqueNodeNames()) # Run stitched-ip RTLsim to have memstream in the test loop - model = model.transform(InsertAndSetFIFODepths(part, target_clk_ns)) + model = InsertAndSetFIFODepths(model, part, target_clk_ns) model = model.transform(PrepareIP(part, target_clk_ns)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP(part, target_clk_ns)) diff --git a/tests/fpgadataflow/test_fpgadataflow_thresholding_runtime.py b/tests/fpgadataflow/test_fpgadataflow_thresholding_runtime.py index 8ee3a2a668..23351fbde3 100644 --- a/tests/fpgadataflow/test_fpgadataflow_thresholding_runtime.py +++ b/tests/fpgadataflow/test_fpgadataflow_thresholding_runtime.py @@ -38,7 +38,6 @@ from qonnx.transformation.general import GiveUniqueNodeNames from qonnx.util.basic import gen_finn_dt_tensor, qonnx_make_model -from finn import xsi from finn.core.rtlsim_exec import rtlsim_exec from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP @@ -47,7 +46,7 @@ from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -finnxsi = xsi if xsi.is_available() else None +import finn.xsi as finnxsi test_fpga_part = "xczu3eg-sbva484-1-e" target_clk_ns = 5 From af01d6d6c0d57df151c43ee705d089060c27ab8c Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 11:28:49 +0200 Subject: [PATCH 107/170] Fix more tests and remove finn xsi from deps update --- external_dependencies.yaml | 5 - src/finn/builder/build_dataflow_steps.py | 3 +- src/finn/interface/manage_deps.py | 31 +- .../fpgadataflow/set_fifo_depths.py | 137 +++++- .../transformation/fpgadataflow/simulation.py | 423 +++++++++++++----- .../fpgadataflow/simulation_connected.py | 8 +- .../fpgadataflow/simulation_controller.py | 334 -------------- .../fpgadataflow/simulation_isolated.py | 16 +- tests/fpgadataflow/test_fifosizing.py | 146 +++--- tests/fpgadataflow/test_fpgadataflow_mvau.py | 2 +- .../test_fpgadataflow_thresholding.py | 2 +- tests/testing_util/test.py | 14 +- 12 files changed, 561 insertions(+), 560 deletions(-) delete mode 100644 src/finn/transformation/fpgadataflow/simulation_controller.py diff --git a/external_dependencies.yaml b/external_dependencies.yaml index 12309e7b46..e19fa7ca1d 100644 --- a/external_dependencies.yaml +++ b/external_dependencies.yaml @@ -51,8 +51,3 @@ direct_download_deps: url: "https://dpoauwgwqsy2x.cloudfront.net/Download/pynq-z2.zip" do_unzip: True target_directory: "board_files" - -custom_deps: - finn_xsi: - installation_function: "_install_finn_xsi" - outdated_function: "_is_outdated_finn_xsi" diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 267ae01547..0a5caa8e06 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -107,7 +107,7 @@ ) from finn.transformation.fpgadataflow.set_folding import SetFolding from finn.transformation.fpgadataflow.set_loop_boundary import SetLoopBoundary -from finn.transformation.fpgadataflow.simulation import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType from finn.transformation.fpgadataflow.simulation_connected import ( NodeConnectedSimulation, @@ -1328,6 +1328,7 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi del res["fifo_cycles_until_first_valid"] cycle_per_sec = 1e9 / cfg.synth_clk_period_ns res["throughput_fps"] = cycle_per_sec / res["intervals"][0] # type: ignore + #TODO: Add latency measurement # Attach entry to output outputs.append(res) diff --git a/src/finn/interface/manage_deps.py b/src/finn/interface/manage_deps.py index d01c919be8..e32a17f082 100644 --- a/src/finn/interface/manage_deps.py +++ b/src/finn/interface/manage_deps.py @@ -4,7 +4,6 @@ import contextlib import importlib.util -import os import shlex import shutil import subprocess as sp @@ -25,7 +24,7 @@ from typing import cast from finn.interface import IS_POSIX -from finn.interface.interface_utils import debug, error, resolve_module_path +from finn.interface.interface_utils import debug, error from finn.util.exception import ( FINNConfigurationError, FINNDependencyInstallationError, @@ -313,10 +312,6 @@ def __init__( except ValidationError as e: raise FINNUserError(f"Validation error: {e}") from e - # Try to find FINN_XSI. If it cannot be found, it is ignored in the - # list of all dependencies (since this is neither a failed nor a successful install) - self.finn_xsi_str = resolve_module_path("finn_xsi") - def _run_silent(self, cmd: str, cwd: Path | None = None, timeout: float | None = None) -> int: """Run a given command silently. Return its returncode.""" debug(f"[DependencyUpdater] Running command: {cmd}", False) @@ -487,30 +482,6 @@ def _install_custom(self, package_name: str) -> bool: f"{package_name} not found in DependencyUpdater!" ) from e - def _is_outdated_finn_xsi(self) -> bool: - """Return whether FINN XSI is outdated.""" - # If finn xsi was found its outdated, if it wasnt found, its never outdated - return self.finn_xsi_str != "" - - def _install_finn_xsi(self) -> bool: - """Install FINN XSI bindings and return if installation was successful.""" - # Hacky workaround - os.environ["FINN_XSI"] = self.finn_xsi_str - from finn.xsi import is_available - - result = sp.run( - shlex.split(f"{sys.executable} -m finn.xsi.setup"), - capture_output=True, - text=True, - env=os.environ.copy(), - ) - if result.returncode != 0: - raise FINNDependencyInstallationError( - "Installation of FINN XSI failed!:\n" + result.stdout - ) - sys.path.append(self.finn_xsi_str) - return is_available() - def install_dependency(self, package_name: str) -> bool: """Install the dependency in the dependency location. If no definition for this dependency exists or the installation failed, return False. diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index b3a9bbf371..e9bbd7a64e 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -36,7 +36,7 @@ from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation from qonnx.transformation.general import GiveReadableTensorNames, SortGraph -from typing import Literal, cast +from typing import Literal, TypeAlias, cast from finn.util.basic import getHWCustomOp from finn.util.exception import FINNUserError @@ -45,6 +45,17 @@ from onnx import NodeProto +from qonnx.custom_op.registry import getCustomOp +from qonnx.transformation.general import GiveUniqueNodeNames + +from finn.builder.build_dataflow_config import DataflowBuildConfig +from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO +from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers +from finn.util.exception import FINNInternalError + +FIFODepthConfig: TypeAlias = list[dict[str, list[int]]] + + class ApplyFIFODepthsFromFile(Transformation): """Apply FIFO depths from a JSON file generated by a previous run of build_dataflow with auto_fifo_depths enabled.""" @@ -241,3 +252,127 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: model = model.transform(SortGraph()) model = model.transform(GiveReadableTensorNames()) return (model, False) + + +class ApplySimulatedFIFOSizes(Transformation): + """Apply a FIFO sizing configuration to the model. + If FIFOs already exist the step is skipped.""" + + def __init__( + self, + cfg: DataflowBuildConfig, + fifo_config: Path | None = None, + max_qsrl_depth: int = 256, + vivado_ram_style: str = "block", + ) -> None: + """If given read the config json from the given path. + Otherwise check in the output directory. + """ + self.cfg = cfg + self.max_qsrl_depth = max_qsrl_depth + self.vivado_ram_style = vivado_ram_style + self.fifo_config = fifo_config + + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Apply FIFO Simulation Depths to the model.""" + if len(list(filter(lambda node: "StreamingFIFO" in node.op_type, model.graph.node))) > 0: + log.warning( + "It seems that StreamingFIFOs have already " + "been inserted into the graph. Skipping insertion of FIFOs." + ) + return model, False + + if self.fifo_config is None: + p = model.get_metadata_prop("fifo_data") + if p == "" or p is None: + raise FINNInternalError( + "FIFO sizing simulation was not run before inserting simulated FIFO sizes!" + ) + self.path = Path(p) + else: + self.path = self.fifo_config + + self.fifo_depths: FIFODepthConfig = [] + with self.path.open() as f: + self.fifo_depths = cast("FIFODepthConfig", json.load(f)) + + if len(model.graph.node) != len(self.fifo_depths): + raise FINNUserError( + "There are no StreamingFIFOs in the graph, yet the number " + "of nodes and number of FIFO sizes differ. There may be " + "unaccounted for nodes that have not been part of the FIFO " + "simulation. Consider re-running simulation directly before " + "applying the FIFO sizes. It might also be that your model " + "or config is outdated, in which case it is recommended to " + "re-run the entire flow from start to finish." + ) + + # FIFO sizes are set as the maximum of outFIFODepth and inFIFODepth of the successor node + # Only set the outFIFODepth, because setting both is redundant as inFIFODepth defaults to 0. + # Remove all in/outFIFODepths in model for clean slate + graph = model.graph + for node in graph.node: + predecessors = model.find_direct_predecessors(node) + successors = model.find_direct_successors(node) + n = getCustomOp(node) + if n is not None: + if predecessors is not None: + n.set_nodeattr( + "inFIFODepths", + cast("list[str | int | float]", [0] * len(predecessors)), + ) + if successors is not None: + n.set_nodeattr( + "outFIFODepths", + cast("list[str | int | float]", [0] * len(successors)), + ) + + # Set new outFIFODepths according to config + graph = model.graph + node_ind = -1 + for first_node in graph.node: + node_ind += 1 + n0 = getCustomOp(first_node) + if n0 is None: + raise FINNInternalError( + f"Node {first_node.name} does not have a custom op instance." + " This is required for FIFO insertion." + ) + if first_node.name != self.fifo_depths[node_ind]["node"]: + raise FINNInternalError( + f"Node name {first_node.name} does not match expected name " + f"{self.fifo_depths[node_ind]['node']} at index {node_ind}. " + "This may be due to a mismatch between the model and the config, " + "or due to changes in the model after the simulation was run. " + "Consider re-running the entire flow from start to finish." + ) + fifos = cast("list[str | int | float]", (self.fifo_depths[node_ind]["depths"])) + n0.set_nodeattr("outFIFODepths", fifos) + + # Insert the FIFOs into the model + model = model.transform(InsertFIFO(True, self.max_qsrl_depth, self.vivado_ram_style)) + + model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveReadableTensorNames()) + model = model.transform(SpecializeLayers(self.cfg._resolve_fpga_part())) # noqa + model = model.transform(GiveUniqueNodeNames()) + model = model.transform(GiveReadableTensorNames()) + + # Sanity check to make sure fifos were inserted + inserted_fifo_count = sum( + [int("StreamingFIFO" in node.op_type) for node in model.graph.node] + ) + if inserted_fifo_count == 0: + raise FINNInternalError( + "No FIFOs were inserted. This may be due to " + "wrong network configuration, step order or " + "a number of other things." + ) + if inserted_fifo_count < int(0.4 * float(len(model.graph.node))): + log.warning( + "The number of inserted FIFOs makes up less than 40%" + " of the total number of nodes in the model. This could " + "point to a potential error." + ) + + return model, False diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index b1ec469374..01971f2edc 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,23 +1,26 @@ """Manages the Simulation superclass as well as general simulation related transforms.""" -import json import pandas as pd from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper -from qonnx.custom_op.registry import getCustomOp -from qonnx.transformation.base import Transformation -from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames -from typing import Any, TypeAlias, cast +from typing import Any, TypeAlias -from finn.builder.build_dataflow_config import DataflowBuildConfig -from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType -from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log -FIFODepthConfig: TypeAlias = list[dict[str, list[int]]] +import json +import socket +import subprocess +import threading +import time +from rich.console import Console +from threading import Lock + +from finn.util.basic import make_build_dir +from finn.util.logging import ThreadsafeProgressDisplay +FIFODepthConfig: TypeAlias = list[dict[str, list[int]]] def store_fifo_data( model: ModelWrapper, @@ -177,125 +180,321 @@ def simulate(self) -> Any: raise NotImplementedError("Call simulate() on subclasses.") -class ApplySimulatedFIFOSizes(Transformation): - """Apply a FIFO sizing configuration to the model. - If FIFOs already exist the step is skipped.""" +class SimulationController: + """Control a node-node IPC connected simulation in threads.""" def __init__( self, - cfg: DataflowBuildConfig, - fifo_config: Path | None = None, - max_qsrl_depth: int = 256, - vivado_ram_style: str = "block", + parallel_simulations: int, + names: list[str], + binaries: list[Path], + console: Console, + poll_interval: float = 1.0, + with_progressbar: bool = True, ) -> None: - """If given read the config json from the given path. - Otherwise check in the output directory. + """Create a new controller, without starting the simulation. + + Args: + parallel_simulations: Number of simulations to run in parallel. + names: List of names for the simulations. + binaries: List of paths to the simulation binaries. + console: The rich.console.Console to print with. + poll_interval: How long the wait between checks of the processes stdout/stdin is. + with_progressbar: Whether or not to display a progressbar for the cycle count. """ - self.cfg = cfg - self.max_qsrl_depth = max_qsrl_depth - self.vivado_ram_style = vivado_ram_style - self.fifo_config = fifo_config - - def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: - """Apply FIFO Simulation Depths to the model.""" - if len(list(filter(lambda node: "StreamingFIFO" in node.op_type, model.graph.node))) > 0: - log.warning( - "It seems that StreamingFIFOs have already " - "been inserted into the graph. Skipping insertion of FIFOs." + if len(names) != len(binaries): + raise FINNInternalError( + f"Simulation controller received non-matching " + f"name and binary count: {len(names)} and {len(binaries)}" ) - return model, False + self.binaries = binaries + self.names = names + self.console = console + self.poll_interval = poll_interval + self.workers = parallel_simulations + self.progress = None + if with_progressbar: + self.progress = ThreadsafeProgressDisplay(names, [0] * len(names), names) + self.running_lock = Lock() + self.running = 0 + self.total = len(names) + self.logdir = Path(make_build_dir("simulation_logfiles_")) - if self.fifo_config is None: - p = model.get_metadata_prop("fifo_data") - if p == "" or p is None: - raise FINNInternalError( - "FIFO sizing simulation was not run before inserting simulated FIFO sizes!" - ) - self.path = Path(p) - else: - self.path = self.fifo_config + # Socket communication management + self.processes: list[tuple[subprocess.Popen, Any, Any]] = [] + self.sockets: list[tuple[socket.socket, str]] = [] - self.fifo_depths: FIFODepthConfig = [] - with self.path.open() as f: - self.fifo_depths = cast("FIFODepthConfig", json.load(f)) + # Early termination flag + self.should_stop = False + self.stop_lock = Lock() - if len(model.graph.node) != len(self.fifo_depths): - raise FINNUserError( - "There are no StreamingFIFOs in the graph, yet the number " - "of nodes and number of FIFO sizes differ. There may be " - "unaccounted for nodes that have not been part of the FIFO " - "simulation. Consider re-running simulation directly before " - "applying the FIFO sizes. It might also be that your model " - "or config is outdated, in which case it is recommended to " - "re-run the entire flow from start to finish." + def _start_process(self, binary: Path, process_id: int) -> int: + """Start a single C++ simulation process with its own Unix socket. + + Args: + binary: Path to the simulation executable + process_id: Unique identifier for this process + + Returns: + Index of the started process + """ + thread_id = threading.get_ident() + + # Create unique socket path which includes thread ID to avoid conflicts + # with multiple threads + socket_path = Path(f"/tmp/fifosim_sockets/{thread_id}/") + socket_path.mkdir(parents=True, exist_ok=True) + socket_path = socket_path / f"sim_socket_{process_id}.sock" + + # Remove socket if it exists + if socket_path.exists(): + socket_path.unlink() + + # Build command arguments + cmd = [str(binary), "--socket", socket_path] + + # Create log files for stdout and stderr + stdout_log = self.logdir / f"{process_id}_stdout_cpp.log" + stderr_log = self.logdir / f"{process_id}_stderr_cpp.log" + + stdout_file = stdout_log.open("w") + stderr_file = stderr_log.open("w") + + # Start C++ process - redirect stdout/stderr to files + cwd = binary.parent + proc = subprocess.Popen(cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd) + + # Check if process started successfully + time.sleep(0.2) # Give process time to fail if there's an immediate error + if proc.poll() is not None: + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + stdout_file.close() + stderr_file.close() + msg = ( + f"C++ process exited immediately with code {proc.returncode}\n" + f"Stderr: {stderr_output}\nStdout: {stdout_output}" ) + self.console.log(str(process_id) + ": " + msg) + raise RuntimeError(msg) - # FIFO sizes are set as the maximum of outFIFODepth and inFIFODepth of the successor node - # Only set the outFIFODepth, because setting both is redundant as inFIFODepth defaults to 0. - # Remove all in/outFIFODepths in model for clean slate - graph = model.graph - for node in graph.node: - predecessors = model.find_direct_predecessors(node) - successors = model.find_direct_successors(node) - n = getCustomOp(node) - if n is not None: - if predecessors is not None: - n.set_nodeattr( - "inFIFODepths", - cast("list[str | int | float]", [0] * len(predecessors)), + # Create Unix socket and connect + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + + # Wait for C++ process to create socket (with timeout) + max_retries = 100 # 20 seconds total + connected = False + for i in range(max_retries): + # Check if process is still alive + if proc.poll() is not None: + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + stdout_file.close() + stderr_file.close() + msg = ( + f"C++ process died during socket wait with code {proc.returncode}\n" + f"Stderr: {stderr_output}\nStdout: {stdout_output}" + ) + self.console.log(str(process_id) + ": " + msg) + raise RuntimeError(msg) + + try: + sock.connect(str(socket_path)) + connected = True + break + except (FileNotFoundError, ConnectionRefusedError) as e: + if i == max_retries - 1: + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + stdout_file.close() + stderr_file.close() + msg = ( + f"Failed to connect to socket after {max_retries} retries\n" + f"Stderr: {stderr_output}\nStdout: {stdout_output}" ) - if successors is not None: - n.set_nodeattr( - "outFIFODepths", - cast("list[str | int | float]", [0] * len(successors)), + self.console.log(str(process_id) + ": " + msg) + raise RuntimeError(msg) from e + time.sleep(0.2) + + if not connected: + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + stdout_file.close() + stderr_file.close() + msg = ( + f"Failed to connect to socket {socket_path}\n" + f"Stderr: {stderr_output}\nStdout: {stdout_output}" + ) + self.console.log(str(process_id) + ": " + msg) + raise RuntimeError(msg) + + self.processes.append((proc, stdout_file, stderr_file)) + self.sockets.append((sock, str(socket_path))) + return len(self.processes) - 1 + + def _send_command(self, process_idx: int, command: str, payload: dict[str, Any]) -> None: + """Send command and payload to a specific process. + + Args: + process_idx: Index of the process to send to + command: Command string (e.g., "start", "status", "stop") + payload: Dictionary containing command-specific data + """ + sock, _ = self.sockets[process_idx] + + message = {"command": command, "payload": payload} + + # Send length-prefixed message + msg_str = json.dumps(message) + msg_bytes = msg_str.encode("utf-8") + length = len(msg_bytes) + + # Send 4-byte length prefix (little-endian) + sock.sendall(length.to_bytes(4, byteorder="little")) + # Send actual message + sock.sendall(msg_bytes) + + def _receive_response(self, process_idx: int) -> dict[str, Any] | None: + """Receive response from a specific process. + + Args: + process_idx: Index of the process to receive from + + Returns: + Dictionary containing the response, or None if error + + Raises: + TimeoutError: If socket times out waiting for response + """ + sock, _ = self.sockets[process_idx] + + # Set 120 second timeout to prevent deadlocks + # Needs to be rather larger to give the simulation IO thread time to answer + sock.settimeout(120.0) + + # Read 4-byte length prefix + length_bytes = sock.recv(4) + if not length_bytes: + self.console.log(f"{process_idx}: Client disconnected.") + return None + + length = int.from_bytes(length_bytes, byteorder="little") + + # Read message data + msg_bytes = b"" + while len(msg_bytes) < length: + chunk = sock.recv(length - len(msg_bytes)) + if not chunk: + break + msg_bytes += chunk + + return json.loads(msg_bytes.decode("utf-8")) + + def _send_and_receive( + self, process_idx: int, command: str, payload: dict[str, Any] + ) -> dict[str, Any] | None: + """Send command and wait for response (convenience method). + + Args: + process_idx: Index of the process + command: Command string + payload: Command payload + + Returns: + Response dictionary + + Raises: + RuntimeError: If the subprocess has terminated with an error + """ + try: + self._send_command(process_idx, command, payload) + response = self._receive_response(process_idx) + + # If we got None (timeout or connection error), check if process crashed + if response is None: + proc, stdout_file, stderr_file = self.processes[process_idx] + returncode = proc.poll() + + if returncode is not None and returncode != 0: + # Process has terminated with an error + # Flush and read error logs + stdout_file.flush() + stderr_file.flush() + + stdout_log = self.logdir / f"{process_idx}_stdout_cpp.log" + stderr_log = self.logdir / f"{process_idx}_stderr_cpp.log" + + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + + # Raise the actual error from the subprocess + msg = ( + f"Subprocess (process_idx={process_idx}) terminated with" + f" exit code {returncode}.\n" + f"Stderr:\n{stderr_output}\n" + f"Stdout:\n{stdout_output}" ) + raise RuntimeError(msg) from None - # Set new outFIFODepths according to config - graph = model.graph - node_ind = -1 - for first_node in graph.node: - node_ind += 1 - n0 = getCustomOp(first_node) - if n0 is None: - raise FINNInternalError( - f"Node {first_node.name} does not have a custom op instance." - " This is required for FIFO insertion." - ) - if first_node.name != self.fifo_depths[node_ind]["node"]: - raise FINNInternalError( - f"Node name {first_node.name} does not match expected name " - f"{self.fifo_depths[node_ind]['node']} at index {node_ind}. " - "This may be due to a mismatch between the model and the config, " - "or due to changes in the model after the simulation was run. " - "Consider re-running the entire flow from start to finish." + return response + except (BrokenPipeError, ConnectionResetError, TimeoutError) as err: + # Connection error or timeout means the subprocess may have died + # Check if it exited with an error and raise that instead + proc, stdout_file, stderr_file = self.processes[process_idx] + returncode = proc.poll() + + if returncode is not None and returncode != 0: + # Process has terminated with an error + # Flush and read error logs + stdout_file.flush() + stderr_file.flush() + + stdout_log = self.logdir / f"{process_idx}_stdout_cpp.log" + stderr_log = self.logdir / f"{process_idx}_stderr_cpp.log" + + stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" + stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" + + # Raise the actual error from the subprocess + msg = ( + f"Subprocess (process_idx={process_idx}) terminated with" + f" exit code {returncode}.\n" + f"Stderr:\n{stderr_output}\n" + f"Stdout:\n{stdout_output}" ) - fifos = cast("list[str | int | float]", (self.fifo_depths[node_ind]["depths"])) - n0.set_nodeattr("outFIFODepths", fifos) + raise RuntimeError(msg) from err # from None - # Insert the FIFOs into the model - model = model.transform(InsertFIFO(True, self.max_qsrl_depth, self.vivado_ram_style)) + # If process exited cleanly (returncode == 0) or hasn't exited yet, + # this is an unexpected connection error + return None - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) - model = model.transform(SpecializeLayers(self.cfg._resolve_fpga_part())) # noqa - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) + def _cleanup_sockets(self) -> None: + """Close all sockets and terminate all processes.""" + # Send stop command to all processes + errors = [] + for i in range(len(self.processes)): + try: + self._send_command(i, "stop", {}) + self._receive_response(i) + except Exception as e: # noqa + errors.append((i, e)) - # Sanity check to make sure fifos were inserted - inserted_fifo_count = sum( - [int("StreamingFIFO" in node.op_type) for node in model.graph.node] - ) - if inserted_fifo_count == 0: - raise FINNInternalError( - "No FIFOs were inserted. This may be due to " - "wrong network configuration, step order or " - "a number of other things." - ) - if inserted_fifo_count < int(0.4 * float(len(model.graph.node))): - log.warning( - "The number of inserted FIFOs makes up less than 40%" - " of the total number of nodes in the model. This could " - "point to a potential error." - ) + # Close sockets + for sock, socket_path in self.sockets: + sock.close() + socket_path_obj = Path(socket_path) + if socket_path_obj.exists(): + socket_path_obj.unlink(True) + + # Terminate processes and close file handles + for proc, stdout_file, stderr_file in self.processes: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + finally: + stdout_file.close() + stderr_file.close() - return model, False diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index f0b779d1a9..a1509f5925 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -18,8 +18,12 @@ from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.set_fifo_depths import get_fifo_split_configs -from finn.transformation.fpgadataflow.simulation import Simulation, SimulationType, store_fifo_data -from finn.transformation.fpgadataflow.simulation_controller import SimulationController +from finn.transformation.fpgadataflow.simulation import ( + Simulation, + SimulationType, + store_fifo_data, + SimulationController, +) from finn.util.basic import getHWCustomOp, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log diff --git a/src/finn/transformation/fpgadataflow/simulation_controller.py b/src/finn/transformation/fpgadataflow/simulation_controller.py deleted file mode 100644 index b06b56e3b1..0000000000 --- a/src/finn/transformation/fpgadataflow/simulation_controller.py +++ /dev/null @@ -1,334 +0,0 @@ -"""Control (node based) simulations via unix sockets.""" - -import json -import socket -import subprocess -import threading -import time -from pathlib import Path -from rich.console import Console -from threading import Lock -from typing import Any - -from finn.util.basic import make_build_dir -from finn.util.exception import FINNInternalError -from finn.util.logging import ThreadsafeProgressDisplay - - -class SimulationController: - """Control a node-node IPC connected simulation in threads.""" - - def __init__( - self, - parallel_simulations: int, - names: list[str], - binaries: list[Path], - console: Console, - poll_interval: float = 1.0, - with_progressbar: bool = True, - ) -> None: - """Create a new controller, without starting the simulation. - - Args: - parallel_simulations: Number of simulations to run in parallel. - names: List of names for the simulations. - binaries: List of paths to the simulation binaries. - console: The rich.console.Console to print with. - poll_interval: How long the wait between checks of the processes stdout/stdin is. - with_progressbar: Whether or not to display a progressbar for the cycle count. - """ - if len(names) != len(binaries): - raise FINNInternalError( - f"Simulation controller received non-matching " - f"name and binary count: {len(names)} and {len(binaries)}" - ) - self.binaries = binaries - self.names = names - self.console = console - self.poll_interval = poll_interval - self.workers = parallel_simulations - self.progress = None - if with_progressbar: - self.progress = ThreadsafeProgressDisplay(names, [0] * len(names), names) - self.running_lock = Lock() - self.running = 0 - self.total = len(names) - self.logdir = Path(make_build_dir("simulation_logfiles_")) - - # Socket communication management - self.processes: list[tuple[subprocess.Popen, Any, Any]] = [] - self.sockets: list[tuple[socket.socket, str]] = [] - - # Early termination flag - self.should_stop = False - self.stop_lock = Lock() - - def _start_process(self, binary: Path, process_id: int) -> int: - """Start a single C++ simulation process with its own Unix socket. - - Args: - binary: Path to the simulation executable - process_id: Unique identifier for this process - - Returns: - Index of the started process - """ - thread_id = threading.get_ident() - - # Create unique socket path which includes thread ID to avoid conflicts - # with multiple threads - socket_path = Path(f"/tmp/fifosim_sockets/{thread_id}/") - socket_path.mkdir(parents=True, exist_ok=True) - socket_path = socket_path / f"sim_socket_{process_id}.sock" - - # Remove socket if it exists - if socket_path.exists(): - socket_path.unlink() - - # Build command arguments - cmd = [str(binary), "--socket", socket_path] - - # Create log files for stdout and stderr - stdout_log = self.logdir / f"{process_id}_stdout_cpp.log" - stderr_log = self.logdir / f"{process_id}_stderr_cpp.log" - - stdout_file = stdout_log.open("w") - stderr_file = stderr_log.open("w") - - # Start C++ process - redirect stdout/stderr to files - cwd = binary.parent - proc = subprocess.Popen(cmd, stdout=stdout_file, stderr=stderr_file, text=True, cwd=cwd) - - # Check if process started successfully - time.sleep(0.2) # Give process time to fail if there's an immediate error - if proc.poll() is not None: - stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" - stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" - stdout_file.close() - stderr_file.close() - msg = ( - f"C++ process exited immediately with code {proc.returncode}\n" - f"Stderr: {stderr_output}\nStdout: {stdout_output}" - ) - self.console.log(str(process_id) + ": " + msg) - raise RuntimeError(msg) - - # Create Unix socket and connect - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - - # Wait for C++ process to create socket (with timeout) - max_retries = 100 # 20 seconds total - connected = False - for i in range(max_retries): - # Check if process is still alive - if proc.poll() is not None: - stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" - stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" - stdout_file.close() - stderr_file.close() - msg = ( - f"C++ process died during socket wait with code {proc.returncode}\n" - f"Stderr: {stderr_output}\nStdout: {stdout_output}" - ) - self.console.log(str(process_id) + ": " + msg) - raise RuntimeError(msg) - - try: - sock.connect(str(socket_path)) - connected = True - break - except (FileNotFoundError, ConnectionRefusedError) as e: - if i == max_retries - 1: - stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" - stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" - stdout_file.close() - stderr_file.close() - msg = ( - f"Failed to connect to socket after {max_retries} retries\n" - f"Stderr: {stderr_output}\nStdout: {stdout_output}" - ) - self.console.log(str(process_id) + ": " + msg) - raise RuntimeError(msg) from e - time.sleep(0.2) - - if not connected: - stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" - stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" - stdout_file.close() - stderr_file.close() - msg = ( - f"Failed to connect to socket {socket_path}\n" - f"Stderr: {stderr_output}\nStdout: {stdout_output}" - ) - self.console.log(str(process_id) + ": " + msg) - raise RuntimeError(msg) - - self.processes.append((proc, stdout_file, stderr_file)) - self.sockets.append((sock, str(socket_path))) - return len(self.processes) - 1 - - def _send_command(self, process_idx: int, command: str, payload: dict[str, Any]) -> None: - """Send command and payload to a specific process. - - Args: - process_idx: Index of the process to send to - command: Command string (e.g., "start", "status", "stop") - payload: Dictionary containing command-specific data - """ - sock, _ = self.sockets[process_idx] - - message = {"command": command, "payload": payload} - - # Send length-prefixed message - msg_str = json.dumps(message) - msg_bytes = msg_str.encode("utf-8") - length = len(msg_bytes) - - # Send 4-byte length prefix (little-endian) - sock.sendall(length.to_bytes(4, byteorder="little")) - # Send actual message - sock.sendall(msg_bytes) - - def _receive_response(self, process_idx: int) -> dict[str, Any] | None: - """Receive response from a specific process. - - Args: - process_idx: Index of the process to receive from - - Returns: - Dictionary containing the response, or None if error - - Raises: - TimeoutError: If socket times out waiting for response - """ - sock, _ = self.sockets[process_idx] - - # Set 120 second timeout to prevent deadlocks - # Needs to be rather larger to give the simulation IO thread time to answer - sock.settimeout(120.0) - - # Read 4-byte length prefix - length_bytes = sock.recv(4) - if not length_bytes: - self.console.log(f"{process_idx}: Client disconnected.") - return None - - length = int.from_bytes(length_bytes, byteorder="little") - - # Read message data - msg_bytes = b"" - while len(msg_bytes) < length: - chunk = sock.recv(length - len(msg_bytes)) - if not chunk: - break - msg_bytes += chunk - - return json.loads(msg_bytes.decode("utf-8")) - - def _send_and_receive( - self, process_idx: int, command: str, payload: dict[str, Any] - ) -> dict[str, Any] | None: - """Send command and wait for response (convenience method). - - Args: - process_idx: Index of the process - command: Command string - payload: Command payload - - Returns: - Response dictionary - - Raises: - RuntimeError: If the subprocess has terminated with an error - """ - try: - self._send_command(process_idx, command, payload) - response = self._receive_response(process_idx) - - # If we got None (timeout or connection error), check if process crashed - if response is None: - proc, stdout_file, stderr_file = self.processes[process_idx] - returncode = proc.poll() - - if returncode is not None and returncode != 0: - # Process has terminated with an error - # Flush and read error logs - stdout_file.flush() - stderr_file.flush() - - stdout_log = self.logdir / f"{process_idx}_stdout_cpp.log" - stderr_log = self.logdir / f"{process_idx}_stderr_cpp.log" - - stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" - stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" - - # Raise the actual error from the subprocess - msg = ( - f"Subprocess (process_idx={process_idx}) terminated with" - f" exit code {returncode}.\n" - f"Stderr:\n{stderr_output}\n" - f"Stdout:\n{stdout_output}" - ) - raise RuntimeError(msg) from None - - return response - except (BrokenPipeError, ConnectionResetError, TimeoutError) as err: - # Connection error or timeout means the subprocess may have died - # Check if it exited with an error and raise that instead - proc, stdout_file, stderr_file = self.processes[process_idx] - returncode = proc.poll() - - if returncode is not None and returncode != 0: - # Process has terminated with an error - # Flush and read error logs - stdout_file.flush() - stderr_file.flush() - - stdout_log = self.logdir / f"{process_idx}_stdout_cpp.log" - stderr_log = self.logdir / f"{process_idx}_stderr_cpp.log" - - stderr_output = stderr_log.read_text() if stderr_log.exists() else "No stderr" - stdout_output = stdout_log.read_text() if stdout_log.exists() else "No stdout" - - # Raise the actual error from the subprocess - msg = ( - f"Subprocess (process_idx={process_idx}) terminated with" - f" exit code {returncode}.\n" - f"Stderr:\n{stderr_output}\n" - f"Stdout:\n{stdout_output}" - ) - raise RuntimeError(msg) from err # from None - - # If process exited cleanly (returncode == 0) or hasn't exited yet, - # this is an unexpected connection error - return None - - def _cleanup_sockets(self) -> None: - """Close all sockets and terminate all processes.""" - # Send stop command to all processes - errors = [] - for i in range(len(self.processes)): - try: - self._send_command(i, "stop", {}) - self._receive_response(i) - except Exception as e: # noqa - errors.append((i, e)) - - # Close sockets - for sock, socket_path in self.sockets: - sock.close() - socket_path_obj = Path(socket_path) - if socket_path_obj.exists(): - socket_path_obj.unlink(True) - - # Terminate processes and close file handles - for proc, stdout_file, stderr_file in self.processes: - proc.terminate() - try: - proc.wait(timeout=5) - except subprocess.TimeoutExpired: - proc.kill() - proc.wait() - finally: - stdout_file.close() - stderr_file.close() diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 60f0ec975d..2322114486 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -1,4 +1,5 @@ """Simulating layers on their own to observe their behaviour.""" + import io import json import pandas as pd @@ -13,9 +14,12 @@ from threading import Lock from typing import Any, Literal, TypeAlias -from finn.transformation.fpgadataflow.simulation import Simulation, store_fifo_data +from finn.transformation.fpgadataflow.simulation import ( + Simulation, + store_fifo_data, + SimulationController, +) from finn.transformation.fpgadataflow.simulation_build import SimulationType -from finn.transformation.fpgadataflow.simulation_controller import SimulationController from finn.util.exception import FINNInternalError from finn.util.logging import log @@ -49,8 +53,7 @@ def get_logfile_path(self, binary_or_idx: Path | int) -> Path: """Get the logfile for the given binary or process index.""" if type(binary_or_idx) is int: return ( - self.logdir / f"{binary_or_idx}_log_isolated_" - f"{self.names[binary_or_idx]}_python.txt" + self.logdir / f"{binary_or_idx}_log_isolated_{self.names[binary_or_idx]}_python.txt" ) elif type(binary_or_idx) in [Path, PurePath, PosixPath]: # noqa process_idx = self.binaries.index(binary_or_idx) # type: ignore @@ -103,7 +106,7 @@ def _f(future: Future) -> None: with datalock: done += 1 log.info( - f"[ [bold green]{int(100 * float(done)/float(total))}%" + f"[ [bold green]{int(100 * float(done) / float(total))}%" f"[/bold green] ] {name} done!", extra={"markup": True, "highlighter": None}, ) @@ -204,8 +207,7 @@ def write_log(msg: str) -> None: write_log("Status response:") write_log(f"\tTotal cycles: {total_cycles}") write_log( - f"\tInput data simulated: {percent_simulated_input}% " - f"({in_done} / {in_target})" + f"\tInput data simulated: {percent_simulated_input}% ({in_done} / {in_target})" ) write_log( f"\tOutput data simulated: {percent_simulated_output}% " diff --git a/tests/fpgadataflow/test_fifosizing.py b/tests/fpgadataflow/test_fifosizing.py index 6c66359716..f53a8fddb8 100644 --- a/tests/fpgadataflow/test_fifosizing.py +++ b/tests/fpgadataflow/test_fifosizing.py @@ -27,14 +27,18 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation import pytest +from pathlib import Path import json import shutil import torch from brevitas.export import export_qonnx from onnx import TensorProto, helper -from qonnx.core.datatype import DataType +from qonnx.core.datatype import BaseDataType, DataType from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.general import GiveUniqueNodeNames @@ -44,19 +48,96 @@ import finn.builder.build_dataflow as build import finn.builder.build_dataflow_config as build_cfg -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.basic import make_build_dir from tests.testing_util.test import get_trained_network_and_ishape +def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + """Run FIFO sizing for testing.""" + cfg = build_cfg.DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform( + RunLayerParallelSimulation( + fpga_part, clk_ns, cfg + ) + ) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model -def fetch_test_model(topology, wbits=2, abits=2): - tmp_output_dir = make_build_dir("build_fifosizing_%s_" % topology) +def fetch_test_model(topology:str, wbits:int=2, abits:int=2) -> Path: + """Fetch the test model for the given topology and bitwidths, + export it to QONNX, and return the output directory.""" + tmp_output_dir = Path(make_build_dir(f"build_fifosizing_{topology}_")) (model, ishape) = get_trained_network_and_ishape(topology, wbits, abits) - chkpt_name = tmp_output_dir + "/model.onnx" + chkpt_name = tmp_output_dir / "model.onnx" export_qonnx(model, torch.randn(ishape), chkpt_name) return tmp_output_dir +def make_multi_io_modelwrapper(ch:int, pe:int, idt:BaseDataType) -> ModelWrapper: + """Make a simple ONNX model with one addstreams node and one duplicate streams node, + with multiple inputs and outputs, for testing multi-IO FIFO sizing.""" + in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, [1, ch]) + in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, [1, ch]) + mid = helper.make_tensor_value_info("mid", TensorProto.FLOAT, [1, ch]) + out0 = helper.make_tensor_value_info("out0", TensorProto.FLOAT, [1, ch]) + out1 = helper.make_tensor_value_info("out1", TensorProto.FLOAT, [1, ch]) + + addstreams_node = helper.make_node( + "ElementwiseAdd", + ["in0", "in1"], + ["mid"], + domain="finn.custom_op.fpgadataflow", + backend="fpgadataflow", + lhs_shape=[1, ch], + rhs_shape=[1, ch], + out_shape=[1, ch], + lhs_dtype=idt.name, + rhs_dtype=idt.name, + out_dtype=idt.name, + lhs_style="input", + rhs_style="input", + PE=pe, + inFIFODepths=[2, 2], + ) + duplicate_node = helper.make_node( + "DuplicateStreams", + ["mid"], + ["out0", "out1"], + domain="finn.custom_op.fpgadataflow", + backend="fpgadataflow", + NumChannels=ch, + NumOutputStreams=2, + PE=pe, + inputDataType=idt.name, + numInputVectors=[1], + outFIFODepths=[2, 2], + ) + graph = helper.make_graph( + nodes=[addstreams_node, duplicate_node], + name="graph", + inputs=[in0, in1], + outputs=[out0, out1], + value_info=[mid], + ) + + model = qonnx_make_model(graph, producer_name="multi-io-model") + model = ModelWrapper(model) + + model.set_tensor_datatype("in0", idt) + model.set_tensor_datatype("in1", idt) + + model = model.transform(InferShapes()) + model = model.transform(InferDataTypes()) + + return model + @pytest.mark.slow @pytest.mark.vivado @@ -72,7 +153,6 @@ def test_fifosizing_linear(method, topology): target_fps=10000 if topology == "tfc" else 1000, synth_clk_period_ns=10.0, board="Pynq-Z1", - rtlsim_batch_size=100 if topology == "tfc" else 2, generate_outputs=[ build_cfg.DataflowOutputType.ESTIMATE_REPORTS, build_cfg.DataflowOutputType.STITCHED_IP, @@ -129,58 +209,4 @@ def test_fifosizing_multi_io(): assert len(fifos) > 1, "No FIFOs inserted" -def make_multi_io_modelwrapper(ch, pe, idt): - in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, [1, ch]) - in1 = helper.make_tensor_value_info("in1", TensorProto.FLOAT, [1, ch]) - mid = helper.make_tensor_value_info("mid", TensorProto.FLOAT, [1, ch]) - out0 = helper.make_tensor_value_info("out0", TensorProto.FLOAT, [1, ch]) - out1 = helper.make_tensor_value_info("out1", TensorProto.FLOAT, [1, ch]) - - addstreams_node = helper.make_node( - "ElementwiseAdd", - ["in0", "in1"], - ["mid"], - domain="finn.custom_op.fpgadataflow", - backend="fpgadataflow", - lhs_shape=[1, ch], - rhs_shape=[1, ch], - out_shape=[1, ch], - lhs_dtype=idt.name, - rhs_dtype=idt.name, - out_dtype=idt.name, - lhs_style="input", - rhs_style="input", - PE=pe, - inFIFODepths=[2, 2], - ) - duplicate_node = helper.make_node( - "DuplicateStreams", - ["mid"], - ["out0", "out1"], - domain="finn.custom_op.fpgadataflow", - backend="fpgadataflow", - NumChannels=ch, - NumOutputStreams=2, - PE=pe, - inputDataType=idt.name, - numInputVectors=[1], - outFIFODepths=[2, 2], - ) - graph = helper.make_graph( - nodes=[addstreams_node, duplicate_node], - name="graph", - inputs=[in0, in1], - outputs=[out0, out1], - value_info=[mid], - ) - - model = qonnx_make_model(graph, producer_name="multi-io-model") - model = ModelWrapper(model) - - model.set_tensor_datatype("in0", idt) - model.set_tensor_datatype("in1", idt) - - model = model.transform(InferShapes()) - model = model.transform(InferDataTypes()) - return model diff --git a/tests/fpgadataflow/test_fpgadataflow_mvau.py b/tests/fpgadataflow/test_fpgadataflow_mvau.py index 93d3f11e6b..877ddd63b1 100644 --- a/tests/fpgadataflow/test_fpgadataflow_mvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_mvau.py @@ -27,7 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from finn.builder.build_dataflow_config import DataflowBuildConfig -from finn.transformation.fpgadataflow.simulation import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes from finn.transformation.fpgadataflow.simulation_build import BuildSimulation from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation import pytest diff --git a/tests/fpgadataflow/test_fpgadataflow_thresholding.py b/tests/fpgadataflow/test_fpgadataflow_thresholding.py index b12c90c6e3..558653671f 100644 --- a/tests/fpgadataflow/test_fpgadataflow_thresholding.py +++ b/tests/fpgadataflow/test_fpgadataflow_thresholding.py @@ -27,7 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. from finn.builder.build_dataflow_config import DataflowBuildConfig -from finn.transformation.fpgadataflow.simulation import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes from finn.transformation.fpgadataflow.simulation_build import BuildSimulation from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation import pytest diff --git a/tests/testing_util/test.py b/tests/testing_util/test.py index 2eaf4fdca1..7c0392de39 100644 --- a/tests/testing_util/test.py +++ b/tests/testing_util/test.py @@ -32,6 +32,7 @@ import onnx import onnx.numpy_helper as nph import os +import torch import torchvision.transforms.functional as torchvision_util import warnings from brevitas_examples import bnn_pynq, imagenet_classification @@ -62,8 +63,8 @@ } -def get_test_model(netname, wbits, abits, pretrained): - """Returns the model specified by input arguments from the Brevitas BNN-PYNQ +def get_test_model(netname: str, wbits: int, abits: int, pretrained: bool) -> torch.nn.Module: + """Return the model specified by input arguments from the Brevitas BNN-PYNQ test networks. Pretrained weights loaded if pretrained is True.""" model_cfg = (netname, wbits, abits) model_def_fxn = example_map[model_cfg] @@ -72,7 +73,7 @@ def get_test_model(netname, wbits, abits, pretrained): def get_test_model_trained(netname, wbits, abits): - "get_test_model with pretrained=True" + """get_test_model with pretrained=True""" return get_test_model(netname, wbits, abits, pretrained=True) @@ -148,9 +149,10 @@ def get_example_input(topology): raise Exception("Unknown topology, can't return example input") -def get_trained_network_and_ishape(topology, wbits, abits): - "Return (trained_model, shape) for given BNN-PYNQ test config." - +def get_trained_network_and_ishape( + topology: str, wbits: int, abits: int +) -> tuple[torch.nn.Module, tuple[int, int, int, int]]: + """Return (trained_model, shape) for given BNN-PYNQ test config.""" topology_to_ishape = { "tfc": (1, 1, 28, 28), "lfc": (1, 1, 28, 28), From 550205b3f73049707c5bb1e5d3468e5d922fc888 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 11:40:01 +0200 Subject: [PATCH 108/170] Fix linting --- external_dependencies.yaml | 2 + finn_xsi/finn_xsi/adapter.py | 3 +- finn_xsi/finn_xsi/sim_engine.py | 5 +-- src/finn/builder/build_dataflow_steps.py | 7 ++-- src/finn/core/rtlsim_exec.py | 16 +++----- .../fpgadataflow/elementwise_binary.py | 17 ++++---- .../custom_op/fpgadataflow/rtl/finn_loop.py | 3 +- src/finn/custom_op/fpgadataflow/rtlbackend.py | 21 ++++++---- src/finn/interface/run_finn.py | 8 ++-- .../fpgadataflow/compile_cppsim.py | 7 ++-- .../fpgadataflow/make_driver.py | 2 +- .../fpgadataflow/prepare_rtlsim.py | 11 ++--- .../fpgadataflow/set_exec_mode.py | 15 +++---- .../fpgadataflow/set_fifo_depths.py | 19 +++------ .../transformation/fpgadataflow/simulation.py | 19 ++++----- .../fpgadataflow/simulation_build.py | 4 +- .../fpgadataflow/simulation_connected.py | 15 ++++--- .../fpgadataflow/simulation_isolated.py | 2 +- src/finn/util/config.py | 7 ++-- src/finn/xsi/__init__.py | 6 +-- src/finn/xsi/setup.py | 8 ++-- tests/fpgadataflow/test_fifosizing.py | 40 +++++++++---------- ...dataflow_convinputgenerator_rtl_dynamic.py | 3 +- tests/fpgadataflow/test_fpgadataflow_mvau.py | 32 +++++++-------- .../test_fpgadataflow_thresholding.py | 29 ++++++-------- .../test_fpgadataflow_thresholding_runtime.py | 3 +- tests/fpgadataflow/test_simulation_build.py | 6 ++- tests/util/test_config.py | 8 ++-- 28 files changed, 148 insertions(+), 170 deletions(-) diff --git a/external_dependencies.yaml b/external_dependencies.yaml index e19fa7ca1d..84a1a2938a 100644 --- a/external_dependencies.yaml +++ b/external_dependencies.yaml @@ -51,3 +51,5 @@ direct_download_deps: url: "https://dpoauwgwqsy2x.cloudfront.net/Download/pynq-z2.zip" do_unzip: True target_directory: "board_files" + +custom_deps: diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 80a21def9b..6c1da08637 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -14,13 +14,12 @@ import os import re from finn_xsi.sim_engine import SimEngine +from pathlib import Path from typing import Literal from finn.util.basic import launch_process_helper from finn.util.exception import FINNInternalError, FINNUserError -from pathlib import Path - def locate_glbl() -> Path | None: """Try to determine the glbl.v file path from environment variables. diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py index de85618dc0..c9fe880145 100644 --- a/finn_xsi/finn_xsi/sim_engine.py +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -10,13 +10,12 @@ ############################################################################# """Simulation engine utilities for FINN XSI-based hardware runs.""" -from typing import Literal -from collections.abc import Generator, Iterator - import numpy as np # provided via pybind11 import xsi +from collections.abc import Generator, Iterator +from typing import Literal class SimEngine: diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 0a5caa8e06..70e9729164 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -103,11 +103,11 @@ from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode from finn.transformation.fpgadataflow.set_fifo_depths import ( ApplyFIFODepthsFromFile, + ApplySimulatedFIFOSizes, SplitLargeFIFOs, ) from finn.transformation.fpgadataflow.set_folding import SetFolding from finn.transformation.fpgadataflow.set_loop_boundary import SetLoopBoundary -from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType from finn.transformation.fpgadataflow.simulation_connected import ( NodeConnectedSimulation, @@ -131,7 +131,7 @@ from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds from finn.util.basic import get_liveness_threshold_cycles, get_rtlsim_trace_depth, getHWCustomOp from finn.util.config import extract_model_config_to_json -from finn.util.exception import FINNUserError, FINNInternalError +from finn.util.exception import FINNUserError from finn.util.execution import execute_parent from finn.util.logging import log from finn.util.mlo_sim import is_mlo, mlo_prehook_func_factory @@ -407,6 +407,7 @@ def step_hw_ipgen( return model + @register_build_dataflow_step() def step_set_fifo_depths( model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None @@ -1328,7 +1329,7 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi del res["fifo_cycles_until_first_valid"] cycle_per_sec = 1e9 / cfg.synth_clk_period_ns res["throughput_fps"] = cycle_per_sec / res["intervals"][0] # type: ignore - #TODO: Add latency measurement + # TODO: Add latency measurement # Attach entry to output outputs.append(res) diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index 8780626ddb..c5ba8e3cd7 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -27,30 +27,24 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +import numpy as np from collections.abc import Callable - from finn_xsi.sim_engine import SimEngine -import numpy as np from pathlib import Path -from finn.util.basic import getHWCustomOp +from typing import TYPE_CHECKING from finn import xsi as finnxsi -from finn.util.basic import ( - get_liveness_threshold_cycles, - make_build_dir, -) +from finn.util.basic import get_liveness_threshold_cycles, getHWCustomOp, make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy -from typing import TYPE_CHECKING - if TYPE_CHECKING: from qonnx.core.datatype import BaseDataType from qonnx.core.modelwrapper import ModelWrapper -from finn.util.exception import FINNUserError - from ast import literal_eval +from finn.util.exception import FINNUserError + def prep_rtlsim_io_dict( model: "ModelWrapper", execution_context: dict[str, np.ndarray] diff --git a/src/finn/custom_op/fpgadataflow/elementwise_binary.py b/src/finn/custom_op/fpgadataflow/elementwise_binary.py index 04834215ba..b03e85b64a 100644 --- a/src/finn/custom_op/fpgadataflow/elementwise_binary.py +++ b/src/finn/custom_op/fpgadataflow/elementwise_binary.py @@ -27,12 +27,13 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import numpy as np -from typing import cast # Helper for creating ONNX nodes -from onnx import NodeProto, helper as oh +from onnx import NodeProto +from onnx import helper as oh from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper +from typing import cast from finn.custom_op.fpgadataflow import register_custom_op from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp @@ -136,17 +137,17 @@ def out_dtype(self): # Shape attribute as property for convenience @property - def lhs_shape(self) ->np.ndarray: + def lhs_shape(self) -> np.ndarray: return cast("np.ndarray", self.get_nodeattr("lhs_shape")) # Shape attribute as property for convenience @property - def rhs_shape(self) ->np.ndarray: + def rhs_shape(self) -> np.ndarray: return cast("np.ndarray", self.get_nodeattr("rhs_shape")) # Shape attribute as property for convenience @property - def out_shape(self) ->np.ndarray: + def out_shape(self) -> np.ndarray: return cast("np.ndarray", self.get_nodeattr("out_shape")) # Style attribute as property for convenience @@ -335,9 +336,9 @@ def minimize_accumulator_width(self, model: ModelWrapper): if not all([self.lhs_dtype.is_integer(), self.rhs_dtype.is_integer()]): # Check the annotated tensor data type corresponds to the stored # attribute - assert model.get_tensor_datatype(self.onnx_node.output[0]) == self.out_dtype, ( - f"Output type mismatch for {self.onnx_node.name}" - ) + assert ( + model.get_tensor_datatype(self.onnx_node.output[0]) == self.out_dtype + ), f"Output type mismatch for {self.onnx_node.name}" # Exit here, returning the not-minimized data type return self.out_dtype # Call the output type derivation specialized by the concrete operator diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index 3c2a0e4916..8f4b320ea5 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -43,6 +43,7 @@ from typing import cast import finn.core.onnx_exec as oxe +import finn.xsi as finnxsi from finn.analysis.fpgadataflow.dataflow_performance import dataflow_performance from finn.custom_op.fpgadataflow import templates from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp @@ -55,8 +56,6 @@ from finn.util.mlo_sim import mlo_prehook_func_factory from finn.util.settings import get_settings -import finn.xsi as finnxsi - def collect_ip_dirs(model, ipstitch_path): # collect list of all IP dirs diff --git a/src/finn/custom_op/fpgadataflow/rtlbackend.py b/src/finn/custom_op/fpgadataflow/rtlbackend.py index ba9c9e62e1..1a41afc33e 100644 --- a/src/finn/custom_op/fpgadataflow/rtlbackend.py +++ b/src/finn/custom_op/fpgadataflow/rtlbackend.py @@ -43,14 +43,13 @@ from onnx import GraphProto from qonnx.core.modelwrapper import ModelWrapper +import finn.xsi as finnxsi from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.util.basic import make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy -from finn.util.exception import FINNInternalError +from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log -import finn.xsi as finnxsi - class RTLBackend(HWCustomOp, ABC): """RTLBackend class all custom ops that correspond to a module in finn-rtllib @@ -209,7 +208,11 @@ def execute_node( # container datatype inp_val = inp_val.astype(np.float32) - assert inp_val.shape == exp_ishape, "Input shape doesn't match expected shape." + if inp_val.shape != exp_ishape: + raise FINNInternalError( + f"Input shape for input {i} of node {node.name} doesn't match expected " + f"shape. (got {inp_val.shape}, expected {exp_ishape})" + ) export_idt = self.get_input_datatype(i) reshaped_input = inp_val.reshape(folded_ishape) @@ -249,12 +252,14 @@ def execute_node( output = np.asarray([output], dtype=np.float32).reshape(*exp_oshape) context[outp] = output - assert context[outp].shape == exp_oshape, ( - "Output shape doesn't match expected shape." - ) + if context[outp].shape != exp_oshape: + raise FINNInternalError( + f"Output shape for output {o} of node {node.name} doesn't match expected " + f"shape. (got {context[outp].shape}, expected {exp_oshape})" + ) else: - raise Exception( + raise FINNUserError( f"""Invalid value for attribute exec_mode! Is currently set to: {mode} has to be set to one of the following value ("cppsim", "rtlsim")""" ) diff --git a/src/finn/interface/run_finn.py b/src/finn/interface/run_finn.py index c471c6a22a..d3e00a1bbd 100644 --- a/src/finn/interface/run_finn.py +++ b/src/finn/interface/run_finn.py @@ -474,9 +474,9 @@ def prepare_finn( if "LD_LIBRARY_PATH" not in os.environ.keys(): os.environ["LD_LIBRARY_PATH"] = f"/lib/x86_64-linux-gnu/:{vivado_path}/lib/lnx64.o" else: - os.environ["LD_LIBRARY_PATH"] = ( - f"/lib/x86_64-linux-gnu/:{vivado_path}/lib/lnx64.o:{os.environ['LD_LIBRARY_PATH']}" - ) + os.environ[ + "LD_LIBRARY_PATH" + ] = f"/lib/x86_64-linux-gnu/:{vivado_path}/lib/lnx64.o:{os.environ['LD_LIBRARY_PATH']}" # Automatically set XILINX_LOCAL_USER_DATA to avoid issues later on if "XILINX_LOCAL_USER_DATA" in os.environ and os.environ["XILINX_LOCAL_USER_DATA"] != "no": @@ -592,7 +592,7 @@ def _build( sys.exit(1) else: model = mp - status(f"Starting FINN build with config {flow_config.name} and model {model.name}!") # type: ignore + status(f"Starting FINN build with config {flow_config.name} and model {model.name}!") if finn_build_dir is not None: finn_build_dir = finn_build_dir.expanduser().absolute() finn_build_dir.mkdir(parents=True, exist_ok=True) diff --git a/src/finn/transformation/fpgadataflow/compile_cppsim.py b/src/finn/transformation/fpgadataflow/compile_cppsim.py index b0adef0ef4..295cbd264f 100644 --- a/src/finn/transformation/fpgadataflow/compile_cppsim.py +++ b/src/finn/transformation/fpgadataflow/compile_cppsim.py @@ -30,14 +30,13 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from typing import cast, TYPE_CHECKING -from onnx import NodeProto - import qonnx.custom_op.registry as registry +from onnx import NodeProto from qonnx.transformation.base import NodeLocalTransformation +from typing import TYPE_CHECKING, cast -from finn.util.fpgadataflow import is_hls_node from finn.util.exception import FINNUserError +from finn.util.fpgadataflow import is_hls_node if TYPE_CHECKING: from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend diff --git a/src/finn/transformation/fpgadataflow/make_driver.py b/src/finn/transformation/fpgadataflow/make_driver.py index 09ea587535..2092227bfb 100644 --- a/src/finn/transformation/fpgadataflow/make_driver.py +++ b/src/finn/transformation/fpgadataflow/make_driver.py @@ -36,6 +36,7 @@ import shutil import subprocess import sys +from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation @@ -49,7 +50,6 @@ from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log -from pathlib import Path def update_bitfile_path_after_copy(bitfile_path: Path, json_path: Path) -> None: """Update the xclbinPath in the JSON configuration to point to the new bitfile location. diff --git a/src/finn/transformation/fpgadataflow/prepare_rtlsim.py b/src/finn/transformation/fpgadataflow/prepare_rtlsim.py index 8639bcf90e..a6b56fc266 100644 --- a/src/finn/transformation/fpgadataflow/prepare_rtlsim.py +++ b/src/finn/transformation/fpgadataflow/prepare_rtlsim.py @@ -32,22 +32,19 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from typing import Literal, cast, TYPE_CHECKING - from onnx import NodeProto - -from finn.util.basic import getHWCustomOp from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import NodeLocalTransformation +from typing import TYPE_CHECKING, Literal, cast from finn.transformation.fpgadataflow.replace_verilog_relpaths import ReplaceVerilogRelPaths -from finn.util.fpgadataflow import is_hls_node, is_rtl_node - +from finn.util.basic import getHWCustomOp from finn.util.exception import FINNUserError +from finn.util.fpgadataflow import is_hls_node, is_rtl_node if TYPE_CHECKING: - from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend + from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend class PrepareRTLSim(NodeLocalTransformation): diff --git a/src/finn/transformation/fpgadataflow/set_exec_mode.py b/src/finn/transformation/fpgadataflow/set_exec_mode.py index 7a4451e433..dc52ea8eef 100644 --- a/src/finn/transformation/fpgadataflow/set_exec_mode.py +++ b/src/finn/transformation/fpgadataflow/set_exec_mode.py @@ -32,13 +32,12 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from typing import Literal - -from finn.util.exception import FINNUserError -from qonnx.core.modelwrapper import ModelWrapper import qonnx.custom_op.registry as registry +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation +from typing import Literal +from finn.util.exception import FINNUserError from finn.util.fpgadataflow import is_hls_node, is_rtl_node @@ -49,7 +48,7 @@ class SetExecMode(Transformation): for RTL components, by default the execution of the HW op parent is executed.""" - def __init__(self, mode:str) -> None: + def __init__(self, mode: str) -> None: """Construct the transformation.""" super().__init__() self.mode = mode @@ -66,8 +65,10 @@ def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: inst.set_nodeattr("exec_mode", self.mode) # ensure that sim_mode is now set if inst.get_nodeattr("exec_mode") == "": - raise FINNUserError("""Transformation - was not successful. Node attribute "exec_mode" is not set""") + raise FINNUserError( + """Transformation + was not successful. Node attribute "exec_mode" is not set""" + ) except KeyError: # exception if op_type is not supported raise FINNUserError( diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index e9bbd7a64e..70bdd78303 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -30,28 +30,21 @@ """Transformations for inserting and setting the size of FIFOs in FINN dataflow graphs.""" import json -from onnx import TensorProto, helper +from onnx import NodeProto, TensorProto, helper from pathlib import Path from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper +from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation -from qonnx.transformation.general import GiveReadableTensorNames, SortGraph +from qonnx.transformation.general import GiveReadableTensorNames, GiveUniqueNodeNames, SortGraph from typing import Literal, TypeAlias, cast -from finn.util.basic import getHWCustomOp -from finn.util.exception import FINNUserError -from finn.util.logging import log - -from onnx import NodeProto - - -from qonnx.custom_op.registry import getCustomOp -from qonnx.transformation.general import GiveUniqueNodeNames - from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -from finn.util.exception import FINNInternalError +from finn.util.basic import getHWCustomOp +from finn.util.exception import FINNInternalError, FINNUserError +from finn.util.logging import log FIFODepthConfig: TypeAlias = list[dict[str, list[int]]] diff --git a/src/finn/transformation/fpgadataflow/simulation.py b/src/finn/transformation/fpgadataflow/simulation.py index 01971f2edc..86e3295da7 100644 --- a/src/finn/transformation/fpgadataflow/simulation.py +++ b/src/finn/transformation/fpgadataflow/simulation.py @@ -1,27 +1,25 @@ """Manages the Simulation superclass as well as general simulation related transforms.""" -import pandas as pd -from pathlib import Path -from qonnx.core.modelwrapper import ModelWrapper -from typing import Any, TypeAlias - -from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType -from finn.util.exception import FINNInternalError, FINNUserError -from finn.util.logging import log - import json +import pandas as pd import socket import subprocess import threading import time +from pathlib import Path +from qonnx.core.modelwrapper import ModelWrapper from rich.console import Console from threading import Lock +from typing import Any, TypeAlias +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation, SimulationType from finn.util.basic import make_build_dir -from finn.util.logging import ThreadsafeProgressDisplay +from finn.util.exception import FINNInternalError, FINNUserError +from finn.util.logging import ThreadsafeProgressDisplay, log FIFODepthConfig: TypeAlias = list[dict[str, list[int]]] + def store_fifo_data( model: ModelWrapper, data: pd.DataFrame, @@ -497,4 +495,3 @@ def _cleanup_sockets(self) -> None: finally: stdout_file.close() stderr_file.close() - diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 40377da6cf..e5768ff05d 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -1,7 +1,6 @@ """Build FINN Simulations.""" import contextlib -import finn.xsi as finnxsi import numpy as np import onnx import os @@ -15,6 +14,7 @@ from collections.abc import Callable from concurrent.futures import Future, ThreadPoolExecutor from enum import Enum +from jinja2 import Environment from onnx import NodeProto, TensorProto, ValueInfoProto from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper @@ -25,6 +25,7 @@ from subprocess import CalledProcessError from typing import TYPE_CHECKING, Any, cast +import finn.xsi as finnxsi from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP @@ -34,7 +35,6 @@ from finn.util.basic import getHWCustomOp, launch_process_helper, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log -from jinja2 import Environment if TYPE_CHECKING: from collections.abc import Sequence diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index a1509f5925..fd714ea9a4 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -20,15 +20,14 @@ from finn.transformation.fpgadataflow.set_fifo_depths import get_fifo_split_configs from finn.transformation.fpgadataflow.simulation import ( Simulation, + SimulationController, SimulationType, store_fifo_data, - SimulationController, ) from finn.util.basic import getHWCustomOp, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log - # Hardware BRAM FIFOs lose entries to internal pipeline registers compared to the software FIFO # model (which has exact capacity). This constant accounts for that overhead so that the # minimization algorithm finds depths that are safe to deploy on hardware. @@ -210,9 +209,9 @@ def run( cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals - fifo_cycles_until_first_valid_results[sim_name] = ( - fifo_cycles_until_first_valid - ) + fifo_cycles_until_first_valid_results[ + sim_name + ] = fifo_cycles_until_first_valid timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -247,9 +246,9 @@ def run( ) = result # Only update if not already collected if sim_name not in fifo_results: - fifo_cycles_until_first_valid_results[sim_name] = ( - fifo_cycles_until_first_valid - ) + fifo_cycles_until_first_valid_results[ + sim_name + ] = fifo_cycles_until_first_valid fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util cycles_results[sim_name] = cycles diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 2322114486..7e261101c2 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -16,8 +16,8 @@ from finn.transformation.fpgadataflow.simulation import ( Simulation, - store_fifo_data, SimulationController, + store_fifo_data, ) from finn.transformation.fpgadataflow.simulation_build import SimulationType from finn.util.exception import FINNInternalError diff --git a/src/finn/util/config.py b/src/finn/util/config.py index cb86cc2287..1c76963edd 100644 --- a/src/finn/util/config.py +++ b/src/finn/util/config.py @@ -13,14 +13,13 @@ # https://github.com/fastmachinelearning/qonnx/blob/ # abb9eb12e0248014a805f505aacfaeb14d42409a/src/qonnx/util/config.py -from numpy import typing as npt +import contextlib import json -from pathlib import Path import onnx +from numpy import typing as npt +from pathlib import Path from qonnx.custom_op.registry import getCustomOp, is_custom_op - from typing import TYPE_CHECKING -import contextlib if TYPE_CHECKING: from qonnx.core.modelwrapper import ModelWrapper diff --git a/src/finn/xsi/__init__.py b/src/finn/xsi/__init__.py index 92effbbaaf..5bf7f06f5b 100644 --- a/src/finn/xsi/__init__.py +++ b/src/finn/xsi/__init__.py @@ -23,9 +23,9 @@ import sys from pathlib import Path from typing import Any -from finn.util.logging import log -from finn.util.exception import FINNUserError +from finn.util.exception import FINNUserError +from finn.util.logging import log # Track if auto-install has been attempted _auto_install_attempted = False @@ -147,7 +147,7 @@ def _load_modules() -> bool: raise FINNUserError("XSI not available. Please run 'finn deps update' to install XSI.") from finn_xsi.sim_engine import SimEngine # noqa -from finn_xsi.adapter import ( #noqa +from finn_xsi.adapter import ( # noqa locate_glbl, compile_sim_obj, get_simkernel_so, diff --git a/src/finn/xsi/setup.py b/src/finn/xsi/setup.py index 05ad3b1c0c..ef0f2f2744 100644 --- a/src/finn/xsi/setup.py +++ b/src/finn/xsi/setup.py @@ -118,10 +118,12 @@ def build_xsi(force: bool = False, verbose: bool = True) -> bool: bool: True if build successful """ xsi_path = get_settings().finn_xsi - + vivado_path = os.environ.get("XILINX_VIVADO") if vivado_path is None: - raise EnvironmentError("XILINX_VIVADO environment variable not set. Please source Vivado settings.") + raise EnvironmentError( + "XILINX_VIVADO environment variable not set. Please source Vivado settings." + ) match = re.search(r"\b(20\d{2})\.(1|2)\b", vivado_path) if not match: raise ValueError(f"Could not parse Vivado version from XILINX_VIVADO path: {vivado_path}") @@ -210,7 +212,7 @@ def build_xsi(force: bool = False, verbose: bool = True) -> bool: if verbose and result.stdout: print(result.stdout) - + # Write version info version_file = xsi_path / "VERSION" with version_file.open("w") as f: f.write(f"Vivado {year}.{minor}") diff --git a/tests/fpgadataflow/test_fifosizing.py b/tests/fpgadataflow/test_fifosizing.py index f53a8fddb8..60c0e86907 100644 --- a/tests/fpgadataflow/test_fifosizing.py +++ b/tests/fpgadataflow/test_fifosizing.py @@ -27,17 +27,14 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes -from finn.transformation.fpgadataflow.simulation_build import BuildSimulation -from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation import pytest -from pathlib import Path import json import shutil import torch from brevitas.export import export_qonnx from onnx import TensorProto, helper +from pathlib import Path from qonnx.core.datatype import BaseDataType, DataType from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp @@ -48,30 +45,31 @@ import finn.builder.build_dataflow as build import finn.builder.build_dataflow_config as build_cfg +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.basic import make_build_dir from tests.testing_util.test import get_trained_network_and_ishape + def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = build_cfg.DataflowBuildConfig() model = model.transform( - BuildSimulation( - fpga_part, - clk_ns, - True, - performance_sim=False, - ) - ) - model = model.transform( - RunLayerParallelSimulation( - fpga_part, clk_ns, cfg - ) - ) + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) model = model.transform(ApplySimulatedFIFOSizes(cfg)) return model -def fetch_test_model(topology:str, wbits:int=2, abits:int=2) -> Path: + +def fetch_test_model(topology: str, wbits: int = 2, abits: int = 2) -> Path: """Fetch the test model for the given topology and bitwidths, export it to QONNX, and return the output directory.""" tmp_output_dir = Path(make_build_dir(f"build_fifosizing_{topology}_")) @@ -80,7 +78,8 @@ def fetch_test_model(topology:str, wbits:int=2, abits:int=2) -> Path: export_qonnx(model, torch.randn(ishape), chkpt_name) return tmp_output_dir -def make_multi_io_modelwrapper(ch:int, pe:int, idt:BaseDataType) -> ModelWrapper: + +def make_multi_io_modelwrapper(ch: int, pe: int, idt: BaseDataType) -> ModelWrapper: """Make a simple ONNX model with one addstreams node and one duplicate streams node, with multiple inputs and outputs, for testing multi-IO FIFO sizing.""" in0 = helper.make_tensor_value_info("in0", TensorProto.FLOAT, [1, ch]) @@ -204,9 +203,6 @@ def test_fifosizing_multi_io(): model = make_multi_io_modelwrapper(2, 2, DataType["INT4"]) model = model.transform(SpecializeLayers("xc7z020clg400-1")) model = model.transform(GiveUniqueNodeNames()) - model = model.transform(InsertAndSetFIFODepths("xc7z020clg400-1", 5)) + model = insert_and_set_fifo_depths(model, "xc7z020clg400-1", 5) fifos = model.get_nodes_by_op_type("StreamingFIFO_rtl") assert len(fifos) > 1, "No FIFOs inserted" - - - diff --git a/tests/fpgadataflow/test_fpgadataflow_convinputgenerator_rtl_dynamic.py b/tests/fpgadataflow/test_fpgadataflow_convinputgenerator_rtl_dynamic.py index 66a7a2c5e3..7b5b9f057b 100644 --- a/tests/fpgadataflow/test_fpgadataflow_convinputgenerator_rtl_dynamic.py +++ b/tests/fpgadataflow/test_fpgadataflow_convinputgenerator_rtl_dynamic.py @@ -52,6 +52,7 @@ import finn.core.onnx_exec as oxe import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw import finn.transformation.streamline.absorb as absorb +import finn.xsi as finnxsi from finn.core.onnx_exec import execute_onnx from finn.core.rtlsim_exec import rtlsim_exec from finn.transformation.fpgadataflow.create_dataflow_partition import CreateDataflowPartition @@ -63,8 +64,6 @@ from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.basic import get_liveness_threshold_cycles -import finn.xsi as finnxsi - def create_conv_model(idim_h, idim_w, ifm, k, stride, ofm, idt, wdt, pad_mode, depthwise): np.random.seed(0) diff --git a/tests/fpgadataflow/test_fpgadataflow_mvau.py b/tests/fpgadataflow/test_fpgadataflow_mvau.py index 877ddd63b1..1c2a148807 100644 --- a/tests/fpgadataflow/test_fpgadataflow_mvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_mvau.py @@ -26,10 +26,6 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from finn.builder.build_dataflow_config import DataflowBuildConfig -from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes -from finn.transformation.fpgadataflow.simulation_build import BuildSimulation -from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation import pytest import numpy as np @@ -49,6 +45,7 @@ from finn import xsi as finnxsi from finn.analysis.fpgadataflow.exp_cycles_per_layer import exp_cycles_per_layer from finn.analysis.fpgadataflow.hls_synth_res_estimation import hls_synth_res_estimation +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.core.rtlsim_exec import rtlsim_exec from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -59,31 +56,31 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.general import ApplyConfig from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds from finn.util.basic import is_versal - from finn.xsi import SimEngine + def InsertAndSetFIFODepths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: cfg = DataflowBuildConfig() model = model.transform( - BuildSimulation( - fpga_part, - clk_ns, - True, - performance_sim=False, - ) - ) - model = model.transform( - RunLayerParallelSimulation( - fpga_part, clk_ns, cfg - ) - ) + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) model = model.transform(ApplySimulatedFIFOSizes(cfg)) return model + def make_single_fclayer_modelwrapper(W, pe, simd, wdt, idt, odt, T=None, tdt=None): mw = W.shape[0] mh = W.shape[1] @@ -676,6 +673,7 @@ def read_weights(sim: SimEngine) -> None: y_expected == output_mvau_rtl_stitch ).all(), "Output of ONNX model not matching output of stitched-IP RTL model!" + @pytest.mark.parametrize("mh", [18]) @pytest.mark.parametrize("mw", [32]) @pytest.mark.parametrize("pe", [1, 9, 18]) diff --git a/tests/fpgadataflow/test_fpgadataflow_thresholding.py b/tests/fpgadataflow/test_fpgadataflow_thresholding.py index 558653671f..89d11977dc 100644 --- a/tests/fpgadataflow/test_fpgadataflow_thresholding.py +++ b/tests/fpgadataflow/test_fpgadataflow_thresholding.py @@ -26,10 +26,6 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from finn.builder.build_dataflow_config import DataflowBuildConfig -from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes -from finn.transformation.fpgadataflow.simulation_build import BuildSimulation -from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation import pytest import numpy as np @@ -45,6 +41,7 @@ import finn.core.onnx_exec as oxe from finn.analysis.fpgadataflow.exp_cycles_per_layer import exp_cycles_per_layer from finn.analysis.fpgadataflow.hls_synth_res_estimation import hls_synth_res_estimation +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.convert_to_hw_layers import InferThresholdingLayer from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -54,27 +51,27 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds test_fpga_part = "xczu3eg-sbva484-1-e" target_clk_ns = 5 + def InsertAndSetFIFODepths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: cfg = DataflowBuildConfig() model = model.transform( - BuildSimulation( - fpga_part, - clk_ns, - True, - performance_sim=False, - ) - ) - model = model.transform( - RunLayerParallelSimulation( - fpga_part, clk_ns, cfg - ) - ) + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) model = model.transform(ApplySimulatedFIFOSizes(cfg)) return model diff --git a/tests/fpgadataflow/test_fpgadataflow_thresholding_runtime.py b/tests/fpgadataflow/test_fpgadataflow_thresholding_runtime.py index 23351fbde3..8c189ba912 100644 --- a/tests/fpgadataflow/test_fpgadataflow_thresholding_runtime.py +++ b/tests/fpgadataflow/test_fpgadataflow_thresholding_runtime.py @@ -38,6 +38,7 @@ from qonnx.transformation.general import GiveUniqueNodeNames from qonnx.util.basic import gen_finn_dt_tensor, qonnx_make_model +import finn.xsi as finnxsi from finn.core.rtlsim_exec import rtlsim_exec from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP @@ -46,8 +47,6 @@ from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -import finn.xsi as finnxsi - test_fpga_part = "xczu3eg-sbva484-1-e" target_clk_ns = 5 diff --git a/tests/fpgadataflow/test_simulation_build.py b/tests/fpgadataflow/test_simulation_build.py index 09618f8280..467cb843a3 100644 --- a/tests/fpgadataflow/test_simulation_build.py +++ b/tests/fpgadataflow/test_simulation_build.py @@ -16,9 +16,11 @@ class _SimulationBuilderProtocol(Protocol): - def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: ... + def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: + ... - def _isolated_node_model(self, by_node: int | str) -> ModelWrapper: ... + def _isolated_node_model(self, by_node: int | str) -> ModelWrapper: + ... def _import_simulation_build_types() -> tuple[type[_SimulationBuilderProtocol], type[Exception]]: diff --git a/tests/util/test_config.py b/tests/util/test_config.py index 5920096afe..8e73209949 100644 --- a/tests/util/test_config.py +++ b/tests/util/test_config.py @@ -8,18 +8,18 @@ # ############################################################################ -import os -from pathlib import Path -from typing import Any +import pytest import onnx -import pytest +import os from onnxscript import BOOL, FLOAT from onnxscript import opset13 as op from onnxscript import script from onnxscript.values import Opset +from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp +from typing import Any from finn.transformation.general import ApplyConfig from finn.util.config import extract_model_config, extract_model_config_to_json From 9ba78b569fb7cfc9b4a8b577d7755855d57827de Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 11:47:05 +0200 Subject: [PATCH 109/170] Remove custom_deps to fix install error --- external_dependencies.yaml | 2 -- src/finn/interface/manage_deps.py | 44 ------------------------------- 2 files changed, 46 deletions(-) diff --git a/external_dependencies.yaml b/external_dependencies.yaml index 84a1a2938a..e19fa7ca1d 100644 --- a/external_dependencies.yaml +++ b/external_dependencies.yaml @@ -51,5 +51,3 @@ direct_download_deps: url: "https://dpoauwgwqsy2x.cloudfront.net/Download/pynq-z2.zip" do_unzip: True target_directory: "board_files" - -custom_deps: diff --git a/src/finn/interface/manage_deps.py b/src/finn/interface/manage_deps.py index e32a17f082..f4244cd8a8 100644 --- a/src/finn/interface/manage_deps.py +++ b/src/finn/interface/manage_deps.py @@ -90,25 +90,12 @@ class DirectDownloadDependency(BaseModel, Dependency): target_directory: Path = Field(strict=False) -class CustomDependency(BaseModel, Dependency): - """Data model for a custom dependency. - - installation_function: Name of the function that should be implemented in the DependencyUpdater - to install this dependency. - outdated_function: Name of function that returns whether this dependency is outdated. - """ - - installation_function: str - outdated_function: str - - class DependencyData(BaseModel): """Data model that stores all dependencies.""" git_deps: dict[str, GitDependency] boardfile_deps: dict[str, BoardfileDependency] direct_download_deps: dict[str, DirectDownloadDependency] - custom_deps: dict[str, CustomDependency] def get_all_dependencies(self) -> list[str]: """Return a list of all packages, across dependency types.""" @@ -120,7 +107,6 @@ def get_all_dependencies(self) -> list[str]: self.git_deps, self.boardfile_deps, self.direct_download_deps, - self.custom_deps, ] ] ) @@ -148,8 +134,6 @@ def dependency_type_str(self, package_name: str) -> str: return "Boardfiles" if package_name in self.direct_download_deps: return "Data" - if package_name in self.custom_deps: - return "Custom" return "Misc" def get_dependency_data(self, package_name: str) -> Dependency | None: @@ -160,7 +144,6 @@ def get_dependency_data(self, package_name: str) -> Dependency | None: self.git_deps, self.boardfile_deps, self.direct_download_deps, - self.custom_deps, ]: if package_name in depdict: return depdict[package_name] @@ -466,22 +449,6 @@ def _install_direct_download_dependency(self, package_name: str) -> bool: return False return unzipped.exists() - def _install_custom(self, package_name: str) -> bool: - """Install the custom dependency. The function name provided by the definition file - must exist as a method of this class. If so, it is executed and it's return value - used to check for success. - """ - data = self.deps.get_dependency_data(package_name) - assert data is not None - function_name = cast("CustomDependency", data).installation_function - try: - return self.__getattribute__(function_name)() - except AttributeError as e: - raise FINNUserError( - f"Implementation for custom installation function for " - f"{package_name} not found in DependencyUpdater!" - ) from e - def install_dependency(self, package_name: str) -> bool: """Install the dependency in the dependency location. If no definition for this dependency exists or the installation failed, return False. @@ -495,8 +462,6 @@ def install_dependency(self, package_name: str) -> bool: return self._install_boardfile_dependency(package_name) if t is DirectDownloadDependency: return self._install_direct_download_dependency(package_name) - if t is CustomDependency: - return self._install_custom(package_name) return False def is_outdated(self, package_name: str, installed: bool = False) -> bool: @@ -507,15 +472,6 @@ def is_outdated(self, package_name: str, installed: bool = False) -> bool: raise FINNUserError( f"Cannot check if non-existing dependency {package_name} is outdated." ) - if package_name in self.deps.custom_deps: - function_name = cast("CustomDependency", data).outdated_function - try: - return self.__getattribute__(function_name)() - except AttributeError as e: - raise FINNUserError( - f"Custom package {package_name} is missing the implementation" - f"of the outdated check function in DependencyUpdater!" - ) from e if package_name in self.deps.direct_download_deps: # TODO: Improve (e.g. by checking directly instead of by using wget). # Check by letting wget compare timestamps. To avoid large wait times From 91ab785b8eec1c9d3938d9a5dff7cdbe874a98ec Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 12:02:25 +0200 Subject: [PATCH 110/170] Fix autobuilding of finn_xsi --- src/finn/xsi/__init__.py | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/finn/xsi/__init__.py b/src/finn/xsi/__init__.py index 5bf7f06f5b..5d039339e6 100644 --- a/src/finn/xsi/__init__.py +++ b/src/finn/xsi/__init__.py @@ -20,12 +20,13 @@ import contextlib import os +import re import sys -from pathlib import Path from typing import Any from finn.util.exception import FINNUserError from finn.util.logging import log +from finn.util.settings import get_settings # Track if auto-install has been attempted _auto_install_attempted = False @@ -34,6 +35,8 @@ _adapter_module: Any | None = None _sim_engine_module: Any | None = None +xsi_path = get_settings().finn_xsi + def is_available() -> bool: """Check if XSI (RTL simulation) support is available. @@ -42,9 +45,27 @@ def is_available() -> bool: bool: True if finn_xsi can be imported, False otherwise """ # Check if xsi.so exists - xsi_path = Path(os.environ["FINN_XSI"]) xsi_so = xsi_path / "xsi.so" - if not xsi_so.exists(): + vivado_path = os.environ.get("XILINX_VIVADO") + if vivado_path is None: + raise OSError("XILINX_VIVADO environment variable not set. Please source Vivado settings.") + match = re.search(r"\b(20\d{2})\.(1|2)\b", vivado_path) + if not match: + raise ValueError(f"Could not parse Vivado version from XILINX_VIVADO path: {vivado_path}") + year, minor = int(match.group(1)), int(match.group(2)) + + version_file = xsi_path / "VERSION" + + if not xsi_so.exists() or not version_file.exists(): + # Attempt auto-install if not yet tried + _attempt_auto_install() + # Check again after auto-install attempt + if not xsi_so.exists(): + print("XSI INSTALL: xsi.so does not exist") + return False + with version_file.open() as f: + version_info = f.read().strip() + if version_info != f"Vivado {year}.{minor}": # Attempt auto-install if not yet tried _attempt_auto_install() # Check again after auto-install attempt @@ -103,7 +124,6 @@ def _load_modules() -> bool: if _adapter_module is not None: return True - xsi_path = Path(os.environ["FINN_XSI"]) xsi_so = xsi_path / "xsi.so" if not xsi_so.exists(): From ca5abd6cc12956901097926b74fe95587c47dc96 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 12:09:34 +0200 Subject: [PATCH 111/170] Remove uses of FINN_XSI env var --- .../fpgadataflow/simulation_build.py | 5 +- src/finn/xsi/setup.py | 4 +- tests/fpgadataflow/test_simulation_build.py | 87 ++++--------------- 3 files changed, 20 insertions(+), 76 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index e5768ff05d..8a503afa89 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -35,6 +35,7 @@ from finn.util.basic import getHWCustomOp, launch_process_helper, make_build_dir from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log +from finn.util.settings import get_settings if TYPE_CHECKING: from collections.abc import Sequence @@ -621,7 +622,7 @@ def _compile_simulation(self, sim_base: Path, silent: bool = True) -> Path: return Path(sim_base) # Check where FINNXSI is - finnxsi_dir = os.environ["FINN_XSI"] + finnxsi_dir = get_settings().finn_xsi # Running CMake first cmake_call = f"{sys.executable} -m cmake -S {finnxsi_dir} -B {sim_base}" @@ -679,7 +680,7 @@ def _template_rtlsim_config( """Template finn_xsi/finn_xsi/rtlsim_config.hpp.template with the correct values and return the templated file. """ - finnxsi_dir = os.environ["FINN_XSI"] + finnxsi_dir = get_settings().finn_xsi # Prepare the C++ driver config template ( instream_descrs_str, diff --git a/src/finn/xsi/setup.py b/src/finn/xsi/setup.py index ef0f2f2744..d6f4dc9594 100644 --- a/src/finn/xsi/setup.py +++ b/src/finn/xsi/setup.py @@ -224,7 +224,7 @@ def build_xsi(force: bool = False, verbose: bool = True) -> bool: def verify_installation() -> bool: """Verify that finn_xsi can be imported and works.""" - xsi_path = Path(os.environ["FINN_XSI"]) + xsi_path = get_settings().finn_xsi # Check if xsi.so exists xsi_so = xsi_path / "xsi.so" @@ -261,7 +261,7 @@ def verify_installation() -> bool: def clean_build() -> bool: """Clean build artifacts.""" - xsi_path = Path(os.environ["FINN_XSI"]) + xsi_path = get_settings().finn_xsi print(f"Cleaning build artifacts in {xsi_path}...") diff --git a/tests/fpgadataflow/test_simulation_build.py b/tests/fpgadataflow/test_simulation_build.py index 467cb843a3..fddfdbe8a5 100644 --- a/tests/fpgadataflow/test_simulation_build.py +++ b/tests/fpgadataflow/test_simulation_build.py @@ -5,14 +5,12 @@ import pytest import numpy as np -import os -import sys -import types from onnx import GraphProto, NodeProto, TensorProto, ValueInfoProto, helper -from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.util.basic import qonnx_make_model from typing import Protocol, cast +from finn.transformation.fpgadataflow.simulation_build import SimulationBuilder +from finn.util.exception import FINNInternalError class _SimulationBuilderProtocol(Protocol): @@ -23,50 +21,6 @@ def _isolated_node_model(self, by_node: int | str) -> ModelWrapper: ... -def _import_simulation_build_types() -> tuple[type[_SimulationBuilderProtocol], type[Exception]]: - finn_xsi_stub_dir = Path("/tmp/finn_xsi_stub") - finn_xsi_stub_dir.mkdir(parents=True, exist_ok=True) - (finn_xsi_stub_dir / "xsi.so").touch(exist_ok=True) - os.environ.setdefault("FINN_XSI", str(finn_xsi_stub_dir)) - - finn_xsi_module = types.ModuleType("finn_xsi") - finn_xsi_module.__path__ = [] - finn_xsi_adapter_module = types.ModuleType("finn_xsi.adapter") - - def _get_simkernel_so() -> str: - return "" - - finn_xsi_adapter_module.__dict__["get_simkernel_so"] = _get_simkernel_so - finn_xsi_sim_engine_module = types.ModuleType("finn_xsi.sim_engine") - - class _SimEngine: - pass - - finn_xsi_sim_engine_module.__dict__["SimEngine"] = _SimEngine - finn_xsi_module.__dict__["adapter"] = finn_xsi_adapter_module - finn_xsi_module.__dict__["sim_engine"] = finn_xsi_sim_engine_module - sys.modules.setdefault("finn_xsi", finn_xsi_module) - sys.modules.setdefault("finn_xsi.adapter", finn_xsi_adapter_module) - sys.modules.setdefault("finn_xsi.sim_engine", finn_xsi_sim_engine_module) - - scipy_module = types.ModuleType("scipy") - scipy_special_module = types.ModuleType("scipy.special") - - def _softmax(x: np.ndarray, axis: int | None = None) -> np.ndarray: - exp_x = np.exp(x) - return exp_x / np.sum(exp_x, axis=axis, keepdims=True) - - scipy_special_module.__dict__["softmax"] = _softmax - scipy_module.__dict__["special"] = scipy_special_module - sys.modules.setdefault("scipy", scipy_module) - sys.modules.setdefault("scipy.special", scipy_special_module) - - from finn.transformation.fpgadataflow.simulation_build import SimulationBuilder - from finn.util.exception import FINNInternalError - - return SimulationBuilder, FINNInternalError - - def _vi(name: str, shape: list[int]) -> ValueInfoProto: return helper.make_tensor_value_info(name, TensorProto.FLOAT, shape) @@ -728,9 +682,8 @@ def test_isolated_node_model_unary_target_with_varied_other_node_inputs( pre_binary: bool, succ_binary: bool ) -> None: """Isolate unary target with unary/binary surrounding nodes.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_unary_target_model(pre_binary=pre_binary, succ_binary=succ_binary) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) isolated = _isolate_node_model(builder, 1) @@ -749,9 +702,8 @@ def test_isolated_node_model_unary_target_with_varied_other_node_inputs( def test_isolated_node_model_select_by_name() -> None: """Selecting node by name returns the correct isolated model.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_unary_target_model(pre_binary=False, succ_binary=False) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) isolated = _isolate_node_model(builder, "target_dwc") @@ -786,9 +738,8 @@ def test_isolated_node_model_binary_target_with_dynamic_and_fixed_inputs( expected_target_inputs: list[str], ) -> None: """Isolate binary target for dynamic/fixed lhs-rhs and MLO/non-MLO cases.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_binary_target_model(initializer_side=initializer_side, mlo=mlo) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) isolated = _isolate_node_model(builder, 0) @@ -810,7 +761,6 @@ def test_isolated_node_model_unary_succ_fifo_chain_transparency( fifo_between_depth: int, ) -> None: """FIFOs between target and successor are transparent for isolation checks.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_unary_target_model( pre_binary=False, succ_binary=False, @@ -818,7 +768,7 @@ def test_isolated_node_model_unary_succ_fifo_chain_transparency( fifo_between=True, fifo_between_depth=fifo_between_depth, ) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) isolated = _isolate_node_model(builder, "succ_dwc") @@ -841,7 +791,6 @@ def test_isolated_node_model_binary_succ_fifo_chain_transparency( fifo_between_depth: int, ) -> None: """Binary successor nodes see FIFO chains as transparent.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_binary_target_model( initializer_side=None, mlo=False, @@ -849,7 +798,7 @@ def test_isolated_node_model_binary_succ_fifo_chain_transparency( fifo_between=True, fifo_between_depth=fifo_between_depth, ) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) isolated = _isolate_node_model(builder, "succ_dwc") @@ -881,7 +830,6 @@ def test_isolated_node_model_binary_target_fifo_pre_transparency( expected_target_inputs: list[str], ) -> None: """FIFO chains before the target are transparent for inputs.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_binary_target_model( initializer_side=initializer_side, mlo=False, @@ -890,7 +838,7 @@ def test_isolated_node_model_binary_target_fifo_pre_transparency( fifo_between=True, fifo_between_depth=2, ) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) model.save("/scratch/pc2-mitarbeiter/linusjun/finn-tmp/source_model.onnx") @@ -974,7 +922,6 @@ def test_isolated_node_model_duplicate_stream_fifo_transparency( config: dict[str, object], ) -> None: """DuplicateStreams models behave identically with FIFO chains present.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_duplicate_target_model( fifos=bool(config["fifos"]), branch_nodes=bool(config["branch_nodes"]), @@ -983,7 +930,7 @@ def test_isolated_node_model_duplicate_stream_fifo_transparency( fifo_after=bool(config["fifo_after"]), fifo_between_depth=cast("int | None", config["fifo_between_depth"]), ) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) isolated = _isolate_node_model(builder, "dup_stream") @@ -1003,7 +950,6 @@ def test_isolated_node_model_duplicate_stream_fifo_transparency( def test_isolated_node_model_fifo_transparency_nodes() -> None: """Compare isolated node inputs/outputs between FIFO and non-FIFO topologies for all nodes.""" - simulation_builder_cls, _ = _import_simulation_build_types() model_no_fifo = _build_unary_target_model( pre_binary=True, succ_binary=False, @@ -1024,8 +970,8 @@ def test_isolated_node_model_fifo_transparency_nodes() -> None: fifo_after=True, ) - builder_no_fifo = simulation_builder_cls(model_no_fifo, "xc7z020clg400-1", 5.0) - builder_fifo = simulation_builder_cls(model_fifo, "xc7z020clg400-1", 5.0) + builder_no_fifo = SimulationBuilder(model_no_fifo, "xc7z020clg400-1", 5.0) + builder_fifo = SimulationBuilder(model_fifo, "xc7z020clg400-1", 5.0) node_names = [node.name for node in model_no_fifo.graph.node if node.op_type != "StreamingFIFO"] @@ -1043,9 +989,8 @@ def test_isolated_node_model_fifo_transparency_nodes() -> None: def test_isolated_node_model_elementwise_sets_const_style_for_mlo_initializer() -> None: """Elementwise ops set lhs_style/rhs_style=const for remapped MLO initializer inputs.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_binary_target_model(initializer_side=None, mlo=True) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) isolated = _isolate_node_model(builder, 0) target_node = next(n for n in isolated.graph.node if n.name == "target_add") @@ -1063,9 +1008,8 @@ def test_isolated_node_model_elementwise_sets_const_style_for_mlo_initializer() def test_isolated_node_model_mvau_sets_internal_decoupled_for_initializer_input() -> None: """MVAU ops set mem_mode=internal_decoupled when an input is remapped to initializer.""" - simulation_builder_cls, _ = _import_simulation_build_types() model = _build_mvau_target_model(mlo=True) - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) isolated = _isolate_node_model(builder, 0) target_node = next(n for n in isolated.graph.node if n.name == "target_mvau") @@ -1079,11 +1023,10 @@ def test_isolated_node_model_mvau_sets_internal_decoupled_for_initializer_input( def test_isolated_node_model_rejects_bad_mlo_metadata() -> None: """Reject invalid mlo_input_parameter_names metadata values.""" - simulation_builder_cls, finn_internal_error_cls = _import_simulation_build_types() model = _build_binary_target_model(initializer_side="rhs", mlo=False) model.set_metadata_prop("is_mlo", "1") model.set_metadata_prop("mlo_input_parameter_names", "42") - builder = simulation_builder_cls(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) - with pytest.raises(finn_internal_error_cls, match="mlo_input_parameter_names"): + with pytest.raises(FINNInternalError, match="mlo_input_parameter_names"): _isolate_node_model(builder, 0) From 0c15918d4ade7f0b7b6a3e99887000505b89b822 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 13:06:29 +0200 Subject: [PATCH 112/170] Add hook to initialize settings correctly in all unittests --- src/finn/core/rtlsim_exec.py | 2 +- tests/conftest.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index c5ba8e3cd7..5c6adc8f0c 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -29,13 +29,13 @@ import numpy as np from collections.abc import Callable -from finn_xsi.sim_engine import SimEngine from pathlib import Path from typing import TYPE_CHECKING from finn import xsi as finnxsi from finn.util.basic import get_liveness_threshold_cycles, getHWCustomOp, make_build_dir from finn.util.data_packing import npy_to_rtlsim_input, rtlsim_output_to_npy +from finn.xsi import SimEngine if TYPE_CHECKING: from qonnx.core.datatype import BaseDataType diff --git a/tests/conftest.py b/tests/conftest.py index aad4fcf8d0..f8063c7504 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -59,6 +59,11 @@ def load_settings(request) -> None: finn.util.settings._SETTINGS = settings # noqa +def pytest_collect_file(file_path: Path, parent) -> None: # noqa: ARG001 + """Initialize FINN settings before each test module is imported.""" + finn.util.settings.initialize_dummy_settings() + + @pytest.fixture(scope="class", autouse=True) def isolate_build_dir(request): # Retrieve settings From 260d6802c78bbde6730a4a07fe6c1e6ec02b0893 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 13:48:01 +0200 Subject: [PATCH 113/170] Fix test_bram_block_search --- tests/fpgadataflow/test_bram_block_search.py | 34 +++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/fpgadataflow/test_bram_block_search.py b/tests/fpgadataflow/test_bram_block_search.py index 2542fde769..4ffa50e2d1 100644 --- a/tests/fpgadataflow/test_bram_block_search.py +++ b/tests/fpgadataflow/test_bram_block_search.py @@ -5,7 +5,7 @@ import math -from finn.transformation.fpgadataflow.simulation import ( +from finn.transformation.fpgadataflow.simulation_connected import ( calculate_bram_blocks, calculate_bram_depth_range, ) @@ -147,7 +147,7 @@ class TestGetValidBlockCounts: def test_all_valid_bitwidth_1(self) -> None: """Test that all block counts are valid for bitwidth=1.""" - from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation # Create dummy instance just to test the method sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) @@ -157,7 +157,7 @@ def test_all_valid_bitwidth_1(self) -> None: def test_wide_bitwidth_filtering(self) -> None: """Test that some block counts may be invalid for wide bitwidths.""" - from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) @@ -170,7 +170,7 @@ def test_wide_bitwidth_filtering(self) -> None: def test_range_respects_bounds(self) -> None: """Test that valid blocks respect min/max bounds.""" - from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) @@ -181,7 +181,7 @@ def test_range_respects_bounds(self) -> None: def test_empty_when_no_valid_in_range(self) -> None: """Test that empty list is returned when no valid configs exist in range.""" - from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) @@ -237,7 +237,7 @@ class TestSRL16ELUTCalculations: def test_calculate_srl16e_luts_basic(self): """Test basic SRL16E LUT calculations.""" - from finn.transformation.fpgadataflow.simulation import calculate_srl16e_luts + from finn.transformation.fpgadataflow.simulation_connected import calculate_srl16e_luts # Formula: LUTs = ⌈depth/32⌉ * ⌈bitwidth/2⌉ # depth=32, bitwidth=2: ⌈32/32⌉ * ⌈2/2⌉ = 1 * 1 = 1 @@ -254,7 +254,7 @@ def test_calculate_srl16e_luts_basic(self): def test_calculate_srl16e_luts_various_bitwidths(self): """Test SRL16E LUT calculations for various bitwidths.""" - from finn.transformation.fpgadataflow.simulation import calculate_srl16e_luts + from finn.transformation.fpgadataflow.simulation_connected import calculate_srl16e_luts # Bitwidth 1: ⌈1/2⌉ = 1 assert calculate_srl16e_luts(32, 1) == 1 @@ -270,7 +270,7 @@ def test_calculate_srl16e_luts_various_bitwidths(self): def test_calculate_srl16e_luts_small_depths(self): """Test SRL16E LUT calculations for small depths.""" - from finn.transformation.fpgadataflow.simulation import calculate_srl16e_luts + from finn.transformation.fpgadataflow.simulation_connected import calculate_srl16e_luts # Small depths still use at least 1 LUT per bitwidth factor assert calculate_srl16e_luts(2, 2) == 1 @@ -283,7 +283,7 @@ class TestSRL16EDepthRange: def test_depth_range_basic(self): """Test basic depth range calculation for SRL16E.""" - from finn.transformation.fpgadataflow.simulation import ( + from finn.transformation.fpgadataflow.simulation_connected import ( calculate_srl16e_depth_range, calculate_srl16e_luts, ) @@ -297,7 +297,7 @@ def test_depth_range_basic(self): def test_depth_range_bitwidth_1(self): """Test depth range for 1-bit data.""" - from finn.transformation.fpgadataflow.simulation import ( + from finn.transformation.fpgadataflow.simulation_connected import ( calculate_srl16e_depth_range, calculate_srl16e_luts, ) @@ -316,7 +316,9 @@ def test_depth_range_bitwidth_1(self): def test_depth_range_invalid_odd_luts(self): """Test that odd LUT counts are invalid for certain bitwidths.""" - from finn.transformation.fpgadataflow.simulation import calculate_srl16e_depth_range + from finn.transformation.fpgadataflow.simulation_connected import ( + calculate_srl16e_depth_range, + ) # Bitwidth=4: ⌈4/2⌉ = 2, so only even LUT counts are valid _, max_d = calculate_srl16e_depth_range(1, 4) @@ -327,7 +329,7 @@ def test_depth_range_invalid_odd_luts(self): def test_depth_range_consistency(self): """Test that all valid ranges produce the correct LUT count.""" - from finn.transformation.fpgadataflow.simulation import ( + from finn.transformation.fpgadataflow.simulation_connected import ( calculate_srl16e_depth_range, calculate_srl16e_luts, ) @@ -358,7 +360,7 @@ class TestNeedsMinimization: # TODO: Maybe remove this behavior def test_small_depths_no_minimization(self): """Test that small depths don't need minimization.""" - from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) sim.max_qsrl_depth = 256 @@ -371,7 +373,7 @@ def test_small_depths_no_minimization(self): # TODO: Maybe remove this behavior def test_qsrl_range_no_minimization(self): """Test that depths within QSRL range don't need minimization.""" - from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) sim.max_qsrl_depth = 256 @@ -382,7 +384,7 @@ def test_qsrl_range_no_minimization(self): def test_large_depths_need_minimization(self): """Test that large depths with multiple BRAM blocks need minimization.""" - from finn.transformation.fpgadataflow.simulation import ( + from finn.transformation.fpgadataflow.simulation_connected import ( RunLayerParallelSimulation, calculate_bram_blocks, calculate_bram_depth_range, @@ -446,7 +448,7 @@ def test_large_depths_need_minimization(self): def test_minimum_bram_edge_case(self): """Test edge case at minimum BRAM blocks.""" - from finn.transformation.fpgadataflow.simulation import RunLayerParallelSimulation + from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) sim.max_qsrl_depth = 256 From 5e9a36887aae94b53d56a4f4ad71b17c3c10ed56 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 14:04:40 +0200 Subject: [PATCH 114/170] Fix vvau test --- tests/fpgadataflow/test_fpgadataflow_vvau.py | 231 +++++++++++++------ 1 file changed, 156 insertions(+), 75 deletions(-) diff --git a/tests/fpgadataflow/test_fpgadataflow_vvau.py b/tests/fpgadataflow/test_fpgadataflow_vvau.py index b0f8b634ba..2d0bcca8a8 100644 --- a/tests/fpgadataflow/test_fpgadataflow_vvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_vvau.py @@ -26,11 +26,14 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Tests for the VVAU dataflow custom op.""" + import pytest import numpy as np +import numpy.typing as npt from onnx import TensorProto, helper -from qonnx.core.datatype import DataType +from qonnx.core.datatype import BaseDataType, DataType from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.general.im2col import compute_conv_output_dim from qonnx.custom_op.general.multithreshold import multithreshold @@ -40,10 +43,12 @@ from qonnx.transformation.infer_shapes import InferShapes from qonnx.transformation.lower_convs_to_matmul import LowerConvsToMatMul from qonnx.util.basic import gen_finn_dt_tensor, qonnx_make_model +from typing import Any, Literal, cast import finn.core.onnx_exec as oxe import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw from finn.analysis.fpgadataflow.exp_cycles_per_layer import exp_cycles_per_layer +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.create_dataflow_partition import CreateDataflowPartition from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -54,31 +59,53 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.general import ApplyConfig -def _infer_sparse_weight_tensor(W_conv, k_h, k_w, channels): - W_sparse = np.zeros((channels, channels, k_h, k_w), dtype=np.float32) +def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + """Run FIFO sizing for testing.""" + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model + + +def _infer_sparse_weight_tensor( + w_conv: npt.NDArray[np.float32], k_h: int, k_w: int, channels: int +) -> npt.NDArray[np.float32]: + """Convert dense weights to a sparse representation for depthwise convolution.""" + w_sparse = np.zeros((channels, channels, k_h, k_w), dtype=np.float32) for ch in range(channels): - W_sparse[ch][ch] = W_conv[ch][0] - W_conv = W_sparse.astype(np.float32) - W_matmul = W_conv.transpose(0, 2, 3, 1) - W_matmul = W_matmul.reshape(channels, channels * k_h * k_w) - W_matmul = W_matmul.T + w_sparse[ch][ch] = w_conv[ch][0] + w_conv = w_sparse.astype(np.float32) + w_matmul = w_conv.transpose(0, 2, 3, 1) + w_matmul = w_matmul.reshape(channels, channels * k_h * k_w) + w_matmul = w_matmul.T - return W_matmul + return w_matmul -def _calculate_dot_prod_range(dt_a, dt_b, len): - """Returns the (min,max) values a dot product between two (un)signed vectors of - types dt_a and dt_b of len elements can take.""" - min_prod = 2**30 - max_prod = -(2**30) +def _calculate_dot_prod_range( + dt_a: BaseDataType, dt_b: BaseDataType, vec_len: int +) -> tuple[float, float]: + """Return the (min, max) values for a dot product of two vectors.""" + min_prod = float("inf") + max_prod = float("-inf") for a_val in [dt_a.min(), dt_a.max()]: for b_val in [dt_b.min(), dt_b.max()]: - prod = a_val * b_val * len + prod = a_val * b_val * vec_len if prod < min_prod: min_prod = prod if prod > max_prod: @@ -87,21 +114,22 @@ def _calculate_dot_prod_range(dt_a, dt_b, len): def _make_single_vvau_modelwrapper( - W, - pe, - simd, - k_h, - k_w, - channels, - dim_h, - dim_w, - wdt, - idt, - odt, - T=None, - tdt=None, - mem_mode="internal_embedded", -): + weights: npt.NDArray[np.float32], + pe: int, + simd: int, + k_h: int, + k_w: int, + channels: int, + dim_h: int, + dim_w: int, + wdt: BaseDataType, + idt: BaseDataType, + odt: BaseDataType, + thresholds: npt.NDArray[np.float32] | None = None, + tdt: BaseDataType | None = None, + mem_mode: str = "internal_embedded", +) -> ModelWrapper: + """Create a ModelWrapper with a single VVAU node.""" in_shape = [1, dim_h, dim_w, k_h * k_w * channels] # [N, H, W, K*K*CH] out_shape = [ 1, @@ -113,19 +141,16 @@ def _make_single_vvau_modelwrapper( inp = helper.make_tensor_value_info("inp", TensorProto.FLOAT, in_shape) outp = helper.make_tensor_value_info("outp", TensorProto.FLOAT, out_shape) - if T is not None: + if thresholds is not None: no_act = 0 node_inp_list = ["inp", "weights", "thresh"] - if odt == DataType["BIPOLAR"]: - actval = 0 - else: - actval = odt.min() + actval = 0 if odt == DataType["BIPOLAR"] else odt.min() else: no_act = 1 node_inp_list = ["inp", "weights"] actval = 0 - VVAU_node = helper.make_node( + vvau_node = helper.make_node( "VVAU", node_inp_list, ["outp"], @@ -145,7 +170,7 @@ def _make_single_vvau_modelwrapper( mem_mode=mem_mode, ) - graph = helper.make_graph(nodes=[VVAU_node], name="vvau_graph", inputs=[inp], outputs=[outp]) + graph = helper.make_graph(nodes=[vvau_node], name="vvau_graph", inputs=[inp], outputs=[outp]) model = qonnx_make_model(graph, producer_name="vvau-model") model = ModelWrapper(model) @@ -154,12 +179,13 @@ def _make_single_vvau_modelwrapper( model.set_tensor_datatype("outp", odt) model.set_tensor_datatype("weights", wdt) - model.set_initializer("weights", W) + model.set_initializer("weights", weights) model.set_tensor_shape("weights", (channels, 1, k_h, k_w)) - if T is not None: + if thresholds is not None: + assert tdt is not None model.set_tensor_datatype("thresh", tdt) - model.set_initializer("thresh", T) + model.set_initializer("thresh", thresholds) model = model.transform(InferShapes()) model = model.transform(InferDataTypes()) @@ -193,8 +219,20 @@ def _make_single_vvau_modelwrapper( @pytest.mark.slow @pytest.mark.vivado def test_fpgadataflow_vvau( - idt, wdt, act, pe, simd, dim_h, dim_w, k_h, k_w, channels, mem_mode, exec_mode -): + idt: BaseDataType, + wdt: BaseDataType, + act: BaseDataType | None, + pe: int, + simd: int, + dim_h: int, + dim_w: int, + k_h: int, + k_w: int, + channels: int, + mem_mode: Literal["internal_embedded", "internal_decoupled"], + exec_mode: Literal["cppsim", "rtlsim"], +) -> None: + """Check VVAU behavior across exec modes and memory styles.""" if dim_w == 1 and k_w != 1: pytest.skip("1D image requires 1D kernel, skipping.") @@ -205,8 +243,10 @@ def test_fpgadataflow_vvau( pytest.skip("Requirement kernel (k_h * k_w) divisable by SIMD is violated.") # Generate weights in expected shape for ONNX and HLS node - W = gen_finn_dt_tensor(wdt, (channels, 1, k_h, k_w)) # shape: [channels, 1, k, k] - W_onnx = _infer_sparse_weight_tensor(W, k_h, k_w, channels) # shape: [k*k*channels, channels] + weights = gen_finn_dt_tensor(wdt, (channels, 1, k_h, k_w)) # shape: [channels, 1, k, k] + weights_onnx = _infer_sparse_weight_tensor( + weights, k_h, k_w, channels + ) # shape: [k*k*channels, channels] # Generate inputs in expected format for ONNX and HLS node x = gen_finn_dt_tensor(idt, (1, dim_h, dim_w, k_h * k_w * channels)) @@ -215,7 +255,7 @@ def test_fpgadataflow_vvau( x_vvau = x_vvau.reshape(1, dim_h, dim_w, channels * k_h * k_w) if act is None: - T = None + thresholds = None tdt = None if wdt == DataType["BIPOLAR"] and idt == DataType["BIPOLAR"]: odt = DataType["UINT32"] @@ -224,19 +264,37 @@ def test_fpgadataflow_vvau( else: odt = act (min_v, max_v) = _calculate_dot_prod_range(idt, wdt, k_h * k_w) + min_v_int = int(min_v) + max_v_int = int(max_v) n_steps = act.get_num_possible_values() - 1 - T = np.random.randint(min_v, max_v - 1, (channels, n_steps)).astype(np.float32) - T = np.sort(T, axis=1) + rng = np.random.default_rng() + thresholds = rng.integers(min_v_int, max_v_int - 1, size=(channels, n_steps)).astype( + np.float32 + ) + thresholds = np.sort(thresholds, axis=1) if wdt == DataType["BIPOLAR"] and idt == DataType["BIPOLAR"]: tdt = DataType["UINT32"] # bias thresholds to be positive - T = np.ceil((T + (k_h * k_w)) / 2) - assert (T >= 0).all() + thresholds = np.ceil((thresholds + (k_h * k_w)) / 2) + assert (thresholds >= 0).all() else: tdt = DataType["INT32"] model = _make_single_vvau_modelwrapper( - W, pe, simd, k_h, k_w, channels, dim_h, dim_w, wdt, idt, odt, T, tdt, mem_mode + weights, + pe, + simd, + k_h, + k_w, + channels, + dim_h, + dim_w, + wdt, + idt, + odt, + thresholds, + tdt, + mem_mode, ) model = model.transform(GiveUniqueNodeNames()) model = model.transform(GiveReadableTensorNames()) @@ -262,15 +320,16 @@ def test_fpgadataflow_vvau( if wdt == DataType["BIPOLAR"] and idt == DataType["BIPOLAR"]: # Simulate XNOR-popcount matrix multiplication, see # qonnx.custom_op.general.xnorpopcount (not usable due to sparse W) - y_expected = np.matmul(x, W_onnx) + y_expected = np.matmul(x, weights_onnx) y_expected = (y_expected + (k_h * k_w)) / 2 else: - y_expected = np.matmul(x, W_onnx) # Y is in [N, H, W, C] format + y_expected = np.matmul(x, weights_onnx) # Y is in [N, H, W, C] format - if T is not None: + if thresholds is not None: + assert act is not None # Reshape Y, as multithreshold expects Y to be in [N, C, H, W] format y_expected = np.transpose(y_expected, (0, 3, 1, 2)) - y_expected = multithreshold(y_expected, T) + y_expected = multithreshold(y_expected, thresholds) y_expected = np.transpose(y_expected, (0, 2, 3, 1)) if act == DataType["BIPOLAR"]: # binary to bipolar @@ -287,7 +346,7 @@ def test_fpgadataflow_vvau( if exec_mode == "rtlsim": node = model.get_nodes_by_op_type("VVAU_hls")[0] inst = getCustomOp(node) - cycles_rtlsim = inst.get_nodeattr("cycles_rtlsim") + cycles_rtlsim = cast("int", inst.get_nodeattr("cycles_rtlsim")) exp_cycles_dict = model.analysis(exp_cycles_per_layer) exp_cycles = exp_cycles_dict[node.name] assert np.isclose(exp_cycles, cycles_rtlsim, atol=10, rtol=1.1) @@ -295,7 +354,7 @@ def test_fpgadataflow_vvau( # if rtlsim and internal_decoupled mode is selected, also run stitched IP rtlsim if mem_mode == "internal_decoupled": - model = model.transform(InsertAndSetFIFODepths("xczu7ev-ffvc1156-2-e", 5)) + model = insert_and_set_fifo_depths(model, "xczu7ev-ffvc1156-2-e", 5) model = model.transform(PrepareIP("xczu7ev-ffvc1156-2-e", 5)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP("xczu7ev-ffvc1156-2-e", 5)) @@ -308,8 +367,11 @@ def test_fpgadataflow_vvau( ).all(), "Output of ONNX model not matching output of stitched-IP RTL model!" -def make_single_dw_conv_modelwrapper(conv_config, idt, wdt): - kernel_size, in_feature_dim, in_chn = conv_config +def make_single_dw_conv_modelwrapper( + conv_params: tuple[int, int, int], idt: BaseDataType, wdt: BaseDataType +) -> ModelWrapper: + """Create a depthwise convolution model for VVAU tests.""" + kernel_size, in_feature_dim, in_chn = conv_params stride = 1 pad = 0 @@ -320,12 +382,12 @@ def make_single_dw_conv_modelwrapper(conv_config, idt, wdt): input_shape = [1, in_chn, in_feature_dim, in_feature_dim] output_shape = [1, out_chn, out_feature_dim, out_feature_dim] - conv_config = {} - conv_config["dilations"] = [1, 1] - conv_config["group"] = group - conv_config["kernel_shape"] = [kernel_size, kernel_size] - conv_config["pads"] = [pad, pad, pad, pad] - conv_config["strides"] = [stride, stride] + conv_attrs: dict[str, int | list[int]] = {} + conv_attrs["dilations"] = [1, 1] + conv_attrs["group"] = group + conv_attrs["kernel_shape"] = [kernel_size, kernel_size] + conv_attrs["pads"] = [pad, pad, pad, pad] + conv_attrs["strides"] = [stride, stride] ifm = helper.make_tensor_value_info("ifm", TensorProto.FLOAT, input_shape) ofm = helper.make_tensor_value_info("ofm", TensorProto.FLOAT, output_shape) @@ -337,7 +399,11 @@ def make_single_dw_conv_modelwrapper(conv_config, idt, wdt): inputs=[ifm], outputs=[ofm], value_info=weights, - nodes=[helper.make_node("Conv", ["ifm", "weights"], ["ofm"], **conv_config)], + nodes=[ + helper.make_node( + "Conv", ["ifm", "weights"], ["ofm"], **cast("dict[str, Any]", conv_attrs) + ) + ], ) ) @@ -352,7 +418,8 @@ def make_single_dw_conv_modelwrapper(conv_config, idt, wdt): return model -def prepare_inputs(input_tensor): +def prepare_inputs(input_tensor: npt.NDArray[np.generic]) -> dict[str, npt.NDArray[np.generic]]: + """Prepare the input dictionary for ONNX execution.""" return {"global_in": input_tensor} @@ -375,7 +442,17 @@ def prepare_inputs(input_tensor): @pytest.mark.fpgadataflow @pytest.mark.slow @pytest.mark.vivado -def test_fpgadataflow_vvau_rtl(kernel_size, in_feature_dim, in_chn, idt, wdt, part, pe, simd): +def test_fpgadataflow_vvau_rtl( + kernel_size: Literal[3], + in_feature_dim: Literal[5], + in_chn: Literal[4], + idt: BaseDataType, + wdt: BaseDataType, + part: Literal["xcvm1802-vsvd1760-2MP-e-S"], + pe: Literal[1, 2, 4], + simd: Literal[1, 3, 9], +) -> None: + """Verify VVAU depthwise convolution in cppsim and rtlsim modes.""" # Create depthwise-separable convolution conv_config = (kernel_size, in_feature_dim, in_chn) model = make_single_dw_conv_modelwrapper(conv_config, idt, wdt) @@ -383,9 +460,9 @@ def test_fpgadataflow_vvau_rtl(kernel_size, in_feature_dim, in_chn, idt, wdt, pa model = model.transform(GiveReadableTensorNames()) # Obtain golden reference output - golden_in = gen_finn_dt_tensor( - model.get_tensor_datatype("global_in"), model.get_tensor_shape("global_in") - ) + shape = model.get_tensor_shape("global_in") + assert shape is not None + golden_in = gen_finn_dt_tensor(model.get_tensor_datatype("global_in"), shape) input_dict = prepare_inputs(golden_in) golden_out = oxe.execute_onnx(model, input_dict, return_full_exec_context=True)["global_out"] @@ -451,12 +528,15 @@ def test_fpgadataflow_vvau_rtl(kernel_size, in_feature_dim, in_chn, idt, wdt, pa # Stitched-IP RTLsim model = model.transform(CreateDataflowPartition()) - partition_model_path = getCustomOp( - model.get_nodes_by_op_type("StreamingDataflowPartition")[0] - ).get_nodeattr("model") + partition_model_path = cast( + "str", + getCustomOp(model.get_nodes_by_op_type("StreamingDataflowPartition")[0]).get_nodeattr( + "model" + ), + ) partitioned_model = ModelWrapper(partition_model_path) # FIFOs needed for stitched-ip RTLsim, DWC needed for VVU operating on SIMD parallelism - partitioned_model = partitioned_model.transform(InsertAndSetFIFODepths(part, 5)) + partitioned_model = insert_and_set_fifo_depths(partitioned_model, part, 5) partitioned_model = partitioned_model.transform(PrepareIP(part, 5)) partitioned_model = partitioned_model.transform(HLSSynthIP()) partitioned_model = partitioned_model.transform(CreateStitchedIP(part, 5)) @@ -467,6 +547,7 @@ def test_fpgadataflow_vvau_rtl(kernel_size, in_feature_dim, in_chn, idt, wdt, pa output_vvau_stitched = oxe.execute_onnx( partitioned_model, input_dict, return_full_exec_context=True )["global_out"] + assert output_vvau_stitched is not None # tranpose hardware-generated outputs NHWC -> NCHW to be comparable output_vvau_stitched = output_vvau_stitched.transpose(0, 3, 1, 2) From 3bc2be3b978e04538b48f8bb94259863fe8bb1e3 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 14:23:27 +0200 Subject: [PATCH 115/170] Fix elementwise bin and requant tests --- .../test_fpgadataflow_elementwise_binary.py | 23 ++++++++++- .../fpgadataflow/test_fpgadataflow_requant.py | 40 +++++++++++++++---- 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/tests/fpgadataflow/test_fpgadataflow_elementwise_binary.py b/tests/fpgadataflow/test_fpgadataflow_elementwise_binary.py index 6c6690474e..f33a55d89e 100644 --- a/tests/fpgadataflow/test_fpgadataflow_elementwise_binary.py +++ b/tests/fpgadataflow/test_fpgadataflow_elementwise_binary.py @@ -39,6 +39,7 @@ from qonnx.transformation.infer_shapes import InferShapes from qonnx.util.basic import gen_finn_dt_tensor, qonnx_make_model +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.core.onnx_exec import execute_onnx from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.convert_to_hw_layers import InferElementwiseBinaryOperation @@ -50,7 +51,9 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers # Mapping of ElementwiseBinaryOperation specializations to numpy reference @@ -78,6 +81,22 @@ } +def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + """Run FIFO sizing for testing.""" + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model + + # Creates a model executing a binary elementwise operation def create_elementwise_binary_operation_onnx( op_type, lhs_dtype, rhs_dtype, out_dtype, lhs_shape, rhs_shape @@ -348,7 +367,7 @@ def test_elementwise_binary_operation_stitched_ip( assert np.all(o_produced == o_expected) # prepare for stitched ip rtlsim - model = model.transform(InsertAndSetFIFODepths("xczu7ev-ffvc1156-2-e", 10)) + model = insert_and_set_fifo_depths(model, "xczu7ev-ffvc1156-2-e", 10) model = model.transform(PrepareIP("xczu7ev-ffvc1156-2-e", 10)) model = model.transform(HLSSynthIP()) model = model.transform( diff --git a/tests/fpgadataflow/test_fpgadataflow_requant.py b/tests/fpgadataflow/test_fpgadataflow_requant.py index 0f75c8c758..67ab8b116f 100644 --- a/tests/fpgadataflow/test_fpgadataflow_requant.py +++ b/tests/fpgadataflow/test_fpgadataflow_requant.py @@ -3,8 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause -""" -Test cases for InferRequantLayer transformation which converts MultiThreshold +"""Test cases for InferRequantLayer transformation which converts MultiThreshold or Quant nodes to Requant nodes. The requant operation computes output as: @@ -31,10 +30,12 @@ from qonnx.transformation.infer_shapes import InferShapes from qonnx.util.basic import gen_finn_dt_tensor from qonnx.util.cleanup import cleanup as qonnx_cleanup +from typing import Literal import finn.core.onnx_exec as oxe import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw from finn.analysis.fpgadataflow.exp_cycles_per_layer import exp_cycles_per_layer +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.convert_to_hw_layers import InferRequantLayer from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -43,7 +44,9 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.qonnx.convert_qonnx_to_finn import ConvertQONNXtoFINN from finn.transformation.qonnx.quant_act_to_multithreshold import default_filter_function_generator @@ -54,6 +57,22 @@ target_clk_ns = 10 +def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + """Run FIFO sizing for testing.""" + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model + + def create_requant_model(abits, max_val, ishape, per_channel): """Create a model with QuantReLU that will be converted to Requant.""" num_channels = ishape[1] @@ -203,7 +222,7 @@ def test_requant_rtl(abits, ishape, per_channel, part, pe, exec_mode): model = model.transform(to_hw.InferElementwiseBinaryOperation()) model = model.transform(SpecializeLayers(part)) model = model.transform(GiveUniqueNodeNames()) - model = model.transform(InsertAndSetFIFODepths(part, target_clk_ns)) + model = insert_and_set_fifo_depths(model, part, target_clk_ns) model = model.transform(PrepareIP(part, target_clk_ns)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP(part, target_clk_ns)) @@ -229,7 +248,14 @@ def test_requant_rtl(abits, ishape, per_channel, part, pe, exec_mode): @pytest.mark.fpgadataflow @pytest.mark.slow @pytest.mark.vivado -def test_requant_hls(abits, ishape, per_channel, input_dtype, pe, exec_mode): +def test_requant_hls( + abits: Literal[8, 16], + ishape: tuple, + per_channel: bool, + input_dtype: Literal["FLOAT32", "INT8"], + pe: Literal[1, 16], + exec_mode: Literal["cppsim", "rtlsim"], +) -> None: """Test Requant HLS backend. Tests float input (naturally uses HLS) and integer input with forced HLS. @@ -321,7 +347,7 @@ def test_requant_hls(abits, ishape, per_channel, input_dtype, pe, exec_mode): model = model.transform(to_hw.InferElementwiseBinaryOperation()) model = model.transform(SpecializeLayers(test_fpga_part)) model = model.transform(GiveUniqueNodeNames()) - model = model.transform(InsertAndSetFIFODepths(test_fpga_part, target_clk_ns)) + model = insert_and_set_fifo_depths(model, test_fpga_part, target_clk_ns) model = model.transform(PrepareIP(test_fpga_part, target_clk_ns)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP(test_fpga_part, target_clk_ns)) @@ -340,7 +366,7 @@ def test_requant_hls(abits, ishape, per_channel, input_dtype, pe, exec_mode): def make_quant_test_model( ishp, channelwise, bitwidth, need_extraction_scale, need_extraction_zeropt -): +) -> ModelWrapper: """Create a test model with a Quant node.""" ishp_str = str(list(ishp)) if channelwise: From 488eb76b7a898acd718215d9b6b9b22247498ee3 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 14:29:37 +0200 Subject: [PATCH 116/170] Fix layernorm test and ooc synthesis test --- tests/end2end/test_ooc_synthesis.py | 73 +++++++++++++------ .../test_fpgadataflow_layernorm.py | 25 ++++++- 2 files changed, 73 insertions(+), 25 deletions(-) diff --git a/tests/end2end/test_ooc_synthesis.py b/tests/end2end/test_ooc_synthesis.py index aacb1d8866..419b32110a 100644 --- a/tests/end2end/test_ooc_synthesis.py +++ b/tests/end2end/test_ooc_synthesis.py @@ -6,11 +6,14 @@ # # ########################################################################## +"""End-to-end test for out-of-context synthesis.""" + import pytest import numpy as np +import numpy.typing as npt from onnx import TensorProto, helper -from qonnx.core.datatype import DataType +from qonnx.core.datatype import BaseDataType, DataType from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.general import GiveUniqueNodeNames from qonnx.transformation.infer_datatypes import InferDataTypes @@ -19,12 +22,15 @@ import finn.core.onnx_exec as oxe import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.fpgadataflow.synth_ooc import SynthOutOfContext @@ -32,22 +38,44 @@ clk_ns = 10 -def generate_random_threshold_values(data_type, num_input_channels, num_steps): +def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + """Run FIFO sizing for testing.""" + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model + + +def generate_random_threshold_values( + data_type: BaseDataType, num_input_channels: int, num_steps: int +) -> npt.NDArray[np.floating]: + """Generate random threshold values for a given datatype.""" + rng = np.random.default_rng() if data_type.is_integer(): - return np.random.randint( - data_type.min(), - data_type.max() + 1, - (num_input_channels, num_steps), + low = int(data_type.min()) + high = int(data_type.max()) + 1 + return rng.integers( + low, + high, + size=(num_input_channels, num_steps), ).astype(np.float32) - else: - return (np.random.randn(num_input_channels, num_steps) * 1000).astype( - data_type.to_numpy_dt() - ) + return (rng.standard_normal(size=(num_input_channels, num_steps)) * 1000).astype( + data_type.to_numpy_dt() + ) -def create_test_model(): - W = gen_finn_dt_tensor(DataType["INT4"], (16, 32)) - T = np.sort( +def create_test_model() -> ModelWrapper: + """Create a small model used for OOC synthesis testing.""" + weights = gen_finn_dt_tensor(DataType["INT4"], (16, 32)) + thresholds = np.sort( generate_random_threshold_values( DataType["FLOAT32"], 1, @@ -55,8 +83,8 @@ def create_test_model(): ), axis=1, ) - MulParam = gen_finn_dt_tensor(DataType["FLOAT32"], [1]) - AddParam = gen_finn_dt_tensor(DataType["FLOAT32"], [1, 4, 32]) + mul_param = gen_finn_dt_tensor(DataType["FLOAT32"], [1]) + add_param = gen_finn_dt_tensor(DataType["FLOAT32"], [1, 4, 32]) # Initialize a new graph nodes = [] @@ -121,10 +149,10 @@ def create_test_model(): model = ModelWrapper(model) # Set initializers and datatypes - model.set_initializer("matmul_weight", W) - model.set_initializer("thresh", T) - model.set_initializer("scalar_input", MulParam) - model.set_initializer("channelwise_bias", AddParam) + model.set_initializer("matmul_weight", weights) + model.set_initializer("thresh", thresholds) + model.set_initializer("scalar_input", mul_param) + model.set_initializer("channelwise_bias", add_param) model.set_tensor_datatype("inp", DataType["FLOAT32"]) model.set_tensor_datatype("matmul_weight", DataType["INT4"]) @@ -138,7 +166,8 @@ def create_test_model(): @pytest.mark.end2end @pytest.mark.vivado @pytest.mark.slow -def test_ooc_synthesis(): +def test_ooc_synthesis() -> None: + """Run OOC synthesis flow and validate expected outputs and reports.""" model = create_test_model() model = model.transform(InferShapes()) model = model.transform(InferDataTypes()) @@ -166,7 +195,7 @@ def test_ooc_synthesis(): assert (y_prod == y_ref).all() # FIFO sizing - model = model.transform(InsertAndSetFIFODepths(fpga_part, clk_ns)) + model = insert_and_set_fifo_depths(model, fpga_part, clk_ns) # stitched IP rtlsim model = model.transform(PrepareIP(fpga_part, clk_ns)) diff --git a/tests/fpgadataflow/test_fpgadataflow_layernorm.py b/tests/fpgadataflow/test_fpgadataflow_layernorm.py index 85fee2b51b..9c5def4f8d 100644 --- a/tests/fpgadataflow/test_fpgadataflow_layernorm.py +++ b/tests/fpgadataflow/test_fpgadataflow_layernorm.py @@ -26,6 +26,7 @@ import finn.core.onnx_exec as oxe import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw from finn.analysis.fpgadataflow.exp_cycles_per_layer import exp_cycles_per_layer +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP @@ -34,7 +35,9 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.streamline.extract_norm_scale_bias import ExtractNormScaleBias @@ -42,6 +45,22 @@ target_clk_ns = 5 +def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + """Run FIFO sizing for testing.""" + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model + + def create_layernorm_model(idt, ishape, has_scale, has_bias, epsilon): scale_bias_shape = [ishape[-1]] inp = helper.make_tensor_value_info("inp", TensorProto.FLOAT, ishape) @@ -138,7 +157,7 @@ def test_fpgadataflow_rtl_layernorm(idt, ishape, simd, sim_style): elif sim_style == "stitched_ip": # Set debug waveform for stitched IP - model = model.transform(InsertAndSetFIFODepths(test_fpga_part, target_clk_ns)) + model = insert_and_set_fifo_depths(model, test_fpga_part, target_clk_ns) model = model.transform(PrepareIP(test_fpga_part, target_clk_ns)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP(test_fpga_part, target_clk_ns)) @@ -272,7 +291,7 @@ def test_fpgadataflow_hls_layernorm(idt, ishape, simd, sim_style): model = model.transform(HLSSynthIP()) model = model.transform(PrepareRTLSim()) elif sim_style == "stitched_ip": - model = model.transform(InsertAndSetFIFODepths(test_fpga_part, target_clk_ns)) + model = insert_and_set_fifo_depths(model, test_fpga_part, target_clk_ns) model = model.transform(PrepareIP(test_fpga_part, target_clk_ns)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP(test_fpga_part, target_clk_ns)) From 261f06b0adcbcd7b3d52f45a9f73a62d89e15742 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 14:36:09 +0200 Subject: [PATCH 117/170] Fix end to end tests --- tests/end2end/test_end2end_bnn_pynq.py | 31 +++++++++++----- tests/end2end/test_end2end_mobilenet_v1.py | 43 +++++++++++----------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/tests/end2end/test_end2end_bnn_pynq.py b/tests/end2end/test_end2end_bnn_pynq.py index 0e07a211e3..4ad4060f5c 100644 --- a/tests/end2end/test_end2end_bnn_pynq.py +++ b/tests/end2end/test_end2end_bnn_pynq.py @@ -33,6 +33,9 @@ import itertools import numpy as np +from finn.builder.build_dataflow_config import DataflowBuildConfig +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.util.exception import FINNSynthesisError from finn.util.logging import log @@ -83,7 +86,7 @@ from finn.transformation.fpgadataflow.prepare_cppsim import PrepareCppSim from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.move_reshape import RemoveCNVtoFCFlatten from finn.transformation.qonnx.convert_qonnx_to_finn import ConvertQONNXtoFINN @@ -106,6 +109,22 @@ rtlsim_trace = False +def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + """Run FIFO sizing for testing.""" + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model + + def get_checkpoint_name(board, topology, wbits, abits, step): """Generate checkpoint filename for a specific build step.""" build_dir = os.environ["FINN_BUILD_DIR"] @@ -747,15 +766,7 @@ def test_set_fifo_depths(self, topology, wbits, abits, board): prev_chkpt_name = get_checkpoint_name(board, topology, wbits, abits, "ipgen") model = load_test_checkpoint_or_skip(prev_chkpt_name) test_fpga_part = get_build_env(board, target_clk_ns)["part"] - if topology == "cnv" and abits == 2 and board == "Pynq-Z1": - # Enabling swg_exception for these test cases. Disabling the exception results in - # a design that exceeds the resources of the Pynq-Z1 board. In future this should be - # revisited and handled correctly as the swg_exception is poorly justified. - model = model.transform( - InsertAndSetFIFODepths(test_fpga_part, target_clk_ns, swg_exception=True) - ) - else: - model = model.transform(InsertAndSetFIFODepths(test_fpga_part, target_clk_ns)) + model = insert_and_set_fifo_depths(model, test_fpga_part, target_clk_ns) fifo_layers = model.get_nodes_by_op_type("StreamingFIFO_rtl") assert len(fifo_layers) > 0 diff --git a/tests/end2end/test_end2end_mobilenet_v1.py b/tests/end2end/test_end2end_mobilenet_v1.py index 663dd73138..98a01785cf 100644 --- a/tests/end2end/test_end2end_mobilenet_v1.py +++ b/tests/end2end/test_end2end_mobilenet_v1.py @@ -59,6 +59,7 @@ import finn.transformation.streamline.absorb as absorb import finn.transformation.streamline.reorder as reorder from finn.analysis.fpgadataflow.dataflow_performance import dataflow_performance +from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.core.onnx_exec import execute_onnx from finn.core.throughput_test import throughput_test_rtlsim from finn.transformation.fpgadataflow.annotate_cycles import AnnotateCycles @@ -72,11 +73,9 @@ from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import ( - InsertAndSetFIFODepths, - RemoveShallowFIFOs, - SplitLargeFIFOs, -) +from finn.transformation.fpgadataflow.set_fifo_depths import ApplySimulatedFIFOSizes +from finn.transformation.fpgadataflow.simulation_build import BuildSimulation +from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.transformation.qonnx.convert_qonnx_to_finn import ConvertQONNXtoFINN from finn.transformation.streamline import Streamline @@ -96,6 +95,22 @@ extra_fold = 1 +def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: + """Run FIFO sizing for testing.""" + cfg = DataflowBuildConfig() + model = model.transform( + BuildSimulation( + fpga_part, + clk_ns, + True, + performance_sim=False, + ) + ) + model = model.transform(RunLayerParallelSimulation(fpga_part, clk_ns, cfg)) + model = model.transform(ApplySimulatedFIFOSizes(cfg)) + return model + + def get_bld_dir(): return os.environ["FINN_BUILD_DIR"] @@ -429,23 +444,7 @@ def test_end2end_mobilenet_rtlsim(self): @pytest.mark.vivado def test_end2end_mobilenet_set_fifo_depths(self): model = load_test_checkpoint_or_skip(get_bld_dir() + "/end2end_mobilenet_hw_ipgen.onnx") - model = model.transform( - InsertAndSetFIFODepths( - fpga_part, - target_clk_ns, - swg_exception=False, - vivado_ram_style="auto", - ) - ) - # perform FIFO splitting and shallow FIFO removal only after the final config - # json file has been written. otherwise, since these transforms may add/remove - # FIFOs, we get name mismatch problems when trying to reuse the final config. - model = model.transform(SplitLargeFIFOs()) - model = model.transform(RemoveShallowFIFOs()) - # after FIFOs are ready to go, call PrepareIP and HLSSynthIP again - # this will only run for the new nodes (e.g. FIFOs and DWCs) - model = model.transform(PrepareIP(fpga_part, target_clk_ns)) - model = model.transform(HLSSynthIP()) + model = insert_and_set_fifo_depths(model, fpga_part, target_clk_ns) model.save(get_bld_dir() + "/end2end_mobilenet_set_fifo_depths.onnx") @pytest.mark.slow From fdfebc727736411b677c096bed024ab3812cb7fb Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 14:40:14 +0200 Subject: [PATCH 118/170] Fix imports for FINNLoop --- .../test_fpgadataflow_finnloop.py | 249 +----------------- 1 file changed, 2 insertions(+), 247 deletions(-) diff --git a/tests/fpgadataflow/test_fpgadataflow_finnloop.py b/tests/fpgadataflow/test_fpgadataflow_finnloop.py index 2970b39849..36839507bb 100644 --- a/tests/fpgadataflow/test_fpgadataflow_finnloop.py +++ b/tests/fpgadataflow/test_fpgadataflow_finnloop.py @@ -7,45 +7,19 @@ from onnx import TensorProto, helper from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper -from qonnx.custom_op.registry import getCustomOp -from qonnx.transformation.general import ( - GiveReadableTensorNames, - GiveUniqueNodeNames, - RemoveUnusedTensors, -) +from qonnx.transformation.general import RemoveUnusedTensors from qonnx.transformation.infer_datatypes import InferDataTypes from qonnx.transformation.infer_shapes import InferShapes from qonnx.transformation.merge_onnx_models import MergeONNXModels -from qonnx.util.basic import gen_finn_dt_tensor, get_by_name, qonnx_make_model +from qonnx.util.basic import gen_finn_dt_tensor, qonnx_make_model import finn.builder.build_dataflow as build import finn.builder.build_dataflow_config as build_cfg import finn.core.onnx_exec as oxe -from finn.core.rtlsim_exec import rtlsim_exec from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim -from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP -from finn.transformation.fpgadataflow.derive_characteristic import ( - DeriveCharacteristic, - DeriveFIFOSizes, -) -from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP -from finn.transformation.fpgadataflow.insert_dwc import InsertDWC -from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO -from finn.transformation.fpgadataflow.loop_rolling import LoopExtraction, LoopRolling from finn.transformation.fpgadataflow.prepare_cppsim import PrepareCppSim -from finn.transformation.fpgadataflow.prepare_ip import PrepareIP -from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim -from finn.transformation.fpgadataflow.replace_verilog_relpaths import ReplaceVerilogRelPaths from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode -from finn.transformation.fpgadataflow.set_fifo_depths import ( - InsertAndSetFIFODepths, - RemoveShallowFIFOs, - SplitLargeFIFOs, -) -from finn.transformation.fpgadataflow.set_loop_boundary import SetLoopBoundary -from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers from finn.util.basic import make_build_dir -from finn.util.mlo_sim import mlo_prehook_func_factory verif_steps = [ "folded_hls_cppsim", @@ -619,222 +593,3 @@ def test_finnloop_end2end_mlo( assert os.path.isfile( tmp_output_dir + "/stitched_ip/finn_design.dcp" ), f"Check vivado.log in {tmp_output_dir}/stitched_ip" - - -# Debug test for manual loop transformation steps below -# This test is intentionally not marked for CI -# Use test_finnloop_end2end_mlo instead -# If required, to run manually: -# pytest tests/fpgadataflow/test_fpgadataflow_finnloop.py::test_fpgadataflow_finnloop - - -# helper functions -def prepare_loop_ops_for_ipgen_step1(node, fpga_part, clk_ns): - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") - # go first into subgraph to check if there are other loop ops - loop_nodes = loop_model.get_nodes_by_op_type("FINNLoop") - for loop_node in loop_nodes: - prepare_loop_ops_for_ipgen_step1(loop_node, fpga_part, clk_ns) - loop_model = loop_model.transform(PrepareIP(fpga_part, clk_ns)) - loop_model = loop_model.transform(HLSSynthIP(fpgapart=fpga_part)) - loop_model = loop_model.transform(ReplaceVerilogRelPaths()) - loop_model = loop_model.transform(GiveUniqueNodeNames()) - loop_model = loop_model.transform(GiveReadableTensorNames()) - if node_inst.get_nodeattr("rtlsim_trace"): - loop_model.set_metadata_prop("rtlsim_trace", f"{node.name}_fifosim_trace.wdb") - loop_model = loop_model.transform( - InsertAndSetFIFODepths( - fpga_part, - clk_ns, - ) - ) - loop_model = loop_model.transform(SplitLargeFIFOs()) - loop_model = loop_model.transform(RemoveShallowFIFOs()) - node_inst.set_nodeattr("body", loop_model.graph) - - -def prepare_loop_ops_for_ipgen_step2(node, fpga_part, clk_ns): - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") - # go first into subgraph to check if there are other loop ops - loop_nodes = loop_model.get_nodes_by_op_type("FINNLoop") - for loop_node in loop_nodes: - prepare_loop_ops_for_ipgen_step2(loop_node, fpga_part, clk_ns) - loop_model = loop_model.transform(HLSSynthIP(fpgapart=fpga_part)) - loop_model = loop_model.transform( - CreateStitchedIP( - fpga_part, - clk_ns, - ) - ) - node_inst.set_nodeattr("body", loop_model.graph) - - -# dimensions -@pytest.mark.parametrize("dim", [16]) -# iteration count, number of models chained together -@pytest.mark.parametrize("iteration", [3]) -# elementwise operation -@pytest.mark.parametrize("elemwise_optype", ["ElementwiseMul_hls", "ElementwiseAdd_hls"]) -# elementwise shape -@pytest.mark.parametrize("rhs_shape", [[1], [16]]) -# eltwise param dtype -@pytest.mark.parametrize("eltw_param_dtype", ["INT8", "FLOAT32"]) -# tail node -@pytest.mark.parametrize("tail_node", [False, True]) -@pytest.mark.fpgadataflow -@pytest.mark.vivado -@pytest.mark.slow -@pytest.mark.skip(reason="Intended only for manual debugging") -def test_fpgadataflow_finnloop_manual( - dim, iteration, elemwise_optype, rhs_shape, eltw_param_dtype, tail_node -): - """Manual step-by-step test for FINNLoop transformations. - - This test manually applies each transformation step for debugging purposes. - For automated CI testing, use test_finnloop_end2end_mlo instead, which uses - the build system and represents the actual end-to-end workflow. - """ - # Check vivado version - vivado_path = os.environ.get("XILINX_VIVADO") - match = re.search(r"\b(20\d{2})\.(1|2)\b", vivado_path) - year, minor = int(match.group(1)), int(match.group(2)) - if (year, minor) < (2024, 2): - pytest.skip("""At least Vivado version 2024.2 needed for MLO.""") - loop_body_models = create_chained_loop_bodies( - dim, dim, iteration, elemwise_optype, rhs_shape, eltw_param_dtype - ) - nodes_per_body = len(loop_body_models[0].graph.node) - model = loop_body_models[0] - for m in loop_body_models[1:]: - model = model.transform(MergeONNXModels(m)) - - if tail_node: - tail_outp = create_tensor_info("tail_outp", [1, 3, 3, dim]) - tr_node = create_node( - "ElementwiseAdd_hls", - [model.graph.output[0].name, "tail_add"], - ["tail_outp"], - "Add_tail", - { - "lhs_shape": [1, 3, 3, dim], - "rhs_shape": [1], - "out_shape": [1, 3, 3, dim], - "lhs_dtype": "INT8", - "rhs_dtype": "INT8", - "out_dtype": "INT9", - }, - ) - model.graph.node.insert(len(model.graph.node), tr_node) - model.graph.value_info.append(model.graph.output[0]) - model.graph.output.pop(0) - model.graph.output.append(tail_outp) - AddtailParam = gen_finn_dt_tensor(DataType["INT8"], [1]) - model.set_initializer("tail_add", AddtailParam) - model.set_tensor_datatype("tail_add", DataType["INT8"]) - - # cleanup - model = model.transform(RemoveUnusedTensors()) - model = model.transform(InferShapes()) - model = model.transform(InferDataTypes()) - - # Generate reference output - x = gen_finn_dt_tensor(DataType["INT8"], (1, 3, 3, dim)) - model = model.transform(PrepareCppSim()) - model = model.transform(CompileCppSim()) - model = model.transform(SetExecMode("cppsim")) - io_dict = {model.graph.input[0].name: x} - y_dict = oxe.execute_onnx(model, io_dict) - y_ref = y_dict[model.graph.output[0].name] - - # set loop boundary - node_metadata = { - "pkg.torch.onnx.name_scopes": "['', 'layers.0']", - "pkg.torch.onnx.class_hierarchy": "['TestModule', 'test']", - } - node_range = (model.graph.node[0], model.graph.node[nodes_per_body - 1]) - model = model.transform(SetLoopBoundary(node_metadata, node_range)) - - # loop extraction and rolling - loop_extraction = LoopExtraction(hierarchy_list=[["", "layers.0"]]) - model = model.transform(loop_extraction) - - assert ( - len(model.get_nodes_by_op_type("fn_loop-body")) == iteration - ), "Loop extraction did not find expected number of loop bodies" - - model = model.transform(LoopRolling(loop_extraction.loop_body_template)) - - # LoopRolling automatically adapts operator attributes for loop context - # (e.g., rhs_style changes from "const" to "input" for streamed parameters) - # This requires recompilation of the elementwise node for cppsim - loop_node = model.get_nodes_by_op_type("FINNLoop")[0] - loop_body_graph = get_by_name(loop_node.attribute, "body").g - elementwise_node = get_by_name(loop_body_graph.node, elemwise_optype, "op_type") - code_gen_dir_cppsim_attr = get_by_name(elementwise_node.attribute, "code_gen_dir_cppsim") - code_gen_dir_cppsim_attr.s = b"" # reset cpp gen directory to force recompilation - executable_path_attr = get_by_name(elementwise_node.attribute, "executable_path") - executable_path_attr.s = b"" # reset cpp exec directory to force recompilation - - # recompile elementwise node for cppsim - model = model.transform(PrepareCppSim(), apply_to_subgraphs=True) - model = model.transform(CompileCppSim(), apply_to_subgraphs=True) - - y_dict = oxe.execute_onnx(model, io_dict) - y_prod = y_dict[model.graph.output[0].name] - assert (y_prod == y_ref).all() - - # node-by-node rtlsim - model = model.transform(GiveUniqueNodeNames(), apply_to_subgraphs=True) - # TODO: allow for node-by-node rtlsim of a finn loop op - model = model.transform(SetExecMode("rtlsim")) - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for node in loop_nodes: - prepare_loop_ops_for_ipgen_step1(node, fpga_part, clk_ns) - model = model.transform(GiveUniqueNodeNames()) - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for loop_node in loop_nodes: - loop_inst = getCustomOp(loop_node) - loop_body = loop_inst.get_nodeattr("body") - loop_body = loop_body.transform(GiveUniqueNodeNames(prefix=loop_node.name + "_")) - loop_inst.set_nodeattr("body", loop_body.graph) - model = model.transform( - PrepareIP(fpga_part, clk_ns), apply_to_subgraphs=True, use_preorder_traversal=False - ) - loop_nodes = model.get_nodes_by_op_type("FINNLoop") - for node in loop_nodes: - prepare_loop_ops_for_ipgen_step2(node, fpga_part, clk_ns) - - model = model.transform(HLSSynthIP(fpgapart=fpga_part)) - model = model.transform(PrepareRTLSim()) - - io_dict = {model.graph.input[0].name: x} - y_dict = oxe.execute_onnx(model, io_dict) - y_prod = y_dict[model.graph.output[0].name] - assert (y_prod == y_ref).all() - - # FIFO sizing - model = model.transform(InsertDWC()) - model = model.transform(SpecializeLayers(fpga_part)) - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(PrepareIP(fpga_part, clk_ns)) - model = model.transform(HLSSynthIP(fpgapart=fpga_part)) - model = model.transform(PrepareRTLSim()) - model = model.transform(DeriveCharacteristic(6000)) - model = model.transform(DeriveFIFOSizes()) - model = model.transform(InsertFIFO(True)) - model = model.transform(SpecializeLayers(fpga_part)) - model = model.transform(GiveUniqueNodeNames()) - model = model.transform(GiveReadableTensorNames()) - - # stitched IP rtlsim - model = model.transform(PrepareIP(fpga_part, clk_ns)) - model = model.transform(HLSSynthIP(fpgapart=fpga_part)) - model = model.transform(CreateStitchedIP(fpga_part, clk_ns)) - - loop_node = model.get_nodes_by_op_type("FINNLoop")[0] - mlo_prehook = mlo_prehook_func_factory(loop_node) - rtlsim_exec(model, io_dict, pre_hook=mlo_prehook) - y_prod = io_dict[model.graph.output[0].name] - assert (y_prod == y_ref).all() From 2960b886bcee478c78938ceec7b654af855f50ab Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 14:49:36 +0200 Subject: [PATCH 119/170] Try to run xsi installation in pytest only once before parallel jobs start --- tests/conftest.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f8063c7504..fd49053812 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -28,11 +28,11 @@ # -*- coding: utf-8 -*- """ - Dummy conftest.py for finn. +Dummy conftest.py for finn. - If you don't know what this is for, just leave it empty. - Read more about conftest.py under: - https://pytest.org/latest/plugins.html +If you don't know what this is for, just leave it empty. +Read more about conftest.py under: +https://pytest.org/latest/plugins.html """ import pytest @@ -64,6 +64,14 @@ def pytest_collect_file(file_path: Path, parent) -> None: # noqa: ARG001 finn.util.settings.initialize_dummy_settings() +def pytest_configure(config) -> None: # noqa: ARG001 + """Initialize FINN settings once per pytest run.""" + import finn.util.settings + + finn.util.settings.initialize_dummy_settings() + import finn.xsi # noqa + + @pytest.fixture(scope="class", autouse=True) def isolate_build_dir(request): # Retrieve settings From 5b7aaedeb35a40dcf1c9b06023de1dba2e486aee Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 16:05:01 +0200 Subject: [PATCH 120/170] Clean up --- finn_xsi/finn_xsi/src/Design.cpp | 2 +- .../analysis/fpgadataflow/res_estimation.py | 24 +++-- src/finn/analysis/verify_custom_nodes.py | 1 + src/finn/builder/passes.py | 17 +--- src/finn/core/onnx_exec.py | 58 ++++++----- .../fpgadataflow/replace_verilog_relpaths.py | 45 +++++---- src/finn/util/create.py | 99 ++++++++++--------- src/finn/util/deprecated.py | 1 + src/finn/util/logging.py | 7 +- tests/end2end/test_end2end_bnn_pynq.py | 2 +- tests/end2end/test_end2end_mobilenet_v1.py | 2 +- .../testing_util}/throughput_test.py | 0 12 files changed, 140 insertions(+), 118 deletions(-) rename {src/finn/core => tests/testing_util}/throughput_test.py (100%) diff --git a/finn_xsi/finn_xsi/src/Design.cpp b/finn_xsi/finn_xsi/src/Design.cpp index fcd85738eb..75c4ac6ca3 100644 --- a/finn_xsi/finn_xsi/src/Design.cpp +++ b/finn_xsi/finn_xsi/src/Design.cpp @@ -6,7 +6,7 @@ using namespace xsi; Design::Design(xsi::Kernel& kernel, const std::string& design_lib, const s_xsi_setup_info& setup_info) : _kernel(std::move(kernel)) { _kernel.open(design_lib, setup_info); } Design::Design(xsi::Kernel& kernel, const std::string& design_lib, const char* const log_file, const char* const wdb_file) - : Design(kernel, design_lib, s_xsi_setup_info{.logFileName = const_cast(log_file), .wdbFileName = const_cast(wdb_file)}) {} + : Design(kernel, design_lib, s_xsi_setup_info{.logFileName = const_cast(log_file), .wdbFileName = const_cast(wdb_file), .xsimDir = ""}) {} // Destructor Design::~Design() { _kernel.close(); } diff --git a/src/finn/analysis/fpgadataflow/res_estimation.py b/src/finn/analysis/fpgadataflow/res_estimation.py index b0ad9f6f1f..aa56a75158 100644 --- a/src/finn/analysis/fpgadataflow/res_estimation.py +++ b/src/finn/analysis/fpgadataflow/res_estimation.py @@ -1,3 +1,5 @@ +"""Resource estimation analysis for dataflow models.""" + # Copyright (c) 2020, Xilinx # All rights reserved. # @@ -26,28 +28,34 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import qonnx.custom_op.registry as registry +from typing import TYPE_CHECKING +from finn.util.basic import getHWCustomOp from finn.util.fpgadataflow import is_hls_node, is_rtl_node +if TYPE_CHECKING: + from qonnx.core.modelwrapper import ModelWrapper + -def res_estimation(model, fpgapart): +def res_estimation(model: "ModelWrapper", fpgapart: str) -> dict[str, dict[str, int | float]]: """Estimates the resources needed for the given model. Ensure that all nodes have unique names (by calling the GiveUniqueNodeNames transformation) prior to calling this analysis pass to ensure all nodes are visible in the results. Returns {node name : resource estimation}.""" - res_dict = {} + res_dict: dict[str, dict[str, int | float]] = {} for node in model.graph.node: if is_hls_node(node) or is_rtl_node(node): - inst = registry.getCustomOp(node) + inst = getHWCustomOp(node) res_dict[node.name] = inst.node_res_estimation(fpgapart) return res_dict -def res_estimation_complete(model, fpgapart): +def res_estimation_complete( + model: "ModelWrapper", fpgapart: str +) -> dict[str, list[dict[str, int | float]]]: """Estimates the resources needed for the given model and all values for resource-related switches. Ensure that all nodes have unique names (by calling the GiveUniqueNodeNames @@ -55,12 +63,12 @@ def res_estimation_complete(model, fpgapart): visible in the results. Returns {node name : [resource estimation(s)]}.""" - res_dict = {} + res_dict: dict[str, list[dict[str, int | float]]] = {} for node in model.graph.node: if is_hls_node(node) or is_rtl_node(node): - inst = registry.getCustomOp(node) + inst = getHWCustomOp(node) op_type = node.op_type - if op_type.startswith("MVAU") or op_type.startswith("VVAU"): + if op_type.startswith(("MVAU", "VVAU")): orig_restype = inst.get_nodeattr("resType") res_dict[node.name] = [] inst.set_nodeattr("resType", "dsp") diff --git a/src/finn/analysis/verify_custom_nodes.py b/src/finn/analysis/verify_custom_nodes.py index ce4bef1cc6..eccfcb6fc2 100644 --- a/src/finn/analysis/verify_custom_nodes.py +++ b/src/finn/analysis/verify_custom_nodes.py @@ -1,3 +1,4 @@ +"""Runs verify nodes on all custom nodes in the model and checks that they pass.""" # Copyright (c) 2020, Xilinx # All rights reserved. # diff --git a/src/finn/builder/passes.py b/src/finn/builder/passes.py index d3fa15e34d..95a583bc53 100644 --- a/src/finn/builder/passes.py +++ b/src/finn/builder/passes.py @@ -19,7 +19,7 @@ from onnx_passes.ops import inject_custom_ops # Make custom Im2Col operator available for convolution lowering -from onnx_passes.ops.im2col import Im2Col # noqa: Used indirectly via registry # noqa: F401 +from onnx_passes.ops.im2col import Im2Col # noqa # noqa: Used indirectly via registry from onnx_passes.ops.qonnx import DOMAIN as QONNX_DOMAIN # Collects named passes from the ONNX Passes registry @@ -41,8 +41,6 @@ from finn.builder.build_dataflow_config import DataflowBuildConfig, VerificationStepType # Makes custom QONNX import and inlining passes available -import onnx_passes.passes.imports.qonnx # isort:skip # noqa: Used indirectly via registry -import onnx_passes.passes.inline.qonnx # isort:skip # noqa: Used indirectly via registry # noqa: F401 def _make_pass_config(cfg: DataflowBuildConfig): @@ -94,7 +92,6 @@ def _make_pass_config(cfg: DataflowBuildConfig): def _apply_passes(model: ir.Model, passes: list[str], cfg: dict, state: dict): """Resolves and applies the list of passes to the ONNX model.""" - # Collect and instantiate all ONNX IR passes from the sequence by name and # connect each pass to the shared configuration and state dictionary passes = [cls(cfg, state) for cls in collect(passes)] @@ -109,7 +106,6 @@ def _apply_passes(model: ir.Model, passes: list[str], cfg: dict, state: dict): def prepare(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Prepares a model to be processed by ONNX Passes.""" - # Deserialize ONNX proto representation wrapped by QONNX to ONNX IR format model = ir.from_proto(model.model) @@ -126,7 +122,6 @@ def prepare(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: def inline(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Applies ONNX Passes inlining transformations.""" - # Deserialize ONNX proto representation wrapped by QONNX to ONNX IR format model = ir.from_proto(model.model) @@ -157,7 +152,6 @@ def inline(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: def streamline(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Applies ONNX Passes streamlining transformations.""" - # Deserialize ONNX proto representation wrapped by QONNX to ONNX IR format model = ir.from_proto(model.model) @@ -176,12 +170,10 @@ class _ExportThresholdsToFINN(Transformation, RewriteRulePass): def pattern(self, op, x, thresholds, weights): """Target pattern to match.""" - return op.MultiThreshold(x, thresholds, weights, _domain=CUSTOM_DOMAIN) def check(self, op, x, thresholds, weights): """Match condition.""" - # Threshold parameter tensors must be constant, otherwise compatibility # with FINN cannot be checked... # TODO: Extend this to support non-constant thresholds to support @@ -204,7 +196,6 @@ def check(self, op, x, thresholds, weights): def rewrite(self, op, x, thresholds, weights): """Replacement pattern.""" - # Remove leading dimensions from the thresholds parameter tensor as # expected by QONNX thresholds = ir.convenience.get_const_tensor(thresholds).numpy() @@ -247,7 +238,6 @@ class _ExportIm2ColToFINN(Transformation, RewriteRulePass): def pattern(self, op, x, indices, dilations, kernel_shape, strides): """Target pattern to match.""" - return op.Im2Col( # Proper input and auxiliary index input holding the access pattern x, @@ -262,14 +252,12 @@ def pattern(self, op, x, indices, dilations, kernel_shape, strides): def check(self, op, x, indices, dilations, kernel_shape, strides): """Match condition.""" - # QONNX needs statically annotated input shape as this will be turned # into an attribute of the node return x.shape is not None and x.shape.is_static() def rewrite(self, op, x, indices, dilations, kernel_shape, strides): """Replacement pattern.""" - # Convert attributes to format required by QONNX attributes = { # TODO: Apparently QONNX needs the shape as a string... @@ -297,7 +285,6 @@ def _export_im2col_to_finn(model: ir.Model): def _infer_qonnx_datatypes(model: ModelWrapper): """Adds QONNX datatypes to a model by inferring types from values.""" - # Try inferring new datatype annotations for all tensors in the model for name in model.get_all_tensor_names(): # Only apply datatype inference on initializer tensors, for all other @@ -318,7 +305,6 @@ def _infer_qonnx_datatypes(model: ModelWrapper): def export(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Converts the model back to the FINN compatible format.""" - # Deserialize ONNX proto representation wrapped by QONNX to ONNX IR format model = ir.from_proto(model.model) @@ -359,7 +345,6 @@ def export(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: def step_passes_frontend(model: ModelWrapper, cfg: DataflowBuildConfig): """Meta build step calling the ONNX Passes steps in the expected order.""" - model = prepare(model, cfg) model = inline(model, cfg) model = streamline(model, cfg) diff --git a/src/finn/core/onnx_exec.py b/src/finn/core/onnx_exec.py index 24264f2abf..655d5c4dc5 100644 --- a/src/finn/core/onnx_exec.py +++ b/src/finn/core/onnx_exec.py @@ -41,13 +41,24 @@ import copy import numpy as np import qonnx.analysis.topology as ta +from collections.abc import Callable +from onnx import NodeProto +from qonnx.core.modelwrapper import ModelWrapper from qonnx.core.onnx_exec import execute_onnx as execute_onnx_base +from typing import cast from finn.core.rtlsim_exec import rtlsim_exec +from finn.util.exception import FINNInternalError, FINNUserError -def execute_onnx(model, input_dict, return_full_exec_context=False, start_node=None, end_node=None): - """Executes given ONNX ModelWrapper with given named inputs. +def execute_onnx( + model: "ModelWrapper", + input_dict: dict[str, np.ndarray], + return_full_exec_context: bool = False, + start_node: NodeProto | None = None, + end_node: NodeProto | None = None, +) -> dict[str, np.ndarray]: + """Execute given ONNX ModelWrapper with given named inputs. If return_full_exec_context is False, a dict of named outputs is returned as indicated by the model.graph.output. If return return_full_exec_context is True, the full set of tensors used by @@ -66,7 +77,7 @@ def execute_onnx(model, input_dict, return_full_exec_context=False, start_node=N if model_exec_mode == "rtlsim": # check sanity of model and then use stitched IP for rtlsim if not model.check_all_tensor_shapes_specified(): - raise Exception("Found unspecified tensor shapes, try infer_shapes") + raise FINNUserError("Found unspecified tensor shapes, try infer_shapes") ret = model.analysis(ta.nodes_topologically_sorted) assert ( ret["nodes_topologically_sorted"] is True @@ -79,26 +90,23 @@ def execute_onnx(model, input_dict, return_full_exec_context=False, start_node=N # the input data as well as the trained parameters) and the graph ValueInfo # (intermediate tensors between layers) # this is provided by the execution_context, which is a dict of np.ndarray - execution_context = model.make_empty_exec_context() + execution_context = cast("dict[str, np.ndarray]", model.make_empty_exec_context()) # fill in any inputs provided to this function for inp_name in input_dict.keys(): if inp_name in execution_context: if execution_context[inp_name].shape == input_dict[inp_name].shape: execution_context[inp_name] = input_dict[inp_name] else: - raise Exception( - "Shape mismatch for provided input %s: found %s expected %s " - % ( - inp_name, - str(execution_context[inp_name].shape), - str(input_dict[inp_name].shape), - ) + raise FINNInternalError( + f"Shape mismatch for provided input {inp_name}: found " + f"{execution_context[inp_name].shape!s} expected " + f"{input_dict[inp_name].shape!s} " ) # use stitched IP for rtlsim rtlsim_exec(model, execution_context) else: - raise Exception( + raise FINNInternalError( """Metadata property "exec_mode" is set to an unknown value. Can be left unset or has to be set to "rtlsim" for execution using xsi!""" ) @@ -106,15 +114,17 @@ def execute_onnx(model, input_dict, return_full_exec_context=False, start_node=N if return_full_exec_context: return execution_context # provide outputs as dict - output_dict = dict() + output_dict = {} for out_tensor in graph.output: out_name = out_tensor.name output_dict[out_name] = execution_context[out_name] return output_dict -def execute_onnx_and_make_model(model, input_dict): - """Executes given ONNX ModelWrapper with given named inputs and return a new +def execute_onnx_and_make_model( + model: "ModelWrapper", input_dict: dict[str, np.ndarray] +) -> ModelWrapper: + """Execute given ONNX ModelWrapper with given named inputs and return a new ModelWrapper where an initializer is provided for each tensor as taken from the execution. This new model is useful for debugging, since it contains all the intermediate activation values.""" @@ -131,15 +141,17 @@ def execute_onnx_and_make_model(model, input_dict): def compare_execution( - model_a, - model_b, - input_dict, - compare_fxn=lambda x, y: np.isclose(x, y, atol=1e-3).all(), -): - """Executes two ONNX models and compare their outputs using given function. + model_a: "ModelWrapper", + model_b: "ModelWrapper", + input_dict: dict[str, np.ndarray], + compare_fxn: Callable[ + [list | np.ndarray, list | np.ndarray], bool | np.bool_ + ] = lambda x, y: np.isclose(x, y, atol=1e-3).all(), +) -> bool | np.bool_: + """Execute two ONNX models and compare their outputs using given function. compare_fxn should take in two tensors and return a Boolean""" # compare values from first output tensors produced - res_a = list(execute_onnx(model_a, input_dict).items())[0][1] - res_b = list(execute_onnx(model_b, input_dict).items())[0][1] + res_a = next(iter(execute_onnx(model_a, input_dict).items()))[1] + res_b = next(iter(execute_onnx(model_b, input_dict).items()))[1] return compare_fxn(res_a, res_b) diff --git a/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py b/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py index 99bfe8bfc0..54d423ca56 100644 --- a/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py +++ b/src/finn/transformation/fpgadataflow/replace_verilog_relpaths.py @@ -27,42 +27,47 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os +"""Replace relative paths inside generated Verilog with absolute paths.""" + import qonnx.custom_op.registry as registry +from pathlib import Path +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation +from typing import Literal, cast from finn.util.fpgadataflow import is_hls_node, is_rtl_node class ReplaceVerilogRelPaths(Transformation): - """Convert ./ relative file paths to absolute ones for generated Verilog""" + """Convert ./ relative file paths to absolute ones for generated Verilog.""" - def __init__(self): + def __init__(self) -> None: + """Initialize the transformation.""" super().__init__() - def apply(self, model): + def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: + """Replace relative $readmemh paths in Verilog files under IP gen dirs.""" for node in model.graph.node: if is_hls_node(node) or is_rtl_node(node): try: # lookup op_type in registry of CustomOps inst = registry.getCustomOp(node) # find the IP gen dir - ipgen_path = inst.get_nodeattr("ipgen_path") - if ipgen_path is not None and os.path.isdir(ipgen_path): - for dname, dirs, files in os.walk(ipgen_path): - for fname in files: - if fname.endswith(".v"): - fpath = os.path.join(dname, fname) - with open(fpath) as f: - s = f.read() - old = '$readmemh(".' - new = '$readmemh("%s' % dname - s = s.replace(old, new) - old = '"./' - new = '"%s/' % dname - s = s.replace(old, new) - with open(fpath, "w") as f: - f.write(s) + ipgen_path = Path(cast("str", inst.get_nodeattr("ipgen_path"))) + if ipgen_path is not None and ipgen_path.is_dir(): + for fpath in ipgen_path.rglob("*.v"): + with fpath.open() as f: + s = f.read() + dname = fpath.parent + dname_str = dname.resolve().as_posix() + old = '$readmemh(".' + new = f'$readmemh("{dname_str}' + s = s.replace(old, new) + old = '"./' + new = f'"{dname_str}/' + s = s.replace(old, new) + with fpath.open("w") as f: + f.write(s) except KeyError: pass return (model, False) diff --git a/src/finn/util/create.py b/src/finn/util/create.py index 93f555fe75..85f516310f 100644 --- a/src/finn/util/create.py +++ b/src/finn/util/create.py @@ -1,3 +1,4 @@ +"""Utility functions for creating ONNX models, including random MLPs and adjacency lists.""" # Copyright (c) 2020 Xilinx, Inc. # Copyright (C) 2025, Advanced Micro Devices, Inc. # All rights reserved. @@ -29,16 +30,19 @@ import numpy as np from collections import defaultdict, deque +from collections.abc import Callable from onnx import TensorProto, helper from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper from qonnx.util.basic import calculate_signed_dot_prod_range, gen_finn_dt_tensor, qonnx_make_model +from typing import Any -def hls_random_mlp_maker(layer_spec): +def hls_random_mlp_maker(layer_spec: list[dict[str, Any]]) -> ModelWrapper: """Create an MLP of given specification using HLSCustomOp instances. Generate random weights/thresholds of appropriate size.""" ret = [] + rng = np.random.default_rng() for lyr in layer_spec: idt = lyr["idt"] wdt = lyr["wdt"] @@ -48,55 +52,58 @@ def hls_random_mlp_maker(layer_spec): lyr["W"] = gen_finn_dt_tensor(wdt, (mw, mh)) if act is None: # no activation, produce accumulators - T = None - tdt = None + thresholds = None + threshold_dtype = None if wdt == DataType["BIPOLAR"] and idt == DataType["BIPOLAR"]: - odt = DataType["UINT32"] + output_dtype = DataType["UINT32"] else: - odt = DataType["INT32"] + output_dtype = DataType["INT32"] else: - odt = act - (min, max) = calculate_signed_dot_prod_range(idt, wdt, mw) + output_dtype = act + (min_val, max_val) = calculate_signed_dot_prod_range(idt, wdt, mw) + min_val_int = int(min_val) + max_val_int = int(max_val) n_steps = act.get_num_possible_values() - 1 - T = np.random.randint(min, max - 1, (mh, n_steps)).astype(np.float32) + thresholds = rng.integers(min_val_int, max_val_int - 1, size=(mh, n_steps)).astype( + np.float32 + ) # provide non-decreasing thresholds - T = np.sort(T, axis=1) + thresholds = np.sort(thresholds, axis=1) # generate thresholds for activation if wdt == DataType["BIPOLAR"] and idt == DataType["BIPOLAR"]: - tdt = DataType["UINT32"] + threshold_dtype = DataType["UINT32"] # bias thresholds to be positive - T = np.ceil((T + mw) / 2) - assert (T >= 0).all() + thresholds = np.ceil((thresholds + mw) / 2) + assert (thresholds >= 0).all() else: - tdt = DataType["INT32"] - lyr["T"] = T - lyr["tdt"] = tdt - lyr["odt"] = odt + threshold_dtype = DataType["INT32"] + lyr["T"] = thresholds + lyr["tdt"] = threshold_dtype + lyr["odt"] = output_dtype ret.append(lyr) return hls_mlp_maker(ret) -def hls_mlp_maker(layer_spec): +def hls_mlp_maker(layer_spec: list[dict[str, Any]]) -> ModelWrapper: """Create an MLP of given specification using HLSCustomOp instances.""" current_in_name = "" current_out_name = "" - i = 0 graph = helper.make_graph(nodes=[], name="mlp", inputs=[], outputs=[]) model = qonnx_make_model(graph, producer_name="finn") model = ModelWrapper(model) - for lyr in layer_spec: - current_W_name = "W_%d" % i - current_T_name = "T_%d" % i - current_in_name = "act_%d" % i - current_out_name = "act_%d" % (i + 1) - - W = lyr["W"] - (mw, mh) = W.shape - T = lyr["T"] + for i, lyr in enumerate(layer_spec): + current_w_name = f"W_{i}" + current_t_name = f"T_{i}" + current_in_name = f"act_{i}" + current_out_name = f"act_{i + 1}" + + weights = lyr["W"] + (mw, mh) = weights.shape + thresholds = lyr["T"] pe = lyr["pe"] simd = lyr["simd"] wdt = lyr["wdt"] @@ -127,19 +134,16 @@ def hls_mlp_maker(layer_spec): export_idt = idt binary_xnor_mode = 0 - if T is not None: + if thresholds is not None: no_act = 0 - node_inp_list = [current_in_name, current_W_name, current_T_name] - if odt == DataType["BIPOLAR"]: - actval = 0 - else: - actval = odt.min() + node_inp_list = [current_in_name, current_w_name, current_t_name] + actval = 0 if odt == DataType["BIPOLAR"] else odt.min() else: # no thresholds - node_inp_list = [current_in_name, current_W_name] + node_inp_list = [current_in_name, current_w_name] actval = 0 no_act = 1 - FCLayer_node = helper.make_node( + fc_layer_node = helper.make_node( "MVAU", node_inp_list, [current_out_name], @@ -157,25 +161,26 @@ def hls_mlp_maker(layer_spec): noActivation=no_act, ) - model.graph.node.append(FCLayer_node) + model.graph.node.append(fc_layer_node) model.set_tensor_datatype(current_in_name, idt) model.set_tensor_datatype(current_out_name, odt) - model.set_tensor_datatype(current_W_name, wdt) + model.set_tensor_datatype(current_w_name, wdt) if binary_xnor_mode: # convert bipolar to binary - model.set_initializer(current_W_name, (W + 1) / 2) + model.set_initializer(current_w_name, (weights + 1) / 2) else: - model.set_initializer(current_W_name, W) - if T is not None: - model.set_tensor_datatype(current_T_name, tdt) - model.set_initializer(current_T_name, T) - i += 1 + model.set_initializer(current_w_name, weights) + if thresholds is not None: + model.set_tensor_datatype(current_t_name, tdt) + model.set_initializer(current_t_name, thresholds) return model -def adjacency_list(model, filter_function): - """Returns adjacency list of nodes based on filter function.""" +def adjacency_list( + model: ModelWrapper, filter_function: Callable[[Any], bool] +) -> dict[str, list[str]]: + """Return adjacency list of nodes based on filter function.""" graph = model.graph full_graph = defaultdict(list) @@ -188,7 +193,7 @@ def adjacency_list(model, filter_function): elif ( hasattr(graph, "input") and graph.input - and input_tensor in [input.name for input in graph.input] + and input_tensor in [inp.name for inp in graph.input] ): full_graph[input_tensor].append(node.name) for output_tensor in node.output: @@ -206,7 +211,7 @@ def adjacency_list(model, filter_function): raise ValueError("filter_function must be callable") filter_nodes = [node.name for node in graph.node if filter_function(node)] graph_inputs = ( - [input.name for input in graph.input] if hasattr(graph, "input") and graph.input else [] + [inp.name for inp in graph.input] if hasattr(graph, "input") and graph.input else [] ) graph_outputs = ( [output.name for output in graph.output] diff --git a/src/finn/util/deprecated.py b/src/finn/util/deprecated.py index 27ee216007..115d031634 100644 --- a/src/finn/util/deprecated.py +++ b/src/finn/util/deprecated.py @@ -16,6 +16,7 @@ def deprecated(func: Callable[pT, rT]) -> Callable[pT, rT]: @functools.wraps(func) def new_func(*args: pT.args, **kwargs: pT.kwargs) -> rT: + """Emit a deprecation warning and call the original function.""" log.warning( f"Using {func.__qualname__} is deprecated and will be removed in the next release.", stacklevel=2, diff --git a/src/finn/util/logging.py b/src/finn/util/logging.py index b0266de63b..e0ce8bcc93 100644 --- a/src/finn/util/logging.py +++ b/src/finn/util/logging.py @@ -1,4 +1,5 @@ """Handle logging related functionality.""" + import logging from rich.console import Console from rich.progress import Progress, TaskID @@ -128,7 +129,11 @@ def stop(self) -> None: self.progress.stop() def __enter__(self) -> None: + """Enter the context and start the display.""" self.start() - def __exit__(self, tp, vl, tb) -> None: + def __exit__( + self, tp: type[BaseException] | None, vl: BaseException | None, tb: TracebackType | None + ) -> None: + """Exit the context and stop the display.""" self.stop() diff --git a/tests/end2end/test_end2end_bnn_pynq.py b/tests/end2end/test_end2end_bnn_pynq.py index 4ad4060f5c..9517692bdd 100644 --- a/tests/end2end/test_end2end_bnn_pynq.py +++ b/tests/end2end/test_end2end_bnn_pynq.py @@ -72,7 +72,6 @@ import finn.transformation.streamline.absorb as absorb from finn.analysis.fpgadataflow.dataflow_performance import dataflow_performance from finn.core.onnx_exec import execute_onnx -from finn.core.throughput_test import throughput_test_rtlsim from finn.transformation.fpgadataflow.annotate_cycles import AnnotateCycles from finn.transformation.fpgadataflow.annotate_resources import AnnotateResources from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim @@ -103,6 +102,7 @@ get_trained_network_and_ishape, load_test_checkpoint_or_skip, ) +from tests.testing_util.throughput_test import throughput_test_rtlsim target_clk_ns = 20 mem_mode = "internal_decoupled" diff --git a/tests/end2end/test_end2end_mobilenet_v1.py b/tests/end2end/test_end2end_mobilenet_v1.py index 98a01785cf..a0bed27f9c 100644 --- a/tests/end2end/test_end2end_mobilenet_v1.py +++ b/tests/end2end/test_end2end_mobilenet_v1.py @@ -61,7 +61,6 @@ from finn.analysis.fpgadataflow.dataflow_performance import dataflow_performance from finn.builder.build_dataflow_config import DataflowBuildConfig from finn.core.onnx_exec import execute_onnx -from finn.core.throughput_test import throughput_test_rtlsim from finn.transformation.fpgadataflow.annotate_cycles import AnnotateCycles from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.create_dataflow_partition import CreateDataflowPartition @@ -88,6 +87,7 @@ load_test_checkpoint_or_skip, resize_smaller_side, ) +from tests.testing_util.throughput_test import throughput_test_rtlsim # Select Versal device such that RTL VVU (i.e. DSP58) can be enabled fpga_part = "xcvm1802-vsvd1760-2MP-e-S" diff --git a/src/finn/core/throughput_test.py b/tests/testing_util/throughput_test.py similarity index 100% rename from src/finn/core/throughput_test.py rename to tests/testing_util/throughput_test.py From 03e3fd9e6d99f8cde348757c7952122ee9e18f36 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 17:31:22 +0200 Subject: [PATCH 121/170] Add missing docstrings --- src/finn/custom_op/fpgadataflow/__init__.py | 4 +- src/finn/custom_op/fpgadataflow/concat.py | 16 +++++ .../fpgadataflow/convolutioninputgenerator.py | 15 +++++ src/finn/custom_op/fpgadataflow/crop.py | 14 +++++ .../fpgadataflow/duplicatestreams.py | 15 ++++- src/finn/custom_op/fpgadataflow/fmpadding.py | 12 ++++ .../custom_op/fpgadataflow/fmpadding_pixel.py | 14 +++++ .../custom_op/fpgadataflow/globalaccpool.py | 10 ++++ .../custom_op/fpgadataflow/hls/__init__.py | 4 +- .../fpgadataflow/hls/checksum_hls.py | 26 +++++++- .../custom_op/fpgadataflow/hls/concat_hls.py | 9 +++ .../custom_op/fpgadataflow/hls/crop_hls.py | 11 ++++ .../fpgadataflow/hls/duplicatestreams_hls.py | 12 ++++ .../hls/elementwise_binary_hls.py | 2 + .../fpgadataflow/hls/fmpadding_pixel_hls.py | 10 ++++ .../fpgadataflow/hls/globalaccpool_hls.py | 9 +++ .../fpgadataflow/hls/hwsoftmax_hls.py | 11 ++++ .../custom_op/fpgadataflow/hls/iodma_hls.py | 18 ++++++ .../fpgadataflow/hls/labelselect_hls.py | 10 ++++ .../fpgadataflow/hls/layernorm_hls.py | 11 ++++ .../custom_op/fpgadataflow/hls/lookup_hls.py | 12 ++++ .../hls/matrixvectoractivation_hls.py | 22 ++++++- .../fpgadataflow/hls/outer_shuffle_hls.py | 11 ++++ .../fpgadataflow/hls/replicate_stream_hls.py | 11 ++++ .../custom_op/fpgadataflow/hls/requant_hls.py | 8 +++ .../custom_op/fpgadataflow/hls/split_hls.py | 10 ++++ .../hls/streamingdatawidthconverter_hls.py | 9 +++ .../fpgadataflow/hls/streamingfifo_hls.py | 9 +++ .../fpgadataflow/hls/tlastmarker_hls.py | 24 ++++++++ .../fpgadataflow/hls/upsampler_hls.py | 8 +++ .../hls/vectorvectoractivation_hls.py | 15 +++++ src/finn/custom_op/fpgadataflow/hwsoftmax.py | 11 ++++ .../custom_op/fpgadataflow/inner_shuffle.py | 13 ++++ .../custom_op/fpgadataflow/labelselect.py | 11 ++++ src/finn/custom_op/fpgadataflow/layernorm.py | 11 ++++ src/finn/custom_op/fpgadataflow/lookup.py | 17 ++++++ .../fpgadataflow/replicate_stream.py | 25 ++++++++ src/finn/custom_op/fpgadataflow/requant.py | 23 +++++--- .../rtl/elementwise_binary_rtl.py | 15 +++++ .../custom_op/fpgadataflow/rtl/finn_loop.py | 19 ++++++ .../fpgadataflow/rtl/inner_shuffle_rtl.py | 13 +++- .../fpgadataflow/rtl/layernorm_rtl.py | 8 +++ .../custom_op/fpgadataflow/rtl/requant_rtl.py | 4 ++ src/finn/custom_op/fpgadataflow/shuffle.py | 15 ++++- src/finn/custom_op/fpgadataflow/split.py | 20 +++++++ .../streamingdataflowpartition.py | 6 ++ .../streamingdatawidthconverter.py | 14 +++++ .../custom_op/fpgadataflow/streamingfifo.py | 17 ++++++ src/finn/custom_op/fpgadataflow/templates.py | 1 + .../custom_op/fpgadataflow/thresholding.py | 1 + src/finn/custom_op/fpgadataflow/upsampler.py | 12 ++++ .../fpgadataflow/annotate_resources.py | 19 ++++-- .../fpgadataflow/convert_to_hw_layers.py | 6 ++ .../fpgadataflow/create_dataflow_partition.py | 19 ++++-- .../fpgadataflow/insert_fifo.py | 4 ++ .../minimize_accumulator_width.py | 11 +++- .../fpgadataflow/minimize_weight_bit_width.py | 15 +++-- .../fpgadataflow/prepare_cppsim.py | 43 +++++++++----- .../fpgadataflow/raise_scalar_to_rank1.py | 4 ++ .../fpgadataflow/replicate_stream.py | 3 + .../fpgadataflow/set_loop_boundary.py | 31 +++++++--- .../fpgadataflow/simulation_build.py | 13 ++++ .../fpgadataflow/simulation_connected.py | 5 +- .../fpgadataflow/simulation_isolated.py | 8 +++ .../fpgadataflow/specialize_layers.py | 24 ++++---- .../fpgadataflow/vivado_power_estimation.py | 3 + src/finn/transformation/move_reshape.py | 59 ++++++++++++++----- .../qonnx/convert_qonnx_to_finn.py | 3 + .../qonnx/fold_quant_weights.py | 2 + .../qonnx/infer_quant_avg_pool_2d.py | 4 ++ .../qonnx/quant_act_to_multithreshold.py | 6 +- .../streamline/collapse_repeated.py | 8 +++ .../streamline/extract_norm_scale_bias.py | 3 + src/finn/transformation/streamline/remove.py | 7 +++ .../streamline/sign_to_thres.py | 2 + .../streamline/streamline_plus.py | 2 + src/finn/util/fpgadataflow.py | 53 ++++++++--------- 77 files changed, 867 insertions(+), 118 deletions(-) diff --git a/src/finn/custom_op/fpgadataflow/__init__.py b/src/finn/custom_op/fpgadataflow/__init__.py index e5f613819a..6ac574bce9 100644 --- a/src/finn/custom_op/fpgadataflow/__init__.py +++ b/src/finn/custom_op/fpgadataflow/__init__.py @@ -27,6 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for init.""" from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp # Dictionary of HWCustomOp implementations @@ -36,8 +37,9 @@ # Registers a class into the custom_op dictionary # Note: This must be defined first, before importing any custom op # implementation to avoid "importing partially initialized module" issues. -def register_custom_op(cls): +def register_custom_op(cls) -> type[HWCustomOp]: # The class must actually implement HWCustomOp + """Register a custom operation.""" assert issubclass(cls, HWCustomOp), f"{cls} must subclass {HWCustomOp}" # Insert the class into the custom_op dictionary by its name custom_op[cls.__name__] = cls diff --git a/src/finn/custom_op/fpgadataflow/concat.py b/src/finn/custom_op/fpgadataflow/concat.py index bba5e79aaf..beea27edfb 100644 --- a/src/finn/custom_op/fpgadataflow/concat.py +++ b/src/finn/custom_op/fpgadataflow/concat.py @@ -27,6 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for concat.""" import math import numpy as np from qonnx.core.datatype import DataType @@ -40,9 +41,11 @@ class StreamingConcat(HWCustomOp): Only supports concatenating along the last (channel) axis.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "SIMD": ("i", True, 0), # number of elements from each stream to concat @@ -59,13 +62,16 @@ def get_nodeattr_types(self): return my_attrs def get_n_inputs(self): + """Return number of inputs.""" return len(self.get_nodeattr("ChannelsPerStream")) def get_total_elems(self): + """Return total elems.""" elems_per_stream = self.get_nodeattr("ChannelsPerStream") return int(np.sum(elems_per_stream)) def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" elems_per_stream = self.get_nodeattr("ChannelsPerStream") elems = elems_per_stream[ind] vecs = list(self.get_nodeattr("numInputVectors")) @@ -73,17 +79,20 @@ def get_normal_input_shape(self, ind=0): return ishape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" simd = self.get_nodeattr("SIMD") folds = self.get_nodeattr("ChannelsPerStream")[ind] // simd vecs = list(self.get_nodeattr("numInputVectors")) return tuple(vecs + [folds, simd]) def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" total_elems = self.get_total_elems() vecs = list(self.get_nodeattr("numInputVectors")) return tuple(vecs + [total_elems]) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" total_elems = self.get_total_elems() simd = self.get_nodeattr("SIMD") folds = total_elems // simd @@ -92,6 +101,7 @@ def get_folded_output_shape(self, ind=0): def infer_node_datatype(self, model): # check all input datatypes + """Infer node datatype.""" for i, inp in enumerate(self.onnx_node.input): idt = model.get_tensor_datatype(inp) if idt != self.get_input_datatype(i): @@ -109,10 +119,12 @@ def infer_node_datatype(self, model): def get_input_datatype(self, ind=0): # input dt identical for all inputs + """Return input datatype.""" return DataType[self.get_nodeattr("inputDataTypes")[ind]] def get_output_datatype(self, ind=0): # infer output datatype from declared inputDataTypes + """Return output datatype.""" min_input = 0 max_input = 0 for i in range(len(self.get_nodeattr("inputDataTypes"))): @@ -134,18 +146,22 @@ def get_output_datatype(self, ind=0): return odt def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype(ind).bitwidth() return ibits * self.get_nodeattr("SIMD") def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() out_width = obits * self.get_nodeattr("SIMD") return out_width def get_exp_cycles(self): + """Return exp cycles.""" return np.prod(self.get_folded_output_shape()[:-1]) def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node inp_values = [] for inp in node.input: diff --git a/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py b/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py index 9d4d50e9a8..b5d6c87157 100644 --- a/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py +++ b/src/finn/custom_op/fpgadataflow/convolutioninputgenerator.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for convolutioninputgenerator.""" from onnx import TensorProto, helper from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper @@ -46,9 +47,11 @@ class ConvolutionInputGenerator(HWCustomOp): """Abstraction layer for HW implementation of ConvolutionInputGenerator""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "ConvKernelDim": ("ints", True, []), # [H, W] = [Y, X] "IFMChannels": ("i", True, 0), @@ -84,12 +87,14 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" ifm_dim_h, ifm_dim_w = self.get_nodeattr("IFMDim") ifm_ch = self.get_nodeattr("IFMChannels") ishape = (1, ifm_dim_h, ifm_dim_w, ifm_ch) return ishape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" ifm_dim_h, ifm_dim_w = self.get_nodeattr("IFMDim") ifm_ch = self.get_nodeattr("IFMChannels") simd = self.get_nodeattr("SIMD") @@ -99,6 +104,7 @@ def get_folded_input_shape(self, ind=0): return folded_ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" k_h, k_w = self.get_nodeattr("ConvKernelDim") ifm_dim_h, ifm_dim_w = self.get_nodeattr("IFMDim") ifm_ch = self.get_nodeattr("IFMChannels") @@ -111,6 +117,7 @@ def get_normal_output_shape(self, ind=0): return oshape def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" k_h, k_w = self.get_nodeattr("ConvKernelDim") ifm_dim_h, ifm_dim_w = self.get_nodeattr("IFMDim") ifm_ch = self.get_nodeattr("IFMChannels") @@ -130,6 +137,7 @@ def get_folded_output_shape(self, ind=0): return folded_oshape def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node # data type stays the same dtype = model.get_tensor_datatype(node.input[0]) @@ -175,6 +183,7 @@ def get_instream_width(self, ind=0): return in_width def get_outstream_width(self, ind=0): + """Return outstream width.""" if self.use_parallel_window_output(): # feed all window pixels in parallel k_h, k_w = self.get_nodeattr("ConvKernelDim") @@ -191,6 +200,7 @@ def get_1d_conv_attrs_normalized(self): # returns the attributes of the layer as follows: # [H, W] = [Y, X] = [1, D] or [D, 1] are always mapped to [1, D]. # The dummy ('1') dimension is the Y-dimension. + """Return 1d conv attrs normalized.""" ifm_ch = self.get_nodeattr("IFMChannels") k = self.get_nodeattr("ConvKernelDim") ifm_dim = self.get_nodeattr("IFMDim") @@ -209,19 +219,24 @@ def get_1d_conv_attrs_normalized(self): return (ifm_ch, ifm_dim, ofm_dim, k, stride, dilation) def get_exp_cycles(self): + """Return exp cycles.""" return 0 def bram_estimation(self): + """Return bram estimation.""" return 0 def lut_estimation(self): + """Return lut estimation.""" return 0 def uram_estimation(self): + """Return uram estimation.""" return 0 def execute_node(self, context, graph): # using Im2Col node to calculate output + """Execute node.""" node = self.onnx_node ifm_dim = self.get_nodeattr("IFMDim") k = self.get_nodeattr("ConvKernelDim") diff --git a/src/finn/custom_op/fpgadataflow/crop.py b/src/finn/custom_op/fpgadataflow/crop.py index 5e0f971d65..fc04847880 100644 --- a/src/finn/custom_op/fpgadataflow/crop.py +++ b/src/finn/custom_op/fpgadataflow/crop.py @@ -10,6 +10,7 @@ # ################################################################################### +"""Module for crop.""" import numpy as np from qonnx.core.datatype import DataType @@ -21,9 +22,11 @@ class Crop(HWCustomOp): """Abstraction layer for Crop layers.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "DataType": ("s", True, ""), "ImgDim": ("ints", True, []), # [h, w] @@ -39,6 +42,7 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" num_vec = self.get_nodeattr("numInputVectors") h, w = self.get_nodeattr("ImgDim") if h == 0: @@ -49,6 +53,7 @@ def get_normal_input_shape(self, ind=0): return num_vec + img_dim + [ch] if num_vec != [0] else img_dim + [ch] def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" num_vec = self.get_nodeattr("numInputVectors") height, width = self.get_nodeattr("ImgDim") ch = self.get_nodeattr("NumChannels") @@ -65,6 +70,7 @@ def get_normal_output_shape(self, ind=0): return num_vec + o_img_dim + [ch] if num_vec != [0] else o_img_dim + [ch] def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node h, w = self.get_nodeattr("ImgDim") crop_north = self.get_nodeattr("CropNorth") @@ -84,9 +90,11 @@ def execute_node(self, context, graph): context[node.output[0]] = cropped_slice def get_input_datatype(self, ind=0): + """Return input datatype.""" return DataType[self.get_nodeattr("DataType")] def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node dt = model.get_tensor_datatype(node.input[0]) if dt != self.get_input_datatype(): @@ -97,19 +105,23 @@ def infer_node_datatype(self, model): self.set_nodeattr("DataType", dt.name) def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return ibits * simd def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return obits * simd def get_output_datatype(self, ind=0): + """Return output datatype.""" return DataType[self.get_nodeattr("DataType")] def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" normal_oshape = list(self.get_normal_output_shape()) simd = self.get_nodeattr("SIMD") assert normal_oshape[-1] % simd == 0, "Innermost dimension must be divisible by SIMD" @@ -118,6 +130,7 @@ def get_folded_output_shape(self, ind=0): return tuple(folded_oshape) def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" normal_ishape = list(self.get_normal_input_shape()) simd = self.get_nodeattr("SIMD") assert normal_ishape[-1] % simd == 0, "Innermost dimension must be divisible by SIMD" @@ -126,6 +139,7 @@ def get_folded_input_shape(self, ind=0): return tuple(folded_ishape) def get_exp_cycles(self): + """Return exp cycles.""" simd = self.get_nodeattr("SIMD") num_vec = self.get_nodeattr("numInputVectors") height, width = self.get_nodeattr("ImgDim") diff --git a/src/finn/custom_op/fpgadataflow/duplicatestreams.py b/src/finn/custom_op/fpgadataflow/duplicatestreams.py index e0b6053014..fde0fb0290 100644 --- a/src/finn/custom_op/fpgadataflow/duplicatestreams.py +++ b/src/finn/custom_op/fpgadataflow/duplicatestreams.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for duplicatestreams.""" import numpy as np from qonnx.core.datatype import DataType @@ -34,12 +35,14 @@ class DuplicateStreams(HWCustomOp): - """Abstraction layer for HW implementation of DuplicateStreams""" + """Abstraction layer for HW implementation of DuplicateStreams.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "NumChannels": ("i", True, 0), "PE": ("i", True, 0), @@ -57,15 +60,18 @@ def get_nodeattr_types(self): return my_attrs def get_num_output_streams(self): + """Return num output streams.""" return self.get_nodeattr("NumOutputStreams") def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" ch = self.get_nodeattr("NumChannels") vecs = list(self.get_nodeattr("numInputVectors")) ishape = tuple(vecs + [ch]) return ishape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" ch = self.get_nodeattr("NumChannels") pe = self.get_nodeattr("PE") vecs = list(self.get_nodeattr("numInputVectors")) @@ -77,19 +83,23 @@ def get_folded_input_shape(self, ind=0): def get_normal_output_shape(self, ind=0): # since the output shape of both out streams are the same # return independently from index + """Return normal output shape.""" return self.get_normal_input_shape() def get_folded_output_shape(self, ind=0): # since the output shape of both out streams are the same # return independently from index + """Return folded output shape.""" return self.get_folded_input_shape() def make_shape_compatible_op(self, model): + """Create shape compatible op.""" ret = super().make_shape_compatible_op(model) ret.output[:] = self.onnx_node.output return ret def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -127,6 +137,7 @@ def get_outstream_width(self, ind=0): return out_width def get_number_output_values(self): + """Return number output values.""" out_val = {} for i in range(len(self.onnx_node.output)): out_val["out%s" % i] = np.prod(self.get_folded_output_shape(i)[1:-1]) @@ -134,11 +145,13 @@ def get_number_output_values(self): def get_exp_cycles(self): # Channels/PE * batch size * fmdim * fmdim + """Return exp cycles.""" return np.prod(self.get_folded_output_shape()[:-1]) def execute_node(self, context, graph): # passing input to both outputs to make # abstraction layer executable + """Execute node.""" node = self.onnx_node inp = context[node.input[0]] exp_shape = self.get_normal_input_shape() diff --git a/src/finn/custom_op/fpgadataflow/fmpadding.py b/src/finn/custom_op/fpgadataflow/fmpadding.py index 098a413931..d3c4e221f0 100644 --- a/src/finn/custom_op/fpgadataflow/fmpadding.py +++ b/src/finn/custom_op/fpgadataflow/fmpadding.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for fmpadding.""" import numpy as np from qonnx.core.datatype import DataType @@ -38,9 +39,11 @@ class FMPadding(HWCustomOp): Pads input image by given amount.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { # spatial size of input images "ImgDim": ("ints", True, []), # [H, W] = [Y, X] @@ -73,6 +76,7 @@ def get_padded_odim(self): return [odim_h, odim_w] def get_exp_cycles(self): + """Return exp cycles.""" odim_h, odim_w = self.get_padded_odim() channels = self.get_nodeattr("NumChannels") simd = self.get_nodeattr("SIMD") @@ -81,12 +85,14 @@ def get_exp_cycles(self): return int(exp_cycles) def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" idim_h, idim_w = self.get_nodeattr("ImgDim") num_ch = self.get_nodeattr("NumChannels") ishape = (1, idim_h, idim_w, num_ch) return ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" odim_h, odim_w = self.get_padded_odim() num_ch = self.get_nodeattr("NumChannels") @@ -94,6 +100,7 @@ def get_normal_output_shape(self, ind=0): return oshape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" normal_ishape = list(self.get_normal_input_shape()) ifm_ch = self.get_nodeattr("NumChannels") simd = self.get_nodeattr("SIMD") @@ -103,6 +110,7 @@ def get_folded_input_shape(self, ind=0): return tuple(folded_ishape) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" normal_oshape = list(self.get_normal_output_shape()) ifm_ch = self.get_nodeattr("NumChannels") simd = self.get_nodeattr("SIMD") @@ -112,6 +120,7 @@ def get_folded_output_shape(self, ind=0): return tuple(folded_oshape) def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -137,17 +146,20 @@ def get_output_datatype(self, ind=0): return self.get_input_datatype() def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return ibits * simd def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return obits * simd def execute_node(self, context, graph): # simulate behavior with Python functionality + """Execute node.""" node = self.onnx_node pad = self.get_nodeattr("Padding") inp_values = context[node.input[0]] diff --git a/src/finn/custom_op/fpgadataflow/fmpadding_pixel.py b/src/finn/custom_op/fpgadataflow/fmpadding_pixel.py index a36eabe949..cb8db34f7b 100644 --- a/src/finn/custom_op/fpgadataflow/fmpadding_pixel.py +++ b/src/finn/custom_op/fpgadataflow/fmpadding_pixel.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for fmpadding pixel.""" import numpy as np from qonnx.core.datatype import DataType @@ -34,10 +35,14 @@ class FMPadding_Pixel(HWCustomOp): + """Class for FM Padding Pixel.""" + def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { # spatial size of input images "ImgDim": ("ints", True, []), @@ -64,6 +69,7 @@ def get_padded_odim(self): return [odim_h, odim_w] def get_exp_cycles(self): + """Return exp cycles.""" odim_h, odim_w = self.get_padded_odim() channels = self.get_nodeattr("NumChannels") simd = self.get_nodeattr("SIMD") @@ -72,18 +78,21 @@ def get_exp_cycles(self): return int(exp_cycles) def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" idim_h, idim_w = self.get_nodeattr("ImgDim") num_ch = self.get_nodeattr("NumChannels") ishape = (1, idim_h, idim_w, num_ch) return ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" odim_h, odim_w = self.get_padded_odim() num_ch = self.get_nodeattr("NumChannels") oshape = (1, odim_h, odim_w, num_ch) return oshape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" normal_ishape = list(self.get_normal_input_shape()) ifm_ch = self.get_nodeattr("NumChannels") simd = self.get_nodeattr("SIMD") @@ -93,6 +102,7 @@ def get_folded_input_shape(self, ind=0): return tuple(folded_ishape) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" normal_oshape = list(self.get_normal_output_shape()) ifm_ch = self.get_nodeattr("NumChannels") simd = self.get_nodeattr("SIMD") @@ -102,6 +112,7 @@ def get_folded_output_shape(self, ind=0): return tuple(folded_oshape) def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -127,17 +138,20 @@ def get_output_datatype(self, ind=0): return self.get_input_datatype() def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return ibits * simd def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return obits * simd def execute_node(self, context, graph): # simulate behavior with Python functionality + """Execute node.""" node = self.onnx_node s_h, s_w = self.get_nodeattr("Stride") inp_values = context[node.input[0]] diff --git a/src/finn/custom_op/fpgadataflow/globalaccpool.py b/src/finn/custom_op/fpgadataflow/globalaccpool.py index feb96ce8d6..19f4c3d695 100644 --- a/src/finn/custom_op/fpgadataflow/globalaccpool.py +++ b/src/finn/custom_op/fpgadataflow/globalaccpool.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for globalaccpool.""" import numpy as np from qonnx.core.datatype import DataType @@ -37,9 +38,11 @@ class GlobalAccPool(HWCustomOp): """Abstraction layer for HW implementation of GlobalAccPool""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "NumChannels": ("i", True, 0), "PE": ("i", True, 0), @@ -55,12 +58,14 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" ch = self.get_nodeattr("NumChannels") vecs = list(self.get_nodeattr("numInputVectors")) ishape = tuple(vecs + [ch]) return ishape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" ch = self.get_nodeattr("NumChannels") pe = self.get_nodeattr("PE") vecs = list(self.get_nodeattr("numInputVectors")) @@ -70,6 +75,7 @@ def get_folded_input_shape(self, ind=0): return folded_ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" ch = self.get_nodeattr("NumChannels") vecs = list(self.get_nodeattr("numInputVectors")) if len(vecs) == 1: @@ -79,6 +85,7 @@ def get_normal_output_shape(self, ind=0): return oshape def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" ch = self.get_nodeattr("NumChannels") pe = self.get_nodeattr("PE") unfolded_shape = list(self.get_normal_output_shape()) @@ -88,6 +95,7 @@ def get_folded_output_shape(self, ind=0): return oshape def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -133,6 +141,7 @@ def get_outstream_width(self, ind=0): def get_exp_cycles(self): # Channels/PE * batch size * idim * idim + Channels/PE + """Return exp cycles.""" ch = self.get_nodeattr("NumChannels") pe = self.get_nodeattr("PE") folds = int(ch / pe) @@ -140,6 +149,7 @@ def get_exp_cycles(self): def execute_node(self, context, graph): # simulate behavior with Python functionality + """Execute node.""" node = self.onnx_node inp_values = context[node.input[0]] oshape = context[node.output[0]].shape diff --git a/src/finn/custom_op/fpgadataflow/hls/__init__.py b/src/finn/custom_op/fpgadataflow/hls/__init__.py index 7425eaf631..d65f552cde 100644 --- a/src/finn/custom_op/fpgadataflow/hls/__init__.py +++ b/src/finn/custom_op/fpgadataflow/hls/__init__.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for init.""" from finn.custom_op.fpgadataflow.hls.streamingfifo_hls import StreamingFIFO_hls from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp @@ -37,8 +38,9 @@ # Registers a class into the custom_op dictionary # Note: This must be defined first, before importing any custom op # implementation to avoid "importing partially initialized module" issues. -def register_custom_op(cls): +def register_custom_op(cls) -> type[HLSBackend]: # The class must actually implement HWCustomOp + """Register a custom HLS operation.""" assert issubclass(cls, HWCustomOp), f"{cls} must subclass {HWCustomOp}" # The class must also implement the HLSBackend assert issubclass(cls, HLSBackend), f"{cls} must subclass {HLSBackend}" diff --git a/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py b/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py index 31d48c51fa..0e4a5bc0a4 100644 --- a/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/checksum_hls.py @@ -27,6 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for checksum hls.""" import numpy as np from qonnx.core.datatype import DataType @@ -39,9 +40,11 @@ class CheckSum_hls(HLSBackend, HWCustomOp): """Class that corresponds to custom_hls checksum function.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { # number of data words in a frame "words_per_frame": ("i", True, 0), @@ -57,6 +60,7 @@ def get_nodeattr_types(self): return my_attrs def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -72,27 +76,31 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], odt) def get_input_datatype(self, ind=0): - """Returns FINN DataType of input.""" + """Return FINN DataType of input.""" return DataType[self.get_nodeattr("inputDataType")] def get_output_datatype(self, ind=0): - """Returns FINN DataType of output.""" + """Return FINN DataType of output.""" # here same as input data type return DataType[self.get_nodeattr("inputDataType")] def get_instream_width(self, ind=0): + """Return instream width.""" dtype = DataType[self.get_nodeattr("inputDataType")] folded_shape = self.get_nodeattr("folded_shape") in_width = folded_shape[-1] * dtype.bitwidth() return in_width def get_outstream_width(self, ind=0): + """Return outstream width.""" return self.get_instream_width() def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" return self.get_nodeattr("folded_shape") def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" return self.get_nodeattr("folded_shape") def get_normal_input_shape(self, ind=0): @@ -105,6 +113,7 @@ def get_normal_input_shape(self, ind=0): # and together with all previous dimensions # this gives the normal input shape + """Return normal input shape.""" folded_shape = self.get_nodeattr("folded_shape") # extract inner dimension inner_dim = folded_shape[-1] @@ -119,9 +128,11 @@ def get_normal_input_shape(self, ind=0): return normal_ishape def get_ap_int_max_w(self): + """Return ap int max w.""" return max(super().get_ap_int_max_w(), 32) def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" if ind == 0: # same shape as input return self.get_normal_input_shape() @@ -131,6 +142,7 @@ def get_normal_output_shape(self, ind=0): raise Exception("Undefined input ind for this layer type") def npy_to_dynamic_output(self, context): + """Return npy to dynamic output.""" super().npy_to_dynamic_output(context) node = self.onnx_node code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") @@ -138,12 +150,15 @@ def npy_to_dynamic_output(self, context): context[node.output[1]] = output_checksum def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "checksum.hpp"'] def defines(self, var): + """Return defines.""" items_per_word = self.get_nodeattr("items_per_word") words_per_frame = self.get_nodeattr("words_per_frame") word_size = self.get_instream_width() @@ -154,6 +169,7 @@ def defines(self, var): self.code_gen_dict["$DEFINES$"] = my_defines def read_npy_data(self): + """Return read npy data.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") dtype = self.get_input_datatype() elem_bits = dtype.bitwidth() @@ -176,6 +192,7 @@ def read_npy_data(self): ) def strm_decl(self): + """Return strm decl.""" self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( f'hls::stream> in0_V ("in0_V");' @@ -188,11 +205,13 @@ def strm_decl(self): self.code_gen_dict["$STREAMDECLARATIONS$"].append("ap_uint<1> drain = false;") def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [ """checksum(in0_V, out0_V, chk, drain);""" ] def dataoutstrm(self): + """Return dataoutstrm.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") dtype = self.get_output_datatype() if dtype == DataType["BIPOLAR"]: @@ -224,12 +243,14 @@ def dataoutstrm(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ f"""using T = ap_uint;\n void {self.onnx_node.name}(hls::stream &in0_V, hls::stream &out0_V, ap_uint<32> &chk, ap_uint<1> &drain)""" ] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = ["#pragma HLS interface axis port=in0_V"] self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS interface axis port=out0_V") self.code_gen_dict["$PRAGMAS$"].append( @@ -243,6 +264,7 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS dataflow disable_start_propagation") def get_verilog_top_module_intf_names(self): + """Return verilog top module intf names.""" intf_names = super().get_verilog_top_module_intf_names() # expose axilite interface intf_names["axilite"] = ["s_axi_checksum"] diff --git a/src/finn/custom_op/fpgadataflow/hls/concat_hls.py b/src/finn/custom_op/fpgadataflow/hls/concat_hls.py index 2c24762eb1..3ba059e118 100644 --- a/src/finn/custom_op/fpgadataflow/hls/concat_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/concat_hls.py @@ -27,6 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for concat hls.""" from finn.custom_op.fpgadataflow.concat import StreamingConcat from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend @@ -36,9 +37,11 @@ class StreamingConcat_hls(StreamingConcat, HLSBackend): Only supports concatenating along the last axis.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(StreamingConcat.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) @@ -46,15 +49,19 @@ def get_nodeattr_types(self): return my_attrs def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "concat.hpp"'] def defines(self, var): + """Return defines.""" self.code_gen_dict["$DEFINES$"] = ["#define SIMD {}".format(self.get_nodeattr("SIMD"))] def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [] n_inputs = self.get_n_inputs() input_folds = [str(self.get_folded_input_shape(i)[-2]) for i in range(n_inputs)] @@ -67,6 +74,7 @@ def docompute(self): self.code_gen_dict["$DOCOMPUTE$"] = [comp_call] def blackboxfunction(self): + """Return blackboxfunction.""" n_inputs = self.get_n_inputs() in_streams = [] for i in range(n_inputs): @@ -82,6 +90,7 @@ def blackboxfunction(self): self.code_gen_dict["$BLACKBOXFUNCTION$"] = [blackbox_hls] def pragmas(self): + """Return pragmas.""" n_inputs = self.get_n_inputs() pragmas = [] for i in range(n_inputs): diff --git a/src/finn/custom_op/fpgadataflow/hls/crop_hls.py b/src/finn/custom_op/fpgadataflow/hls/crop_hls.py index 14a60de42f..a19ba25f10 100644 --- a/src/finn/custom_op/fpgadataflow/hls/crop_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/crop_hls.py @@ -10,23 +10,30 @@ # ################################################################################### +"""Module for crop hls.""" from finn.custom_op.fpgadataflow.crop import Crop from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend class Crop_hls(Crop, HLSBackend): + """Class for Crop hls.""" + def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" return Crop.get_nodeattr_types(self) | HLSBackend.get_nodeattr_types(self) def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = [ '#include "crop.hpp"', ] def defines(self, var): + """Return defines.""" simd = self.get_nodeattr("SIMD") dtype = self.get_input_datatype() height, width = self.get_nodeattr("ImgDim") @@ -49,6 +56,7 @@ def defines(self, var): ] def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [ """ hls::stream src0; @@ -63,6 +71,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ f""" void {self.onnx_node.name} ( @@ -73,6 +82,7 @@ def blackboxfunction(self): ] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = [ """ #pragma HLS interface AXIS port=in0_V @@ -86,4 +96,5 @@ def pragmas(self): ] def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) diff --git a/src/finn/custom_op/fpgadataflow/hls/duplicatestreams_hls.py b/src/finn/custom_op/fpgadataflow/hls/duplicatestreams_hls.py index 3aeb2b818b..0e57a78dcf 100644 --- a/src/finn/custom_op/fpgadataflow/hls/duplicatestreams_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/duplicatestreams_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for duplicatestreams hls.""" from finn.custom_op.fpgadataflow.duplicatestreams import DuplicateStreams from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend @@ -34,15 +35,18 @@ class DuplicateStreams_hls(DuplicateStreams, HLSBackend): """Class that corresponds to finn-hlslib function of the same name.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(DuplicateStreams.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def verify_node(self): + """Verify node.""" info_messages = [] # verify that "backend" is set to "fpgadataflow" backend_value = self.get_nodeattr("backend") @@ -66,15 +70,19 @@ def verify_node(self): return info_messages def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "dup.hpp"'] def defines(self, var): + """Return defines.""" self.code_gen_dict["$DEFINES$"] = [] def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [] n_outputs = self.get_nodeattr("NumOutputStreams") out_streams = [] @@ -85,6 +93,7 @@ def docompute(self): self.code_gen_dict["$DOCOMPUTE$"] = [comp_call] def blackboxfunction(self): + """Return blackboxfunction.""" input_elem_hls_type = self.get_input_datatype().get_hls_datatype_str() pe = self.get_nodeattr("PE") in_stream = "hls::stream> &in0_V" % (input_elem_hls_type, pe) @@ -99,6 +108,7 @@ def blackboxfunction(self): self.code_gen_dict["$BLACKBOXFUNCTION$"] = [blackbox_hls] def pragmas(self): + """Return pragmas.""" pragmas = [] pragmas.append("#pragma HLS dataflow disable_start_propagation") pragmas.append("#pragma HLS INTERFACE axis port=in0_V") @@ -112,6 +122,7 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"] = pragmas def timeout_condition(self): + """Return timeout condition.""" condition = [] n_outputs = self.get_nodeattr("NumOutputStreams") for i in range(n_outputs): @@ -120,6 +131,7 @@ def timeout_condition(self): self.code_gen_dict["$TIMEOUT_CONDITION$"] = [condition] def timeout_read_stream(self): + """Return timeout read stream.""" read_stream_command = [] n_outputs = self.get_nodeattr("NumOutputStreams") for i in range(n_outputs): diff --git a/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py b/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py index 6372ac4af1..64d71406a5 100644 --- a/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/elementwise_binary_hls.py @@ -1022,6 +1022,8 @@ class ElementwiseAbsDiff_hls( ElementwiseBinaryOperation_hls, elementwise_binary.ElementwiseAbsDiff, ): + """HLS implementation of elementwise absolute diff operation.""" + pass diff --git a/src/finn/custom_op/fpgadataflow/hls/fmpadding_pixel_hls.py b/src/finn/custom_op/fpgadataflow/hls/fmpadding_pixel_hls.py index b85120172d..3b703848ba 100644 --- a/src/finn/custom_op/fpgadataflow/hls/fmpadding_pixel_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/fmpadding_pixel_hls.py @@ -26,24 +26,31 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for fmpadding pixel hls.""" from finn.custom_op.fpgadataflow.fmpadding_pixel import FMPadding_Pixel from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend class FMPadding_Pixel_hls(FMPadding_Pixel, HLSBackend): + """Class for FM Padding Pixel hls.""" + def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(FMPadding_Pixel.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "streamtools.h"'] def defines(self, var): + """Return defines.""" odim_h, odim_w = self.get_padded_odim() stride_h, stride_w = self.get_nodeattr("Stride") self.code_gen_dict["$DEFINES$"] = [ @@ -65,6 +72,7 @@ def defines(self, var): ] def docompute(self): + """Return docompute.""" in_t = self.get_input_datatype().get_hls_datatype_str() odim_h, odim_w = self.get_padded_odim() stride_h, stride_w = self.get_nodeattr("Stride") @@ -75,6 +83,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" packed_bits = self.get_instream_width() packed_hls_type = "ap_uint<%d>" % packed_bits self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ @@ -87,4 +96,5 @@ def blackboxfunction(self): ] def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) diff --git a/src/finn/custom_op/fpgadataflow/hls/globalaccpool_hls.py b/src/finn/custom_op/fpgadataflow/hls/globalaccpool_hls.py index 369d89cb30..ccb8c18341 100644 --- a/src/finn/custom_op/fpgadataflow/hls/globalaccpool_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/globalaccpool_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for globalaccpool hls.""" from finn.custom_op.fpgadataflow.globalaccpool import GlobalAccPool from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend @@ -34,15 +35,18 @@ class GlobalAccPool_hls(GlobalAccPool, HLSBackend): """Class that corresponds to finn-hlslib AccPool_Batch function.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(GlobalAccPool.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def verify_node(self): + """Verify node.""" info_messages = [] # verify that "backend" is set to "fpgadataflow" backend_value = self.get_nodeattr("backend") @@ -70,15 +74,19 @@ def verify_node(self): return info_messages def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "maxpool.h"'] def defines(self, var): + """Return defines.""" self.code_gen_dict["$DEFINES$"] = [] def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [ """AccPool_Batch<{}, {}, {}, {}, {}> (in0_V, out0_V, 1);""".format( self.get_normal_input_shape()[1], @@ -90,6 +98,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ f"""void {self.onnx_node.name}(hls::stream> &in0_V, hls::stream> &out0_V)""" diff --git a/src/finn/custom_op/fpgadataflow/hls/hwsoftmax_hls.py b/src/finn/custom_op/fpgadataflow/hls/hwsoftmax_hls.py index 5875dba172..2eaf5fb3fe 100644 --- a/src/finn/custom_op/fpgadataflow/hls/hwsoftmax_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/hwsoftmax_hls.py @@ -7,6 +7,7 @@ # @author Shane T. Fleming ############################################################################ +"""Module for hwsoftmax hls.""" import numpy as np from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend @@ -14,16 +15,21 @@ class HWSoftmax_hls(HWSoftmax, HLSBackend): + """Class for HW Softmax hls.""" + def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(HWSoftmax.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = [ "#include ", '#include "softmax.hpp"', @@ -31,6 +37,7 @@ def global_includes(self): ] def defines(self, var): + """Return defines.""" simd = self.get_nodeattr("SIMD") idtype = self.get_input_datatype() w = self.get_nodeattr("ifm_dim")[-1] @@ -44,6 +51,7 @@ def defines(self, var): ] def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [ """ static hls::stream> src0; @@ -57,6 +65,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ f""" void {self.onnx_node.name}( @@ -67,6 +76,7 @@ def blackboxfunction(self): ] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = [ """ #pragma HLS interface AXIS port=in0_V @@ -80,6 +90,7 @@ def pragmas(self): ] def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) def timeout_value(self): diff --git a/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py b/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py index 2d2617f695..a4c150ebfe 100644 --- a/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/iodma_hls.py @@ -27,6 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for iodma hls.""" import math import numpy as np from qonnx.core.datatype import DataType @@ -78,9 +79,11 @@ class IODMA_hls(HLSBackend, HWCustomOp): """Class that corresponds to finn-hlslib DMA function(s).""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "NumChannels": ("i", True, 0), # FINN input datatype @@ -104,15 +107,18 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" vecs = list(self.get_nodeattr("numInputVectors")) num_ch = self.get_nodeattr("NumChannels") ishape = tuple(vecs + [num_ch]) return ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" return self.get_normal_input_shape() def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" if self.get_nodeattr("direction") == "in": raise ValueError("Folded input shape not defined for input IODMA") shape = list(self.get_normal_input_shape()) @@ -127,6 +133,7 @@ def get_folded_input_shape(self, ind=0): return tuple(shape) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" if self.get_nodeattr("direction") == "out": raise ValueError("Folded output shape not defined for output IODMA") shape = list(self.get_normal_output_shape()) @@ -141,6 +148,7 @@ def get_folded_output_shape(self, ind=0): return tuple(shape) def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -162,6 +170,7 @@ def get_output_datatype(self, ind=0): return self.get_input_datatype() def get_instream_width(self, ind=0): + """Return instream width.""" if self.get_nodeattr("direction") == "in": return self.get_nodeattr("intfWidth") if self.get_nodeattr("direction") == "out": @@ -169,6 +178,7 @@ def get_instream_width(self, ind=0): raise ValueError("Invalid IODMA direction, please set to in or out") def get_outstream_width(self, ind=0): + """Return outstream width.""" if self.get_nodeattr("direction") == "out": return self.get_nodeattr("intfWidth") if self.get_nodeattr("direction") == "in": @@ -176,6 +186,7 @@ def get_outstream_width(self, ind=0): raise ValueError("Invalid IODMA direction, please set to in or out") def get_number_output_values(self): + """Return number output values.""" oshape = self.get_normal_output_shape() itype_bits = self.get_input_datatype().bitwidth() stream_width = self.get_nodeattr("streamWidth") @@ -186,10 +197,12 @@ def get_number_output_values(self): return ovalues def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "dma.h"'] self.code_gen_dict["$GLOBALS$"].append('#include "streamtools.h"') def defines(self, var): + """Return defines.""" itype_bits = self.get_input_datatype().bitwidth() total_bits = itype_bits * np.prod(self.get_normal_input_shape()) assert total_bits % 8 == 0, "DMA input not a multiple of 1 Byte" @@ -208,6 +221,7 @@ def get_ap_int_max_w(self): return width_lcm def docompute(self): + """Return docompute.""" direction = self.get_nodeattr("direction") mode = self.get_nodeattr("burstMode") dwc_func = "StreamingDataWidthConverter_Batch" @@ -316,6 +330,7 @@ def docompute(self): raise Exception("Unknown IODMA direction: %s" % direction) def blackboxfunction(self): + """Return blackboxfunction.""" packed_ibits = self.get_instream_width() packed_hls_type_in = "ap_uint<%d>" % packed_ibits packed_obits = self.get_outstream_width() @@ -343,6 +358,7 @@ def blackboxfunction(self): raise ValueError("Invalid IODMA direction, please set to in or out") def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = [ "#pragma HLS INTERFACE s_axilite port=numReps bundle=control" ] @@ -382,9 +398,11 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS DATAFLOW") def execute_node(self, context, graph): + """Execute node.""" pass def get_verilog_top_module_intf_names(self): + """Return verilog top module intf names.""" intf_names = super().get_verilog_top_module_intf_names() if self.get_nodeattr("direction") == "out": intf_names["m_axis"] = [] diff --git a/src/finn/custom_op/fpgadataflow/hls/labelselect_hls.py b/src/finn/custom_op/fpgadataflow/hls/labelselect_hls.py index 8c20f967bc..1b93ac2ddd 100644 --- a/src/finn/custom_op/fpgadataflow/hls/labelselect_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/labelselect_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for labelselect hls.""" import numpy as np from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend @@ -36,15 +37,18 @@ class LabelSelect_hls(LabelSelect, HLSBackend): """Class that corresponds to finn-hlslib LabelSelect_Batch function.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(LabelSelect.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def verify_node(self): + """Verify node.""" info_messages = [] # verify that "backend" is set to "fpgadataflow" backend_value = self.get_nodeattr("backend") @@ -74,6 +78,7 @@ def verify_node(self): return info_messages def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) # TopK ind output normally uses TensorProto.INT64, which # can cause issues for the node-by-node simulation in FINN @@ -84,12 +89,15 @@ def execute_node(self, context, graph): context[outp] = ret.astype(np.int64) def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "maxpool.h"'] def defines(self, var): + """Return defines.""" self.code_gen_dict["$DEFINES$"] = [] def read_npy_data(self): + """Return read npy data.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") dtype = self.get_input_datatype() elem_bits = dtype.bitwidth() @@ -116,6 +124,7 @@ def read_npy_data(self): ) def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [ """LabelSelect_Batch<{}, {}, {}, {}, {} > (in0_V, out0_V, 1);""".format( self.get_nodeattr("Labels"), @@ -127,6 +136,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ """void {}(hls::stream> &in0_V, hls::stream > &out0_V)""".format( diff --git a/src/finn/custom_op/fpgadataflow/hls/layernorm_hls.py b/src/finn/custom_op/fpgadataflow/hls/layernorm_hls.py index ca4f45df7b..7b517b3acd 100644 --- a/src/finn/custom_op/fpgadataflow/hls/layernorm_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/layernorm_hls.py @@ -6,6 +6,7 @@ # ############################################################################ +"""Module for layernorm hls.""" import numpy as np from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend @@ -13,10 +14,14 @@ class LayerNorm_hls(LayerNorm, HLSBackend): + """Class for Layer Norm hls.""" + def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(LayerNorm.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) @@ -29,12 +34,14 @@ def get_nodeattr_types(self): return my_attrs def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = [ "#include ", '#include "layernorm.hpp"', ] def defines(self, var): + """Return defines.""" simd = self.get_nodeattr("SIMD") idtype = self.get_input_datatype() n = self.get_nodeattr("ifm_dim")[-1] @@ -48,9 +55,11 @@ def defines(self, var): ] def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = ["layernorm(in0_V, out0_V);"] def blackboxfunction(self): + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ f""" void {self.onnx_node.name}( @@ -61,6 +70,7 @@ def blackboxfunction(self): ] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = [ """ #pragma HLS interface AXIS port=in0_V @@ -74,6 +84,7 @@ def pragmas(self): ] def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) def timeout_value(self): diff --git a/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py b/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py index f55a90c3c8..ce54d4a5f8 100644 --- a/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/lookup_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for lookup hls.""" import numpy as np from math import ceil, log2 from qonnx.core.datatype import DataType @@ -39,15 +40,18 @@ class Lookup_hls(Lookup, HLSBackend): """Streaming elementwise HLS lookup, mapping indices to values.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(Lookup.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def global_includes(self): + """Return global includes.""" mem_mode = self.get_nodeattr("mem_mode") global_incls = [] global_incls.append('#include "lookup.hpp"') @@ -56,6 +60,7 @@ def global_includes(self): self.code_gen_dict["$GLOBALS$"] = global_incls def defines(self, var): + """Return defines.""" n_inputs = np.prod(self.get_folded_input_shape()[:-1]) dtype = self.get_input_datatype() elem_hls_type = dtype.get_hls_datatype_str() @@ -82,6 +87,7 @@ def defines(self, var): self.code_gen_dict["$DEFINES$"] = my_defines def dataoutstrm(self): + """Return dataoutstrm.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") dtype = self.get_output_datatype() if dtype == DataType["BIPOLAR"]: @@ -110,6 +116,7 @@ def dataoutstrm(self): ] def docompute(self): + """Return docompute.""" mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "internal_embedded": self.code_gen_dict["$DOCOMPUTE$"] = [ @@ -123,6 +130,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" mem_mode = self.get_nodeattr("mem_mode") ibits = self.get_instream_width() packed_input_hls_type = "ap_uint<%d>" % ibits @@ -147,6 +155,7 @@ def blackboxfunction(self): ] def pragmas(self): + """Return pragmas.""" mem_mode = self.get_nodeattr("mem_mode") my_pragmas = ["#pragma HLS INTERFACE axis port=in0_V"] my_pragmas.append("#pragma HLS INTERFACE axis port=out0_V") @@ -164,6 +173,7 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"] = my_pragmas def generate_params(self, model, path): + """Generate params.""" mem_mode = self.get_nodeattr("mem_mode") embeddings = model.get_initializer(self.onnx_node.input[1]) if mem_mode == "internal_embedded": @@ -211,6 +221,7 @@ def generate_params(self, model, path): raise Exception("Unrecognized mem_mode: " + mem_mode) def execute_node(self, context, graph): + """Execute node.""" mem_mode = self.get_nodeattr("mem_mode") assert ( mem_mode == "internal_embedded" @@ -218,6 +229,7 @@ def execute_node(self, context, graph): HLSBackend.execute_node(self, context, graph) def get_ap_int_max_w(self): + """Return ap int max w.""" parent_max = super().get_ap_int_max_w() mem_mode = self.get_nodeattr("mem_mode") ext_mem_width = self.get_nodeattr("ext_mem_width") diff --git a/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py b/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py index 73573ca770..d207b49afd 100644 --- a/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/matrixvectoractivation_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for matrixvectoractivation hls.""" import math import numpy as np import os @@ -52,9 +53,11 @@ class MVAU_hls(MVAU, HLSBackend): """Corresponds to finn-hlslib MatrixVectorActivation_Batch function.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(MVAU.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) @@ -63,12 +66,12 @@ def get_nodeattr_types(self): return my_attrs def lut_estimation(self): - """Calculates resource estimations for LUTs based on: + """Calculate resource estimations for LUTs based on: - FINN-R: An End-to-End Deep-Learning Framework for Fast Exploration of Quantized Neural Networks - M. Blott, T. B. Preusser, N. J. Fraser, G. Gambardella, K. O'Brien, Y. Umuroglu, M. Leeser and K. Vissers - - 12. Sep 2018 + - 12. Sep 2018. """ # TODO add in/out FIFO contributions P = self.get_nodeattr("PE") @@ -127,6 +130,7 @@ def lut_estimation(self): def dsp_estimation(self, fpgapart): # multiplication + """Return dsp estimation.""" P = self.get_nodeattr("PE") res_type = self.get_nodeattr("resType") Q = self.get_nodeattr("SIMD") @@ -159,7 +163,7 @@ def code_generation_ipgen(self, model, fpgapart, clk): self.generate_hdl_fetch_weights(fpgapart) def get_template_param_values(self): - """Returns the template parameter values according to input, output and weight + """Return the template parameter values according to input, output and weight data types.""" ret = dict() inp_hls_str = self.get_input_datatype(0).get_hls_datatype_str() @@ -198,6 +202,7 @@ def get_template_param_values(self): return ret def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "weights.hpp"'] self.code_gen_dict["$GLOBALS$"] += ['#include "activations.hpp"'] @@ -214,6 +219,7 @@ def global_includes(self): def defines(self, var): # Only ipgen mode: Make sure that SIMD parameter satisfies minimum requirements. + """Return defines.""" if var == "ipgen": SIMD = self.get_nodeattr("SIMD") MW = self.get_nodeattr("MW") @@ -249,6 +255,7 @@ def defines(self, var): self.code_gen_dict["$DEFINES$"].append(f"#define WP1 {wdt.bitwidth()}\n") def read_npy_data(self): + """Return read npy data.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") dtype = self.get_input_datatype(0) if dtype == DataType["BIPOLAR"]: @@ -301,6 +308,7 @@ def read_npy_data(self): ) def strm_decl(self): + """Return strm decl.""" mem_mode = self.get_nodeattr("mem_mode") self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( @@ -323,6 +331,7 @@ def strm_decl(self): ) def docompute(self): + """Return docompute.""" mem_mode = self.get_nodeattr("mem_mode") map_to_hls_mult_style = { "auto": "ap_resource_dflt()", @@ -376,6 +385,7 @@ def docompute(self): ) def dataoutstrm(self): + """Return dataoutstrm.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") dtype = self.get_output_datatype() if dtype == DataType["BIPOLAR"]: @@ -404,9 +414,11 @@ def dataoutstrm(self): ] def save_as_npy(self): + """Save as npy.""" self.code_gen_dict["$SAVEASCNPY$"] = [] def blackboxfunction(self): + """Return blackboxfunction.""" mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "internal_embedded": self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ @@ -438,6 +450,7 @@ def blackboxfunction(self): ) def pragmas(self): + """Return pragmas.""" mem_mode = self.get_nodeattr("mem_mode") ram_style_thresholds = self.get_nodeattr("ram_style_thresholds") self.code_gen_dict["$PRAGMAS$"] = ["#pragma HLS INTERFACE axis port=in0_V"] @@ -492,6 +505,7 @@ def pragmas(self): def get_ap_int_max_w(self): # base class impl (max of inp/out stream widths) + """Return ap int max w.""" max_of_io = super().get_ap_int_max_w() # internal_decoupled mode weight stream weightstream = self.get_instream_width(1) @@ -504,6 +518,7 @@ def get_ap_int_max_w(self): return max([weightstream, max_of_io, single_pe_w]) def execute_node(self, context, graph): + """Execute node.""" mode = self.get_nodeattr("exec_mode") dynamic_input = self.get_nodeattr("dynamic_input") mem_mode = self.get_nodeattr("mem_mode") @@ -678,6 +693,7 @@ def minimize_weight_bit_width(self, model: "ModelWrapper") -> BaseDataType: def instantiate_ip(self, cmd): # instantiate the HLS IP + """Return instantiate ip.""" vlnv = self.get_nodeattr("ip_vlnv") node_name = self.onnx_node.name if self.get_nodeattr("mem_mode") == "internal_decoupled" or self.get_nodeattr( diff --git a/src/finn/custom_op/fpgadataflow/hls/outer_shuffle_hls.py b/src/finn/custom_op/fpgadataflow/hls/outer_shuffle_hls.py index b1f2781a76..1e7b44381d 100644 --- a/src/finn/custom_op/fpgadataflow/hls/outer_shuffle_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/outer_shuffle_hls.py @@ -7,6 +7,7 @@ # @author Shane T. Fleming ############################################################################ +"""Module for outer shuffle hls.""" import math import numpy as np @@ -40,7 +41,10 @@ def auto_size_simd(I_dim: int, SIMD: int) -> int | None: class OuterShuffle_hls(OuterShuffle, HLSBackend): + """Class for Outer Shuffle hls.""" + def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) # check some constraints that it is a legal shuffle_hls @@ -54,9 +58,11 @@ def __init__(self, onnx_node, **kwargs): raise RuntimeError("Unable to determine a new SIMD value for this transpose.") def get_nodeattr_types(self): + """Return nodeattr types.""" return OuterShuffle.get_nodeattr_types(self) | HLSBackend.get_nodeattr_types(self) def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = [ '#include "input_gen.hpp"', "#include ", @@ -65,6 +71,7 @@ def global_includes(self): ] def defines(self, var): + """Return defines.""" simd = self.get_nodeattr("SIMD") dtype = self.get_input_datatype() self.code_gen_dict["$DEFINES$"] = [ @@ -76,6 +83,7 @@ def defines(self, var): ] def docompute(self): + """Return docompute.""" simd = self.get_nodeattr("SIMD") out_shape = self.get_nodeattr("transpose_out_shape") out_shape[-1] = int(out_shape[-1] / simd) @@ -96,6 +104,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ f""" void {self.onnx_node.name} ( @@ -106,6 +115,7 @@ def blackboxfunction(self): ] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = [ """ #pragma HLS interface AXIS port=in0_V @@ -119,6 +129,7 @@ def pragmas(self): ] def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) def timeout_value(self): diff --git a/src/finn/custom_op/fpgadataflow/hls/replicate_stream_hls.py b/src/finn/custom_op/fpgadataflow/hls/replicate_stream_hls.py index 72c8315eb8..108abb28bb 100644 --- a/src/finn/custom_op/fpgadataflow/hls/replicate_stream_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/replicate_stream_hls.py @@ -3,6 +3,7 @@ # per line. Black, however, formats some lines going beyond this. # Numpy math and arrays +"""Module for replicate stream hls.""" import numpy as np # Base class for specializing HW operators as implemented via HLS @@ -17,8 +18,10 @@ class ReplicateStream_hls( # noqa: Class name does not follow ReplicateStream, HLSBackend ): # Node attributes matching the HLS operator + """Class for Replicate Stream hls.""" def get_nodeattr_types(self): # Start from parent operator class attributes + """Return nodeattr types.""" attrs = ReplicateStream.get_nodeattr_types(self) # Add the HLSBackend default attributes on top attrs.update(HLSBackend.get_nodeattr_types(self)) @@ -30,6 +33,7 @@ def get_nodeattr_types(self): def get_ap_int_max_w(self): # Find the widths of the widest input # Note: There is just one input. + """Return ap int max w.""" i_bits_max = self.get_instream_width(ind=0) # Find the widths of the widest output # Note: there is one output per replica @@ -45,11 +49,13 @@ def get_ap_int_max_w(self): # code def global_includes(self): # Currently nothing to include + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = [] # Generates C++ code of type alias, global constant and macro definitions def defines(self, var): # Insert constants and type aliases into the dictionary + """Return defines.""" self.code_gen_dict["$DEFINES$"] = [ # Input and output element datatypes f"using IType = {self.dtype.get_hls_datatype_str()};", @@ -73,7 +79,9 @@ def defines(self, var): # Generates C++ code for calling the computation part of the operator def docompute(self): # Generates the name of the ith output stream + """Return docompute.""" def out(i): + """Return out.""" return f"out{i}_{self.hls_sname()}" # Number of iterations required to process the whole folded input stream @@ -100,6 +108,7 @@ def out(i): def blackboxfunction(self): # Insert function head describing the top level interface of the stream # replicating operator + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ # @formatter:off Prevent Python formatter from messing with C++ # formatting @@ -119,6 +128,7 @@ def blackboxfunction(self): def pragmas(self): # Add HLS interface directives specifying how to create RTL ports for # the top-level function arguments + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = [ # Connect the input stream with an axi stream interface f"#pragma HLS INTERFACE axis port=in0_{self.hls_sname()}" @@ -138,6 +148,7 @@ def pragmas(self): def get_verilog_top_module_intf_names(self): # Start collecting interface names in a dictionary # starting with clock and reset + """Return verilog top module intf names.""" intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} # AXI stream input interfaces intf_names["s_axis"] = [ diff --git a/src/finn/custom_op/fpgadataflow/hls/requant_hls.py b/src/finn/custom_op/fpgadataflow/hls/requant_hls.py index a8c90fb452..ca11f17fc6 100644 --- a/src/finn/custom_op/fpgadataflow/hls/requant_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/requant_hls.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Module for requant hls.""" import numpy as np import os import warnings @@ -28,9 +29,11 @@ class Requant_hls(Requant, HLSBackend): """ def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(Requant.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) @@ -81,6 +84,7 @@ def generate_params(self, model, path): f.write("};\n") def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = [ "#include ", "#include ", @@ -90,6 +94,7 @@ def global_includes(self): ] def defines(self, var): + """Return defines.""" pe = self.get_nodeattr("PE") num_channels = self.get_nodeattr("NumChannels") cf = num_channels // pe @@ -139,6 +144,7 @@ def defines(self, var): def docompute(self): # Get optimization flags (set during generate_params) + """Return docompute.""" scale_is_one = getattr(self, "_scale_is_one", False) bias_is_zero = getattr(self, "_bias_is_zero", False) @@ -194,6 +200,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ f""" void {self.onnx_node.name}( @@ -242,6 +249,7 @@ def save_as_npy(self): self.code_gen_dict["$SAVEASCNPY$"] = [] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = [ """ #pragma HLS interface AXIS port=in0_V diff --git a/src/finn/custom_op/fpgadataflow/hls/split_hls.py b/src/finn/custom_op/fpgadataflow/hls/split_hls.py index 6bdae1f304..bdcb3ccda9 100644 --- a/src/finn/custom_op/fpgadataflow/hls/split_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/split_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for split hls.""" from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.split import StreamingSplit @@ -35,21 +36,26 @@ class StreamingSplit_hls(StreamingSplit, HLSBackend): Only supports splitting along the last axis.""" def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(StreamingSplit.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "split.hpp"'] def defines(self, var): + """Return defines.""" self.code_gen_dict["$DEFINES$"] = [] def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [] n_outputs = self.get_n_outputs() output_folds = [str(self.get_folded_output_shape(i)[-2]) for i in range(n_outputs)] @@ -62,6 +68,7 @@ def docompute(self): self.code_gen_dict["$DOCOMPUTE$"] = [comp_call] def blackboxfunction(self): + """Return blackboxfunction.""" input_elem_hls_type = self.get_input_datatype().get_hls_datatype_str() simd = self.get_nodeattr("SIMD") in_stream = "hls::stream> &in0_V" % (input_elem_hls_type, simd) @@ -75,6 +82,7 @@ def blackboxfunction(self): self.code_gen_dict["$BLACKBOXFUNCTION$"] = [blackbox_hls] def pragmas(self): + """Return pragmas.""" pragmas = [] pragmas.append("#pragma HLS INTERFACE axis port=in0_V") for i in range(self.get_n_outputs()): @@ -86,6 +94,7 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"] = pragmas def timeout_condition(self): + """Return timeout condition.""" condition = [] for i in range(self.get_n_outputs()): condition.append(f"out{i}_V.empty()") @@ -93,6 +102,7 @@ def timeout_condition(self): self.code_gen_dict["$TIMEOUT_CONDITION$"] = [condition] def timeout_read_stream(self): + """Return timeout read stream.""" read_stream_command = [] for i in range(self.get_n_outputs()): read_stream_command.append( diff --git a/src/finn/custom_op/fpgadataflow/hls/streamingdatawidthconverter_hls.py b/src/finn/custom_op/fpgadataflow/hls/streamingdatawidthconverter_hls.py index b1ea6ab510..175b014611 100644 --- a/src/finn/custom_op/fpgadataflow/hls/streamingdatawidthconverter_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/streamingdatawidthconverter_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for streamingdatawidthconverter hls.""" import numpy as np from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend @@ -40,15 +41,18 @@ class StreamingDataWidthConverter_hls(StreamingDataWidthConverter, HLSBackend): function.""" def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(StreamingDataWidthConverter.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "streamtools.h"'] def defines(self, var): + """Return defines.""" numReps = 1 numInWords = int(np.prod(self.get_folded_input_shape()[:-1])) inWidth = self.get_nodeattr("inWidth") @@ -67,6 +71,7 @@ def defines(self, var): self.code_gen_dict["$DEFINES$"].append("#define NumLCMToOut %d" % (numLCMToOut)) def strm_decl(self): + """Return strm decl.""" self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( f'hls::stream> in0_V ("in0_V");' @@ -77,6 +82,7 @@ def strm_decl(self): def docompute(self): # TODO continue with fxns below, they are copy-pasted + """Return docompute.""" op = "StreamingDataWidthConverter_Batch" if self.needs_lcm(): self.code_gen_dict["$DOCOMPUTE$"] = [ @@ -90,6 +96,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" in_packed_bits = self.get_instream_width() in_packed_hls_type = "ap_uint<%d>" % in_packed_bits out_packed_bits = self.get_outstream_width() @@ -104,6 +111,7 @@ def blackboxfunction(self): ] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = ["#pragma HLS INTERFACE axis port=in0_V"] self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE axis port=out0_V") self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE ap_ctrl_none port=return") @@ -111,6 +119,7 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS DATAFLOW disable_start_propagation") def execute_node(self, context, graph): + """Execute node.""" mode = self.get_nodeattr("exec_mode") if mode == "cppsim": exp_shape = self.get_normal_input_shape() diff --git a/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py b/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py index 62210ec733..5558d3cffe 100644 --- a/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/streamingfifo_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for streamingfifo hls.""" import numpy as np import os from qonnx.core.datatype import DataType @@ -39,6 +40,7 @@ class StreamingFIFO_hls(StreamingFIFO, HLSBackend): """HLS-based FIFO implementation. Currently only used as virtual FIFO for live FIFO-sizing.""" def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { # Only purpose of this CustomOp for now: virtual FIFO for live FIFO-sizing "impl_style": ("s", False, "virtual", {"virtual"}), @@ -52,6 +54,7 @@ def global_includes(self) -> None: self.code_gen_dict["$GLOBALS$"] = ['#include "virtual_fifo.hpp"'] def defines(self, var) -> None: + """Return defines.""" numReps = 1 width = self.get_instream_width() self.code_gen_dict["$DEFINES$"] = [ @@ -60,6 +63,7 @@ def defines(self, var) -> None: ] def strm_decl(self): + """Return strm decl.""" self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( f"hls::stream> " @@ -71,6 +75,7 @@ def strm_decl(self): ) def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [ f""" #pragma HLS dataflow disable_start_propagation @@ -92,6 +97,7 @@ def docompute(self): ] def blackboxfunction(self): + """Return blackboxfunction.""" in_packed_bits = self.get_instream_width() in_packed_hls_type = f"ap_uint<{in_packed_bits}>" out_packed_bits = self.get_outstream_width() @@ -111,6 +117,7 @@ def blackboxfunction(self): ] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = [ "#pragma HLS INTERFACE axis port=in0_" + self.hls_sname() ] @@ -125,11 +132,13 @@ def pragmas(self): def get_verilog_top_module_intf_names(self): # Overload default HWCustomOp implementation to add axilite control IF + """Return verilog top module intf names.""" intf_names = super().get_verilog_top_module_intf_names() intf_names["axilite"] = ["s_axi_control"] return intf_names def execute_node(self, context, graph): + """Execute node.""" mode = self.get_nodeattr("exec_mode") node = self.onnx_node exp_shape = self.get_normal_input_shape() diff --git a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py index 34f93705af..8f0221f552 100644 --- a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py @@ -27,6 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for tlastmarker hls.""" from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp @@ -40,9 +41,11 @@ class TLastMarker_hls(HLSBackend, HWCustomOp): from DMA read.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { # number of (static) iterations until TLAST=1 is generated for Direction=out "NumIters": ("i", True, 0), @@ -68,6 +71,7 @@ def execute_node(self, context, graph): # of the current image/input sample. when executing # inside FINN as a single node, this is not visible. # so here we simply return the input as output + """Execute node.""" i_name = self.onnx_node.input[0] o_name = self.onnx_node.output[0] i_tensor = context[i_name] @@ -75,16 +79,20 @@ def execute_node(self, context, graph): def make_shape_compatible_op(self, model): # not supported for shape inference + """Create shape compatible op.""" pass def infer_node_datatype(self, model): # not supported for datatype inference + """Infer node datatype.""" pass def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "ap_axi_sdata.h"'] def defines(self, var): + """Return defines.""" stream_width = self.get_nodeattr("StreamWidth") direction = self.get_nodeattr("Direction") protocol = self.get_nodeattr("Protocol") @@ -117,9 +125,11 @@ def defines(self, var): ] def read_npy_data(self): + """Return read npy data.""" self.code_gen_dict["$READNPYDATA$"] = [] def docompute(self): + """Return docompute.""" dyn_iters = self.get_nodeattr("DynIters") direction = self.get_nodeattr("Direction") use_qdma_axis = self.get_nodeattr("Protocol") == "external" @@ -175,9 +185,11 @@ def docompute(self): ] def dataoutstrm(self): + """Return dataoutstrm.""" self.code_gen_dict["$DATAOUTSTREAM$"] = [] def blackboxfunction(self): + """Return blackboxfunction.""" dyn_iters = self.get_nodeattr("DynIters") if dyn_iters == 1: @@ -194,6 +206,7 @@ def blackboxfunction(self): ] def pragmas(self): + """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = ["#pragma HLS INTERFACE axis port=in0_V"] self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE axis port=out0_V") @@ -206,25 +219,31 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE ap_ctrl_none port=return") def get_number_output_values(self): + """Return number output values.""" return self.get_nodeattr("NumIters") def get_input_datatype(self, ind=0): # not supported + """Return input datatype.""" raise Exception("get_input_datatype not implemented for TlastMarker") def get_output_datatype(self, ind=0): # not supported + """Return output datatype.""" raise Exception("get_output_datatype not implemented for TlastMarker") def get_normal_input_shape(self, ind=0): # not supported + """Return normal input shape.""" raise Exception("get_normal_input_shape not implemented for TlastMarker") def get_normal_output_shape(self, ind=0): # not supported + """Return normal output shape.""" raise Exception("get_normal_input_shape not implemented for TlastMarker") def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" stream_width = self.get_nodeattr("StreamWidth") elem_width = self.get_nodeattr("ElemWidth") n_packed_elems = stream_width // elem_width @@ -232,17 +251,21 @@ def get_folded_input_shape(self, ind=0): return (1, n_iters, n_packed_elems) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" return self.get_folded_input_shape() def get_instream_width(self, ind=0): + """Return instream width.""" stream_width = self.get_nodeattr("StreamWidth") return stream_width def get_outstream_width(self, ind=0): + """Return outstream width.""" stream_width = self.get_nodeattr("StreamWidth") return stream_width def strm_decl(self): + """Return strm decl.""" self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append('hls::stream in0_V ("in0_V");') self.code_gen_dict["$STREAMDECLARATIONS$"].append( @@ -250,6 +273,7 @@ def strm_decl(self): ) def get_verilog_top_module_intf_names(self): + """Return verilog top module intf names.""" intf_names = super().get_verilog_top_module_intf_names() stream_width = self.get_nodeattr("StreamWidth") intf_names["s_axis"] = [("in0_V", stream_width)] diff --git a/src/finn/custom_op/fpgadataflow/hls/upsampler_hls.py b/src/finn/custom_op/fpgadataflow/hls/upsampler_hls.py index 53dca68359..ca71822275 100644 --- a/src/finn/custom_op/fpgadataflow/hls/upsampler_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/upsampler_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for upsampler hls.""" from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.upsampler import UpsampleNearestNeighbour @@ -37,18 +38,22 @@ class UpsampleNearestNeighbour_hls(UpsampleNearestNeighbour, HLSBackend): """ def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(UpsampleNearestNeighbour.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "upsample.hpp"'] def defines(self, var): + """Return defines.""" self.code_gen_dict["$DEFINES$"] = [] HI = self.get_nodeattr("HI") @@ -69,11 +74,13 @@ def defines(self, var): self.code_gen_dict["$DEFINES$"] += [f"#define CF {CF}"] def docompute(self): + """Return docompute.""" self.code_gen_dict["$DOCOMPUTE$"] = [ """upsample_nn(in0_V, out0_V);""" ] def blackboxfunction(self): + """Return blackboxfunction.""" simd = self.get_nodeattr("SIMD") input_elem_hls_type = self.get_input_datatype().get_hls_datatype_str() output_elem_hls_type = self.get_output_datatype().get_hls_datatype_str() @@ -90,4 +97,5 @@ def blackboxfunction(self): ] def execute_node(self, context, graph): + """Execute node.""" HLSBackend.execute_node(self, context, graph) diff --git a/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py b/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py index 67f671dc39..be5e270bd8 100644 --- a/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/vectorvectoractivation_hls.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for vectorvectoractivation hls.""" import math import numpy as np import os @@ -41,9 +42,11 @@ class VVAU_hls(VVAU, HLSBackend): """Corresponds to finn-hlslib Vector_Vector_Activate_Batch function""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(VVAU.get_nodeattr_types(self)) my_attrs.update(HLSBackend.get_nodeattr_types(self)) @@ -115,6 +118,7 @@ def lut_estimation(self): def dsp_estimation(self, fpgapart): # multiplication + """Return dsp estimation.""" P = self.get_nodeattr("PE") res_type = self.get_nodeattr("resType") wdt = self.get_input_datatype(1) @@ -128,6 +132,7 @@ def dsp_estimation(self, fpgapart): return int(mult_dsp) def execute_node(self, context, graph): + """Execute node.""" mode = self.get_nodeattr("exec_mode") mem_mode = self.get_nodeattr("mem_mode") node = self.onnx_node @@ -285,6 +290,7 @@ def get_template_param_values(self): return ret def global_includes(self): + """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "weights.hpp"'] self.code_gen_dict["$GLOBALS$"] += ['#include "activations.hpp"'] mem_mode = self.get_nodeattr("mem_mode") @@ -297,6 +303,7 @@ def global_includes(self): self.code_gen_dict["$GLOBALS$"] += ['#include "thresh.h"'] def defines(self, var): + """Return defines.""" dim_h, dim_w = self.get_nodeattr("Dim") numReps = 1 * dim_h * dim_w k_h, k_w = self.get_nodeattr("Kernel") @@ -318,6 +325,7 @@ def defines(self, var): self.code_gen_dict["$DEFINES$"].append(f"#define WP1 {wdt.bitwidth()}\n") def read_npy_data(self): + """Return read npy data.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") dtype = self.get_input_datatype(0) if dtype == DataType["BIPOLAR"]: @@ -364,6 +372,7 @@ def read_npy_data(self): ) def strm_decl(self): + """Return strm decl.""" mem_mode = self.get_nodeattr("mem_mode") self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append( @@ -378,6 +387,7 @@ def strm_decl(self): ) def docompute(self): + """Return docompute.""" mem_mode = self.get_nodeattr("mem_mode") map_to_hls_mult_style = { "auto": "ap_resource_dflt()", @@ -428,6 +438,7 @@ def docompute(self): ) def dataoutstrm(self): + """Return dataoutstrm.""" code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") dtype = self.get_output_datatype() if dtype == DataType["BIPOLAR"]: @@ -456,9 +467,11 @@ def dataoutstrm(self): ] def save_as_npy(self) -> None: + """Save as npy.""" self.code_gen_dict["$SAVEASCNPY$"] = [] def blackboxfunction(self) -> None: + """Return blackboxfunction.""" mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "internal_embedded": self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ @@ -482,6 +495,7 @@ def blackboxfunction(self) -> None: ) def pragmas(self) -> None: + """Return pragmas.""" mem_mode = self.get_nodeattr("mem_mode") self.code_gen_dict["$PRAGMAS$"] = ["#pragma HLS INTERFACE axis port=in0_V"] self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE axis port=out0_V") @@ -568,6 +582,7 @@ def minimize_weight_bit_width(self, model) -> BaseDataType: def instantiate_ip(self, cmd): # instantiate the HLS IP + """Return instantiate ip.""" vlnv = self.get_nodeattr("ip_vlnv") node_name = self.onnx_node.name if self.get_nodeattr("mem_mode") == "internal_decoupled": diff --git a/src/finn/custom_op/fpgadataflow/hwsoftmax.py b/src/finn/custom_op/fpgadataflow/hwsoftmax.py index db734fcbd3..4019174551 100644 --- a/src/finn/custom_op/fpgadataflow/hwsoftmax.py +++ b/src/finn/custom_op/fpgadataflow/hwsoftmax.py @@ -7,6 +7,7 @@ # @author Shane T. Fleming ############################################################################ +"""Module for hwsoftmax.""" from qonnx.core.datatype import DataType from scipy.special import softmax @@ -18,9 +19,11 @@ class HWSoftmax(HWCustomOp): """Abstraction layer for HW implementation of SoftMax layers.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "ifm_dim": ("ints", True, []), "SIMD": ("i", False, 1), @@ -32,12 +35,15 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" return self.get_nodeattr("ifm_dim") def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" return self.get_normal_input_shape() def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node input_data = context[node.input[0]] output_data = softmax(input_data, axis=-1) @@ -52,6 +58,7 @@ def get_input_datatype(self, ind=0): return data_type def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -68,11 +75,13 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], odt) def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return ibits * simd def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return obits * simd @@ -82,9 +91,11 @@ def get_output_datatype(self, ind=0): return DataType["FLOAT32"] def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" return self.get_folded_input_shape() def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" normal_ishape = list(self.get_normal_input_shape()) simd = self.get_nodeattr("SIMD") assert normal_ishape[-1] % simd == 0, "SIMD must divide into input dimension" diff --git a/src/finn/custom_op/fpgadataflow/inner_shuffle.py b/src/finn/custom_op/fpgadataflow/inner_shuffle.py index 02bc9ef03b..83f18abceb 100644 --- a/src/finn/custom_op/fpgadataflow/inner_shuffle.py +++ b/src/finn/custom_op/fpgadataflow/inner_shuffle.py @@ -6,6 +6,7 @@ # # @author Shane T. Fleming ############################################################################ +"""Module for inner shuffle.""" import numpy as np from qonnx.core.datatype import DataType @@ -17,9 +18,11 @@ class InnerShuffle(HWCustomOp): """Abstraction layer for the Parallel 2D transpose.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "data_type": ("s", True, ""), "in_shape": ("ints", True, []), # Needs to be len==2 can we assert that somewhere? @@ -31,13 +34,16 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" return self.get_nodeattr("in_shape") def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" ishape = tuple(self.get_normal_input_shape()) return ishape[:-2] + (ishape[-1], ishape[-2]) def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node input_data = context[node.input[0]] assert len(input_data.shape) >= 2, "InnerShuffle HWCustomOp requires at least 2D input" @@ -48,10 +54,12 @@ def execute_node(self, context, graph): context[node.output[0]] = transposed def get_input_datatype(self, ind=0): + """Return input datatype.""" data_type = DataType[self.get_nodeattr("data_type")] return data_type def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node dt = model.get_tensor_datatype(node.input[0]) if dt != self.get_input_datatype(): @@ -63,20 +71,24 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], dt) def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return ibits * simd def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return obits * simd def get_output_datatype(self, ind=0): + """Return output datatype.""" data_type = DataType[self.get_nodeattr("data_type")] return data_type def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" normal_oshape = list(self.get_normal_output_shape()) simd = self.get_nodeattr("SIMD") assert normal_oshape[-1] % simd == 0, "SIMD must divide into the innermost output dimension" @@ -85,6 +97,7 @@ def get_folded_output_shape(self, ind=0): return tuple(folded_oshape) def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" normal_ishape = list(self.get_normal_input_shape()) simd = self.get_nodeattr("SIMD") fold = int(np.prod(normal_ishape) / simd) diff --git a/src/finn/custom_op/fpgadataflow/labelselect.py b/src/finn/custom_op/fpgadataflow/labelselect.py index f925b51652..fa1848f7ec 100644 --- a/src/finn/custom_op/fpgadataflow/labelselect.py +++ b/src/finn/custom_op/fpgadataflow/labelselect.py @@ -25,6 +25,7 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for labelselect.""" import numpy as np import onnxruntime as rt from onnx import TensorProto, helper @@ -38,6 +39,7 @@ class LabelSelect(HWCustomOp): """Abstraction layer for HW implementation of LabelSelect""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) odt_name = self.get_nodeattr("outputDataType") if odt_name == "": @@ -52,6 +54,7 @@ def __init__(self, onnx_node, **kwargs): self.set_nodeattr("outputDataType", odt_name) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "Labels": ("i", True, 0), "PE": ("i", True, 0), @@ -69,12 +72,14 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" nlabels = self.get_nodeattr("Labels") vecs = list(self.get_nodeattr("numInputVectors")) ishape = tuple(vecs + [nlabels]) return ishape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" nlabels = self.get_nodeattr("Labels") pe = self.get_nodeattr("PE") vecs = list(self.get_nodeattr("numInputVectors")) @@ -84,18 +89,21 @@ def get_folded_input_shape(self, ind=0): return folded_ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" k = self.get_nodeattr("K") vecs = list(self.get_nodeattr("numInputVectors")) oshape = tuple(vecs + [k]) return oshape def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" k = self.get_nodeattr("K") vecs = list(self.get_nodeattr("numInputVectors")) oshape = tuple(vecs + [k, 1]) return oshape def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node # check input datatype against property idt = model.get_tensor_datatype(node.input[0]) @@ -126,10 +134,12 @@ def get_outstream_width(self, ind=0): return self.get_output_datatype().bitwidth() def get_number_output_values(self): + """Return number output values.""" return self.get_nodeattr("K") def execute_node(self, context, graph): # create a standard add node to help calculate the result + """Execute node.""" node = self.onnx_node k = self.get_nodeattr("K") @@ -159,6 +169,7 @@ def execute_node(self, context, graph): context[node.output[0]] = np.asarray(result[1], dtype=np.float32).reshape(oshape) def get_exp_cycles(self): + """Return exp cycles.""" nlabels = self.get_nodeattr("Labels") pe = self.get_nodeattr("PE") exp_cycles = nlabels / pe diff --git a/src/finn/custom_op/fpgadataflow/layernorm.py b/src/finn/custom_op/fpgadataflow/layernorm.py index dc66a7bbe1..aecf56431f 100644 --- a/src/finn/custom_op/fpgadataflow/layernorm.py +++ b/src/finn/custom_op/fpgadataflow/layernorm.py @@ -10,6 +10,7 @@ # ################################################################################### +"""Module for layernorm.""" import numpy as np import torch import torch.nn.functional as F @@ -23,9 +24,11 @@ class LayerNorm(HWCustomOp): """Abstraction layer for HW implementation of the LayerNorm layer.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = super().get_nodeattr_types() my_attrs.update( { @@ -40,6 +43,7 @@ def get_nodeattr_types(self): return my_attrs def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node # Get tensor values in_values = context[node.input[0]] @@ -53,12 +57,15 @@ def execute_node(self, context, graph): context[node.output[0]] = np.asarray(out_act, dtype=np.float32).reshape(oshape) def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" return self.get_nodeattr("ifm_dim") def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" return self.get_normal_input_shape() def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" normal_ishape = list(self.get_normal_input_shape()) simd = self.get_nodeattr("SIMD") assert normal_ishape[-1] % simd == 0, "SIMD must divide into input dimension" @@ -67,6 +74,7 @@ def get_folded_input_shape(self, ind=0): return tuple(folded_ishape) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" return self.get_folded_input_shape() def get_input_datatype(self, ind=0): @@ -80,6 +88,7 @@ def get_output_datatype(self, ind=0): return DataType[self.get_nodeattr("outputDataType")] def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -95,11 +104,13 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], odt) def get_instream_width(self, ind=0): + """Return instream width.""" i_bits = self.get_input_datatype().bitwidth() in_width = i_bits * self.get_nodeattr("SIMD") return in_width def get_outstream_width(self, ind=0): + """Return outstream width.""" o_bits = self.get_output_datatype().bitwidth() out_width = o_bits * self.get_nodeattr("SIMD") return out_width diff --git a/src/finn/custom_op/fpgadataflow/lookup.py b/src/finn/custom_op/fpgadataflow/lookup.py index 3394a2b5bf..aa8d615dfc 100644 --- a/src/finn/custom_op/fpgadataflow/lookup.py +++ b/src/finn/custom_op/fpgadataflow/lookup.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for lookup.""" import numpy as np import onnxruntime as rt from math import ceil @@ -42,9 +43,11 @@ class Lookup(HWCustomOp): mapping indices to values.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { # Number of embeddings ("memory depth") "NumEmbeddings": ("i", True, 0), @@ -68,11 +71,13 @@ def get_nodeattr_types(self): return my_attrs def get_exp_cycles(self): + """Return exp cycles.""" n_inputs = np.prod(self.get_nodeattr("InputShape")) exp_cycles = int(n_inputs) return exp_cycles def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" if ind == 0: return self.get_nodeattr("InputShape") if ind == 1: @@ -80,12 +85,14 @@ def get_normal_input_shape(self, ind=0): raise Exception("Undefined input ind for this layer type") def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" ishape = self.get_normal_input_shape() emb_dim = self.get_nodeattr("EmbeddingDim") oshape = list(ishape) + [emb_dim] return tuple(oshape) def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" if ind == 0: ishape = self.get_normal_input_shape() folded_ishape = list(ishape) + [1] @@ -94,6 +101,7 @@ def get_folded_input_shape(self, ind=0): return tuple(folded_ishape) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" ishape = self.get_normal_input_shape() mem_mode = self.get_nodeattr("mem_mode") emb_dim = self.get_nodeattr("EmbeddingDim") @@ -113,6 +121,7 @@ def get_folded_output_shape(self, ind=0): return tuple(oshape) def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -127,6 +136,7 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], odt) def get_input_datatype(self, ind=0): + """Return input datatype.""" if ind == 0: ret = DataType[self.get_nodeattr("InputType")] elif ind == 1: @@ -136,10 +146,12 @@ def get_input_datatype(self, ind=0): return ret def get_output_datatype(self, ind=0): + """Return output datatype.""" ret = DataType[self.get_nodeattr("EmbeddingType")] return ret def get_instream_width(self, ind=0): + """Return instream width.""" if ind == 0: bits = self.get_input_datatype().bitwidth() elif ind == 1: @@ -152,12 +164,14 @@ def get_instream_width(self, ind=0): return bits def get_outstream_width(self, ind=0): + """Return outstream width.""" folded_oshape = self.get_folded_output_shape() obits = self.get_output_datatype().bitwidth() return obits * folded_oshape[-1] def execute_node(self, context, graph): # create a standard add node to help calculate the result + """Execute node.""" node = self.onnx_node inp_values = context[node.input[0]] ishape = inp_values.shape @@ -188,6 +202,7 @@ def execute_node(self, context, graph): context[node.output[0]] = np.asarray(result, dtype=np.float32).reshape(oshape) def bram_estimation(self): + """Return bram estimation.""" mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "internal_embedded": # current calculation assumes embeddings always stored in BRAM_18Ks @@ -199,6 +214,7 @@ def bram_estimation(self): return 0 def bram_efficiency_estimation(self): + """Return bram efficiency estimation.""" bram16_est = self.bram_estimation() if bram16_est == 0: return 1 @@ -207,6 +223,7 @@ def bram_efficiency_estimation(self): return ebits / bram16_est_capacity def get_verilog_top_module_intf_names(self): + """Return the names of the interface signals for the verilog top module.""" intf_names = super().get_verilog_top_module_intf_names() mem_mode = self.get_nodeattr("mem_mode") if mem_mode == "external": diff --git a/src/finn/custom_op/fpgadataflow/replicate_stream.py b/src/finn/custom_op/fpgadataflow/replicate_stream.py index eeadf3d09f..4375c07b41 100644 --- a/src/finn/custom_op/fpgadataflow/replicate_stream.py +++ b/src/finn/custom_op/fpgadataflow/replicate_stream.py @@ -3,6 +3,7 @@ # per line. Black, however, formats some lines going beyond this. # Numpy math and arrays +"""Module for replicate stream.""" import numpy as np # Helper for creating ONNX nodes @@ -26,8 +27,10 @@ # See DuplicateStreams_Batch for feeding exactly two streams class ReplicateStream(HWCustomOp): # Initializes the operator given an onnx graph node + """Class for Replicate Stream.""" def __init__(self, onnx_node, **kwargs): # Just forward all arguments to the init method of the CustomOp base + """Initialize instance.""" super().__init__(onnx_node, **kwargs) # Need to override the default depths of outputs FIFOs here as these @@ -39,6 +42,7 @@ def __init__(self, onnx_node, **kwargs): # Defines attributes which must be present on this node def get_nodeattr_types(self): # Start from parent operator class attributes + """Return nodeattr types.""" attrs = HWCustomOp.get_nodeattr_types(self) # Update attributes dictionary for new custom operator attrs.update({ @@ -68,27 +72,32 @@ def get_nodeattr_types(self): # Number of replicas attribute as property for convenience @property def num(self): + """Return num.""" return self.get_nodeattr("num") # Datatype attribute as property for convenience @property def dtype(self): # Note: Converts from string to QONNX data type + """Return dtype.""" return DataType[self.get_nodeattr("dtype")] # Number of elements attribute as property for convenience @property def num_elems(self): + """Return num elems.""" return self.get_nodeattr("num_elems") # Number of parallel processed elements as property for convenience @property def pe(self): + """Return pe.""" return self.get_nodeattr("PE") # Number of inputs attribute as property for convenience @property def num_inputs(self): + """Return num inputs.""" return self.get_nodeattr("num_inputs") # Makes an operation compatible with the output shape for shape inference @@ -96,6 +105,7 @@ def num_inputs(self): # output, even if it seems easier. def make_shape_compatible_op(self, model: ModelWrapper): # noqa # Get the node wrapped by this custom op + """Create shape compatible op.""" node = self.onnx_node # Prepare a dummy input to simulate a large input that can be split into # the desired number and shapes of outputs @@ -112,6 +122,7 @@ def make_shape_compatible_op(self, model: ModelWrapper): # noqa # Infers the datatype of the node output def infer_node_datatype(self, model: ModelWrapper): # noqa # Get the node wrapped by this custom op + """Infer node datatype.""" node = self.onnx_node # Test for changing input datatype if model.get_tensor_datatype(node.input[0]) != self.dtype: @@ -131,6 +142,7 @@ def infer_node_datatype(self, model: ModelWrapper): # noqa # Executes replicating inputs in python def _execute_node_python(self, context, graph): # noqa: graph unused # Get the node wrapped by this custom op + """Execute node.""" node = self.onnx_node # Get the input out of the execution context inp = context[node.input[0]] @@ -142,6 +154,7 @@ def _execute_node_python(self, context, graph): # noqa: graph unused # Executes replicating inputs in C++ simulation def _execute_node_cppsim(self, context, graph): # noqa: graph unused # C++ Simulation needs to be implemented in HLS backend specialization + """Execute node.""" raise NotImplementedError( f"exec_mode cppsim of {self.__class__.__name__} is not implemented!" ) @@ -149,6 +162,7 @@ def _execute_node_cppsim(self, context, graph): # noqa: graph unused # Executes replicating inputs in simulation (either python c++ or rtl sim) def execute_node(self, context, graph): # Get the configured execution mode + """Execute node.""" mode = self.get_nodeattr("exec_mode") if mode == "python": self._execute_node_python(context, graph) @@ -159,6 +173,7 @@ def execute_node(self, context, graph): # Verifies the node attributes, inputs and outputs def verify_node(self): # TODO: Implement + """Verify node.""" return [] # Note: End of QONNX CustomOp region, below is FINN HWCustomOp stuff @@ -166,28 +181,33 @@ def verify_node(self): # Gets the datatype of input at index ind def get_input_datatype(self, ind=0): # All inputs (there should only be one) have the same type + """Return input datatype.""" return self.dtype # Gets the datatype of the output at index ind def get_output_datatype(self, ind=0): # All outputs will hae the same type, which is the same as the input + """Return output datatype.""" return self.dtype # Gets the shape of the input at index ind without folding def get_normal_input_shape(self, ind=0): # There is only one input with shape configured as attributes # Unpack multi-axis inputs list to yield a flat tuple as shape + """Return normal input shape.""" return *self.num_inputs, self.num_elems # Gets the shape of the output at index ind without folding def get_normal_output_shape(self, ind=0): # All outputs have the same shape, which is the same as the input # Unpack multi-axis inputs list to yield a flat tuple as shape + """Return normal output shape.""" return *self.num_inputs, self.num_elems # Gets the shape of the input at index ind with folding def get_folded_input_shape(self, ind=0): # Valid folding requires the PE to divides the number of elements + """Return folded input shape.""" assert self.num_elems % self.pe == 0, "PE must divide num_elems" # Folding along the last dimension return *self.num_inputs, self.num_elems // self.pe, self.pe @@ -195,6 +215,7 @@ def get_folded_input_shape(self, ind=0): # Gets the shape of the output at index ind with folding def get_folded_output_shape(self, ind=0): # Valid folding requires the PE to divides the number of elements + """Return folded output shape.""" assert self.num_elems % self.pe == 0, "PE must divide num_elems" # Folding along the last dimension return *self.num_inputs, self.num_elems // self.pe, self.pe @@ -202,6 +223,7 @@ def get_folded_output_shape(self, ind=0): # Widths of the input data stream of the input at index ind def get_instream_width(self, ind=0): # Get the number of bits used to represent the input + """Return instream width.""" i_bits = self.get_input_datatype(ind).bitwidth() # Parallelism is the number of elements in the last dimension of the # folded input @@ -212,6 +234,7 @@ def get_instream_width(self, ind=0): # Widths of the output data stream of the output at index ind def get_outstream_width(self, ind=0): # Get the number of bits used to represent the output + """Return outstream width.""" o_bits = self.get_output_datatype(ind).bitwidth() # Parallelism is the number of elements in the last dimension of the # folded output @@ -226,6 +249,7 @@ def get_number_output_values(self): # the embedding dimension. # In case of multiple outputs, the new FINN XSI simulation back-end requires # this to be specified on a per-output basis, in the form of a dict. + """Return number output values.""" num_outputs_per_stream = np.prod(self.get_folded_output_shape()[:-1]) if self.num > 1: return {f"out{i}": num_outputs_per_stream for i in range(self.num)} @@ -236,4 +260,5 @@ def get_number_output_values(self): def get_exp_cycles(self): # Number of iterations required to process the whole folded input stream # Note: This is all but the PE (last, parallelized) dimension + """Return exp cycles.""" return np.prod(self.get_folded_output_shape()[:-1]) diff --git a/src/finn/custom_op/fpgadataflow/requant.py b/src/finn/custom_op/fpgadataflow/requant.py index 4d26bb0f96..6fc7ddce51 100644 --- a/src/finn/custom_op/fpgadataflow/requant.py +++ b/src/finn/custom_op/fpgadataflow/requant.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Module for requant.""" import numpy as np import warnings from qonnx.core.datatype import DataType @@ -27,9 +28,11 @@ class Requant(HWCustomOp): """ def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { # parallelization; channels processed per cycle "PE": ("i", False, 1), @@ -75,6 +78,7 @@ def is_per_channel(self, model): return scale.size > 1 or bias.size > 1 def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -90,31 +94,32 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], odt) def verify_node(self): + """Verify node.""" pass def get_input_datatype(self, ind=0): - """Returns FINN DataType of input.""" + """Return FINN DataType of input.""" if ind == 0: return DataType[self.get_nodeattr("inputDataType")] # Scale and bias are float return DataType["FLOAT32"] def get_output_datatype(self, ind=0): - """Returns FINN DataType of output.""" + """Return FINN DataType of output.""" return DataType[self.get_nodeattr("outputDataType")] def get_normal_input_shape(self, ind=0): - """Returns input shape in format [N, H, W, C] or [N, C].""" + """Return input shape in format [N, H, W, C] or [N, C].""" num_input_vecs = self.get_nodeattr("numInputVectors") num_channels = self.get_nodeattr("NumChannels") return tuple(num_input_vecs + [num_channels]) def get_normal_output_shape(self, ind=0): - """Returns output shape.""" + """Return output shape.""" return self.get_normal_input_shape(0) def get_folded_input_shape(self, ind=0): - """Returns folded input shape.""" + """Return folded input shape.""" if ind == 0: normal_shape = self.get_normal_input_shape(0) pe = self.get_nodeattr("PE") @@ -124,11 +129,11 @@ def get_folded_input_shape(self, ind=0): return self.get_normal_input_shape(ind) def get_folded_output_shape(self, ind=0): - """Returns folded output shape.""" + """Return folded output shape.""" return self.get_folded_input_shape(0) def get_exp_cycles(self): - """Returns expected number of cycles for execution.""" + """Return expected number of cycles for execution.""" return self.get_number_output_values() def execute_node(self, context, graph): @@ -165,7 +170,7 @@ def execute_node(self, context, graph): context[node.output[0]] = x_clipped.astype(np.float32) def get_instream_width(self, ind=0): - """Returns input stream width.""" + """Return input stream width.""" if ind == 0: pe = self.get_nodeattr("PE") idt = self.get_input_datatype(0) @@ -174,7 +179,7 @@ def get_instream_width(self, ind=0): return 0 def get_outstream_width(self, ind=0): - """Returns output stream width.""" + """Return output stream width.""" pe = self.get_nodeattr("PE") odt = self.get_output_datatype() return pe * odt.bitwidth() diff --git a/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py index fb8dcc1982..af2dd31ff7 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/elementwise_binary_rtl.py @@ -6,6 +6,7 @@ # # @author Shane T. Fleming ############################################################################ +"""Module for elementwise binary rtl.""" import numpy as np import os import shutil @@ -26,9 +27,11 @@ class ElementwiseBinary_rtl(ElementwiseBinaryOperation, RTLBackend): """Base CustomOp wrapper for the finn-rtllib eltwisef component.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(ElementwiseBinaryOperation.get_nodeattr_types(self)) my_attrs.update(RTLBackend.get_nodeattr_types(self)) @@ -68,6 +71,7 @@ def adapt_for_loop_body(self, input_types): self.set_nodeattr("lhs_style", "input") def generate_hdl(self, model, fpgapart, clk): + """Generate hdl.""" rhs_style = self.get_nodeattr("rhs_style") mlo = self.get_nodeattr("mlo_max_iter") @@ -121,6 +125,7 @@ def generate_hdl(self, model, fpgapart, clk): self.set_nodeattr("ip_path", code_gen_dir) def get_rtl_file_list(self, abspath=False): + """Return rtl file list.""" if abspath: code_gen_dir = f"{self.get_nodeattr('code_gen_dir_ipgen')}/" rtllib_dir = os.path.join(get_settings().finn_rtllib, "eltwisef/") @@ -292,6 +297,7 @@ def code_generation_ipi(self): return cmd def instantiate_ip(self, cmd): + """Return instantiate ip.""" node_name = self.onnx_node.name top_module = self.get_nodeattr("gen_top_module") source_target = "./ip/verilog/rtl_ops/%s" % node_name @@ -307,6 +313,7 @@ def instantiate_ip(self, cmd): ) def execute_node(self, context, graph): + """Execute node.""" mode = self.get_nodeattr("exec_mode") if mode == "rtlsim": node = self.onnx_node @@ -379,12 +386,14 @@ def execute_node(self, context, graph): ElementwiseBinaryOperation.execute_node(self, context, graph) def generate_params(self, model, code_gen_dir): + """Generate params.""" weights = model.get_initializer(self.onnx_node.input[1]) if weights is not None: self.make_weight_file(weights, "decoupled_npy", f"{code_gen_dir}/input_1.npy") self.make_weight_file(weights, "decoupled_verilog_dat", f"{code_gen_dir}/memblock.dat") def make_weight_file(self, weights, weight_file_mode, weight_file_name): + """Create weight file.""" folded_weight_shape = self.get_folded_input_shape(1) weight_tensor = weights.reshape(folded_weight_shape).copy() @@ -438,6 +447,7 @@ def make_weight_file(self, weights, weight_file_mode, weight_file_name): f.write(val + "\n") def calc_wmem(self): + """Compute wmem.""" base_wmem = super().calc_wmem() num_w_reps = np.prod(self.calc_numInputVectors()) mlo = self.get_nodeattr("mlo_max_iter") @@ -446,12 +456,14 @@ def calc_wmem(self): return int(base_wmem * num_w_reps) def calc_numInputVectors(self): + """Compute numInputVectors.""" folded_lhs = self.get_folded_input_shape(0) if len(folded_lhs) >= 2: return list(folded_lhs[:-1]) return [1] def minimize_weight_bit_width(self, model): + """Return minimize weight bit width.""" super().minimize_weight_bit_width(model) def _get_rtl_op_name(self): @@ -465,6 +477,7 @@ class ElementwiseAdd_rtl(ElementwiseBinary_rtl, elementwise_binary.ElementwiseAd _operation = "Add", np.add, "({0} + {1})", '"ADD"' def _get_rtl_op_name(self): + """Return rtl op name.""" return '"ADD"' @@ -474,6 +487,7 @@ class ElementwiseSub_rtl(ElementwiseBinary_rtl, elementwise_binary.ElementwiseSu _operation = "Sub", np.subtract, "({0} - {1})", '"SUB"' def _get_rtl_op_name(self): + """Return rtl op name.""" return '"SUB"' @@ -483,4 +497,5 @@ class ElementwiseMul_rtl(ElementwiseBinary_rtl, elementwise_binary.ElementwiseMu _operation = "Mul", np.multiply, "({0} * {1})", '"MUL"' def _get_rtl_op_name(self): + """Return rtl op name.""" return '"MUL"' diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index 8f4b320ea5..088cf920c6 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for finn loop.""" import copy import math import numpy as np @@ -59,6 +60,7 @@ def collect_ip_dirs(model, ipstitch_path): # collect list of all IP dirs + """Return collect ip dirs.""" ip_dirs = [] need_memstreamer = False for node in model.graph.node: @@ -85,6 +87,7 @@ class FINNLoop(RTLBackend, HWCustomOp): out into a FINN-ONNX model of its own and are meant to be executed in a loop.""" def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "body": ("g", True, ""), "iteration": ("i", False, 1), @@ -163,6 +166,7 @@ def set_nodeattr( raise AttributeError("Op has no such attribute: " + name) def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" loop_body = self.get_nodeattr("body") if ind == 0: # get first node in loop body and return @@ -186,6 +190,7 @@ def get_normal_input_shape(self, ind=0): return ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" loop_body = self.get_nodeattr("body") # get last node in loop body and return # normal output shape @@ -198,6 +203,7 @@ def get_normal_output_shape(self, ind=0): return oshape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" loop_body = self.get_nodeattr("body") if ind == 0: # get first node in loop body and return @@ -214,6 +220,7 @@ def get_folded_input_shape(self, ind=0): return ishape def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" loop_body = self.get_nodeattr("body") # get last node in loop body and return # normal output shape @@ -222,6 +229,7 @@ def get_folded_output_shape(self, ind=0): return inst.get_folded_output_shape(0) def infer_node_datatype(self, model): + """Infer node datatype.""" pass def get_input_datatype(self, ind=0): @@ -241,10 +249,12 @@ def get_input_datatype(self, ind=0): return idt def get_output_datatype(self, ind=0): + """Return output datatype.""" odt = DataType[self.get_nodeattr("outputDataType")] return odt def get_instream_width(self, ind=0): + """Return instream width.""" loop_body = self.get_nodeattr("body") if ind == 0: # get first node in loop body and return @@ -261,6 +271,7 @@ def get_instream_width(self, ind=0): return iwidth def get_exp_cycles(self): + """Return exp cycles.""" loop_body = self.get_nodeattr("body") check_if_cycles_annotated = False @@ -278,6 +289,7 @@ def get_exp_cycles(self): return (body_cycles + overhead_per_iter) * iteration def get_outstream_width(self, ind=0): + """Return outstream width.""" loop_body = self.get_nodeattr("body") # get last node in loop body and return # normal output shape @@ -286,6 +298,7 @@ def get_outstream_width(self, ind=0): return inst.get_outstream_width(0) def get_number_output_values(self): + """Return number output values.""" loop_body = self.get_nodeattr("body") # get last node in loop body and return # normal output values @@ -312,6 +325,7 @@ def prepare_rtlsim(self, behav=False): self.set_nodeattr("rtlsim_so", sim_base + "/" + sim_rel) def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node inp_values = context[node.input[0]] if self.get_nodeattr("exec_mode") == "rtlsim": @@ -378,6 +392,7 @@ def execute_node(self, context, graph): def generate_hdl(self, model, fpgapart, clk): # Generate params as part of IP preparation + """Generate hdl.""" code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") self.generate_hdl_stream_tap() self.generate_params(model, code_gen_dir) @@ -614,6 +629,7 @@ def generate_hdl_stream_tap(self): f.write(template_wrapper) def ipgen_singlenode_code(self, fpgapart=None): + """Return ipgen singlenode code.""" prjname = "MakeLoopIP" block_name = self.onnx_node.name vivado_stitch_proj_dir = self.get_nodeattr("code_gen_dir_ipgen") @@ -1139,6 +1155,7 @@ def ipgen_singlenode_code(self, fpgapart=None): def get_verilog_top_module_intf_names(self): # from wrapper template + """Return verilog top module intf names.""" addr_bits = 64 intf_names = {} @@ -1175,6 +1192,7 @@ def get_verilog_top_module_intf_names(self): return intf_names def code_generation_ipi(self): + """Return code generation ipi.""" vlnv = self.get_nodeattr("ip_vlnv") cmd = [] # add all the generated IP dirs to ip_repo_paths @@ -1194,4 +1212,5 @@ def code_generation_ipi(self): return cmd def get_rtl_file_list(self, abspath: bool = False): + """Return rtl file list.""" return [] diff --git a/src/finn/custom_op/fpgadataflow/rtl/inner_shuffle_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/inner_shuffle_rtl.py index 5f1392cdf8..29969f6069 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/inner_shuffle_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/inner_shuffle_rtl.py @@ -6,6 +6,7 @@ # # @author Shane T. Fleming ############################################################################ +"""Module for inner shuffle rtl.""" import math import os import shutil @@ -45,6 +46,7 @@ class InnerShuffle_rtl(InnerShuffle, RTLBackend): """CustomOp wrapper for the finn-rtllib inner_shuffle component.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) # check some constraints that it is a legal InnerShuffle @@ -58,12 +60,14 @@ def __init__(self, onnx_node, **kwargs): raise RuntimeError("Unable to determine a new SIMD value for this transpose.") def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(InnerShuffle.get_nodeattr_types(self)) my_attrs.update(RTLBackend.get_nodeattr_types(self)) return my_attrs def get_template_values(self, idims, simd, dt): + """Return template values.""" code_gen_dict = { "TOP_MODULE_NAME": self.get_verilog_top_module_name(), "I": idims[0], @@ -75,6 +79,7 @@ def get_template_values(self, idims, simd, dt): return code_gen_dict def generate_hdl(self, model, fpgapart, clk): + """Generate hdl.""" rtlsrc = os.path.join(get_settings().finn_rtllib, "inner_shuffle") template_path = os.path.join(rtlsrc, "inner_shuffle_template.v") code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") @@ -102,12 +107,13 @@ def generate_hdl(self, model, fpgapart, clk): self.set_nodeattr("gen_top_module", self.get_verilog_top_module_name()) sv_files = ["inner_shuffle.sv", "skid.sv", "elasticmem.sv"] - for sv_files in sv_files: - shutil.copy(f"{rtlsrc}/{sv_files}", code_gen_dir) + for sv_file in sv_files: + shutil.copy(f"{rtlsrc}/{sv_file}", code_gen_dir) self.set_nodeattr("ipgen_path", code_gen_dir) self.set_nodeattr("ip_path", code_gen_dir) def get_rtl_file_list(self, abspath=False): + """Return rtl file list.""" if abspath: code_gen_dir = f"{self.get_nodeattr('code_gen_dir_ipgen')}/" rtllib_dir = os.path.join(get_settings().finn_rtllib, "inner_shuffle") @@ -124,7 +130,7 @@ def get_rtl_file_list(self, abspath=False): ] def code_generation_ipi(self): - """Constructs and returns the TCL for node instantiation in Vivado IPI.""" + """Construct and returns the TCL for node instantiation in Vivado IPI.""" code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") top_module = self.get_nodeattr("gen_top_module") sourcefiles = ["inner_shuffle.sv", "skid.sv", "elasticmem.sv", f"{top_module}.v"] @@ -137,6 +143,7 @@ def code_generation_ipi(self): return cmd def execute_node(self, context, graph): + """Execute node.""" mode = self.get_nodeattr("exec_mode") if mode == "rtlsim": RTLBackend.execute_node(self, context, graph) diff --git a/src/finn/custom_op/fpgadataflow/rtl/layernorm_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/layernorm_rtl.py index 36d4b47e0c..abfdc68096 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/layernorm_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/layernorm_rtl.py @@ -10,6 +10,7 @@ # ############################################################################ +"""Module for layernorm rtl.""" import math import numpy as np import os @@ -26,15 +27,18 @@ class LayerNorm_rtl(LayerNorm, RTLBackend): """ def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(RTLBackend.get_nodeattr_types(self)) my_attrs.update(LayerNorm.get_nodeattr_types(self)) return my_attrs def generate_hdl(self, model, fpgapart, clk): + """Generate hdl.""" rtllib_dir = os.path.join(get_settings().finn_rtllib, "layernorm") template_path = os.path.join(rtllib_dir, "layernorm_wrapper_template.v") simd = self.get_nodeattr("SIMD") @@ -76,6 +80,7 @@ def generate_hdl(self, model, fpgapart, clk): self.set_nodeattr("ip_path", code_gen_dir) def get_rtl_file_list(self, abspath=False): + """Return rtl file list.""" if abspath: code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") + "/" rtllib_dir = os.path.join(get_settings().finn_rtllib, "layernorm") @@ -94,6 +99,7 @@ def get_rtl_file_list(self, abspath=False): return verilog_files def code_generation_ipi(self): + """Return code generation ipi.""" code_gen_dir = self.get_nodeattr("code_gen_dir_ipgen") sourcefiles = [ @@ -118,6 +124,7 @@ def code_generation_ipi(self): return cmd def execute_node(self, context, graph): + """Execute node.""" mode = self.get_nodeattr("exec_mode") if mode == "cppsim": LayerNorm.execute_node(self, context, graph) @@ -125,6 +132,7 @@ def execute_node(self, context, graph): RTLBackend.execute_node(self, context, graph) def get_exp_cycles(self): + """Return exp cycles.""" simd = self.get_nodeattr("SIMD") idim = self.get_normal_input_shape() n = idim[-1] diff --git a/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py b/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py index c3b7ff7564..b0681564e3 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py +++ b/src/finn/custom_op/fpgadataflow/rtl/requant_rtl.py @@ -3,6 +3,7 @@ # # SPDX-License-Identifier: BSD-3-Clause +"""Module for requant rtl.""" import numpy as np import os @@ -17,9 +18,11 @@ class Requant_rtl(Requant, RTLBackend): """RTL backend for Requant operation using finn-rtllib/requant.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = {} my_attrs.update(Requant.get_nodeattr_types(self)) my_attrs.update(RTLBackend.get_nodeattr_types(self)) @@ -155,6 +158,7 @@ def get_rtl_file_list(self, abspath=False): return [os.path.basename(f) for f in rtl_files] def code_generation_ipi(self): + """Return code generation ipi.""" sourcefiles = self.get_rtl_file_list(abspath=True) cmd = [] diff --git a/src/finn/custom_op/fpgadataflow/shuffle.py b/src/finn/custom_op/fpgadataflow/shuffle.py index fd77db4281..031937d963 100644 --- a/src/finn/custom_op/fpgadataflow/shuffle.py +++ b/src/finn/custom_op/fpgadataflow/shuffle.py @@ -7,6 +7,7 @@ # @author Shane T. Fleming ############################################################################ +"""Module for shuffle.""" import numpy as np from onnx import helper from operator import itemgetter @@ -22,6 +23,7 @@ class Shuffle(HWCustomOp): This operator is later transformed into InnerShuffle and OuterShuffle operations.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): @@ -54,7 +56,7 @@ def get_nodeattr_types(self): │ │ out_shape ▼ - """ + """ # noqa: D401 my_attrs = { "data_type": ("s", True, ""), "transpose_in_shape": ("ints", True, []), @@ -71,12 +73,15 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" return self.get_nodeattr("in_shape") def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" return self.get_nodeattr("out_shape") def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node input_data = context[node.input[0]] input_reshaped = input_data.reshape(self.get_nodeattr("transpose_in_shape")) @@ -85,10 +90,12 @@ def execute_node(self, context, graph): context[node.output[0]] = output_reshaped def get_input_datatype(self, ind=0): + """Return input datatype.""" data_type = DataType[self.get_nodeattr("data_type")] return data_type def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node dt = model.get_tensor_datatype(node.input[0]) if dt != self.get_input_datatype(): @@ -100,23 +107,28 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], dt) def verify_node(self): + """Verify node.""" raise NotImplementedError("This function is not yet immplemented.") def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return ibits * simd def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return obits * simd def get_output_datatype(self, ind=0): + """Return output datatype.""" data_type = DataType[self.get_nodeattr("data_type")] return data_type def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" normal_oshape = list(self.get_normal_output_shape()) simd = self.get_nodeattr("SIMD") assert normal_oshape[-1] % simd == 0, "SIMD must divide into the innermost output dimension" @@ -125,6 +137,7 @@ def get_folded_output_shape(self, ind=0): return tuple(folded_oshape) def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" normal_ishape = list(self.get_normal_input_shape()) simd = self.get_nodeattr("SIMD") assert normal_ishape[-1] % simd == 0, "SIMD must divide into the innermost input dimension" diff --git a/src/finn/custom_op/fpgadataflow/split.py b/src/finn/custom_op/fpgadataflow/split.py index 76aa6859b6..578d391e48 100644 --- a/src/finn/custom_op/fpgadataflow/split.py +++ b/src/finn/custom_op/fpgadataflow/split.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for split.""" import numpy as np from onnx import helper from qonnx.core.datatype import DataType @@ -40,9 +41,11 @@ class StreamingSplit(HWCustomOp): Only supports splitting along the last (channel) axis.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "SIMD": ("i", True, 0), # number of elements of each output streams @@ -59,30 +62,36 @@ def get_nodeattr_types(self): return my_attrs def get_n_outputs(self): + """Return n outputs.""" return len(self.get_nodeattr("ChannelsPerStream")) def get_total_elems(self): + """Return total elems.""" elems_per_stream = self.get_nodeattr("ChannelsPerStream") return int(np.sum(elems_per_stream)) def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" total_elems = self.get_total_elems() vecs = list(self.get_nodeattr("numInputVectors")) ishape = tuple(vecs + [total_elems]) return ishape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" simd = self.get_nodeattr("SIMD") folds = self.get_total_elems() // simd vecs = list(self.get_nodeattr("numInputVectors")) return tuple(vecs + [folds, simd]) def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" elems = self.get_nodeattr("ChannelsPerStream")[ind] vecs = list(self.get_nodeattr("numInputVectors")) return tuple(vecs + [elems]) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" elems = self.get_nodeattr("ChannelsPerStream")[ind] simd = self.get_nodeattr("SIMD") folds = elems // simd @@ -91,6 +100,7 @@ def get_folded_output_shape(self, ind=0): def make_shape_compatible_op(self, model): # check input shape + """Create shape compatible op.""" exp_ishape = self.get_normal_input_shape() ishape = tuple(model.get_tensor_shape(self.onnx_node.input[0])) assert ishape == exp_ishape, "Unexpected input shape" @@ -101,6 +111,7 @@ def make_shape_compatible_op(self, model): def infer_node_datatype(self, model): # check input datatype + """Infer node datatype.""" inp = self.onnx_node.input[0] idt = model.get_tensor_datatype(inp) if idt != self.get_input_datatype(): @@ -116,34 +127,42 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(out, odt) def verify_node(self): + """Verify node.""" pass def get_input_datatype(self, ind=0): + """Return input datatype.""" return DataType[self.get_nodeattr("inputDataType")] def get_output_datatype(self, ind=0): # all output datatypes are the same as the input datatype + """Return output datatype.""" return self.get_input_datatype() def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype().bitwidth() return ibits * self.get_nodeattr("SIMD") def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() out_width = obits * self.get_nodeattr("SIMD") return out_width def get_number_output_values(self): + """Return number output values.""" out_val = {} for i in range(len(self.onnx_node.output)): out_val["out%s" % i] = np.prod(self.get_folded_output_shape(i)[1:-1]) return out_val def get_exp_cycles(self): + """Return exp cycles.""" return np.prod(self.get_folded_input_shape()[:-1]) def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node split = self.get_nodeattr("ChannelsPerStream") np_split_param = np.cumsum(split[:-1]) @@ -152,5 +171,6 @@ def execute_node(self, context, graph): context[out] = np_result[i] def get_instream_width_padded(self, ind=0): + """Return instream width padded.""" in_width = self.get_instream_width() return roundup_to_integer_multiple(in_width, 8) diff --git a/src/finn/custom_op/fpgadataflow/streamingdataflowpartition.py b/src/finn/custom_op/fpgadataflow/streamingdataflowpartition.py index 0aa4439d35..0aafa9067a 100644 --- a/src/finn/custom_op/fpgadataflow/streamingdataflowpartition.py +++ b/src/finn/custom_op/fpgadataflow/streamingdataflowpartition.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for streamingdataflowpartition.""" from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.base import CustomOp @@ -41,6 +42,7 @@ class StreamingDataflowPartition(CustomOp): bitfile by itself.""" def get_nodeattr_types(self): + """Return nodeattr types.""" return { "model": ("s", True, ""), "res_estimate": ("s", False, ""), @@ -55,12 +57,15 @@ def get_nodeattr_types(self): } def make_shape_compatible_op(self, model): + """Create shape compatible op.""" pass def infer_node_datatype(self, model): + """Infer node datatype.""" pass def execute_node(self, context, graph): + """Execute node.""" model = ModelWrapper(self.get_nodeattr("model")) return_full_exec_context = self.get_nodeattr("return_full_exec_context") == 1 node = self.onnx_node @@ -83,6 +88,7 @@ def execute_node(self, context, graph): context[node.name + "_" + tname] = ret[tname] def verify_node(self): + """Verify node.""" info_messages = [] # verify number of attributes diff --git a/src/finn/custom_op/fpgadataflow/streamingdatawidthconverter.py b/src/finn/custom_op/fpgadataflow/streamingdatawidthconverter.py index 4235639409..76e8487fe2 100644 --- a/src/finn/custom_op/fpgadataflow/streamingdatawidthconverter.py +++ b/src/finn/custom_op/fpgadataflow/streamingdatawidthconverter.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for streamingdatawidthconverter.""" import math import numpy as np from qonnx.core.datatype import DataType @@ -41,6 +42,7 @@ class StreamingDataWidthConverter(HWCustomOp): """Abstraction layer for HW implementation of StreamingDataWidthConverter""" def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { # shape of input tensor "inShape": ("ints", True, []), @@ -65,19 +67,23 @@ def get_output_datatype(self, ind=0): return DataType[self.get_nodeattr("dataType")] def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" ishape = self.get_nodeattr("inShape") return ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" oshape = self.get_nodeattr("outShape") return oshape def get_iowidth_lcm(self): + """Return iowidth lcm.""" iwidth = self.get_nodeattr("inWidth") owidth = self.get_nodeattr("outWidth") return int(np.lcm(iwidth, owidth)) def needs_lcm(self): + """Return needs lcm.""" iwidth = self.get_nodeattr("inWidth") owidth = self.get_nodeattr("outWidth") maxwidth = max(iwidth, owidth) @@ -85,9 +91,11 @@ def needs_lcm(self): return maxwidth % minwidth != 0 def check_divisible_iowidths(self): + """Return check divisible iowidths.""" pass def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" self.check_divisible_iowidths() iwidth = self.get_nodeattr("inWidth") ishape = self.get_normal_input_shape() @@ -108,6 +116,7 @@ def get_folded_input_shape(self, ind=0): return dummy_t.shape def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" self.check_divisible_iowidths() owidth = self.get_nodeattr("outWidth") oshape = self.get_normal_output_shape() @@ -129,14 +138,17 @@ def get_folded_output_shape(self, ind=0): return dummy_t.shape def get_instream_width(self, ind=0): + """Return instream width.""" in_width = self.get_nodeattr("inWidth") return in_width def get_outstream_width(self, ind=0): + """Return outstream width.""" out_width = self.get_nodeattr("outWidth") return out_width def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -151,6 +163,7 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], idt) def verify_node(self): + """Verify node.""" info_messages = [] # verify that "backend" is set to "fpgadataflow" backend_value = self.get_nodeattr("backend") @@ -168,6 +181,7 @@ def verify_node(self): return info_messages def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node exp_shape = self.get_normal_input_shape() inp = context[node.input[0]] diff --git a/src/finn/custom_op/fpgadataflow/streamingfifo.py b/src/finn/custom_op/fpgadataflow/streamingfifo.py index b7238fa0e5..1bea7c8ffa 100644 --- a/src/finn/custom_op/fpgadataflow/streamingfifo.py +++ b/src/finn/custom_op/fpgadataflow/streamingfifo.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for streamingfifo.""" import math from qonnx.core.datatype import DataType @@ -34,10 +35,14 @@ class StreamingFIFO(HWCustomOp): + """Class for Streaming FIFO.""" + def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = super().get_nodeattr_types() my_attrs.update( { @@ -71,6 +76,7 @@ def get_nodeattr_types(self): return my_attrs def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node idt = model.get_tensor_datatype(node.input[0]) if idt != self.get_input_datatype(): @@ -85,6 +91,7 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], idt) def get_verilog_top_module_intf_names(self): + """Return verilog top module intf names.""" ret = super().get_verilog_top_module_intf_names() try: is_rtl = self.get_nodeattr("impl_style") == "rtl" @@ -100,6 +107,7 @@ def get_verilog_top_module_intf_names(self): return ret def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" try: depth = self.get_adjusted_depth() except AttributeError: @@ -114,33 +122,41 @@ def get_normal_input_shape(self, ind=0): return self.get_nodeattr("normal_shape") def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" return self.get_normal_input_shape() def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" return self.get_nodeattr("folded_shape") def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" return self.get_nodeattr("folded_shape") def get_instream_width(self, ind=0): + """Return instream width.""" dtype = DataType[self.get_nodeattr("dataType")] folded_shape = self.get_nodeattr("folded_shape") in_width = folded_shape[-1] * dtype.bitwidth() return in_width def get_outstream_width(self, ind=0): + """Return outstream width.""" dtype = DataType[self.get_nodeattr("dataType")] folded_shape = self.get_nodeattr("folded_shape") in_width = folded_shape[-1] * dtype.bitwidth() return in_width def get_input_datatype(self, ind=0): + """Return input datatype.""" return DataType[self.get_nodeattr("dataType")] def get_output_datatype(self, ind=0): + """Return output datatype.""" return DataType[self.get_nodeattr("dataType")] def execute_node(self, context, graph): + """Execute node.""" node = self.onnx_node context[node.output[0]] = context[node.input[0]] @@ -201,6 +217,7 @@ def uram_estimation(self): return (math.ceil(depth / 4096)) * (math.ceil(W / 72)) def bram_efficiency_estimation(self): + """Return bram efficiency estimation.""" try: depth = self.get_adjusted_depth() except AttributeError: diff --git a/src/finn/custom_op/fpgadataflow/templates.py b/src/finn/custom_op/fpgadataflow/templates.py index 4264ba36be..9591da7e5f 100644 --- a/src/finn/custom_op/fpgadataflow/templates.py +++ b/src/finn/custom_op/fpgadataflow/templates.py @@ -28,6 +28,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # template for single node execution +"""Module for templates which will be filled at runtime.""" docompute_template = """ #define HLS_CONSTEXPR_ENABLE #define AP_INT_MAX_W $AP_INT_MAX_W$ diff --git a/src/finn/custom_op/fpgadataflow/thresholding.py b/src/finn/custom_op/fpgadataflow/thresholding.py index 18afd1d287..e452e304df 100644 --- a/src/finn/custom_op/fpgadataflow/thresholding.py +++ b/src/finn/custom_op/fpgadataflow/thresholding.py @@ -397,6 +397,7 @@ def calc_tmem(self): return num_channels // pe def get_verilog_top_module_intf_names(self): + """Return the signal names for the Verilog top module.""" intf_names = {} intf_names["clk"] = ["ap_clk"] intf_names["rst"] = ["ap_rst_n"] diff --git a/src/finn/custom_op/fpgadataflow/upsampler.py b/src/finn/custom_op/fpgadataflow/upsampler.py index 1ed23ab2d8..470cb5eadc 100644 --- a/src/finn/custom_op/fpgadataflow/upsampler.py +++ b/src/finn/custom_op/fpgadataflow/upsampler.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for upsampler.""" import numpy as np import onnxruntime as rt from onnx import TensorProto, helper @@ -40,9 +41,11 @@ class UpsampleNearestNeighbour(HWCustomOp): """Abstraction layer for HW implementation of UpsampleNearestNeighbour.""" def __init__(self, onnx_node, **kwargs): + """Initialize instance.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return nodeattr types.""" my_attrs = { "SIMD": ("i", True, 0), # Height, width of the output feature map @@ -62,9 +65,11 @@ def get_nodeattr_types(self): return my_attrs def get_exp_cycles(self): + """Return exp cycles.""" return np.prod(self.get_folded_output_shape()[:-1]) def get_normal_input_shape(self, ind=0): + """Return normal input shape.""" batch = self.get_nodeattr("batchSize") HI = self.get_nodeattr("HI") WI = self.get_nodeattr("WI") @@ -73,6 +78,7 @@ def get_normal_input_shape(self, ind=0): return ishape def get_normal_output_shape(self, ind=0): + """Return normal output shape.""" batch = self.get_nodeattr("batchSize") HO = self.get_nodeattr("HO") WO = self.get_nodeattr("WO") @@ -81,18 +87,21 @@ def get_normal_output_shape(self, ind=0): return oshape def get_folded_input_shape(self, ind=0): + """Return folded input shape.""" spatial_shape = list(self.get_normal_input_shape())[:-1] simd = self.get_nodeattr("SIMD") folds = self.get_nodeattr("NumChannels") // simd return tuple(spatial_shape + [folds, simd]) def get_folded_output_shape(self, ind=0): + """Return folded output shape.""" spatial_shape = list(self.get_normal_output_shape())[:-1] simd = self.get_nodeattr("SIMD") folds = self.get_nodeattr("NumChannels") // simd return tuple(spatial_shape + [folds, simd]) def infer_node_datatype(self, model): + """Infer node datatype.""" node = self.onnx_node # data type stays the same idt = model.get_tensor_datatype(node.input[0]) @@ -116,17 +125,20 @@ def get_output_datatype(self, ind=0): return self.get_input_datatype() def get_instream_width(self, ind=0): + """Return instream width.""" ibits = self.get_input_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return ibits * simd def get_outstream_width(self, ind=0): + """Return outstream width.""" obits = self.get_output_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return obits * simd def execute_node(self, context, graph): # create a standard resize node to help calculate the result + """Execute node.""" node = self.onnx_node inp_values = context[node.input[0]] ishape = inp_values.shape diff --git a/src/finn/transformation/fpgadataflow/annotate_resources.py b/src/finn/transformation/fpgadataflow/annotate_resources.py index ee2da2094c..7feb235ab0 100644 --- a/src/finn/transformation/fpgadataflow/annotate_resources.py +++ b/src/finn/transformation/fpgadataflow/annotate_resources.py @@ -26,11 +26,14 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for annotate resources.""" import qonnx.custom_op.registry as registry +from ast import literal_eval from functools import partial from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation +from typing import Any, Literal, cast from finn.analysis.fpgadataflow.hls_synth_res_estimation import hls_synth_res_estimation from finn.analysis.fpgadataflow.post_synth_res import post_synth_res @@ -43,19 +46,23 @@ class AnnotateResources(Transformation): node as an attribute on the node, depending on the mode parameter: * 'estimate' -- use the analytical estimation model * 'hls' -- use results from the HLS synthesis report - * 'synth' -- use post-synthesis (Vivado or Vitis) report + * 'synth' -- use post-synthesis (Vivado or Vitis) report. No annotations can be provided unless the relevant transformation for the chosen mode (e.g. HLSSynthIP for hls) was previously run. """ - def __init__(self, mode, fpgapart, override_res_dict=None): + def __init__( + self, mode: str, fpgapart: str, override_res_dict: dict[str, Any] | None = None + ) -> None: + """Initialize instance.""" super().__init__() self.mode = mode self.fpgapart = fpgapart self.res_dict = override_res_dict - def apply(self, model): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: + """Apply transformation.""" graph = model.graph if self.mode == "estimate": res_fxn = partial(res_estimation, fpgapart=self.fpgapart) @@ -76,13 +83,13 @@ def apply(self, model): children_dict[node.name] = self.res_dict[node.name] elif node.op_type == "StreamingDataflowPartition": # recurse into model to manually annotate per-layer resources - sdp_model_filename = getCustomOp(node).get_nodeattr("model") + sdp_model_filename = cast("str", getCustomOp(node).get_nodeattr("model")) sdp_model = ModelWrapper(sdp_model_filename) sdp_model = sdp_model.transform( AnnotateResources(self.mode, self.fpgapart, self.res_dict) ) - sdp_dict = sdp_model.get_metadata_prop("res_total_" + self.mode) - sdp_dict = eval(sdp_dict) + sdp_dict = cast("str", sdp_model.get_metadata_prop("res_total_" + self.mode)) + sdp_dict = literal_eval(sdp_dict) # save transformed model sdp_model.save(sdp_model_filename) # set res attribute for sdp node diff --git a/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py b/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py index 91e4fe4e0a..a91121af55 100644 --- a/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py +++ b/src/finn/transformation/fpgadataflow/convert_to_hw_layers.py @@ -468,9 +468,11 @@ class InferRequantLayer(Transformation): """ def __init__(self): + """Initialize instance.""" super().__init__() def apply(self, model): + """Apply transformation.""" graph = model.graph node_ind = 0 graph_modified = False @@ -777,6 +779,7 @@ class InferAddStreamsLayer(Transformation): """ def apply(self, model): + """Apply transformation.""" log.warning( "InferAddStreamsLayer is deprecated. " "Use InferElementwiseBinaryOperation instead. " @@ -945,6 +948,7 @@ class InferChannelwiseLinearLayer(Transformation): """ def apply(self, model): + """Apply transformation.""" log.warning( "InferChannelwiseLinearLayer is deprecated. " "Use InferElementwiseBinaryOperation instead. " @@ -1669,6 +1673,7 @@ class InferStreamingEltwise(Transformation): """ def apply(self, model): + """Apply transformation.""" log.warning( "InferStreamingEltwise is deprecated. " "Use InferElementwiseBinaryOperation instead. " @@ -2172,6 +2177,7 @@ class InferShuffle(Transformation): """ def __init__(self, _filter=skip_first_node_transpose): + """Initialize instance.""" super().__init__() # Register the filter function as attribute self._filter = _filter diff --git a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py index ff6eed2f18..9a63bac336 100644 --- a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py +++ b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py @@ -26,11 +26,14 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for create dataflow partition.""" +from onnx import NodeProto from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.create_generic_partitions import PartitionFromLambda from qonnx.util.basic import get_by_name +from typing import Literal, cast from finn.transformation.fpgadataflow.externalize_params import ExternalizeParams from finn.util.basic import make_build_dir @@ -43,26 +46,32 @@ class CreateDataflowPartition(Transformation): that indicates the filename for the second graph that only contains dataflow nodes. No action is taken if there are no dataflow nodes.""" - def __init__(self, partition_model_dir=None): + def __init__(self, partition_model_dir: str | None = None) -> None: + """Initialize instance.""" super().__init__() if partition_model_dir is None: self.partition_model_dir = make_build_dir("dataflow_partition_") else: self.partition_model_dir = partition_model_dir - def apply(self, model): - def filter_fc_extw(x): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: + """Apply transformation.""" + + def filter_fc_extw(x: NodeProto) -> bool: + """Return true if node is an IODMA_hls with burst mode "wrap".""" if x.op_type == "IODMA_hls": burst_mode = get_by_name(x.attribute, "burstMode") if burst_mode is not None: burst_mode = burst_mode.s.decode("UTF-8") return burst_mode == "wrap" + return False extw_dma_nodes = list(filter(filter_fc_extw, model.graph.node)) if len(extw_dma_nodes) > 0: model = model.transform(ExternalizeParams()) - def assign_partition_id(node): + def assign_partition_id(node: NodeProto) -> int: + """Return partition id.""" if node.op_type in ["GenericPartition", "StreamingDataflowPartition"]: return -1 backend = get_by_name(node.attribute, "backend") @@ -84,7 +93,7 @@ def assign_partition_id(node): for partition_ind, p_node in enumerate(p_nodes): # go into partition to extract some info p_node_inst = getCustomOp(p_node) - node_model_filename = p_node_inst.get_nodeattr("model") + node_model_filename = cast("str", p_node_inst.get_nodeattr("model")) p_model = ModelWrapper(node_model_filename) # check floorplan (SLR assignment per node) inst = getCustomOp(p_model.graph.node[0]) diff --git a/src/finn/transformation/fpgadataflow/insert_fifo.py b/src/finn/transformation/fpgadataflow/insert_fifo.py index eac4897aaf..172dc4388f 100644 --- a/src/finn/transformation/fpgadataflow/insert_fifo.py +++ b/src/finn/transformation/fpgadataflow/insert_fifo.py @@ -84,9 +84,11 @@ def __init__( self.vivado_ram_style = vivado_ram_style def _is_fifo_node(self, node: NodeProto) -> bool: + """Return whether node is a FIFO node.""" return bool(node.op_type.startswith("StreamingFIFO")) def _suitable_node(self, node: NodeProto) -> bool: + """Return whether node is suitable for FIFO insertion.""" if node is not None: if is_fpgadataflow_node(node): return bool(not self._is_fifo_node(node)) @@ -98,11 +100,13 @@ def _suitable_folded_shapes( ishape: Sequence[int] | npt.NDArray[np.int_], oshape: Sequence[int] | npt.NDArray[np.int_], ) -> bool: + """Return suitable folded shapes.""" matching_stream_width = ishape[-1] == oshape[-1] matching_size = np.prod(ishape) == np.prod(oshape) return matching_stream_width and matching_size def _shape_to_onnx(self, shape: Sequence[int] | npt.NDArray[np.int_]) -> list[int]: + """Return shape as a list of integers.""" return [int(dim) for dim in shape] def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: diff --git a/src/finn/transformation/fpgadataflow/minimize_accumulator_width.py b/src/finn/transformation/fpgadataflow/minimize_accumulator_width.py index 61159fde0c..e04867a1da 100644 --- a/src/finn/transformation/fpgadataflow/minimize_accumulator_width.py +++ b/src/finn/transformation/fpgadataflow/minimize_accumulator_width.py @@ -27,9 +27,12 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for minimize accumulator width.""" +from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.transformation.infer_datatypes import InferDataTypes +from typing import Literal from finn.util.fpgadataflow import is_fpgadataflow_node @@ -39,10 +42,12 @@ class MinimizeAccumulatorWidth(Transformation): functions to save on resources. May alter tensor DataType for certain nodes if they produce an accumulator as result.""" - def __init__(self): + def __init__(self) -> None: + """Initialize instance.""" super().__init__() - def apply(self, model): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: + """Apply transformation.""" for node_id in range(len(model.graph.node)): # Since InferDataTypes potentially changes node attributes in each loop iterations, # the for-loop cannot loop over a list of a snapshot of the graph's node protos @@ -50,7 +55,7 @@ def apply(self, model): if is_fpgadataflow_node(node): inst = getCustomOp(node) if hasattr(inst, "minimize_accumulator_width"): - inst.minimize_accumulator_width(model) + inst.minimize_accumulator_width(model) # type: ignore # Since this transformation is applied iteratively, we have to ensure that # we propagate the new datatype to other layers model = model.transform(InferDataTypes()) diff --git a/src/finn/transformation/fpgadataflow/minimize_weight_bit_width.py b/src/finn/transformation/fpgadataflow/minimize_weight_bit_width.py index 49770f7d0c..64a746de5a 100644 --- a/src/finn/transformation/fpgadataflow/minimize_weight_bit_width.py +++ b/src/finn/transformation/fpgadataflow/minimize_weight_bit_width.py @@ -26,9 +26,12 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -from qonnx.custom_op.registry import getCustomOp +"""Module for minimize weight bit width.""" +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation +from typing import Literal +from finn.util.basic import getHWCustomOp from finn.util.fpgadataflow import is_fpgadataflow_node @@ -37,13 +40,15 @@ class MinimizeWeightBitWidth(Transformation): functions to save on resources. May alter tensor weightDataType if the node does not have runtime writeable weights.""" - def __init__(self): + def __init__(self) -> None: + """Initialize instance.""" super().__init__() - def apply(self, model): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: + """Apply transformation.""" for node in model.graph.node: if is_fpgadataflow_node(node): - inst = getCustomOp(node) + inst = getHWCustomOp(node) if hasattr(inst, "minimize_weight_bit_width"): - inst.minimize_weight_bit_width(model) + inst.minimize_weight_bit_width(model) # type: ignore return (model, False) diff --git a/src/finn/transformation/fpgadataflow/prepare_cppsim.py b/src/finn/transformation/fpgadataflow/prepare_cppsim.py index 8ad9b55d53..01285f1a81 100644 --- a/src/finn/transformation/fpgadataflow/prepare_cppsim.py +++ b/src/finn/transformation/fpgadataflow/prepare_cppsim.py @@ -27,35 +27,45 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for prepare cppsim.""" import copy import multiprocessing as mp -import os -import qonnx.custom_op.registry as registry +from onnx import NodeProto +from pathlib import Path +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation from qonnx.util.basic import get_num_default_workers +from typing import TYPE_CHECKING, Literal, cast -from finn.util.basic import make_build_dir +from finn.util.basic import getHWCustomOp, make_build_dir +from finn.util.exception import FINNUserError from finn.util.fpgadataflow import is_hls_node +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend -def _codegen_single_node(node, model): - """Calls C++ code generation for one node. Resulting code can be used + +def _codegen_single_node(node: NodeProto, model: ModelWrapper) -> None: + """Call C++ code generation for one node. Resulting code can be used to simulate node using cppsim.""" op_type = node.op_type try: # lookup op_type in registry of CustomOps - inst = registry.getCustomOp(node) + inst = cast("HLSBackend", getHWCustomOp(node)) # get the path of the code generation directory - code_gen_dir = inst.get_nodeattr("code_gen_dir_cppsim") + code_gen_dir = cast("str", inst.get_nodeattr("code_gen_dir_cppsim")) # ensure that there is a directory - if code_gen_dir == "" or not os.path.isdir(code_gen_dir): + if code_gen_dir == "" or not Path(code_gen_dir).is_dir(): code_gen_dir = make_build_dir(prefix="code_gen_cppsim_" + str(node.name) + "_") - inst.set_nodeattr("code_gen_dir_cppsim", code_gen_dir) + inst.set_nodeattr("code_gen_dir_cppsim", str(code_gen_dir)) # ensure that there is generated code inside the dir inst.code_generation_cppsim(model) except KeyError: # exception if op_type is not supported - raise Exception("Custom op_type %s is currently not supported." % op_type) + raise FINNUserError( + f"Custom op_type {op_type} is currently not supported. " + f"Could this be a streamlining error?" + ) from None class PrepareCppSim(Transformation): @@ -67,7 +77,8 @@ class PrepareCppSim(Transformation): that contains generated C++ code that can be used to simulate node using cppsim. The subsequent transformation is CompileCppSim""" - def __init__(self, num_workers=None): + def __init__(self, num_workers: int | None = None) -> None: + """Initialize instance.""" super().__init__() if num_workers is None: self._num_workers = get_num_default_workers() @@ -77,21 +88,23 @@ def __init__(self, num_workers=None): if self._num_workers == 0: self._num_workers = mp.cpu_count() - def prepareCppSim_node(self, node): + def prepare_cpp_sim_node(self, node: NodeProto) -> tuple[NodeProto, Literal[False]]: + """Prepare CppSim node.""" if is_hls_node(node): _codegen_single_node(node, self.model) return (node, False) - def apply(self, model): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Remove old nodes from the current model + """Apply transformation.""" self.model = copy.deepcopy(model) old_nodes = [] - for i in range(len(model.graph.node)): + for _i in range(len(model.graph.node)): old_nodes.append(model.graph.node.pop()) # Execute transformation in parallel with mp.Pool(self._num_workers) as p: - new_nodes_and_bool = p.map(self.prepareCppSim_node, old_nodes, chunksize=1) + new_nodes_and_bool = p.map(self.prepare_cpp_sim_node, old_nodes, chunksize=1) # extract nodes and check if the transformation needs to run again # Note: .pop() had initially reversed the node order diff --git a/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py b/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py index 1a146370a1..11bc242d88 100644 --- a/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py +++ b/src/finn/transformation/fpgadataflow/raise_scalar_to_rank1.py @@ -27,6 +27,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for raise scalar to rank1.""" from __future__ import annotations from collections.abc import Iterable @@ -47,9 +48,11 @@ class RaiseScalarToRank1(Transformation): """ def __init__(self): + """Initialize instance.""" super().__init__() def _tensor_names(self, model: ModelWrapper) -> Iterable[str]: + """Return tensor names.""" graph = model.graph tensors = [vi.name for vi in graph.value_info] tensors += [inp.name for inp in graph.input] @@ -62,6 +65,7 @@ def _tensor_names(self, model: ModelWrapper) -> Iterable[str]: yield name def apply(self, model: ModelWrapper): + """Apply transformation.""" graph_modified = False for tensor_name in self._tensor_names(model): tensor_shape = model.get_tensor_shape(tensor_name) diff --git a/src/finn/transformation/fpgadataflow/replicate_stream.py b/src/finn/transformation/fpgadataflow/replicate_stream.py index fa7fd6a275..a48b99fe73 100644 --- a/src/finn/transformation/fpgadataflow/replicate_stream.py +++ b/src/finn/transformation/fpgadataflow/replicate_stream.py @@ -3,6 +3,7 @@ # per line. Black, however, formats some lines going beyond this. # Utility for handling ONNX nodes and tensors +"""Module for replicate stream.""" from onnx import TensorProto from onnx import helper as oh @@ -23,8 +24,10 @@ # consumers class InferReplicateStream(Transformation): # Applies the transform to a whole model graph + """Transformation for Infer Replicate Stream.""" def apply(self, model: ModelWrapper): # noqa # Get the model graph out of the model wrapper object + """Apply transformation.""" graph = model.graph # Keep track of whether the graph has been modified graph_modified = False diff --git a/src/finn/transformation/fpgadataflow/set_loop_boundary.py b/src/finn/transformation/fpgadataflow/set_loop_boundary.py index 5f300f8b4b..83f21f9d49 100644 --- a/src/finn/transformation/fpgadataflow/set_loop_boundary.py +++ b/src/finn/transformation/fpgadataflow/set_loop_boundary.py @@ -6,8 +6,16 @@ # ############################################################################ +"""Module for set loop boundary.""" + import onnx +from ast import literal_eval +from onnx import NodeProto +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation +from typing import Literal + +from finn.util.exception import FINNInternalError class SetLoopBoundary(Transformation): @@ -18,19 +26,25 @@ class SetLoopBoundary(Transformation): :param tensor_range: Tuple containing start and end tensor names (start_tensor, end_tensor). """ - def __init__(self, node_metadata, node_range=None, tensor_range=None): + def __init__( + self, + node_metadata: dict[str, str], + node_range: tuple[NodeProto, NodeProto] | None = None, + tensor_range: tuple[str, str] | None = None, + ) -> None: + """Initialize instance.""" super().__init__() if (node_range is None and tensor_range is None) or ( node_range is not None and tensor_range is not None ): - raise ValueError( + raise FINNInternalError( "You must provide either a node_range or a tensor_range, but not both or none." ) - self.start_node = None - self.end_node = None - self.start_tensor = None - self.end_tensor = None + self.start_node: NodeProto | None = None + self.end_node: NodeProto | None = None + self.start_tensor: str | None = None + self.end_tensor: str | None = None if node_range: self.start_node, self.end_node = node_range @@ -39,7 +53,8 @@ def __init__(self, node_metadata, node_range=None, tensor_range=None): self.node_metadata = node_metadata - def apply(self, model): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: + """Apply transformation.""" graph = model.graph # Transformation can only be applied to cleaned up (const-folded) FINN-ONNX model @@ -74,7 +89,7 @@ def apply(self, model): # that the set metadata in the beginning applies to all nodes else: for key, value in self.node_metadata.items(): - values = eval(value) + values = literal_eval(value) node.metadata_props.append( onnx.StringStringEntryProto( key=key, value=f"['{values[0]}', '{values[1]}1']" diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 8a503afa89..f460d6a1f1 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -265,6 +265,7 @@ def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: nodes_graph: list[NodeProto] = [] def _find_first_non_fifo_pred(pred: NodeProto) -> NodeProto | None: + """Return the first non fifo predecessor.""" if "FIFO" in pred.op_type: # Replace FIFOs with their predecessor pred_fifo = self.model.find_direct_predecessors(pred) @@ -282,6 +283,7 @@ def _find_first_non_fifo_pred(pred: NodeProto) -> NodeProto | None: return pred def _find_first_non_fifo_succ(succ: NodeProto) -> NodeProto | None: + """Return the first non fifo successor.""" if "FIFO" in succ.op_type: # Replace FIFOs with their successor succ_fifo = self.model.find_direct_successors(succ) @@ -562,6 +564,7 @@ def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, str]: # Convert to the format required by the C++ sim config header # (initializer list of pairs of name and iters) def _format_descr_name(s: list[tuple[str, int]]) -> str: + """Return formated Stream Descriptor.""" return ", ".join([f'StreamDescriptor{{"{name}", {iters}}}' for name, iters in s]) instream_descrs = [ @@ -835,6 +838,7 @@ def _build( total_nodes: int, build_dir: Path, ) -> Any: + """Build simulation for a single node.""" nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(InferShapes()) nodemodel = nodemodel.transform(PrepareIP(self.fpgapart, self.clk_ns)) @@ -870,9 +874,11 @@ def _build( # Progress display callback def _callback_progress(name: str) -> Callable: + """Return formatted progress callback.""" nonlocal total_nodes, built_nodes def _f(f: Future) -> None: + """Return callback function for progress display.""" nonlocal total_nodes, built_nodes built_nodes += 1 log.info( @@ -889,6 +895,7 @@ def _f(f: Future) -> None: # Build sims in parallel def _try_int(value: str | None) -> int | None: + """Cast value to int, return None if it fails or if value is None.""" if value is None: return None try: @@ -898,6 +905,7 @@ def _try_int(value: str | None) -> int | None: return parsed if parsed > 0 else None def _parse_slurm_job_cpus_per_node(value: str | None) -> int | None: + """Return parse slurm job cpus per node.""" if value is None: return None # Example values: "16", "16(x2)", "16(x2),8" @@ -909,6 +917,7 @@ def _parse_slurm_job_cpus_per_node(value: str | None) -> int | None: return parsed if parsed > 0 else None def _get_slurm_cpus() -> int | None: + """Return slurm workers while considering cpu allocation.""" cpus_per_task = _try_int(os.environ.get("SLURM_CPUS_PER_TASK")) if cpus_per_task is not None: return cpus_per_task @@ -921,6 +930,7 @@ def _get_slurm_cpus() -> int | None: def _get_slurm_mem_workers(cpus_alloc: int | None) -> int | None: # SLURM memory env vars are in MB. + """Return number of slurm workers while considering memory allocation.""" mem_per_node_mb = _try_int(os.environ.get("SLURM_MEM_PER_NODE")) if mem_per_node_mb is not None: return max(1, mem_per_node_mb // (10 * 1024)) # 10GB per synthesis @@ -1089,6 +1099,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: else: # Run only compilation again, and avoid repeating building of the stitched IPs def _compile(binary: Path) -> None: + """Compile binary in path binary.""" result = subprocess.run( "cmake .;make", shell=True, @@ -1107,9 +1118,11 @@ def _compile(binary: Path) -> None: done = 0 def _progress_callback(binary: str | Path) -> Callable: + """Return progress callback formatted.""" nonlocal done, total def _f(future: Future) -> None: + """Return progress callback.""" nonlocal done, total done += 1 log.info( diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index fd714ea9a4..33827e04b7 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -338,6 +338,7 @@ def _run_binary( with (self.logdir / f"{name}_{process_index}_of_{self.total}.txt").open("w+") as logfile: def _print(msg: str, color: str = "green") -> None: + """Return formatted print.""" if self.progress is None: if is_special_for_display: color = "orange3" @@ -598,7 +599,9 @@ def simulate( return data, merged_data.get("timeout_occurred", False) -class RunLayerParallelSimulation(Transformation): # noqa +class RunLayerParallelSimulation(Transformation): + """Transformation for running Layer Parallel Simulation.""" + def __init__( self, fpgapart: str, diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index 7e261101c2..beb796d5f2 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -99,9 +99,11 @@ def run(self) -> dict[str, IsolatedSimLogData]: # Callback to show progress and save the simulation result def _done_callback_generator(name: str) -> Callable: + """Return done callback generator.""" nonlocal total, done, data, datalock def _f(future: Future) -> None: + """Return f.""" nonlocal total, done, data, datalock with datalock: done += 1 @@ -152,6 +154,7 @@ def _run_binary(self, binary: Path) -> IsolatedSimLogData | None: with self.get_logfile_path(binary).open("w+") as logfile: # Logging helper def write_log(msg: str) -> None: + """Return write log.""" self.write_log(logfile, msg) # Initialize: Start simulation process and give the start command @@ -221,6 +224,8 @@ def write_log(msg: str) -> None: class IsolatedSimulation(Simulation): + """Class for Isolated Simulation.""" + def __init__( self, model: ModelWrapper, @@ -230,6 +235,7 @@ def __init__( functional_sim: bool, workers: int | None = None, ) -> None: + """Initialize instance.""" super().__init__(model, simulation_type, fpgapart, clk_ns, functional_sim, workers) def simulate(self) -> IsoSimLogDataByLayer: @@ -309,6 +315,7 @@ def calculate_upper_bounds(self, data: IsoSimLogDataByLayer) -> dict[str, dict[s # TODO: Proper pytest tests def _any_ready(cycle_data: dict[str, int]) -> bool: + """Return any ready.""" for key in cycle_data.keys(): if ( key not in ["totalCycles", "inputCyclesDone", "inputCyclesTarget"] @@ -571,6 +578,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Find the index/tensor that connects the predecessor and the current one # Use that index to retrieve the fifo depth between them and save it def get_index(a: Any, values: Any) -> int | None: + """Return index.""" for i, val in enumerate(values): if val == a: return i diff --git a/src/finn/transformation/fpgadataflow/specialize_layers.py b/src/finn/transformation/fpgadataflow/specialize_layers.py index 002c589eeb..8309c602f0 100644 --- a/src/finn/transformation/fpgadataflow/specialize_layers.py +++ b/src/finn/transformation/fpgadataflow/specialize_layers.py @@ -294,10 +294,10 @@ def _vvu_rtl_possible(n, fpgapart): def _elementwise_rtl_possible(n, fpgapart): - # Checks whether RTL-based ElementwiseOp is possible - # Currently, we only support float32 inputs, versal fabric, - # the rhs needs to be a const input while the lhs is the dynamic data input - # and no broadcasting support + """Check whether RTL-based ElementwiseOp is possible + Currently, we only support float32 inputs, versal fabric, + the rhs needs to be a const input while the lhs is the dynamic data input + and no broadcasting support.""" if not is_versal(fpgapart): return False @@ -329,8 +329,8 @@ def _elementwise_rtl_possible(n, fpgapart): def _layernorm_rtl_possible(n, fpgapart): - # Checks whether RTL-based Layernorm is supported - # Currently, we only support float32 inputs and versal fabric + """Check whether RTL-based Layernorm is supported + Currently, we only support float32 inputs and versal fabric.""" if not is_versal(fpgapart): return False node_inst = getCustomOp(n) @@ -341,11 +341,11 @@ def _layernorm_rtl_possible(n, fpgapart): def _requant_rtl_possible(n, fpgapart): - # Checks whether RTL-based Requant is supported - # RTL Requant requires: - # - Integer input (not float) - # - Unsigned output (RTL clips to [0, 2^N-1]) - # - Full range (narrow=0) + """Check whether RTL-based Requant is supported + RTL Requant requires: + - Integer input (not float) + - Unsigned output (RTL clips to [0, 2^N-1]) + - Full range (narrow=0).""" node_inst = getCustomOp(n) idt = node_inst.get_input_datatype(0) odt = node_inst.get_output_datatype(0) @@ -355,7 +355,7 @@ def _requant_rtl_possible(n, fpgapart): class SpecializeLayers(Transformation): - """Specialize all layers to either HLS or RTL variants""" + """Specialize all layers to either HLS or RTL variants.""" def __init__(self, fpgapart): """Initialize the SpecializeLayers transformation. diff --git a/src/finn/transformation/fpgadataflow/vivado_power_estimation.py b/src/finn/transformation/fpgadataflow/vivado_power_estimation.py index 7f78c2f10b..238f487a7f 100644 --- a/src/finn/transformation/fpgadataflow/vivado_power_estimation.py +++ b/src/finn/transformation/fpgadataflow/vivado_power_estimation.py @@ -1,3 +1,4 @@ +"""Module for vivado power estimation.""" import json import os from qonnx.custom_op.registry import getCustomOp @@ -26,6 +27,7 @@ def __init__( simulate_switching_activity=True, vivado_power_simulation_type="functional", ): + """Initialize instance.""" super().__init__() self.report_dir = report_dir self.clk_period_ns = clk_period_ns @@ -33,6 +35,7 @@ def __init__( self.vivado_power_simulation_type = vivado_power_simulation_type def apply(self, model): + """Apply transformation.""" ooc_res_dict = eval(model.get_metadata_prop("res_total_ooc_synth")) vivado_proj_folder = ooc_res_dict["vivado_proj_folder"] project_path = os.path.join(vivado_proj_folder, "vivadocompile", "vivadocompile.xpr") diff --git a/src/finn/transformation/move_reshape.py b/src/finn/transformation/move_reshape.py index ed43c74663..50029ad9e6 100644 --- a/src/finn/transformation/move_reshape.py +++ b/src/finn/transformation/move_reshape.py @@ -1,17 +1,27 @@ +"""Module to removes a flatten node if it is between two fpgadataflow nodes.""" + +from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation from qonnx.util.basic import get_by_name +from typing import TYPE_CHECKING, cast +from finn.util.exception import FINNInternalError from finn.util.fpgadataflow import is_fpgadataflow_node from finn.util.logging import log +if TYPE_CHECKING: + import numpy as np + from onnx import NodeProto + class RemoveCNVtoFCFlatten(Transformation): """Removes a flatten node if it is between two fpgadataflow nodes. For an NHWC-Conv to FC transition, the preceding transpose is absorbed. The flatten operation can also be implemented by a reshape node.""" - def apply(self, model): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Apply transformation.""" graph = model.graph graph_modified = False for n in graph.node: @@ -19,6 +29,11 @@ def apply(self, model): if n.op_type == "Flatten" or n.op_type == "Reshape": ishape = model.get_tensor_shape(n.input[0]) oshape = model.get_tensor_shape(n.output[0]) + if ishape is None or oshape is None: + raise FINNInternalError( + f"Could not determine tensor shape for node: {n.name}, " + f"input shape: {ishape}, output shape: {oshape}" + ) if len(oshape) == 2 and ishape[0] == oshape[0]: producer = model.find_producer(n.input[0]) if producer is None: @@ -29,32 +44,48 @@ def apply(self, model): consumer = model.find_consumer(n.output[0]) if is_fpgadataflow_node(consumer): graph_modified = True - consumer.input[0] = n.input[0] + cast("NodeProto", consumer).input[0] = n.input[0] graph.node.remove(n) elif producer.op_type == "Transpose": # transpose + flatten, absorb into following node transp_node = producer # check if transpose converts NHWC to NCHW - perms = list(get_by_name(transp_node.attribute, "perm").ints) + ret = get_by_name(transp_node.attribute, "perm") + if ret is None: + raise FINNInternalError( + f"Could not find 'perm' attribute for node: {transp_node.name}" + ) + perms = list(ret.ints) if perms == [0, 3, 1, 2]: producer = model.find_producer(transp_node.input[0]) if is_fpgadataflow_node(producer): consumer = model.find_consumer(n.output[0]) + if consumer is None: + raise FINNInternalError( + f"Could not find consumer for node: {n.name}" + ) if consumer.op_type.startswith("MVAU"): fc_inst = getCustomOp(consumer) - mw = fc_inst.get_nodeattr("MW") - mh = fc_inst.get_nodeattr("MH") - (b, h, w, c) = model.get_tensor_shape(transp_node.input[0]) + mw = cast("int", fc_inst.get_nodeattr("MW")) + mh = cast("int", fc_inst.get_nodeattr("MH")) + shape = model.get_tensor_shape(transp_node.input[0]) + if shape is None: + raise FINNInternalError( + f"Could not determine tensor shape for node: {n.name}, " + f"input shape: {shape}" + ) + (_b, h, w, c) = shape # absorb transpose into weight matrix, # allowing FC layer to operate on the NHWC input - W = model.get_initializer(consumer.input[1]) - assert ( - W is not None - ), "Initializer for matmul weights is not set." - W_new = W.reshape(c, h, w, mh) - W_new = W_new.transpose((1, 2, 0, 3)) - W_new = W_new.reshape(mw, mh) - model.set_initializer(consumer.input[1], W_new) + w = cast("np.ndarray", model.get_initializer(consumer.input[1])) + if w is None: + raise FINNInternalError( + "Initializer for matmul weights is not set." + ) + w_new = w.reshape(c, h, w, mh) + w_new = w_new.transpose((1, 2, 0, 3)) + w_new = w_new.reshape(mw, mh) + model.set_initializer(consumer.input[1], w_new) # remove transpose & flatten nodes consumer.input[0] = transp_node.input[0] graph.node.remove(n) diff --git a/src/finn/transformation/qonnx/convert_qonnx_to_finn.py b/src/finn/transformation/qonnx/convert_qonnx_to_finn.py index 4e58dd229b..9c85264d90 100644 --- a/src/finn/transformation/qonnx/convert_qonnx_to_finn.py +++ b/src/finn/transformation/qonnx/convert_qonnx_to_finn.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for convert qonnx to finn onnx.""" from qonnx.transformation.base import Transformation from qonnx.transformation.extract_conv_bias import ExtractBiasFromConv from qonnx.transformation.gemm_to_matmul import GemmToMatMul @@ -67,11 +68,13 @@ def __init__( self, filter_function=default_filter_function_generator(max_multithreshold_bit_width=8), ): + """Initialize instance.""" super().__init__() self._filter_function = filter_function def apply(self, model): # Extract the bias from Conv node + """Apply transformation.""" model = model.transform(ExtractBiasFromConv()) # Gemm operations are not supported by FINN, so we convert them to MatMul model = model.transform(GemmToMatMul()) diff --git a/src/finn/transformation/qonnx/fold_quant_weights.py b/src/finn/transformation/qonnx/fold_quant_weights.py index f948f64bb2..ee41e013da 100644 --- a/src/finn/transformation/qonnx/fold_quant_weights.py +++ b/src/finn/transformation/qonnx/fold_quant_weights.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for fold quant weights.""" import numpy as np import qonnx.core.onnx_exec as oxe from onnx import TensorProto, helper @@ -43,6 +44,7 @@ class FoldQuantWeights(Transformation): """ def apply(self, model): + """Apply transformation.""" graph = model.graph node_ind = 0 graph_modified = False diff --git a/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py b/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py index 4b8b3d9f91..9171483c1d 100644 --- a/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py +++ b/src/finn/transformation/qonnx/infer_quant_avg_pool_2d.py @@ -27,6 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for infer quant avg pool 2d.""" import math import numpy as np from onnx import TensorProto, helper @@ -117,6 +118,7 @@ class AvgPoolAndTruncToQuantAvgPool(Transformation): """ def apply(self, model): + """Apply transformation.""" opset_imports = model.get_opset_imports() if "qonnx.custom_op.general" in opset_imports: trunc_opset = opset_imports["qonnx.custom_op.general"] @@ -143,6 +145,7 @@ class AvgPoolAndTruncv1ToQuantAvgPool(Transformation): """ def apply(self, model): + """Apply transformation.""" graph = model.graph node_ind = 0 for n in graph.node: @@ -316,6 +319,7 @@ class AvgPoolAndTruncv2ToQuantAvgPool(Transformation): """ def apply(self, model): + """Apply transformation.""" graph = model.graph node_ind = 0 for node in graph.node: diff --git a/src/finn/transformation/qonnx/quant_act_to_multithreshold.py b/src/finn/transformation/qonnx/quant_act_to_multithreshold.py index 0998bca073..4ddc2aa890 100644 --- a/src/finn/transformation/qonnx/quant_act_to_multithreshold.py +++ b/src/finn/transformation/qonnx/quant_act_to_multithreshold.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for quant act to multithreshold.""" from qonnx.transformation.base import Transformation from finn.transformation.qonnx.qonnx_activation_handlers import ( @@ -36,7 +37,7 @@ def default_filter_function_generator(max_multithreshold_bit_width=8): - """This function generates the default filter function for the + """Generate the default filter function for the ConvertQuantActToMultiThreshold transformation. Per default the returned function disables the conversion of Quant nodes which have a bit width above 8 bit. @@ -45,6 +46,7 @@ def default_filter_function_generator(max_multithreshold_bit_width=8): """ def filter_function(model, q_node): + """Return filter function.""" if q_node.op_type == "Quant": bit_width = model.get_initializer(q_node.input[3]) elif q_node.op_type == "BipolarQuant": @@ -87,10 +89,12 @@ def __init__( self, filter_function=default_filter_function_generator(max_multithreshold_bit_width=8), ): + """Initialize instance.""" super().__init__() self._filter_function = filter_function def apply(self, model): + """Apply transformation.""" graph = model.graph node_ind = 0 graph_modified = False diff --git a/src/finn/transformation/streamline/collapse_repeated.py b/src/finn/transformation/streamline/collapse_repeated.py index db18aeed39..38cb0e95a5 100644 --- a/src/finn/transformation/streamline/collapse_repeated.py +++ b/src/finn/transformation/streamline/collapse_repeated.py @@ -27,6 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Helper for creating ONNX nodes +"""Module for collapsing repeated operations.""" from onnx import helper as oh # QONNX arbitrary precision data types @@ -51,11 +52,13 @@ class CollapseRepeatedOp(Transformation): return a tensor which gives the equivalent result using a single op.""" def __init__(self, op_name, make_collapsed_param_fxn): + """Initialize instance.""" super().__init__() self.op_name = op_name self.make_collapsed_param_fxn = make_collapsed_param_fxn def apply(self, model): + """Apply transformation.""" graph = model.graph node_ind = 0 graph_modified = False @@ -111,6 +114,7 @@ class CollapseRepeatedAdd(CollapseRepeatedOp): """Collapse repeated adder node into a single operation.""" def __init__(self): + """Initialize instance.""" super().__init__("Add", lambda x, y: y + x) @@ -118,6 +122,7 @@ class CollapseRepeatedMul(CollapseRepeatedOp): """Collapse repeated multiplier node into a single operation.""" def __init__(self): + """Initialize instance.""" super().__init__("Mul", lambda x, y: y * x) @@ -125,8 +130,11 @@ def __init__(self): # having the same effect class CollapseRepeatedTranspose(Transformation): # Applies the transform to a whole model graph + """Transformation for collapsing repeated Transpose operations.""" + def apply(self, model: ModelWrapper): # noqa # Get the model graph out of the model wrapper object + """Apply transformation.""" graph = model.graph # Keep track of whether the graph has been modified graph_modified = False diff --git a/src/finn/transformation/streamline/extract_norm_scale_bias.py b/src/finn/transformation/streamline/extract_norm_scale_bias.py index 8e0841cd3a..cbf85b8b3f 100644 --- a/src/finn/transformation/streamline/extract_norm_scale_bias.py +++ b/src/finn/transformation/streamline/extract_norm_scale_bias.py @@ -13,6 +13,7 @@ # ############################################################################ +"""Module for extracting norm scale bias.""" import numpy as np from onnx import TensorProto from onnx import helper as oh @@ -26,9 +27,11 @@ class ExtractNormScaleBias(Transformation): and set initializers to 1 or 0 respectively.""" def __init__(self): + """Initialize instance.""" super().__init__() def apply(self, model): + """Apply transformation.""" graph = model.graph for node in graph.node: if node.op_type == "LayerNormalization": diff --git a/src/finn/transformation/streamline/remove.py b/src/finn/transformation/streamline/remove.py index e9b25691fb..a0d1c7af0b 100644 --- a/src/finn/transformation/streamline/remove.py +++ b/src/finn/transformation/streamline/remove.py @@ -1,4 +1,5 @@ # QONNX wrapper of ONNX model graphs +"""Module to remove identity operations.""" from qonnx.core.modelwrapper import ModelWrapper # QONNX graph transformation base class @@ -18,8 +19,11 @@ # same as the target shape class RemoveIdentityReshape(Transformation): # Applies the transform to a whole model graph + """Transformation to remove Identity Reshape operations.""" + def apply(self, model: ModelWrapper): # noqa # Get the model graph out of the model wrapper object + """Apply transformation.""" graph = model.graph # Keep track of whether the graph has been modified graph_modified = False @@ -57,8 +61,11 @@ def apply(self, model: ModelWrapper): # noqa # the same as the target permutation class RemoveIdentityTranspose(Transformation): # Applies the transform to a whole model graph + """Transformation to remove Identity Transpose operations.""" + def apply(self, model: ModelWrapper): # noqa # Get the model graph out of the model wrapper object + """Apply transformation.""" graph = model.graph # Keep track of whether the graph has been modified graph_modified = False diff --git a/src/finn/transformation/streamline/sign_to_thres.py b/src/finn/transformation/streamline/sign_to_thres.py index eafc071fb6..214770e703 100644 --- a/src/finn/transformation/streamline/sign_to_thres.py +++ b/src/finn/transformation/streamline/sign_to_thres.py @@ -26,6 +26,7 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Module for sign to thres.""" import numpy as np from onnx import helper as oh from qonnx.core.datatype import DataType @@ -36,6 +37,7 @@ class ConvertSignToThres(Transformation): """Convert Sign node instances to MultiThreshold with threshold at 0.""" def apply(self, model): + """Apply transformation.""" graph = model.graph graph_modified = False node_ind = 0 diff --git a/src/finn/transformation/streamline/streamline_plus.py b/src/finn/transformation/streamline/streamline_plus.py index b3d4c1b66e..b44456a0ed 100644 --- a/src/finn/transformation/streamline/streamline_plus.py +++ b/src/finn/transformation/streamline/streamline_plus.py @@ -3,6 +3,7 @@ # fmt: off # Exhaustive composition of ONNX graph transformation +"""Module for streamline plus.""" from qonnx.transformation.batchnorm_to_affine import BatchNormToAffine from qonnx.transformation.composed import ComposedTransformation @@ -27,6 +28,7 @@ # transformations once again) def StreamlinePlus(): # noqa: Uppercase # Return a set of exhaustively applied transformations + """Return ComposedTransformation.""" return ComposedTransformation([ # On skip-connections: prefer pushing scalar multiplication forward # before MoveAddPastMul diff --git a/src/finn/util/fpgadataflow.py b/src/finn/util/fpgadataflow.py index db111e0a48..4b22a217fd 100644 --- a/src/finn/util/fpgadataflow.py +++ b/src/finn/util/fpgadataflow.py @@ -1,3 +1,4 @@ +"""Utility functions for working with fpgadataflow nodes in ONNX graphs.""" # Copyright (c) 2020 Xilinx, Inc. # All rights reserved. # @@ -26,47 +27,45 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +from onnx import NodeProto from qonnx.custom_op.registry import is_custom_op from qonnx.util.basic import get_by_name -def is_fpgadataflow_node(node): - """Returns True if given node is fpgadataflow node. Otherwise False.""" +def is_fpgadataflow_node(node: NodeProto | None) -> bool: + """Return True if given node is fpgadataflow node. Otherwise False.""" is_node = False - if node is not None: - if is_custom_op(node.domain): - n_backend = get_by_name(node.attribute, "backend") - if n_backend is not None: - backend_value = n_backend.s.decode("UTF-8") - if backend_value == "fpgadataflow": - is_node = True + if node is not None and is_custom_op(node.domain): + n_backend = get_by_name(node.attribute, "backend") + if n_backend is not None: + backend_value = n_backend.s.decode("UTF-8") + if backend_value == "fpgadataflow": + is_node = True return is_node -def is_hls_node(node): - """Returns True if given node is hls node. Otherwise False.""" +def is_hls_node(node: NodeProto | None) -> bool: + """Return True if given node is hls node. Otherwise False.""" is_node = False - if node is not None: - if node.domain == "finn.custom_op.fpgadataflow.hls": - n_backend = get_by_name(node.attribute, "backend") - if n_backend is not None: - backend_value = n_backend.s.decode("UTF-8") - if backend_value == "fpgadataflow": - is_node = True + if node is not None and node.domain == "finn.custom_op.fpgadataflow.hls": + n_backend = get_by_name(node.attribute, "backend") + if n_backend is not None: + backend_value = n_backend.s.decode("UTF-8") + if backend_value == "fpgadataflow": + is_node = True return is_node -def is_rtl_node(node): - """Returns True if given node is rtl node. Otherwise False.""" +def is_rtl_node(node: NodeProto | None) -> bool: + """Return True if given node is rtl node. Otherwise False.""" is_node = False - if node is not None: - if node.domain == "finn.custom_op.fpgadataflow.rtl": - n_backend = get_by_name(node.attribute, "backend") - if n_backend is not None: - backend_value = n_backend.s.decode("UTF-8") - if backend_value == "fpgadataflow": - is_node = True + if node is not None and node.domain == "finn.custom_op.fpgadataflow.rtl": + n_backend = get_by_name(node.attribute, "backend") + if n_backend is not None: + backend_value = n_backend.s.decode("UTF-8") + if backend_value == "fpgadataflow": + is_node = True return is_node From ca2b5ac556082629e9199eacc1bdeb0bae2fada0 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 19 May 2026 17:48:48 +0200 Subject: [PATCH 122/170] Some more missing docstrings --- finn_xsi/finn_xsi/sim_engine.py | 1 + src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py | 2 ++ src/finn/analysis/fpgadataflow/floorplan_params.py | 6 ++++-- src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py | 2 ++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py index c9fe880145..f5c8d4a025 100644 --- a/finn_xsi/finn_xsi/sim_engine.py +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -68,6 +68,7 @@ def __init__( p.clear().write_back() def cycle(updates: dict[xsi.Port, str]) -> None: + """Perform one clock cycle with the given port updates.""" # Rising Edge clk.set(1).write_back() if clk2x is not None: diff --git a/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py b/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py index 715b80a296..618d6e889e 100644 --- a/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py +++ b/src/finn/analysis/fpgadataflow/exp_cycles_per_layer.py @@ -1,3 +1,5 @@ +"""Module which contains an analysis pass that estimates the number of cycles +per sample for dataflow layers in a given model.""" # Copyright (c) 2020, Xilinx, Inc. # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. diff --git a/src/finn/analysis/fpgadataflow/floorplan_params.py b/src/finn/analysis/fpgadataflow/floorplan_params.py index f047aeaa0d..7094a063ed 100644 --- a/src/finn/analysis/fpgadataflow/floorplan_params.py +++ b/src/finn/analysis/fpgadataflow/floorplan_params.py @@ -36,11 +36,13 @@ from qonnx.core.modelwrapper import ModelWrapper -def floorplan_params(model: "ModelWrapper"): +def floorplan_params( + model: "ModelWrapper", +) -> dict[str, dict[str, list[int | str | list[str]] | int | str]]: """Gathers SLR and partition IDs from nodes. Returns {node name : {slr, device id, partition id, memory port}}.""" - ret_dict = { + ret_dict: dict[str, dict[str, list[int | str | list[str]] | int | str]] = { "Defaults": { "slr": [-1, ["all"]], "partition_id": [0, ["all"]], diff --git a/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py b/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py index 96b45c7dc6..a2bc348d4d 100644 --- a/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py +++ b/src/finn/analysis/fpgadataflow/hls_synth_res_estimation.py @@ -1,3 +1,5 @@ +"""Module which contains an analysis pass that extracts the resource estimation results from the +Vitis HLS synthesis reports for nodes with an HLS backend in the given model.""" # Copyright (c) 2020, Xilinx # All rights reserved. # From 5b05126992a642958f2e7534f9c7c6f9b28cf352 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Wed, 20 May 2026 14:05:46 +0200 Subject: [PATCH 123/170] Add more docstrings --- finn_xsi/finn_xsi/sim_engine.py | 9 +- .../analysis/fpgadataflow/floorplan_params.py | 1 + .../fpgadataflow/op_and_param_counts.py | 12 +- .../analysis/fpgadataflow/post_synth_res.py | 26 ++- src/finn/builder/build_dataflow_steps.py | 3 + .../fpgadataflow/elementwise_binary.py | 99 +++++++++ .../fpgadataflow/hls/attention_heads_hls.py | 35 +++ .../custom_op/fpgadataflow/outer_shuffle.py | 17 ++ .../fpgadataflow/externalize_params.py | 5 + .../transformation/fpgadataflow/floorplan.py | 4 + .../infer_pixel_padding_deconv.py | 3 + .../transformation/fpgadataflow/insert_dwc.py | 6 + .../fpgadataflow/insert_hook.py | 6 + .../fpgadataflow/insert_iodma.py | 4 + .../fpgadataflow/loop_rolling.py | 33 +++ .../fpgadataflow/transpose_decomposition.py | 7 + .../qonnx/qonnx_activation_handlers.py | 14 ++ src/finn/transformation/streamline/absorb.py | 14 ++ src/finn/transformation/streamline/reorder.py | 119 ++++++++-- src/finn/util/data_packing.py | 13 ++ src/finn/util/mlo_sim.py | 53 +++-- src/finn/util/onnxscript_helpers.py | 185 ++++++++++------ src/finn/util/platforms.py | 208 +++++++++++------- 23 files changed, 672 insertions(+), 204 deletions(-) diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py index f5c8d4a025..d7174f2bef 100644 --- a/finn_xsi/finn_xsi/sim_engine.py +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -15,6 +15,7 @@ # provided via pybind11 import xsi from collections.abc import Generator, Iterator +from numpy._typing._array_like import NDArray from typing import Literal @@ -484,7 +485,9 @@ def read_axilite(self, m_axilite: str, reads: Iterator[int]) -> "SimEngine.AxiLi class AximmRoImage: """Serve a read-only AXI memory image from a byte buffer.""" - def __init__(self, top: "SimEngine", mm_axi: "str", base: int, img: list[str]) -> None: + def __init__( + self, top: "SimEngine", mm_axi: "str", base: int, img: NDArray[np.uint8] + ) -> None: """Bind to AXI memory ports and stage the image data.""" self.mm_axi = mm_axi self.rd_count = 0 @@ -572,7 +575,9 @@ def __call__(self, sim: "SimEngine") -> dict[xsi.Port, str] | None: # noqa: ARG return ret - def aximm_ro_image(self, mm_axi: "str", base: int, img: list[str]) -> "SimEngine.AximmRoImage": + def aximm_ro_image( + self, mm_axi: "str", base: int, img: NDArray[np.uint8] + ) -> "SimEngine.AximmRoImage": """Register a read-only AXI memory image task.""" ret = SimEngine.AximmRoImage(self, mm_axi, base, img) self.enlist(ret) diff --git a/src/finn/analysis/fpgadataflow/floorplan_params.py b/src/finn/analysis/fpgadataflow/floorplan_params.py index 7094a063ed..88ddf4ce4f 100644 --- a/src/finn/analysis/fpgadataflow/floorplan_params.py +++ b/src/finn/analysis/fpgadataflow/floorplan_params.py @@ -1,3 +1,4 @@ +"""Module for gathering floorplanning parameters from nodes in a model.""" # Copyright (c) 2020, Xilinx # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. diff --git a/src/finn/analysis/fpgadataflow/op_and_param_counts.py b/src/finn/analysis/fpgadataflow/op_and_param_counts.py index 885eb8994f..76a3e3f5c2 100644 --- a/src/finn/analysis/fpgadataflow/op_and_param_counts.py +++ b/src/finn/analysis/fpgadataflow/op_and_param_counts.py @@ -1,3 +1,4 @@ +"""Module for gathering operator and parameter counts from nodes in a model.""" # Copyright (c) 2020, Xilinx # All rights reserved. # @@ -27,9 +28,12 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import qonnx.custom_op.registry as registry +from qonnx.core.modelwrapper import ModelWrapper +from finn.util.basic import getHWCustomOp -def aggregate_dict_keys(res_dict): +def aggregate_dict_keys(res_dict: dict[str, dict[str, int]]) -> dict[str, int]: + """Aggregate counts across all nodes in the provided dictionary.""" total_dict = {} for layer in res_dict: layer_res_dict = res_dict[layer] @@ -45,12 +49,12 @@ def aggregate_dict_keys(res_dict): return total_dict -def op_and_param_counts(model): +def op_and_param_counts(model: ModelWrapper) -> dict[str, dict[str, int]]: """Return per-node and aggregate op counts per inference.""" - ret_dict = {} + ret_dict: dict[str, dict[str, int]] = {} for node in model.graph.node: if registry.is_custom_op(node.domain): - inst = registry.getCustomOp(node) + inst = getHWCustomOp(node) if hasattr(inst, "get_op_and_param_counts"): node_op_and_param_counts = inst.get_op_and_param_counts() ret_dict[node.name] = node_op_and_param_counts diff --git a/src/finn/analysis/fpgadataflow/post_synth_res.py b/src/finn/analysis/fpgadataflow/post_synth_res.py index 4874de1503..1aade0757c 100644 --- a/src/finn/analysis/fpgadataflow/post_synth_res.py +++ b/src/finn/analysis/fpgadataflow/post_synth_res.py @@ -1,3 +1,4 @@ +"""Module for post-synthesis resource analysis of FPGA dataflow models.""" # Copyright (c) 2020, Xilinx, Inc. # Copyright (C) 2024, Advanced Micro Devices, Inc. # All rights reserved. @@ -27,16 +28,19 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import os import xml.etree.ElementTree as ET +from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp +from typing import cast from finn.util.fpgadataflow import is_hls_node, is_rtl_node -def post_synth_res(model, override_synth_report_filename=None): - """Extracts the FPGA resource results from the Vivado synthesis. +def post_synth_res( + model: ModelWrapper, override_synth_report_filename: str | None = None +) -> dict[str, dict[str, int]]: + """Extract the FPGA resource results from the Vivado synthesis. Ensure that all nodes have unique names (by calling the GiveUniqueNodeNames transformation) prior to calling this analysis pass to ensure all nodes are visible in the results. @@ -46,8 +50,9 @@ def post_synth_res(model, override_synth_report_filename=None): if override_synth_report_filename is not None: synth_report_filename = override_synth_report_filename else: - synth_report_filename = model.get_metadata_prop("vivado_synth_rpt") - if os.path.isfile(synth_report_filename): + synth_report_filename = cast("str", model.get_metadata_prop("vivado_synth_rpt")) + synth_report_filename = Path(synth_report_filename) + if synth_report_filename.is_file(): tree = ET.parse(synth_report_filename) root = tree.getroot() all_cells = root.findall(".//tablecell") @@ -103,10 +108,11 @@ def post_synth_res(model, override_synth_report_filename=None): else: restype_to_ind = restype_to_ind_default - def get_instance_stats(inst_name): - row = root.findall(".//*[@contents='%s']/.." % inst_name) + def get_instance_stats(inst_name: str) -> dict[str, int] | None: + """Return resource stats for a specific instance name.""" + row = root.findall(f".//*[@contents='{inst_name}']/..") if row != []: - node_dict = {} + node_dict: dict[str, int] = {} row = list(row[0]) for restype, ind in restype_to_ind.items(): node_dict[restype] = int(row[ind].attrib["contents"]) @@ -120,8 +126,8 @@ def get_instance_stats(inst_name): for node in model.graph.node: if node.op_type == "StreamingDataflowPartition": - sdp_model = ModelWrapper(getCustomOp(node).get_nodeattr("model")) - sdp_res_dict = post_synth_res(sdp_model, synth_report_filename) + sdp_model = ModelWrapper(cast("str", getCustomOp(node).get_nodeattr("model"))) + sdp_res_dict = post_synth_res(sdp_model, str(synth_report_filename)) res_dict.update(sdp_res_dict) elif is_hls_node(node) or is_rtl_node(node): node_dict = get_instance_stats(node.name) diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 70e9729164..501363529f 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -153,6 +153,7 @@ def register_build_dataflow_step( """ def _decorator(step_fn: BuildDataflowStep) -> BuildDataflowStep: + """Register the build step function in the lookup table.""" key = step_name if step_name is not None else step_fn.__name__ if key in build_dataflow_step_lookup: raise ValueError(f"Duplicate build step registration: {key}") @@ -726,6 +727,7 @@ def step_convert_to_hw(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWr def apply_if_relevant( model: ModelWrapper, op_types: list[str], transform: Transformation, desc: str = "" ) -> ModelWrapper: + """Apply a transform only if relevant op types exist in the model.""" # Check if any of the relevant op types exist in the model if any(len(model.get_nodes_by_op_type(op_type)) > 0 for op_type in op_types): if desc: @@ -1170,6 +1172,7 @@ def step_insert_dwc(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapp def verify_mlo(model: ModelWrapper, cfg: DataflowBuildConfig, step: str): + """Verify a multi-layer offload model via RTL simulation.""" finn_loop = model.get_nodes_by_op_type("FINNLoop") # TODO: allow for multiple FINNLoops mlo_prehook = mlo_prehook_func_factory(finn_loop[0]) diff --git a/src/finn/custom_op/fpgadataflow/elementwise_binary.py b/src/finn/custom_op/fpgadataflow/elementwise_binary.py index b03e85b64a..9befa058cd 100644 --- a/src/finn/custom_op/fpgadataflow/elementwise_binary.py +++ b/src/finn/custom_op/fpgadataflow/elementwise_binary.py @@ -26,6 +26,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Elementwise binary HW custom ops for FINN dataflow.""" + import numpy as np # Helper for creating ONNX nodes @@ -45,6 +47,8 @@ # Generic implementation for elementwise binary operations class ElementwiseBinaryOperation(HWCustomOp): + """Base class for elementwise binary dataflow operators.""" + # Specifies the elementwise operation to be implemented # Format: (Identifier, Python, C++, RTL) _operation: tuple[str, np.ufunc, str, str] | None = None @@ -52,25 +56,30 @@ class ElementwiseBinaryOperation(HWCustomOp): # Numpy operation available as property @property def npy_op(self) -> np.ufunc: + """Return the NumPy ufunc implementing the operation.""" return self._operation[1] # C++ operation template available as property @property def cpp_op(self) -> str: + """Return the C++ operator template string.""" return self._operation[2] # RTL operation template available as property @property def rtl_op(self) -> str: + """Return the RTL operator template string.""" return self._operation[3] # Initializes the operator given an onnx graph node def __init__(self, onnx_node, **kwargs): + """Initialize the custom op wrapper.""" # Just forward all arguments to the init method of the CustomOp base super().__init__(onnx_node, **kwargs) # Defines attributes which must be present on this node def get_nodeattr_types(self): + """Return the node attribute schema for this operator.""" # Start from parent operator class attributes attrs = HWCustomOp.get_nodeattr_types(self) # Update attributes dictionary for new custom operator @@ -120,60 +129,71 @@ def get_nodeattr_types(self): # Datatype attribute as property for convenience @property def lhs_dtype(self): + """Return the lhs data type as a QONNX ``DataType``.""" # Note: Converts from string to QONNX data type return DataType[self.get_nodeattr("lhs_dtype")] # Datatype attribute as property for convenience @property def rhs_dtype(self): + """Return the rhs data type as a QONNX ``DataType``.""" # Note: Converts from string to QONNX data type return DataType[self.get_nodeattr("rhs_dtype")] # Datatype attribute as property for convenience @property def out_dtype(self): + """Return the output data type as a QONNX ``DataType``.""" # Note: Converts from string to QONNX data type return DataType[self.get_nodeattr("out_dtype")] # Shape attribute as property for convenience @property def lhs_shape(self) -> np.ndarray: + """Return the stored lhs shape.""" return cast("np.ndarray", self.get_nodeattr("lhs_shape")) # Shape attribute as property for convenience @property def rhs_shape(self) -> np.ndarray: + """Return the stored rhs shape.""" return cast("np.ndarray", self.get_nodeattr("rhs_shape")) # Shape attribute as property for convenience @property def out_shape(self) -> np.ndarray: + """Return the stored output shape.""" return cast("np.ndarray", self.get_nodeattr("out_shape")) # Style attribute as property for convenience @property def lhs_style(self): + """Return the lhs input style attribute.""" return self.get_nodeattr("lhs_style") # Style attribute as property for convenience @property def rhs_style(self): + """Return the rhs input style attribute.""" return self.get_nodeattr("rhs_style") # Number of parallel processed elements as property for convenience @property def pe(self): + """Return the parallelism (PE) setting.""" return self.get_nodeattr("PE") # Checks whether the last axis is broadcast @property def broadcast_last_axis(self): + """Return True if only one input broadcasts the last axis.""" return (self.lhs_shape[-1] == 1) != (self.rhs_shape[-1] == 1) # Makes an operation compatible with the output shape for shape inference # Note: Propagates shape forward, i.e., never asks for the shape of the # output, even if it seems easier. def make_shape_compatible_op(self, model: ModelWrapper) -> NodeProto: + """Return an ONNX op used for shape inference with validated shapes.""" # Get the node wrapped by this custom op node = self.onnx_node # There must be exactly two inputs to the binary operation @@ -202,6 +222,7 @@ def make_shape_compatible_op(self, model: ModelWrapper) -> NodeProto: # Infers the datatype of the node output def infer_node_datatype(self, model: ModelWrapper) -> None: + """Infer and update output datatype metadata.""" # Get the node wrapped by this custom op node = self.onnx_node # Test for changing left-hand-side input datatype @@ -224,6 +245,7 @@ def infer_node_datatype(self, model: ModelWrapper) -> None: model.set_tensor_datatype(node.output[0], self.out_dtype) def execute_node(self, context, graph) -> None: + """Execute the elementwise op in a numpy-based context.""" # Get the node wrapped by this custom op node = self.onnx_node # Get the inputs out of the execution context @@ -249,26 +271,31 @@ def execute_node(self, context, graph) -> None: # Gets the datatype of input at index ind def get_input_datatype(self, ind=0): + """Return input datatype for the requested index.""" # Get input data type by index, order inputs from left to right return [self.lhs_dtype, self.rhs_dtype][ind] # Gets the datatype of the output at index ind def get_output_datatype(self, ind=0): + """Return output datatype for the requested index.""" # There is only one output, the type is set as an attribute return self.out_dtype # Gets the shape of the input at index ind without folding def get_normal_input_shape(self, ind=0): + """Return the non-folded input shape for the requested index.""" # Input shapes are stored as a node attributes return [self.lhs_shape, self.rhs_shape][ind] # Gets the shape of the output at index ind without folding def get_normal_output_shape(self, ind=0): + """Return the non-folded output shape.""" # The output shape is stored as a node attribute return self.out_shape # Gets the shape of the input at index ind with folding def get_folded_input_shape(self, ind=0): + """Return the folded input shape for the requested index.""" # Get the normal shape before applying folding *num_inputs, num_elems = self.get_normal_input_shape(ind=ind) # Folding only applies if the folded axis is not broadcast @@ -283,6 +310,7 @@ def get_folded_input_shape(self, ind=0): # Gets the shape of the output at index ind with folding def get_folded_output_shape(self, ind=0): + """Return the folded output shape.""" # Get the normal shape before applying folding *num_inputs, num_elems = self.get_normal_output_shape(ind=ind) # Valid folding requires the PE to divide the number of elements @@ -297,6 +325,7 @@ def calc_wmem(self): # Widths of the input data stream of the input at index ind def get_instream_width(self, ind=0): + """Return the input stream width in bits.""" mem_mode = self.get_nodeattr("mem_mode") mlo_enabled = self.get_nodeattr("mlo_max_iter") lhs_const = self.get_nodeattr("lhs_style") == "const" @@ -320,6 +349,7 @@ def get_instream_width(self, ind=0): # Widths of the output data stream of the output at index ind def get_outstream_width(self, ind=0): + """Return the output stream width in bits.""" # Get the number of bits used to represent the output o_bits = self.get_output_datatype(ind).bitwidth() # Parallelism is the number of elements in the last dimension of the @@ -331,6 +361,7 @@ def get_outstream_width(self, ind=0): # Minimizes the width of the accumulator data type, 'accumulator width' here # due to convention, it is actually the output data type def minimize_accumulator_width(self, model: ModelWrapper): + """Minimize output bit-width when possible.""" # If any of the inputs is not an integer, the bit-width cannot be # minimized if not all([self.lhs_dtype.is_integer(), self.rhs_dtype.is_integer()]): @@ -354,6 +385,7 @@ def minimize_accumulator_width(self, model: ModelWrapper): # Derives the optimal width of the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output data type for this operation.""" # Depends on the actual operation performed and must be specialized by # the concrete implementations raise NotImplementedError( @@ -363,6 +395,7 @@ def _derive_out_dtype(self, model: ModelWrapper): # Minimizes the width of the weight data type, 'weight' here due to # convention, it actually applies to any constant initializer input def minimize_weight_bit_width(self, model: ModelWrapper): + """Minimize constant input bit-widths when possible.""" # Check for an initializer providing the left hand side input lhs = model.get_initializer(self.onnx_node.input[0]) # If the left hand side input is provided as initializer, minimize the @@ -451,6 +484,7 @@ def minimize_weight_bit_width(self, model: ModelWrapper): # Derives the expected cycles for the elementwise binary operation given the # folding configuration def get_exp_cycles(self): + """Return expected cycles based on the folded output shape.""" # Number of iterations required to process the whole folded input stream # Note: This is all but the PE (last, parallelized) dimension return np.prod(self.get_folded_output_shape()[:-1]) @@ -459,12 +493,15 @@ def get_exp_cycles(self): # Derive a specialization to implement elementwise addition of two inputs @register_custom_op class ElementwiseAdd(ElementwiseBinaryOperation): + """Elementwise addition custom op.""" + # Specialize to implement the addition operation of left hand side and right # hand side input _operation = "Add", np.add, "({0} + {1})", None # Derives the output data type according to UG1399 def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for addition.""" # Get the width of the data types of the inputs and the larger of the # two widths lhs_width = self.lhs_dtype.bitwidth() @@ -499,12 +536,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise subtraction of two inputs @register_custom_op class ElementwiseSub(ElementwiseBinaryOperation): + """Elementwise subtraction custom op.""" + # Specialize to implement the subtraction operation of left hand side and # right hand side input _operation = "Sub", np.subtract, "({0} - {1})", None # Derives the output data type according to UG1399 def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for subtraction.""" # Get the width of the data types of the inputs and the larger of the # two widths lhs_width = self.lhs_dtype.bitwidth() @@ -535,25 +575,31 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise absolute difference of two inputs @register_custom_op class ElementwiseAbsDiff(ElementwiseBinaryOperation): + """Elementwise absolute difference custom op.""" + # Specialize to implement the absolute difference operation of left hand side # and right hand side input @property def npy_op(self): + """Return the NumPy abs-diff implementation.""" # NumPy doesn't have a built-in absdiff, so we use a lambda return lambda a, b: np.abs(a - b) # C++ operation template available as property @property def cpp_op(self) -> str: + """Return the C++ operator template string.""" return "({0} > {1} ? {0} - {1} : {1} - {0})" # RTL operation template available as property @property def rtl_op(self) -> str: + """Return the RTL operator template string.""" return None # Derives the output data type - AbsDiff result is always unsigned for integers def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for absolute difference.""" # If either input is floating-point, output is the same float type if self.lhs_dtype in [DataType["FLOAT32"], DataType["FLOAT16"]]: return self.lhs_dtype @@ -573,12 +619,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise multiplication of two inputs @register_custom_op class ElementwiseMul(ElementwiseBinaryOperation): + """Elementwise multiplication custom op.""" + # Specialize to implement the multiplication operation of left hand side and # right hand side input _operation = "Mul", np.multiply, "({0} * {1})", None # Derives the output data type according to UG1399 def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for multiplication.""" # Get the width of the data types of the inputs lhs_width = self.lhs_dtype.bitwidth() rhs_width = self.rhs_dtype.bitwidth() @@ -594,6 +643,8 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise division of two inputs @register_custom_op class ElementwiseDiv(ElementwiseBinaryOperation): + """Elementwise division custom op.""" + # TODO: Not tested due to divide by zero from randomly generated inputs... # Specialize to implement the division operation of left hand side and # right hand side input @@ -601,6 +652,7 @@ class ElementwiseDiv(ElementwiseBinaryOperation): # Derives the output data type according to UG1399 def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for division.""" # Get the width of the data types of the inputs lhs_width = self.lhs_dtype.bitwidth() # Check whether the addition operation is a signed addition @@ -620,12 +672,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise logical and of two inputs @register_custom_op class ElementwiseAnd(ElementwiseBinaryOperation): + """Elementwise logical and custom op.""" + # Specialize to implement the logical and operation of left hand side and # right hand side input _operation = "And", np.logical_and, "({0} && {1})", None # Derives the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for logical and.""" # Treat the boolean output of a logical operation as unsigned integer of # width 1, i.e., a single bit True/False return DataType["BINARY"] @@ -634,12 +689,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise logical or of two inputs @register_custom_op class ElementwiseOr(ElementwiseBinaryOperation): + """Elementwise logical or custom op.""" + # Specialize to implement the logical or operation of left hand side and # right hand side input _operation = "Or", np.logical_or, "({0} || {1})", None # Derives the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for logical or.""" # Treat the boolean output of a logical operation as unsigned integer of # width 1, i.e., a single bit True/False return DataType["BINARY"] @@ -648,12 +706,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise logical xor of two inputs @register_custom_op class ElementwiseXor(ElementwiseBinaryOperation): + """Elementwise logical xor custom op.""" + # Specialize to implement the logical xor operation of left hand side and # right hand side input _operation = "Xor", np.logical_xor, "(bool({0}) != bool({1}))", None # Derives the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for logical xor.""" # Treat the boolean output of a logical operation as unsigned integer of # width 1, i.e., a single bit True/False return DataType["BINARY"] @@ -662,12 +723,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise equality of two inputs @register_custom_op class ElementwiseEqual(ElementwiseBinaryOperation): + """Elementwise logical equal custom op.""" + # Specialize to implement the logical equal operation of left hand side and # right hand side input _operation = "Equal", np.equal, "({0} == {1})", None # Derives the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for logical equal.""" # Treat the boolean output of a logical operation as unsigned integer of # width 1, i.e., a single bit True/False return DataType["BINARY"] @@ -676,12 +740,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise less of two inputs @register_custom_op class ElementwiseLess(ElementwiseBinaryOperation): + """Elementwise logical less custom op.""" + # Specialize to implement the logical less operation of left hand side and # right hand side input _operation = "Less", np.less, "({0} < {1})", None # Derives the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for logical less.""" # Treat the boolean output of a logical operation as unsigned integer of # width 1, i.e., a single bit True/False return DataType["BINARY"] @@ -690,12 +757,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise less or equal of two inputs @register_custom_op class ElementwiseLessOrEqual(ElementwiseBinaryOperation): + """Elementwise logical less-or-equal custom op.""" + # Specialize to implement the logical less or equal operation of left hand # side and right hand side input _operation = "LessOrEqual", np.less_equal, "({0} <= {1})", None # Derives the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for logical less-or-equal.""" # Treat the boolean output of a logical operation as unsigned integer of # width 1, i.e., a single bit True/False return DataType["BINARY"] @@ -704,12 +774,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise greater of two inputs @register_custom_op class ElementwiseGreater(ElementwiseBinaryOperation): + """Elementwise logical greater custom op.""" + # Specialize to implement the logical greater operation of left hand side # and right hand side input _operation = "Greater", np.greater, "({0} > {1})", None # Derives the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for logical greater.""" # Treat the boolean output of a logical operation as unsigned integer of # width 1, i.e., a single bit True/False return DataType["BINARY"] @@ -719,12 +792,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # inputs @register_custom_op class ElementwiseGreaterOrEqual(ElementwiseBinaryOperation): + """Elementwise logical greater-or-equal custom op.""" + # Specialize to implement the logical greater or equal operation of left # hand side and right hand side input _operation = "GreaterOrEqual", np.greater_equal, "({0} >= {1})", None # Derives the output data type def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for logical greater-or-equal.""" # Treat the boolean output of a logical operation as unsigned integer of # width 1, i.e., a single bit True/False return DataType["BINARY"] @@ -733,12 +809,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise bitwise and of two inputs @register_custom_op class ElementwiseBitwiseAnd(ElementwiseBinaryOperation): + """Elementwise bitwise and custom op.""" + # Specialize to implement the bitwise and operation of left hand side and # right hand side input _operation = "BitwiseAnd", np.bitwise_and, "({0} & {1})", None # Derives the output data type according to UG1399 def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for bitwise and.""" # Get the width of the data types of the inputs lhs_width = self.lhs_dtype.bitwidth() rhs_width = self.rhs_dtype.bitwidth() @@ -755,12 +834,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise bitwise or of two inputs @register_custom_op class ElementwiseBitwiseOr(ElementwiseBinaryOperation): + """Elementwise bitwise or custom op.""" + # Specialize to implement the bitwise or operation of left hand side and # right hand side input _operation = "BitwiseOr", np.bitwise_or, "({0} | {1})", None # Derives the output data type according to UG1399 def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for bitwise or.""" # Get the width of the data types of the inputs lhs_width = self.lhs_dtype.bitwidth() rhs_width = self.rhs_dtype.bitwidth() @@ -777,12 +859,15 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise bitwise xor of two inputs @register_custom_op class ElementwiseBitwiseXor(ElementwiseBinaryOperation): + """Elementwise bitwise xor custom op.""" + # Specialize to implement the bitwise xor operation of left hand side and # right hand side input _operation = "BitwiseXor", np.bitwise_xor, "({0} ^ {1})", None # Derives the output data type according to UG1399 def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for bitwise xor.""" # Get the width of the data types of the inputs lhs_width = self.lhs_dtype.bitwidth() rhs_width = self.rhs_dtype.bitwidth() @@ -799,8 +884,11 @@ def _derive_out_dtype(self, model: ModelWrapper): # ElementwiseBitShift - Requires extra attribute selecting the direction @register_custom_op class ElementwiseBitShift(ElementwiseBinaryOperation): + """Elementwise bit shift custom op.""" + # Defines attributes which must be present on this node def get_nodeattr_types(self): + """Return the attribute schema including shift direction.""" # Start from parent operator class attributes attrs = ElementwiseBinaryOperation.get_nodeattr_types(self) # Update attributes dictionary for new custom operator @@ -815,20 +903,24 @@ def get_nodeattr_types(self): @property def npy_op(self): + """Return the NumPy shift operation for the configured direction.""" return {"LEFT": np.left_shift, "RIGHT": np.right_shift}[self.get_nodeattr("direction")] # C++ operation template available as property @property def cpp_op(self) -> str: + """Return the C++ operator template string.""" return {"LEFT": "({0} << {1})", "RIGHT": "({0} >> {1})"}[self.get_nodeattr("direction")] # RTL operation template available as property @property def rtl_op(self) -> str: + """Return the RTL operator template string.""" return None # Derives the output data type just as annotated... def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype from the configured attribute.""" # The attributes decide the output datatype return DataType[self.get_nodeattr("out_dtype")] @@ -846,23 +938,29 @@ def _derive_out_dtype(self, model: ModelWrapper): # Derive a specialization to implement elementwise maximum of two inputs @register_custom_op class ElementwiseMax(ElementwiseBinaryOperation): + """Elementwise maximum custom op.""" + @property def npy_op(self) -> np.ufunc: + """Return the NumPy maximum implementation.""" return np.maximum # C++ operation template available as property @property def cpp_op(self) -> str: + """Return the C++ operator template string.""" odt_hls_name = self.out_dtype.get_hls_datatype_str() return "({0} >= {1} ? (%s){0} : (%s){1})" % (odt_hls_name, odt_hls_name) # RTL operation template available as property @property def rtl_op(self) -> str: + """Return the RTL operator template string.""" return None # Override minimize_weight_bit_width to prevent type incompatibility def minimize_weight_bit_width(self, model: ModelWrapper): + """Skip minimization when float comparisons would be incompatible.""" # For comparison operations like max/min, both operands must have # compatible types. Don't minimize if one side is float and the # minimized constant would become integer. @@ -881,6 +979,7 @@ def minimize_weight_bit_width(self, model: ModelWrapper): super().minimize_weight_bit_width(model) def _derive_out_dtype(self, model: ModelWrapper): + """Derive the output datatype for the max operation.""" if self.lhs_dtype.get_canonical_name().startswith( "FLOAT" ) or self.rhs_dtype.get_canonical_name().startswith("FLOAT"): diff --git a/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py b/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py index 429959f41e..886b1a64ef 100644 --- a/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/attention_heads_hls.py @@ -2,6 +2,8 @@ # Disable formatter. This is deliberately formatted to stay within 80 characters # per line. Black, however, formats some lines going beyond this. +"""HLS backend specializations for attention head split/merge ops.""" + # Numpy math and arrays import numpy as np @@ -20,8 +22,11 @@ class SplitMultiHeads_hls( # noqa: Class name does not follow # CapWords convention SplitMultiHeads, HLSBackend ): + """HLS backend implementation for splitting attention heads.""" + # Node attributes matching the HLS operator def get_nodeattr_types(self): + """Return attribute definitions including HLS backend settings.""" # Start from parent operator class attributes attrs = SplitMultiHeads.get_nodeattr_types(self) # Add the HLSBackend default attributes on top @@ -32,6 +37,7 @@ def get_nodeattr_types(self): # Executes multi-head splitting in C++ simulation def _execute_node_cppsim(self, context, graph): # noqa: graph unused + """Execute the operator using the precompiled C++ simulation.""" # Get the node wrapped by this custom op node = self.onnx_node # Input data is stored in numpy files in the code generation dictionary @@ -59,6 +65,7 @@ def _execute_node_cppsim(self, context, graph): # noqa: graph unused # Maximum width of any ap_int used in this operator def get_ap_int_max_w(self): + """Return the maximum ap_int width used by this operator.""" # Find the widths of the widest input # Note: There is just one input. i_bits_max = self.get_instream_width(ind=0) @@ -75,11 +82,13 @@ def get_ap_int_max_w(self): # Generates list of C++ includes to be placed at the top of the generated # code def global_includes(self): + """Populate global C++ includes for code generation.""" # Currently nothing to include self.code_gen_dict["$GLOBALS$"] = [] # Generates C++ code of type alias, global constant and macro definitions def defines(self, var): + """Emit C++ type aliases and constant definitions.""" # Insert constants and type aliases into the dictionary self.code_gen_dict["$DEFINES$"] = [ # Input and output element datatypes @@ -101,6 +110,7 @@ def defines(self, var): # Generates C++ code for reading data from .npy (numpy format) for testing # in C++ simulation def read_npy_data(self): + """Emit C++ code to read numpy input data for cppsim.""" # Input data is stored in numpy files in the code generation dictionary code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") # Generate function calls for reading the input files into the input @@ -116,6 +126,7 @@ def read_npy_data(self): # Generates C++ code for declaring all streams involved in C++ simulation # for testing def strm_decl(self): + """Emit C++ stream declarations for cppsim.""" # Declare input and output streams # Note: Assumes stream type aliases to be set in defines self.code_gen_dict["$STREAMDECLARATIONS$"] = [ @@ -127,14 +138,17 @@ def strm_decl(self): # Generates C++ code for calling the computation part of the operator def docompute(self): + """Emit C++ compute loop for head splitting.""" # Generates the bit-slicing indices string for the ith split of the # input def split(i): + """Return the C++ bit-slice for the i-th head.""" # Assemble a C++ indexing/bit-slicing string return f"({i + 1} * OPacked::width - 1, {i} * OPacked::width)" # Generates the name of the ith output stream def out(i): + """Return the name of the i-th output stream.""" return f"out{i}_{self.hls_sname()}" # Write the body of the head-splitting top-level function @@ -156,6 +170,7 @@ def out(i): # Generates C++ code for reading the output stream and converting back to # numpy format for testing in C++ simulation def dataoutstrm(self): + """Emit C++ code to write output streams to numpy files.""" # Output data will be stored in numpy files in the # code generation dictionary code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") @@ -170,6 +185,7 @@ def dataoutstrm(self): # Generates the name of the ith output stream def out(i): + """Return the name of the i-th output stream.""" return f"out{i}_{self.hls_sname()}" # Generate code for each output stream @@ -187,6 +203,7 @@ def out(i): # Generates C++ code for saving the output of C++ simulation to a file in # numpy format def save_as_npy(self): + """Emit C++ code for saving outputs as numpy (unused).""" # Note: This seems to be empty in ALL HLSCustomOps. Probably it was used # for something before, which is now integrated into dataoutstrm()? self.code_gen_dict["$SAVEASCNPY$"] = [] @@ -194,6 +211,7 @@ def save_as_npy(self): # Generates essentially the head of the C++ function from which the IP block # will be generated during ipgen, i.e. actual synthesis def blackboxfunction(self): + """Emit the top-level HLS function signature.""" # Insert function head describing the top level interface of the head # splitting operator self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ @@ -213,6 +231,7 @@ def blackboxfunction(self): # Generates C++ pragmas to be inserted into the main function of the C++ # simulation and the ipgen-blackboxfunction as well def pragmas(self): + """Emit HLS pragmas for interface synthesis.""" # Add HLS interface directives specifying how to create RTL ports for # the top-level function arguments self.code_gen_dict["$PRAGMAS$"] = [ @@ -232,6 +251,7 @@ def pragmas(self): # Returns the names of input and output interfaces grouped by protocol def get_verilog_top_module_intf_names(self): + """Return interface names grouped by protocol.""" # Start collecting interface names in a dictionary # starting with clock and reset intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} @@ -259,8 +279,11 @@ class MergeMultiHeads_hls( # noqa: Class name does not follow # CapWords convention MergeMultiHeads, HLSBackend ): + """HLS backend implementation for merging attention heads.""" + # Node attributes matching the HLS operator def get_nodeattr_types(self): + """Return attribute definitions including HLS backend settings.""" # Start from parent operator class attributes attrs = MergeMultiHeads.get_nodeattr_types(self) # Add the HLSBackend default attributes on top @@ -271,6 +294,7 @@ def get_nodeattr_types(self): # Executes multi-head slicing in C++ simulation def _execute_node_cppsim(self, context, graph): # noqa: graph unused + """Execute the operator using the precompiled C++ simulation.""" # Get the node wrapped by this custom op node = self.onnx_node # Input data is stored in numpy files in the code generation dictionary @@ -301,6 +325,7 @@ def _execute_node_cppsim(self, context, graph): # noqa: graph unused # Maximum width of any ap_int used in this operator def get_ap_int_max_w(self): + """Return the maximum ap_int width used by this operator.""" # Find the widths of the widest input # Note: There is just one input. i_bits_max = self.get_instream_width(ind=0) @@ -317,11 +342,13 @@ def get_ap_int_max_w(self): # Generates list of C++ includes to be placed at the top of the generated # code def global_includes(self): + """Populate global C++ includes for code generation.""" # Currently nothing to include self.code_gen_dict["$GLOBALS$"] = [] # Generates C++ code of type alias, global constant and macro definitions def defines(self, var): + """Emit C++ type aliases and constant definitions.""" # Insert constants and type aliases into the dictionary self.code_gen_dict["$DEFINES$"] = [ # Input and output element datatypes @@ -343,6 +370,7 @@ def defines(self, var): # Generates C++ code for reading data from .npy (numpy format) for testing # in C++ simulation def read_npy_data(self): + """Emit C++ code to read numpy input data for cppsim.""" # Input data is stored in numpy files in the code generation dictionary code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") # Generate function calls for reading the input files into the input @@ -362,6 +390,7 @@ def read_npy_data(self): # Generates C++ code for declaring all streams involved in C++ simulation # for testing def strm_decl(self): + """Emit C++ stream declarations for cppsim.""" # Declare input and output streams # Note: Assumes stream type aliases to be set in defines self.code_gen_dict["$STREAMDECLARATIONS$"] = [ @@ -373,6 +402,7 @@ def strm_decl(self): # Generates C++ code for calling the computation part of the operator def docompute(self): + """Emit C++ compute loop for head merging.""" reversed_reads = ", ".join([ f"in{i}_{self.hls_sname()}.read()" for i in reversed(range(self.heads)) @@ -396,6 +426,7 @@ def docompute(self): # Generates C++ code for reading the output stream and converting back to # numpy format for testing in C** simulation def dataoutstrm(self): + """Emit C++ code to write output streams to numpy files.""" # Output data will be stored in numpy files in the code generation # dictionary code_gen_dir = self.get_nodeattr("code_gen_dir_cppsim") @@ -418,6 +449,7 @@ def dataoutstrm(self): # Generates C++ code for saving the output of C++ simulation to a file in # numpy format def save_as_npy(self): + """Emit C++ code for saving outputs as numpy (unused).""" # Note: This seems to be empty in ALL HLSCustomOps. Probably it was used # for something before, which is now integrated into dataoutstrm()? self.code_gen_dict["$SAVEASCNPY$"] = [] @@ -425,6 +457,7 @@ def save_as_npy(self): # Generates essentially the head of the C++ function from which the IP block # will be generated during ipgen, i.e. actual synthesis def blackboxfunction(self): + """Emit the top-level HLS function signature.""" # Insert function head describing the top level interface of the head # splitting operator self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ @@ -444,6 +477,7 @@ def blackboxfunction(self): # Generates C++ pragmas to be inserted into the main function of the C++ # simulation and the ipgen-blackboxfunction as well def pragmas(self): + """Emit HLS pragmas for interface synthesis.""" # Add HLS interface directives specifying how to create RTL ports for # the top-level function arguments self.code_gen_dict["$PRAGMAS$"] = [ @@ -463,6 +497,7 @@ def pragmas(self): # Returns the names of input and output interfaces grouped by protocol def get_verilog_top_module_intf_names(self): + """Return interface names grouped by protocol.""" # Start collecting interface names in a dictionary starting with clock # and reset intf_names = {"clk": ["ap_clk"], "rst": ["ap_rst_n"]} diff --git a/src/finn/custom_op/fpgadataflow/outer_shuffle.py b/src/finn/custom_op/fpgadataflow/outer_shuffle.py index 82afef146c..da2e240eff 100644 --- a/src/finn/custom_op/fpgadataflow/outer_shuffle.py +++ b/src/finn/custom_op/fpgadataflow/outer_shuffle.py @@ -7,6 +7,8 @@ # @author Shane T. Fleming ############################################################################ +"""OuterShuffle custom op and simulation helpers.""" + import math import numpy as np import os @@ -25,6 +27,7 @@ class _NestSim: """ def __init__(self, R, W, *rest): + """Initialize the nested loop simulation state.""" self.R = R self.W = W self.is_terminal = len(rest) == 0 @@ -44,6 +47,7 @@ def __init__(self, R, W, *rest): self.max_rp_retract = max(-self.terminal_rp_inc, self.inner.max_rp_retract) def tick(self): + """Advance the simulation by one step and return increments.""" if self.is_terminal: return self.W, (self.W if self.R else 0), True rp_inc, fp_inc, term = self.inner.tick() @@ -64,9 +68,11 @@ class OuterShuffle(HWCustomOp): Only permutations that do not effect the inner most dimensions are feasible""" def __init__(self, onnx_node, **kwargs): + """Initialize the OuterShuffle custom op wrapper.""" super().__init__(onnx_node, **kwargs) def get_nodeattr_types(self): + """Return the node attribute schema for OuterShuffle.""" my_attrs = { "data_type": ("s", True, ""), "transpose_in_shape": ("ints", True, []), @@ -84,12 +90,15 @@ def get_nodeattr_types(self): return my_attrs def get_normal_input_shape(self, ind=0): + """Return the non-folded input shape.""" return self.get_nodeattr("in_shape") def get_normal_output_shape(self, ind=0): + """Return the non-folded output shape.""" return self.get_nodeattr("out_shape") def execute_node(self, context, graph): + """Execute the outer shuffle using numpy reshape/transpose.""" node = self.onnx_node input_data = context[node.input[0]] input_reshaped = input_data.reshape(self.get_nodeattr("transpose_in_shape")) @@ -98,10 +107,12 @@ def execute_node(self, context, graph): context[node.output[0]] = output_reshaped def get_input_datatype(self, ind=0): + """Return the input datatype.""" data_type = DataType[self.get_nodeattr("data_type")] return data_type def infer_node_datatype(self, model): + """Infer and propagate the node datatype.""" node = self.onnx_node dt = model.get_tensor_datatype(node.input[0]) if dt != self.get_input_datatype(): @@ -113,23 +124,28 @@ def infer_node_datatype(self, model): model.set_tensor_datatype(node.output[0], dt) def verify_node(self): + """Validate node attributes and shapes (not implemented).""" raise NotImplementedError("This function is not yet immplemented.") def get_instream_width(self, ind=0): + """Return the input stream width in bits.""" ibits = self.get_input_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return ibits * simd def get_outstream_width(self, ind=0): + """Return the output stream width in bits.""" obits = self.get_output_datatype().bitwidth() simd = self.get_nodeattr("SIMD") return obits * simd def get_output_datatype(self, ind=0): + """Return the output datatype.""" data_type = DataType[self.get_nodeattr("data_type")] return data_type def get_folded_output_shape(self, ind=0): + """Return the folded output shape for SIMD streaming.""" normal_oshape = list(self.get_normal_output_shape()) simd = self.get_nodeattr("SIMD") assert normal_oshape[-1] % simd == 0, "SIMD must divide into the innermost output dimension" @@ -138,6 +154,7 @@ def get_folded_output_shape(self, ind=0): return tuple(folded_oshape) def get_folded_input_shape(self, ind=0): + """Return the folded input shape for SIMD streaming.""" normal_ishape = list(self.get_normal_input_shape()) simd = self.get_nodeattr("SIMD") assert normal_ishape[-1] % simd == 0, "SIMD must divide into the innermost input dimension" diff --git a/src/finn/transformation/fpgadataflow/externalize_params.py b/src/finn/transformation/fpgadataflow/externalize_params.py index d18a137a5e..1a0d7f715e 100644 --- a/src/finn/transformation/fpgadataflow/externalize_params.py +++ b/src/finn/transformation/fpgadataflow/externalize_params.py @@ -26,6 +26,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Transformation for externalizing weight parameters via IODMA inputs.""" + from qonnx.transformation.base import Transformation from qonnx.util.basic import get_by_name @@ -36,12 +38,15 @@ class ExternalizeParams(Transformation): marked as external using mem_mode="external".""" def __init__(self): + """Initialize the transformation.""" super().__init__() def apply(self, model): + """Apply the transformation to externalize DMA-fed weights.""" graph_modified = False def filter_fc_extw(x): + """Return True for IODMA nodes using external wrap burst mode.""" if x.op_type == "IODMA_hls": burst_mode = get_by_name(x.attribute, "burstMode") if burst_mode is not None: diff --git a/src/finn/transformation/fpgadataflow/floorplan.py b/src/finn/transformation/fpgadataflow/floorplan.py index 4a8332ff1a..1799356c88 100644 --- a/src/finn/transformation/fpgadataflow/floorplan.py +++ b/src/finn/transformation/fpgadataflow/floorplan.py @@ -26,6 +26,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Floorplanning transformation for dataflow graphs.""" + import json from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation @@ -52,10 +54,12 @@ class Floorplan(Transformation): """ def __init__(self, floorplan=None): + """Initialize the transform with an optional floorplan file.""" super().__init__() self.user_floorplan = floorplan def apply(self, model): + """Apply floorplanning and partition assignment to the model.""" # read in a user-specified floorplan or generate a default one if self.user_floorplan is None: self.user_floorplan = model.analysis(floorplan_params) diff --git a/src/finn/transformation/fpgadataflow/infer_pixel_padding_deconv.py b/src/finn/transformation/fpgadataflow/infer_pixel_padding_deconv.py index e0c3cf8e40..bea5efeaab 100644 --- a/src/finn/transformation/fpgadataflow/infer_pixel_padding_deconv.py +++ b/src/finn/transformation/fpgadataflow/infer_pixel_padding_deconv.py @@ -1,3 +1,5 @@ +"""Infer pixel-padding lowering for ConvTranspose nodes.""" + import numpy as np from onnx import TensorProto, helper from qonnx.transformation.base import Transformation @@ -16,6 +18,7 @@ class InferPixelPaddingDeconv(Transformation): """ def apply(self, model): + """Apply ConvTranspose lowering into pixel padding and matmul.""" graph = model.graph node_ind = 0 graph_modified = False diff --git a/src/finn/transformation/fpgadataflow/insert_dwc.py b/src/finn/transformation/fpgadataflow/insert_dwc.py index 1b157c083c..43a30d1792 100644 --- a/src/finn/transformation/fpgadataflow/insert_dwc.py +++ b/src/finn/transformation/fpgadataflow/insert_dwc.py @@ -26,6 +26,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Insert StreamingDataWidthConverter nodes between mismatched layers.""" + from onnx import helper as oh from qonnx.custom_op.registry import getCustomOp from qonnx.transformation.base import Transformation @@ -34,10 +36,12 @@ def _is_dwc_node(node): + """Return True if the node is a data width converter.""" return node.op_type.startswith("StreamingDataWidthConverter") def _suitable_node(node): + """Return True if the node can participate in DWC insertion.""" if node is not None: if is_fpgadataflow_node(node): if _is_dwc_node(node): @@ -55,9 +59,11 @@ class InsertDWC(Transformation): """Add data width converters between layers where necessary.""" def __init__(self): + """Initialize the transformation.""" super().__init__() def apply(self, model): + """Insert DWC nodes where stream widths do not match.""" graph = model.graph node_ind = -1 graph_modified = False diff --git a/src/finn/transformation/fpgadataflow/insert_hook.py b/src/finn/transformation/fpgadataflow/insert_hook.py index 3895b9cf58..095a4439af 100644 --- a/src/finn/transformation/fpgadataflow/insert_hook.py +++ b/src/finn/transformation/fpgadataflow/insert_hook.py @@ -27,6 +27,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Insert hook layers into dataflow graphs based on node attributes.""" + import numpy as np from onnx import TensorProto from onnx import helper as oh @@ -38,12 +40,14 @@ def _is_hook_node(node): + """Return True if the node is a supported hook op.""" if node.op_type in ["CheckSum_hls"]: return True return False def _suitable_node(node): + """Return True if the node can have a hook inserted after it.""" if node is not None: if is_hls_node(node) or is_rtl_node(node): if not _is_hook_node(node): @@ -58,9 +62,11 @@ class InsertHook(Transformation): 'output_hook' specified""" def __init__(self): + """Initialize the transformation.""" super().__init__() def apply(self, model): + """Insert supported hook nodes after eligible operators.""" list_supported_hooks = ["checksum"] graph = model.graph node_ind = -1 diff --git a/src/finn/transformation/fpgadataflow/insert_iodma.py b/src/finn/transformation/fpgadataflow/insert_iodma.py index 353fbb29b6..527b9702eb 100644 --- a/src/finn/transformation/fpgadataflow/insert_iodma.py +++ b/src/finn/transformation/fpgadataflow/insert_iodma.py @@ -26,6 +26,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Insert IODMA nodes at graph boundaries and external weights.""" + import math import numpy as np from onnx import TensorProto @@ -47,6 +49,7 @@ def __init__( insert_output=True, insert_extmemw=True, ): + """Initialize the transformation with insertion options.""" super().__init__() self.insert_input = insert_input self.insert_output = insert_output @@ -92,6 +95,7 @@ def get_mem_init(self, weights, pe, simd): return reshaped_w def apply(self, model): + """Insert IODMA nodes for inputs, outputs, and external weights.""" modified = False # only makes sense for a pure fpgadataflow graph -- so we check! all_nodes = list(model.graph.node) diff --git a/src/finn/transformation/fpgadataflow/loop_rolling.py b/src/finn/transformation/fpgadataflow/loop_rolling.py index 93b5d09330..38a6d11342 100644 --- a/src/finn/transformation/fpgadataflow/loop_rolling.py +++ b/src/finn/transformation/fpgadataflow/loop_rolling.py @@ -11,6 +11,8 @@ ################################################################################### # ruff: noqa: SLF001 +"""Loop extraction and rolling utilities for fpgadataflow graphs.""" + import copy import numpy as np import onnx @@ -55,6 +57,7 @@ def same_values(inputs): def build_loop_replace_pattern(graph, LoopBody): + """Build a replacement pattern graph for a repeated loop body.""" nodes = osh.find_nodes_of_optype(graph, LoopBody.function.name) iterations = len(nodes) @@ -211,7 +214,10 @@ def build_loop_replace_pattern(graph, LoopBody): class LoopExtraction(Transformation): + """Extract repeated subgraphs into a loop body template.""" + def __init__(self, hierarchy_list: list[list[str]]): + """Initialize with a list of module hierarchies to extract.""" super().__init__() assert isinstance(hierarchy_list, list), "Hierarchy list must be a list of strings" @@ -224,6 +230,7 @@ def __init__(self, hierarchy_list: list[list[str]]): self.loop_body_template = None def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Apply loop extraction and replace nodes with function calls.""" # Apply the loop extraction transformation # Extract the Loop Body from ONNX metadata model_ir = onnxscript.ir.serde.deserialize_model(model.model) @@ -296,6 +303,7 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: def add_finn_datatype_if_needed(tensor): + """Ensure the tensor metadata includes a FINN datatype.""" if not tensor_has_finn_datatype(tensor): if "quant_parameter_tensor_names" not in tensor.meta: tensor.meta["quant_parameter_tensor_names"] = {} @@ -305,10 +313,12 @@ def add_finn_datatype_if_needed(tensor): def validate_loop_type(loop_node: ir.Node): + """Validate that the node is a FINNLoop.""" assert loop_node.op_type == "FINNLoop", "Node is not a FINNLoop" def validate_loop_attributes(loop_node: ir.Node): + """Validate required attributes on the FINNLoop node.""" required_attrs = ["body", "backend", "iteration", "inputDataType", "outputDataType"] for attr in required_attrs: assert attr in loop_node.attributes, f"FINNLoop node missing required attribute: {attr}" @@ -325,6 +335,7 @@ def validate_loop_attributes(loop_node: ir.Node): def tensor_has_finn_datatype(tensor): + """Return True if the tensor metadata includes a FINN datatype.""" return ( "quant_parameter_tensor_names" in tensor.meta and "finn_datatype" in tensor.meta["quant_parameter_tensor_names"] @@ -332,18 +343,22 @@ def tensor_has_finn_datatype(tensor): def finn_datatypes_match(datatype_a, datatype_b): + """Return True when two FINN datatype strings match.""" return datatype_a == datatype_b def tensor_types_match(value_a, value_b): + """Return True when two IR values have the same type.""" return value_a.type == value_b.type def tensor_shapes_match(value_a, value_b): + """Return True when two IR values have the same shape.""" return value_a.shape == value_b.shape def validate_loop_io_tensor_pair(tensor_a, tensor_b): + """Validate type, shape, and FINN datatype alignment for a tensor pair.""" assert tensor_types_match( tensor_a, tensor_b ), f"FINNLoop body activation input/output type mismatch {tensor_a.type} != {tensor_b.type}" @@ -363,6 +378,7 @@ def validate_loop_io_tensor_pair(tensor_a, tensor_b): def validate_loop_io_tensors(loop_node: ir.Node): + """Validate FINNLoop input/output tensor pairs.""" # Validate that loop body activation input and output types and shapes match body_graph = loop_node.attributes["body"].value for i in range(len(body_graph.outputs)): @@ -372,12 +388,15 @@ def validate_loop_io_tensors(loop_node: ir.Node): def validate_loop_node(loop_node: ir.Node): + """Validate FINNLoop node structure and metadata.""" validate_loop_type(loop_node) validate_loop_attributes(loop_node) validate_loop_io_tensors(loop_node) class LoopBodyInputType(Enum): + """Enumeration of loop body input kinds.""" + UNDEFINED = 0 ACTIVATION = 1 CONSTANT = 2 @@ -386,11 +405,15 @@ class LoopBodyInputType(Enum): CONDITION = 5 def __str__(self): + """Return the enum name as a string.""" return self.name class LoopBodyTemplate: + """Encapsulate loop body graph patterns and function templates.""" + def __init__(self, filename): + """Load a loop body template from disk.""" self.load(filename) self._ir_graph.sort() self.pattern = osh.direct_convert_ir_graph_to_pattern(self._ir_graph) @@ -399,11 +422,13 @@ def __init__(self, filename): self.signature = [LoopBodyInputType.UNDEFINED] * len(self._ir_graph.inputs) def _build_ir_function(self): + """Build an IR function from the loop body graph.""" return ir.Function( domain="loop", name="fn_" + self._ir_graph.name, graph=self._ir_graph, attributes=[] ) def _build_function_replace_pattern(self): + """Build a replacement pattern that calls the loop body function.""" inputs = [osh.vdisconnect(copy.copy(x)) for x in self._ir_graph.inputs] outputs = [osh.vdisconnect(copy.copy(x)) for x in self._ir_graph.outputs] @@ -420,6 +445,7 @@ def _build_function_replace_pattern(self): return osh.ReplacementPatternGraph(g) def build_function_match_pattern(self, graph, use_iteration_ext=True): + """Build a pattern that matches inlined loop body instances.""" graph.sort() nodes = osh.find_nodes_of_optype(graph, self.function.name) if use_iteration_ext: @@ -436,23 +462,28 @@ def build_function_match_pattern(self, graph, use_iteration_ext=True): return (pattern, nodes) def load(self, filename): + """Load the loop body template from an ONNX file.""" self._model_proto = onnx.load(filename) self._ir_model = ir.serde.deserialize_model(self._model_proto) self._ir_graph = self._ir_model.graph def update(self): + """Refresh the serialized model proto from the IR graph.""" self._ir_model = ir.Model(self._ir_graph, ir_version=self._model_proto.ir_version) self._model_proto = ir.serde.serialize_model(self._ir_model) def save(self, filename): + """Save the loop body template to an ONNX file.""" self.update() onnx.save(self._model_proto, filename) def set_signature_index(self, index, stype): + """Set the input signature type at the given index.""" self.signature[index] = stype @property def output_signature(self): + """Return the output signature (without iterator/condition inputs).""" # The output signature is the same as the input signature but without the iteration input return self.signature[1:] @@ -461,10 +492,12 @@ class LoopRolling(Transformation): """Boilerplate Transformation for loop rolling in fpgadataflow.""" def __init__(self, loop_body_template): + """Initialize the transformation with a loop body template.""" super().__init__() self.loop_body_template = loop_body_template def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Apply loop rolling to repeated function calls.""" model_ir = onnxscript.ir.serde.deserialize_model(model.model) graph = model_ir.graph LoopBody = self.loop_body_template diff --git a/src/finn/transformation/fpgadataflow/transpose_decomposition.py b/src/finn/transformation/fpgadataflow/transpose_decomposition.py index db8aa42e93..f813c0060a 100644 --- a/src/finn/transformation/fpgadataflow/transpose_decomposition.py +++ b/src/finn/transformation/fpgadataflow/transpose_decomposition.py @@ -6,6 +6,7 @@ # # @author Shane T. Fleming ############################################################################ +"""Decompose Shuffle nodes into inner/outer shuffle operations.""" import numpy as np from collections import deque from onnx import helper @@ -302,21 +303,25 @@ class ShuffleDecomposition(Transformation): """ def __init__(self, debug=False): + """Initialize the transformation with optional debug logging.""" super().__init__() self.debug = debug self._name_counter = 0 def _unique(self, base): + """Return a unique name using the provided base string.""" self._name_counter += 1 return f"{base}_{self._name_counter}" def get_perm(self, node) -> list[int]: + """Extract the permutation list from a Shuffle node.""" for a in node.attribute: if a.name == "perm": return list(a.ints) raise RuntimeError("Unable to determine the permutations from the Transpose node") def apply(self, model): + """Apply shuffle decomposition to eligible Shuffle nodes.""" g = model.graph original_nodes = list(g.node) @@ -427,9 +432,11 @@ class InferInnerOuterShuffles(Transformation): """ def __init__(self): + """Initialize the transformation.""" super().__init__() def apply(self, model): + """Replace Shuffle nodes with InnerShuffle or OuterShuffle nodes.""" graph = model.graph graph_modified = False node_ind = 0 diff --git a/src/finn/transformation/qonnx/qonnx_activation_handlers.py b/src/finn/transformation/qonnx/qonnx_activation_handlers.py index bf78abf9ae..90d0563e77 100644 --- a/src/finn/transformation/qonnx/qonnx_activation_handlers.py +++ b/src/finn/transformation/qonnx/qonnx_activation_handlers.py @@ -25,6 +25,8 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +"""Handlers for converting QONNX activations into FINN graph patterns.""" import numpy as np from abc import ABC, abstractmethod from onnx import TensorProto, helper @@ -291,12 +293,14 @@ class QuantReluHandler(QuantActBaseHandler): @classmethod def valid_predecessor_op_types(self): + """Return supported predecessor op types for quantized ReLU.""" return [ "Relu", "Selu", ] def _check_compatibility(self): + """Validate that the quantized activation is FINN-compatible.""" if self._q_node.op_type == "Quant": q_inst = getCustomOp(self._q_node) narrow = q_inst.get_nodeattr("narrow") @@ -320,6 +324,7 @@ def _check_compatibility(self): raise RuntimeError("Got an unexpected quantizer node type") def _calculate_act_bias(self): + """Calculate activation bias for the replacement pattern.""" # No bias allowed for Relu activations, see: https://github.com/Xilinx/ # brevitas/blob/a5bfd6dc5e030f0047ac1ee47932b60e8e873e17/src/brevitas/ # export/onnx/finn/handler/act.py#L48 @@ -351,6 +356,7 @@ def _calculate_act_bias(self): return bias def _calculate_thresholds(self): + """Calculate MultiThreshold thresholds for the activation.""" # Gather parameters if self._q_node.op_type == "Quant": bit_width = self._model.get_initializer(self._q_node.input[3]) @@ -444,6 +450,7 @@ def _calculate_thresholds(self): return thresholds def _calculate_act_scale(self): + """Calculate activation scale for the replacement pattern.""" # Gather parameters quant_scale = self._model.get_initializer(self._q_node.input[1]) # Calculate scale, see: https://github.com/Xilinx/brevitas/blob/ @@ -453,6 +460,7 @@ def _calculate_act_scale(self): return scale def _remove_activation_node(self, multi_threshold_node): + """Remove the activation node preceding the Quant node.""" # Find the activation node act_node = self._model.find_direct_predecessors(self._q_node) if act_node is None: @@ -482,6 +490,7 @@ class QuantIdentityHandler(QuantActBaseHandler): @classmethod def valid_predecessor_op_types(self): + """Return supported predecessor op types for quantized identity.""" return [ "BatchNormalization", "Sub", @@ -493,6 +502,7 @@ def valid_predecessor_op_types(self): ] def _check_compatibility(self): + """Validate that the quantized identity is FINN-compatible.""" # Gather parameters to check if self._q_node.op_type == "Quant": q_inst = getCustomOp(self._q_node) @@ -515,6 +525,7 @@ def _check_compatibility(self): raise RuntimeError("Got an unexpected quantizer node type") def _calculate_act_bias(self): + """Calculate activation bias for identity activations.""" # Gather parameters q_inst = getCustomOp(self._q_node) if self._q_node.op_type == "Quant": @@ -538,6 +549,7 @@ def _calculate_act_bias(self): return bias def _calculate_thresholds(self): + """Calculate MultiThreshold thresholds for identity activations.""" # Gather parameters quant_scale = self._model.get_initializer(self._q_node.input[1]) q_inst = getCustomOp(self._q_node) @@ -618,6 +630,7 @@ def _calculate_thresholds(self): return thresholds def _calculate_act_scale(self): + """Calculate activation scale for identity activations.""" # Gather parameters if self._q_node.op_type == "Quant": bit_width = self._model.get_initializer(self._q_node.input[3]) @@ -638,5 +651,6 @@ def _calculate_act_scale(self): return scale def _remove_activation_node(self, multi_threshold_node): + """Remove the activation node if one exists (no-op).""" # The Quant identity activation has per definition no explicit activation node return diff --git a/src/finn/transformation/streamline/absorb.py b/src/finn/transformation/streamline/absorb.py index 817c5db43a..2556ce96d1 100644 --- a/src/finn/transformation/streamline/absorb.py +++ b/src/finn/transformation/streamline/absorb.py @@ -26,6 +26,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Streamline transformations that absorb or reorder simple ops.""" + import numpy as np import qonnx.core.data_layout as DataLayout from onnx import helper as oh @@ -48,6 +50,7 @@ class AbsorbSignBiasIntoMultiThreshold(Transformation): MultiThreshold and re-evaluate the output datatype.""" def apply(self, model: ModelWrapper): + """Absorb scalar bias into MultiThreshold when possible.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -189,6 +192,7 @@ class AbsorbAddIntoMultiThreshold(Transformation): values. Only scalar/1D add vectors can be absorbed.""" def apply(self, model): + """Absorb Add nodes into MultiThreshold thresholds.""" graph = model.graph node_ind = 0 graph_modified = False @@ -260,6 +264,7 @@ class AbsorbMulIntoMultiThreshold(Transformation): values. Only *positive* scalar/1D mul vectors can be absorbed.""" def apply(self, model): + """Absorb Mul nodes into MultiThreshold thresholds when allowed.""" graph = model.graph node_ind = 0 graph_modified = False @@ -299,6 +304,7 @@ class FactorOutMulSignMagnitude(Transformation): vector of magnitudes.""" def apply(self, model): + """Factor signed muls into sign and magnitude stages.""" graph = model.graph node_ind = 0 graph_modified = False @@ -337,6 +343,7 @@ class Absorb1BitMulIntoMatMul(Transformation): multiply.""" def apply(self, model): + """Absorb 1-bit muls into MatMul weights where valid.""" graph = model.graph node_ind = 0 graph_modified = False @@ -379,6 +386,7 @@ class Absorb1BitMulIntoConv(Transformation): """Absorb bipolar or binary multiplications into the preceding convolution.""" def apply(self, model): + """Absorb 1-bit muls into Conv weights where valid.""" graph = model.graph node_ind = 0 graph_modified = False @@ -425,6 +433,7 @@ class AbsorbTransposeIntoMultiThreshold(Transformation): and set its data_layout mode to NHWC.""" def apply(self, model): + """Absorb Transpose into MultiThreshold when applicable.""" graph = model.graph node_ind = 0 graph_modified = False @@ -483,6 +492,7 @@ class AbsorbTransposeIntoFlatten(Transformation): by a reshape node with shape [1, -1] and the first input dimension is 1""" def apply(self, model): + """Absorb Transpose into Flatten or equivalent Reshape.""" graph = model.graph graph_modified = False node_ind = 0 @@ -544,6 +554,7 @@ class AbsorbScalarMulAddIntoTopK(Transformation): the TopK output probabilities will change, but the indices won't.""" def apply(self, model): + """Remove scalar mul/add nodes before TopK when safe.""" graph = model.graph node_ind = 0 graph_modified = False @@ -583,6 +594,7 @@ class AbsorbConsecutiveTransposes(Transformation): of the pattern have the same layout.""" def are_opposite_permutations(self, perms1, perms2): + """Return True if two permutations are inverses.""" if len(perms1) != len(perms2): return False assert 0 <= max(perms2) < len(perms2), "invalid permutation" @@ -595,6 +607,7 @@ def are_opposite_permutations(self, perms1, perms2): return True def apply(self, model): + """Remove consecutive Transpose pairs that cancel.""" graph = model.graph graph_modified = False for node in graph.node: @@ -643,6 +656,7 @@ class AbsorbTransposeIntoResize(Transformation): change the Resize node's attributes accordingly.""" def apply(self, model): + """Move Transpose past Resize and adjust scales if needed.""" graph = model.graph node_ind = 0 graph_modified = False diff --git a/src/finn/transformation/streamline/reorder.py b/src/finn/transformation/streamline/reorder.py index c53f16ff78..e400e0716e 100644 --- a/src/finn/transformation/streamline/reorder.py +++ b/src/finn/transformation/streamline/reorder.py @@ -27,6 +27,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Graph-reordering transformations used by FINN streamline passes.""" + import numpy as np import qonnx.core.data_layout as DataLayout from copy import deepcopy @@ -54,6 +56,7 @@ class MoveAddPastMul(Transformation): a single add.""" def apply(self, model: ModelWrapper): + """Apply Add/Mul reordering where safe.""" graph = model.graph node_ind = 0 graph_modified = False @@ -118,6 +121,7 @@ class MoveScalarMulPastMatMul(Transformation): # Applies the transform to a whole model graph def apply(self, model): + """Apply scalar Mul/MatMul reordering where possible.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -175,6 +179,7 @@ class MoveScalarAddPastMatMul(Transformation): next to each other such that they can be collapsed into a single add.""" def apply(self, model): + """Apply scalar Add/MatMul reordering where possible.""" graph = model.graph node_ind = 0 graph_modified = False @@ -232,6 +237,7 @@ class MoveAddPastConv(Transformation): next to each other such that they can be collapsed into a single add.""" def apply(self, model): + """Apply Add/Conv reordering when padding permits.""" graph = model.graph node_ind = 0 graph_modified = False @@ -312,6 +318,7 @@ class MoveScalarMulPastConv(Transformation): next to each other such that they can be collapsed into a single mul.""" def apply(self, model): + """Apply scalar Mul/Conv reordering where possible.""" graph = model.graph node_ind = 0 graph_modified = False @@ -361,6 +368,7 @@ class MoveScalarMulPastConvTranspose(Transformation): next to each other such that they can be collapsed into a single mul.""" def apply(self, model): + """Apply scalar Mul/ConvTranspose reordering where possible.""" graph = model.graph node_ind = 0 graph_modified = False @@ -410,6 +418,7 @@ class MoveMulPastDWConv(Transformation): next to each other such that they can be collapsed into a single mul.""" def apply(self, model): + """Apply channelwise Mul/depthwise Conv reordering.""" graph = model.graph node_ind = 0 graph_modified = False @@ -472,6 +481,7 @@ class MoveMulPastMaxPool(Transformation): single mul.""" def apply(self, model): + """Apply nonnegative Mul/MaxPool reordering when possible.""" graph = model.graph node_ind = 0 graph_modified = False @@ -543,6 +553,7 @@ class MoveLinearPastEltwiseAdd(Transformation): """ def move_node(self, graph, n, prod0, prod1, node_ind): + """Rewire the matched linear/eltwise add pattern in-place.""" # found! move one of the muls to output, remove the other one lin0_in0 = prod0.input[0] lin1_in0 = prod1.input[0] @@ -563,6 +574,7 @@ def move_node(self, graph, n, prod0, prod1, node_ind): graph.node.insert(node_ind - 2, prod0) def apply(self, model): + """Apply linear operation reordering past elementwise Add.""" graph = model.graph node_ind = 0 graph_modified = False @@ -652,6 +664,7 @@ class MoveScalarLinearPastInvariants(Transformation): } def apply(self, model): + """Apply scalar linear reordering past invariant ops.""" graph = model.graph node_ind = 0 graph_modified = False @@ -731,6 +744,7 @@ class MakeMaxPoolNHWC(Transformation): and (NCHWTranspose, MaxPool) into (MaxPoolNHWC, NCHWTranspose).""" def apply(self, model): + """Apply MaxPool/NHWC transpose reordering patterns.""" graph = model.graph node_ind = 0 graph_modified = False @@ -804,6 +818,7 @@ class MakeScaleResizeNHWC(Transformation): """ def apply(self, model): + """Apply NHWC conversions for Resize/Upsample scale inputs.""" graph = model.graph node_ind = 0 for n in graph.node: @@ -905,10 +920,12 @@ class MoveOpPastFork(Transformation): """ def __init__(self, op_name_list): + """Configure which op types should be moved past forks.""" super().__init__() self.ops_to_move = op_name_list def apply(self, model): + """Apply operation replication past fork nodes.""" graph = model.graph graph_modified = False nodes = [n for n in graph.node] @@ -973,26 +990,39 @@ def apply(self, model): class MoveAddPastFork(MoveOpPastFork): + """Move Add operations past fork nodes.""" + def __init__(self): + """Configure the Add-only fork transformation.""" super().__init__(["Add"]) class MoveMulPastFork(MoveOpPastFork): + """Move Mul operations past fork nodes.""" + def __init__(self): + """Configure the Mul-only fork transformation.""" super().__init__(["Mul"]) class MoveLinearPastFork(MoveOpPastFork): + """Move Add/Mul operations past fork nodes.""" + def __init__(self): + """Configure the Add/Mul fork transformation.""" super().__init__(["Add", "Mul"]) class MoveTransposePastFork(MoveOpPastFork): + """Move Transpose operations past fork nodes.""" + def __init__(self): + """Configure the Transpose fork transformation.""" super().__init__(["Transpose"]) def permute_shape(shape, perm): + """Return shape permuted by the given index order.""" new_shape = np.zeros(len(shape)) for i, p in enumerate(perm): new_shape[i] = shape[p] @@ -1003,11 +1033,13 @@ class MoveScalarLinearPastSplit(Transformation): """Move scalar Mul and Add nodes past channel split operation.""" def __init__(self): + """Configure scalar linear ops to move past Split.""" super().__init__() self.ops_to_move = ["Mul", "Add"] self.fork_ops = ["Split"] def apply(self, model): + """Apply scalar linear reordering past Split nodes.""" graph = model.graph graph_modified = False node_ind = 0 @@ -1054,12 +1086,16 @@ def apply(self, model): class MoveTransposePastSplit(Transformation): + """Move Transpose operations past Split nodes.""" + def __init__(self): + """Configure transpose moves past Split.""" super().__init__() self.ops_to_move = ["Transpose"] self.fork_ops = ["Split"] def apply(self, model): + """Apply Transpose/Split reordering where safe.""" graph = model.graph graph_modified = False node_ind = 0 @@ -1107,6 +1143,7 @@ class MoveMaxPoolPastMultiThreshold(Transformation): """Move MaxPool nodes past MultiThreshold nodes on linear segments of the graph.""" def apply(self, model): + """Apply MaxPool/MultiThreshold reordering on linear segments.""" graph = model.graph node_ind = 0 graph_modified = False @@ -1170,6 +1207,7 @@ class MoveFlattenPastTopK(Transformation): is set to -1 and the data layout before the flatten is NHWC with H=W=1""" def apply(self, model): + """Apply Flatten/TopK reordering for NHWC H=W=1 cases.""" graph = model.graph node_ind = 0 graph_modified = False @@ -1230,6 +1268,7 @@ class MoveFlattenPastAffine(Transformation): """Moves a node that implements a (1, -1) reshape past a MatMul, Mul or Add node.""" def apply(self, model): + """Apply Flatten reordering past MatMul/Mul/Add ops.""" graph = model.graph graph_modified = False node_ind = 0 @@ -1316,6 +1355,7 @@ class MoveTransposePastScalarMul(Transformation): """Moves a Transpose node past a scalar Mul node""" def apply(self, model): + """Apply Transpose/scalar Mul reordering where possible.""" graph = model.graph node_ind = 0 graph_modified = False @@ -1377,6 +1417,7 @@ class MoveIdenticalOpPastJoinOp(Transformation): """ def __init__(self, identical_op_list, join_node_list): + """Configure identical ops and join op types to target.""" super().__init__() self.ops_to_move = identical_op_list self.join_node_op = join_node_list @@ -1419,6 +1460,7 @@ def are_producers_identical(self, model, producers): return True def apply(self, model): + """Apply identical-op movement past join nodes.""" graph = model.graph graph_modified = False for n in graph.node: @@ -1454,10 +1496,14 @@ def apply(self, model): class MoveTransposePastJoinAdd(MoveIdenticalOpPastJoinOp): + """Move identical Transpose ops past Add joins.""" + def __init__(self): + """Configure Transpose/Add join reordering.""" super().__init__(["Transpose"], ["Add"]) def are_producers_identical(self, model, producers): + """Return True when all producer permutations match.""" if not super().are_producers_identical(model, producers): return False first_perm = get_by_name(producers[0].attribute, "perm").ints @@ -1468,10 +1514,14 @@ def are_producers_identical(self, model, producers): class MoveTransposePastJoinMul(MoveIdenticalOpPastJoinOp): + """Move identical Transpose ops past Mul joins.""" + def __init__(self): + """Configure Transpose/Mul join reordering.""" super().__init__(["Transpose"], ["Mul"]) def are_producers_identical(self, model, producers): + """Return True when all producer permutations match.""" if not super().are_producers_identical(model, producers): return False first_perm = get_by_name(producers[0].attribute, "perm").ints @@ -1482,10 +1532,14 @@ def are_producers_identical(self, model, producers): class MoveMulPastJoinAdd(MoveIdenticalOpPastJoinOp): + """Move identical Mul ops past Add joins.""" + def __init__(self): + """Configure Mul/Add join reordering.""" super().__init__(["Mul"], ["Add"]) def are_producers_identical(self, model, producers): + """Return True when all producer constants match.""" if not super().are_producers_identical(model, producers): return False first_mul = model.get_initializer(producers[0].input[1]) @@ -1498,10 +1552,14 @@ def are_producers_identical(self, model, producers): class MoveAddPastJoinAdd(MoveIdenticalOpPastJoinOp): + """Move Add ops past Add joins when constants exist.""" + def __init__(self): + """Configure Add/Add join reordering.""" super().__init__(["Add"], ["Add"]) def are_producers_identical(self, model, producers): + """Return True when all producers have constant addends.""" if not super().are_producers_identical(model, producers): return False for producer in producers: @@ -1522,10 +1580,14 @@ def move_node(self, model, n, producers): class MoveTransposePastJoinConcat(MoveIdenticalOpPastJoinOp): + """Move identical Transpose ops past Concat joins.""" + def __init__(self): + """Configure Transpose/Concat join reordering.""" super().__init__(["Transpose"], ["Concat"]) def are_producers_identical(self, model, producers): + """Return True when all producer permutations match.""" if not super().are_producers_identical(model, producers): return False first_perm = get_by_name(producers[0].attribute, "perm").ints @@ -1535,6 +1597,7 @@ def are_producers_identical(self, model, producers): return True def move_node(self, model, n, producers): + """Rewire Concat and Transpose nodes for the matched pattern.""" trans_inputs = [prod.input[0] for prod in producers] concat_out = n.output[0] # Rewire concat inputs @@ -1570,9 +1633,11 @@ class MoveAffinePastJoinConcat(MoveIdenticalOpPastJoinOp): """Applies to scalar linear or channelwise affine ops with the same parameter value""" def __init__(self, linear_ops=["Mul", "Add"]): + """Configure affine ops that can be moved past Concat joins.""" super().__init__(linear_ops, ["Concat"]) def are_producers_identical_scalar_ops(self, model, producers): + """Return True when all scalar op parameters match.""" first_param = model.get_initializer(producers[0].input[1]) for producer in producers: producer_param = model.get_initializer(producer.input[1]) @@ -1582,6 +1647,7 @@ def are_producers_identical_scalar_ops(self, model, producers): return True def are_producers_channelwise_ops(self, channel_dim, model, producers): + """Return True when all producer params are channelwise compatible.""" for producer in producers: producer_input = producer.input[0] num_channels = model.get_tensor_shape(producer_input)[channel_dim] @@ -1595,6 +1661,7 @@ def are_producers_channelwise_ops(self, channel_dim, model, producers): return True def move_node(self, model, n, producers): + """Rewire Concat and affine ops for the matched pattern.""" # check if single input for producer in producers: producer_init = model.get_initializer(producer.input[1]) @@ -1645,20 +1712,27 @@ def move_node(self, model, n, producers): class MoveMulPastJoinConcat(MoveAffinePastJoinConcat): + """Move Mul ops past Concat joins when compatible.""" + def __init__(self): + """Configure Mul/Concat join reordering.""" super().__init__(["Mul"]) class MoveAddPastJoinConcat(MoveAffinePastJoinConcat): + """Move Add ops past Concat joins when compatible.""" + def __init__(self): + """Configure Add/Concat join reordering.""" super().__init__(["Add"]) -# Moves a Squeeze operation past MultiThresholds -# TODO: extend to all operations invariant to or compatible with squeezing class MoveSqueezePastMultiThreshold(Transformation): + """Move Squeeze past MultiThreshold nodes on linear segments.""" + # Applies the transform to a whole model graph def apply(self, model: ModelWrapper): # noqa + """Apply Squeeze/MultiThreshold reordering.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -1716,11 +1790,12 @@ def apply(self, model: ModelWrapper): # noqa return model, graph_modified -# Moves a Squeeze operation past MatMul -# TODO: extend to all operations invariant to or compatible with squeezing class MoveSqueezePastMatMul(Transformation): + """Move Squeeze past MatMul nodes on linear segments.""" + # Applies the transform to a whole model graph def apply(self, model: ModelWrapper): # noqa + """Apply Squeeze/MatMul reordering.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -1781,10 +1856,12 @@ def apply(self, model: ModelWrapper): # noqa return model, graph_modified -# Moves a transpose operator past elementwise addition or multiplication class MoveTransposePastEltwise(Transformation): + """Move Transpose past elementwise Add/Mul when possible.""" + # Applies the transform to a whole model graph def apply(self, model: ModelWrapper): # noqa + """Apply Transpose/elementwise reordering.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -1885,11 +1962,12 @@ def apply(self, model: ModelWrapper): # noqa return model, graph_modified -# Moves elementwise additions past MatMul operations: Applicable if each -# operation has one initializer input class MoveAddPastMatMul(Transformation): + """Move elementwise Add past MatMul when inputs are constant.""" + # Applies the transform to a whole model graph # noqa: Duplicate def apply(self, model: ModelWrapper): # noqa + """Apply Add/MatMul reordering for constant addends.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -1988,10 +2066,12 @@ def apply(self, model: ModelWrapper): # noqa return model, graph_modified -# Moves constant elementwise multiplication past another joining multiplication class MoveConstMulPastJoinMul(Transformation): + """Move constant Mul past a joining Mul when possible.""" + # Applies the transform to a whole model graph # noqa: Duplicate def apply(self, model: ModelWrapper): # noqa + """Apply constant Mul/JoinMul reordering.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -2063,12 +2143,12 @@ def apply(self, model: ModelWrapper): # noqa return model, graph_modified -# Moves elementwise multiplication past elementwise addition if one input to -# each of the operators is a known constant -# Note: Reverse of MoveAddPastMul class MoveMulPastAdd(Transformation): + """Move elementwise Mul past Add when constants allow.""" + # Applies the transform to a whole model graph def apply(self, model: ModelWrapper): # noqa + """Apply Mul/Add reordering for constant inputs.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -2147,11 +2227,12 @@ def apply(self, model: ModelWrapper): # noqa return model, graph_modified -# Moves scalar linear elementwise operations past fork nodes, applies to Add, -# Mul, Sub, Div, etc. class MoveScalarLinearPastFork(Transformation): + """Move scalar linear ops past fork nodes.""" + # Applies the transform to a whole model graph def apply(self, model: ModelWrapper): # noqa + """Apply scalar linear replication across forks.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -2201,11 +2282,12 @@ def apply(self, model: ModelWrapper): # noqa return model, graph_modified -# Moves scalar linear channel-wise operations past fork nodes, applies to Add, -# Mul, Sub, Div, etc. class MoveChannelwiseLinearPastFork(Transformation): + """Move channelwise linear ops past fork nodes.""" + # Applies the transform to a whole model graph def apply(self, model: ModelWrapper): # noqa + """Apply channelwise linear replication across forks.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified @@ -2255,6 +2337,7 @@ def apply(self, model: ModelWrapper): # noqa # Tests whether two shapes can be broadcast according to NumPy # semantics def can_broadcast_to(lhs, rhs): + """Return True if lhs can broadcast to rhs.""" # Broadcasting might raise an exception try: # Try broadcasting the shapes @@ -2306,12 +2389,12 @@ def can_broadcast_to(lhs, rhs): return model, graph_modified -# Moves scale factor, i.e., scalar Mul and Div, past Im2Col (and Col2Im): These -# cannot be handled by MoveScalarLinearPastInvariants as potential padding makes -# Add-Im2Col not commute to Im2Col-Add class MoveScalesPastIm2Col(Transformation): + """Move scalar scales past Im2Col/Col2Im/Pad when safe.""" + # Applies the transform to a whole model graph def apply(self, model: ModelWrapper): # noqa + """Apply scalar scale reordering past Im2Col/Col2Im/Pad.""" # Get the model graph out of the model wrapper object graph = model.graph # Keep track of whether the graph has been modified diff --git a/src/finn/util/data_packing.py b/src/finn/util/data_packing.py index 6f15bd5b29..e4ffd280c3 100644 --- a/src/finn/util/data_packing.py +++ b/src/finn/util/data_packing.py @@ -27,6 +27,8 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Helpers for packing and unpacking FINN tensor data representations.""" + import binascii import numpy as np import os @@ -144,6 +146,7 @@ def pack_innermost_dim_as_hex_string( ndarray = np.asarray(ndarray, dtype=np.float32) def fun(x): + """Pack one innermost-dimension slice into a hex string.""" return array2hexstring(x, dtype, pad_to_nbits, reverse=reverse_inner, prefix=prefix) return np.apply_along_axis(fun, ndarray.ndim - 1, ndarray) @@ -258,6 +261,7 @@ def numpy_to_hls_code(ndarray, dtype, hls_var_name, pack_innermost_dim=True, no_ # define a function to convert a single element into a C++ init string # a single element can be a hex string if we are using packing def elem2str(x): + """Format a single element for C++ array initialization.""" if type(x) is str or type(x) is np.str_: return '%s("%s", 16)' % (hls_dtype, x) if type(x) is np.float32: @@ -376,6 +380,7 @@ def finnpy_to_packed_bytearray( ) def fn(x): + """Convert a sequence of hex strings into byte arrays.""" return np.asarray(list(map(hexstring2npbytearray, x))) if packed_hexstring.ndim == 0: @@ -442,6 +447,7 @@ def prepare_values( reverse_inner, reverse_endian, ): + """Unpack bytes into an integer array matching dtype bitwidth.""" target_bits = dtype.bitwidth() if reverse_endian: @@ -495,6 +501,7 @@ def prepare_values( def unsiged_array_to_signed(data_array, bitsize): + """Convert an unsigned integer array to signed with sign extension.""" # Convert uint to int (do the sign extension) data_type_bits = np.dtype(data_array.dtype).itemsize * 8 shift_sign_value = (2 ** (data_type_bits - bitsize) - 1) << bitsize @@ -513,23 +520,27 @@ def unsiged_array_to_signed(data_array, bitsize): def packed_bytearray_to_finnpy_fast(packed_bytearray, dtype, output_shape): + """Fast path for unpacking byte arrays into float32 arrays.""" as_np_type = packed_bytearray.view(dtype.to_numpy_dt()) return as_np_type.reshape(output_shape).astype(np.float32) def data_prepared_to_finnpy_bipolar(data_prepared): + """Convert prepared integer data into bipolar float32 values.""" data_prepared_converted = data_prepared.astype(np.int32) data_prepared_bipolar = data_prepared_converted * 2 - 1 return data_prepared_bipolar.astype(np.float32) def data_prepared_to_finnpy_ternary(data_prepared): + """Convert prepared integer data into ternary float32 values.""" data_prepared_converted = data_prepared.astype(np.int32) data_prepared = np.where(data_prepared_converted == 3, -1, data_prepared_converted) return data_prepared.astype(np.float32) def data_prepared_to_finnpy_fixed(data_prepared, dtype): + """Convert prepared integer data into fixed-point float32 values.""" int_bits = dtype.int_bits() frac_bits = dtype.frac_bits() # Mask data @@ -546,6 +557,7 @@ def data_prepared_to_finnpy_fixed(data_prepared, dtype): def data_prepared_to_finnpy_int(data_prepared, dtype): + """Convert prepared integer data into signed or unsigned float32 values.""" target_bits = dtype.bitwidth() signed = True if dtype.name.startswith("INT") or dtype.name == "BIPOLAR" else False if signed: @@ -557,6 +569,7 @@ def data_prepared_to_finnpy_int(data_prepared, dtype): def packed_bytearray_to_finnpy_float( packed_bytearray, dtype, reverse_inner=False, reverse_endian=False ): + """Unpack packed bytes into float arrays for FLOAT datatypes.""" target_bits = dtype.bitwidth() if reverse_endian: packed_bytearray = np.flip(packed_bytearray, axis=-1) diff --git a/src/finn/util/mlo_sim.py b/src/finn/util/mlo_sim.py index d01911cb31..00251a5e6e 100644 --- a/src/finn/util/mlo_sim.py +++ b/src/finn/util/mlo_sim.py @@ -27,31 +27,35 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -# This module contains helpers for handling the MLO rtlsimulation. It instantiates -# aximm simulation tasks for handling the aximm interfaces. +"""Module contains helpers for handling the MLO rtlsimulation. It instantiates +aximm simulation tasks for handling the aximm interfaces.""" import numpy as np from collections.abc import Callable +from numpy._typing._array_like import NDArray +from onnx import NodeProto +from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp +from typing import TYPE_CHECKING, cast -from finn import xsi +from finn.util.exception import FINNInternalError +from finn.xsi import SimEngine -SimEngine = xsi.SimEngine if xsi.is_available() else None +if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.rtl.finn_loop import FINNLoop def is_mlo(model: ModelWrapper) -> bool: - """Returns True if the model is an MLO model, false otherwise""" - for node in model.graph.node: - if node.op_type == "FINNLoop": - return True - return False + """Return True if the model is an MLO model, false otherwise.""" + return any(node.op_type == "FINNLoop" for node in model.graph.node) -def dat_file_to_numpy_array(file_path): +def dat_file_to_numpy_array(file_path: Path) -> NDArray[np.uint8]: + """Load a .dat file of hex strings into a uint8 numpy array.""" byte_values = [] - with open(file_path) as file: + with file_path.open() as file: for line in file: hex_string = line.strip() for i in range(len(hex_string) - 2, -1, -2): @@ -64,33 +68,40 @@ def dat_file_to_numpy_array(file_path): return byte_array -def mlo_prehook_func_factory(node) -> Callable[[SimEngine], None]: - """Factory that will construct a prehook function to - setup the axi memory mapped interfaces for MLO validation. +def mlo_prehook_func_factory(node: NodeProto) -> Callable[[SimEngine], None]: + """Construct a prehook function to + setup the axi memory mapped interfaces for MLO validation using a function factory. """ # Get the FINNLoop - finnloop_op = getCustomOp(node) + finnloop_op = cast("FINNLoop", getCustomOp(node)) - finnloop_body = finnloop_op.get_nodeattr("body") + finnloop_body = cast("ModelWrapper", finnloop_op.get_nodeattr("body")) - mvau_hbm_weights = {} + mvau_hbm_weights: dict[int, dict[str, np.ndarray | str | int]] = {} extern_idx = 0 for idx, lb_inp in enumerate(finnloop_body.graph.input): downstream = finnloop_body.find_consumer(lb_inp.name) + if downstream is None: + raise FINNInternalError( + f"Input {lb_inp.name} has no consumer in the FINNLoop body graph" + ) if downstream.op_type.startswith("MVAU"): mvau_hbm_weights[idx] = {} mvau_hbm_weights[idx]["name"] = lb_inp.name datfile = ( f"{finnloop_op.get_nodeattr('code_gen_dir_ipgen')}/memblock_MVAU_rtl_id_{idx}.dat" ) - mvau_hbm_weights[idx]["value"] = dat_file_to_numpy_array(datfile) + mvau_hbm_weights[idx]["value"] = dat_file_to_numpy_array(Path(datfile)) mvau_hbm_weights[idx]["extern_idx"] = extern_idx mvau_hbm_weights[idx]["extern_name"] = f"m_axi_MVAU_id_{idx}" extern_idx += 1 - def mlo_rtlsim_prehook(sim): + def mlo_rtlsim_prehook(sim: SimEngine) -> None: + """Prehook that queues and populates AXI memory for MLO sims.""" sim.aximm_queue("m_axi_hbm") - for name, intf in mvau_hbm_weights.items(): - sim.aximm_ro_image(intf["extern_name"], 0, intf["value"].flatten()) + for intf in mvau_hbm_weights.values(): + sim.aximm_ro_image( + cast("str", intf["extern_name"]), 0, cast("np.ndarray", intf["value"]).flatten() + ) return mlo_rtlsim_prehook diff --git a/src/finn/util/onnxscript_helpers.py b/src/finn/util/onnxscript_helpers.py index d517ec88ca..0c47162b6b 100644 --- a/src/finn/util/onnxscript_helpers.py +++ b/src/finn/util/onnxscript_helpers.py @@ -1,3 +1,5 @@ +"""Helpers for manipulating ONNX Script IR graphs in FINN.""" + import ast from collections.abc import Iterable from onnx_ir import _enums @@ -11,6 +13,9 @@ pattern_builder, ) from qonnx.custom_op.registry import is_custom_op +from typing import Literal, cast + +from finn.util.exception import FINNInternalError class SubGraphView(ir.GraphView): @@ -24,7 +29,10 @@ class SubGraphView(ir.GraphView): subgraph nodes as part of the subgraph. """ - def __init__(self, graph, name, nodes, include_initializers=False): + def __init__( + self, graph: ir.Graph, name: str, nodes: list[ir.Node], include_initializers: bool = False + ) -> None: + """Initialize a subgraph view over the selected nodes.""" self._assert_graph_subset(graph, nodes) self.include_initializers = include_initializers super().__init__( @@ -35,29 +43,33 @@ def __init__(self, graph, name, nodes, include_initializers=False): nodes=nodes, ) - def _assert_graph_subset(self, graph, nodes): + def _assert_graph_subset(self, graph: ir.Graph, nodes: list[ir.Node]) -> None: + """Validate that all nodes belong to the supplied graph.""" for node in nodes: if node.graph != graph: - raise ValueError("All nodes must belong to the same graph") + raise FINNInternalError("All nodes must belong to the same graph") - def _identify_inputs(self, nodes): + def _identify_inputs(self, nodes: list[ir.Node]) -> list[ir.Value]: + """Return the external input values for the subgraph.""" inputs = set() for node in nodes: - for input in node.inputs: - if input.is_graph_input() or input.producer() not in nodes: - inputs.add(input) + for inp in node.inputs: + if inp is not None and (inp.is_graph_input() or inp.producer() not in nodes): + inputs.add(inp) return list(inputs) - def _identify_initializers(self, nodes): + def _identify_initializers(self, nodes: list[ir.Node]) -> list[ir.Value]: + """Return initializers connected to the subgraph when enabled.""" initializers = set() if self.include_initializers: for node in nodes: - for input in node.inputs: - if input.is_initializer(): - initializers.add(input) + for inp in node.inputs: + if inp is not None and inp.is_initializer(): + initializers.add(inp) return list(initializers) - def _identify_outputs(self, nodes): + def _identify_outputs(self, nodes: list[ir.Node]) -> list[ir.Value]: + """Return values that exit the subgraph boundary.""" outputs = set() for node in nodes: for output in node.outputs: @@ -79,7 +91,8 @@ class PytorchMetadataNode: querying instance/class names at different nesting depths. """ - def __init__(self, node): + def __init__(self, node: ir.Node) -> None: + """Wrap a node and parse exporter metadata when present.""" self._node = node if self.check_node_metadata_exists(): @@ -90,25 +103,25 @@ def __init__(self, node): self._node.metadata_props["pkg.torch.onnx.class_hierarchy"] ) - def check_node_metadata_exists(self): - if ( + def check_node_metadata_exists(self) -> bool: + """Return True if the required PyTorch metadata keys are present.""" + return bool( "pkg.torch.onnx.name_scopes" in self._node.metadata_props and "pkg.torch.onnx.class_hierarchy" in self._node.metadata_props - ): - return True - return False + ) - def is_last_level(self, level): - if len(self.instance_metadata) - 1 == level: - return True - return False + def is_last_level(self, level: int) -> bool: + """Return True if the provided level is the last metadata entry.""" + return len(self.instance_metadata) - 1 == level - def get_instance_name(self, depth=0): + def get_instance_name(self, depth: int = 0) -> str | None: + """Return the instance name at the given depth, if available.""" if depth >= len(self.instance_metadata): return None return self.instance_metadata[depth] - def get_class_name(self, depth=0): + def get_class_name(self, depth: int = 0) -> str | None: + """Return the class name at the given depth, if available.""" if depth >= len(self.instance_metadata): return None return self.class_metadata[depth] @@ -140,13 +153,15 @@ class PytorchHierarchyNode: depth matches the length of the serialized ``name_scopes`` list. """ - def __init__(self): + def __init__(self) -> None: + """Initialize an empty hierarchy node.""" self.instance_name = None self.module_type = None self.children = [] self.nodes = [] - def print_hierarchy(self, instance_hierarchy: list[str] | None = None): + def print_hierarchy(self, instance_hierarchy: list[str] | None = None) -> None: + """Print the module hierarchy and nodes to stdout.""" if instance_hierarchy is None: instance_hierarchy = [] if self.instance_name is not None: @@ -157,28 +172,31 @@ def print_hierarchy(self, instance_hierarchy: list[str] | None = None): for node in self.nodes: print( - f"Node: {node._node.name}, Instance: {'/'.join(instance_hierarchy)}," + f"Node: {node._node.name}, " # noqa: SLF001 + f"Instance: {'/'.join(instance_hierarchy)}," f" Module: {self.module_type}" ) - def get_unwrapped_nodes(self): + def get_unwrapped_nodes(self) -> list[ir.Node]: + """Return the underlying IR nodes stored in this hierarchy node.""" # Return _node from self._nodes - return [node._node for node in self.nodes] + return [node._node for node in self.nodes] # noqa: SLF001 # Checks if the search hierarchy matches the instance hierarchy def hierarchy_matches( self, search_hierarchy: list[str], instance_hierarchy: list[str] | None = None - ): + ) -> bool: + """Return True if the instance path matches the search prefix.""" if instance_hierarchy is None: instance_hierarchy = [] search_length = min(len(search_hierarchy), len(instance_hierarchy)) - for i in range(search_length): - if search_hierarchy[i] != instance_hierarchy[i]: - return False - return True + return all(search_hierarchy[i] == instance_hierarchy[i] for i in range(search_length)) # Return all nodes from the given name hierarchy on down - def get_nodes(self, search_hierarchy: list[str], instance_hierarchy: list[str] | None = None): + def get_nodes( + self, search_hierarchy: list[str], instance_hierarchy: list[str] | None = None + ) -> list[ir.Node]: + """Return all IR nodes under the matched hierarchy path.""" if instance_hierarchy is None: instance_hierarchy = [] @@ -200,7 +218,8 @@ def get_nodes(self, search_hierarchy: list[str], instance_hierarchy: list[str] | return nodes_to_return - def add_node(self, node, level=0): + def add_node(self, node: PytorchMetadataNode | ir.Node, level: int = 0) -> bool: + """Insert a node into the hierarchy, creating children as needed.""" if not isinstance(node, PytorchMetadataNode): node = PytorchMetadataNode(node) if node.check_node_metadata_exists() is False: @@ -234,7 +253,7 @@ def add_node(self, node, level=0): return new_child.add_node(node, level + 1) -def direct_convert_ir_graph_to_pattern(graph): +def direct_convert_ir_graph_to_pattern(graph: ir.Graph) -> GraphPattern: """Convert an IR graph into an ONNX Script ``GraphPattern``. The conversion walks nodes in order, mapping each IR ``Value`` to the @@ -245,20 +264,20 @@ def direct_convert_ir_graph_to_pattern(graph): # Transform IR values to ValuePatterns vmap = {} - for input in graph.inputs: - vmap[input] = ValuePattern(input.name) + for inp in graph.inputs: + vmap[inp] = ValuePattern(inp.name) for init in graph.initializers: vmap[init] = ValuePattern(init) - for node in graph._nodes: + for node in graph._nodes: # noqa: SLF001 if node.op_type == "Constant": vmap[node.outputs[0]] = ValuePattern(node.outputs[0].name) builder = OpsetPatternBuilder("", record=True) with pattern_builder(builder): - for node in graph._nodes: + for node in graph._nodes: # noqa: SLF001 ninputs = [] for ninput in node.inputs: ninputs.append(vmap[ninput]) @@ -274,8 +293,8 @@ def direct_convert_ir_graph_to_pattern(graph): vmap[node.outputs[vp_output.output_index]] = vp_output pinputs = [] - for input in graph.inputs: - pinputs.append(vmap[input]) + for inp in graph.inputs: + pinputs.append(vmap[inp]) # build graph outputs poutputs = [] @@ -285,32 +304,40 @@ def direct_convert_ir_graph_to_pattern(graph): return GraphPattern(inputs=pinputs, outputs=poutputs, nodes=builder.nodes()) -def remove_input_from_node(node, inp): - node._inputs = [x for x in node._inputs if x is not inp] - inp._remove_usage(node) +def remove_input_from_node(node: ir.Node, inp: ir.Value) -> None: + """Remove a single input value from a node and update usages.""" + index = None + for i, ninput in enumerate(node.inputs): + if ninput == inp: + index = i + break + if index is None: + raise FINNInternalError("Input value not found in node inputs") + node._inputs = tuple([x for x in node._inputs if x is not inp]) # noqa: SLF001 + inp._remove_usage(node, index) # noqa: SLF001 -def same(input_list): +def same(input_list: tuple[ir.Value | None, ...]) -> bool: + """Return True if all values in the tuple are identical.""" return len(set(input_list)) == 1 -def vdisconnect(value): - value._uses = {} - value._producer = None - value._index = None - value._graph = None +def vdisconnect(value: ir.Value) -> ir.Value: + """Clear graph connectivity metadata from a value.""" + value._uses = {} # noqa: SLF001 + value._producer = None # noqa: SLF001 + value._index = None # noqa: SLF001 + value._graph = None # noqa: SLF001 return value -def is_fpgadataflow_onnxir_node(node): - """Returns True if given node is fpgadataflow node. Otherwise False.""" +def is_fpgadataflow_onnxir_node(node: ir.Node) -> bool: + """Return True if given node is fpgadataflow node. Otherwise False.""" is_node = False - if node is not None: - if is_custom_op(node.domain): - if "backend" in node.attributes: - backend_value = node.attributes["backend"].as_string() - if backend_value == "fpgadataflow": - is_node = True + if node is not None and is_custom_op(node.domain) and "backend" in node.attributes: + backend_value = node.attributes["backend"].as_string() + if backend_value == "fpgadataflow": + is_node = True return is_node @@ -324,10 +351,12 @@ class ReplacementPatternGraph(ReplacementPatternFunction): match result. """ - def __init__(self, ir_graph): + def __init__(self, ir_graph: ir.Graph) -> None: + """Store the IR graph to materialize during rewrite.""" self._graph = ir_graph def get_replacement(self, match: MatchResult) -> ReplacementSubgraph | None: + """Build the replacement subgraph for a successful match.""" context = RewriterContext() # ``match.bindings`` maps ``value_name`` (str) from the replacement # subgraph pattern to actual IR values. @@ -339,7 +368,7 @@ def get_replacement(self, match: MatchResult) -> ReplacementSubgraph | None: else: vvmap[value] = value - for node in self._graph._nodes: + for node in self._graph._nodes: # noqa: SLF001 ninputs = [] for ninput in node.inputs: ninputs.append(vvmap[ninput]) @@ -355,8 +384,8 @@ def get_replacement(self, match: MatchResult) -> ReplacementSubgraph | None: coutput = [coutput] for i, cout in enumerate(coutput): - cout._type = node.outputs[i].type - cout._shape = node.outputs[i].shape + cout._type = node.outputs[i].type # noqa: SLF001 + cout._shape = node.outputs[i].shape # noqa: SLF001 for key in node.outputs[i].meta: cout.meta[key] = node.outputs[i].meta[key] vvmap[node.outputs[cout.index()]] = cout @@ -367,7 +396,8 @@ def get_replacement(self, match: MatchResult) -> ReplacementSubgraph | None: ) -def find_nodes_of_optype(graph, layername): +def find_nodes_of_optype(graph: ir.Graph, layername: str) -> list[ir.Node]: + """Return all nodes matching the requested op type.""" nodes = [] for node in ir.traversal.RecursiveGraphIterator(graph): if node.op_type == layername: @@ -375,7 +405,8 @@ def find_nodes_of_optype(graph, layername): return nodes -def build_constant_from_tensor(name, tensor): +def build_constant_from_tensor(name: str, tensor: ir.Tensor) -> ir.Node: + """Create a Constant node holding the provided tensor.""" value_attribute = ir.Attr(name="value", type=ir.AttributeType.TENSOR, value=tensor) ir_value_out = ir.Value(name=name + "_out", type=ir.TensorType(tensor.dtype)) return ir.Node( @@ -383,21 +414,31 @@ def build_constant_from_tensor(name, tensor): ) -def build_concat_node_from_inputs(inputs): +def build_concat_node_from_inputs(inputs: tuple[ir.Value | None, ...]) -> ir.Node: + """Build a Concat node that joins the provided inputs along axis 0.""" axis = ir.Attr(name="axis", type=ir.AttributeType.INT, value=0) - - ndim = len(inputs) * inputs[0].shape.dims[0] + if inputs[0] is None: + raise FINNInternalError("First input to concat cannot be None") + if inputs[0].shape is None: + raise FINNInternalError("Input to concat must have known shape") + ndim = len(inputs) * cast("int", inputs[0].shape.dims[0]) output_shape = ir.Shape([ndim, *inputs[0].shape.dims[1:]]) output = ir.Value(name=f"{inputs[0].name}_concat", shape=output_shape, type=inputs[0].type) return ir.Node("", "Concat", inputs=inputs, attributes=[axis], outputs=[output]) -def build_reshape_node(inp, reshape_shape): +def build_reshape_node(inp: ir.Value, reshape_shape: ir.Value) -> ir.Node: + """Build a Reshape node using the provided shape value.""" reshape_out = ir.Value(name=f"{inp.name}_reshape", type=inp.type) return ir.Node("", "Reshape", inputs=[inp, reshape_shape], outputs=[reshape_out]) -def tensor_type_to_finn_datatype_string(tensor_type): +def tensor_type_to_finn_datatype_string( + tensor_type: ir.TensorType, +) -> Literal[ + "FLOAT32", "INT8", "INT16", "INT32", "INT64", "UINT8", "UINT16", "UINT32", "UINT64", "BOOL" +]: + """Map an ONNX Script tensor type to a FINN datatype string.""" if tensor_type == ir.TensorType(_enums.DataType.FLOAT): return "FLOAT32" if tensor_type == ir.TensorType(_enums.DataType.INT8): @@ -418,4 +459,4 @@ def tensor_type_to_finn_datatype_string(tensor_type): return "UINT64" if tensor_type == ir.TensorType(_enums.DataType.BOOL): return "BOOL" - raise ValueError(f"Unsupported tensor type: {tensor_type}") + raise FINNInternalError(f"Unsupported tensor type: {tensor_type}") diff --git a/src/finn/util/platforms.py b/src/finn/util/platforms.py index 2f7caf3d31..c793b41488 100644 --- a/src/finn/util/platforms.py +++ b/src/finn/util/platforms.py @@ -26,30 +26,33 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""FPGA platform resource definitions and floorplanning constraints.""" + import numpy as np from abc import abstractmethod +from numpy.typing import NDArray # contains the amount of available FPGA resources for several # Xilinx platforms, as well as certain resource limit guidelines # for creating designs that can achieve timing closure # explicit value for res types/costs we don't care about -DONT_CARE = -1 +DONT_CARE: int = -1 # recommended resource limits from Xilinx for timing closure # respectively for LUT, FF, BRAM_18K, URAM, DSP res types -DEFAULT_RES_LIMITS = np.array([0.7, 0.5, 0.80, 0.80, 0.80]) -DEFAULT_AVG_CONSTRAINTS = [((2, 3, 4), 0.7)] # +DEFAULT_RES_LIMITS: NDArray[np.float64] = np.array([0.7, 0.5, 0.80, 0.80, 0.80]) +DEFAULT_AVG_CONSTRAINTS: list[tuple[tuple[int, ...], float]] = [((2, 3, 4), 0.7)] # resources required to instantiate certain infrastructure components # such as memory controllers and network interfaces -DDR_RESOURCE_REQUIREMENTS = { +DDR_RESOURCE_REQUIREMENTS: dict[str, int] = { "LUT": 33256, "FF": 44889, "BRAM_18K": 199, "URAM": 0, "DSP": 3, } -HBM_RESOURCE_REQUIREMENTS = { +HBM_RESOURCE_REQUIREMENTS: dict[str, int] = { "LUT": 10718, "FF": 21793, "BRAM_18K": 8, @@ -59,7 +62,7 @@ # we assume use of VNx Alveo UDP stack # see: https://gitenterprise.xilinx.com/mruiznog/vitis_network_layer -ETH_RESOURCE_REQUIREMENTS = { +ETH_RESOURCE_REQUIREMENTS: dict[str, int] = { "LUT": 35219, "FF": 86269, "BRAM_18K": 183, @@ -69,18 +72,25 @@ class Platform: + """Base class for platform resource and interconnect models.""" + def __init__( self, - nslr=1, - ndevices=1, - sll_count=[], - hbm_slr=-1, - ddr_slr=[0], - eth_slr=0, - eth_gbps=0, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + nslr: int = 1, + ndevices: int = 1, + sll_count: list[list[int]] | None = None, + hbm_slr: int = -1, + ddr_slr: list[int] | None = None, + eth_slr: int = 0, + eth_gbps: int = 0, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize platform parameters and resource constraints.""" + if ddr_slr is None: + ddr_slr = [0] + if sll_count is None: + sll_count = [] self.nslr = nslr self.sll_count = sll_count self.eth_slr = eth_slr @@ -97,11 +107,12 @@ def __init__( @property @abstractmethod - def compute_resources(self): - pass + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources as [LUT, FF, BRAM_18K, URAM, DSP].""" @property - def guide_resources(self): + def guide_resources(self) -> list[list[int]]: + """Return per-SLR guide resources after subtracting infra costs.""" guide = [] # TODO: assert limits is of correct size guide_res = (np.tile(np.array(self.compute_resources), (self.ndevices, 1))).astype(int) @@ -132,10 +143,11 @@ def guide_resources(self): return guide @property - def resource_count_dict(self): - res = dict() + def resource_count_dict(self) -> dict[str, dict[str, int]]: + """Return per-SLR resource counts keyed by SLR name.""" + res = {} for i in range(self.nslr * self.ndevices): - slr_res = dict() + slr_res = {} slr_res["LUT"] = self.compute_resources[i % self.nslr][0] slr_res["FF"] = self.compute_resources[i % self.nslr][1] slr_res["BRAM_18K"] = self.compute_resources[i % self.nslr][2] @@ -145,7 +157,8 @@ def resource_count_dict(self): return res @property - def compute_connection_cost(self): + def compute_connection_cost(self) -> NDArray[np.int_]: + """Return an SLR-to-SLR connection cost matrix.""" x = np.full((self.nslr * self.ndevices, self.nslr * self.ndevices), DONT_CARE) # build connection cost matrix for one device's SLRs xlocal = np.full((self.nslr, self.nslr), DONT_CARE) @@ -165,7 +178,8 @@ def compute_connection_cost(self): return x @property - def compute_connection_resource(self): + def compute_connection_resource(self) -> list[list[tuple[int, int]]]: + """Return SLL/ETH constraints for each SLR-to-SLR connection.""" sll = np.full((self.nslr * self.ndevices, self.nslr * self.ndevices), 0) # build connection resource matrix for one device's SLRs slllocal = np.full((self.nslr, self.nslr), -1) @@ -207,19 +221,22 @@ def compute_connection_resource(self): constraints.append(constraints_line) return constraints - def map_device_to_slr(self, idx): - """Given a global SLR index, return device id and local slr index""" + def map_device_to_slr(self, idx: int) -> tuple[int, int]: + """Map a global SLR index to (local_slr, device_id).""" assert idx <= self.nslr * self.ndevices return (idx % self.nslr, idx // self.nslr) class Zynq7020_Platform(Platform): + """Platform definition for Zynq-7020 devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize a Zynq-7020 platform.""" super().__init__( nslr=1, ndevices=ndevices, @@ -232,17 +249,21 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" return [[53200, 2 * 53200, 280, 0, 220] for i in range(1)] class ZU3EG_Platform(Platform): + """Platform definition for ZU3EG devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize a ZU3EG platform.""" super().__init__( nslr=1, ndevices=ndevices, @@ -255,17 +276,21 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" return [[71000, 2 * 71000, 412, 0, 360] for i in range(1)] class ZU7EV_Platform(Platform): + """Platform definition for ZU7EV devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize a ZU7EV platform.""" super().__init__( nslr=1, ndevices=ndevices, @@ -278,17 +303,21 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" return [[230000, 2 * 230000, 610, 92, 1728] for i in range(1)] class ZU9EG_Platform(Platform): + """Platform definition for ZU9EG devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize a ZU9EG platform.""" super().__init__( nslr=1, ndevices=ndevices, @@ -301,17 +330,21 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" return [[274000, 2 * 274000, 1824, 0, 2520] for i in range(1)] class ZU28DR_Platform(Platform): + """Platform definition for ZU28DR devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize a ZU28DR platform.""" super().__init__( nslr=1, ndevices=ndevices, @@ -324,17 +357,21 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" return [[425000, 2 * 425000, 2160, 80, 4272] for i in range(1)] class Alveo_NxU50_Platform(Platform): + """Platform definition for Alveo U50 devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize an Alveo U50 platform.""" # according to Vivado: 23040 SLR0 <-> SLR1 sll_counts = [[0, 5000], [5000, 0]] super().__init__( @@ -350,7 +387,8 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" # According to UG1120: # U50 has identical resource counts on both SLRs # return [[365000,2*365000,2*564, 304, 2580] for i in range(2)] @@ -362,12 +400,15 @@ def compute_resources(self): class Alveo_NxU200_Platform(Platform): + """Platform definition for Alveo U200 devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize an Alveo U200 platform.""" sll_counts = [[0, 5000, 0], [5000, 0, 5000], [0, 5000, 0]] super().__init__( nslr=3, @@ -381,7 +422,8 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" # According to UG1120: # return [[355000, 723000, 2*638, 320, 2265], # [160000, 331000, 2*326, 160, 1317], @@ -395,12 +437,15 @@ def compute_resources(self): class Alveo_NxU250_Platform(Platform): + """Platform definition for Alveo U250 devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize an Alveo U250 platform.""" sll_counts = [ [0, 5000, 0, 0], [5000, 0, 5000, 0], @@ -419,7 +464,8 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" # According to UG1120: # U250 has identical resource counts on all 4 SLRs: # return [[345000,2*345000,2*500, 320, 2877] for i in range(4)] @@ -428,12 +474,15 @@ def compute_resources(self): class Alveo_NxU280_Platform(Platform): + """Platform definition for Alveo U280 devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize an Alveo U280 platform.""" sll_counts = [[0, 5000, 0], [5000, 0, 5000], [0, 5000, 0]] super().__init__( nslr=3, @@ -448,7 +497,8 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" # according to UG1120 # return [[369000, 746000, 2*507, 320, 2733], # [333000, 675000, 2*468, 320, 2877], @@ -462,12 +512,15 @@ def compute_resources(self): class Alveo_NxU55C_Platform(Platform): + """Platform definition for Alveo U55C devices.""" + def __init__( self, - ndevices=1, - limits=DEFAULT_RES_LIMITS, - avg_constraints=DEFAULT_AVG_CONSTRAINTS, - ): + ndevices: int = 1, + limits: NDArray[np.float64] = DEFAULT_RES_LIMITS, + avg_constraints: list[tuple[tuple[int, ...], float]] = DEFAULT_AVG_CONSTRAINTS, + ) -> None: + """Initialize an Alveo U55C platform.""" sll_counts = [[0, 5000, 0], [5000, 0, 5000], [0, 5000, 0]] super().__init__( nslr=3, @@ -482,7 +535,8 @@ def __init__( ) @property - def compute_resources(self): + def compute_resources(self) -> list[list[int]]: + """Return per-SLR compute resources.""" # according to UG1120 return [ [386000, 773000, 2 * 600, 320, 2664], @@ -491,7 +545,7 @@ def compute_resources(self): ] -platforms = dict() +platforms: dict[str, type[Platform]] = {} platforms["U50"] = Alveo_NxU50_Platform platforms["U200"] = Alveo_NxU200_Platform platforms["U250"] = Alveo_NxU250_Platform From 7cafb3005d67ccbc848d66f0f5c311339df705fc Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Wed, 20 May 2026 14:16:04 +0200 Subject: [PATCH 124/170] Add timestamp to docstrings comment --- .github/workflows/check-docstrings.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/check-docstrings.yml b/.github/workflows/check-docstrings.yml index 13ce4709ae..1274a683a4 100644 --- a/.github/workflows/check-docstrings.yml +++ b/.github/workflows/check-docstrings.yml @@ -208,6 +208,9 @@ jobs: const commentMarker = ''; body = commentMarker + '\n' + body; + const updatedAt = new Date().toISOString(); + body += `\n\n_Last updated: ${updatedAt}_`; + // Find existing comment from this action const comments = await github.rest.issues.listComments({ issue_number: context.issue.number, From 3dbadb115c8c748cfe5025eb41cdce8aef0b1536 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 22 May 2026 18:31:26 +0200 Subject: [PATCH 125/170] Fix rtlsim input bug --- finn_xsi/finn_xsi/adapter.py | 3 +- finn_xsi/finn_xsi/include/Simulation.hpp | 4 +- finn_xsi/finn_xsi/include/helper.h | 2 +- finn_xsi/finn_xsi/sim_engine.py | 20 +++-- finn_xsi/finn_xsi/src/Design.cpp | 2 +- finn_xsi/finn_xsi/src/Port.cpp | 80 +++++++++---------- src/finn/core/onnx_exec.py | 57 +------------ src/finn/custom_op/fpgadataflow/hwcustomop.py | 6 +- .../fpgadataflow/simulation_build.py | 2 +- src/finn/util/execution.py | 11 ++- tests/fpgadataflow/test_fpgadataflow_vvau.py | 3 - 11 files changed, 72 insertions(+), 118 deletions(-) diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 6c1da08637..43ff93e5bb 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -40,6 +40,7 @@ def compile_sim_obj( sim_out_dir: Path, debug: bool = False, behav: bool = False, + fifosim: bool = False, ) -> tuple[Path, Path]: """Compile the simulation object (.so) for the given top module and source files.""" # create a .prj file with the source files @@ -92,7 +93,7 @@ def compile_sim_obj( cmd_xelab = [ "xelab", - "work." + top_module_name, + "work." + top_module_name if not fifosim else "finn_design_wrapper", "-relax", "-dll", "--O3", diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 42d3fe575a..77ba245feb 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -48,10 +48,10 @@ class Simulation { // Find I/O Streams and initialize their Status for (size_t i = 0; i < _istream_descs.size(); ++i) { - istreams[i] = S_AXIS_Control{top, clk, std::data(_istream_descs)[i].job_size, std::data(_istream_descs)[i].job_size, std::data(_istream_descs)[i].name}; + istreams[i] = S_AXIS_Control{top, clk, std::data(_istream_descs)[i].job_size, std::data(_istream_descs)[i].job_size, std::string(std::data(_istream_descs)[i].name)}; } for (size_t i = 0; i < _ostream_descs.size(); ++i) { - ostreams[i] = M_AXIS_Control{top, clk, std::data(_ostream_descs)[i].job_size, std::data(_ostream_descs)[i].name}; + ostreams[i] = M_AXIS_Control{top, clk, std::data(_ostream_descs)[i].job_size, std::string(std::data(_ostream_descs)[i].name)}; } // Save simulation input output behaviour diff --git a/finn_xsi/finn_xsi/include/helper.h b/finn_xsi/finn_xsi/include/helper.h index aa13c4612a..ad142e3c54 100644 --- a/finn_xsi/finn_xsi/include/helper.h +++ b/finn_xsi/finn_xsi/include/helper.h @@ -10,7 +10,7 @@ constexpr std::array HEX = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; struct StreamDescriptor { - std::string name; + std::string_view name; std::size_t job_size; // // Next job can only start this many clock ticks after start of predecessor. // std::size_t job_ticks; diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py index d7174f2bef..55085ecbb6 100644 --- a/finn_xsi/finn_xsi/sim_engine.py +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -57,8 +57,6 @@ def __init__( """Create a simulation engine bound to the given kernel and design.""" top = xsi.Design(xsi.Kernel(kernel), design, log, wdb) clk = top.getPort("ap_clk") - for port in top.ports(): - print(port.name()) # If clock pumping is disabled, set clk2x to None try: clk2x = top.getPort("ap_clk2x") @@ -71,6 +69,7 @@ def __init__( def cycle(updates: dict[xsi.Port, str]) -> None: """Perform one clock cycle with the given port updates.""" # Rising Edge + top.run(1) clk.set(1).write_back() if clk2x is not None: clk2x.set(1).write_back() @@ -83,7 +82,7 @@ def cycle(updates: dict[xsi.Port, str]) -> None: if clk2x is None: top.run(4999) clk.set(0).write_back() - top.run(5000) + top.run(4999) else: top.run(2499) clk2x.set(0).write_back() @@ -92,12 +91,21 @@ def cycle(updates: dict[xsi.Port, str]) -> None: clk2x.set(1).write_back() top.run(2500) clk2x.set(0).write_back() - top.run(2500) + top.run(2499) self.top = top self.cycle = cycle self.ticks = 0 - self.tasks = [] + self.tasks: list[ + SimEngine.Reset + | SimEngine.InputStreamer + | SimEngine.OutputCollector + | SimEngine.StreamTracer + | SimEngine.AxiLiteWriter + | SimEngine.AxiLiteReader + | SimEngine.AximmRoImage + | SimEngine.AximmQueue + ] = [] self.watchdogs: list[SimEngine.Watchdog] = [] # ------------------------------------------------------------------------ @@ -116,7 +124,7 @@ def get_bus_port(self, bus: str, suffix: str) -> "xsi.Port": # Task Scheduling def enlist( self, - task: "SimEngine.Reset | SimEngine.InputStreamer | SimEngine.OutputCollector | SimEngine.Watchdog | SimEngine.StreamTracer | SimEngine.AxiLiteWriter | SimEngine.AxiLiteReader | SimEngine.AximmRoImage | SimEngine.AximmQueue", # noqa + task: "SimEngine.Reset | SimEngine.InputStreamer | SimEngine.OutputCollector | SimEngine.StreamTracer | SimEngine.AxiLiteWriter | SimEngine.AxiLiteReader | SimEngine.AximmRoImage | SimEngine.AximmQueue", # noqa ) -> None: """Register a task to be driven by the simulation loop.""" self.tasks.append(task) diff --git a/finn_xsi/finn_xsi/src/Design.cpp b/finn_xsi/finn_xsi/src/Design.cpp index 75c4ac6ca3..fcd85738eb 100644 --- a/finn_xsi/finn_xsi/src/Design.cpp +++ b/finn_xsi/finn_xsi/src/Design.cpp @@ -6,7 +6,7 @@ using namespace xsi; Design::Design(xsi::Kernel& kernel, const std::string& design_lib, const s_xsi_setup_info& setup_info) : _kernel(std::move(kernel)) { _kernel.open(design_lib, setup_info); } Design::Design(xsi::Kernel& kernel, const std::string& design_lib, const char* const log_file, const char* const wdb_file) - : Design(kernel, design_lib, s_xsi_setup_info{.logFileName = const_cast(log_file), .wdbFileName = const_cast(wdb_file), .xsimDir = ""}) {} + : Design(kernel, design_lib, s_xsi_setup_info{.logFileName = const_cast(log_file), .wdbFileName = const_cast(wdb_file)}) {} // Destructor Design::~Design() { _kernel.close(); } diff --git a/finn_xsi/finn_xsi/src/Port.cpp b/finn_xsi/finn_xsi/src/Port.cpp index 436c2f4778..7da5a955c5 100644 --- a/finn_xsi/finn_xsi/src/Port.cpp +++ b/finn_xsi/finn_xsi/src/Port.cpp @@ -124,28 +124,30 @@ Port& Port::set_binstr(const std::string& val) { uint32_t a = 0; uint32_t b = 0; - // Process up to 32 characters for this buffer element - const size_t chars_to_process = std::min(32UL, val_length - chars_processed); + // chars_to_process is the number of binary digits this buffer word should + // consume from the input. It is needed because one 32-bit word can hold + // at most 32 binary digits, and we must not read past the end of val. + // Examples: "1" -> 1 digit here, "1011..." with more than 32 digits -> + // 32 digits in this word and the rest in later words, empty input -> 0 + // digits. + const size_t chars_to_process = std::min(32UL, val_length > chars_processed ? val_length - chars_processed : 0); for (size_t j = 0; j < chars_to_process; ++j) { - a <<= 1; - b <<= 1; - - if (val_iter != val.crend()) { - switch (*val_iter++) { - case '1': - a |= 1; - [[fallthrough]]; - case '0': - break; - default: - a |= 1; - [[fallthrough]]; - case 'Z': - case 'z': - b |= 1; - break; - } + const unsigned shift = static_cast(j); + + switch (*val_iter++) { + case '1': + a |= (1u << shift); + [[fallthrough]]; + case '0': + break; + default: + a |= (1u << shift); + [[fallthrough]]; + case 'Z': + case 'z': + b |= (1u << shift); + break; } } @@ -153,8 +155,6 @@ Port& Port::set_binstr(const std::string& val) { elem.bVal = b; chars_processed += chars_to_process; - if (chars_processed >= val_length) - break; } return *this; @@ -170,27 +170,28 @@ Port& Port::set_hexstr(const std::string& val) { uint32_t a = 0; uint32_t b = 0; - // Process up to 8 hex characters (32 bits) for this buffer element - const size_t chars_to_process = std::min(8UL, val_length - chars_processed); + // chars_to_process is the number of hex digits this buffer word should + // consume from the input. It is needed because one 32-bit word can hold + // at most 8 hex digits, and we must not read past the end of val. + // Examples: "1" -> 1 digit here, "9fa42b4a3" -> 8 digits in this word + // and 1 digit in the next, empty input -> 0 digits. + const size_t chars_to_process = std::min(8UL, val_length > chars_processed ? val_length - chars_processed : 0); for (size_t j = 0; j < chars_to_process; ++j) { - a <<= 4; - b <<= 4; + const unsigned shift = static_cast(j * 4); - if (val_iter != val.crend()) { - char c = *val_iter++; + char c = *val_iter++; - if (('0' <= c) && c <= '9') { - a |= c & 0xF; + if (('0' <= c) && c <= '9') { + a |= (static_cast(c - '0') << shift); + } else { + c |= 0x20; // Convert to lowercase + if (('a' <= c) && (c <= 'f')) { + a |= (static_cast(c - ('a' - 10)) << shift); } else { - c |= 0x20; // Convert to lowercase - if (('a' <= c) && (c <= 'f')) { - a |= static_cast(c - ('a' - 10)); - } else { - b |= 0xF; - if (c != 'z') { - a |= 0xF; - } + b |= (0xFu << shift); + if (c != 'z') { + a |= (0xFu << shift); } } } @@ -200,9 +201,6 @@ Port& Port::set_hexstr(const std::string& val) { elem.bVal = b; chars_processed += chars_to_process; - if (chars_processed >= val_length) - break; } - return *this; } diff --git a/src/finn/core/onnx_exec.py b/src/finn/core/onnx_exec.py index 655d5c4dc5..b0118f6e6d 100644 --- a/src/finn/core/onnx_exec.py +++ b/src/finn/core/onnx_exec.py @@ -40,15 +40,10 @@ import copy import numpy as np -import qonnx.analysis.topology as ta from collections.abc import Callable from onnx import NodeProto from qonnx.core.modelwrapper import ModelWrapper from qonnx.core.onnx_exec import execute_onnx as execute_onnx_base -from typing import cast - -from finn.core.rtlsim_exec import rtlsim_exec -from finn.util.exception import FINNInternalError, FINNUserError def execute_onnx( @@ -68,57 +63,7 @@ def execute_onnx( If they are set to particular ONNX nodes, only the subgraph between (and including) those nodes is executed. """ - # check if model has an execution mode set - # if None, execute model node using the QONNX-provided execute_onnx impl - # if set to "rtlsim" execute model using xsi - model_exec_mode = model.get_metadata_prop("exec_mode") - if (model_exec_mode is None) or (model_exec_mode == ""): - return execute_onnx_base(model, input_dict, return_full_exec_context, start_node, end_node) - if model_exec_mode == "rtlsim": - # check sanity of model and then use stitched IP for rtlsim - if not model.check_all_tensor_shapes_specified(): - raise FINNUserError("Found unspecified tensor shapes, try infer_shapes") - ret = model.analysis(ta.nodes_topologically_sorted) - assert ( - ret["nodes_topologically_sorted"] is True - ), """Nodes must be - topologically sorted.""" - - graph = model.graph - # first, we need to make sure that every variable required by the graph has - # some buffer associated with it. this includes graph inputs (which includes - # the input data as well as the trained parameters) and the graph ValueInfo - # (intermediate tensors between layers) - # this is provided by the execution_context, which is a dict of np.ndarray - execution_context = cast("dict[str, np.ndarray]", model.make_empty_exec_context()) - # fill in any inputs provided to this function - for inp_name in input_dict.keys(): - if inp_name in execution_context: - if execution_context[inp_name].shape == input_dict[inp_name].shape: - execution_context[inp_name] = input_dict[inp_name] - else: - raise FINNInternalError( - f"Shape mismatch for provided input {inp_name}: found " - f"{execution_context[inp_name].shape!s} expected " - f"{input_dict[inp_name].shape!s} " - ) - - # use stitched IP for rtlsim - rtlsim_exec(model, execution_context) - else: - raise FINNInternalError( - """Metadata property "exec_mode" is set to an unknown value. Can be left - unset or has to be set to "rtlsim" for execution using xsi!""" - ) - - if return_full_exec_context: - return execution_context - # provide outputs as dict - output_dict = {} - for out_tensor in graph.output: - out_name = out_tensor.name - output_dict[out_name] = execution_context[out_name] - return output_dict + return execute_onnx_base(model, input_dict, return_full_exec_context, start_node, end_node) def execute_onnx_and_make_model( diff --git a/src/finn/custom_op/fpgadataflow/hwcustomop.py b/src/finn/custom_op/fpgadataflow/hwcustomop.py index 8bf152d468..a173e4425d 100644 --- a/src/finn/custom_op/fpgadataflow/hwcustomop.py +++ b/src/finn/custom_op/fpgadataflow/hwcustomop.py @@ -41,7 +41,7 @@ from qonnx.core.datatype import BaseDataType from qonnx.custom_op.base import CustomOp from qonnx.util.basic import roundup_to_integer_multiple -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, cast from finn.util.basic import get_liveness_threshold_cycles, is_versal from finn.util.exception import FINNInternalError @@ -298,7 +298,9 @@ def reset_rtlsim(self, sim: SimEngine) -> None: """Set reset input in finnxsi to zero, toggle the clock and set it back to one.""" finnxsi.reset_rtlsim(sim) - def rtlsim_multi_io(self, sim: SimEngine, io_dict: dict[str, Any], sname: str = "_V") -> None: + def rtlsim_multi_io( + self, sim: SimEngine, io_dict: dict[str, dict[str, list[int]]], sname: str = "_V" + ) -> None: """Run rtlsim for this node, supports multiple i/o streams.""" num_out_values = self.get_number_output_values() # Use the larger of expected cycles or liveness threshold diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index f460d6a1f1..7b20166f0e 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -602,7 +602,7 @@ def _create_sim_so( else build_dir ) sim_base, sim_rel = finnxsi.compile_sim_obj( - top_module_name, all_verilog_srcs, sim_dir, debug=debug + top_module_name, all_verilog_srcs, sim_dir, debug=debug, fifosim=True ) rtlsim_so = Path(sim_base) / Path(sim_rel) model.set_metadata_prop("rtlsim_so", str(rtlsim_so)) diff --git a/src/finn/util/execution.py b/src/finn/util/execution.py index 3dbb7bfcc6..619293723c 100644 --- a/src/finn/util/execution.py +++ b/src/finn/util/execution.py @@ -32,14 +32,15 @@ StreamingDataflowPartition nodes and other execution-related utilities. """ -import os +import numpy as np +from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp from finn.core.onnx_exec import execute_onnx -def load_model_checkpoint(filename): +def load_model_checkpoint(filename: str) -> ModelWrapper: """Load given .onnx file and return ModelWrapper. Args: @@ -51,13 +52,15 @@ def load_model_checkpoint(filename): Raises: FileNotFoundError: If the model file doesn't exist """ - if os.path.isfile(filename): + if Path(filename).is_file(): model = ModelWrapper(filename) return model raise FileNotFoundError(f"Model file {filename} not found") -def execute_parent(parent_path, child_path, input_tensor_npy, return_full_ctx=False): +def execute_parent( + parent_path: str, child_path: str, input_tensor_npy: np.ndarray, return_full_ctx: bool = False +) -> np.ndarray | dict[str, np.ndarray]: """Execute parent model containing a single StreamingDataflowPartition by replacing it with the model at child_path and return result. diff --git a/tests/fpgadataflow/test_fpgadataflow_vvau.py b/tests/fpgadataflow/test_fpgadataflow_vvau.py index 2d0bcca8a8..5947c5506e 100644 --- a/tests/fpgadataflow/test_fpgadataflow_vvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_vvau.py @@ -359,7 +359,6 @@ def test_fpgadataflow_vvau( model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP("xczu7ev-ffvc1156-2-e", 5)) - model.set_metadata_prop("exec_mode", "rtlsim") y_expected = oxe.execute_onnx(model, input_dict)["global_out"] assert ( @@ -540,8 +539,6 @@ def test_fpgadataflow_vvau_rtl( partitioned_model = partitioned_model.transform(PrepareIP(part, 5)) partitioned_model = partitioned_model.transform(HLSSynthIP()) partitioned_model = partitioned_model.transform(CreateStitchedIP(part, 5)) - # set top-level prop for stitched-ip rtlsim and launch - partitioned_model.set_metadata_prop("exec_mode", "rtlsim") # transpose input since we're now simulating HW layers (NCHW --> NHWC) input_dict["global_in"] = np.transpose(input_dict["global_in"], (0, 2, 3, 1)) output_vvau_stitched = oxe.execute_onnx( From d89577377c553f7e37b0eeba93398fff720098e7 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 26 May 2026 12:05:13 +0200 Subject: [PATCH 126/170] Fixed/readded stitched ip sim using python interface --- src/finn/core/onnx_exec.py | 70 ++++++++++++++++++-- tests/fpgadataflow/test_fpgadataflow_vvau.py | 3 + 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/finn/core/onnx_exec.py b/src/finn/core/onnx_exec.py index b0118f6e6d..e3741cf52a 100644 --- a/src/finn/core/onnx_exec.py +++ b/src/finn/core/onnx_exec.py @@ -40,10 +40,15 @@ import copy import numpy as np +import qonnx.analysis.topology as ta from collections.abc import Callable from onnx import NodeProto from qonnx.core.modelwrapper import ModelWrapper from qonnx.core.onnx_exec import execute_onnx as execute_onnx_base +from typing import cast + +from finn.core.rtlsim_exec import rtlsim_exec +from finn.util.exception import FINNInternalError def execute_onnx( @@ -63,7 +68,64 @@ def execute_onnx( If they are set to particular ONNX nodes, only the subgraph between (and including) those nodes is executed. """ - return execute_onnx_base(model, input_dict, return_full_exec_context, start_node, end_node) + # check if model has an execution mode set + # if None, execute model node using the QONNX-provided execute_onnx impl + # if set to "rtlsim" execute model using xsi + model_exec_mode = model.get_metadata_prop("exec_mode") + if (model_exec_mode is None) or (model_exec_mode == ""): + return execute_onnx_base(model, input_dict, return_full_exec_context, start_node, end_node) + if model_exec_mode == "rtlsim": + # check sanity of model and then use stitched IP for rtlsim + if not model.check_all_tensor_shapes_specified(): + raise Exception("Found unspecified tensor shapes, try infer_shapes") + ret = model.analysis(ta.nodes_topologically_sorted) + assert ( + ret["nodes_topologically_sorted"] is True + ), """Nodes must be + topologically sorted.""" + + graph = model.graph + # first, we need to make sure that every variable required by the graph has + # some buffer associated with it. this includes graph inputs (which includes + # the input data as well as the trained parameters) and the graph ValueInfo + # (intermediate tensors between layers) + # this is provided by the execution_context, which is a dict of np.ndarray + execution_context = model.make_empty_exec_context() + # fill in any inputs provided to this function + for inp_name in input_dict.keys(): + if inp_name in execution_context: + ex = execution_context[inp_name] + if ex is None: + raise FINNInternalError( + f"Shape of input {inp_name} is None in the execution " + f"context, but an input value was provided." + ) + if ex.shape == input_dict[inp_name].shape: + execution_context[inp_name] = input_dict[inp_name] + else: + raise FINNInternalError( + f"Shape mismatch for provided input {inp_name}: " + f"found {ex.shape!s} expected {input_dict[inp_name].shape!s} " + ) + + # use stitched IP for rtlsim + rtlsim_exec(model, cast("dict[str, np.ndarray]", execution_context)) + else: + raise FINNInternalError( + """Metadata property "exec_mode" is set to an unknown value. Can be left + unset or has to be set to "rtlsim" for execution using xsi!""" + ) + + if return_full_exec_context: + if "" in execution_context: + del execution_context[""] # remove empty string entry if it exists + return cast("dict[str, np.ndarray]", execution_context) + # provide outputs as dict + output_dict = {} + for out_tensor in graph.output: + out_name = out_tensor.name + output_dict[out_name] = execution_context[out_name] + return output_dict def execute_onnx_and_make_model( @@ -89,9 +151,9 @@ def compare_execution( model_a: "ModelWrapper", model_b: "ModelWrapper", input_dict: dict[str, np.ndarray], - compare_fxn: Callable[ - [list | np.ndarray, list | np.ndarray], bool | np.bool_ - ] = lambda x, y: np.isclose(x, y, atol=1e-3).all(), + compare_fxn: Callable[[list | np.ndarray, list | np.ndarray], bool | np.bool_] = lambda x, y: ( + np.isclose(x, y, atol=1e-3).all() + ), ) -> bool | np.bool_: """Execute two ONNX models and compare their outputs using given function. diff --git a/tests/fpgadataflow/test_fpgadataflow_vvau.py b/tests/fpgadataflow/test_fpgadataflow_vvau.py index 5947c5506e..cc30e66910 100644 --- a/tests/fpgadataflow/test_fpgadataflow_vvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_vvau.py @@ -69,6 +69,8 @@ def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, @@ -358,6 +360,7 @@ def test_fpgadataflow_vvau( model = model.transform(PrepareIP("xczu7ev-ffvc1156-2-e", 5)) model = model.transform(HLSSynthIP()) model = model.transform(CreateStitchedIP("xczu7ev-ffvc1156-2-e", 5)) + model.set_metadata_prop("exec_mode", "rtlsim") y_expected = oxe.execute_onnx(model, input_dict)["global_out"] From ed35b8069f6d646a0ccebff942d747b254591635 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 26 May 2026 12:55:37 +0200 Subject: [PATCH 127/170] Remove notebook tests, because notebooks will be removed anyway --- tests/notebooks/test_jupyter_notebooks.py | 116 ---------------------- 1 file changed, 116 deletions(-) delete mode 100644 tests/notebooks/test_jupyter_notebooks.py diff --git a/tests/notebooks/test_jupyter_notebooks.py b/tests/notebooks/test_jupyter_notebooks.py deleted file mode 100644 index ce9c608dee..0000000000 --- a/tests/notebooks/test_jupyter_notebooks.py +++ /dev/null @@ -1,116 +0,0 @@ -import pytest - -import nbformat -import os -from _pytest.mark.structures import ParameterSet -from nbconvert.preprocessors import ExecutePreprocessor -from pathlib import Path -import finn.util.settings -from finn.interface.settings import FINNSettings - -notebook_timeout_seconds = 3600 -notebook_basic_dir = os.path.join(os.environ["FINN_NOTEBOOKS"], "basics/") -notebook_advanced_dir = os.path.join(os.environ["FINN_NOTEBOOKS"], "advanced/") -notebook_cyber_dir = os.path.join(os.environ["FINN_NOTEBOOKS"], "end2end_example/cybersecurity/") -notebook_bnn_dir = os.path.join(os.environ["FINN_NOTEBOOKS"], "end2end_example/bnn-pynq/") - -basics_notebooks = [ - pytest.param( - notebook_basic_dir + "0_how_to_work_with_onnx.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_general"), - ), - pytest.param( - notebook_basic_dir + "1_brevitas_network_import_via_QONNX.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_general"), - ), -] - -advanced_notebooks = [ - pytest.param( - notebook_advanced_dir + "0_custom_analysis_pass.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_general"), - ), - pytest.param( - notebook_advanced_dir + "1_custom_transformation_pass.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_general"), - ), - pytest.param( - notebook_advanced_dir + "2_custom_op.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_general"), - ), - pytest.param( - notebook_advanced_dir + "3_folding.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_general"), - ), - pytest.param( - notebook_advanced_dir + "4_advanced_builder_settings.ipynb", - marks=[ - pytest.mark.xdist_group(name="notebooks_general"), - ], - ), -] - -cyber_notebooks = [ - pytest.param( - notebook_cyber_dir + "1-train-mlp-with-brevitas.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_cybsec"), - ), - pytest.param( - notebook_cyber_dir + "2-import-into-finn-and-verify.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_cybsec"), - ), - pytest.param( - notebook_cyber_dir + "3-build-accelerator-with-finn.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_cybsec"), - ), -] - -bnn_notebooks = [ - pytest.param( - notebook_bnn_dir + "cnv_end2end_example.ipynb", - marks=[ - pytest.mark.xdist_group(name="notebooks_cnv"), - ], - ), - pytest.param( - notebook_bnn_dir + "tfc_end2end_example.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_tfc"), - ), - pytest.param( - notebook_bnn_dir + "tfc_end2end_verification.ipynb", - marks=pytest.mark.xdist_group(name="notebooks_tfc"), - ), -] - - -@pytest.mark.notebooks -class Test_notebooks: - @pytest.mark.parametrize( - "notebook", basics_notebooks + advanced_notebooks + cyber_notebooks + bnn_notebooks - ) - def test_notebook_exec(self, notebook: ParameterSet, request): - settings = FINNSettings.init( - flow_config=Path("/tmp/FINN_TEST_BUILD_DIR/dummy.yaml"), auto_set_environmenmt_vars=True - ) - finn.util.settings._SETTINGS = settings # noqa - os.environ["FINN_SETTINGS"] = str(settings.get_path()) - with open(notebook) as f: - # Set different NETRON_PORT for each xdist group to avoid conflicts - xdist_groups = [ - "notebooks_general", - "notebooks_cybsec", - "notebooks_cnv", - "notebooks_tfc", - ] - for mark in request.node.own_markers: - if mark.name == "xdist_group": - group = mark.kwargs["name"] - os.environ["NETRON_PORT"] = str(8081 + xdist_groups.index(group)) - break - - nb = nbformat.read(f, as_version=4) - ep = ExecutePreprocessor(timeout=notebook_timeout_seconds, kernel_name="python3") - try: - assert ep.preprocess(nb) is not None, f"Got empty notebook for {notebook}" - except Exception: - assert False, f"Failed executing {notebook}" From 6de25e9a43414dfa3fbff5487ec05533af285bde Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 26 May 2026 13:26:47 +0200 Subject: [PATCH 128/170] Bump Python to 3.11.5 to be able to load Boost module. --- ci/.gitlab-setup.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/.gitlab-setup.yml b/ci/.gitlab-setup.yml index 7bb7e933fe..9e120c9b01 100644 --- a/ci/.gitlab-setup.yml +++ b/ci/.gitlab-setup.yml @@ -2,9 +2,10 @@ .n2_setup_general: before_script: - - ml lang/Python/3.10.4-GCCcore-11.3.0 + - ml lang/Python/3.11.5-GCCcore-13.2.0 + - ml devel/Boost/1.83.0-GCC-13.2.0 - ml tools/git-lfs/3.6.1 - - ml tools/UnZip/6.0-GCCcore-11.3.0 + - ml tools/UnZip/6.0-GCCcore-13.2.0 - ml devel/ncurses/5.9 - ulimit -s unlimited # Increase stack size limit From 604cb239a2cf29c660f3b5f262fa5bb355cd4f36 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 26 May 2026 14:03:05 +0200 Subject: [PATCH 129/170] Fix BRAM search tests --- tests/fpgadataflow/test_bram_block_search.py | 29 -------------------- 1 file changed, 29 deletions(-) diff --git a/tests/fpgadataflow/test_bram_block_search.py b/tests/fpgadataflow/test_bram_block_search.py index 4ffa50e2d1..a2c20b7f9d 100644 --- a/tests/fpgadataflow/test_bram_block_search.py +++ b/tests/fpgadataflow/test_bram_block_search.py @@ -357,7 +357,6 @@ def test_depth_range_consistency(self): class TestNeedsMinimization: """Test the needs_minimization method.""" - # TODO: Maybe remove this behavior def test_small_depths_no_minimization(self): """Test that small depths don't need minimization.""" from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation @@ -370,18 +369,6 @@ def test_small_depths_no_minimization(self): assert not sim._needs_minimization(16, 8) assert not sim._needs_minimization(2, 8) - # TODO: Maybe remove this behavior - def test_qsrl_range_no_minimization(self): - """Test that depths within QSRL range don't need minimization.""" - from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation - - sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) - sim.max_qsrl_depth = 256 - - # Depths within max_qsrl_depth don't need minimization - assert not sim._needs_minimization(128, 8) - assert not sim._needs_minimization(256, 8) - def test_large_depths_need_minimization(self): """Test that large depths with multiple BRAM blocks need minimization.""" from finn.transformation.fpgadataflow.simulation_connected import ( @@ -446,22 +433,6 @@ def test_large_depths_need_minimization(self): f"should need minimization" ) - def test_minimum_bram_edge_case(self): - """Test edge case at minimum BRAM blocks.""" - from finn.transformation.fpgadataflow.simulation_connected import RunLayerParallelSimulation - - sim = RunLayerParallelSimulation.__new__(RunLayerParallelSimulation) - sim.max_qsrl_depth = 256 - - # A depth that's just slightly above max_qsrl_depth with minimum BRAM blocks - # The behavior depends on whether it's deemed too close to optimize - depth = 300 - bitwidth = 1 - - # Verify the method executes without error - result = sim._needs_minimization(depth, bitwidth) - assert isinstance(result, bool) - if __name__ == "__main__": pytest.main([__file__, "-v"]) From 445bfa7944e182a1e528a2a5313c7e23b1db9ea9 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 26 May 2026 15:01:01 +0200 Subject: [PATCH 130/170] Fix squeeze and unsqueeze tests --- finn_xsi/finn_xsi/adapter.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 43ff93e5bb..60012fa4dd 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -11,6 +11,7 @@ ############################################################################# import errno +import numpy as np import os import re from finn_xsi.sim_engine import SimEngine @@ -187,7 +188,7 @@ def close_rtlsim(sim: SimEngine) -> None: def rtlsim_multi_io( sim: SimEngine, io_dict: dict[str, dict[str, list[int]]], - num_out_values: int | dict[str, int], + num_out_values: int | np.integer | dict[str, int | np.integer], sname: str = "_V_V", liveness_threshold: int = 10000, ) -> int: @@ -198,8 +199,11 @@ def rtlsim_multi_io( else: # num_out_values is provided as integer (indicating the expected # outputs from the single output stream) - make into dict - if not isinstance(num_out_values, int): - raise FINNInternalError("num_out_values must be int for single output stream") + if not isinstance(num_out_values, int) and not (isinstance(num_out_values, np.integer)): + raise FINNInternalError( + f"num_out_values must be int for single output stream, " + f"but got {type(num_out_values)}" + ) oname = next(iter(io_dict["outputs"].keys())) num_out_values = {oname: num_out_values} @@ -219,7 +223,7 @@ def rtlsim_multi_io( stream_name = out + sname hex_output_streams[out] = sim.collect_output( stream_name, - num_out_values[out], + int(num_out_values[out]), watchdog=sim.create_watchdog(f"{stream_name} timeout", liveness_threshold), ) From 1f96c6f5af4fe4c04cc976eb9d4eff377fbecc55 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 26 May 2026 15:25:14 +0200 Subject: [PATCH 131/170] Fix batchnorm to affine test for parallel execution --- .../test_batchnorm_to_affine_bnn_pynq.py | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/transformation/test_batchnorm_to_affine_bnn_pynq.py b/tests/transformation/test_batchnorm_to_affine_bnn_pynq.py index 84b52c265d..f048597925 100644 --- a/tests/transformation/test_batchnorm_to_affine_bnn_pynq.py +++ b/tests/transformation/test_batchnorm_to_affine_bnn_pynq.py @@ -31,7 +31,6 @@ import numpy as np import onnx import onnx.numpy_helper as nph -import os import torch from brevitas.export import export_qonnx from pathlib import Path @@ -46,11 +45,13 @@ from finn.transformation.qonnx.convert_qonnx_to_finn import ConvertQONNXtoFINN from tests.testing_util.test import get_test_model_trained -export_onnx_path = "test_output_bn2affine.onnx" - @pytest.mark.transform -def test_batchnorm_to_affine_cnv_w1a1(): +def test_batchnorm_to_affine_cnv_w1a1() -> None: + """Test that BatchNormToAffine transformation produces the same output as the original model, + and that there are no BN nodes left in the transformed model. + Also check that the predicted class is the same before and after transformation.""" + export_onnx_path = "test_output_bn2affine_cnv_w1a1.onnx" lfc = get_test_model_trained("CNV", 1, 1) export_qonnx(lfc, torch.randn(1, 3, 32, 32), export_onnx_path) qonnx_cleanup(export_onnx_path, out_file=export_onnx_path) @@ -66,20 +67,23 @@ def test_batchnorm_to_affine_cnv_w1a1(): assert input_tensor.shape == (1, 3, 32, 32) input_dict = {"0": input_tensor} output_dict = oxe.execute_onnx(model, input_dict) - expected = output_dict[list(output_dict.keys())[0]] + expected = output_dict[next(iter(output_dict.keys()))] new_model = model.transform(BatchNormToAffine()) # check that there are no BN nodes left - op_types = list(map(lambda x: x.op_type, new_model.graph.node)) + op_types = [x.op_type for x in new_model.graph.node] assert "BatchNormalization" not in op_types output_dict_p = oxe.execute_onnx(new_model, input_dict) - produced = output_dict_p[list(output_dict_p.keys())[0]] + produced = output_dict_p[next(iter(output_dict_p.keys()))] assert np.isclose(expected, produced).all() assert np.argmax(produced) == 3 - os.remove(export_onnx_path) + Path(export_onnx_path).unlink() @pytest.mark.transform -def test_batchnorm_to_affine_lfc_w1a1(): +def test_batchnorm_to_affine_lfc_w1a1() -> None: + """Test that BatchNormToAffine transformation produces the same output as the original model, + and that there are no BN nodes left in the transformed model.""" + export_onnx_path = "test_output_bn2affine_lfc_w1a1.onnx" lfc = get_test_model_trained("LFC", 1, 1) export_qonnx(lfc, torch.randn(1, 1, 28, 28), export_onnx_path) qonnx_cleanup(export_onnx_path, out_file=export_onnx_path) @@ -90,7 +94,8 @@ def test_batchnorm_to_affine_lfc_w1a1(): new_model = model.transform(BatchNormToAffine()) # load one of the test vectors raw_i = get_data("qonnx.data", "onnx/mnist-conv/test_data_set_0/input_0.pb") + assert raw_i is not None input_tensor = onnx.load_tensor_from_string(raw_i) input_dict = {"0": nph.to_array(input_tensor)} assert oxe.compare_execution(model, new_model, input_dict) - os.remove(export_onnx_path) + Path(export_onnx_path).unlink() From b1e22ab416a48d8707ec0de27df2afabfbeb4e75 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 26 May 2026 16:31:27 +0200 Subject: [PATCH 132/170] Fix colliding unittests during parallel execution --- src/finn/transformation/move_reshape.py | 8 +++++--- tests/fpgadataflow/test_convert_to_hw_layers_cnv.py | 7 ++++--- tests/fpgadataflow/test_convert_to_hw_layers_fc.py | 4 ++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/finn/transformation/move_reshape.py b/src/finn/transformation/move_reshape.py index 50029ad9e6..0a9646f974 100644 --- a/src/finn/transformation/move_reshape.py +++ b/src/finn/transformation/move_reshape.py @@ -77,12 +77,14 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: (_b, h, w, c) = shape # absorb transpose into weight matrix, # allowing FC layer to operate on the NHWC input - w = cast("np.ndarray", model.get_initializer(consumer.input[1])) - if w is None: + w_arr = cast( + "np.ndarray", model.get_initializer(consumer.input[1]) + ) + if w_arr is None: raise FINNInternalError( "Initializer for matmul weights is not set." ) - w_new = w.reshape(c, h, w, mh) + w_new = w_arr.reshape(c, h, w, mh) w_new = w_new.transpose((1, 2, 0, 3)) w_new = w_new.reshape(mw, mh) model.set_initializer(consumer.input[1], w_new) diff --git a/tests/fpgadataflow/test_convert_to_hw_layers_cnv.py b/tests/fpgadataflow/test_convert_to_hw_layers_cnv.py index 01f3d8113a..871816251d 100644 --- a/tests/fpgadataflow/test_convert_to_hw_layers_cnv.py +++ b/tests/fpgadataflow/test_convert_to_hw_layers_cnv.py @@ -64,14 +64,15 @@ from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds from tests.testing_util.test import get_test_model_trained -export_onnx_path_cnv = "test_convert_to_hw_layers_cnv.onnx" - @pytest.mark.fpgadataflow @pytest.mark.vivado # Standalone or fused thresholding-based activation @pytest.mark.parametrize("fused_activation", [True, False]) -def test_convert_to_hw_layers_cnv_w1a1(fused_activation): +def test_convert_to_hw_layers_cnv_w1a1(fused_activation: bool) -> None: + export_onnx_path_cnv = ( + "test_convert_to_hw_layers_cnv_" + ("fused" if fused_activation else "standalone") + ".onnx" + ) cnv = get_test_model_trained("CNV", 1, 1) export_qonnx(cnv, torch.randn(1, 3, 32, 32), export_onnx_path_cnv) qonnx_cleanup(export_onnx_path_cnv, out_file=export_onnx_path_cnv) diff --git a/tests/fpgadataflow/test_convert_to_hw_layers_fc.py b/tests/fpgadataflow/test_convert_to_hw_layers_fc.py index 4be04446b6..fe4783efca 100644 --- a/tests/fpgadataflow/test_convert_to_hw_layers_fc.py +++ b/tests/fpgadataflow/test_convert_to_hw_layers_fc.py @@ -60,12 +60,11 @@ from finn.transformation.streamline.round_thresholds import RoundAndClipThresholds from tests.testing_util.test import get_test_model_trained -export_onnx_path = "test_convert_to_hw_layers_fc.onnx" - @pytest.mark.fpgadataflow @pytest.mark.vivado def test_convert_to_hw_layers_tfc_w1a1(): + export_onnx_path = "test_convert_to_hw_layers_tfc_w1a1.onnx" tfc = get_test_model_trained("TFC", 1, 1) export_qonnx(tfc, torch.randn(1, 1, 28, 28), export_onnx_path) qonnx_cleanup(export_onnx_path, out_file=export_onnx_path) @@ -142,6 +141,7 @@ def test_convert_to_hw_layers_tfc_w1a1(): @pytest.mark.vivado def test_convert_to_hw_layers_tfc_w1a2(): tfc = get_test_model_trained("TFC", 1, 2) + export_onnx_path = "test_convert_to_hw_layers_tfc_w1a2.onnx" export_qonnx(tfc, torch.randn(1, 1, 28, 28), export_onnx_path) qonnx_cleanup(export_onnx_path, out_file=export_onnx_path) model = ModelWrapper(export_onnx_path) From fed57399f537da859763b406571bd8fbdb0cefed Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 26 May 2026 19:53:17 +0200 Subject: [PATCH 133/170] Increase CI timeout to two days --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e428737ebd..193e26dad7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -30,7 +30,7 @@ variables: value: "1" SLURM_TIMEOUT: description: "Select SLURM timeout" - value: "1-0" # [days-hours] + value: "2-0" # [days-hours] SLURM_PARTITION: description: "Slurm partition (e.g., normal, largemem, fpga, gpu)" value: "normal" From 81678fe5cfbc89d23baf6812b58e08a9bc6ad85b Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Wed, 27 May 2026 15:42:11 +0200 Subject: [PATCH 134/170] Fix some more unittests --- .../fpgadataflow/set_fifo_depths.py | 16 +++++++++++----- tests/end2end/test_end2end_bnn_pynq.py | 2 ++ tests/end2end/test_end2end_mobilenet_v1.py | 2 ++ tests/end2end/test_ooc_synthesis.py | 2 ++ tests/fpgadataflow/test_fifosizing.py | 3 ++- .../test_fpgadataflow_elementwise_binary.py | 2 ++ .../fpgadataflow/test_fpgadataflow_layernorm.py | 2 ++ tests/fpgadataflow/test_fpgadataflow_mvau.py | 2 ++ tests/fpgadataflow/test_fpgadataflow_requant.py | 2 ++ 9 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/set_fifo_depths.py b/src/finn/transformation/fpgadataflow/set_fifo_depths.py index 70bdd78303..b321227614 100644 --- a/src/finn/transformation/fpgadataflow/set_fifo_depths.py +++ b/src/finn/transformation/fpgadataflow/set_fifo_depths.py @@ -305,19 +305,25 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # Remove all in/outFIFODepths in model for clean slate graph = model.graph for node in graph.node: + # Calculate number of real inputs (predecessors + global inputs) predecessors = model.find_direct_predecessors(node) - successors = model.find_direct_successors(node) + num_preds = len(predecessors) if predecessors is not None else 0 + global_ins = [inp for inp in node.input if model.get_initializer(inp) is None] + num_inputs = len(global_ins) + num_preds + # Number of outputs is equal to number of elements in node.output, + # because no initializers can be used as outputs + successors = len(node.output) n = getCustomOp(node) if n is not None: - if predecessors is not None: + if num_inputs > 0: n.set_nodeattr( "inFIFODepths", - cast("list[str | int | float]", [0] * len(predecessors)), + cast("list[str | int | float]", [0] * num_inputs), ) - if successors is not None: + if successors > 0: n.set_nodeattr( "outFIFODepths", - cast("list[str | int | float]", [0] * len(successors)), + cast("list[str | int | float]", [0] * successors), ) # Set new outFIFODepths according to config diff --git a/tests/end2end/test_end2end_bnn_pynq.py b/tests/end2end/test_end2end_bnn_pynq.py index 9517692bdd..abc6146358 100644 --- a/tests/end2end/test_end2end_bnn_pynq.py +++ b/tests/end2end/test_end2end_bnn_pynq.py @@ -112,6 +112,8 @@ def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, diff --git a/tests/end2end/test_end2end_mobilenet_v1.py b/tests/end2end/test_end2end_mobilenet_v1.py index a0bed27f9c..97920e2df7 100644 --- a/tests/end2end/test_end2end_mobilenet_v1.py +++ b/tests/end2end/test_end2end_mobilenet_v1.py @@ -98,6 +98,8 @@ def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, diff --git a/tests/end2end/test_ooc_synthesis.py b/tests/end2end/test_ooc_synthesis.py index 419b32110a..f17ae3f7b6 100644 --- a/tests/end2end/test_ooc_synthesis.py +++ b/tests/end2end/test_ooc_synthesis.py @@ -41,6 +41,8 @@ def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, diff --git a/tests/fpgadataflow/test_fifosizing.py b/tests/fpgadataflow/test_fifosizing.py index 60c0e86907..8df970c87a 100644 --- a/tests/fpgadataflow/test_fifosizing.py +++ b/tests/fpgadataflow/test_fifosizing.py @@ -56,6 +56,8 @@ def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = build_cfg.DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, @@ -141,7 +143,6 @@ def make_multi_io_modelwrapper(ch: int, pe: int, idt: BaseDataType) -> ModelWrap @pytest.mark.slow @pytest.mark.vivado @pytest.mark.fpgadataflow -@pytest.mark.parametrize("method", ["largefifo_rtlsim", "characterize"]) @pytest.mark.parametrize("topology", ["tfc", "cnv"]) def test_fifosizing_linear(method, topology): tmp_output_dir = fetch_test_model(topology) diff --git a/tests/fpgadataflow/test_fpgadataflow_elementwise_binary.py b/tests/fpgadataflow/test_fpgadataflow_elementwise_binary.py index f33a55d89e..a64a51c30f 100644 --- a/tests/fpgadataflow/test_fpgadataflow_elementwise_binary.py +++ b/tests/fpgadataflow/test_fpgadataflow_elementwise_binary.py @@ -84,6 +84,8 @@ def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, diff --git a/tests/fpgadataflow/test_fpgadataflow_layernorm.py b/tests/fpgadataflow/test_fpgadataflow_layernorm.py index 9c5def4f8d..e6d7699621 100644 --- a/tests/fpgadataflow/test_fpgadataflow_layernorm.py +++ b/tests/fpgadataflow/test_fpgadataflow_layernorm.py @@ -48,6 +48,8 @@ def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, diff --git a/tests/fpgadataflow/test_fpgadataflow_mvau.py b/tests/fpgadataflow/test_fpgadataflow_mvau.py index 1c2a148807..0e406bc0a2 100644 --- a/tests/fpgadataflow/test_fpgadataflow_mvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_mvau.py @@ -68,6 +68,8 @@ def InsertAndSetFIFODepths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, diff --git a/tests/fpgadataflow/test_fpgadataflow_requant.py b/tests/fpgadataflow/test_fpgadataflow_requant.py index 67ab8b116f..0fad4211a3 100644 --- a/tests/fpgadataflow/test_fpgadataflow_requant.py +++ b/tests/fpgadataflow/test_fpgadataflow_requant.py @@ -60,6 +60,8 @@ def insert_and_set_fifo_depths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: """Run FIFO sizing for testing.""" cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, From 31cd73baeffcaa2270bff3d15f747eb8efcbb2b4 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 28 May 2026 18:26:44 +0200 Subject: [PATCH 135/170] Fix more unittests --- .gitlab-ci.yml | 2 +- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 13 +++ finn_xsi/finn_xsi/adapter.py | 2 + finn_xsi/finn_xsi/include/Simulation.hpp | 8 ++ src/finn/builder/build_dataflow_steps.py | 23 ++-- .../fpgadataflow/create_stitched_ip.py | 22 +++- .../fpgadataflow/simulation_build.py | 40 +++++-- .../fpgadataflow/simulation_connected.py | 42 ++++++- src/finn/util/basic.py | 55 +++++++++ tests/fpgadataflow/test_fifosizing.py | 106 ++++++++++++++---- 10 files changed, 268 insertions(+), 45 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 193e26dad7..3c8d15e8a2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -157,7 +157,7 @@ FINN Test Suite 2022.2: paths: - deps variables: - SCHEDULER_PARAMETERS: "-A $PROJECT_ACCOUNT -p $SLURM_PARTITION -t $SLURM_TIMEOUT $SLURM_QOS --nodes 1 --ntasks 1 --cpus-per-task $CPU_CORES --exclusive" + SCHEDULER_PARAMETERS: "-A $PROJECT_ACCOUNT -p $SLURM_PARTITION -t $SLURM_TIMEOUT $SLURM_QOS --nodes 1 --ntasks 1 --exclusive" PYTEST_PARALLEL: "$CPU_CORES" extends: .setup_full_2022_2 script: diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 3f9d8570a8..98216372b7 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -203,6 +203,16 @@ class SimulationController { } status["output_job_size"] = out_job_sizes; } + // Add latency data + { + json latencies = json::array(); + for (size_t i = 0; i < OutstreamCount; ++i) { + latencies.push_back(sim.getLatencyCycles(i)); + } + if (!latencies.empty()) { + status["latency_cycles"] = latencies; + } + } break; case SimulationState::ERROR: status["state"] = "error"; @@ -302,6 +312,9 @@ void process_command(const json& request, json& response, SimulationController& if (final_status.contains("output_job_size")) { response["output_job_size"] = final_status["output_job_size"]; } + if (final_status.contains("latency_cycles")) { + response["latency_cycles"] = final_status["latency_cycles"]; + } } else { response["status"] = "error"; response["message"] = "Unknown command: " + command; diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 60012fa4dd..c5425ee1ba 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -159,6 +159,8 @@ def load_sim_obj( if simkernel_so is None: simkernel_so = get_simkernel_so() oldcwd = Path.cwd() + if not sim_out_dir.is_dir() or not (sim_out_dir / out_so_relative_path).is_file(): + raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), sim_out_dir) os.chdir(sim_out_dir) sim = SimEngine(simkernel_so, str(out_so_relative_path), "finnxsi_rtlsim.log", tracefile) if tracefile: diff --git a/finn_xsi/finn_xsi/include/Simulation.hpp b/finn_xsi/finn_xsi/include/Simulation.hpp index 77ba245feb..86655d98d6 100644 --- a/finn_xsi/finn_xsi/include/Simulation.hpp +++ b/finn_xsi/finn_xsi/include/Simulation.hpp @@ -164,6 +164,9 @@ class SingleNodeSimulation : public Simulationistreams[inputIndex].job_size; } + /// Get the latency in cycles of the specified output stream + std::size_t getLatencyCycles(std::size_t outputIndex = 0) { + return this->ostreams[outputIndex].first_complete; + } + /// Get the number of cycles the simulation has run std::size_t getCyclesRun() const noexcept { return cyclesRun; } diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 501363529f..9591d466c1 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -553,7 +553,7 @@ def step_set_fifo_depths( fifo_info["impl_style"][node.name] = node_inst.get_nodeattr("impl_style") fifo_info["ram_style"][node.name] = node_inst.get_nodeattr("ram_style") total_fifo_size += fifo_info["fifo_sizes"][node.name] - fifo_info["total_fifo_size_kiB"] = int(total_fifo_size / 8.0 / 1024.0) + fifo_info["total_fifo_size_kiB"] = total_fifo_size / 8.0 / 1024.0 with (Path(cfg.output_dir) / "report" / "fifo_sizing.json").open("w") as f: json.dump(fifo_info, f, indent=2) @@ -1283,13 +1283,17 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi os.environ["RTLSIM_TRACE_DEPTH"] = "3" model.set_metadata_prop("rtlsim_trace", str(report_dir.resolve() / "rtlsim_perf_trace.wdb")) - # Use critical path estimate to set the timeout limit for FIFO sim - model = model.transform(AnnotateCycles()) - perf = model.analysis(dataflow_performance) - latency = cast("int", perf["critical_path_cycles"]) - max_iters = latency * 10 + if not cfg.auto_fifo_depths and cfg.fifo_config_file is not None: + # Use critical path estimate to set the timeout limit for FIFO sim + model = model.transform(AnnotateCycles()) + perf = model.analysis(dataflow_performance) + latency = cast("int", perf["critical_path_cycles"]) + max_iters = latency * 100 + else: + max_iters = ( + None # Auto FIFO depths are garanteed to prevent deadlock, no need for a timeout + ) # prepare simulation - # model = step_build_simulation(model, cfg, parent_node = None, performance_sim=True) sim = NodeConnectedSimulation( model, SimulationType.NODE_BASED_CONNECTED, @@ -1298,6 +1302,7 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi cfg.functional_simulation, max_qsrl_depth=256, performance_sim=True, + shm_prefix=None, ) nodes = [node for node in model.graph.node if "FIFO" not in node.op_type] @@ -1332,7 +1337,6 @@ def step_measure_rtlsim_performance(model: ModelWrapper, cfg: DataflowBuildConfi del res["fifo_cycles_until_first_valid"] cycle_per_sec = 1e9 / cfg.synth_clk_period_ns res["throughput_fps"] = cycle_per_sec / res["intervals"][0] # type: ignore - # TODO: Add latency measurement # Attach entry to output outputs.append(res) @@ -1472,7 +1476,8 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo cfg.enable_hw_debug, cfg.enable_instrumentation, cfg.instrumentation_no_dma, - cfg.live_fifo_sizing, + cfg.auto_fifo_depths + and cfg.auto_fifo_strategy == AutoFIFOSizingMethod.LIVE_FIFO, partition_model_dir=partition_model_dir, ) ) diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py index 1513023cf6..55b5024e3a 100644 --- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py +++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py @@ -52,7 +52,7 @@ from finn.custom_op.fpgadataflow.rtlbackend import RTLBackend from finn.templates import get_templates_folder from finn.transformation.fpgadataflow.replace_verilog_relpaths import ReplaceVerilogRelPaths -from finn.util.basic import launch_process_helper, make_build_dir +from finn.util.basic import launch_process_helper, make_build_dir, wait_for_file from finn.util.exception import FINNInternalError, FINNUserError from finn.util.fpgadataflow import is_hls_node, is_rtl_node from finn.util.hbm_mock import HBMDummy @@ -491,7 +491,21 @@ def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: f"IP generation directory doesn't exist in node {node.name}." ) ip_dirs += [ip_dir_value] - self.create_cmds += node_inst.code_generation_ipi() + filter_str = "add_files -norecurse" + ipi_commands = node_inst.code_generation_ipi() + for cmd in ipi_commands: + if cmd.startswith(filter_str): + split_cmd = cmd.split() + if len(split_cmd) != 3: + raise FINNInternalError( + f"Unexpected command format in node {node.name}: {cmd}" + ) + src = split_cmd[2] + if not wait_for_file(Path(src), timeout=5.0): + raise FINNInternalError( + f"Expected file {src} from code generation for {node.name} not found." + ) + self.create_cmds += ipi_commands self.connect_clk_rst(node) self.connect_ap_none_external(node) self.connect_axi(node, model) @@ -842,10 +856,10 @@ def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: f.write(f"{fifosim_wrapper_filename}\n") # wrapper may be created in different location depending on Vivado version - if not Path(wrapper_filename).is_file(): + if not wait_for_file(Path(wrapper_filename), timeout=5.0): # check in alternative location (.gen instead of .srcs) wrapper_filename_alt = wrapper_filename.replace(".srcs", ".gen") - if Path(wrapper_filename_alt).is_file(): + if wait_for_file(Path(wrapper_filename_alt), timeout=5.0): if not self.functional_simulation: model.set_metadata_prop("wrapper_filename", wrapper_filename_alt) else: diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 7b20166f0e..eeae07940b 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -5,8 +5,10 @@ import onnx import os import psutil +import random import re import shlex +import string import subprocess import sys import time @@ -32,7 +34,13 @@ from finn.transformation.fpgadataflow.insert_dwc import InsertDWC from finn.transformation.fpgadataflow.prepare_ip import PrepareIP from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -from finn.util.basic import getHWCustomOp, launch_process_helper, make_build_dir +from finn.util.basic import ( + getHWCustomOp, + launch_process_helper, + make_build_dir, + wait_for_dir, + wait_for_file, +) from finn.util.exception import FINNInternalError, FINNUserError from finn.util.logging import log from finn.util.settings import get_settings @@ -55,12 +63,18 @@ class SimulationBuilder: """Build simulations in FINN.""" def __init__( - self, model: ModelWrapper, fpgapart: str, clk_ns: float, performance_sim: bool = False + self, + model: ModelWrapper, + fpgapart: str, + clk_ns: float, + shm_prefix: str, + performance_sim: bool = False, ) -> None: """Create a new simulation instance.""" self.model = model self.fpgapart = fpgapart self.clk_ns = clk_ns + self.shm_prefix = shm_prefix self.performance_sim = performance_sim def _create_existing_initializer_input( @@ -703,11 +717,13 @@ def _template_rtlsim_config( "SIMKERNEL_SO": finnxsi.get_simkernel_so(), # log file for xsi (not the sim driver) "XSIM_LOG_FILE": '"xsi.log"', - "INPUT_INTERFACE_NAMES": ",".join(['"' + name + '"' for name in input_interface_names]) + "INPUT_INTERFACE_NAMES": ",".join( + ['"' + self.shm_prefix + name + '"' for name in input_interface_names] + ) if input_interface_names is not None else "", "OUTPUT_INTERFACE_NAMES": ",".join( - ['"' + name + '"' for name in output_interface_names] + ['"' + self.shm_prefix + name + '"' for name in output_interface_names] ) if output_interface_names is not None else "", @@ -771,7 +787,7 @@ def build_single_node_simulation( """ # Check that the relevant data exists wrapper_filename = node_model.get_metadata_prop("wrapper_filename") - if wrapper_filename is None or not Path(wrapper_filename).exists(): + if wrapper_filename is None or not wait_for_file(Path(wrapper_filename), timeout=5.0): raise FINNUserError( f"Call CreateStitchedIP prior to building " f"the simulation for {self.model.graph.node[node_index].name}. " @@ -779,7 +795,9 @@ def build_single_node_simulation( ) vivado_stitched_proj = node_model.get_metadata_prop("vivado_stitch_proj") - if vivado_stitched_proj is None or not Path(vivado_stitched_proj).exists(): + if vivado_stitched_proj is None or not wait_for_dir( + Path(vivado_stitched_proj), timeout=5.0 + ): raise FINNUserError( f"Call CreateStitchedIP prior to building " f"the simulation for {self.model.graph.node[node_index].name}." @@ -1083,8 +1101,16 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: # For rtlsim performance, we assume, that we already have a complete model. if not self.performance_sim: self._prepare_model() + + # Set shm prefix + prefix = f"finn_sim_{os.getpid()}_" + self.shm_prefix = ( + prefix + "".join(random.choices(string.ascii_letters + string.digits, k=12)) + "_" + ) + self.model.set_metadata_prop("shm_prefix", self.shm_prefix) + self.builder = SimulationBuilder( - self.model, self.fpgapart, self.clk_ns, self.performance_sim + self.model, self.fpgapart, self.clk_ns, self.shm_prefix, self.performance_sim ) with contextlib.suppress(AttributeError): sys.stdout = sys.stdout.console # type: ignore diff --git a/src/finn/transformation/fpgadataflow/simulation_connected.py b/src/finn/transformation/fpgadataflow/simulation_connected.py index 33827e04b7..3201e5f484 100644 --- a/src/finn/transformation/fpgadataflow/simulation_connected.py +++ b/src/finn/transformation/fpgadataflow/simulation_connected.py @@ -93,6 +93,7 @@ def __init__( names: list[str], binaries: list[Path], console: Console, + shm_prefix: str, poll_interval: float = 1.0, with_progressbar: bool = True, ) -> None: @@ -102,6 +103,7 @@ def __init__( ) # Synchronization barrier for configuration phase self.sync_barrier: Barrier | None = None + self.shm_prefix = shm_prefix for binary in binaries: if not binary.exists(): console.log(f"Binary {binary} does not exist!") @@ -109,13 +111,17 @@ def __init__( def _cleanup_shm_resources(self) -> None: """Remove any existing shared memory segments and semaphores from /dev/shm.""" + if self.shm_prefix is None: + return try: removed_count = 0 for filepath in Path("/dev/shm").glob("*"): try: + if not filepath.name.startswith(self.shm_prefix): + continue filepath.unlink() removed_count += 1 - except (FileNotFoundError, PermissionError): # noqa: PERF203 + except (FileNotFoundError, PermissionError): # File might already be removed or we don't have permission pass @@ -152,6 +158,7 @@ def run( timeout_result = False fifo_depths: dict[str, list[int]] = {} fifo_cycles_until_first_valid_results: dict[str, list[int]] = {} + latency_cycles_results: dict[str, list[int]] = {} # Clean up any existing shared memory resources before starting self._cleanup_shm_resources() @@ -203,6 +210,7 @@ def run( timeout, fifo_depth, fifo_cycles_until_first_valid, + latency_cycles, ) = result fifo_depths[sim_name] = fifo_depth fifo_results[sim_name] = fifo_util @@ -212,6 +220,7 @@ def run( fifo_cycles_until_first_valid_results[ sim_name ] = fifo_cycles_until_first_valid + latency_cycles_results[sim_name] = latency_cycles timeout_result = timeout_result or timeout except Exception as e: # noqa self.console.log(f"Simulation failed: {e}") @@ -243,6 +252,7 @@ def run( timeout, fifo_depth, fifo_cycles_until_first_valid, + latency_cycles, ) = result # Only update if not already collected if sim_name not in fifo_results: @@ -254,6 +264,7 @@ def run( cycles_results[sim_name] = cycles samples_results[sim_name] = samps intervals_results[sim_name] = intervals + latency_cycles_results[sim_name] = latency_cycles timeout_result = timeout_result or timeout except Exception as e: self.console.log(f"Error collecting result: {e}") @@ -293,6 +304,7 @@ def run( "fifo_cycles_until_first_valid": fifo_cycles_until_first_valid_results.get( name, [] ), + "latency_cycles": latency_cycles_results.get(name, []), } for name in self.names ], @@ -312,7 +324,7 @@ def _run_binary( is_special_for_display: bool = False, max_cycles: int | None = None, fifo_first_valid_cycles: list[int] | None = None, - ) -> tuple[str, list[int], int, int, list[int], bool, list[int], list[int]] | None: + ) -> tuple[str, list[int], int, int, list[int], bool, list[int], list[int], list[int]] | None: """Run the specified simulation binary in a new subprocess and communicate with it. Args: @@ -398,6 +410,7 @@ def _print(msg: str, color: str = "green") -> None: fifo_util: list[int] = [] fifo_depth: list[int] = [] fifo_cycles_until_first_valid: list[int] = [] + latency_cycles: list[int] = [] # Poll for status updates while True: @@ -419,6 +432,7 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = stop_response.get( "fifo_cycles_until_first_valid", [] ) + latency_cycles = stop_response.get("latency_cycles", []) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") return ( @@ -430,6 +444,7 @@ def _print(msg: str, color: str = "green") -> None: timeout, fifo_depth, fifo_cycles_until_first_valid, + latency_cycles, ) time.sleep(self.poll_interval) @@ -453,6 +468,7 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = response.get( "fifo_cycles_until_first_valid", [] ) + latency_cycles = response.get("latency_cycles", []) with self.stop_lock: self.should_stop = True break @@ -480,6 +496,7 @@ def _print(msg: str, color: str = "green") -> None: fifo_cycles_until_first_valid = stop_response.get( "fifo_cycles_until_first_valid", [] ) + latency_cycles = stop_response.get("latency_cycles", []) if fifo_util: logfile.write(f"Final FIFO utilization: {fifo_util}\n") @@ -492,6 +509,7 @@ def _print(msg: str, color: str = "green") -> None: timeout, fifo_depth, fifo_cycles_until_first_valid, + latency_cycles, ) except Exception as e: @@ -514,6 +532,7 @@ def __init__( fpgapart: str, clk_ns: float, functional_sim: bool, + shm_prefix: str | None, workers: int | None = None, max_qsrl_depth: int = 256, performance_sim: bool = False, @@ -522,8 +541,11 @@ def __init__( super().__init__( model, simulation_type, fpgapart, clk_ns, functional_sim, workers, performance_sim ) + if shm_prefix is None: + shm_prefix = model.get_metadata_prop("shm_prefix") self.max_qsrl_depth = max_qsrl_depth self.performance_sim = performance_sim + self.shm_prefix = cast("str", shm_prefix) def simulate( self, @@ -572,7 +594,13 @@ def simulate( start = time.time() output_json = Path(make_build_dir("simulation_results_")) / "simulation_data.json" controller = NodeConnectedSimulationController( - len(self.binaries), names, list(self.binaries.values()), Console(), 0.1, False + len(self.binaries), + names, + list(self.binaries.values()), + Console(), + self.shm_prefix, + 0.1, + False, ) controller.run(adjusted_depth, output_json, max_cycles, fifo_first_valid_cycles) end = time.time() @@ -593,6 +621,7 @@ def simulate( "samples": sim_entry["samples"], "intervals": sim_entry["intervals"], "fifo_cycles_until_first_valid": sim_entry["fifo_cycles_until_first_valid"], + "latency_cycles": sim_entry["latency_cycles"], } ) json.dump(data, output_json.open("w"), indent=4) @@ -695,12 +724,19 @@ def get_minimization_order_indices( def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: """Run layer parallel simulations.""" + shm_prefix = model.get_metadata_prop("shm_prefix") + if shm_prefix is None or shm_prefix == "": + raise FINNInternalError( + "Expected model to have non-empty 'shm_prefix' metadata property " + "for node-connected simulation" + ) sim = NodeConnectedSimulation( model, SimulationType.NODE_BASED_CONNECTED, self.fpgapart, self.clk_ns, self.cfg.functional_simulation, + shm_prefix=shm_prefix, max_qsrl_depth=self.max_qsrl_depth, ) model = sim.model # TODO:clean up diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 398aa527e6..9cbe783b21 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -42,8 +42,10 @@ """ import os +import stat as statmod import subprocess import tempfile +import time from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp @@ -114,6 +116,59 @@ part_map["V80"] = "xcv80-lsva4737-2MHP-e-s" +def wait_for_file( + path: Path, + timeout: float = 30.0, + stable_for: float = 0.2, + interval: float = 0.05, + expect_dir: bool | None = False, +) -> bool: + """Wait until path exists and is stable in size/mtime for stable_for seconds. + + If expect_dir is True, only a directory satisfies the check. If False, + only a regular file satisfies it. If None, either file or directory is ok. + """ + deadline = time.time() + timeout + last = None + last_change = None + + while time.time() < deadline: + try: + st = path.stat() + except FileNotFoundError: + time.sleep(interval) + continue + + if expect_dir is True and not statmod.S_ISDIR(st.st_mode): + time.sleep(interval) + continue + if expect_dir is False and not statmod.S_ISREG(st.st_mode): + time.sleep(interval) + continue + + cur = (st.st_size, st.st_mtime_ns) + now = time.time() + if cur == last: + if last_change is not None and (now - last_change) >= stable_for: + return True + else: + last = cur + last_change = now + + time.sleep(interval) + + return False + + +def wait_for_dir( + path: Path, timeout: float = 30.0, stable_for: float = 0.2, interval: float = 0.05 +) -> bool: + """Wait until directory exists and is stable in size/mtime for stable_for seconds.""" + return wait_for_file( + path, timeout=timeout, stable_for=stable_for, interval=interval, expect_dir=True + ) + + def getHWCustomOp(node: "NodeProto") -> "HWCustomOp": # noqa: N802 """Get the HWCustomOp from a node. Throws an error if the node is not an HWCustomOp.""" from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp diff --git a/tests/fpgadataflow/test_fifosizing.py b/tests/fpgadataflow/test_fifosizing.py index 8df970c87a..7a9c104117 100644 --- a/tests/fpgadataflow/test_fifosizing.py +++ b/tests/fpgadataflow/test_fifosizing.py @@ -25,7 +25,7 @@ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - +"""Test FIFO sizing functionality.""" import pytest @@ -42,6 +42,7 @@ from qonnx.transformation.infer_datatypes import InferDataTypes from qonnx.transformation.infer_shapes import InferShapes from qonnx.util.basic import qonnx_make_model +from typing import Literal import finn.builder.build_dataflow as build import finn.builder.build_dataflow_config as build_cfg @@ -144,42 +145,105 @@ def make_multi_io_modelwrapper(ch: int, pe: int, idt: BaseDataType) -> ModelWrap @pytest.mark.vivado @pytest.mark.fpgadataflow @pytest.mark.parametrize("topology", ["tfc", "cnv"]) -def test_fifosizing_linear(method, topology): +def test_fifosizing_linear(topology: Literal["tfc", "cnv"]) -> None: + """Test FIFO sizing on a simple linear topology, and check that the generated FIFO config.""" tmp_output_dir = fetch_test_model(topology) cfg = build_cfg.DataflowBuildConfig( output_dir=tmp_output_dir, auto_fifo_depths=True, - auto_fifo_strategy=method, target_fps=10000 if topology == "tfc" else 1000, synth_clk_period_ns=10.0, board="Pynq-Z1", generate_outputs=[ build_cfg.DataflowOutputType.ESTIMATE_REPORTS, - build_cfg.DataflowOutputType.STITCHED_IP, build_cfg.DataflowOutputType.RTLSIM_PERFORMANCE, ], + steps=[ + "step_qonnx_to_finn", + "step_tidy_up", + "step_streamline", + "step_convert_to_hw", + "step_create_dataflow_partition", + "step_specialize_layers", + "step_target_fps_parallelization", + "step_apply_folding_config", + "step_minimize_bit_width", + "step_generate_estimate_reports", + "step_generate_hardware", + "step_measure_rtlsim_performance", + ], ) - build.build_dataflow_cfg(tmp_output_dir + "/model.onnx", cfg) - with open(tmp_output_dir + "/report/estimate_network_performance.json") as f: - est_data = json.load(f) - with open(tmp_output_dir + "/report/rtlsim_performance.json") as f: - sim_data = json.load(f) - assert ( - float(sim_data["stable_throughput[images/s]"]) / float(est_data["estimated_throughput_fps"]) - > 0.9 - ) + build.build_dataflow_cfg(str(tmp_output_dir / "model.onnx"), cfg) + + expected_fifos = { + "fifo_depths": { + "StreamingFIFO_rtl_0": 2, + "StreamingFIFO_rtl_1": 32, + "StreamingFIFO_rtl_2": 32, + "StreamingFIFO_rtl_3": 32, + "StreamingFIFO_rtl_4": 32, + "StreamingFIFO_rtl_5": 32, + "StreamingFIFO_rtl_6": 32, + "StreamingFIFO_rtl_7": 32, + "StreamingFIFO_rtl_8": 32, + "StreamingFIFO_rtl_9": 32, + }, + "fifo_sizes": { + "StreamingFIFO_rtl_0": 1024, + "StreamingFIFO_rtl_1": 1024, + "StreamingFIFO_rtl_2": 64, + "StreamingFIFO_rtl_3": 448, + "StreamingFIFO_rtl_4": 64, + "StreamingFIFO_rtl_5": 64, + "StreamingFIFO_rtl_6": 64, + "StreamingFIFO_rtl_7": 256, + "StreamingFIFO_rtl_8": 1024, + "StreamingFIFO_rtl_9": 1024, + }, + "impl_style": { + "StreamingFIFO_rtl_0": "rtl", + "StreamingFIFO_rtl_1": "rtl", + "StreamingFIFO_rtl_2": "rtl", + "StreamingFIFO_rtl_3": "rtl", + "StreamingFIFO_rtl_4": "rtl", + "StreamingFIFO_rtl_5": "rtl", + "StreamingFIFO_rtl_6": "rtl", + "StreamingFIFO_rtl_7": "rtl", + "StreamingFIFO_rtl_8": "rtl", + "StreamingFIFO_rtl_9": "rtl", + }, + "ram_style": { + "StreamingFIFO_rtl_0": "block", + "StreamingFIFO_rtl_1": "block", + "StreamingFIFO_rtl_2": "block", + "StreamingFIFO_rtl_3": "block", + "StreamingFIFO_rtl_4": "block", + "StreamingFIFO_rtl_5": "block", + "StreamingFIFO_rtl_6": "block", + "StreamingFIFO_rtl_7": "block", + "StreamingFIFO_rtl_8": "block", + "StreamingFIFO_rtl_9": "block", + }, + "total_fifo_size_kiB": 0.6171875, + } + + with (tmp_output_dir / "report/fifo_sizing.json").open() as f: + fifo_sizing_report = json.load(f) + assert fifo_sizing_report == expected_fifos # now run the same build using the generated folding and FIFO config tmp_output_dir_cmp = fetch_test_model(topology) cfg_cmp = cfg cfg_cmp.output_dir = tmp_output_dir_cmp cfg_cmp.auto_fifo_depths = False cfg_cmp.target_fps = None - cfg_cmp.generate_outputs = [build_cfg.DataflowOutputType.STITCHED_IP] - cfg_cmp.folding_config_file = tmp_output_dir + "/report/final_hw_config.json" - build.build_dataflow_cfg(tmp_output_dir_cmp + "/model.onnx", cfg_cmp) + cfg_cmp.folding_config_file = tmp_output_dir / "report/auto_folding_config.json" + cfg_cmp.fifo_config_file = tmp_output_dir / "report/fifo_sizing.json" + build.build_dataflow_cfg(str(tmp_output_dir_cmp / "model.onnx"), cfg_cmp) - model0 = ModelWrapper(tmp_output_dir + "/intermediate_models/step_create_stitched_ip.onnx") - model1 = ModelWrapper(tmp_output_dir_cmp + "/intermediate_models/step_create_stitched_ip.onnx") + model0 = ModelWrapper(str(tmp_output_dir / "intermediate_models/step_generate_hardware.onnx")) + model1 = ModelWrapper( + str(tmp_output_dir_cmp / "intermediate_models/step_generate_hardware.onnx") + ) assert len(model0.graph.node) == len(model1.graph.node) for i in range(len(model0.graph.node)): @@ -198,9 +262,9 @@ def test_fifosizing_linear(method, topology): @pytest.mark.slow @pytest.mark.vivado @pytest.mark.fpgadataflow -def test_fifosizing_multi_io(): - # construct small onnx graph with addstreams, followed by duplicate streams - # to have test model with multiple inputs and multiple outputs +def test_fifosizing_multi_io() -> None: + """Construct small onnx graph with addstreams, followed by duplicate streams + to have test model with multiple inputs and multiple outputs.""" model = make_multi_io_modelwrapper(2, 2, DataType["INT4"]) model = model.transform(SpecializeLayers("xc7z020clg400-1")) model = model.transform(GiveUniqueNodeNames()) From b25fda552101b7b1ab2244212994e51355981fe7 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 29 May 2026 10:11:25 +0200 Subject: [PATCH 136/170] Fix end2end tests --- src/finn/builder/build_dataflow_config.py | 6 ++-- src/finn/builder/build_dataflow_steps.py | 44 +++++++++++++++++------ src/finn/util/execution.py | 19 +++++----- tests/end2end/test_end2end_cybsec_mlp.py | 26 +++++++------- 4 files changed, 60 insertions(+), 35 deletions(-) diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index b965626fc8..f13fdab038 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -182,9 +182,7 @@ class VerificationStepType(str, Enum): "step_minimize_bit_width", "step_transpose_decomposition", "step_generate_estimate_reports", - "step_set_fifo_depths", - "step_hw_codegen", - "step_hw_ipgen", + "step_generate_hardware", "step_create_stitched_ip", "step_measure_rtlsim_performance", "step_out_of_context_synthesis", @@ -712,7 +710,7 @@ def _resolve_verification_steps(self) -> list[VerificationStepType]: return [] return self.verify_steps - def _resolve_verification_io_pair(self) -> None | tuple[Any, Any]: + def _resolve_verification_io_pair(self) -> None | tuple[np.ndarray, np.ndarray]: """Load and validate the input/output numpy arrays for verification. Loads the verification input and expected output arrays from the files diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 9591d466c1..b80edfe573 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -180,41 +180,62 @@ def verify_step( rtlsim_pre_hook: Optional pre-hook function for RTL simulation """ log.info(f"Running verification for {step_name}") - verify_out_dir = cfg.output_dir + "/verification_output" - intermediate_models_dir = cfg.output_dir + "/intermediate_models" + output_dir = Path(cfg.output_dir) + verify_out_dir = output_dir / "verification_output" + intermediate_models_dir = output_dir / "intermediate_models" # Ensure tensor names are sorted and readable for easier debugging model = model.transform(SortGraph()) model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) os.makedirs(verify_out_dir, exist_ok=True) - (in_npy_all, exp_out_npy_all) = cfg._resolve_verification_io_pair() + if cfg.verify_steps is None: + raise FINNUserError("verify_steps is not set in config, but verification step was called") + (in_npy_all, exp_out_npy_all) = cast( + "tuple[np.ndarray, np.ndarray]", cfg._resolve_verification_io_pair() + ) bsize_in = in_npy_all.shape[0] bsize_out = exp_out_npy_all.shape[0] assert bsize_in == bsize_out, "Batch sizes don't match for verification IO pair" all_res = True + out_dict: dict[str, np.ndarray] = {} + parent_model = None + res_to_str = {True: "SUCCESS", False: "FAIL"} for b in range(bsize_in): in_npy = np.expand_dims(in_npy_all[b], axis=0) exp_out_npy = np.expand_dims(exp_out_npy_all[b], axis=0) if need_parent: assert cfg.save_intermediate_models, "Enable save_intermediate_models for verification" - parent_model_fn = intermediate_models_dir + "/dataflow_parent.onnx" - child_model_fn = intermediate_models_dir + "/verify_%s.onnx" % step_name + parent_model_fn = intermediate_models_dir / "dataflow_parent.onnx" + child_model_fn = intermediate_models_dir / f"verify_{step_name}.onnx" model.save(child_model_fn) - parent_model = ModelWrapper(parent_model_fn) + parent_model = ModelWrapper(str(parent_model_fn)) out_tensor_name = parent_model.get_first_global_out() exp_ishape = parent_model.get_tensor_shape(parent_model.get_first_global_in()) + if exp_ishape is None: + raise FINNUserError( + f"Unable to determine expected input shape for verification. " + f"Shape of tensor {parent_model.get_first_global_in()} is None." + ) if in_npy.shape != exp_ishape: log.warning( f"Verification input has shape {in_npy.shape} while model expects {exp_ishape}" ) log.info("Attempting to force model shape on verification input") in_npy = in_npy.reshape(exp_ishape) - out_dict = execute_parent(parent_model_fn, child_model_fn, in_npy, return_full_ctx=True) + out_dict = cast( + "dict[str, np.ndarray]", + execute_parent(parent_model_fn, child_model_fn, in_npy, return_full_ctx=True), + ) out_npy = out_dict[out_tensor_name] else: inp_tensor_name = model.get_first_global_in() out_tensor_name = model.get_first_global_out() exp_ishape = model.get_tensor_shape(inp_tensor_name) + if exp_ishape is None: + raise FINNUserError( + f"Unable to determine expected input shape for verification. " + f"Shape of tensor {model.get_first_global_in()} is None." + ) if in_npy.shape != exp_ishape: log.warning( f"Verification input has shape {in_npy.shape} while model expects {exp_ishape}" @@ -251,8 +272,7 @@ def verify_step( res = res1 and res2 and res3 all_res = all_res and res - res_to_str = {True: "SUCCESS", False: "FAIL"} - res_str = res_to_str[res] + res_str = res_to_str[bool(res)] if cfg.verify_save_full_context and (rtlsim_pre_hook is None): verification_output_fn = os.path.join( verify_out_dir, f"verify_{step_name}_{b}_{res_str}.npz" @@ -262,6 +282,8 @@ def verify_step( # Log tensor statistics for debugging (only output tensors, in topological order) tensors_to_log = ["global_in"] if need_parent: + if parent_model is None: + raise FINNUserError("Parent model is needed for verification but is None") for node in parent_model.graph.node: for output in node.output: tensors_to_log.append(output) @@ -344,12 +366,12 @@ def verify_step( if step_name == "node_by_node_rtlsim": for node in model.graph.node: node_inst = getCustomOp(node) - node_wdb_path = node_inst.get_nodeattr("rtlsim_trace") + node_wdb_path = cast("str", node_inst.get_nodeattr("rtlsim_trace")) if node_wdb_path is not None and os.path.isfile(node_wdb_path): new_node_wdb_path = node_wdb_path.replace(".wdb", "_%d.wdb" % b) shutil.move(node_wdb_path, new_node_wdb_path) - log.info(f"Verification for {step_name} : {res_to_str[all_res]}") + log.info(f"Verification for {step_name} : {res_to_str[bool(all_res)]}") @register_build_dataflow_step() diff --git a/src/finn/util/execution.py b/src/finn/util/execution.py index 619293723c..0dca500cb1 100644 --- a/src/finn/util/execution.py +++ b/src/finn/util/execution.py @@ -40,11 +40,11 @@ from finn.core.onnx_exec import execute_onnx -def load_model_checkpoint(filename: str) -> ModelWrapper: +def load_model_checkpoint(filename: str | Path) -> ModelWrapper: """Load given .onnx file and return ModelWrapper. Args: - filename (str): Path to the ONNX model file + filename (str|Path): Path to the ONNX model file Returns: ModelWrapper: Loaded model @@ -53,20 +53,23 @@ def load_model_checkpoint(filename: str) -> ModelWrapper: FileNotFoundError: If the model file doesn't exist """ if Path(filename).is_file(): - model = ModelWrapper(filename) + model = ModelWrapper(str(filename)) return model raise FileNotFoundError(f"Model file {filename} not found") def execute_parent( - parent_path: str, child_path: str, input_tensor_npy: np.ndarray, return_full_ctx: bool = False + parent_path: str | Path, + child_path: str | Path, + input_tensor_npy: np.ndarray, + return_full_ctx: bool = False, ) -> np.ndarray | dict[str, np.ndarray]: """Execute parent model containing a single StreamingDataflowPartition by replacing it with the model at child_path and return result. Args: - parent_path (str): Path to the parent ONNX model file - child_path (str): Path to the child ONNX model file to replace the partition + parent_path (str|Path): Path to the parent ONNX model file + child_path (str|Path): Path to the child ONNX model file to replace the partition input_tensor_npy (numpy.ndarray): Input tensor data return_full_ctx (bool): If True, return full execution context, otherwise return only output tensor @@ -74,12 +77,12 @@ def execute_parent( Returns: numpy.ndarray or dict: Output tensor or full execution context """ - parent_model = load_model_checkpoint(parent_path) + parent_model = load_model_checkpoint(str(parent_path)) iname = parent_model.get_first_global_in() oname = parent_model.get_first_global_out() sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0] sdp_node = getCustomOp(sdp_node) - sdp_node.set_nodeattr("model", child_path) + sdp_node.set_nodeattr("model", str(child_path)) sdp_node.set_nodeattr("return_full_exec_context", 1 if return_full_ctx else 0) ret = execute_onnx(parent_model, {iname: input_tensor_npy}, True) if return_full_ctx: diff --git a/tests/end2end/test_end2end_cybsec_mlp.py b/tests/end2end/test_end2end_cybsec_mlp.py index 0de44cd2de..4b6f2dcc99 100644 --- a/tests/end2end/test_end2end_cybsec_mlp.py +++ b/tests/end2end/test_end2end_cybsec_mlp.py @@ -167,19 +167,21 @@ def test_end2end_cybsec_mlp_build(self): ) build.build_dataflow_cfg(model_file, cfg) # check the generated files - assert os.path.isfile(output_dir + "/report/time_per_step.json") - assert os.path.isfile(output_dir + "/report/final_hw_config.json") - assert os.path.isfile(output_dir + "/template_specialize_layers_config.json") - assert os.path.isfile(output_dir + "/driver/driver.py") - est_cycles_report = output_dir + "/report/estimate_layer_cycles.json" + output_dir = Path(output_dir) + assert os.path.isfile(output_dir / "report/time_per_step.json") + assert os.path.isfile(output_dir / "report/auto_folding_config.json") + assert os.path.isfile(output_dir / "report/fifo_sizing.json") + assert os.path.isfile(output_dir / "template_specialize_layers_config.json") + assert os.path.isfile(output_dir / "driver/driver.py") + est_cycles_report = output_dir / "report/estimate_layer_cycles.json" assert os.path.isfile(est_cycles_report) - est_res_report = output_dir + "/report/estimate_layer_resources.json" + est_res_report = output_dir / "report/estimate_layer_resources.json" assert os.path.isfile(est_res_report) - assert os.path.isfile(output_dir + "/report/estimate_network_performance.json") - assert os.path.isfile(output_dir + "/bitfile/finn-accel.bit") - assert os.path.isfile(output_dir + "/bitfile/finn-accel.hwh") - assert os.path.isfile(output_dir + "/report/post_synth_resources.xml") - assert os.path.isfile(output_dir + "/report/post_route_timing.rpt") + assert os.path.isfile(output_dir / "report/estimate_network_performance.json") + assert os.path.isfile(output_dir / "bitfile/finn-accel.bit") + assert os.path.isfile(output_dir / "bitfile/finn-accel.hwh") + assert os.path.isfile(output_dir / "report/post_synth_resources.xml") + assert os.path.isfile(output_dir / "report/post_route_timing.rpt") # examine the report contents with open(est_cycles_report, "r") as f: est_cycles_dict = json.load(f) @@ -189,5 +191,5 @@ def test_end2end_cybsec_mlp_build(self): est_res_dict = json.load(f) assert est_res_dict["total"]["LUT"] == 7899.0 assert est_res_dict["total"]["BRAM_18K"] == 36.0 - shutil.copytree(output_dir + "/deploy", get_checkpoint_name("build")) + shutil.copytree(output_dir / "deploy", get_checkpoint_name("build")) shutil.rmtree(get_checkpoint_name("build")) From 7777d29027687d73b98052e14874d1cbafbaf417 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 29 May 2026 10:16:22 +0200 Subject: [PATCH 137/170] Try reduced test suite to debug hanging tests --- .gitlab-ci.yml | 2 +- src/finn/interface/manage_tests.py | 48 +++++++++++++++--------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3c8d15e8a2..e6318a3e7c 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -33,7 +33,7 @@ variables: value: "2-0" # [days-hours] SLURM_PARTITION: description: "Slurm partition (e.g., normal, largemem, fpga, gpu)" - value: "normal" + value: "fpga" SLURM_QOS: description: "Optional --exclusive or QoS option (include --qos, e.g., --qos express)" value: "" diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index cfc865c552..15c894adbd 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -118,36 +118,36 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: posix=IS_POSIX, ) ) - test_2_process = subprocess.Popen( - shlex.split( - ( - f"{sys.executable} -m pytest -v -m 'end2end or sanity_bnn or notebooks' " - f"--junitxml={ci_project_dir}/reports/end2end.xml " - f"--html={ci_project_dir}/reports/end2end.html " - f"--reruns 1 --dist loadgroup -n {num_workers}" - ), - posix=IS_POSIX, - ) - ) + # test_2_process = subprocess.Popen( + # shlex.split( + # ( + # f"{sys.executable} -m pytest -v -m 'end2end or sanity_bnn or notebooks' " + # f"--junitxml={ci_project_dir}/reports/end2end.xml " + # f"--html={ci_project_dir}/reports/end2end.html " + # f"--reruns 1 --dist loadgroup -n {num_workers}" + # ), + # posix=IS_POSIX, + # ) + # ) test_1_process.communicate() test_1_returncode = test_1_process.returncode - test_2_process.communicate() - test_2_returncode = test_2_process.returncode + # test_2_process.communicate() + # test_2_returncode = test_2_process.returncode # Run doctests for all FINN submodules - test_3_returncode = run_doctests(int(num_workers)) + # test_3_returncode = run_doctests(int(num_workers)) - subprocess.run( - shlex.split( - ( - f"{sys.executable} -m pytest_html_merger -i {ci_project_dir}/reports/ " - f"-o {ci_project_dir}/reports/full_test_suite.html" - ), - posix=IS_POSIX, - ) - ) + # subprocess.run( + # shlex.split( + # ( + # f"{sys.executable} -m pytest_html_merger -i {ci_project_dir}/reports/ " + # f"-o {ci_project_dir}/reports/full_test_suite.html" + # ), + # posix=IS_POSIX, + # ) + # ) - if test_1_returncode or test_2_returncode or test_3_returncode: + if test_1_returncode: # or test_2_returncode or test_3_returncode: sys.exit(1) case _: From 93de11f2e0fa7dc73d30fbab5bfa2366e8b201a9 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Sat, 30 May 2026 14:01:15 +0200 Subject: [PATCH 138/170] Fix some more bugs --- ci/cfg/live_fifosizing.yml | 57 ++++--- ci/collect/collect.py | 2 +- src/finn/analysis/__init__.py | 1 + src/finn/analysis/fpgadataflow/__init__.py | 1 + src/finn/benchmarking/bench_base.py | 136 ++++++++------- src/finn/benchmarking/dut/bnn-pynq.yml | 7 +- src/finn/benchmarking/dut/gtsrb.yml | 4 +- src/finn/benchmarking/dut/kws.yml | 4 +- src/finn/benchmarking/dut/mobilenetv1.yml | 7 +- src/finn/benchmarking/dut/resnet18.yml | 4 +- src/finn/benchmarking/dut/resnet50.yml | 7 +- src/finn/benchmarking/dut/transformer.yml | 8 +- src/finn/benchmarking/dut/vgg10.yml | 8 +- src/finn/builder/build_dataflow.py | 6 +- src/finn/core/__init__.py | 1 + src/finn/interface/__init__.py | 1 + src/finn/interface/manage_tests.py | 4 +- .../qonnx/qonnx_activation_handlers.py | 2 +- src/finn/transformation/streamline/absorb.py | 2 +- .../test_fpgadataflow_finnloop.py | 1 - .../test_fpgadataflow_relu_elementwisemax.py | 14 +- .../fpgadataflow/test_fpgadataflow_shuffle.py | 3 +- .../test_fpgadataflow_thresholding.py | 2 + tests/fpgadataflow/test_simulation_build.py | 156 ++++++++++-------- tests/fpgadataflow/test_split_large_fifos.py | 1 - 25 files changed, 234 insertions(+), 205 deletions(-) diff --git a/ci/cfg/live_fifosizing.yml b/ci/cfg/live_fifosizing.yml index 76516848eb..c66af0121d 100644 --- a/ci/cfg/live_fifosizing.yml +++ b/ci/cfg/live_fifosizing.yml @@ -6,7 +6,8 @@ "model_path": [models/bnn-pynq/cnv-w1a1_qonnx.onnx], "folding_config_file": [models/bnn-pynq/cnv-w1a1_folding_config.json], "specialize_layers_config_file": [models/bnn-pynq/cnv-w1a1_specialize_layers.json], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -16,7 +17,8 @@ "model_path": [models/bnn-pynq/cnv-w1a2_qonnx.onnx], "folding_config_file": [models/bnn-pynq/cnv-w1a2_folding_config.json], "specialize_layers_config_file": [models/bnn-pynq/cnv-w1a2_specialize_layers.json], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -26,7 +28,8 @@ "model_path": [models/bnn-pynq/cnv-w2a2_qonnx.onnx], "folding_config_file": [models/bnn-pynq/cnv-w2a2_folding_config.json], "specialize_layers_config_file": [models/bnn-pynq/cnv-w2a2_specialize_layers.json], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -36,7 +39,8 @@ "model_path": [models/bnn-pynq/tfc-w1a1_qonnx.onnx], "folding_config_file": [models/bnn-pynq/tfc-w1a1_folding_config.json], "specialize_layers_config_file": [models/bnn-pynq/tfc-w1a1_specialize_layers.json], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -46,7 +50,8 @@ "model_path": [models/bnn-pynq/tfc-w1a2_qonnx.onnx], "folding_config_file": [models/bnn-pynq/tfc-w1a2_folding_config.json], "specialize_layers_config_file": [models/bnn-pynq/tfc-w1a2_specialize_layers.json], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -56,7 +61,8 @@ "model_path": [models/bnn-pynq/tfc-w2a2_qonnx.onnx], "folding_config_file": [models/bnn-pynq/tfc-w2a2_folding_config.json], "specialize_layers_config_file": [models/bnn-pynq/tfc-w2a2_specialize_layers.json], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -64,7 +70,8 @@ # GTSRB (cnv-w1a1) { "dut": ["gtsrb"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -72,7 +79,8 @@ # KWS (MLP) { "dut": ["kws"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -80,7 +88,8 @@ # Cybersecurity (MLP) { "dut": ["cybsec"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -88,7 +97,8 @@ # MobileNetV1 (ImageNet) { "dut": ["mobilenetv1"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -96,7 +106,8 @@ # VGG-10 (1D CNN) (RadioML) { "dut": ["vgg10"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -104,7 +115,8 @@ # ResNet-50 (ImageNet) { "dut": ["resnet50"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -115,7 +127,8 @@ "model_path": [models/resnet18/resnet18_w3a3_cifar100.onnx], "folding_config_file": [models/resnet18/resnet18_folding_config.json], "specialize_layers_config_file": [models/resnet18/resnet18_specialize_layers.json], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "synth_clk_period_ns": [10], }, @@ -128,7 +141,8 @@ "model_path": ["models/transformer/finn-transformers/benchmark/streamlined.onnx"], "verify_input_npy": ["models/transformer/finn-transformers/benchmark/inp.npy"], "verify_expected_output_npy": ["models/transformer/finn-transformers/benchmark/out.npy"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "target_fps": [1000], }, @@ -140,7 +154,8 @@ "model_path": ["models/transformer/finn-transformers/language/streamlined.onnx"], "verify_input_npy": ["models/transformer/finn-transformers/language/inp.npy"], "verify_expected_output_npy": ["models/transformer/finn-transformers/language/out.npy"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "target_fps": [1000], }, @@ -152,7 +167,8 @@ "model_path": ["models/transformer/finn-transformers/radioml/streamlined.onnx"], "verify_input_npy": ["models/transformer/finn-transformers/radioml/inp.npy"], "verify_expected_output_npy": ["models/transformer/finn-transformers/radioml/out.npy"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "target_fps": [1000], }, @@ -164,7 +180,8 @@ "model_path": ["models/transformer/finn-transformers/vision/streamlined.onnx"], "verify_input_npy": ["models/transformer/finn-transformers/vision/inp.npy"], "verify_expected_output_npy": ["models/transformer/finn-transformers/vision/out.npy"], - "live_fifo_sizing": [True], + "auto_fifo_depths": [True], + "auto_fifo_strategy": ["live_fifo"], "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], "target_fps": [1000], }, @@ -182,7 +199,8 @@ # "lb_num_layers": [1], # "rb_num_layers": [4, 8, 16], - # "live_fifo_sizing": [True], + # "auto_fifo_depths": [True], + # "auto_fifo_strategy": ["live_fifo"], # "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], # "synth_clk_period_ns": [10], # }, @@ -198,7 +216,8 @@ # "lb_num_layers": [1], # "rb_num_layers": [4, 8, 16], - # "live_fifo_sizing": [True], + # "auto_fifo_depths": [True], + # "auto_fifo_strategy": ["live_fifo"], # "generate_outputs": [["bitfile", "pynq_driver", "deployment_package"]], # "synth_clk_period_ns": [10], # } diff --git a/ci/collect/collect.py b/ci/collect/collect.py index c9715da1f2..3e49202ee6 100644 --- a/ci/collect/collect.py +++ b/ci/collect/collect.py @@ -1030,7 +1030,7 @@ def extract_model_name(self, metadata): # wrap in list configuration[key] = [metadata_bench["params"][key]] # overwrite FIFO-related params - configuration["live_fifo_sizing"] = [False] + # configuration["live_fifo_sizing"] = [False] configuration["auto_fifo_depths"] = [False] configuration["target_fps"] = ["None"] configuration["folding_config_file"] = [folding_config_lfs_path] diff --git a/src/finn/analysis/__init__.py b/src/finn/analysis/__init__.py index e69de29bb2..01b6390fcb 100644 --- a/src/finn/analysis/__init__.py +++ b/src/finn/analysis/__init__.py @@ -0,0 +1 @@ +"""Analysis utilities for FINN+.""" diff --git a/src/finn/analysis/fpgadataflow/__init__.py b/src/finn/analysis/fpgadataflow/__init__.py index e69de29bb2..17236638f5 100644 --- a/src/finn/analysis/fpgadataflow/__init__.py +++ b/src/finn/analysis/fpgadataflow/__init__.py @@ -0,0 +1 @@ +"""Dataflow analysis utilities for FINN+.""" diff --git a/src/finn/benchmarking/bench_base.py b/src/finn/benchmarking/bench_base.py index 320b4b883d..76ed8a27d0 100644 --- a/src/finn/benchmarking/bench_base.py +++ b/src/finn/benchmarking/bench_base.py @@ -12,13 +12,15 @@ import os import shutil import yaml +from pathlib import Path from shutil import copy as shcopy from shutil import copytree +from typing import Literal, cast import finn.builder.build_dataflow as build import finn.builder.build_dataflow_config as build_cfg from finn.benchmarking.util import delete_dir_contents -from finn.builder.build_dataflow_config import DataflowBuildConfig +from finn.builder.build_dataflow_config import AutoFIFOSizingMethod, DataflowBuildConfig from finn.util.basic import alveo_default_platform, alveo_part_map, part_map from finn.util.logging import log from finn.util.settings import get_settings @@ -38,7 +40,16 @@ class bench: output_dict (dict): Collection of additional metrics produced by this infrastructure """ - def __init__(self, params, task_id, run_id, work_dir, artifacts_dir, save_dir, debug=True): + def __init__( + self, + params: dict, + task_id: int, + run_id: int, + work_dir: str, + artifacts_dir: str, + save_dir: str, + debug: bool | None = True, + ) -> None: """Initialize a new benchmark instance that manages a single FINN build. Args: @@ -59,7 +70,7 @@ def __init__(self, params, task_id, run_id, work_dir, artifacts_dir, save_dir, d - Prepares build directories and clears previous build artifacts """ super().__init__() - self._params = params + self._params: dict[str, str | int] = params self._task_id = task_id self._run_id = run_id self._work_dir = work_dir @@ -87,7 +98,7 @@ def __init__(self, params, task_id, run_id, work_dir, artifacts_dir, save_dir, d elif self._board in part_map: self._part = part_map[self._board] else: - raise Exception("No part specified for board %s" % self._board) + raise Exception(f"No part specified for board {self._board}") if self._board in alveo_part_map: self._params["shell_flow_type"] = build_cfg.ShellFlowType.VITIS_ALVEO @@ -125,23 +136,23 @@ def __init__(self, params, task_id, run_id, work_dir, artifacts_dir, save_dir, d ] if "experiments_config" in params: - self.experiments_config = params["experiments_config"] + self.experiments_config = Path(cast("str", params["experiments_config"])) else: # Set default experiment config if not explicitly defined as absolute or relative path # TODO: this assumes we are running from the repo root, where ci/ is available - if "live_fifo_sizing" in params and params["live_fifo_sizing"] is True: + if ("auto_fifo_depths" in params and params["auto_fifo_depths"] is True) and ( + "auto_fifo_strategy" in params and params["auto_fifo_strategy"] == "live_fifo" + ): # Default experiment config for FIFO-Sizing - self.experiments_config = os.path.join( - "ci", "experiments", "fifosizing_default.json" - ) + self.experiments_config = Path("ci") / "experiments" / "fifosizing_default.json" else: # Default experiment config for normal builds - self.experiments_config = os.path.join("ci", "experiments", "default.json") + self.experiments_config = Path("ci") / "experiments" / "default.json" - dut_yaml_name = self._params["dut"] + ".yml" - dut_path = os.path.join(os.path.dirname(__file__), "dut", dut_yaml_name) - if os.path.isfile(dut_path): - with open(dut_path) as f: + dut_yaml_name = cast("str", self._params["dut"]) + ".yml" + dut_path = Path(__file__).parent / "dut" / dut_yaml_name + if dut_path.is_file(): + with dut_path.open() as f: dut_cfg = yaml.load(f, Loader=yaml.SafeLoader) for key in dut_cfg: if key in custom_params: @@ -174,26 +185,27 @@ def __init__(self, params, task_id, run_id, work_dir, artifacts_dir, save_dir, d # SETUP # Use a temporary dir for buildflow-related files (next to FINN_BUILD_DIR) # Ensure it exists but is empty (clear potential artifacts from previous runs) - tmp_buildflow_dir = os.path.join(self._work_dir, "buildflow") - os.makedirs(tmp_buildflow_dir, exist_ok=True) + tmp_buildflow_dir = Path(self._work_dir) / "buildflow" + tmp_buildflow_dir.mkdir(exist_ok=True) delete_dir_contents(tmp_buildflow_dir) - self._build_inputs["build_dir"] = os.path.join( - tmp_buildflow_dir, "build_output" - ) # TODO remove in favor of self.build_dir - self._build_dir = os.path.join(tmp_buildflow_dir, "build_output") - self.report_dir = os.path.join(self._build_dir, "report") - os.makedirs(self.report_dir, exist_ok=True) + self._build_inputs["build_dir"] = tmp_buildflow_dir / "build_output" + # TODO remove in favor of self.build_dir + self._build_dir = tmp_buildflow_dir / "build_output" + self.report_dir = self._build_dir / "report" + self.report_dir.mkdir(exist_ok=True) # Save full build dir as local artifact self._local_artifacts_collection.append(("build_output", self._build_dir, False)) # Save reports and deployment package as pipeline artifacts self._artifacts_collection.append(("reports", self.report_dir, False)) self._artifacts_collection.append( - ("reports", os.path.join(self._build_dir, "build_dataflow.log"), False) + ("reports", self._build_dir / "build_dataflow.log", False) ) - self._artifacts_collection.append(("deploy", os.path.join(self._build_dir, "deploy"), True)) + self._artifacts_collection.append(("deploy", self._build_dir / "deploy", True)) - def _save_artifact(self, target_path, source_path, archive=False): + def _save_artifact( + self, target_path: str, source_path: str, archive: bool | None = False + ) -> None: """Save a single artifact from source to target location. Args: @@ -207,18 +219,18 @@ def _save_artifact(self, target_path, source_path, archive=False): - For files: copies to target directory - Automatically creates parent directories as needed """ - if os.path.isdir(source_path): + if Path(source_path).is_dir(): if archive: - os.makedirs(os.path.dirname(target_path), exist_ok=True) + Path(target_path).parent.mkdir(parents=True, exist_ok=True) shutil.make_archive(target_path, "zip", source_path) else: - os.makedirs(target_path, exist_ok=True) + Path(target_path).mkdir(parents=True, exist_ok=True) copytree(source_path, target_path, dirs_exist_ok=True) - elif os.path.isfile(source_path): - os.makedirs(target_path, exist_ok=True) + elif Path(source_path).is_file(): + Path(target_path).parent.mkdir(parents=True, exist_ok=True) shcopy(source_path, target_path) - def save_artifacts_collection(self): + def save_artifacts_collection(self) -> None: """Save all collected pipeline artifacts. This method should be called upon successful or failed completion of a run. @@ -230,12 +242,10 @@ def save_artifacts_collection(self): - Deployment packages """ for name, source_path, archive in self._artifacts_collection: - target_path = os.path.join( - self._artifacts_dir, "runs_output", "run_%d" % (self._run_id), name - ) - self._save_artifact(target_path, source_path, archive) + target_path = Path(self._artifacts_dir) / "runs_output" / f"run_{self._run_id}" / name + self._save_artifact(str(target_path), str(source_path), archive) - def save_local_artifacts_collection(self): + def save_local_artifacts_collection(self) -> None: """Save all collected local artifacts for debugging. This method should be called upon successful or failed completion of a run. @@ -247,10 +257,10 @@ def save_local_artifacts_collection(self): - FINN build directory contents (when debug=True) """ for name, source_path, archive in self._local_artifacts_collection: - target_path = os.path.join(self._save_dir, name, "run_%d" % (self._run_id)) - self._save_artifact(target_path, source_path, archive) + target_path = Path(self._save_dir) / name / f"run_{self._run_id}" + self._save_artifact(str(target_path), str(source_path), archive) - def _step_export_onnx(self): + def _step_export_onnx(self, onnx_export_path: str) -> None: """Export or generate ONNX model for benchmarking. This method must be implemented by subclasses to provide the ONNX model @@ -266,7 +276,7 @@ def _step_export_onnx(self): benchmark implementations. """ - def _step_build_setup(self): + def _step_build_setup(self) -> DataflowBuildConfig: """Initialize the DataflowBuildConfig for this benchmark. This method can be overridden by subclasses if the setup is too complex @@ -282,15 +292,16 @@ def _step_build_setup(self): The YAML file should be located at: benchmarking/dut/{dut_name}.yml where {dut_name} is the value of params["dut"]. """ - dut_yaml_name = self._params["dut"] + ".yml" - dut_path = os.path.join(os.path.dirname(__file__), "dut", dut_yaml_name) - if os.path.isfile(dut_path): - with open(dut_path) as f: - return DataflowBuildConfig.from_yaml(f) + dut_yaml_name = cast("str", self._params["dut"]) + ".yml" + dut_path = Path(__file__).parent / "dut" / dut_yaml_name + if dut_path.is_file(): + with dut_path.open() as f: + data = yaml.load(f, Loader=yaml.SafeLoader) + return DataflowBuildConfig.from_yaml(data) else: raise Exception("No DUT-specific YAML build definition found") - def run(self): + def run(self) -> None | Literal["skipped"]: """Execute the benchmark run. This method defaults to running the complete FINN build flow but may be @@ -302,7 +313,7 @@ def run(self): """ return self._steps_full_build_flow() - def _step_parse_builder_output(self, build_dir): + def _step_parse_builder_output(self, build_dir: str) -> None: """Parse and analyze the output from the FINN builder. Args: @@ -316,12 +327,14 @@ def _step_parse_builder_output(self, build_dir): TODO: Output results as .json or integrate as a new build step """ - if os.path.exists(os.path.join(build_dir, "verification_output")): + if (Path(build_dir) / "verification_output").is_dir(): # Collect all verification output filenames - outputs = glob.glob(os.path.join(build_dir, "verification_output/*.npy")) + outputs = glob.glob( + str(Path(build_dir) / "verification_output" / "*.npy") + ) # noqa: PTH207 # Extract the verification status for each verification output by matching # to the SUCCESS string contained in the filename - status = all([out.split("_")[-1].split(".")[0] == "SUCCESS" for out in outputs]) + status = all(out.split("_")[-1].split(".")[0] == "SUCCESS" for out in outputs) # Construct a dictionary reporting the verification status as string self.output_dict["builder_verification"] = { @@ -329,7 +342,7 @@ def _step_parse_builder_output(self, build_dir): } # TODO: mark job as failed if verification fails? - def _steps_full_build_flow(self): + def _steps_full_build_flow(self) -> None | Literal["skipped"]: """Execute the complete FINN dataflow build sequence. This method implements the default step sequence for benchmarking a full @@ -356,18 +369,18 @@ def _steps_full_build_flow(self): """ if "model_dir" in self._params: # input ONNX model and verification input/output pairs are provided - model_dir = self._params["model_dir"] - self._build_inputs["onnx_path"] = os.path.join(model_dir, "model.onnx") - self._build_inputs["input_npy_path"] = os.path.join(model_dir, "inp.npy") - self._build_inputs["output_npy_path"] = os.path.join(model_dir, "out.npy") + model_dir = Path(cast("str", self._params["model_dir"])) + self._build_inputs["onnx_path"] = model_dir / "model.onnx" + self._build_inputs["input_npy_path"] = model_dir / "inp.npy" + self._build_inputs["output_npy_path"] = model_dir / "out.npy" elif "model_path" in self._params: - self._build_inputs["onnx_path"] = self._params["model_path"] + self._build_inputs["onnx_path"] = Path(cast("str", self._params["model_path"])) else: # input ONNX model (+ optional I/O pair for verification) will be generated - self._build_inputs["onnx_path"] = os.path.join( - self._build_inputs["build_dir"], "model_export.onnx" + self._build_inputs["onnx_path"] = ( + Path(self._build_inputs["build_dir"]) / "model_export.onnx" ) - if self._step_export_onnx(self._build_inputs["onnx_path"]) == "skipped": + if self._step_export_onnx(str(self._build_inputs["onnx_path"])) == "skipped": # microbenchmarks might skip because no model can be generated for given params return "skipped" @@ -393,7 +406,7 @@ def _steps_full_build_flow(self): # cfg.default_swg_exception # cfg.large_fifo_mem_style - cfg.experiments_config_path = self.experiments_config + cfg.experiments_config_path = str(self.experiments_config) # Set verification i/o paths if available if "input_npy_path" in self._build_inputs and "output_npy_path" in self._build_inputs: @@ -432,7 +445,7 @@ def _steps_full_build_flow(self): setattr(cfg, param_key, param_value) # disable verification if live FIFO-sizing is on - if cfg.live_fifo_sizing: + if cfg.auto_fifo_depths and cfg.auto_fifo_strategy == AutoFIFOSizingMethod.LIVE_FIFO: cfg.verify_steps = None # Default of 1M cycles is insufficient for MetaFi (6M) and RN-50 (2.5M) @@ -444,3 +457,4 @@ def _steps_full_build_flow(self): # ANALYSIS self._step_parse_builder_output(self._build_inputs["build_dir"]) + return None diff --git a/src/finn/benchmarking/dut/bnn-pynq.yml b/src/finn/benchmarking/dut/bnn-pynq.yml index f03a7c73c7..bbbe6cfe23 100644 --- a/src/finn/benchmarking/dut/bnn-pynq.yml +++ b/src/finn/benchmarking/dut/bnn-pynq.yml @@ -18,15 +18,10 @@ steps: - step_apply_folding_config - step_minimize_bit_width - step_generate_estimate_reports - - step_set_fifo_depths - - step_hw_codegen - - step_hw_ipgen + - step_generate_hardware - step_create_stitched_ip - step_measure_rtlsim_performance - step_out_of_context_synthesis - step_synthesize_bitfile - step_make_driver - step_deployment_package - -# Workaround for auto FIFO-sizing from finn-examples -default_swg_exception: True diff --git a/src/finn/benchmarking/dut/gtsrb.yml b/src/finn/benchmarking/dut/gtsrb.yml index 8a89a8adf2..5b0946af0d 100644 --- a/src/finn/benchmarking/dut/gtsrb.yml +++ b/src/finn/benchmarking/dut/gtsrb.yml @@ -18,9 +18,7 @@ steps: - step_apply_folding_config - step_minimize_bit_width - step_generate_estimate_reports - - step_set_fifo_depths - - step_hw_codegen - - step_hw_ipgen + - step_generate_hardware - step_create_stitched_ip - step_measure_rtlsim_performance - step_out_of_context_synthesis diff --git a/src/finn/benchmarking/dut/kws.yml b/src/finn/benchmarking/dut/kws.yml index 9346c58beb..f816c97e30 100644 --- a/src/finn/benchmarking/dut/kws.yml +++ b/src/finn/benchmarking/dut/kws.yml @@ -17,9 +17,7 @@ steps: - step_apply_folding_config - step_minimize_bit_width - step_generate_estimate_reports - - step_set_fifo_depths - - step_hw_codegen - - step_hw_ipgen + - step_generate_hardware - step_create_stitched_ip - step_measure_rtlsim_performance - step_out_of_context_synthesis diff --git a/src/finn/benchmarking/dut/mobilenetv1.yml b/src/finn/benchmarking/dut/mobilenetv1.yml index 16a68f4143..603f4fddd9 100644 --- a/src/finn/benchmarking/dut/mobilenetv1.yml +++ b/src/finn/benchmarking/dut/mobilenetv1.yml @@ -11,13 +11,8 @@ steps: - step_apply_folding_config - step_minimize_bit_width - step_generate_estimate_reports - - step_set_fifo_depths - - step_hw_codegen - - step_hw_ipgen + - step_generate_hardware - step_create_stitched_ip - step_synthesize_bitfile - step_make_driver - step_deployment_package - -# folding config comes with FIFO sizes -auto_fifo_depths: False diff --git a/src/finn/benchmarking/dut/resnet18.yml b/src/finn/benchmarking/dut/resnet18.yml index fb8a6589fe..a5f3a98e0e 100644 --- a/src/finn/benchmarking/dut/resnet18.yml +++ b/src/finn/benchmarking/dut/resnet18.yml @@ -15,9 +15,7 @@ steps: - step_apply_folding_config - step_minimize_bit_width - step_generate_estimate_reports - - step_set_fifo_depths - - step_hw_codegen - - step_hw_ipgen + - step_generate_hardware - step_create_stitched_ip - step_synthesize_bitfile - step_make_driver diff --git a/src/finn/benchmarking/dut/resnet50.yml b/src/finn/benchmarking/dut/resnet50.yml index 26d81b2d7d..cd7e68dfcf 100644 --- a/src/finn/benchmarking/dut/resnet50.yml +++ b/src/finn/benchmarking/dut/resnet50.yml @@ -17,15 +17,10 @@ steps: - step_apply_folding_config - step_minimize_bit_width - step_generate_estimate_reports - - step_set_fifo_depths - - step_hw_codegen - - step_hw_ipgen + - step_generate_hardware - step_create_stitched_ip - step_measure_rtlsim_performance - step_out_of_context_synthesis - step_synthesize_bitfile - step_make_driver - step_deployment_package - -# folding config comes with FIFO sizes -auto_fifo_depths: False diff --git a/src/finn/benchmarking/dut/transformer.yml b/src/finn/benchmarking/dut/transformer.yml index 1232cb5778..0ce90ec57e 100644 --- a/src/finn/benchmarking/dut/transformer.yml +++ b/src/finn/benchmarking/dut/transformer.yml @@ -26,9 +26,7 @@ steps: # Default FINN backend step sequence - step_minimize_bit_width - step_generate_estimate_reports - - step_set_fifo_depths - - step_hw_codegen - - step_hw_ipgen + - step_generate_hardware - step_create_stitched_ip - step_measure_rtlsim_performance - step_out_of_context_synthesis @@ -63,7 +61,3 @@ standalone_thresholds: true max_multithreshold_bit_width: 16 # Maximum width of MVAU stream per PE mvau_wwidth_max: 2048 -# FIFO nodes with depth larger than 32768 will be split -split_large_fifos: true -# Disable automatic FIFO-sizing (rely on live FIFO-sizing for now) -auto_fifo_depths: false diff --git a/src/finn/benchmarking/dut/vgg10.yml b/src/finn/benchmarking/dut/vgg10.yml index 99a9ab333d..a45c73c2e9 100644 --- a/src/finn/benchmarking/dut/vgg10.yml +++ b/src/finn/benchmarking/dut/vgg10.yml @@ -14,9 +14,7 @@ steps: - step_apply_folding_config - step_minimize_bit_width - step_generate_estimate_reports - - step_set_fifo_depths - - step_hw_codegen - - step_hw_ipgen + - step_generate_hardware - step_create_stitched_ip - step_measure_rtlsim_performance - step_out_of_context_synthesis @@ -24,8 +22,4 @@ steps: - step_make_driver - step_deployment_package -# folding config doesn't come with FIFO sizes -auto_fifo_depths: True -auto_fifo_strategy: largefifo_rtlsim - standalone_thresholds: True diff --git a/src/finn/builder/build_dataflow.py b/src/finn/builder/build_dataflow.py index fbe670dd28..2fa5349520 100644 --- a/src/finn/builder/build_dataflow.py +++ b/src/finn/builder/build_dataflow.py @@ -331,13 +331,13 @@ def exit_buildflow( return exit_code -def create_model_wrapper(model_filename: str, cfg: DataflowBuildConfig) -> ModelWrapper: +def create_model_wrapper(model_filename: str | Path, cfg: DataflowBuildConfig) -> ModelWrapper: """Create a modelwrapper from the given config and filename. If a start-step is given, the ModelWrapper is constructed from a previous intermediate model. """ if cfg.start_step is None: print(f"Building dataflow accelerator from {model_filename}") - return ModelWrapper(model_filename) + return ModelWrapper(str(model_filename)) if model_filename != "": log.warning( "When using a start-step, FINN automatically searches " @@ -354,7 +354,7 @@ def create_model_wrapper(model_filename: str, cfg: DataflowBuildConfig) -> Model return ModelWrapper(str(intermediate_model_filename)) -def build_dataflow_cfg(model_filename: str, cfg: DataflowBuildConfig) -> int: +def build_dataflow_cfg(model_filename: str | Path, cfg: DataflowBuildConfig) -> int: """Build a dataflow accelerator using the given configuration. Main entry point for building FINN dataflow accelerators. Handles step execution, diff --git a/src/finn/core/__init__.py b/src/finn/core/__init__.py index e69de29bb2..f5a21d4da2 100644 --- a/src/finn/core/__init__.py +++ b/src/finn/core/__init__.py @@ -0,0 +1 @@ +"""Core utilities for FINN+.""" diff --git a/src/finn/interface/__init__.py b/src/finn/interface/__init__.py index 5a6fd6228d..817a22466a 100644 --- a/src/finn/interface/__init__.py +++ b/src/finn/interface/__init__.py @@ -1,3 +1,4 @@ +"""FINN+ CLI interface package.""" import os IS_POSIX = os.name == "posix" diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index 15c894adbd..ca9a065f7d 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -118,6 +118,8 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: posix=IS_POSIX, ) ) + test_1_process.communicate() + test_1_returncode = test_1_process.returncode # test_2_process = subprocess.Popen( # shlex.split( # ( @@ -129,8 +131,6 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: # posix=IS_POSIX, # ) # ) - test_1_process.communicate() - test_1_returncode = test_1_process.returncode # test_2_process.communicate() # test_2_returncode = test_2_process.returncode diff --git a/src/finn/transformation/qonnx/qonnx_activation_handlers.py b/src/finn/transformation/qonnx/qonnx_activation_handlers.py index 90d0563e77..881f369781 100644 --- a/src/finn/transformation/qonnx/qonnx_activation_handlers.py +++ b/src/finn/transformation/qonnx/qonnx_activation_handlers.py @@ -182,7 +182,7 @@ def replace_quant_node(self): # which is the other way around in Brevitas, # we thus need to adjust the bias in the MultiThreshold node finn_bias = adder_bias[0].item() * mul_scale[0].item() - mt_inst.set_nodeattr("out_bias", finn_bias) + mt_inst.set_nodeattr("out_bias", float(finn_bias)) # Set the output data type mt_inst.set_nodeattr("out_dtype", out_dtype) diff --git a/src/finn/transformation/streamline/absorb.py b/src/finn/transformation/streamline/absorb.py index 2556ce96d1..6f6903df55 100644 --- a/src/finn/transformation/streamline/absorb.py +++ b/src/finn/transformation/streamline/absorb.py @@ -163,7 +163,7 @@ def apply(self, model: ModelWrapper): # Set new bias and datatype attributes into the threshold # operator - threshold_op.set_nodeattr("out_bias", out_bias) + threshold_op.set_nodeattr("out_bias", float(out_bias)) threshold_op.set_nodeattr("out_dtype", odt.name) # Remove the bias operator and rewire the graph to skip the # now-missing node diff --git a/tests/fpgadataflow/test_fpgadataflow_finnloop.py b/tests/fpgadataflow/test_fpgadataflow_finnloop.py index 36839507bb..0e50b671cf 100644 --- a/tests/fpgadataflow/test_fpgadataflow_finnloop.py +++ b/tests/fpgadataflow/test_fpgadataflow_finnloop.py @@ -507,7 +507,6 @@ def test_finnloop_end2end_mlo( target_fps=1000, synth_clk_period_ns=10.0, board="V80", - rtlsim_batch_size=100, standalone_thresholds=True, mlo=True, loop_body_hierarchy=[["", "layers.0"]], diff --git a/tests/fpgadataflow/test_fpgadataflow_relu_elementwisemax.py b/tests/fpgadataflow/test_fpgadataflow_relu_elementwisemax.py index 84f9e2674d..f1b506b0c6 100644 --- a/tests/fpgadataflow/test_fpgadataflow_relu_elementwisemax.py +++ b/tests/fpgadataflow/test_fpgadataflow_relu_elementwisemax.py @@ -38,6 +38,7 @@ from qonnx.transformation.infer_datatypes import InferDataTypes from qonnx.transformation.infer_shapes import InferShapes from qonnx.util.basic import gen_finn_dt_tensor, qonnx_make_model +from typing import Literal from finn.core.onnx_exec import execute_onnx from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim @@ -53,7 +54,7 @@ # Creates a model executing a ReLU operation -def create_relu_model_onnx(inp_dtype, inp_shape): +def create_relu_model_onnx(inp_dtype: str, inp_shape: list[int]) -> ModelWrapper: # Create a node representing the binary elementwise operation node = oh.make_node( op_type="Relu", @@ -88,7 +89,12 @@ def create_relu_model_onnx(inp_dtype, inp_shape): @pytest.mark.fpgadataflow @pytest.mark.slow @pytest.mark.vivado -def test_relu_elementwisemax(inp_dtype, inp_shape, pe, exec_mode): +def test_relu_elementwisemax( + inp_dtype: Literal["INT8", "FLOAT32", "FLOAT16", "FIXED<8,3>"], + inp_shape: list[int], + pe: Literal[1, 2, 4], + exec_mode: Literal["cppsim", "rtlsim"], +) -> None: # Make dummy model for testing model = create_relu_model_onnx(inp_dtype, inp_shape) # Prepare the execution context @@ -109,7 +115,7 @@ def test_relu_elementwisemax(inp_dtype, inp_shape, pe, exec_mode): o_hw = execute_onnx(model, context)["out"] # Compare the expected to the produced for exact equality - assert np.all(o_hw == o_ref) + assert np.all(np.isclose(o_hw, o_ref)) # Test running shape and data type inference on the model graph model = model.transform(InferDataTypes()) @@ -141,4 +147,4 @@ def test_relu_elementwisemax(inp_dtype, inp_shape, pe, exec_mode): o_sim = execute_onnx(model, context)["out"] # Compare the expected to the produced for exact equality - assert np.all(o_sim == o_ref) + assert np.all(np.isclose(o_sim, o_ref)) diff --git a/tests/fpgadataflow/test_fpgadataflow_shuffle.py b/tests/fpgadataflow/test_fpgadataflow_shuffle.py index 4fe06cd825..b7650fc164 100644 --- a/tests/fpgadataflow/test_fpgadataflow_shuffle.py +++ b/tests/fpgadataflow/test_fpgadataflow_shuffle.py @@ -16,6 +16,7 @@ import torch import torch.onnx from brevitas.export import export_qonnx +from pathlib import Path from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp @@ -610,7 +611,7 @@ def test_shuffle_config_consolidation(): assert len(decomposed_nodes) > 0 consolidated_file = os.environ["FINN_BUILD_DIR"] + "/consolidated.json" - extract_model_config_consolidate_shuffles(model, consolidated_file, ["SIMD"]) + extract_model_config_consolidate_shuffles(model, Path(consolidated_file), ["SIMD"]) with open(consolidated_file, "r") as f: consolidated_config = json.load(f) diff --git a/tests/fpgadataflow/test_fpgadataflow_thresholding.py b/tests/fpgadataflow/test_fpgadataflow_thresholding.py index 89d11977dc..a6663554c4 100644 --- a/tests/fpgadataflow/test_fpgadataflow_thresholding.py +++ b/tests/fpgadataflow/test_fpgadataflow_thresholding.py @@ -63,6 +63,8 @@ def InsertAndSetFIFODepths(model: ModelWrapper, fpga_part: str, clk_ns: float) -> ModelWrapper: cfg = DataflowBuildConfig() + cfg.fpga_part = fpga_part + cfg.synth_clk_period_ns = clk_ns model = model.transform( BuildSimulation( fpga_part, diff --git a/tests/fpgadataflow/test_simulation_build.py b/tests/fpgadataflow/test_simulation_build.py index fddfdbe8a5..18674ed468 100644 --- a/tests/fpgadataflow/test_simulation_build.py +++ b/tests/fpgadataflow/test_simulation_build.py @@ -8,16 +8,23 @@ from onnx import GraphProto, NodeProto, TensorProto, ValueInfoProto, helper from qonnx.core.modelwrapper import ModelWrapper from qonnx.util.basic import qonnx_make_model -from typing import Protocol, cast +from typing import Protocol, TypedDict from finn.transformation.fpgadataflow.simulation_build import SimulationBuilder from finn.util.exception import FINNInternalError class _SimulationBuilderProtocol(Protocol): - def __init__(self, model: ModelWrapper, fpgapart: str, clk_ns: float) -> None: + def __init__( + self, + model: ModelWrapper, + fpgapart: str, + clk_ns: float, + shm_prefix: str, + performance_sim: bool = False, + ) -> None: ... - def _isolated_node_model(self, by_node: int | str) -> ModelWrapper: + def _isolated_node_model(self, by_node: int | str | NodeProto) -> ModelWrapper: ... @@ -559,6 +566,61 @@ def _build_mvau_target_model(mlo: bool = False) -> ModelWrapper: return model +class _DuplicateStreamConfig(TypedDict): + fifos: bool + branch_nodes: bool + fifo_pre: bool + fifo_between: bool + fifo_after: bool + fifo_between_depth: int | None + expected_input_node: bool + expected_output_node: bool + + +_DUPLICATE_STREAM_CONFIGS: list[_DuplicateStreamConfig] = [ + { + "fifos": False, + "branch_nodes": False, + "fifo_pre": False, + "fifo_between": False, + "fifo_after": False, + "fifo_between_depth": None, + "expected_input_node": True, + "expected_output_node": True, + }, + { + "fifos": False, + "branch_nodes": False, + "fifo_pre": True, + "fifo_between": True, + "fifo_after": False, + "fifo_between_depth": 2, + "expected_input_node": True, + "expected_output_node": True, + }, + { + "fifos": False, + "branch_nodes": True, + "fifo_pre": False, + "fifo_between": False, + "fifo_after": False, + "fifo_between_depth": None, + "expected_input_node": True, + "expected_output_node": False, + }, + { + "fifos": False, + "branch_nodes": True, + "fifo_pre": True, + "fifo_between": True, + "fifo_after": True, + "fifo_between_depth": 2, + "expected_input_node": True, + "expected_output_node": False, + }, +] + + def _assert_isolated_model( isolated_model: ModelWrapper, source_model: ModelWrapper | None, @@ -683,7 +745,7 @@ def test_isolated_node_model_unary_target_with_varied_other_node_inputs( ) -> None: """Isolate unary target with unary/binary surrounding nodes.""" model = _build_unary_target_model(pre_binary=pre_binary, succ_binary=succ_binary) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_1_") isolated = _isolate_node_model(builder, 1) @@ -703,7 +765,7 @@ def test_isolated_node_model_unary_target_with_varied_other_node_inputs( def test_isolated_node_model_select_by_name() -> None: """Selecting node by name returns the correct isolated model.""" model = _build_unary_target_model(pre_binary=False, succ_binary=False) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_2_") isolated = _isolate_node_model(builder, "target_dwc") @@ -739,7 +801,7 @@ def test_isolated_node_model_binary_target_with_dynamic_and_fixed_inputs( ) -> None: """Isolate binary target for dynamic/fixed lhs-rhs and MLO/non-MLO cases.""" model = _build_binary_target_model(initializer_side=initializer_side, mlo=mlo) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_3_") isolated = _isolate_node_model(builder, 0) @@ -768,7 +830,7 @@ def test_isolated_node_model_unary_succ_fifo_chain_transparency( fifo_between=True, fifo_between_depth=fifo_between_depth, ) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_4_") isolated = _isolate_node_model(builder, "succ_dwc") @@ -798,7 +860,7 @@ def test_isolated_node_model_binary_succ_fifo_chain_transparency( fifo_between=True, fifo_between_depth=fifo_between_depth, ) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_5_") isolated = _isolate_node_model(builder, "succ_dwc") @@ -838,7 +900,7 @@ def test_isolated_node_model_binary_target_fifo_pre_transparency( fifo_between=True, fifo_between_depth=2, ) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_6_") model.save("/scratch/pc2-mitarbeiter/linusjun/finn-tmp/source_model.onnx") @@ -873,64 +935,20 @@ def test_isolated_node_model_binary_target_fifo_pre_transparency( ) -@pytest.mark.parametrize( - "config", - [ - { - "fifos": False, - "branch_nodes": False, - "fifo_pre": False, - "fifo_between": False, - "fifo_after": False, - "fifo_between_depth": None, - "expected_input_node": True, - "expected_output_node": True, - }, - { - "fifos": False, - "branch_nodes": False, - "fifo_pre": True, - "fifo_between": True, - "fifo_after": False, - "fifo_between_depth": 2, - "expected_input_node": True, - "expected_output_node": True, - }, - { - "fifos": False, - "branch_nodes": True, - "fifo_pre": False, - "fifo_between": False, - "fifo_after": False, - "fifo_between_depth": None, - "expected_input_node": True, - "expected_output_node": False, - }, - { - "fifos": False, - "branch_nodes": True, - "fifo_pre": True, - "fifo_between": True, - "fifo_after": True, - "fifo_between_depth": 2, - "expected_input_node": True, - "expected_output_node": False, - }, - ], -) +@pytest.mark.parametrize("config", _DUPLICATE_STREAM_CONFIGS) def test_isolated_node_model_duplicate_stream_fifo_transparency( - config: dict[str, object], + config: _DuplicateStreamConfig, ) -> None: """DuplicateStreams models behave identically with FIFO chains present.""" model = _build_duplicate_target_model( - fifos=bool(config["fifos"]), - branch_nodes=bool(config["branch_nodes"]), - fifo_pre=bool(config["fifo_pre"]), - fifo_between=bool(config["fifo_between"]), - fifo_after=bool(config["fifo_after"]), - fifo_between_depth=cast("int | None", config["fifo_between_depth"]), + fifos=config["fifos"], + branch_nodes=config["branch_nodes"], + fifo_pre=config["fifo_pre"], + fifo_between=config["fifo_between"], + fifo_after=config["fifo_after"], + fifo_between_depth=config["fifo_between_depth"], ) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_7_") isolated = _isolate_node_model(builder, "dup_stream") @@ -941,10 +959,10 @@ def test_isolated_node_model_duplicate_stream_fifo_transparency( expected_graph_inputs=["dup_in"], expected_graph_outputs=["dup_out0", "dup_out1"], expected_initializer_inputs=[], - expected_input_node_flag=bool(config["expected_input_node"]), + expected_input_node_flag=config["expected_input_node"], expected_target_inputs=["dup_in_dummy"], expected_target_outputs=["dup_out0_dummy", "dup_out1_dummy"], - expected_output_node_flag=bool(config["expected_output_node"]), + expected_output_node_flag=config["expected_output_node"], ) @@ -970,8 +988,8 @@ def test_isolated_node_model_fifo_transparency_nodes() -> None: fifo_after=True, ) - builder_no_fifo = SimulationBuilder(model_no_fifo, "xc7z020clg400-1", 5.0) - builder_fifo = SimulationBuilder(model_fifo, "xc7z020clg400-1", 5.0) + builder_no_fifo = SimulationBuilder(model_no_fifo, "xc7z020clg400-1", 5.0, "test_isolated_8_") + builder_fifo = SimulationBuilder(model_fifo, "xc7z020clg400-1", 5.0, "test_isolated_9_") node_names = [node.name for node in model_no_fifo.graph.node if node.op_type != "StreamingFIFO"] @@ -990,7 +1008,7 @@ def test_isolated_node_model_fifo_transparency_nodes() -> None: def test_isolated_node_model_elementwise_sets_const_style_for_mlo_initializer() -> None: """Elementwise ops set lhs_style/rhs_style=const for remapped MLO initializer inputs.""" model = _build_binary_target_model(initializer_side=None, mlo=True) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_10_") isolated = _isolate_node_model(builder, 0) target_node = next(n for n in isolated.graph.node if n.name == "target_add") @@ -1009,7 +1027,7 @@ def test_isolated_node_model_elementwise_sets_const_style_for_mlo_initializer() def test_isolated_node_model_mvau_sets_internal_decoupled_for_initializer_input() -> None: """MVAU ops set mem_mode=internal_decoupled when an input is remapped to initializer.""" model = _build_mvau_target_model(mlo=True) - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_11_") isolated = _isolate_node_model(builder, 0) target_node = next(n for n in isolated.graph.node if n.name == "target_mvau") @@ -1026,7 +1044,7 @@ def test_isolated_node_model_rejects_bad_mlo_metadata() -> None: model = _build_binary_target_model(initializer_side="rhs", mlo=False) model.set_metadata_prop("is_mlo", "1") model.set_metadata_prop("mlo_input_parameter_names", "42") - builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0) + builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_12_") with pytest.raises(FINNInternalError, match="mlo_input_parameter_names"): _isolate_node_model(builder, 0) diff --git a/tests/fpgadataflow/test_split_large_fifos.py b/tests/fpgadataflow/test_split_large_fifos.py index 201f8e5244..1000a5a117 100644 --- a/tests/fpgadataflow/test_split_large_fifos.py +++ b/tests/fpgadataflow/test_split_large_fifos.py @@ -77,7 +77,6 @@ def test_split_large_fifos(depth): target_fps=10000, synth_clk_period_ns=10.0, board="Pynq-Z1", - rtlsim_batch_size=100, shell_flow_type=build_cfg.ShellFlowType.VIVADO_ZYNQ, generate_outputs=[ build_cfg.DataflowOutputType.ESTIMATE_REPORTS, From de2be56e12ac9ab9d18ae3a00c5326a024f6b136 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Sun, 31 May 2026 13:26:31 +0200 Subject: [PATCH 139/170] Fix remaining tests --- .../fpgadataflow/simulation_build.py | 2 + .../test_fpgadataflow_finnloop.py | 4 +- tests/fpgadataflow/test_fpgadataflow_vvau.py | 1 + tests/fpgadataflow/test_split_large_fifos.py | 141 ++++++++++-------- 4 files changed, 83 insertions(+), 65 deletions(-) diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index eeae07940b..607baac6b1 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -489,6 +489,8 @@ def _get_first_non_fifo_output(node: NodeProto, outp: str) -> str: params_changed = True if "runtime_writeable_weights" in params and params["runtime_writeable_weights"] == 1: params["runtime_writeable_weights"] = 0 + if "ram_style" in params and params["ram_style"] == "ultra": + params["ram_style"] = "block" params_changed = True if "dynamic_mode" in params and params["dynamic_mode"] == 1: params["dynamic_mode"] = 0 diff --git a/tests/fpgadataflow/test_fpgadataflow_finnloop.py b/tests/fpgadataflow/test_fpgadataflow_finnloop.py index 0e50b671cf..c804475410 100644 --- a/tests/fpgadataflow/test_fpgadataflow_finnloop.py +++ b/tests/fpgadataflow/test_fpgadataflow_finnloop.py @@ -495,9 +495,7 @@ def test_finnloop_end2end_mlo( "step_apply_folding_config", "step_minimize_bit_width", "step_generate_estimate_reports", - "step_hw_codegen", - "step_hw_ipgen", - "step_set_fifo_depths", + "step_generate_hardware", "step_create_stitched_ip", ] diff --git a/tests/fpgadataflow/test_fpgadataflow_vvau.py b/tests/fpgadataflow/test_fpgadataflow_vvau.py index cc30e66910..39f53df926 100644 --- a/tests/fpgadataflow/test_fpgadataflow_vvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_vvau.py @@ -542,6 +542,7 @@ def test_fpgadataflow_vvau_rtl( partitioned_model = partitioned_model.transform(PrepareIP(part, 5)) partitioned_model = partitioned_model.transform(HLSSynthIP()) partitioned_model = partitioned_model.transform(CreateStitchedIP(part, 5)) + partitioned_model.set_metadata_prop("exec_mode", "rtlsim") # transpose input since we're now simulating HW layers (NCHW --> NHWC) input_dict["global_in"] = np.transpose(input_dict["global_in"], (0, 2, 3, 1)) output_vvau_stitched = oxe.execute_onnx( diff --git a/tests/fpgadataflow/test_split_large_fifos.py b/tests/fpgadataflow/test_split_large_fifos.py index 1000a5a117..45c274209f 100644 --- a/tests/fpgadataflow/test_split_large_fifos.py +++ b/tests/fpgadataflow/test_split_large_fifos.py @@ -26,86 +26,103 @@ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +"""Tests for splitting large FIFOs in dataflow graphs.""" import pytest -import json -import shutil -import torch -from brevitas.export import export_qonnx +import numpy as np +from onnx import NodeProto, TensorProto +from onnx import helper as oh +from qonnx.core.datatype import DataType from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp +from qonnx.util.basic import qonnx_make_model +from typing import Literal, cast -import finn.builder.build_dataflow as build -import finn.builder.build_dataflow_config as build_cfg -from finn.transformation.fpgadataflow.set_fifo_depths import get_fifo_split_configs -from finn.util.basic import make_build_dir -from tests.testing_util.test import get_trained_network_and_ishape +from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO +from finn.transformation.fpgadataflow.set_fifo_depths import SplitLargeFIFOs, get_fifo_split_configs +from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers -def fetch_test_model(topology, wbits=2, abits=2): - tmp_output_dir = make_build_dir("build_fifosizing_%s_" % topology) - (model, ishape) = get_trained_network_and_ishape(topology, wbits, abits) - chkpt_name = tmp_output_dir + "/model.onnx" - export_qonnx(model, torch.randn(ishape), chkpt_name) - return tmp_output_dir +def _make_elementwise_add( + name: str, + lhs: str, + rhs: str, + out: str, + shape: list[int], + dtype: str = "INT8", + lhs_style: str = "input", + rhs_style: str = "input", +) -> NodeProto: + return oh.make_node( + "ElementwiseAdd", + [lhs, rhs], + [out], + domain="finn.custom_op.fpgadataflow", + backend="fpgadataflow", + lhs_dtype=dtype, + rhs_dtype=dtype, + out_dtype=dtype, + lhs_shape=list(shape), + rhs_shape=list(shape), + out_shape=list(shape), + lhs_style=lhs_style, + rhs_style=rhs_style, + PE=1, + name=name, + ) -def get_folding_cfg(depth=65536): - cfg = dict() - cfg["Defaults"] = dict() - for i in range(9): - key = "StreamingFIFO_rtl_" + str(i) - cfg[key] = {"depth": depth, "ram_style": "auto", "impl_style": "vivado"} - return cfg +def _build_elementwise_add_model() -> ModelWrapper: + shape = [1, 4] + inp0 = oh.make_tensor_value_info("inp0", TensorProto.INT8, shape) + rhs0 = oh.make_tensor_value_info("rhs0", TensorProto.INT8, shape) + rhs1 = oh.make_tensor_value_info("rhs1", TensorProto.INT8, shape) + mid = oh.make_tensor_value_info("mid", TensorProto.INT8, shape) + out = oh.make_tensor_value_info("out", TensorProto.INT8, shape) + nodes = [ + _make_elementwise_add("add_0", "inp0", "rhs0", "mid", shape, rhs_style="const"), + _make_elementwise_add("add_1", "mid", "rhs1", "out", shape, rhs_style="const"), + ] + graph = oh.make_graph( + nodes=nodes, + inputs=[inp0], + outputs=[out], + value_info=[mid, rhs0, rhs1], + name="two_elementwise_adds", + ) + model = ModelWrapper(qonnx_make_model(graph, producer_name="test_split_large_fifos")) + model.set_initializer("rhs0", np.ones(shape, dtype=np.int8)) + model.set_initializer("rhs1", np.ones(shape, dtype=np.int8)) + for tensor_name in ["inp0", "rhs0", "rhs1", "mid", "out"]: + model.set_tensor_datatype(tensor_name, DataType["INT8"]) + return model @pytest.mark.slow @pytest.mark.vivado @pytest.mark.fpgadataflow @pytest.mark.parametrize("depth", [16384, 65536, 45000, 1537]) -def test_split_large_fifos(depth): - tmp_output_dir = fetch_test_model("tfc") - folding_cfg = get_folding_cfg(depth) - with open(tmp_output_dir + "/folding_config.json", "w") as f: - json.dump(folding_cfg, f, indent=2) - cfg = build_cfg.DataflowBuildConfig( - output_dir=tmp_output_dir, - auto_fifo_depths=False, - split_large_fifos=True, - folding_config_file=tmp_output_dir + "/folding_config.json", - target_fps=10000, - synth_clk_period_ns=10.0, - board="Pynq-Z1", - shell_flow_type=build_cfg.ShellFlowType.VIVADO_ZYNQ, - generate_outputs=[ - build_cfg.DataflowOutputType.ESTIMATE_REPORTS, - build_cfg.DataflowOutputType.STITCHED_IP, - build_cfg.DataflowOutputType.RTLSIM_PERFORMANCE, - ], - ) - build.build_dataflow_cfg(tmp_output_dir + "/model.onnx", cfg) - with open(tmp_output_dir + "/report/estimate_network_performance.json") as f: - est_data = json.load(f) - with open(tmp_output_dir + "/report/rtlsim_performance.json") as f: - sim_data = json.load(f) - assert ( - float(sim_data["throughput[images/s]"]) / float(est_data["estimated_throughput_fps"]) > 0.9 - ) - model = ModelWrapper(tmp_output_dir + "/intermediate_models/step_set_fifo_depths.onnx") - # exclude final FIFO node (output FIFO, not part of test) - fifo_nodes = model.get_nodes_by_op_type("StreamingFIFO_rtl")[:-1] - golden_cfg = get_fifo_split_configs(depth, 256, 32768) - for i, fifo_node in enumerate(fifo_nodes): - inst = getCustomOp(fifo_node) - fifo_depth = inst.get_nodeattr("depth") - assert fifo_depth == golden_cfg[i % len(golden_cfg)][0] - assert fifo_depth > 1 - - shutil.rmtree(tmp_output_dir) +def test_split_large_fifos(depth: Literal[16384, 65536, 45000, 1537]) -> None: + """Split oversized FIFOs into supported power-of-two depths.""" + model = _build_elementwise_add_model() + model = model.transform(SpecializeLayers("xcvm1802-vsvd1760-2MP-e-S")) + for node in model.graph.node: + n = getCustomOp(node) + n.set_nodeattr("inFIFODepths", [depth]) + n.set_nodeattr("outFIFODepths", [depth]) + model = model.transform(InsertFIFO(True, 256, "auto")) + model = model.transform(SplitLargeFIFOs(256, 32768)) + for node in model.get_nodes_by_op_type("StreamingFIFO_rtl"): + n = getCustomOp(node) + # Each FIFO needs to be a power of 2 in depth + assert ( + cast("int", n.get_nodeattr("depth")) & (cast("int", n.get_nodeattr("depth")) - 1) == 0 + ), f"FIFO depth {n.get_nodeattr('depth')} is not a power of 2" -def test_split_large_fifo_configs(): +def test_split_large_fifo_configs() -> None: + """Validate FIFO split configurations for fixed depth inputs.""" ret0 = get_fifo_split_configs(513, 256, 32768) assert ret0 == [(512, "vivado"), (2, "rtl")] ret1 = get_fifo_split_configs(1200, 256, 32768) From 7464ab0da7cb7965a556df806c24acaa2e059e9e Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Sun, 31 May 2026 22:08:47 +0200 Subject: [PATCH 140/170] Fix finn loop test --- src/finn/custom_op/fpgadataflow/rtl/finn_loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py index 088cf920c6..ddd454391f 100644 --- a/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py +++ b/src/finn/custom_op/fpgadataflow/rtl/finn_loop.py @@ -314,7 +314,7 @@ def prepare_rtlsim(self, behav=False): all_verilog_srcs = f.read().split() top_module_file_name = os.path.basename(os.path.realpath(self.get_nodeattr("ipgen_path"))) top_module_name = top_module_file_name.strip(".v") - single_src_dir = make_build_dir("rtlsim_" + top_module_name + "_") + single_src_dir = Path(make_build_dir("rtlsim_" + top_module_name + "_")) trace_file = self.get_nodeattr("rtlsim_trace") debug = not (trace_file is None or trace_file == "") rtlsim_so = finnxsi.compile_sim_obj( @@ -322,7 +322,7 @@ def prepare_rtlsim(self, behav=False): ) # save generated lib filename in attribute sim_base, sim_rel = rtlsim_so - self.set_nodeattr("rtlsim_so", sim_base + "/" + sim_rel) + self.set_nodeattr("rtlsim_so", str(sim_base) + "/" + str(sim_rel)) def execute_node(self, context, graph): """Execute node.""" From 5b667dce96c897164fc630443e1599ccceb254ce Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:14:25 +0200 Subject: [PATCH 141/170] Add timeout to launch process helper to combat sometimes stuck xelab and xvlog processes --- finn_xsi/finn_xsi/adapter.py | 10 +++++----- src/finn/util/basic.py | 11 +++++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index c5425ee1ba..79fb05a40b 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -18,7 +18,7 @@ from pathlib import Path from typing import Literal -from finn.util.basic import launch_process_helper +from finn.util.basic import launch_process_helper, wait_for_file from finn.util.exception import FINNInternalError, FINNUserError @@ -118,12 +118,12 @@ def compile_sim_obj( cmd_xvlog = ["xvlog", "--incr", "--relax", "-prj", "rtlsim.prj"] - launch_process_helper(cmd_xvlog, cwd=sim_out_dir, print_stdout=False) - launch_process_helper(cmd_xelab, cwd=sim_out_dir, print_stdout=False) + launch_process_helper(cmd_xvlog, cwd=sim_out_dir, print_stdout=False, timeout=600) + launch_process_helper(cmd_xelab, cwd=sim_out_dir, print_stdout=False, timeout=600) out_so_relative_path = Path(f"xsim.dir/{top_module_name}/xsimk.so") out_so_full_path = sim_out_dir / out_so_relative_path - if not out_so_full_path.is_file(): + if not wait_for_file(out_so_full_path): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), out_so_full_path) return (sim_out_dir, out_so_relative_path) @@ -159,7 +159,7 @@ def load_sim_obj( if simkernel_so is None: simkernel_so = get_simkernel_so() oldcwd = Path.cwd() - if not sim_out_dir.is_dir() or not (sim_out_dir / out_so_relative_path).is_file(): + if not sim_out_dir.is_dir() or not wait_for_file(sim_out_dir / out_so_relative_path): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), sim_out_dir) os.chdir(sim_out_dir) sim = SimEngine(simkernel_so, str(out_so_relative_path), "finnxsi_rtlsim.log", tracefile) diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 9cbe783b21..59304cf8b1 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -256,12 +256,19 @@ def launch_process_helper( cwd: str | Path | None = None, print_stdout: bool = True, print_stderr: bool = True, + timeout: float | None = None, ) -> tuple[str, str]: """Launch a helper process in a way that facilitates logging stdout/stderr with Python loggers. - Returns (cmd_out, cmd_err) if successful, raises CalledProcessError otherwise.""" + Returns (cmd_out, cmd_err) if successful, raises CalledProcessError otherwise. + If timeout is set, subprocess.run may raise TimeoutExpired.""" process = subprocess.run( - [str(arg) for arg in args], capture_output=True, env=proc_env, cwd=cwd, text=True + [str(arg) for arg in args], + capture_output=True, + env=proc_env, + cwd=cwd, + text=True, + timeout=timeout, ) cmd_out = process.stdout.strip() cmd_err = process.stderr.strip() From d20160e9b2339119b155b91a00be86d677227a7a Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:46:24 +0200 Subject: [PATCH 142/170] Fix mkdir error --- src/finn/benchmarking/bench_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/finn/benchmarking/bench_base.py b/src/finn/benchmarking/bench_base.py index 76ed8a27d0..723f821049 100644 --- a/src/finn/benchmarking/bench_base.py +++ b/src/finn/benchmarking/bench_base.py @@ -186,13 +186,13 @@ def __init__( # Use a temporary dir for buildflow-related files (next to FINN_BUILD_DIR) # Ensure it exists but is empty (clear potential artifacts from previous runs) tmp_buildflow_dir = Path(self._work_dir) / "buildflow" - tmp_buildflow_dir.mkdir(exist_ok=True) + tmp_buildflow_dir.mkdir(exist_ok=True, parents=True) delete_dir_contents(tmp_buildflow_dir) self._build_inputs["build_dir"] = tmp_buildflow_dir / "build_output" # TODO remove in favor of self.build_dir self._build_dir = tmp_buildflow_dir / "build_output" self.report_dir = self._build_dir / "report" - self.report_dir.mkdir(exist_ok=True) + self.report_dir.mkdir(exist_ok=True, parents=True) # Save full build dir as local artifact self._local_artifacts_collection.append(("build_output", self._build_dir, False)) @@ -329,9 +329,9 @@ def _step_parse_builder_output(self, build_dir: str) -> None: """ if (Path(build_dir) / "verification_output").is_dir(): # Collect all verification output filenames - outputs = glob.glob( + outputs = glob.glob( # noqa: PTH207 str(Path(build_dir) / "verification_output" / "*.npy") - ) # noqa: PTH207 + ) # Extract the verification status for each verification output by matching # to the SUCCESS string contained in the filename status = all(out.split("_")[-1].split(".")[0] == "SUCCESS" for out in outputs) From f80fa437a0ce54ebd762896427a2df37dfc54448 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:24:55 +0200 Subject: [PATCH 143/170] Make sure that no child processes are left over after xelab/xvlog exists --- finn_xsi/finn_xsi/adapter.py | 20 ++++++++-- src/finn/util/basic.py | 75 ++++++++++++++++++++++++++++-------- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 79fb05a40b..eeb885a1c3 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -118,12 +118,26 @@ def compile_sim_obj( cmd_xvlog = ["xvlog", "--incr", "--relax", "-prj", "rtlsim.prj"] - launch_process_helper(cmd_xvlog, cwd=sim_out_dir, print_stdout=False, timeout=600) - launch_process_helper(cmd_xelab, cwd=sim_out_dir, print_stdout=False, timeout=600) + launch_process_helper( + cmd_xvlog, + cwd=sim_out_dir, + print_stdout=False, + timeout=240, + start_new_session=True, + kill_process_group=True, + ) + launch_process_helper( + cmd_xelab, + cwd=sim_out_dir, + print_stdout=False, + timeout=300, + start_new_session=True, + kill_process_group=True, + ) out_so_relative_path = Path(f"xsim.dir/{top_module_name}/xsimk.so") out_so_full_path = sim_out_dir / out_so_relative_path - if not wait_for_file(out_so_full_path): + if not wait_for_file(out_so_full_path, timeout=5): raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), out_so_full_path) return (sim_out_dir, out_so_relative_path) diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 59304cf8b1..e48e40415b 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -41,7 +41,9 @@ basic system operations, hardware abstraction, and build tool integration. """ +import contextlib import os +import signal import stat as statmod import subprocess import tempfile @@ -257,21 +259,61 @@ def launch_process_helper( print_stdout: bool = True, print_stderr: bool = True, timeout: float | None = None, + start_new_session: bool = False, + kill_process_group: bool = False, ) -> tuple[str, str]: """Launch a helper process in a way that facilitates logging stdout/stderr with Python loggers. Returns (cmd_out, cmd_err) if successful, raises CalledProcessError otherwise. - If timeout is set, subprocess.run may raise TimeoutExpired.""" - process = subprocess.run( - [str(arg) for arg in args], - capture_output=True, - env=proc_env, - cwd=cwd, - text=True, - timeout=timeout, - ) - cmd_out = process.stdout.strip() - cmd_err = process.stderr.strip() + If timeout is set, subprocess.run/communicate may raise TimeoutExpired. + When kill_process_group is True, the whole process group is + terminated after completion or timeout cleanup.""" + use_new_session = start_new_session or kill_process_group + cmd = [str(arg) for arg in args] + if not use_new_session: + process = subprocess.run( + cmd, + capture_output=True, + env=proc_env, + cwd=cwd, + text=True, + timeout=timeout, + ) + cmd_out = process.stdout.strip() + cmd_err = process.stderr.strip() + returncode = process.returncode + else: + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env=proc_env, + cwd=cwd, + text=True, + start_new_session=True, + ) + try: + cmd_out, cmd_err = process.communicate(timeout=timeout) + except subprocess.TimeoutExpired: + if kill_process_group: + with contextlib.suppress(ProcessLookupError): + os.killpg(process.pid, signal.SIGTERM) + try: + cmd_out, cmd_err = process.communicate(timeout=5) + except subprocess.TimeoutExpired: + with contextlib.suppress(ProcessLookupError): + os.killpg(process.pid, signal.SIGKILL) + cmd_out, cmd_err = process.communicate() + else: + process.kill() + cmd_out, cmd_err = process.communicate() + raise + if kill_process_group: + with contextlib.suppress(ProcessLookupError): + os.killpg(process.pid, signal.SIGTERM) + cmd_out = "" if cmd_out is None else cmd_out.strip() + cmd_err = "" if cmd_err is None else cmd_err.strip() + returncode = process.returncode # Handle stdout if cmd_out: @@ -282,7 +324,7 @@ def launch_process_helper( log.debug(cmd_out) # Handle stderr, depending on return code - if process.returncode == 0: + if returncode == 0: # Process completed successfully, log stderr only as WARNING if cmd_err and print_stderr: log.warning(cmd_err) @@ -293,12 +335,15 @@ def launch_process_helper( # Log additional ERROR message cmd = " ".join(str(arg) for arg in args) if isinstance(args, list) else str(args) - log.error(f"Launched process returned non-zero exit code ({process.returncode}): {cmd}") + log.error(f"Launched process returned non-zero exit code ({returncode}): {cmd}") # Raise CalledProcessError for non-zero return code, including captured output - if process.returncode != 0: + if returncode != 0: raise VerboseCalledProcessError( - process.returncode, args, output=process.stdout, stderr=process.stderr + returncode, + args, + output=cmd_out, + stderr=cmd_err, ) return (cmd_out, cmd_err) From f1b83535c9eb8de30826eb9b7ea41d2582fa4c93 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 2 Jun 2026 15:29:01 +0200 Subject: [PATCH 144/170] Change CI configuration for testing --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e6318a3e7c..9769370d18 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ variables: - "full_ci" CPU_CORES: description: "Select number of CPU cores and test workers" - value: "32" + value: "8" CPU_CORES_BENCH: description: "Select number of CPU cores for benchmark runs" value: "32" @@ -30,7 +30,7 @@ variables: value: "1" SLURM_TIMEOUT: description: "Select SLURM timeout" - value: "2-0" # [days-hours] + value: "3-0" # [days-hours] SLURM_PARTITION: description: "Slurm partition (e.g., normal, largemem, fpga, gpu)" value: "fpga" From df3de74d30c38db52c6adb22de1d7c5ad15db6ce Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:31:48 +0200 Subject: [PATCH 145/170] Remove non existant port from SimEngine --- finn_xsi/finn_xsi/sim_engine.py | 1 - 1 file changed, 1 deletion(-) diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py index 55085ecbb6..0819ccd79d 100644 --- a/finn_xsi/finn_xsi/sim_engine.py +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -609,7 +609,6 @@ def __init__(self, top: "SimEngine", mm_axi: "str") -> None: self.wlast = top.get_bus_port(mm_axi, "wlast") self.bready = top.get_bus_port(mm_axi, "bready") self.bvalid = top.get_bus_port(mm_axi, "bvalid") - self.bdata = top.get_bus_port(mm_axi, "bdata") self.bresp = top.get_bus_port(mm_axi, "bresp") self.arready = top.get_bus_port(mm_axi, "arready") self.arvalid = top.get_bus_port(mm_axi, "arvalid") From 54677af4ad2d14f6f306de56657fdcfa08ea1b7b Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:01:46 +0200 Subject: [PATCH 146/170] Changes to debug mlo tests --- finn_xsi/finn_xsi/adapter.py | 11 +++++++++ src/finn/interface/manage_tests.py | 5 ++-- src/finn/util/basic.py | 23 ++++++++++++++----- .../test_fpgadataflow_finnloop.py | 4 +--- tests/pyproject.toml | 1 + 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index eeb885a1c3..550e50ae58 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -50,6 +50,14 @@ def compile_sim_obj( if glbl is not None: f.write(f"verilog work {glbl}\n") + # Check that every source file is found in the filesystem + for src in source_list: + if not wait_for_file(Path(src), timeout=5): + raise FINNInternalError( + f"Source file {src} not found. Check that all codegen ran successfully and " + "that the source files are in the expected location." + ) + # extract (unique, by using a set) verilog headers for inclusion verilog_headers = {str(Path(x).parent) for x in source_list if x.endswith((".vh", ".svh"))} verilog_header_incl_str = " ".join(["--include " + x for x in verilog_headers]) @@ -100,6 +108,9 @@ def compile_sim_obj( "--O3", "-s", top_module_name, + "-prj", + "rtlsim.prj", + "-incr", ] # Add debug flag if debug is enabled if debug: diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index ca9a065f7d..bbad271dc3 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -110,10 +110,11 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: shlex.split( ( f"{sys.executable} -m pytest -v -m 'not " - f"(end2end or sanity_bnn or notebooks)' " + f"(end2end or sanity_bnn or notebooks) and only' " f"--junitxml={ci_project_dir}/reports/main.xml " f"--html={ci_project_dir}/reports/main.html " - f"--reruns 1 --dist worksteal -n {num_workers}" + f"-p no:xdist -p no:cov -p no:rerunfailures" + # f"--reruns 1 --dist load -n {num_workers}" ), posix=IS_POSIX, ) diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index e48e40415b..af5fbb6ff0 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -42,6 +42,7 @@ """ import contextlib +import errno import os import signal import stat as statmod @@ -121,7 +122,7 @@ def wait_for_file( path: Path, timeout: float = 30.0, - stable_for: float = 0.2, + stable_for: float = 0.05, interval: float = 0.05, expect_dir: bool | None = False, ) -> bool: @@ -130,16 +131,23 @@ def wait_for_file( If expect_dir is True, only a directory satisfies the check. If False, only a regular file satisfies it. If None, either file or directory is ok. """ - deadline = time.time() + timeout + deadline = time.monotonic() + timeout last = None last_change = None - while time.time() < deadline: + while time.monotonic() < deadline: try: st = path.stat() - except FileNotFoundError: + except (FileNotFoundError, NotADirectoryError): time.sleep(interval) continue + except OSError as e: + # Under high parallelism, avoid failing immediately on transient + # process/file table exhaustion and retry until timeout. + if e.errno in (errno.EMFILE, errno.ENFILE): + time.sleep(interval) + continue + raise if expect_dir is True and not statmod.S_ISDIR(st.st_mode): time.sleep(interval) @@ -148,8 +156,11 @@ def wait_for_file( time.sleep(interval) continue + if stable_for <= 0: + return True + cur = (st.st_size, st.st_mtime_ns) - now = time.time() + now = time.monotonic() if cur == last: if last_change is not None and (now - last_change) >= stable_for: return True @@ -163,7 +174,7 @@ def wait_for_file( def wait_for_dir( - path: Path, timeout: float = 30.0, stable_for: float = 0.2, interval: float = 0.05 + path: Path, timeout: float = 30.0, stable_for: float = 0.05, interval: float = 0.05 ) -> bool: """Wait until directory exists and is stable in size/mtime for stable_for seconds.""" return wait_for_file( diff --git a/tests/fpgadataflow/test_fpgadataflow_finnloop.py b/tests/fpgadataflow/test_fpgadataflow_finnloop.py index c804475410..415af99b58 100644 --- a/tests/fpgadataflow/test_fpgadataflow_finnloop.py +++ b/tests/fpgadataflow/test_fpgadataflow_finnloop.py @@ -27,9 +27,6 @@ "stitched_ip_rtlsim", ] -fpga_part = "xcvc1902-vsva2197-2MP-e-S" -clk_ns = 5 - def generate_random_threshold_values(data_type, num_input_channels, num_steps): if data_type.is_integer(): @@ -419,6 +416,7 @@ def create_chained_loop_bodies( @pytest.mark.fpgadataflow @pytest.mark.vivado @pytest.mark.slow +@pytest.mark.only def test_finnloop_end2end_mlo( dim, iteration, elemwise_optype, rhs_shape, eltw_param_dtype, tail_node ): diff --git a/tests/pyproject.toml b/tests/pyproject.toml index acbf1ff498..b6b7263dc1 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -62,6 +62,7 @@ markers = [ "bnn_pynq: mark tests that execute Pynq-Z1 BNN tests", "bnn_zcu104: mark tests that execute ZCU104 BNN tests", "analysis: mark tests that run analysis tests", + "only: mark", ] [build-system] From bbcc9d9e72ee0d37cfa89f8158217aa210fca731 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:19:07 +0200 Subject: [PATCH 147/170] Fix FINNLoop Tests --- finn_xsi/finn_xsi/adapter.py | 3 +- src/finn/builder/build_dataflow_steps.py | 38 +++++++++--------- src/finn/core/onnx_exec.py | 6 ++- src/finn/core/rtlsim_exec.py | 4 +- src/finn/interface/manage_tests.py | 3 +- .../fpgadataflow/create_stitched_ip.py | 40 +++++++++++++++++++ .../qonnx/give_unique_node_names_recursive.py | 10 +++-- 7 files changed, 75 insertions(+), 29 deletions(-) diff --git a/finn_xsi/finn_xsi/adapter.py b/finn_xsi/finn_xsi/adapter.py index 550e50ae58..73adf49ae4 100644 --- a/finn_xsi/finn_xsi/adapter.py +++ b/finn_xsi/finn_xsi/adapter.py @@ -258,7 +258,8 @@ def rtlsim_multi_io( ret = sim.run() if len(ret) > 0: raise FINNUserError( - f"RTL simulation watchdogs {ret!s} timed out. Check rtlsim_trace if any." + f"RTL simulation watchdogs {ret!s} timed out with {liveness_threshold} cycles. " + f"Check rtlsim_trace if any." ) end_ticks = sim.ticks for out in io_dict["outputs"]: diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index b80edfe573..aa59bbec71 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -78,7 +78,6 @@ ) from finn.builder.passes import step_passes_frontend from finn.core.onnx_exec import execute_onnx -from finn.core.rtlsim_exec import rtlsim_exec from finn.transformation.fpgadataflow.annotate_cycles import AnnotateCycles from finn.transformation.fpgadataflow.compile_cppsim import CompileCppSim from finn.transformation.fpgadataflow.create_dataflow_partition import CreateDataflowPartition @@ -136,6 +135,8 @@ from finn.util.logging import log from finn.util.mlo_sim import is_mlo, mlo_prehook_func_factory +from finn.xsi import SimEngine + if TYPE_CHECKING: from finn.custom_op.fpgadataflow.rtl.finn_loop import FINNLoop @@ -168,7 +169,7 @@ def verify_step( cfg: DataflowBuildConfig, step_name: str, need_parent: bool, - rtlsim_pre_hook=None, + rtlsim_pre_hook: Callable[[SimEngine], None] | None = None, ) -> None: """Verify a build step by running simulation and comparing results. @@ -243,12 +244,8 @@ def verify_step( log.info("Attempting to force model shape on verification input") in_npy = in_npy.reshape(exp_ishape) inp_dict = {inp_tensor_name: in_npy} - if rtlsim_pre_hook is not None: - rtlsim_exec(model, inp_dict, pre_hook=rtlsim_pre_hook) - out_npy = inp_dict[out_tensor_name] - else: - out_dict = execute_onnx(model, inp_dict, True) - out_npy = out_dict[out_tensor_name] + out_dict = execute_onnx(model, inp_dict, True, pre_hook=rtlsim_pre_hook) + out_npy = out_dict[out_tensor_name] exp_oshape = exp_out_npy.shape if out_npy.shape != exp_oshape: log.warning( @@ -375,10 +372,12 @@ def verify_step( @register_build_dataflow_step() -def step_hw_codegen(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: +def step_hw_codegen( + model: ModelWrapper, cfg: DataflowBuildConfig, parent_node: str | None = None +) -> ModelWrapper: """Generate Vitis HLS code to prepare HLSBackend nodes for IP generation. And fills RTL templates for RTLBackend nodes.""" - model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveUniqueNodeNamesRecursive(prefix=parent_node)) model = model.transform( PrepareIP(cfg._resolve_fpga_part(), cfg._resolve_hls_clk_period()), apply_to_subgraphs=True, @@ -502,7 +501,7 @@ def step_set_fifo_depths( # Clean up model model = model.transform(SortGraph()) - model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveUniqueNodeNamesRecursive(prefix=parent_node)) model = model.transform(GiveReadableTensorNames()) # save original folding config before potentially modifying it @@ -540,7 +539,7 @@ def step_set_fifo_depths( # Clean up model model = model.transform(SortGraph()) - model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveUniqueNodeNamesRecursive(prefix=parent_node)) model = model.transform(GiveReadableTensorNames()) # Set impl_style + ID attributes @@ -582,7 +581,7 @@ def step_set_fifo_depths( if cfg.split_large_fifos: model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) - model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveUniqueNodeNamesRecursive(prefix=parent_node)) model = model.transform(GiveReadableTensorNames()) else: if cfg.fifo_config_file is None: @@ -596,18 +595,18 @@ def step_set_fifo_depths( # set by ApplyConfig, so create_shallow_fifos=True model = model.transform(InsertFIFO(create_shallow_fifos=True)) model = model.transform(SpecializeLayers(cfg._resolve_fpga_part())) - model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveUniqueNodeNamesRecursive(prefix=parent_node)) model = model.transform(GiveReadableTensorNames()) model = model.transform(ApplyFIFODepthsFromFile(cfg.fifo_config_file)) if cfg.split_large_fifos: model = model.transform(SplitLargeFIFOs(max_qsrl_depth=256)) - model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveUniqueNodeNamesRecursive(prefix=parent_node)) model = model.transform(GiveReadableTensorNames()) # after FIFOs are ready to go, call PrepareIP and HLSSynthIP again # this will only run for the new nodes (e.g. FIFOs and DWCs) # Codegen for the inserted FIFOs - model = step_hw_codegen(model, cfg) + model = step_hw_codegen(model, cfg, parent_node=parent_node) # IP Gen for the inserted FIFOs and any remaining # IPs that needed to be re-gen after FIFO insertion model = step_hw_ipgen(model, cfg, parent_node=parent_node) @@ -620,7 +619,7 @@ def step_generate_hardware( ) -> ModelWrapper: """Generate the hardware IP of the model. This includes generating the code, IPs and sizing the fifos for the model and all submodels.""" - model = model.transform(GiveUniqueNodeNamesRecursive()) + model = model.transform(GiveUniqueNodeNamesRecursive(prefix=parent_node)) # Recursively call this step for all subgraphs for node in model.get_nodes_by_op_type("FINNLoop"): node_inst = cast("FINNLoop", getCustomOp(node)) @@ -633,7 +632,7 @@ def step_generate_hardware( node_inst.set_nodeattr("body", loop_model.graph) # Codegen for the current model - model = step_hw_codegen(model, cfg) + model = step_hw_codegen(model, cfg, parent_node=parent_node) # Stitch submodels for node in model.get_nodes_by_op_type("FINNLoop"): @@ -648,7 +647,6 @@ def step_generate_hardware( ) ) node_inst.set_nodeattr("body", loop_model.graph) - # IP Gen for the current model model = step_hw_ipgen(model, cfg, parent_node=parent_node) @@ -998,7 +996,7 @@ def step_transpose_decomposition(model: ModelWrapper, cfg: DataflowBuildConfig) for node in loop_nodes: node_inst = getCustomOp(node) loop_model = node_inst.get_nodeattr("body") - loop_model = loop_model.transform(GiveUniqueNodeNamesRecursive(prefix=node.name + "_")) + loop_model = loop_model.transform(GiveUniqueNodeNamesRecursive(prefix=node.name)) node_inst.set_nodeattr("body", loop_model.graph) else: log.info("Model doesn't contain any Shuffle nodes, skipping step_transpose_decomposition.") diff --git a/src/finn/core/onnx_exec.py b/src/finn/core/onnx_exec.py index e3741cf52a..89ce631094 100644 --- a/src/finn/core/onnx_exec.py +++ b/src/finn/core/onnx_exec.py @@ -50,6 +50,8 @@ from finn.core.rtlsim_exec import rtlsim_exec from finn.util.exception import FINNInternalError +from finn.xsi import SimEngine + def execute_onnx( model: "ModelWrapper", @@ -57,6 +59,8 @@ def execute_onnx( return_full_exec_context: bool = False, start_node: NodeProto | None = None, end_node: NodeProto | None = None, + pre_hook: Callable[[SimEngine], None] | None = None, + post_hook: Callable[[SimEngine], None] | None = None, ) -> dict[str, np.ndarray]: """Execute given ONNX ModelWrapper with given named inputs. If return_full_exec_context is False, a dict of named outputs is returned @@ -109,7 +113,7 @@ def execute_onnx( ) # use stitched IP for rtlsim - rtlsim_exec(model, cast("dict[str, np.ndarray]", execution_context)) + rtlsim_exec(model, cast("dict[str, np.ndarray]", execution_context), pre_hook, post_hook) else: raise FINNInternalError( """Metadata property "exec_mode" is set to an unknown value. Can be left diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index 5c6adc8f0c..b488416ce7 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -30,7 +30,7 @@ import numpy as np from collections.abc import Callable from pathlib import Path -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, cast from finn import xsi as finnxsi from finn.util.basic import get_liveness_threshold_cycles, getHWCustomOp, make_build_dir @@ -224,7 +224,7 @@ def rtlsim_exec_finnxsi( n_cycles = finnxsi.rtlsim_multi_io( sim, io_dict, - num_out_values, + cast("dict[str, int | np.integer]|int", num_out_values), sname="", liveness_threshold=get_liveness_threshold_cycles() * batchsize, ) diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index bbad271dc3..64318eace2 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -113,8 +113,7 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: f"(end2end or sanity_bnn or notebooks) and only' " f"--junitxml={ci_project_dir}/reports/main.xml " f"--html={ci_project_dir}/reports/main.html " - f"-p no:xdist -p no:cov -p no:rerunfailures" - # f"--reruns 1 --dist load -n {num_workers}" + f"--reruns 1 --dist loadgroup -n {num_workers}" ), posix=IS_POSIX, ) diff --git a/src/finn/transformation/fpgadataflow/create_stitched_ip.py b/src/finn/transformation/fpgadataflow/create_stitched_ip.py index 55b5024e3a..2bfddcc8e3 100644 --- a/src/finn/transformation/fpgadataflow/create_stitched_ip.py +++ b/src/finn/transformation/fpgadataflow/create_stitched_ip.py @@ -45,6 +45,7 @@ from typing import TYPE_CHECKING, Literal, cast if TYPE_CHECKING: + from finn.custom_op.fpgadataflow.rtl.finn_loop import FINNLoop from onnx import NodeProto from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend @@ -851,10 +852,49 @@ def apply(self, model: "ModelWrapper") -> tuple[ModelWrapper, Literal[False]]: f"{vivado_stitch_proj_dir} to find out why it failed." ) from e + # Extract list of Verilog files used in the loop bodies and + # add it to the list of files for the stitched IP + files_loop_body: list[str] = [] + loop_nodes = model.get_nodes_by_op_type("FINNLoop") + for node in loop_nodes: + node_inst = cast("FINNLoop", getCustomOp(node)) + loop_model = cast("ModelWrapper", node_inst.get_nodeattr("body")) + loop_stitch_proj = loop_model.get_metadata_prop("vivado_stitch_proj") + if loop_stitch_proj is None: + raise FINNInternalError( + f"Loop node {node.name} does not have metadata property " + f"vivado_stitch_proj after CreateStitchedIP. This should " + f"have been set during the recursive call to CreateStitchedIP " + f"on the loop body. Aborting." + ) + loop_stitch_proj_dir = Path(loop_stitch_proj) + if not loop_stitch_proj_dir.is_dir(): + raise FINNInternalError( + f"Loop node {node.name} has vivado_stitch_proj metadata property " + f"set to {loop_stitch_proj}, but this is not a valid directory. " + f"Aborting." + ) + loop_v_file_list = loop_stitch_proj_dir / "all_verilog_srcs.txt" + if not loop_v_file_list.is_file(): + raise FINNInternalError( + f"Loop node {node.name} has vivado_stitch_proj metadata property " + f"set to {loop_stitch_proj}, but expected file all_verilog_srcs.txt " + f"not found in that directory. Aborting." + ) + with loop_v_file_list.open("r") as f: + for line in f: + files_loop_body.append(line.strip()) + # Deduplicate list of files and add to the list of files for the stitched IP + files_loop_body = list(set(files_loop_body)) + if self.functional_simulation: with Path(v_file_list).open("a") as f: f.write(f"{fifosim_wrapper_filename}\n") + with Path(v_file_list).open("a") as f: + for file in files_loop_body: + f.write(f"{file}\n") + # wrapper may be created in different location depending on Vivado version if not wait_for_file(Path(wrapper_filename), timeout=5.0): # check in alternative location (.gen instead of .srcs) diff --git a/src/finn/transformation/qonnx/give_unique_node_names_recursive.py b/src/finn/transformation/qonnx/give_unique_node_names_recursive.py index caaa57a323..4c746c815c 100644 --- a/src/finn/transformation/qonnx/give_unique_node_names_recursive.py +++ b/src/finn/transformation/qonnx/give_unique_node_names_recursive.py @@ -15,7 +15,7 @@ class GiveUniqueNodeNamesRecursive(Transformation): """Give unique names to each node in the graph using enumeration, starting with given prefix (if specified in the constructor).""" - def __init__(self, prefix: str = "") -> None: + def __init__(self, prefix: str | None = None) -> None: """Initialize the transformation with an optional prefix for node names.""" super().__init__() self.prefix = prefix @@ -26,12 +26,16 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: for n in model.graph.node: if n.op_type not in optype_count.keys(): optype_count[n.op_type] = 0 - n.name = f"{self.prefix}{n.op_type}_{optype_count[n.op_type]}" + n.name = ( + f"{self.prefix}_{n.op_type}_{optype_count[n.op_type]}" + if self.prefix is not None + else f"{n.op_type}_{optype_count[n.op_type]}" + ) optype_count[n.op_type] += 1 if n.op_type == "FINNLoop": loop_inst = cast("FINNLoop", getCustomOp(n)) loop_body = cast("ModelWrapper", loop_inst.get_nodeattr("body")) - loop_body = loop_body.transform(GiveUniqueNodeNamesRecursive(prefix=n.name + "_")) + loop_body = loop_body.transform(GiveUniqueNodeNamesRecursive(prefix=n.name)) loop_inst.set_nodeattr("body", loop_body.graph) # return model_was_changed = False as single iteration is always enough return (model, False) From 99e0d2d1bedb489ddbfb8f84b1f5c3cc2262d72b Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:25:03 +0200 Subject: [PATCH 148/170] Mark some mvau configs xfail --- finn_xsi/finn_xsi/LayerSimulationBackend.cpp | 2 +- .../transformation/fpgadataflow/simulation_build.py | 10 ++++++++++ tests/fpgadataflow/test_fpgadataflow_finnloop.py | 1 - tests/fpgadataflow/test_fpgadataflow_mvau.py | 4 +++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp index 98216372b7..e60cc41f2f 100644 --- a/finn_xsi/finn_xsi/LayerSimulationBackend.cpp +++ b/finn_xsi/finn_xsi/LayerSimulationBackend.cpp @@ -354,7 +354,7 @@ int main(int argc, const char* argv[]) { // Construct simulation SingleNodeSimulation - sim(RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, "xsim_log_file.txt", "trace_file.txt", RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs, + sim(RTLSimConfig::kernel_libname, RTLSimConfig::design_libname, RTLSimConfig::xsim_log_filename.c_str(), RTLSimConfig::trace_filename.value_or("").c_str(), RTLSimConfig::istream_descs, RTLSimConfig::ostream_descs, RTLSimConfig::inputInterfaceNames, RTLSimConfig::outputInterfaceNames, 2); // Create simulation controller diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 607baac6b1..2036bfa26e 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -534,6 +534,15 @@ def _get_first_non_fifo_output(node: NodeProto, outp: str) -> str: node_model.set_metadata_prop("input_node", str(input_node).lower()) node_model.set_metadata_prop("output_node", str(output_node).lower()) + rtlsim_trace_str = self.model.get_metadata_prop("rtlsim_trace") + if rtlsim_trace_str is not None: + rtlsim_trace_path = Path(rtlsim_trace_str) + # Add suffix to name depending on node name + rtlsim_trace_path = rtlsim_trace_path.with_name( + rtlsim_trace_path.stem + f"_{target_node.name}" + rtlsim_trace_path.suffix + ) + node_model.set_metadata_prop("rtlsim_trace", str(rtlsim_trace_path)) + return node_model def _get_stream_descriptions(self, model: ModelWrapper) -> tuple[str, str]: @@ -859,6 +868,7 @@ def _build( build_dir: Path, ) -> Any: """Build simulation for a single node.""" + self.model.save("/scratch/pc2-mitarbeiter/linusjun/mvaumodel.onnx") nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(InferShapes()) nodemodel = nodemodel.transform(PrepareIP(self.fpgapart, self.clk_ns)) diff --git a/tests/fpgadataflow/test_fpgadataflow_finnloop.py b/tests/fpgadataflow/test_fpgadataflow_finnloop.py index 415af99b58..cb01378e97 100644 --- a/tests/fpgadataflow/test_fpgadataflow_finnloop.py +++ b/tests/fpgadataflow/test_fpgadataflow_finnloop.py @@ -416,7 +416,6 @@ def create_chained_loop_bodies( @pytest.mark.fpgadataflow @pytest.mark.vivado @pytest.mark.slow -@pytest.mark.only def test_finnloop_end2end_mlo( dim, iteration, elemwise_optype, rhs_shape, eltw_param_dtype, tail_node ): diff --git a/tests/fpgadataflow/test_fpgadataflow_mvau.py b/tests/fpgadataflow/test_fpgadataflow_mvau.py index 0e406bc0a2..6f6ff9cf62 100644 --- a/tests/fpgadataflow/test_fpgadataflow_mvau.py +++ b/tests/fpgadataflow/test_fpgadataflow_mvau.py @@ -702,7 +702,9 @@ def test_fpgadataflow_rtl_mvau( ) if pe == 1 and simd == 1 and pumpedMemory: - pytest.skip("Skip PE=SIMD=1 with pumpedMemory=True, known weight generation bug") + pytest.xfail("Skip PE=SIMD=1 with pumpedMemory=True, known weight generation bug") + if simd == mw and pumpedCompute: + pytest.xfail("Skip SIMD=MW with pumpedCompute=True, memstreamer not working in rtl sim") if simd == 1 and pumpedCompute: pytest.skip("""Clock pumping an input of SIMD=1 is not meaningful. Skipping test""") From 10e81eab8ddd47acb85b083fec16fbe73a8dba40 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:31:30 +0200 Subject: [PATCH 149/170] Reenable all unittests --- src/finn/interface/manage_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index 64318eace2..5e78655d93 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -110,7 +110,7 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: shlex.split( ( f"{sys.executable} -m pytest -v -m 'not " - f"(end2end or sanity_bnn or notebooks) and only' " + f"(end2end or sanity_bnn or notebooks)' " f"--junitxml={ci_project_dir}/reports/main.xml " f"--html={ci_project_dir}/reports/main.html " f"--reruns 1 --dist loadgroup -n {num_workers}" From cca477ab32a55cf02d1f637754f46519ac635dec Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 22 Jun 2026 15:57:09 +0200 Subject: [PATCH 150/170] Remove leftover model.save and change some CI settings --- .gitlab-ci.yml | 4 ++-- src/finn/transformation/fpgadataflow/simulation_build.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9769370d18..08a33daac1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ variables: - "full_ci" CPU_CORES: description: "Select number of CPU cores and test workers" - value: "8" + value: "16" CPU_CORES_BENCH: description: "Select number of CPU cores for benchmark runs" value: "32" @@ -169,7 +169,7 @@ FINN Test Suite 2022.2: source finn-plus-venv/bin/activate dvc config cache.dir $CI_DVC_CACHE_DIR dvc pull - finn test --variant $TEST_SUITE --dependency-path ./deps --build-path $FINN_BUILD_DIR --num-default-workers 1 --num-test-workers $PYTEST_PARALLEL + finn test --variant $TEST_SUITE --dependency-path ./deps --build-path $FINN_BUILD_DIR --num-test-workers $PYTEST_PARALLEL artifacts: name: "test_reports" when: always diff --git a/src/finn/transformation/fpgadataflow/simulation_build.py b/src/finn/transformation/fpgadataflow/simulation_build.py index 2036bfa26e..de3b0712fa 100644 --- a/src/finn/transformation/fpgadataflow/simulation_build.py +++ b/src/finn/transformation/fpgadataflow/simulation_build.py @@ -868,7 +868,6 @@ def _build( build_dir: Path, ) -> Any: """Build simulation for a single node.""" - self.model.save("/scratch/pc2-mitarbeiter/linusjun/mvaumodel.onnx") nodemodel = self._isolated_node_model(node_index) nodemodel = nodemodel.transform(InferShapes()) nodemodel = nodemodel.transform(PrepareIP(self.fpgapart, self.clk_ns)) From 6291fcb558204dfbe07109cf8161a3b0b78c275d Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Mon, 22 Jun 2026 17:43:25 +0200 Subject: [PATCH 151/170] Fix bench_base --- src/finn/benchmarking/bench_base.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/finn/benchmarking/bench_base.py b/src/finn/benchmarking/bench_base.py index 723f821049..c55a1f69b3 100644 --- a/src/finn/benchmarking/bench_base.py +++ b/src/finn/benchmarking/bench_base.py @@ -296,8 +296,7 @@ def _step_build_setup(self) -> DataflowBuildConfig: dut_path = Path(__file__).parent / "dut" / dut_yaml_name if dut_path.is_file(): with dut_path.open() as f: - data = yaml.load(f, Loader=yaml.SafeLoader) - return DataflowBuildConfig.from_yaml(data) + return DataflowBuildConfig.from_yaml(f) else: raise Exception("No DUT-specific YAML build definition found") From b1abbf3187be2347c50a456f3803ae46beebb480 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:22:49 +0200 Subject: [PATCH 152/170] Start debugging intermittent segfault --- .gitlab-ci.yml | 2 +- finn_xsi/finn_xsi/sim_engine.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 08a33daac1..6009f16de2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ variables: - "full_ci" CPU_CORES: description: "Select number of CPU cores and test workers" - value: "16" + value: "32" CPU_CORES_BENCH: description: "Select number of CPU cores for benchmark runs" value: "32" diff --git a/finn_xsi/finn_xsi/sim_engine.py b/finn_xsi/finn_xsi/sim_engine.py index 0819ccd79d..71d284252c 100644 --- a/finn_xsi/finn_xsi/sim_engine.py +++ b/finn_xsi/finn_xsi/sim_engine.py @@ -55,7 +55,8 @@ def __init__( self, kernel: str, design: str, log: str | None = None, wdb: str | None = None ) -> None: """Create a simulation engine bound to the given kernel and design.""" - top = xsi.Design(xsi.Kernel(kernel), design, log, wdb) + k = xsi.Kernel(kernel) + top = xsi.Design(k, design, log, wdb) clk = top.getPort("ap_clk") # If clock pumping is disabled, set clk2x to None try: From f36eeeef2e7d824875ccb82935ae766035087ecf Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:39:25 +0200 Subject: [PATCH 153/170] Test increasing pytest-rerunfailures version to deal with non-deterministic segmentation faults. --- tests/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pyproject.toml b/tests/pyproject.toml index b6b7263dc1..c3d00de762 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -34,7 +34,7 @@ pytest-html = "~4.1.1" pytest-html-merger = "~0.1.0" pytest-cov = "~6.2.1" pytest-forked = "~1.6.0" -pytest-rerunfailures = "~15.1" +pytest-rerunfailures = "~16.1" pytest-dependency = "~0.6.0" pytest-parallel = "~0.1.1" pytest-xdist = { version = "~3.6.1", extras = ["setproctitle"] } From 56296f287f38f06c652956d6638a60840cc3c786 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:26:57 +0200 Subject: [PATCH 154/170] Remove pytest coverage --- tests/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/pyproject.toml b/tests/pyproject.toml index c3d00de762..092ebe68d4 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -32,7 +32,6 @@ pytest = "~8.4.1" pytest-metadata = "~3.1.1" pytest-html = "~4.1.1" pytest-html-merger = "~0.1.0" -pytest-cov = "~6.2.1" pytest-forked = "~1.6.0" pytest-rerunfailures = "~16.1" pytest-dependency = "~0.6.0" From e0b7d56f9f96ffaa9204ccb6f2b2c31ed49e32c8 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 23 Jun 2026 14:57:46 +0200 Subject: [PATCH 155/170] Limit the number of workers --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6009f16de2..5eec03d79d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -169,7 +169,7 @@ FINN Test Suite 2022.2: source finn-plus-venv/bin/activate dvc config cache.dir $CI_DVC_CACHE_DIR dvc pull - finn test --variant $TEST_SUITE --dependency-path ./deps --build-path $FINN_BUILD_DIR --num-test-workers $PYTEST_PARALLEL + finn test --variant $TEST_SUITE --dependency-path ./deps --build-path $FINN_BUILD_DIR --num-test-workers $PYTEST_PARALLEL -n 1 artifacts: name: "test_reports" when: always From 19ba220ae8cf1c650fbc06df1bbc6cd4109727db Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:43:42 +0200 Subject: [PATCH 156/170] Switch scheduler to loadscope instead of loadgroup --- src/finn/interface/manage_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index 5e78655d93..4d0412b646 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -113,7 +113,7 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: f"(end2end or sanity_bnn or notebooks)' " f"--junitxml={ci_project_dir}/reports/main.xml " f"--html={ci_project_dir}/reports/main.html " - f"--reruns 1 --dist loadgroup -n {num_workers}" + f"--reruns 1 --dist loadscope -n {num_workers}" ), posix=IS_POSIX, ) From 781244c30d1e586efc75de52f363c5029ea95972 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Tue, 23 Jun 2026 16:49:16 +0200 Subject: [PATCH 157/170] Revert settings fix --- src/finn/interface/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finn/interface/settings.py b/src/finn/interface/settings.py index 2603bc60c7..4f44f0726a 100644 --- a/src/finn/interface/settings.py +++ b/src/finn/interface/settings.py @@ -294,7 +294,7 @@ def update_from(self, data: dict[str, Any], update_type: str | None = None) -> F modified_data = self.model_dump() for key in data.keys(): lkey = key.lower() - if lkey in modified_data and data[key] is not None: + if lkey in modified_data: modified_data[lkey] = data[key] try: new_model = FINNSettings.model_validate(modified_data) From bb24666464193701dd6fe9b166da1a2335c3b360 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:32:29 +0200 Subject: [PATCH 158/170] Fix some of the last tests --- scripts/merge_xml_reports.py | 166 +++++++++++ .../fpgadataflow/op_and_param_counts.py | 6 +- src/finn/builder/build_dataflow_config.py | 7 +- src/finn/builder/build_dataflow_steps.py | 159 ++++++----- src/finn/interface/manage_tests.py | 179 ++++++++++-- .../fpgadataflow/vivado_power_estimation.py | 2 +- tests/fpgadataflow/test_fifosizing.py | 259 ++++++++++++++---- tests/fpgadataflow/test_simulation_build.py | 2 - tests/pyproject.toml | 1 + 9 files changed, 645 insertions(+), 136 deletions(-) create mode 100755 scripts/merge_xml_reports.py diff --git a/scripts/merge_xml_reports.py b/scripts/merge_xml_reports.py new file mode 100755 index 0000000000..a91352fb38 --- /dev/null +++ b/scripts/merge_xml_reports.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Merge arbitrary many pytest JUnit XML files with this rule: +- Identity key: (classname, name) +- If same testcase appears multiple times: + - PASSED always wins over FAILED/ERROR/SKIPPED + - Otherwise, latest file wins (input order). + +Usage: + python merge_junit_prefer_pass.py -o merged.xml main.xml rerun1.xml rerun2.xml +""" + +import argparse +import sys +from collections import OrderedDict +from collections.abc import Iterable +from junitparser import JUnitXml, TestSuite +from junitparser.junitparser import TestCase +from typing import Literal + +TestStatus = Literal["passed", "failed", "skipped", "unknown"] +TestKey = tuple[str, str] + + +def testcase_key(tc: TestCase) -> TestKey: + """Build the identity key for a testcase. + + Args: + tc: TestCase object parsed from JUnit XML. + + Returns: + Tuple of (classname, name). Missing attributes default to empty strings. + """ + classname: str = getattr(tc, "classname", "") or "" + name: str = getattr(tc, "name", "") or "" + return (classname, name) + + +def testcase_status(tc: TestCase) -> TestStatus: + """Derive a normalized status from a JUnit testcase. + + Status rules (pytest JUnit convention): + - passed: no result children + - failed: contains or + - skipped: contains + - unknown: any other non-empty result shape + + Args: + tc: TestCase object. + + Returns: + One of: "passed", "failed", "skipped", "unknown". + """ + result_items = tc.result + if not result_items: + return "passed" + + tags = {item._tag for item in result_items} # noqa: SLF001 + if "failure" in tags or "error" in tags: + return "failed" + if "skipped" in tags: + return "skipped" + return "unknown" + + +def should_replace(existing_tc: TestCase, new_tc: TestCase) -> bool: + """Decide whether a newly seen testcase should replace the currently stored one. + + Priority: + 1) PASSED always wins over non-passed. + 2) If existing is passed, never replace with non-passed. + 3) If neither side is passed, latest file wins (replace with new). + + Args: + existing_tc: Previously stored testcase for the same key. + new_tc: Newly encountered testcase for the same key. + + Returns: + True if new_tc should replace existing_tc, else False. + """ + old_status = testcase_status(existing_tc) + new_status = testcase_status(new_tc) + + if old_status == "passed": + return False + if new_status == "passed": + return True + return True # latest wins if no pass involved + + +def merge_reports(inputs: Iterable[str], output: str) -> None: + """Merge multiple JUnit XML reports into one output report. + + Input order is chronological (earlier -> later). + For duplicate test keys, replacement follows `should_replace`. + + Args: + inputs: Iterable of input XML file paths. + output: Output XML file path. + + Returns: + None + """ + by_key: OrderedDict[TestKey, TestCase] = OrderedDict() + + for path in inputs: + xml = JUnitXml.fromfile(path) + for suite in xml: + for tc in suite: + key = testcase_key(tc) + if key not in by_key or should_replace(by_key[key], tc): + by_key[key] = tc + + merged = JUnitXml() + out_suite = TestSuite("merged") + + for tc in by_key.values(): + out_suite.add_testcase(tc) + + merged.add_testsuite(out_suite) + merged.write(output) + + +def build_parser() -> argparse.ArgumentParser: + """Build and return the CLI argument parser. + + Returns: + Configured ArgumentParser instance. + """ + parser = argparse.ArgumentParser( + description="Merge JUnit XML files, always preferring PASSED results." + ) + parser.add_argument( + "-o", + "--output", + required=True, + help="Output merged XML path.", + ) + parser.add_argument( + "inputs", + nargs="+", + help="Input XML files in chronological order (earlier -> later).", + ) + return parser + + +def main() -> int: + """CLI entrypoint. + + Returns: + Process exit code (0 on success, non-zero on error). + """ + parser = build_parser() + args = parser.parse_args() + + try: + merge_reports(args.inputs, args.output) + except Exception as exc: # pragma: no cover + print(f"[ERROR] {exc}", file=sys.stderr) + return 1 + + print(f"Merged {len(args.inputs)} files into {args.output}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/finn/analysis/fpgadataflow/op_and_param_counts.py b/src/finn/analysis/fpgadataflow/op_and_param_counts.py index 76a3e3f5c2..49d0f1816b 100644 --- a/src/finn/analysis/fpgadataflow/op_and_param_counts.py +++ b/src/finn/analysis/fpgadataflow/op_and_param_counts.py @@ -29,10 +29,14 @@ import qonnx.custom_op.registry as registry from qonnx.core.modelwrapper import ModelWrapper +from typing import TypeVar + from finn.util.basic import getHWCustomOp +T = TypeVar("T", int | float, int) + -def aggregate_dict_keys(res_dict: dict[str, dict[str, int]]) -> dict[str, int]: +def aggregate_dict_keys(res_dict: dict[str, dict[str, T]]) -> dict[str, T]: """Aggregate counts across all nodes in the provided dictionary.""" total_dict = {} for layer in res_dict: diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index 2db3939af9..e2d33d0957 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -56,11 +56,14 @@ from mashumaro.mixins.json import DataClassJSONMixin from mashumaro.mixins.yaml import DataClassYAMLMixin from pathlib import Path, PosixPath, PurePath -from typing import Any, Literal, Optional, cast +from typing import TYPE_CHECKING, Any, Literal, Optional, cast from finn.util.basic import alveo_default_platform, part_map from finn.util.exception import FINNConfigurationError +if TYPE_CHECKING: + from onnx import NodeProto + class LogLevel(str, Enum): """Log levels printed on the commandline for the build process.""" @@ -581,7 +584,7 @@ def _fix_path(p: Path | None) -> Path | None: #: For this node range, the PyTorch metadata hierarchy will be simulated #: TODO: this argument will be replaced or extended when there is a way #: to preserve node metadata from the PyTorch model (e.g. from dynamo exporter) - loop_body_range: Optional[list[Any]] = None + loop_body_range: Optional[tuple[NodeProto, NodeProto]] = None #: (Only relevant if CPP_DRIVER output product is enabled) Selects C++ driver version. #: If set to "latest", newest version will be used. diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 49857df0c3..bc7df2a619 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -188,7 +188,7 @@ def verify_step( model = model.transform(SortGraph()) model = model.transform(GiveUniqueNodeNamesRecursive()) model = model.transform(GiveReadableTensorNames()) - os.makedirs(verify_out_dir, exist_ok=True) + verify_out_dir.mkdir(parents=True, exist_ok=True) if cfg.verify_steps is None: raise FINNUserError("verify_steps is not set in config, but verification step was called") (in_npy_all, exp_out_npy_all) = cast( @@ -271,9 +271,7 @@ def verify_step( all_res = all_res and res res_str = res_to_str[bool(res)] if cfg.verify_save_full_context and (rtlsim_pre_hook is None): - verification_output_fn = os.path.join( - verify_out_dir, f"verify_{step_name}_{b}_{res_str}.npz" - ) + verification_output_fn = verify_out_dir / f"verify_{step_name}_{b}_{res_str}.npz" np.savez(verification_output_fn, **out_dict) # Log tensor statistics for debugging (only output tensors, in topological order) @@ -283,7 +281,7 @@ def verify_step( raise FINNUserError("Parent model is needed for verification but is None") for node in parent_model.graph.node: for output in node.output: - tensors_to_log.append(output) + tensors_to_log.append(output) # noqa: PERF402 sdp_node = parent_model.get_nodes_by_op_type("StreamingDataflowPartition")[0] sdp_prefix = sdp_node.name + "_" else: @@ -306,9 +304,7 @@ def verify_step( tensor_stats.append(stat_dict) # Write tensor statistics in compact human-readable table format - with open( - os.path.join(verify_out_dir, f"verify_{step_name}_{b}_{res_str}_stats.txt"), "w" - ) as f: + with (verify_out_dir / f"verify_{step_name}_{b}_{res_str}_stats.txt").open("w") as f: # Write header f.write( f"{'Tensor':<40} {'Shape':<20} {'Mean':<12} " @@ -348,24 +344,22 @@ def verify_step( else: if cfg.verify_save_full_context: log.warning("Warning: Unable to save the full context when using MLO") - verification_output_fn = os.path.join( - verify_out_dir, f"verify_{step_name}_{b}_{res_str}.npy" - ) + verification_output_fn = verify_out_dir / f"verify_{step_name}_{b}_{res_str}.npy" np.save(verification_output_fn, out_npy) if cfg.verify_save_rtlsim_waveforms: # Handle model-level waveform (stitched IP rtlsim) wdb_path = model.get_metadata_prop("rtlsim_trace") - if wdb_path is not None and os.path.isfile(wdb_path): - new_wdb_path = wdb_path.replace(".wdb", "_%d.wdb" % b) + if wdb_path is not None and Path(wdb_path).is_file(): + new_wdb_path = wdb_path.replace(".wdb", f"_{b}.wdb") shutil.move(wdb_path, new_wdb_path) # Handle node-level waveforms (only for node-by-node rtlsim) if step_name == "node_by_node_rtlsim": for node in model.graph.node: node_inst = getCustomOp(node) node_wdb_path = cast("str", node_inst.get_nodeattr("rtlsim_trace")) - if node_wdb_path is not None and os.path.isfile(node_wdb_path): - new_node_wdb_path = node_wdb_path.replace(".wdb", "_%d.wdb" % b) + if node_wdb_path is not None and Path(node_wdb_path).is_file(): + new_node_wdb_path = node_wdb_path.replace(".wdb", f"_{b}.wdb") shutil.move(node_wdb_path, new_node_wdb_path) log.info(f"Verification for {step_name} : {res_to_str[bool(all_res)]}") @@ -981,9 +975,9 @@ def step_transpose_decomposition(model: ModelWrapper, cfg: DataflowBuildConfig) has_shuffle = bool(model.get_nodes_by_op_type("Shuffle")) loop_nodes = model.get_nodes_by_op_type("FINNLoop") for node in loop_nodes: - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") - has_shuffle = True if loop_model.get_nodes_by_op_type("Shuffle") else False + node_inst = cast("FINNLoop", getCustomOp(node)) + loop_model = cast("ModelWrapper", node_inst.get_nodeattr("body")) + has_shuffle = bool(loop_model.get_nodes_by_op_type("Shuffle")) if has_shuffle: model = model.transform(ShuffleDecomposition(), apply_to_subgraphs=True) @@ -994,8 +988,8 @@ def step_transpose_decomposition(model: ModelWrapper, cfg: DataflowBuildConfig) model = model.transform(GiveUniqueNodeNamesRecursive()) loop_nodes = model.get_nodes_by_op_type("FINNLoop") for node in loop_nodes: - node_inst = getCustomOp(node) - loop_model = node_inst.get_nodeattr("body") + node_inst = cast("FINNLoop", getCustomOp(node)) + loop_model = cast("ModelWrapper", node_inst.get_nodeattr("body")) loop_model = loop_model.transform(GiveUniqueNodeNamesRecursive(prefix=node.name)) node_inst.set_nodeattr("body", loop_model.graph) else: @@ -1074,7 +1068,7 @@ def step_generate_estimate_reports(model: ModelWrapper, cfg: DataflowBuildConfig estimate_layer_cycles = model.analysis(exp_cycles_per_layer) with (report_dir / "estimate_layer_cycles.json").open("w") as f: json.dump(estimate_layer_cycles, f, indent=2) - estimate_layer_resources = model.analysis( + estimate_layer_resources: dict[str, dict[str, int | float]] = model.analysis( partial(res_estimation, fpgapart=cfg._resolve_fpga_part()) ) estimate_layer_resources["total"] = aggregate_dict_keys(estimate_layer_resources) @@ -1167,14 +1161,14 @@ def step_minimize_bit_width(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo model = model.transform(SetExecMode("cppsim"), apply_to_subgraphs=True) # Set iteration context path on FINNLoop nodes if verify_save_full_context is enabled if cfg.verify_save_full_context: - verify_out_dir = cfg.output_dir + "/verification_output" - os.makedirs(verify_out_dir, exist_ok=True) + verify_out_dir = Path(cfg.output_dir) / "verification_output" + verify_out_dir.mkdir(parents=True, exist_ok=True) for loop_node in model.get_nodes_by_op_type("FINNLoop"): loop_inst = getCustomOp(loop_node) - ctx_path = os.path.join( - verify_out_dir, f"iteration_context_{loop_node.name}_folded_hls_cppsim.npz" + ctx_path = ( + verify_out_dir / f"iteration_context_{loop_node.name}_folded_hls_cppsim.npz" ) - loop_inst.set_nodeattr("iteration_context_path", ctx_path) + loop_inst.set_nodeattr("iteration_context_path", str(ctx_path)) verify_step(model, cfg, "folded_hls_cppsim", need_parent=True) # Clear iteration_context_path after verification if cfg.verify_save_full_context: @@ -1191,7 +1185,7 @@ def step_insert_dwc(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapp return model.transform(SpecializeLayers(cfg._resolve_fpga_part())) -def verify_mlo(model: ModelWrapper, cfg: DataflowBuildConfig, step: str): +def verify_mlo(model: ModelWrapper, cfg: DataflowBuildConfig, step: str) -> None: # noqa: ARG001 """Verify a multi-layer offload model via RTL simulation.""" finn_loop = model.get_nodes_by_op_type("FINNLoop") # TODO: allow for multiple FINNLoops @@ -1272,16 +1266,16 @@ def step_create_stitched_ip(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo verify_model = verify_model.transform(AnnotateCycles()) liveness = get_liveness_threshold_cycles() perf = verify_model.analysis(dataflow_performance) - latency = perf["critical_path_cycles"] + latency = cast("int", perf["critical_path_cycles"]) max_iters = max(liveness, int(np.ceil(latency * 1.1 + 20))) os.environ["LIVENESS_THRESHOLD"] = str(max_iters) if cfg.verify_save_rtlsim_waveforms: - verify_out_dir = cfg.output_dir + "/verification_output" - waveform_dir = verify_out_dir + "/stitched_ip_rtlsim_waveforms" - os.makedirs(waveform_dir, exist_ok=True) - abspath = os.path.abspath(waveform_dir) - verify_model.set_metadata_prop("rtlsim_trace", abspath + "/verify_rtlsim.wdb") + verify_out_dir = Path(cfg.output_dir) / "verification_output" + waveform_dir = verify_out_dir / "stitched_ip_rtlsim_waveforms" + waveform_dir.mkdir(parents=True, exist_ok=True) + abspath = waveform_dir.absolute() + verify_model.set_metadata_prop("rtlsim_trace", str(abspath / "verify_rtlsim.wdb")) if is_mlo(model): verify_mlo(verify_model, cfg, "stitched_ip_rtlsim") else: @@ -1439,17 +1433,22 @@ def step_out_of_context_synthesis(model: ModelWrapper, cfg: DataflowBuildConfig) model = model.transform( SynthOutOfContext(part=cfg._resolve_fpga_part(), clk_period_ns=cfg.synth_clk_period_ns) ) - report_dir = cfg.output_dir + "/report" - os.makedirs(report_dir, exist_ok=True) + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) ooc_res_dict = model.get_metadata_prop("res_total_ooc_synth") + if ooc_res_dict is None: + raise FINNUserError( + "Out-of-context synthesis results not found in model metadata. " + "Did the OOC synthesis step fail? Check the logs." + ) ooc_res_dict = eval(ooc_res_dict) estimate_network_performance = model.analysis(dataflow_performance) # add some more metrics to estimated performance n_clock_cycles_per_sec = float(ooc_res_dict["fmax_mhz"]) * (10**6) - est_fps = n_clock_cycles_per_sec / estimate_network_performance["max_cycles"] + est_fps = n_clock_cycles_per_sec / cast("int", estimate_network_performance["max_cycles"]) ooc_res_dict["estimated_throughput_fps"] = est_fps - with open(report_dir + "/ooc_synth_and_timing.json", "w") as f: + with (report_dir / "ooc_synth_and_timing.json").open("w") as f: json.dump(ooc_res_dict, f, indent=2) else: @@ -1466,10 +1465,10 @@ def step_vivado_power_estimation(model: ModelWrapper, cfg: DataflowBuildConfig) if DataflowOutputType.OOC_SYNTH not in cfg.generate_outputs: raise FINNUserError("Vivado power estimation needs OOC synth") - report_dir = cfg.output_dir + "/report" + report_dir = Path(cfg.output_dir) / "report" model.transform( VivadoPowerEstimation( - report_dir, + str(report_dir), cfg.synth_clk_period_ns, cfg.vivado_power_simulate_activity, cfg.vivado_power_simulation_type, @@ -1483,12 +1482,16 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo """Synthesize a bitfile for the using the specified shell flow, using either Vivado or Vitis, to target the specified board.""" if DataflowOutputType.BITFILE in cfg.generate_outputs: - bitfile_dir = cfg.output_dir + "/bitfile" - os.makedirs(bitfile_dir, exist_ok=True) - report_dir = cfg.output_dir + "/report" - os.makedirs(report_dir, exist_ok=True) - partition_model_dir = cfg.output_dir + "/intermediate_models/kernel_partitions" + bitfile_dir = Path(cfg.output_dir) / "bitfile" + bitfile_dir.mkdir(parents=True, exist_ok=True) + report_dir = Path(cfg.output_dir) / "report" + report_dir.mkdir(parents=True, exist_ok=True) + partition_model_dir = Path(cfg.output_dir) / "intermediate_models" / "kernel_partitions" if cfg.shell_flow_type == ShellFlowType.VIVADO_ZYNQ: + if cfg.instrumentation_no_dma is None: + raise FINNUserError( + "instrumentation_no_dma must be set in the config for Vivado Zynq flow" + ) model = model.transform( ZynqBuild( cfg.board, @@ -1503,26 +1506,46 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo ) ) - bitfile_path = os.path.join(bitfile_dir, "finn-accel.bit") - copy(model.get_metadata_prop("bitfile"), bitfile_path) - copy(model.get_metadata_prop("hw_handoff"), bitfile_dir + "/finn-accel.hwh") + bitfile_path = bitfile_dir / "finn-accel.bit" + bitfile_src = model.get_metadata_prop("bitfile") + if bitfile_src is None: + raise FINNUserError( + "Bitfile path not found in model metadata. " + "Did the Vivado synthesis step fail? Check the logs." + ) + hwh_src = model.get_metadata_prop("hw_handoff") + if hwh_src is None: + raise FINNUserError( + "HWH path not found in model metadata. " + "Did the Vivado synthesis step fail? Check the logs." + ) + rpt_dir = model.get_metadata_prop("vivado_synth_rpt") + if rpt_dir is None: + raise FINNUserError( + "Vivado synthesis report path not found in model metadata. " + "Did the Vivado synthesis step fail? Check the logs." + ) + copy(Path(bitfile_src), bitfile_path) + copy(Path(hwh_src), bitfile_dir / "finn-accel.hwh") copy( - model.get_metadata_prop("vivado_synth_rpt"), - report_dir + "/post_synth_resources.xml", + Path(rpt_dir), + report_dir / "/post_synth_resources.xml", ) - model.set_metadata_prop("bitfile_output", os.path.abspath(bitfile_path)) + model.set_metadata_prop("bitfile_output", str(bitfile_path.absolute())) post_synth_resources = model.analysis(post_synth_res) - with open(report_dir + "/post_synth_resources.json", "w") as f: + with (report_dir / "post_synth_resources.json").open("w") as f: json.dump(post_synth_resources, f, indent=2) vivado_pynq_proj_dir = model.get_metadata_prop("vivado_pynq_proj") timing_rpt = ( - "%s/finn_zynq_link.runs/impl_1/top_wrapper_timing_summary_routed.rpt" - % vivado_pynq_proj_dir + Path(f"{vivado_pynq_proj_dir}") + / "finn_zynq_link.runs" + / "impl_1" + / "top_wrapper_timing_summary_routed.rpt" ) - copy(timing_rpt, report_dir + "/post_route_timing.rpt") + copy(timing_rpt, report_dir / "post_route_timing.rpt") elif cfg.shell_flow_type == ShellFlowType.VITIS_ALVEO: model = model.transform( @@ -1538,17 +1561,29 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo ) ) - bitfile_path = os.path.join(bitfile_dir, "finn-accel.xclbin") - copy(model.get_metadata_prop("bitfile"), bitfile_path) + bitfile_path = bitfile_dir / "finn-accel.xclbin" + bitfile_src = model.get_metadata_prop("bitfile") + if bitfile_src is None: + raise FINNUserError( + "Bitfile path not found in model metadata. " + "Did the Vitis synthesis step fail? Check the logs." + ) + rpt_dir = model.get_metadata_prop("vivado_synth_rpt") + if rpt_dir is None: + raise FINNUserError( + "Vivado synthesis report path not found in model metadata. " + "Did the Vitis synthesis step fail? Check the logs." + ) + copy(Path(bitfile_src), bitfile_path) copy( - model.get_metadata_prop("vivado_synth_rpt"), - report_dir + "/post_synth_resources.xml", + Path(rpt_dir), + report_dir / "post_synth_resources.xml", ) - model.set_metadata_prop("bitfile_output", os.path.abspath(bitfile_path)) + model.set_metadata_prop("bitfile_output", str(bitfile_path.absolute())) post_synth_resources = model.analysis(post_synth_res) - with open(report_dir + "/post_synth_resources.json", "w") as f: + with (report_dir / "post_synth_resources.json").open("w") as f: json.dump(post_synth_resources, f, indent=2) else: raise Exception("Unrecognized shell_flow_type: " + str(cfg.shell_flow_type)) @@ -1589,7 +1624,7 @@ def step_deployment_package(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo @register_build_dataflow_step() -def step_loop_rolling(model, cfg): +def step_loop_rolling(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWrapper: """Roll a repeating sequence of layers into a loop. PyTorch metadata node hierarchy is used to indicate the loop structure.""" if cfg.mlo: @@ -1610,7 +1645,7 @@ def step_loop_rolling(model, cfg): loop_extraction = LoopExtraction(cfg.loop_body_hierarchy) model = model.transform(loop_extraction) model = model.transform(LoopRolling(loop_extraction.loop_body_template)) - move("loop-body-template.onnx", cfg.output_dir + "/loop-body-template.onnx") + move("loop-body-template.onnx", Path(cfg.output_dir) / "loop-body-template.onnx") else: log.info("MLO not selected, skipping step_loop_rolling.") diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index 4d0412b646..3987e0d552 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -1,9 +1,14 @@ """Manage FINNs testsuite.""" + import os +import re import shlex import subprocess import sys +from dataclasses import dataclass +from junitparser import JUnitXml, TestCase from pathlib import Path +from re import Pattern from finn.interface import IS_POSIX from finn.interface.interface_utils import status @@ -66,8 +71,7 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: case "doctest": if name == "": status( - "No test name was specified, running " - "doctests on all relevant FINN submodules." + "No test name was specified, running doctests on all relevant FINN submodules." ) run_doctests(int(num_workers)) return @@ -106,26 +110,152 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: ) ) case "full_ci": + main_xml = f"{ci_project_dir}/reports/main.xml" + main_html = f"{ci_project_dir}/reports/main.html" + crash_xml = f"{ci_project_dir}/reports/crash_rerun.xml" + crash_html = f"{ci_project_dir}/reports/crash_rerun.html" + end2end_xml = f"{ci_project_dir}/reports/end2end.xml" + # end2end_html = f"{ci_project_dir}/reports/end2end.html" + + crash_re: Pattern[str] = re.compile( + r"(worker.*crash|worker.*terminated|segmentation fault|sigsegv|signal 11|fatal python error)", # noqa + re.IGNORECASE, + ) + + @dataclass(frozen=True) + class CrashDetectionResult: + """Result of scanning a JUnit XML report for crash-like and non-crash failures.""" + + crashed_nodeids: list[str] + has_non_crash_failures: bool + + def make_nodeid(case: TestCase) -> str: + """Return pytest-like nodeid from junit testcase.""" + classname: str = getattr(case, "classname", "") or "" + name: str = getattr(case, "name", "") or "" + return f"{classname}::{name}" if classname else name + + def detect_crashed_nodeids(junit_xml_path: str) -> CrashDetectionResult: + """Parse a JUnit XML file and classify failing testcases. + + A testcase is considered: + - crash-like failure: if any failure/error message matches CRASH_RE + - non-crash failure: failure/error exists but no crash marker matched + + Skipped/passed cases are ignored for failure classification. + + Args: + junit_xml_path: Path to JUnit XML file. + + Returns: + CrashDetectionResult with: + - crashed_nodeids: deduplicated list of crash-like testcase nodeids + - has_non_crash_failures: True if any failure/error is non-crash-like + """ + xml = JUnitXml.fromfile(junit_xml_path) + + crashed_nodeids: list[str] = [] + seen: set[str] = set() + has_non_crash_failures = False + + for suite in xml: + for case in suite: + # Collect failure/error blocks only (ignore skipped) + failure_blocks: list[str] = [] + for res in case.result: + tag = getattr(res, "_tag", "") + if tag not in {"failure", "error"}: + continue + msg: str = getattr(res, "message", "") or "" + txt: str = getattr(res, "text", "") or "" + failure_blocks.append(f"{msg}\n{txt}".strip()) + + if not failure_blocks: + continue # passed or skipped-only + + combined_failure_text = "\n".join(failure_blocks) + is_crash_like = bool(crash_re.search(combined_failure_text)) + + if is_crash_like: + nodeid = make_nodeid(case) + if nodeid and nodeid not in seen: + seen.add(nodeid) + crashed_nodeids.append(nodeid) + else: + has_non_crash_failures = True + + return CrashDetectionResult( + crashed_nodeids=crashed_nodeids, + has_non_crash_failures=has_non_crash_failures, + ) + + # -------------------------- + # 1) Main suite + # -------------------------- + test_1_process = subprocess.Popen( shlex.split( ( f"{sys.executable} -m pytest -v -m 'not " f"(end2end or sanity_bnn or notebooks)' " - f"--junitxml={ci_project_dir}/reports/main.xml " - f"--html={ci_project_dir}/reports/main.html " - f"--reruns 1 --dist loadscope -n {num_workers}" + f"--junitxml={main_xml} " + f"--html={main_html} " + f"--reruns 1 --dist worksteal -n {num_workers}" ), posix=IS_POSIX, ) ) test_1_process.communicate() test_1_returncode = test_1_process.returncode + + # -------------------------- + # 2) Detect crashed tests + # -------------------------- + crashed_tests: list[str] = [] + if Path(main_xml).exists(): + try: + result = detect_crashed_nodeids(main_xml) + crashed_tests = result.crashed_nodeids + has_non_crash_failures = result.has_non_crash_failures + test_1_returncode = 1 if has_non_crash_failures else 0 + except Exception as exc: + print(f"[WARN] Failed to parse {main_xml}: {exc}") + else: + print(f"[WARN] Main XML not found: {main_xml}") + + print(f"[INFO] Crashed tests detected: {len(crashed_tests)}") + + # -------------------------- + # 3) Rerun only crashed tests + # -------------------------- + test_2_returncode = 0 + if crashed_tests: + nodeids = " ".join(shlex.quote(t) for t in crashed_tests) + rerun_cmd = ( + f"{sys.executable} -m pytest -v " + f"--junitxml={shlex.quote(crash_xml)} " + f"--html={shlex.quote(crash_html)} " + f"--reruns 3 -n 1 " + f"{nodeids}" + ) + test_2_process = subprocess.Popen( + shlex.split( + rerun_cmd, + posix=IS_POSIX, + ) + ) + test_2_process.communicate() + test_2_returncode = test_2_process.returncode + else: + print("[INFO] No crash-like tests to rerun.") + main_returncode = test_1_returncode or test_2_returncode + # test_2_process = subprocess.Popen( # shlex.split( # ( # f"{sys.executable} -m pytest -v -m 'end2end or sanity_bnn or notebooks' " - # f"--junitxml={ci_project_dir}/reports/end2end.xml " - # f"--html={ci_project_dir}/reports/end2end.html " + # f"--junitxml={end2end_xml} " + # f"--html={end2end_html} " # f"--reruns 1 --dist loadgroup -n {num_workers}" # ), # posix=IS_POSIX, @@ -137,17 +267,32 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: # Run doctests for all FINN submodules # test_3_returncode = run_doctests(int(num_workers)) - # subprocess.run( - # shlex.split( - # ( - # f"{sys.executable} -m pytest_html_merger -i {ci_project_dir}/reports/ " - # f"-o {ci_project_dir}/reports/full_test_suite.html" - # ), - # posix=IS_POSIX, - # ) - # ) + subprocess.run( + shlex.split( + ( + f"{sys.executable} -m pytest_html_merger -i {ci_project_dir}/reports/ " + f"-o {ci_project_dir}/reports/full_test_suite.html" + ), + posix=IS_POSIX, + ) + ) + script_dir = Path(__file__).parent.parent.resolve() / "scripts" / "merge_xml_reports.py" + subprocess.run( + shlex.split( + ( + f"{sys.executable} {script_dir} " + f"-o {ci_project_dir}/reports/full_test_suite.xml " + f"{main_xml} {crash_xml} {end2end_xml}" + ), + posix=IS_POSIX, + ) + ) + # Remove individual XML reports to avoid confusion with the merged report + for xml_file in [main_xml, crash_xml, end2end_xml]: + if Path(xml_file).exists(): + Path(xml_file).unlink() - if test_1_returncode: # or test_2_returncode or test_3_returncode: + if main_returncode: # or test_2_returncode or test_3_returncode: sys.exit(1) case _: diff --git a/src/finn/transformation/fpgadataflow/vivado_power_estimation.py b/src/finn/transformation/fpgadataflow/vivado_power_estimation.py index 238f487a7f..654e92b23b 100644 --- a/src/finn/transformation/fpgadataflow/vivado_power_estimation.py +++ b/src/finn/transformation/fpgadataflow/vivado_power_estimation.py @@ -23,7 +23,7 @@ class VivadoPowerEstimation(Transformation): def __init__( self, report_dir, - clk_period_ns=10, + clk_period_ns=10.0, simulate_switching_activity=True, vivado_power_simulation_type="functional", ): diff --git a/tests/fpgadataflow/test_fifosizing.py b/tests/fpgadataflow/test_fifosizing.py index 7a9c104117..63325e681f 100644 --- a/tests/fpgadataflow/test_fifosizing.py +++ b/tests/fpgadataflow/test_fifosizing.py @@ -175,57 +175,214 @@ def test_fifosizing_linear(topology: Literal["tfc", "cnv"]) -> None: ) build.build_dataflow_cfg(str(tmp_output_dir / "model.onnx"), cfg) - expected_fifos = { - "fifo_depths": { - "StreamingFIFO_rtl_0": 2, - "StreamingFIFO_rtl_1": 32, - "StreamingFIFO_rtl_2": 32, - "StreamingFIFO_rtl_3": 32, - "StreamingFIFO_rtl_4": 32, - "StreamingFIFO_rtl_5": 32, - "StreamingFIFO_rtl_6": 32, - "StreamingFIFO_rtl_7": 32, - "StreamingFIFO_rtl_8": 32, - "StreamingFIFO_rtl_9": 32, - }, - "fifo_sizes": { - "StreamingFIFO_rtl_0": 1024, - "StreamingFIFO_rtl_1": 1024, - "StreamingFIFO_rtl_2": 64, - "StreamingFIFO_rtl_3": 448, - "StreamingFIFO_rtl_4": 64, - "StreamingFIFO_rtl_5": 64, - "StreamingFIFO_rtl_6": 64, - "StreamingFIFO_rtl_7": 256, - "StreamingFIFO_rtl_8": 1024, - "StreamingFIFO_rtl_9": 1024, - }, - "impl_style": { - "StreamingFIFO_rtl_0": "rtl", - "StreamingFIFO_rtl_1": "rtl", - "StreamingFIFO_rtl_2": "rtl", - "StreamingFIFO_rtl_3": "rtl", - "StreamingFIFO_rtl_4": "rtl", - "StreamingFIFO_rtl_5": "rtl", - "StreamingFIFO_rtl_6": "rtl", - "StreamingFIFO_rtl_7": "rtl", - "StreamingFIFO_rtl_8": "rtl", - "StreamingFIFO_rtl_9": "rtl", - }, - "ram_style": { - "StreamingFIFO_rtl_0": "block", - "StreamingFIFO_rtl_1": "block", - "StreamingFIFO_rtl_2": "block", - "StreamingFIFO_rtl_3": "block", - "StreamingFIFO_rtl_4": "block", - "StreamingFIFO_rtl_5": "block", - "StreamingFIFO_rtl_6": "block", - "StreamingFIFO_rtl_7": "block", - "StreamingFIFO_rtl_8": "block", - "StreamingFIFO_rtl_9": "block", - }, - "total_fifo_size_kiB": 0.6171875, - } + if topology == "tfc": + expected_fifos = { + "fifo_depths": { + "StreamingFIFO_rtl_0": 2, + "StreamingFIFO_rtl_1": 32, + "StreamingFIFO_rtl_2": 32, + "StreamingFIFO_rtl_3": 32, + "StreamingFIFO_rtl_4": 32, + "StreamingFIFO_rtl_5": 32, + "StreamingFIFO_rtl_6": 32, + "StreamingFIFO_rtl_7": 32, + "StreamingFIFO_rtl_8": 32, + "StreamingFIFO_rtl_9": 32, + }, + "fifo_sizes": { + "StreamingFIFO_rtl_0": 1024, + "StreamingFIFO_rtl_1": 1024, + "StreamingFIFO_rtl_2": 64, + "StreamingFIFO_rtl_3": 448, + "StreamingFIFO_rtl_4": 64, + "StreamingFIFO_rtl_5": 64, + "StreamingFIFO_rtl_6": 64, + "StreamingFIFO_rtl_7": 256, + "StreamingFIFO_rtl_8": 1024, + "StreamingFIFO_rtl_9": 1024, + }, + "impl_style": { + "StreamingFIFO_rtl_0": "rtl", + "StreamingFIFO_rtl_1": "rtl", + "StreamingFIFO_rtl_2": "rtl", + "StreamingFIFO_rtl_3": "rtl", + "StreamingFIFO_rtl_4": "rtl", + "StreamingFIFO_rtl_5": "rtl", + "StreamingFIFO_rtl_6": "rtl", + "StreamingFIFO_rtl_7": "rtl", + "StreamingFIFO_rtl_8": "rtl", + "StreamingFIFO_rtl_9": "rtl", + }, + "ram_style": { + "StreamingFIFO_rtl_0": "block", + "StreamingFIFO_rtl_1": "block", + "StreamingFIFO_rtl_2": "block", + "StreamingFIFO_rtl_3": "block", + "StreamingFIFO_rtl_4": "block", + "StreamingFIFO_rtl_5": "block", + "StreamingFIFO_rtl_6": "block", + "StreamingFIFO_rtl_7": "block", + "StreamingFIFO_rtl_8": "block", + "StreamingFIFO_rtl_9": "block", + }, + "total_fifo_size_kiB": 0.6171875, + } + else: + expected_fifos = { + "fifo_depths": { + "StreamingFIFO_rtl_0": 2, + "StreamingFIFO_rtl_1": 32, + "StreamingFIFO_rtl_2": 32, + "StreamingFIFO_rtl_3": 32, + "StreamingFIFO_rtl_4": 32, + "StreamingFIFO_rtl_5": 1024, + "StreamingFIFO_rtl_6": 32, + "StreamingFIFO_rtl_7": 32, + "StreamingFIFO_rtl_8": 32, + "StreamingFIFO_rtl_9": 32, + "StreamingFIFO_rtl_10": 32, + "StreamingFIFO_rtl_11": 32, + "StreamingFIFO_rtl_12": 32, + "StreamingFIFO_rtl_13": 512, + "StreamingFIFO_rtl_14": 32, + "StreamingFIFO_rtl_15": 32, + "StreamingFIFO_rtl_16": 32, + "StreamingFIFO_rtl_17": 32, + "StreamingFIFO_rtl_18": 32, + "StreamingFIFO_rtl_19": 32, + "StreamingFIFO_rtl_20": 32, + "StreamingFIFO_rtl_21": 32, + "StreamingFIFO_rtl_22": 32, + "StreamingFIFO_rtl_23": 512, + "StreamingFIFO_rtl_24": 32, + "StreamingFIFO_rtl_25": 32, + "StreamingFIFO_rtl_26": 32, + "StreamingFIFO_rtl_27": 32, + "StreamingFIFO_rtl_28": 32, + "StreamingFIFO_rtl_29": 32, + "StreamingFIFO_rtl_30": 32, + "StreamingFIFO_rtl_31": 32, + "StreamingFIFO_rtl_32": 32, + "StreamingFIFO_rtl_33": 32, + "StreamingFIFO_rtl_34": 32, + "StreamingFIFO_rtl_35": 32, + }, + "fifo_sizes": { + "StreamingFIFO_rtl_0": 1024, + "StreamingFIFO_rtl_1": 256, + "StreamingFIFO_rtl_2": 256, + "StreamingFIFO_rtl_3": 6912, + "StreamingFIFO_rtl_4": 64, + "StreamingFIFO_rtl_5": 16384, + "StreamingFIFO_rtl_6": 512, + "StreamingFIFO_rtl_7": 1152, + "StreamingFIFO_rtl_8": 2048, + "StreamingFIFO_rtl_9": 64, + "StreamingFIFO_rtl_10": 64, + "StreamingFIFO_rtl_11": 64, + "StreamingFIFO_rtl_12": 64, + "StreamingFIFO_rtl_13": 18432, + "StreamingFIFO_rtl_14": 512, + "StreamingFIFO_rtl_15": 128, + "StreamingFIFO_rtl_16": 128, + "StreamingFIFO_rtl_17": 1152, + "StreamingFIFO_rtl_18": 1024, + "StreamingFIFO_rtl_19": 64, + "StreamingFIFO_rtl_20": 64, + "StreamingFIFO_rtl_21": 64, + "StreamingFIFO_rtl_22": 64, + "StreamingFIFO_rtl_23": 18432, + "StreamingFIFO_rtl_24": 128, + "StreamingFIFO_rtl_25": 64, + "StreamingFIFO_rtl_26": 64, + "StreamingFIFO_rtl_27": 384, + "StreamingFIFO_rtl_28": 64, + "StreamingFIFO_rtl_29": 128, + "StreamingFIFO_rtl_30": 64, + "StreamingFIFO_rtl_31": 256, + "StreamingFIFO_rtl_32": 64, + "StreamingFIFO_rtl_33": 352, + "StreamingFIFO_rtl_34": 1024, + "StreamingFIFO_rtl_35": 1024, + }, + "impl_style": { + "StreamingFIFO_rtl_0": "rtl", + "StreamingFIFO_rtl_1": "rtl", + "StreamingFIFO_rtl_2": "rtl", + "StreamingFIFO_rtl_3": "rtl", + "StreamingFIFO_rtl_4": "rtl", + "StreamingFIFO_rtl_5": "vivado", + "StreamingFIFO_rtl_6": "rtl", + "StreamingFIFO_rtl_7": "rtl", + "StreamingFIFO_rtl_8": "rtl", + "StreamingFIFO_rtl_9": "rtl", + "StreamingFIFO_rtl_10": "rtl", + "StreamingFIFO_rtl_11": "rtl", + "StreamingFIFO_rtl_12": "rtl", + "StreamingFIFO_rtl_13": "vivado", + "StreamingFIFO_rtl_14": "rtl", + "StreamingFIFO_rtl_15": "rtl", + "StreamingFIFO_rtl_16": "rtl", + "StreamingFIFO_rtl_17": "rtl", + "StreamingFIFO_rtl_18": "rtl", + "StreamingFIFO_rtl_19": "rtl", + "StreamingFIFO_rtl_20": "rtl", + "StreamingFIFO_rtl_21": "rtl", + "StreamingFIFO_rtl_22": "rtl", + "StreamingFIFO_rtl_23": "vivado", + "StreamingFIFO_rtl_24": "rtl", + "StreamingFIFO_rtl_25": "rtl", + "StreamingFIFO_rtl_26": "rtl", + "StreamingFIFO_rtl_27": "rtl", + "StreamingFIFO_rtl_28": "rtl", + "StreamingFIFO_rtl_29": "rtl", + "StreamingFIFO_rtl_30": "rtl", + "StreamingFIFO_rtl_31": "rtl", + "StreamingFIFO_rtl_32": "rtl", + "StreamingFIFO_rtl_33": "rtl", + "StreamingFIFO_rtl_34": "rtl", + "StreamingFIFO_rtl_35": "rtl", + }, + "ram_style": { + "StreamingFIFO_rtl_0": "block", + "StreamingFIFO_rtl_1": "block", + "StreamingFIFO_rtl_2": "block", + "StreamingFIFO_rtl_3": "block", + "StreamingFIFO_rtl_4": "block", + "StreamingFIFO_rtl_5": "block", + "StreamingFIFO_rtl_6": "block", + "StreamingFIFO_rtl_7": "block", + "StreamingFIFO_rtl_8": "block", + "StreamingFIFO_rtl_9": "block", + "StreamingFIFO_rtl_10": "block", + "StreamingFIFO_rtl_11": "block", + "StreamingFIFO_rtl_12": "block", + "StreamingFIFO_rtl_13": "block", + "StreamingFIFO_rtl_14": "block", + "StreamingFIFO_rtl_15": "block", + "StreamingFIFO_rtl_16": "block", + "StreamingFIFO_rtl_17": "block", + "StreamingFIFO_rtl_18": "block", + "StreamingFIFO_rtl_19": "block", + "StreamingFIFO_rtl_20": "block", + "StreamingFIFO_rtl_21": "block", + "StreamingFIFO_rtl_22": "block", + "StreamingFIFO_rtl_23": "block", + "StreamingFIFO_rtl_24": "block", + "StreamingFIFO_rtl_25": "block", + "StreamingFIFO_rtl_26": "block", + "StreamingFIFO_rtl_27": "block", + "StreamingFIFO_rtl_28": "block", + "StreamingFIFO_rtl_29": "block", + "StreamingFIFO_rtl_30": "block", + "StreamingFIFO_rtl_31": "block", + "StreamingFIFO_rtl_32": "block", + "StreamingFIFO_rtl_33": "block", + "StreamingFIFO_rtl_34": "block", + "StreamingFIFO_rtl_35": "block", + }, + "total_fifo_size_kiB": 8.85546875, + } with (tmp_output_dir / "report/fifo_sizing.json").open() as f: fifo_sizing_report = json.load(f) diff --git a/tests/fpgadataflow/test_simulation_build.py b/tests/fpgadataflow/test_simulation_build.py index 18674ed468..6e54a03b1d 100644 --- a/tests/fpgadataflow/test_simulation_build.py +++ b/tests/fpgadataflow/test_simulation_build.py @@ -902,8 +902,6 @@ def test_isolated_node_model_binary_target_fifo_pre_transparency( ) builder = SimulationBuilder(model, "xc7z020clg400-1", 5.0, "test_isolated_6_") - model.save("/scratch/pc2-mitarbeiter/linusjun/finn-tmp/source_model.onnx") - isolated = _isolate_node_model(builder, "target_add") _assert_isolated_model( diff --git a/tests/pyproject.toml b/tests/pyproject.toml index 092ebe68d4..6c45229f78 100644 --- a/tests/pyproject.toml +++ b/tests/pyproject.toml @@ -40,6 +40,7 @@ pytest-xdist = { version = "~3.6.1", extras = ["setproctitle"] } torch = "~2.7.1" torchvision = "~0.22.1" wget = "~3.2" +junitparser = "~5.0.1" [tool.pytest.ini_options] markers = [ From 6c443221a587f39a6a1d4a1e4938817771f41ab5 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 25 Jun 2026 13:53:04 +0200 Subject: [PATCH 159/170] Try fixing unresolved type reference error --- src/finn/builder/build_dataflow_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index e2d33d0957..9643b68120 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -56,13 +56,13 @@ from mashumaro.mixins.json import DataClassJSONMixin from mashumaro.mixins.yaml import DataClassYAMLMixin from pathlib import Path, PosixPath, PurePath -from typing import TYPE_CHECKING, Any, Literal, Optional, cast +from typing import Any, Literal, Optional, cast from finn.util.basic import alveo_default_platform, part_map from finn.util.exception import FINNConfigurationError -if TYPE_CHECKING: - from onnx import NodeProto + +from onnx import NodeProto # noqa class LogLevel(str, Enum): From f3764c67f22e7d50a852ec14045909ead7ebc321 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 25 Jun 2026 14:21:22 +0200 Subject: [PATCH 160/170] Fix serialisation error in build config --- src/finn/builder/build_dataflow_config.py | 5 +---- src/finn/builder/build_dataflow_steps.py | 8 ++++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/finn/builder/build_dataflow_config.py b/src/finn/builder/build_dataflow_config.py index 9643b68120..ec19e0a22d 100644 --- a/src/finn/builder/build_dataflow_config.py +++ b/src/finn/builder/build_dataflow_config.py @@ -62,9 +62,6 @@ from finn.util.exception import FINNConfigurationError -from onnx import NodeProto # noqa - - class LogLevel(str, Enum): """Log levels printed on the commandline for the build process.""" @@ -584,7 +581,7 @@ def _fix_path(p: Path | None) -> Path | None: #: For this node range, the PyTorch metadata hierarchy will be simulated #: TODO: this argument will be replaced or extended when there is a way #: to preserve node metadata from the PyTorch model (e.g. from dynamo exporter) - loop_body_range: Optional[tuple[NodeProto, NodeProto]] = None + loop_body_range: Optional[tuple[Any, Any]] = None #: (Only relevant if CPP_DRIVER output product is enabled) Selects C++ driver version. #: If set to "latest", newest version will be used. diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index bc7df2a619..a870d86fed 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -40,6 +40,7 @@ from collections.abc import Callable from copy import deepcopy from functools import partial +from onnx import NodeProto from pathlib import Path from qonnx.core.modelwrapper import ModelWrapper from qonnx.custom_op.registry import getCustomOp @@ -134,7 +135,6 @@ from finn.util.execution import execute_parent from finn.util.logging import log from finn.util.mlo_sim import is_mlo, mlo_prehook_func_factory - from finn.xsi import SimEngine if TYPE_CHECKING: @@ -1628,7 +1628,11 @@ def step_loop_rolling(model: ModelWrapper, cfg: DataflowBuildConfig) -> ModelWra """Roll a repeating sequence of layers into a loop. PyTorch metadata node hierarchy is used to indicate the loop structure.""" if cfg.mlo: - if cfg.loop_body_range is not None: + if ( + cfg.loop_body_range is not None + and isinstance(cfg.loop_body_range[0], NodeProto) + and isinstance(cfg.loop_body_range[1], NodeProto) + ): # set node metadata like loop rolling would expect node_metadata = { "pkg.torch.onnx.name_scopes": "['', 'layers.0']", From e2aa37833623e61278bb1f322cfa46f21162fd2f Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:26:32 +0200 Subject: [PATCH 161/170] Make TLastMarker compatible with distributed rtlsim --- scripts/merge_xml_reports.py | 3 + .../fpgadataflow/hls/tlastmarker_hls.py | 221 +++++++++++++----- src/finn/interface/manage_tests.py | 42 ++-- .../fpgadataflow/insert_fifo.py | 10 +- .../fpgadataflow/insert_tlastmarker.py | 51 +++- 5 files changed, 231 insertions(+), 96 deletions(-) diff --git a/scripts/merge_xml_reports.py b/scripts/merge_xml_reports.py index a91352fb38..3aa38e572b 100755 --- a/scripts/merge_xml_reports.py +++ b/scripts/merge_xml_reports.py @@ -15,6 +15,7 @@ from collections.abc import Iterable from junitparser import JUnitXml, TestSuite from junitparser.junitparser import TestCase +from pathlib import Path from typing import Literal TestStatus = Literal["passed", "failed", "skipped", "unknown"] @@ -103,6 +104,8 @@ def merge_reports(inputs: Iterable[str], output: str) -> None: by_key: OrderedDict[TestKey, TestCase] = OrderedDict() for path in inputs: + if not Path(path).is_file(): + continue xml = JUnitXml.fromfile(path) for suite in xml: for tc in suite: diff --git a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py index 8f0221f552..0997c0c754 100644 --- a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py @@ -28,8 +28,21 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Module for tlastmarker hls.""" + +import numpy as np +import numpy.typing as npt +from collections.abc import Sequence +from onnx import NodeProto +from qonnx.core.datatype import BaseDataType, DataType +from typing import TYPE_CHECKING, Any, Literal, cast + from finn.custom_op.fpgadataflow.hlsbackend import HLSBackend from finn.custom_op.fpgadataflow.hwcustomop import HWCustomOp +from finn.util.exception import FINNInternalError + +if TYPE_CHECKING: + from onnx import GraphProto + from qonnx.core.modelwrapper import ModelWrapper class TLastMarker_hls(HLSBackend, HWCustomOp): @@ -40,13 +53,27 @@ class TLastMarker_hls(HLSBackend, HWCustomOp): (needed by the FINN PYNQ shell) or at the beginning to remove the end-of-burst from DMA read.""" - def __init__(self, onnx_node, **kwargs): + def __init__(self, onnx_node: "NodeProto", **kwargs: Any) -> None: """Initialize instance.""" super().__init__(onnx_node, **kwargs) - def get_nodeattr_types(self): + def get_nodeattr_types( + self, + ) -> dict[ + str, + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], + ]: """Return nodeattr types.""" - my_attrs = { + my_attrs: dict[ + str, + tuple[str, bool, int | float | str | bool | npt.NDArray | list] + | tuple[str, bool, int | float | str | bool | npt.NDArray | list, set | None], + ] = { + # normal shape of input/output + "normal_shape": ("ints", True, []), + # FINN DataTypes for inputs/outputs + "dataType": ("s", True, ""), # number of (static) iterations until TLAST=1 is generated for Direction=out "NumIters": ("i", True, 0), # whether static or dynamic (from AXI lite) number of iterations are used @@ -65,7 +92,9 @@ def get_nodeattr_types(self): my_attrs.update(HLSBackend.get_nodeattr_types(self)) return my_attrs - def execute_node(self, context, graph): + def execute_node( + self, context: dict[str, np.ndarray], graph: "GraphProto" + ) -> None: # noqa: ARG002 # TLastMarker's behavior is only visible when doing # rtlsim with stitched IP, since it marks the end # of the current image/input sample. when executing @@ -77,21 +106,16 @@ def execute_node(self, context, graph): i_tensor = context[i_name] context[o_name] = i_tensor - def make_shape_compatible_op(self, model): + def make_shape_compatible_op(self, model: "ModelWrapper") -> NodeProto: # not supported for shape inference """Create shape compatible op.""" - pass + return super().make_shape_compatible_op(model) - def infer_node_datatype(self, model): - # not supported for datatype inference - """Infer node datatype.""" - pass - - def global_includes(self): + def global_includes(self) -> None: """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "ap_axi_sdata.h"'] - def defines(self, var): + def defines(self, var: str) -> None: # noqa: ARG002 """Return defines.""" stream_width = self.get_nodeattr("StreamWidth") direction = self.get_nodeattr("Direction") @@ -100,35 +124,35 @@ def defines(self, var): # qdma_axis if direction == "out": if protocol == "external": - out_stream_dtype = "qdma_axis<%d,0,0,0>" % stream_width + out_stream_dtype = f"qdma_axis<{stream_width},0,0,0>" elif protocol == "internal": - out_stream_dtype = "ap_axiu<%d,0,0,0>" % stream_width + out_stream_dtype = f"ap_axiu<{stream_width},0,0,0>" else: raise Exception("Unrecognized Protocol in TLastMarker") - in_stream_dtype = "ap_uint<%d>" % stream_width + in_stream_dtype = f"ap_uint<{stream_width}>" elif direction == "in": - out_stream_dtype = "ap_uint<%d>" % stream_width + out_stream_dtype = f"ap_uint<{stream_width}>" if protocol == "external": - in_stream_dtype = "qdma_axis<%d,0,0,0>" % stream_width + in_stream_dtype = f"qdma_axis<{stream_width},0,0,0>" elif protocol == "internal": - in_stream_dtype = "ap_axiu<%d,0,0,0>" % stream_width + in_stream_dtype = f"ap_axiu<{stream_width},0,0,0>" else: raise Exception("Unrecognized Protocol in TLastMarker") else: raise Exception("Unrecognized Direction in TLastMarker") self.code_gen_dict["$DEFINES$"] = [ - "#define StreamWidth %d" % stream_width, - "#define OutDType %s" % out_stream_dtype, - "#define InDType %s" % in_stream_dtype, - "#define NumItersPerImg %d" % self.get_nodeattr("NumIters"), + f"#define StreamWidth {stream_width}", + f"#define OutDType {out_stream_dtype}", + f"#define InDType {in_stream_dtype}", + f"#define NumItersPerImg {self.get_nodeattr('NumIters')}", ] - def read_npy_data(self): + def read_npy_data(self) -> None: """Return read npy data.""" self.code_gen_dict["$READNPYDATA$"] = [] - def docompute(self): + def docompute(self) -> None: """Return docompute.""" dyn_iters = self.get_nodeattr("DynIters") direction = self.get_nodeattr("Direction") @@ -184,28 +208,26 @@ def docompute(self): "}", ] - def dataoutstrm(self): + def dataoutstrm(self) -> None: """Return dataoutstrm.""" self.code_gen_dict["$DATAOUTSTREAM$"] = [] - def blackboxfunction(self): + def blackboxfunction(self) -> None: """Return blackboxfunction.""" dyn_iters = self.get_nodeattr("DynIters") if dyn_iters == 1: self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """void %s(hls::stream &in0_V, + f"""void {self.onnx_node.name}(hls::stream &in0_V, hls::stream &out0_V, unsigned int numIters)""" - % self.onnx_node.name ] else: self.code_gen_dict["$BLACKBOXFUNCTION$"] = [ - """void %s(hls::stream &in0_V, + f"""void {self.onnx_node.name}(hls::stream &in0_V, hls::stream &out0_V)""" - % self.onnx_node.name ] - def pragmas(self): + def pragmas(self) -> None: """Return pragmas.""" self.code_gen_dict["$PRAGMAS$"] = ["#pragma HLS INTERFACE axis port=in0_V"] self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE axis port=out0_V") @@ -218,53 +240,128 @@ def pragmas(self): self.code_gen_dict["$PRAGMAS$"].append("#pragma HLS INTERFACE ap_ctrl_none port=return") - def get_number_output_values(self): + def get_number_output_values(self) -> int: """Return number output values.""" - return self.get_nodeattr("NumIters") + return cast("int", self.get_nodeattr("NumIters")) + + def get_input_datatype(self, ind: int = 0) -> BaseDataType: # noqa: ARG002 + """Return the input data type. + + Args: + ind: Input index (unused, kept for interface compatibility). - def get_input_datatype(self, ind=0): - # not supported - """Return input datatype.""" - raise Exception("get_input_datatype not implemented for TlastMarker") + Returns: + The QONNX data type for the input. - def get_output_datatype(self, ind=0): - # not supported - """Return output datatype.""" - raise Exception("get_output_datatype not implemented for TlastMarker") + Raises: + FINNInternalError: If dataType attribute is invalid. - def get_normal_input_shape(self, ind=0): - # not supported - """Return normal input shape.""" - raise Exception("get_normal_input_shape not implemented for TlastMarker") + """ + dtype = self.get_nodeattr("dataType") + if type(dtype) is not str: + raise FINNInternalError( + f"dataType attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + dtype = DataType[dtype] + return dtype + + def get_output_datatype(self, ind: int = 0) -> BaseDataType: # noqa: ARG002 + """Return the output data type. + + Args: + ind: Output index (unused, kept for interface compatibility). - def get_normal_output_shape(self, ind=0): - # not supported - """Return normal output shape.""" - raise Exception("get_normal_input_shape not implemented for TlastMarker") + Returns: + The QONNX data type for the output. - def get_folded_input_shape(self, ind=0): + Raises: + FINNInternalError: If dataType attribute is invalid. + + """ + dtype = self.get_nodeattr("dataType") + if type(dtype) is not str: + raise FINNInternalError( + f"dataType attribute not set correctly in {self.onnx_node.name}, " + "cannot get outstream width" + ) + dtype = DataType[dtype] + return dtype + + def get_normal_input_shape( + self, ind: int = 0 # noqa: ARG002 + ) -> Sequence[int] | npt.NDArray[np.int_]: + """Return the normal (unfolded) input shape. + + Args: + ind: Input index (unused, kept for interface compatibility). + + Returns: + The normal input shape dimensions. + + Raises: + FINNInternalError: If normal_shape attribute is invalid or empty. + + """ + normal_shape = self.get_nodeattr("normal_shape") + if ( + type(normal_shape) is not list + and type(normal_shape) is not tuple + and not isinstance(normal_shape, np.ndarray) + ): + raise FINNInternalError( + f"normal_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get normal input shape" + ) + if len(normal_shape) == 0: + raise FINNInternalError( + f"normal_shape attribute is empty in {self.onnx_node.name}, " + "cannot get normal input shape" + ) + if not isinstance(normal_shape[0], int) and not isinstance(normal_shape[0], np.integer): + raise FINNInternalError( + f"normal_shape attribute not set correctly in {self.onnx_node.name}, " + "cannot get normal input shape" + ) + return cast("Sequence[int]|npt.NDArray[np.int_]", normal_shape) + + def get_normal_output_shape( + self, ind: int = 0 # noqa: ARG002 + ) -> Sequence[int] | npt.NDArray[np.int_]: + """Return the normal (unfolded) output shape. + + Args: + ind: Output index (unused, kept for interface compatibility). + + Returns: + Tuple containing the normal output shape dimensions. + + """ + return self.get_normal_input_shape() + + def get_folded_input_shape(self, ind: int = 0) -> tuple[Literal[1], int, int]: # noqa: ARG002 """Return folded input shape.""" - stream_width = self.get_nodeattr("StreamWidth") - elem_width = self.get_nodeattr("ElemWidth") + stream_width = cast("int", self.get_nodeattr("StreamWidth")) + elem_width = cast("int", self.get_nodeattr("ElemWidth")) n_packed_elems = stream_width // elem_width - n_iters = self.get_nodeattr("NumIters") + n_iters = cast("int", self.get_nodeattr("NumIters")) return (1, n_iters, n_packed_elems) - def get_folded_output_shape(self, ind=0): + def get_folded_output_shape(self, ind: int = 0) -> tuple[Literal[1], int, int]: # noqa: ARG002 """Return folded output shape.""" return self.get_folded_input_shape() - def get_instream_width(self, ind=0): + def get_instream_width(self, ind: int = 0) -> int: # noqa: ARG002 """Return instream width.""" - stream_width = self.get_nodeattr("StreamWidth") + stream_width = cast("int", self.get_nodeattr("StreamWidth")) return stream_width - def get_outstream_width(self, ind=0): + def get_outstream_width(self, ind: int = 0) -> int: # noqa: ARG002 """Return outstream width.""" - stream_width = self.get_nodeattr("StreamWidth") + stream_width = cast("int", self.get_nodeattr("StreamWidth")) return stream_width - def strm_decl(self): + def strm_decl(self) -> None: """Return strm decl.""" self.code_gen_dict["$STREAMDECLARATIONS$"] = [] self.code_gen_dict["$STREAMDECLARATIONS$"].append('hls::stream in0_V ("in0_V");') @@ -272,10 +369,10 @@ def strm_decl(self): 'hls::stream out0_V ("out0_V");' ) - def get_verilog_top_module_intf_names(self): + def get_verilog_top_module_intf_names(self) -> dict[str, list[tuple[str, int]] | list[str]]: """Return verilog top module intf names.""" intf_names = super().get_verilog_top_module_intf_names() - stream_width = self.get_nodeattr("StreamWidth") + stream_width = cast("int", self.get_nodeattr("StreamWidth")) intf_names["s_axis"] = [("in0_V", stream_width)] intf_names["m_axis"] = [("out0_V", stream_width)] if self.get_nodeattr("DynIters") == 1: diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index 3987e0d552..894e872385 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -115,7 +115,7 @@ def run_test(variant: str, num_workers: str, name: str = "") -> None: crash_xml = f"{ci_project_dir}/reports/crash_rerun.xml" crash_html = f"{ci_project_dir}/reports/crash_rerun.html" end2end_xml = f"{ci_project_dir}/reports/end2end.xml" - # end2end_html = f"{ci_project_dir}/reports/end2end.html" + end2end_html = f"{ci_project_dir}/reports/end2end.html" crash_re: Pattern[str] = re.compile( r"(worker.*crash|worker.*terminated|segmentation fault|sigsegv|signal 11|fatal python error)", # noqa @@ -250,22 +250,28 @@ def detect_crashed_nodeids(junit_xml_path: str) -> CrashDetectionResult: print("[INFO] No crash-like tests to rerun.") main_returncode = test_1_returncode or test_2_returncode - # test_2_process = subprocess.Popen( - # shlex.split( - # ( - # f"{sys.executable} -m pytest -v -m 'end2end or sanity_bnn or notebooks' " - # f"--junitxml={end2end_xml} " - # f"--html={end2end_html} " - # f"--reruns 1 --dist loadgroup -n {num_workers}" - # ), - # posix=IS_POSIX, - # ) - # ) - # test_2_process.communicate() - # test_2_returncode = test_2_process.returncode - - # Run doctests for all FINN submodules - # test_3_returncode = run_doctests(int(num_workers)) + # -------------------------- + # 4) Run end2end tests + # -------------------------- + + test_3_process = subprocess.Popen( + shlex.split( + ( + f"{sys.executable} -m pytest -v -m 'end2end or sanity_bnn or notebooks' " + f"--junitxml={end2end_xml} " + f"--html={end2end_html} " + f"--reruns 1 --dist loadgroup -n {num_workers}" + ), + posix=IS_POSIX, + ) + ) + test_3_process.communicate() + test_3_returncode = test_3_process.returncode + + # -------------------------- + # 5) Run doctests and merge all reports into a single HTML and XML report + # -------------------------- + test_4_returncode = run_doctests(int(num_workers)) subprocess.run( shlex.split( @@ -292,7 +298,7 @@ def detect_crashed_nodeids(junit_xml_path: str) -> CrashDetectionResult: if Path(xml_file).exists(): Path(xml_file).unlink() - if main_returncode: # or test_2_returncode or test_3_returncode: + if main_returncode or test_2_returncode or test_3_returncode or test_4_returncode: sys.exit(1) case _: diff --git a/src/finn/transformation/fpgadataflow/insert_fifo.py b/src/finn/transformation/fpgadataflow/insert_fifo.py index 172dc4388f..32f5ede854 100644 --- a/src/finn/transformation/fpgadataflow/insert_fifo.py +++ b/src/finn/transformation/fpgadataflow/insert_fifo.py @@ -43,7 +43,7 @@ from typing import cast from finn.util.basic import getHWCustomOp -from finn.util.exception import FINNInternalError +from finn.util.exception import FINNInternalError, FINNUserError from finn.util.fpgadataflow import is_fpgadataflow_node from finn.util.logging import log @@ -300,10 +300,10 @@ def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: not final_node.op_type.startswith("StreamingFIFO") and final_node.op_type != "IODMA_hls" ): - assert ( - final_node.op_type != "TLastMarker_hls" - ), """Insert tlast marker should be done - after inserting the FIFOs""" + if final_node.op_type == "TLastMarker_hls": + raise FINNUserError( + "Inserting tlast marker should be done after inserting the FIFOs" + ) n0 = getHWCustomOp(final_node) out_ind = list(final_node.output).index(graph_out_name) # determine fifo node attributes diff --git a/src/finn/transformation/fpgadataflow/insert_tlastmarker.py b/src/finn/transformation/fpgadataflow/insert_tlastmarker.py index f72de2f794..809b31773a 100644 --- a/src/finn/transformation/fpgadataflow/insert_tlastmarker.py +++ b/src/finn/transformation/fpgadataflow/insert_tlastmarker.py @@ -1,3 +1,5 @@ +"""Transformation that inserts TLastMarker_hls nodes at the beginning and/or end of a graph +if they are not already present.""" # Copyright (c) 2020, Xilinx # All rights reserved. # @@ -27,11 +29,25 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import numpy as np -from onnx import TensorProto +from collections.abc import Sequence +from onnx import AttributeProto, TensorProto from onnx import helper as oh -from qonnx.custom_op.registry import getCustomOp +from qonnx.core.modelwrapper import ModelWrapper from qonnx.transformation.base import Transformation from qonnx.util.basic import get_by_name +from typing import cast + +from finn.util.basic import getHWCustomOp +from finn.util.exception import FINNInternalError +from finn.util.fpgadataflow import is_fpgadataflow_node + + +def _get_by_name(attributes: Sequence[AttributeProto], name: str) -> AttributeProto: + """Get attribute by name from a list of attributes.""" + ret = get_by_name(attributes, name) + if ret is not None: + return ret + raise FINNInternalError(f"Attribute {name} not found in node attributes") class InsertTLastMarker(Transformation): @@ -41,22 +57,33 @@ class InsertTLastMarker(Transformation): More information available on the TLastMarker documentation. """ - def __init__(self, both=False, external=True, dynamic=True): + def __init__(self, both: bool = False, external: bool = True, dynamic: bool = True) -> None: + """Construct the InsertTLastMarker transformation.""" super().__init__() self.dyniters = dynamic self.external = external self.both = both - def apply(self, model): - # TODO only makes sense for a pure fpgadataflow graph -- check! + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, bool]: + """Apply the InsertTLastMarker transformation to the provided model.""" + for node in model.graph.node: + if not is_fpgadataflow_node(node): + raise FINNInternalError( + "InsertTLastMarker transformation can only be applied to fpgadataflow graphs " + f"but found node {node.name} of type {node.op_type} with domain {node.domain}." + ) graph_out_name = model.get_first_global_out() final_node = model.find_producer(graph_out_name) + if final_node is None: + raise FINNInternalError( + "Graph output has no producer node in InsertTLastMarker transformation" + ) graph_modified = False if final_node.op_type != "TLastMarker_hls" and not ( final_node.op_type == "IODMA_hls" - and get_by_name(final_node.attribute, "direction").s.decode("UTF-8") == "out" + and _get_by_name(final_node.attribute, "direction").s.decode("UTF-8") == "out" ): - custom_op = getCustomOp(final_node) + custom_op = getHWCustomOp(final_node) num_iters = int(custom_op.get_number_output_values()) stream_width = int(custom_op.get_outstream_width()) out_shape = model.get_tensor_shape(graph_out_name) @@ -82,6 +109,8 @@ def apply(self, model): Protocol=("external" if self.external else "internal"), domain="finn.custom_op.fpgadataflow.hls", backend="fpgadataflow", + normal_shape=out_shape, + dataType=out_dtype.name, ) model.graph.node.append(tlast_node) graph_modified = True @@ -104,16 +133,16 @@ def apply(self, model): # initializer (TODO: fix this with a clean-up transform) if ( first_node.op_type.startswith("MVAU") - and get_by_name(first_node.attribute, "mem_mode").s.decode("UTF-8") + and _get_by_name(first_node.attribute, "mem_mode").s.decode("UTF-8") != "external" ): continue # 2. node is either a TLastMarker or an input IODMA if first_node.op_type != "TLastMarker_hls" and not ( first_node.op_type == "IODMA_hls" - and get_by_name(first_node.attribute, "direction").s.decode("UTF-8") == "in" + and _get_by_name(first_node.attribute, "direction").s.decode("UTF-8") == "in" ): - custom_op = getCustomOp(first_node) + custom_op = getHWCustomOp(first_node) num_iters = np.prod(custom_op.get_folded_input_shape()[1:-1]) inp_idx = list(first_node.input).index(graph_in_name) if inp_idx > 0: @@ -143,7 +172,7 @@ def apply(self, model): ini = model.get_initializer(graph_in_name) # copy initializer if it exists if ini is not None: - model.set_initializer(first_node_in.name, ini) + model.set_initializer(first_node_in.name, cast("np.ndarray", ini)) # reroute final node output to first_node_in_name first_node.input[inp_idx] = first_node_in.name tlast_node = oh.make_node( From 01c64e8fe66cad6fe7fe63ffe4b0502371fbab16 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:38:38 +0200 Subject: [PATCH 162/170] Fix type error of path concat --- src/finn/builder/build_dataflow_steps.py | 4 ++- .../fpgadataflow/hls/tlastmarker_hls.py | 4 +-- .../fpgadataflow/create_dataflow_partition.py | 2 +- .../fpgadataflow/make_zynq_proj.py | 25 ++++++++++--------- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index a870d86fed..2c6d28932d 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -1492,6 +1492,8 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo raise FINNUserError( "instrumentation_no_dma must be set in the config for Vivado Zynq flow" ) + if cfg.board is None: + raise FINNUserError("board must be set in the config for Vivado Zynq flow") model = model.transform( ZynqBuild( cfg.board, @@ -1502,7 +1504,7 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo cfg.instrumentation_avg_n, cfg.auto_fifo_depths and cfg.auto_fifo_strategy == AutoFIFOSizingMethod.LIVE_FIFO, - partition_model_dir=partition_model_dir, + partition_model_dir=str(partition_model_dir), ) ) diff --git a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py index 0997c0c754..81156ce528 100644 --- a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py @@ -93,8 +93,8 @@ def get_nodeattr_types( return my_attrs def execute_node( - self, context: dict[str, np.ndarray], graph: "GraphProto" - ) -> None: # noqa: ARG002 + self, context: dict[str, np.ndarray], graph: "GraphProto" # noqa: ARG002 + ) -> None: # TLastMarker's behavior is only visible when doing # rtlsim with stitched IP, since it marks the end # of the current image/input sample. when executing diff --git a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py index 9a63bac336..129a2cd1d5 100644 --- a/src/finn/transformation/fpgadataflow/create_dataflow_partition.py +++ b/src/finn/transformation/fpgadataflow/create_dataflow_partition.py @@ -85,7 +85,7 @@ def assign_partition_id(node: NodeProto) -> int: # first, use the generic partitioning functionality to split up the graph parent_model = model.transform( PartitionFromLambda( - partitioning=assign_partition_id, partition_dir=self.partition_model_dir + partitioning=assign_partition_id, partition_dir=str(self.partition_model_dir) ) ) # change node types to StreamingDataflowPartition diff --git a/src/finn/transformation/fpgadataflow/make_zynq_proj.py b/src/finn/transformation/fpgadataflow/make_zynq_proj.py index 38d1da5d50..df90ec415b 100644 --- a/src/finn/transformation/fpgadataflow/make_zynq_proj.py +++ b/src/finn/transformation/fpgadataflow/make_zynq_proj.py @@ -28,6 +28,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Transformation to create Zynq Vivado projects for FINN dataflow designs.""" + import json import math import os @@ -39,6 +40,7 @@ from qonnx.transformation.infer_data_layouts import InferDataLayouts from shutil import copy from subprocess import CalledProcessError +from typing import Literal from finn.transformation.fpgadataflow.create_dataflow_partition import CreateDataflowPartition from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP @@ -614,21 +616,20 @@ def apply(self, model): class ZynqBuild(Transformation): """Best-effort attempt at building the accelerator for Zynq. - It assumes the model has only fpgadataflow nodes - + It assumes the model has only fpgadataflow nodes. """ def __init__( self, - platform, - period_ns, - enable_debug=False, - enable_instrumentation=False, - instrumentation_no_dma=False, - instrumentation_avg_n=64, - live_fifo_sizing=False, - partition_model_dir=None, - ): + platform: str, + period_ns: float, + enable_debug: bool = False, + enable_instrumentation: bool = False, + instrumentation_no_dma: bool = False, + instrumentation_avg_n: int = 64, + live_fifo_sizing: bool = False, + partition_model_dir: str | None = None, + ) -> None: """Initialize ZynqBuild with platform and build settings.""" super().__init__() self.fpga_part = pynq_part_map[platform] @@ -642,7 +643,7 @@ def __init__( self.live_fifo_sizing = live_fifo_sizing self.partition_model_dir = partition_model_dir - def apply(self, model): + def apply(self, model: ModelWrapper) -> tuple[ModelWrapper, Literal[False]]: """Apply the ZynqBuild transformation to create a complete Zynq accelerator.""" model = model.transform(InferDataLayouts()) # prepare at global level, then break up into kernels From fb766917e9976011f5d1bef9664ad06f92e29d46 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:17:50 +0200 Subject: [PATCH 163/170] Readd missing function --- src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py index 81156ce528..c91b5a889f 100644 --- a/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py +++ b/src/finn/custom_op/fpgadataflow/hls/tlastmarker_hls.py @@ -111,6 +111,9 @@ def make_shape_compatible_op(self, model: "ModelWrapper") -> NodeProto: """Create shape compatible op.""" return super().make_shape_compatible_op(model) + def infer_node_datatype(self, model: "ModelWrapper") -> None: + """Not supported for datatype inference.""" + def global_includes(self) -> None: """Return global includes.""" self.code_gen_dict["$GLOBALS$"] = ['#include "ap_axi_sdata.h"'] From 0eb54b80227664c8094b8bf23420be50cc4ec7fa Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Fri, 26 Jun 2026 16:32:37 +0200 Subject: [PATCH 164/170] Remove additional slash --- src/finn/builder/build_dataflow_steps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/finn/builder/build_dataflow_steps.py b/src/finn/builder/build_dataflow_steps.py index 2c6d28932d..efa1c468bc 100644 --- a/src/finn/builder/build_dataflow_steps.py +++ b/src/finn/builder/build_dataflow_steps.py @@ -1531,7 +1531,7 @@ def step_synthesize_bitfile(model: ModelWrapper, cfg: DataflowBuildConfig) -> Mo copy(Path(hwh_src), bitfile_dir / "finn-accel.hwh") copy( Path(rpt_dir), - report_dir / "/post_synth_resources.xml", + report_dir / "post_synth_resources.xml", ) model.set_metadata_prop("bitfile_output", str(bitfile_path.absolute())) From 0155b664a265c38dcae85b86a7791bca20b78c77 Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Sat, 27 Jun 2026 10:54:00 +0200 Subject: [PATCH 165/170] Fix xml merging and remove doctests in isolated sim --- src/finn/interface/manage_tests.py | 17 ++++--- .../fpgadataflow/simulation_isolated.py | 49 +------------------ .../testing_util}/merge_xml_reports.py | 0 3 files changed, 11 insertions(+), 55 deletions(-) rename {scripts => tests/testing_util}/merge_xml_reports.py (100%) diff --git a/src/finn/interface/manage_tests.py b/src/finn/interface/manage_tests.py index 894e872385..650b9ec56f 100644 --- a/src/finn/interface/manage_tests.py +++ b/src/finn/interface/manage_tests.py @@ -282,8 +282,8 @@ def detect_crashed_nodeids(junit_xml_path: str) -> CrashDetectionResult: posix=IS_POSIX, ) ) - script_dir = Path(__file__).parent.parent.resolve() / "scripts" / "merge_xml_reports.py" - subprocess.run( + script_dir = Path(get_settings().finn_tests) / "testing_util" / "merge_xml_reports.py" + success = subprocess.run( shlex.split( ( f"{sys.executable} {script_dir} " @@ -292,11 +292,14 @@ def detect_crashed_nodeids(junit_xml_path: str) -> CrashDetectionResult: ), posix=IS_POSIX, ) - ) - # Remove individual XML reports to avoid confusion with the merged report - for xml_file in [main_xml, crash_xml, end2end_xml]: - if Path(xml_file).exists(): - Path(xml_file).unlink() + ).returncode + if success != 0: + print(f"[WARN] Merging XML reports failed with exit code {success}.") + else: + # Remove individual XML reports to avoid confusion with the merged report + for xml_file in [main_xml, crash_xml, end2end_xml]: + if Path(xml_file).exists(): + Path(xml_file).unlink() if main_returncode or test_2_returncode or test_3_returncode or test_4_returncode: sys.exit(1) diff --git a/src/finn/transformation/fpgadataflow/simulation_isolated.py b/src/finn/transformation/fpgadataflow/simulation_isolated.py index beb796d5f2..48720470f6 100644 --- a/src/finn/transformation/fpgadataflow/simulation_isolated.py +++ b/src/finn/transformation/fpgadataflow/simulation_isolated.py @@ -278,39 +278,6 @@ def __init__( def calculate_upper_bounds(self, data: IsoSimLogDataByLayer) -> dict[str, dict[str, int]]: """Try to calculate an upper bound for the incoming FIFO size of the layers. Return size indexed by layer name and stream name. - - >>> step = RunLayerIsolatedSimulation("", 0.0, False) - >>> bounds = step.calculate_upper_bounds({ - ... "A": { - ... "ready": [ - ... {"totalCycles": 43, "inputCyclesDone": 12, - ... "inputCyclesTarget": 24, "s_axi_0": 1, "s_axi_1": 0}, - ... {"totalCycles": 44, "inputCyclesDone": 13, - ... "inputCyclesTarget": 24, "s_axi_0": 0, "s_axi_1": 0}, - ... ], "valid": [] - ... }, - ... "B": { - ... "ready": [ - ... {"totalCycles": 100, "inputCyclesDone": 3, - ... "inputCyclesTarget": 10, "s_axi_0": 1, "s_axi_1": 1, - ... "s_axi_2": 0}, - ... ], "valid": [] - ... }, - ... "C": { - ... "ready": [ - ... {"totalCycles": 43, "inputCyclesDone": 14, - ... "inputCyclesTarget": 24, "s_axi_0": 1, "s_axi_1": 0}, - ... {"totalCycles": 44, "inputCyclesDone": 15, - ... "inputCyclesTarget": 24, "s_axi_0": 0, "s_axi_1": 0}, - ... ], "valid": [] - ... } - ... }) - >>> bounds["A"] - {'s_axi_0': 1, 's_axi_1': 2} - >>> bounds["B"] - {'s_axi_0': 0, 's_axi_1': 0, 's_axi_2': 1} - >>> bounds["C"] - {'s_axi_0': 0, 's_axi_1': 0} """ # TODO: Proper pytest tests @@ -370,21 +337,7 @@ def _any_ready(cycle_data: dict[str, int]) -> bool: return results def sanity_check_logged_data(self, data: IsoSimLogDataByLayer) -> None: - """Do checks on the returned data to make sure it is in spec. - - A correctly formatted example would be: - >>> data = { - ... "layer1": { - ... "ready": [{"totalCycles": 10, "inputCyclesDone": 5, - ... "inputCyclesTarget": 10, "s_axi_0": 1}], - ... "valid": [{"totalCycles": 10, "outputCyclesDone": 5, - ... "outputCyclesTarget": 10, "m_axi_0": 1}] - ... } - ... } - >>> sim = RunLayerIsolatedSimulation("", 0.0, False) - >>> sim.sanity_check_logged_data(data) - >>> - """ + """Do checks on the returned data to make sure it is in spec.""" # 0. Valid and ready are present for layer, ldata in data.items(): if "valid" not in ldata.keys(): diff --git a/scripts/merge_xml_reports.py b/tests/testing_util/merge_xml_reports.py similarity index 100% rename from scripts/merge_xml_reports.py rename to tests/testing_util/merge_xml_reports.py From ea84b5550cfdbaff8171fe86b47be204c29f72ca Mon Sep 17 00:00:00 2001 From: Linus Jungemann <38974033+LinusJungemann@users.noreply.github.com> Date: Thu, 9 Jul 2026 22:54:28 +0200 Subject: [PATCH 166/170] Add reduce operator --- custom_hls/reduce.hpp | 621 ++++++ src/finn/builder/passes.py | 35 +- src/finn/custom_op/fpgadataflow/__init__.py | 11 +- .../custom_op/fpgadataflow/hls/__init__.py | 9 +- .../custom_op/fpgadataflow/hls/reduce_hls.py | 72 + src/finn/custom_op/fpgadataflow/hlsbackend.py | 122 +- src/finn/custom_op/fpgadataflow/hwcustomop.py | 71 +- src/finn/custom_op/fpgadataflow/reduce.py | 384 ++++ src/finn/custom_op/fpgadataflow/rtlbackend.py | 59 +- .../fpgadataflow/convert_to_hw_layers.py | 1942 ++++++++++------- .../fpgadataflow/test_fpgadataflow_reduce.py | 399 ++++ tests/fpgadataflow/test_minimize_bit_width.py | 2 +- 12 files changed, 2922 insertions(+), 805 deletions(-) create mode 100644 custom_hls/reduce.hpp create mode 100644 src/finn/custom_op/fpgadataflow/hls/reduce_hls.py create mode 100644 src/finn/custom_op/fpgadataflow/reduce.py create mode 100644 tests/fpgadataflow/test_fpgadataflow_reduce.py diff --git a/custom_hls/reduce.hpp b/custom_hls/reduce.hpp new file mode 100644 index 0000000000..6e63b47994 --- /dev/null +++ b/custom_hls/reduce.hpp @@ -0,0 +1,621 @@ +#ifndef REDUCE +#define REDUCE + +#include "utils.hpp" + +#include +#include +#include + +// ----------------------------------------------------------------------------- +// Detection idiom (C++14) to constrain Functor compatibility +// Required interface for F = Functor: +// TO init() const; +// void operator()(TO& accu, const TI& x) const; +// ----------------------------------------------------------------------------- +template using void_t = void; + +template