diff --git a/docker/Dockerfile.finn_ci b/docker/Dockerfile.finn_ci index 2668927602ebb8de5fdc3d7c25b20a0c8c4a2e55..5772b16abc8b927def1e2dfbbb8193a2f964f87d 100644 --- a/docker/Dockerfile.finn_ci +++ b/docker/Dockerfile.finn_ci @@ -37,7 +37,7 @@ WORKDIR /workspace RUN apt-get update RUN apt-get -y upgrade RUN apt-get install -y build-essential libglib2.0-0 libsm6 libxext6 libxrender-dev -RUN apt install verilator +RUN apt-get install -y verilator zsh RUN apt-get -y install sshpass RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config @@ -52,6 +52,8 @@ RUN git clone https://github.com/Xilinx/finn-hlslib.git /workspace/finn-hlslib RUN git clone https://github.com/maltanar/pyverilator /workspace/pyverilator # PYNQ-HelloWorld RUN git clone https://github.com/maltanar/PYNQ-HelloWorld.git /workspace/PYNQ-HelloWorld +# oh-my-xilinx +RUN git clone https://bitbucket.org/maltanar/oh-my-xilinx.git /workspace/oh-my-xilinx # checkout desired FINN branch for testing RUN git clone --branch $FINN_CI_BRANCH https://github.com/Xilinx/finn /workspace/finn @@ -64,6 +66,8 @@ ENV PYTHONPATH "${PYTHONPATH}:/workspace/finn/src" ENV PYTHONPATH "${PYTHONPATH}:/workspace/pyverilator" ENV PYNQSHELL_PATH "/workspace/PYNQ-HelloWorld/boards" ENV VIVADO_IP_CACHE "$BUILD_PATH/vivado_ip_cache" +ENV PATH "${PATH}:/workspace/oh-my-xilinx" +ENV OHMYXILINX "/workspace/oh-my-xilinx" # colorful terminal output RUN echo "PS1='\[\033[1;36m\]\u\[\033[1;31m\]@\[\033[1;32m\]\h:\[\033[1;35m\]\w\[\033[1;31m\]\$\[\033[0m\] '" >> /root/.bashrc diff --git a/docker/Dockerfile.finn_dev b/docker/Dockerfile.finn_dev index 1200c7d5d15bbd62e15f19f84e70d5fe0b8aca28..22e3eb623c7a5da19a5e3ae2284557577898ad23 100644 --- a/docker/Dockerfile.finn_dev +++ b/docker/Dockerfile.finn_dev @@ -37,16 +37,12 @@ ARG PASSWD ARG JUPYTER_PORT ARG NETRON_PORT -EXPOSE $JUPYTER_PORT -EXPOSE $NETRON_PORT - WORKDIR /workspace RUN apt-get update RUN apt-get -y upgrade RUN apt-get install -y build-essential libglib2.0-0 libsm6 libxext6 libxrender-dev -RUN apt-get install verilator -RUN apt-get install nano +RUN apt-get install -y verilator nano zsh RUN apt-get -y install sshpass RUN echo "StrictHostKeyChecking no" >> /etc/ssh/ssh_config @@ -81,12 +77,16 @@ RUN git clone https://github.com/Xilinx/finn-hlslib.git /workspace/finn-hlslib RUN git clone https://github.com/maltanar/pyverilator /workspace/pyverilator # PYNQ-HelloWorld RUN git clone https://github.com/maltanar/PYNQ-HelloWorld.git /workspace/PYNQ-HelloWorld +# oh-my-xilinx +RUN git clone https://bitbucket.org/maltanar/oh-my-xilinx.git /workspace/oh-my-xilinx # for this developer-oriented Docker container we assume the FINN repo is cloned and mounted from the host # at /workspace/finn -- see run-docker.sh for an example of how to do this. ENV PYTHONPATH "${PYTHONPATH}:/workspace/finn/src" ENV PYTHONPATH "${PYTHONPATH}:/workspace/pyverilator" ENV PYNQSHELL_PATH "/workspace/PYNQ-HelloWorld/boards" +ENV PATH "${PATH}:/workspace/oh-my-xilinx" +ENV OHMYXILINX "/workspace/oh-my-xilinx" WORKDIR /home/$UNAME/finn RUN echo "PS1='\[\033[1;36m\]\u\[\033[1;31m\]@\[\033[1;32m\]\h:\[\033[1;35m\]\w\[\033[1;31m\]\$\[\033[0m\] '" >> /home/$UNAME/.bashrc @@ -100,5 +100,8 @@ RUN chmod 755 /usr/local/bin/finn_entrypoint.sh RUN chmod 755 /usr/local/bin/quicktest.sh USER $UNAME +EXPOSE $JUPYTER_PORT +EXPOSE $NETRON_PORT + ENTRYPOINT ["finn_entrypoint.sh"] CMD ["bash"] diff --git a/docker/finn_entrypoint.sh b/docker/finn_entrypoint.sh index 3a1554ac13902f7ca82cd3d11f55373a0fd6ea54..6ad4adc352ca5d71993ee8118568df20dba00810 100644 --- a/docker/finn_entrypoint.sh +++ b/docker/finn_entrypoint.sh @@ -16,8 +16,9 @@ gecho () { BREVITAS_COMMIT=989cdfdba4700fdd900ba0b25a820591d561c21a CNPY_COMMIT=4e8810b1a8637695171ed346ce68f6984e585ef4 HLSLIB_COMMIT=afcfe75f3404249bddeeb3f15df65bd1fcb1072e -PYVERILATOR_COMMIT=1d89cb0d4e0c97469cc6352c611f876ec13edfa6 +PYVERILATOR_COMMIT=c97a5ba41bbc7c419d6f25c74cdf3bdc3393174f PYNQSHELL_COMMIT=0c82a61b0ec1a07fa275a14146233824ded7a13d +OMX_COMMIT=1bae737669901e762f581af73348332b5c4b2ada gecho "Setting up known-good commit versions for FINN dependencies" @@ -42,6 +43,10 @@ git -C /workspace/pyverilator checkout $PYVERILATOR_COMMIT --quiet gecho "PYNQ shell @ $PYNQSHELL_COMMIT" git -C /workspace/PYNQ-HelloWorld pull --quiet git -C /workspace/PYNQ-HelloWorld checkout $PYNQSHELL_COMMIT --quiet +# oh-my-xilinx +gecho "oh-my-xilinx @ $OMX_COMMIT" +git -C /workspace/oh-my-xilinx pull --quiet +git -C /workspace/oh-my-xilinx checkout $OMX_COMMIT --quiet # source Vivado env.vars source $VIVADO_PATH/settings64.sh diff --git a/requirements.txt b/requirements.txt index 6b8e4d02c8ca1dcdbe607aabdccd27cec8056332..b15d86ed89f7b0e76b772ce42aba6481937310b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,4 +9,5 @@ pyverilator scipy sphinx toposort +vcdvcd wget diff --git a/src/finn/core/onnx_exec.py b/src/finn/core/onnx_exec.py index 44787e1d26049e6075e2222316b45ab3898acbc7..da0a8a18b6d514eefed89c9766cd526fee338204 100644 --- a/src/finn/core/onnx_exec.py +++ b/src/finn/core/onnx_exec.py @@ -102,10 +102,7 @@ def execute_node(node, context, graph): raise Exception( """Output shapes disagree after node execution: found %s vs expected %s""" - % ( - str(output_list[list_ind].shape.shape), - str(context[outp].shape), - ) + % (str(output_list[list_ind].shape), str(context[outp].shape),) ) context[outp] = output_list[list_ind] diff --git a/src/finn/core/rtlsim_exec.py b/src/finn/core/rtlsim_exec.py index e5e6d29bd8d8ed23f6a4958856ed1ddea3617175..1e1bee3aa7435d5cab6cbf5ea23dd37dcdfa4380 100644 --- a/src/finn/core/rtlsim_exec.py +++ b/src/finn/core/rtlsim_exec.py @@ -66,6 +66,11 @@ def rtlsim_exec(model, execution_context): i_stream_w = first_node.get_instream_width() # convert input into time multiplexed shape i_folded_shape = first_node.get_folded_input_shape() + batchsize = i_tensor.shape[0] + # override batch size for input + i_folded_shape = list(i_folded_shape) + i_folded_shape[0] = batchsize + i_folded_shape = tuple(i_folded_shape) # TODO any other layout transformations need to happen here! i_tensor = i_tensor.reshape(i_folded_shape) # extract output shape @@ -74,21 +79,27 @@ def rtlsim_exec(model, execution_context): o_dt = model.get_tensor_datatype(o_name) last_node = getCustomOp(model.find_producer(o_name)) o_folded_shape = last_node.get_folded_output_shape() + # override batch size from actual input + o_shape = list(o_shape) + o_shape[0] = batchsize + o_shape = tuple(o_shape) + o_folded_shape = list(o_folded_shape) + o_folded_shape[0] = batchsize + o_folded_shape = tuple(o_folded_shape) o_stream_w = last_node.get_outstream_width() packedBits = o_stream_w targetBits = o_dt.bitwidth() # pack input packed_input = npy_to_rtlsim_input(i_tensor, i_dt, i_stream_w) num_out_values = last_node.get_number_output_values() + num_out_values *= batchsize # prepare pyverilator model rtlsim_so = model.get_metadata_prop("rtlsim_so") if (rtlsim_so is None) or (not os.path.isfile(rtlsim_so)): sim = pyverilate_stitched_ip(model) model.set_metadata_prop("rtlsim_so", sim.lib._name) else: - sim = PyVerilator(rtlsim_so) - _reset_rtlsim(sim) - _toggle_clk(sim) + sim = PyVerilator(rtlsim_so, auto_eval=False) ret = _run_rtlsim(sim, packed_input, num_out_values, trace_file) packed_output = ret[0] model.set_metadata_prop("sim_cycles", str(ret[1])) @@ -104,18 +115,22 @@ def _reset_rtlsim(sim): """Sets reset input in pyverilator to zero, toggles the clock and set it back to one""" sim.io.ap_rst_n_0 = 0 - sim.io.ap_clk_0 = 1 - sim.io.ap_clk_0 = 0 + _toggle_clk(sim) + _toggle_clk(sim) sim.io.ap_rst_n_0 = 1 + _toggle_clk(sim) + _toggle_clk(sim) def _toggle_clk(sim): """Toggles the clock input in pyverilator once.""" - sim.io.ap_clk_0 = 1 sim.io.ap_clk_0 = 0 + sim.eval() + sim.io.ap_clk_0 = 1 + sim.eval() -def _run_rtlsim(sim, inp, num_out_values, trace_file=None): +def _run_rtlsim(sim, inp, num_out_values, trace_file=None, reset=True): """Runs the pyverilator simulation by passing the input values to the simulation, toggle the clock and observing the execution time. Argument num_out_values contains the number of expected output values, so the simulation is closed after all @@ -140,6 +155,8 @@ def _run_rtlsim(sim, inp, num_out_values, trace_file=None): if trace_file is not None: sim.start_vcd_trace(trace_file) + if reset: + _reset_rtlsim(sim) while not (output_observed): sim.io.in0_V_V_0_tvalid = 1 if len(inputs) > 0 else 0 @@ -148,8 +165,7 @@ def _run_rtlsim(sim, inp, num_out_values, trace_file=None): inputs = inputs[1:] if sim.io.out_r_0_tvalid == 1 and sim.io.out_r_0_tready == 1: outputs = outputs + [sim.io.out_r_0_tdata] - sim.io.ap_clk_0 = 1 - sim.io.ap_clk_0 = 0 + _toggle_clk(sim) observation_count = observation_count + 1 no_change_count = no_change_count + 1 diff --git a/src/finn/core/throughput_test.py b/src/finn/core/throughput_test.py index 8d3dabcf8af51327d5d951464c6d9b36e2f67497..4444e7584f843cd0edb016b520d01d71e659b904 100644 --- a/src/finn/core/throughput_test.py +++ b/src/finn/core/throughput_test.py @@ -28,6 +28,10 @@ import os import subprocess +import numpy as np + +from finn.util.basic import gen_finn_dt_tensor +from finn.core.rtlsim_exec import rtlsim_exec def throughput_test(model, batchsize=1000): @@ -88,3 +92,50 @@ def throughput_test(model, batchsize=1000): return res except FileNotFoundError: return None + + +def throughput_test_rtlsim(model, 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 + metadata_prop must be set to rtlsim""" + + # create random input + iname = model.graph.input[0].name + ishape = model.get_tensor_shape(iname) + ishape_batch = ishape + ishape_batch[0] = batchsize + idt = model.get_tensor_datatype(iname) + dummy_input = gen_finn_dt_tensor(idt, ishape_batch) + # compute input/output sizes + oname = model.graph.output[0].name + oshape = model.get_tensor_shape(oname) + oshape_batch = oshape + oshape_batch[0] = batchsize + odt = model.get_tensor_datatype(oname) + i_bytes = (np.prod(ishape_batch) * idt.bitwidth()) / 8 + o_bytes = (np.prod(oshape_batch) * odt.bitwidth()) / 8 + # make empty exec context and insert input + ctx = model.make_empty_exec_context() + ctx[iname] = dummy_input + # remove liveness threshold, launch rtlsim + os.environ["LIVENESS_THRESHOLD"] = "-1" + rtlsim_exec(model, ctx) + # extract metrics + cycles = int(model.get_metadata_prop("sim_cycles")) + clk_ns = float(model.get_metadata_prop("clk_ns")) + fclk_mhz = 1 / (clk_ns * 0.001) + runtime_s = (cycles * clk_ns) * (10 ** -9) + res = dict() + res["cycles"] = cycles + res["runtime[ms]"] = runtime_s * 1000 + res["throughput[images/s]"] = batchsize / runtime_s + res["DRAM_in_bandwidth[Mb/s]"] = i_bytes * 0.000001 / runtime_s + res["DRAM_out_bandwidth[Mb/s]"] = o_bytes * 0.000001 / runtime_s + res["fclk[mhz]"] = fclk_mhz + res["N"] = batchsize + + return res diff --git a/src/finn/custom_op/fpgadataflow/streamingfifo.py b/src/finn/custom_op/fpgadataflow/streamingfifo.py index 66190333ce8d71dafba99aaeae4fb2c973d67410..1f734b548f923341687843c538d1887fcc069bee 100644 --- a/src/finn/custom_op/fpgadataflow/streamingfifo.py +++ b/src/finn/custom_op/fpgadataflow/streamingfifo.py @@ -110,6 +110,8 @@ class StreamingFIFO(HLSCustomOp): ] # make instream width a multiple of 8 for axi interface in_width = self.get_instream_width_padded() + count_width = int(self.get_nodeattr("depth") - 1).bit_length() + self.code_gen_dict["$COUNT_RANGE$"] = ["[{}:0]".format(count_width - 1)] self.code_gen_dict["$IN_RANGE$"] = ["[{}:0]".format(in_width - 1)] self.code_gen_dict["$OUT_RANGE$"] = ["[{}:0]".format(in_width - 1)] self.code_gen_dict["$WIDTH$"] = [str(in_width)] diff --git a/src/finn/custom_op/fpgadataflow/templates.py b/src/finn/custom_op/fpgadataflow/templates.py index 5f526aa2aa1917144c7a048c9d9314aa9288a2d8..1a8216f64bf71b7fb9f1f8becf4732970b5bf451 100644 --- a/src/finn/custom_op/fpgadataflow/templates.py +++ b/src/finn/custom_op/fpgadataflow/templates.py @@ -408,6 +408,7 @@ strm_fifo_wrapper = """ module $TOPNAME$( ap_clk, ap_rst_n, +count, in0_V_V_TDATA, in0_V_V_TVALID, in0_V_V_TREADY, @@ -418,6 +419,7 @@ out_V_V_TREADY input ap_clk; input ap_rst_n; +output $COUNT_RANGE$ count; input $IN_RANGE$ in0_V_V_TDATA; input in0_V_V_TVALID; output in0_V_V_TREADY; @@ -433,6 +435,7 @@ $LAYER_NAME$ ( .clock(ap_clk), .reset(!ap_rst_n), + .count(count), .i_d(in0_V_V_TDATA), .i_v(in0_V_V_TVALID), .i_r(in0_V_V_TREADY), diff --git a/src/finn/transformation/fpgadataflow/synth_ooc.py b/src/finn/transformation/fpgadataflow/synth_ooc.py new file mode 100644 index 0000000000000000000000000000000000000000..1d49970c819961d1794cc89e998108639ca15593 --- /dev/null +++ b/src/finn/transformation/fpgadataflow/synth_ooc.py @@ -0,0 +1,64 @@ +# Copyright (c) 2020, Xilinx +# 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 os +from shutil import copy2 + +from finn.transformation import Transformation +from finn.util.vivado import out_of_context_synth +from finn.util.basic import make_build_dir + + +class SynthOutOfContext(Transformation): + """Run out-of-context Vivado synthesis on a stitched IP design.""" + + def __init__(self, part, clk_period_ns, clk_name="ap_clk_0"): + super().__init__() + self.part = part + self.clk_period_ns = clk_period_ns + self.clk_name = clk_name + + def apply(self, model): + def file_to_basename(x): + return os.path.basename(os.path.realpath(x)) + + vivado_stitch_proj_dir = model.get_metadata_prop("vivado_stitch_proj") + assert vivado_stitch_proj_dir is not None, "Need stitched IP to run." + top_module_name = model.get_metadata_prop("wrapper_filename") + top_module_name = file_to_basename(top_module_name).strip(".v") + build_dir = make_build_dir("synth_out_of_context_") + with open(vivado_stitch_proj_dir + "/all_verilog_srcs.txt", "r") as f: + all_verilog_srcs = f.read().split() + for file in all_verilog_srcs: + if file.endswith(".v"): + copy2(file, build_dir) + ret = out_of_context_synth( + build_dir, top_module_name, self.part, self.clk_name, self.clk_period_ns + ) + model.set_metadata_prop("res_total_ooc_synth", str(ret)) + return (model, False) diff --git a/src/finn/util/basic.py b/src/finn/util/basic.py index 3880bb9591e27af5fe9d063dba2485d304e4db54..eb3d46bcd66e3dc307a679e6b8dfbb9913398d36 100644 --- a/src/finn/util/basic.py +++ b/src/finn/util/basic.py @@ -56,6 +56,12 @@ def get_rtlsim_trace_depth(): 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 will only show top-level signals and yield smaller .vcd files. + + The following depth values are of interest for whole-network stitched IP + rtlsim: + - level 1 shows top-level input/output streams + - level 2 shows per-layer input/output streams + - level 3 shows per full-layer I/O including FIFO count signals """ try: @@ -64,6 +70,16 @@ def get_rtlsim_trace_depth(): return 1 +def get_remote_vivado(): + """Return the address of the remote Vivado synthesis server as set by the, + REMOTE_VIVADO environment variable, otherwise return None""" + + try: + return os.environ["REMOTE_VIVADO"] + except KeyError: + return None + + def get_num_default_workers(): """Return the number of workers for parallel transformations. Controllable via the NUM_DEFAULT_WORKERS environment variable. If the env.var. is diff --git a/src/finn/util/create.py b/src/finn/util/create.py new file mode 100644 index 0000000000000000000000000000000000000000..853cdd0d44a05426b34bf1db3caa58d9289b2e9e --- /dev/null +++ b/src/finn/util/create.py @@ -0,0 +1,178 @@ +# Copyright (c) 2020, Xilinx +# 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 numpy as np +from finn.core.modelwrapper import ModelWrapper +from onnx import TensorProto, helper +from finn.core.datatype import DataType +from finn.util.basic import calculate_signed_dot_prod_range, gen_finn_dt_tensor + + +def hls_random_mlp_maker(layer_spec): + """Create an MLP of given specification using HLSCustomOp instances. + Generate random weights/thresholds of appropriate size.""" + ret = [] + for l in layer_spec: + idt = l["idt"] + wdt = l["wdt"] + mw = l["mw"] + mh = l["mh"] + act = l["act"] + l["W"] = gen_finn_dt_tensor(wdt, (mw, mh)) + if act is None: + # no activation, produce accumulators + T = None + tdt = None + if wdt == DataType.BIPOLAR and idt == DataType.BIPOLAR: + odt = DataType.UINT32 + else: + odt = DataType.INT32 + else: + odt = act + (min, max) = calculate_signed_dot_prod_range(idt, wdt, mw) + n_steps = act.get_num_possible_values() - 1 + T = np.random.randint(min, max - 1, (mh, n_steps)).astype(np.float32) + # provide non-decreasing thresholds + T = np.sort(T, axis=1) + # generate thresholds for activation + if wdt == DataType.BIPOLAR and idt == DataType.BIPOLAR: + tdt = DataType.UINT32 + # bias thresholds to be positive + T = np.ceil((T + mw) / 2) + assert (T >= 0).all() + else: + tdt = DataType.INT32 + l["T"] = T + l["tdt"] = tdt + l["odt"] = odt + ret.append(l) + + return hls_mlp_maker(ret) + + +def hls_mlp_maker(layer_spec): + """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 = helper.make_model(graph, producer_name="finn") + model = ModelWrapper(model) + + for l 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 = l["W"] + (mw, mh) = W.shape + T = l["T"] + pe = l["pe"] + simd = l["simd"] + wdt = l["wdt"] + idt = l["idt"] + tdt = l["tdt"] + odt = l["odt"] + + if i == 0: + global_in = helper.make_tensor_value_info( + current_in_name, TensorProto.FLOAT, [1, mw] + ) + model.graph.input.append(global_in) + + if i == len(layer_spec) - 1: + global_out = helper.make_tensor_value_info( + current_out_name, TensorProto.FLOAT, [1, mh] + ) + model.graph.output.append(global_out) + + # there are two ways to implement bipolar weights and inputs for + # StreamingFC: + # - specify their datatypes as such + # - specify their datatypes as BINARY as use binaryXnorMode + if wdt == DataType.BIPOLAR and idt == DataType.BIPOLAR: + # we'll internally convert weights/inputs to binary and specify the + # datatypes as such, and also set the binaryXnorMode attribute to 1 + export_wdt = DataType.BINARY + export_idt = DataType.BINARY + binary_xnor_mode = 1 + else: + export_wdt = wdt + export_idt = idt + binary_xnor_mode = 0 + + if T 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() + else: + # no thresholds + node_inp_list = [current_in_name, current_W_name] + actval = 0 + no_act = 1 + FCLayer_node = helper.make_node( + "StreamingFCLayer_Batch", + node_inp_list, + [current_out_name], + domain="finn", + backend="fpgadataflow", + resType="ap_resource_lut()", + MW=mw, + MH=mh, + SIMD=simd, + PE=pe, + inputDataType=export_idt.name, + weightDataType=export_wdt.name, + outputDataType=odt.name, + ActVal=actval, + binaryXnorMode=binary_xnor_mode, + noActivation=no_act, + ) + + model.graph.node.append(FCLayer_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) + if binary_xnor_mode: + # convert bipolar to binary + model.set_initializer(current_W_name, (W + 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 + + return model diff --git a/src/finn/util/fpgadataflow.py b/src/finn/util/fpgadataflow.py index d1669444e55cb0fddb2690e51849c4603d47d32c..3fe747a84985b2702ffb1e5855d9071362efebda 100644 --- a/src/finn/util/fpgadataflow.py +++ b/src/finn/util/fpgadataflow.py @@ -104,6 +104,7 @@ def pyverilate_stitched_ip(model): build_dir=build_dir, trace_depth=get_rtlsim_trace_depth(), top_module_name=top_module_name, + auto_eval=False, ) return sim diff --git a/src/finn/util/vcd.py b/src/finn/util/vcd.py new file mode 100644 index 0000000000000000000000000000000000000000..d9e244422065314ceb790dc6719b57688ff76828 --- /dev/null +++ b/src/finn/util/vcd.py @@ -0,0 +1,184 @@ +# Copyright (c) 2020, Xilinx +# 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. + +from vcdvcd import VCDVCD +from finn.util.basic import get_num_default_workers +import multiprocessing as mp + +# string patterns to search for to find particular interfaces +# streaming interfaces +vname = "TVALID" +rname = "TREADY" +# FIFO count signals +fifo_mod_name = "StreamingFIFO" +fifo_cname = "count" + + +def list_stream_if(vcd_file): + "Return a list of stream interface names from given vcd trace." + + sig_names = VCDVCD(vcd_file, print_dumps=False, only_sigs=True).get_signals() + stream_if_names = [] + for cand_name in filter(lambda x: x.endswith(vname), sig_names): + base_name = cand_name.replace(vname, "") + if base_name + rname in sig_names: + stream_if_names.append(base_name) + return stream_if_names + + +def list_fifo_count_signals(vcd_file): + "Return a list of FIFO count signal names from given vcd trace." + + sig_names = VCDVCD(vcd_file, print_dumps=False, only_sigs=True).get_signals() + fifo_cnt_names = [] + for cand_name in filter(lambda x: fifo_cname in x, sig_names): + if fifo_mod_name in cand_name: + fifo_cnt_names.append(cand_name) + return fifo_cnt_names + + +def get_fifo_count_max(vcd_file, fifo_count_signal): + "Return the maximum value of the given FIFO count signal in vcd trace." + + d = VCDVCD(vcd_file, signals=[fifo_count_signal], store_tvs=True).get_data() + assert len(d) != 0, "FIFO count signal not found" + events = list(d.values())[0]["tv"] + max = 0 + for (time, val) in events: + current = int(val, base=2) + if current > max: + max = current + return max + + +def _get_fifo_max(x): + return (x[0], get_fifo_count_max(x[1], x[0])) + + +def get_all_fifo_count_max(vcd_file, fifo_count_signals=None): + """Return a list of max FIFO counts. If fifo_count_signals is None, + all FIFO count signals will be returned, otherwise treated as a list of + signal names to return the stats for.""" + if fifo_count_signals is None: + fifo_count_signals = list_fifo_count_signals(vcd_file) + + with mp.Pool(get_num_default_workers()) as p: + fifo_count_signals = map(lambda x: (x, vcd_file), fifo_count_signals) + all_stats = p.map(_get_fifo_max, fifo_count_signals) + + return all_stats + + +def get_stream_if_stats(vcd_file, if_base_name): + """Return statistics for given streaming interface in vcd trace in the + following dict format: + + <stream_state>: (<num_samples>, <fraction_of_time>), + + where <stream_state> is the combination of (V)alid/(R)eady values, + <num_samples> is the approximate number of rising clock edges spent in <state> + , and <fraction_of_time> is the fraction of <num_samples> to total + amount of time recorded by the trace. + + Example: + {"{'V': 0, 'R': 0}": (5, 0.0006060606060606061), + "{'V': 1, 'R': 0}": (0, 0.0), + "{'V': 0, 'R': 1}": (7605, 0.9218181818181819), + "{'V': 1, 'R': 1}": (640, 0.07757575757575758)} + + Here we can see the stream was transmitting values 7.7% of the time, + and 9.2% of the time there was no incoming data (valid 0, ready 1) + """ + if_valid = if_base_name + vname + if_ready = if_base_name + rname + v = VCDVCD(vcd_file, signals=[if_valid], store_tvs=True) + endtime = v.get_endtime() + v = v.get_data() + assert len(v) != 0, "Streaming interface not found" + v = list(v.values())[0]["tv"] + v = list(map(lambda x: ("V", x[0], x[1]), v)) + v.append(("V", endtime, "0")) + r = VCDVCD(vcd_file, signals=[if_ready], store_tvs=True).get_data() + assert len(r) != 0, "Streaming interface not found" + r = list(r.values())[0]["tv"] + r = list(map(lambda x: ("R", x[0], x[1]), r)) + r.append(("R", endtime, "0")) + events = sorted(v + r, key=lambda x: x[1]) + ret = { + "{'V': 0, 'R': 0}": 0, + "{'V': 1, 'R': 0}": 0, + "{'V': 0, 'R': 1}": 0, + "{'V': 1, 'R': 1}": 0, + } + status = {"V": 0, "R": 0} + last_time = 0 + total_rising_clock_edges = 0 + for (sig, time, val) in events: + # pyverilator generates 5 time units per sample + time = time / 5 + # pyverilator generates 4 samples per clock period + n_rising_clock_edges = int((time - last_time) / 4) + # note that the calculation of n_rising_clock_edges is approximate + # doing this exactly would require a cycle-by-cycle walkthrough of the + # trace, which can take very long + ret[str(status)] += n_rising_clock_edges + total_rising_clock_edges += n_rising_clock_edges + status[sig] = int(val) + last_time = time + + for state in ret: + v = ret[state] + ret[state] = (v, v / total_rising_clock_edges) + + return ret + + +def _get_stats(x): + return (x[0], get_stream_if_stats(x[1], x[0])) + + +def get_all_stream_if_stats(vcd_file, stream_ifs=None, sort_by="{'V': 1, 'R': 0}"): + """Return a list of streaming interface stats, sorted by the percentage + for the given sort_by key. If stream_ifs is None, all streamin interface + stats will be returned, otherwise treated as a list of interface names to + return the stats for.""" + + if stream_ifs is None: + stream_ifs = list_stream_if(vcd_file) + + with mp.Pool(get_num_default_workers()) as p: + stream_ifs = map(lambda x: (x, vcd_file), stream_ifs) + all_stats = p.map(_get_stats, stream_ifs) + + def sort_key(x): + stat = x[1] + (samples, percent) = stat[sort_by] + return percent + + ret = sorted(all_stats, key=sort_key) + return ret diff --git a/src/finn/util/vivado.py b/src/finn/util/vivado.py new file mode 100644 index 0000000000000000000000000000000000000000..0f82c52cb2c1fc5ee4ed5a1927f46e222e0ab9b5 --- /dev/null +++ b/src/finn/util/vivado.py @@ -0,0 +1,142 @@ +# Copyright (c) 2020, Xilinx +# 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 os +import subprocess +from finn.util.basic import get_remote_vivado + + +def which(program): + "Python equivalent of the shell cmd 'which'." + + # source: + # https://stackoverflow.com/questions/377017/test-if-executable-exists-in-python + def is_exe(fpath): + return os.path.isfile(fpath) and os.access(fpath, os.X_OK) + + 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) + if is_exe(exe_file): + return exe_file + + return None + + +def out_of_context_synth( + verilog_dir, + top_name, + fpga_part="xczu3eg-sbva484-1-e", + clk_name="ap_clk_0", + clk_period_ns=5.0, + remote_server=get_remote_vivado(), +): + "Run out-of-context Vivado synthesis, return resources and slack." + + # ensure that the OH_MY_XILINX envvar is set + if "OHMYXILINX" not in os.environ: + raise Exception("The environment variable OHMYXILINX is not defined.") + # ensure that vivado is in PATH: source $VIVADO_PATH/settings64.sh + if which("vivado") is None: + raise Exception("vivado is not in PATH, ensure settings64.sh is sourced.") + omx_path = os.environ["OHMYXILINX"] + if remote_server is None: + script = "vivadocompile.sh" + else: + script = "vivadoprojgen.sh" + # vivadocompile.sh <top-level-entity> <clock-name (optional)> <fpga-part (optional)> + call_omx = "zsh %s/%s %s %s %s %f" % ( + omx_path, + script, + top_name, + clk_name, + fpga_part, + float(clk_period_ns), + ) + call_omx = call_omx.split() + proc = subprocess.Popen( + call_omx, cwd=verilog_dir, stdout=subprocess.PIPE, env=os.environ + ) + proc.communicate() + + vivado_proj_folder = "%s/results_%s" % (verilog_dir, top_name) + res_counts_path = vivado_proj_folder + "/res.txt" + if remote_server is not None: + run_synth = """ +#!/bin/bash +which vivado; +cd %s; +vivado -mode tcl -source %s.tcl -tclargs %s; +cat %s + """ % ( + vivado_proj_folder, + top_name, + top_name, + res_counts_path, + ) + with open(vivado_proj_folder + "/run.sh", "w") as f: + f.write(run_synth) + # note that this assumes the same temp folder can be created on the + # remote server + remote_server_uri = remote_server + ":" + verilog_dir + copy_files = "rsync -avz %s %s" % (verilog_dir + "/", remote_server_uri + "/") + copy_files = copy_files.split() + proc = subprocess.Popen(copy_files, cwd=verilog_dir, env=os.environ) + proc.communicate() + vivado_cmd = "bash %s/run.sh" % vivado_proj_folder + run_vivado = ["ssh", "-t", remote_server, vivado_cmd] + proc = subprocess.Popen(run_vivado, cwd=verilog_dir, env=os.environ) + proc.communicate() + remote_server_result = remote_server + ":" + res_counts_path + copy_results = "rsync -avz %s %s" % (remote_server_result, res_counts_path) + copy_results = copy_results.split() + proc = subprocess.Popen(copy_results, cwd=verilog_dir, env=os.environ) + proc.communicate() + + with open(res_counts_path, "r") as myfile: + res_data = myfile.read().split("\n") + ret = {} + ret["vivado_proj_folder"] = vivado_proj_folder + for res_line in res_data: + res_fields = res_line.split("=") + print(res_fields) + try: + ret[res_fields[0]] = float(res_fields[1]) + except ValueError: + ret[res_fields[0]] = 0 + except IndexError: + ret[res_fields[0]] = 0 + if ret["WNS"] == 0: + ret["fmax_mhz"] = 0 + else: + ret["fmax_mhz"] = 1000.0 / (clk_period_ns - ret["WNS"]) + return ret diff --git a/tests/end2end/test_end2end_cnv_w1a1.py b/tests/end2end/test_end2end_cnv_w1a1.py index c3359dcc82650bf0e9e8a5bc5276f5ca770ee96c..e3f281904d7db1349d74d6eb70cad20a8f3d10af 100644 --- a/tests/end2end/test_end2end_cnv_w1a1.py +++ b/tests/end2end/test_end2end_cnv_w1a1.py @@ -72,6 +72,7 @@ from finn.util.test import get_test_model_trained, load_test_checkpoint_or_skip from finn.transformation.fpgadataflow.annotate_resources import AnnotateResources from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim from finn.transformation.fpgadataflow.insert_fifo import InsertFIFO +from finn.core.throughput_test import throughput_test_rtlsim build_dir = "/tmp/" + os.environ["FINN_INST_NAME"] test_pynq_board = os.getenv("PYNQ_BOARD", default="Pynq-Z1") @@ -142,15 +143,15 @@ def test_end2end_cnv_w1a1_fold_and_tlastmarker(): fc_layers = model.get_nodes_by_op_type("StreamingFCLayer_Batch") # each tuple is (PE, SIMD, in_fifo_depth) for a layer folding = [ - (16, 3, 128), - (32, 32, 128), - (16, 32, 128), - (16, 32, 128), - (4, 32, 81), + (16, 3, 256), + (32, 32, 256), + (16, 32, 256), + (16, 32, 256), + (4, 32, 214), (1, 32, 2), - (1, 4, 2), - (1, 8, 128), - (5, 1, 3), + (1, 4, 126), + (1, 8, 62), + (5, 1, 6), ] for fcl, (pe, simd, ififodepth) in zip(fc_layers, folding): fcl_inst = getCustomOp(fcl) @@ -159,10 +160,12 @@ def test_end2end_cnv_w1a1_fold_and_tlastmarker(): fcl_inst.set_nodeattr("inFIFODepth", ififodepth) swg_layers = model.get_nodes_by_op_type("ConvolutionInputGenerator") + swg_idepth = [2, 51, 9, 106, 2, 2] for i in range(len(swg_layers)): swg_inst = getCustomOp(swg_layers[i]) simd = folding[i][1] swg_inst.set_nodeattr("SIMD", simd) + swg_inst.set_nodeattr("inFIFODepth", swg_idepth[i]) model = model.transform(InsertDWC()) model = model.transform(InsertFIFO()) @@ -221,6 +224,20 @@ def test_end2end_cnv_w1a1_verify_dataflow_part(): assert np.isclose(res_cppsim, res_rtlsim_whole).all() +@pytest.mark.vivado +def test_end2end_cnv_w1a1_throughput_test_rtlsim(): + model = load_test_checkpoint_or_skip( + build_dir + "/end2end_cnv_w1a1_ipstitch_whole_rtlsim.onnx" + ) + model.set_metadata_prop("rtlsim_trace", "rtlsim_trace.vcd") + # os.environ["RTLSIM_TRACE_DEPTH"] = "4" + # run through IP-stitched rtlsim with increasing batch sizes and + # check the number of cycles it takes to execute + ret = throughput_test_rtlsim(model, 10) + # TODO check for expected performance + assert ret["cycles"] > 0 + + @pytest.mark.vivado def test_end2end_cnv_w1a1_verify_all(): # use the streamlined model as the "golden" model for right answers diff --git a/tests/end2end/test_end2end_tfc_w1a1.py b/tests/end2end/test_end2end_tfc_w1a1.py index 15c1c41b006c6f87d79a0e7eb6a4458838de5fd2..ebfed5e571f1e7e2499c3501c6859239a329677a 100644 --- a/tests/end2end/test_end2end_tfc_w1a1.py +++ b/tests/end2end/test_end2end_tfc_w1a1.py @@ -72,6 +72,8 @@ from finn.util.basic import pynq_part_map from finn.util.test import get_test_model_trained, load_test_checkpoint_or_skip from finn.transformation.fpgadataflow.annotate_resources import AnnotateResources from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim +from finn.core.throughput_test import throughput_test_rtlsim +import finn.util.vcd as vcd build_dir = "/tmp/" + os.environ["FINN_INST_NAME"] test_pynq_board = os.getenv("PYNQ_BOARD", default="Pynq-Z1") @@ -197,6 +199,8 @@ def test_end2end_tfc_w1a1_verify_dataflow_part(): res_rtlsim_nodebynode = ret_rtlsim_nodebynode[out_name] # whole-network (ip-stitched) rtlsim model.set_metadata_prop("exec_mode", "rtlsim") + model.set_metadata_prop("rtlsim_trace", build_dir + "/tfc_w1a1.vcd") + os.environ["RTLSIM_TRACE_DEPTH"] = "3" model.save(build_dir + "/end2end_tfc_w1a1_ipstitch_whole_rtlsim.onnx") ret_rtlsim_whole = execute_onnx(model, inp_dict, True) res_rtlsim_whole = ret_rtlsim_whole[out_name] @@ -204,6 +208,39 @@ def test_end2end_tfc_w1a1_verify_dataflow_part(): assert np.isclose(res_cppsim, res_rtlsim_whole).all() +def test_end2end_tfc_w1a1_verify_fifo_fullness(): + vcdf = build_dir + "/tfc_w1a1.vcd" + if not os.path.isfile(vcdf): + pytest.skip("Cannot find %s, skipping" % vcdf) + stream_ifs = vcd.list_stream_if(vcdf) + fifos = vcd.list_fifo_count_signals(vcdf) + assert len(stream_ifs) == 37 + assert len(fifos) == 6 + fifo_max = vcd.get_all_fifo_count_max(vcdf) + assert fifo_max[0][0] == "TOP.v.finn_design_i.StreamingFIFO_0.count[3:0]" + assert fifo_max[0][1] == 3 + stream_stat = vcd.get_all_stream_if_stats(vcdf) + assert ( + stream_stat[0][0] + == "TOP.v.finn_design_i.StreamingDataWidthConverter_Batch_0_out_V_V_" + ) + + +@pytest.mark.vivado +def test_end2end_tfc_w1a1_throughput_test_rtlsim(): + model = load_test_checkpoint_or_skip( + build_dir + "/end2end_tfc_w1a1_ipstitch_whole_rtlsim.onnx" + ) + # run through IP-stitched rtlsim with increasing batch sizes and + # check the number of cycles it takes to execute + ret = throughput_test_rtlsim(model, 1) + assert ret["cycles"] == 205 + ret = throughput_test_rtlsim(model, 10) + assert ret["cycles"] == 844 + ret = throughput_test_rtlsim(model, 100) + assert ret["cycles"] == 7234 + + @pytest.mark.vivado def test_end2end_tfc_w1a1_verify_all(): # use the streamlined model as the "golden" model for right answers diff --git a/tests/fpgadataflow/test_fpgadataflow_ip_stitch.py b/tests/fpgadataflow/test_fpgadataflow_ip_stitch.py index 16100522aa94fd25d234efa1d03edfdc866ca1bb..61dd81b728aafcd8ccc812cf0cb4c27eff00f471 100644 --- a/tests/fpgadataflow/test_fpgadataflow_ip_stitch.py +++ b/tests/fpgadataflow/test_fpgadataflow_ip_stitch.py @@ -53,6 +53,7 @@ from finn.transformation.general import GiveUniqueNodeNames from finn.util.basic import gen_finn_dt_tensor, pynq_part_map from finn.util.fpgadataflow import pyverilate_stitched_ip from finn.util.test import load_test_checkpoint_or_skip +from finn.transformation.fpgadataflow.synth_ooc import SynthOutOfContext test_pynq_board = os.getenv("PYNQ_BOARD", default="Pynq-Z1") test_fpga_part = pynq_part_map[test_pynq_board] @@ -281,6 +282,27 @@ def test_fpgadataflow_ipstitch_rtlsim(): assert (rtlsim_res == x).all() +@pytest.mark.vivado +@pytest.mark.slow +def test_fpgadataflow_ipstitch_synth_ooc(): + model = load_test_checkpoint_or_skip( + ip_stitch_model_dir + "/test_fpgadataflow_ip_stitch.onnx" + ) + model = model.transform(SynthOutOfContext(test_fpga_part, 5)) + ret = model.get_metadata_prop("res_total_ooc_synth") + assert ret is not None + # example expected output: (details may differ based on Vivado version etc) + # "{'vivado_proj_folder': ..., + # 'LUT': 708.0, 'FF': 1516.0, 'DSP': 0.0, 'BRAM': 0.0, 'WNS': 0.152, '': 0, + # 'fmax_mhz': 206.27062706270627}" + ret = eval(ret) + assert ret["LUT"] > 0 + assert ret["FF"] > 0 + assert ret["DSP"] == 0 + assert ret["BRAM"] == 0 + assert ret["fmax_mz"] > 100 + + @pytest.mark.vivado def test_fpgadataflow_ipstitch_pynq_projgen(): model = load_test_checkpoint_or_skip( diff --git a/tests/util/test_create.py b/tests/util/test_create.py new file mode 100644 index 0000000000000000000000000000000000000000..7173add35abf04a35c33b0ef10b42ffdb296a653 --- /dev/null +++ b/tests/util/test_create.py @@ -0,0 +1,64 @@ +# Copyright (c) 2020, Xilinx +# 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 pytest +import finn.util.create as create +from finn.core.datatype import DataType + + +@pytest.mark.parametrize("bitwidth", [DataType.BIPOLAR, DataType.INT2, DataType.INT4]) +def test_hls_random_mlp_maker(bitwidth): + w = bitwidth + a = bitwidth + layer_spec = [ + { + "mw": 185, + "mh": 100, + "simd": 185, + "pe": 100, + "idt": DataType.BIPOLAR, + "wdt": w, + "act": a, + }, + {"mw": 100, "mh": 100, "simd": 100, "pe": 100, "idt": a, "wdt": w, "act": a}, + {"mw": 100, "mh": 100, "simd": 100, "pe": 100, "idt": a, "wdt": w, "act": a}, + {"mw": 100, "mh": 100, "simd": 100, "pe": 100, "idt": a, "wdt": w, "act": a}, + { + "mw": 100, + "mh": 1, + "simd": 100, + "pe": 1, + "idt": a, + "wdt": w, + "act": DataType.BIPOLAR, + }, + ] + + ret = create.hls_random_mlp_maker(layer_spec) + assert len(ret.graph.node) == 5 + ret.save("mlp-%s.onnx" % str(bitwidth))