Skip to content

Commit bb73ece

Browse files
authored
Merge pull request #1454 from Xilinx/debug/float_ooc
Enable floating point ip for out of context synthesis
2 parents 19c9844 + daf0f24 commit bb73ece

5 files changed

Lines changed: 215 additions & 4 deletions

File tree

docker/finn_entrypoint.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@ else
151151
echo "See https://docs.xilinx.com/r/en-US/ug835-vivado-tcl-commands/Tcl-Initialization-Scripts"
152152
fi
153153

154+
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$VITIS_PATH/lnx64/tools/fpo_v7_1"
155+
154156
export PATH=$PATH:$HOME/.local/bin
155157
# execute the provided command(s) as root
156158
exec "$@"

fetch-repos.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ FINN_EXP_COMMIT="0724be21111a21f0d81a072fccc1c446e053f851"
3333
BREVITAS_COMMIT="4617f7bd136e96fa21c7f76e3c7e2e37fe563837"
3434
CNPY_COMMIT="8c82362372ce600bbd1cf11d64661ab69d38d7de"
3535
HLSLIB_COMMIT="5dde96382b84979c6caa6f34cdad2ac72fa28489"
36-
OMX_COMMIT="0b59762f9e4c4f7e5aa535ee9bc29f292434ca7a"
36+
OMX_COMMIT="a5d48f93309b235fdd21556d16e86e6ef5db6e2e"
3737
AVNET_BDF_COMMIT="2d49cfc25766f07792c0b314489f21fe916b639b"
3838
XIL_BDF_COMMIT="8cf4bb674a919ac34e3d99d8d71a9e60af93d14e"
3939
RFSOC4x2_BDF_COMMIT="13fb6f6c02c7dfd7e4b336b18b959ad5115db696"

src/finn/transformation/fpgadataflow/synth_ooc.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,23 @@
2727
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2828

2929
import os
30+
from qonnx.custom_op.registry import getCustomOp
3031
from qonnx.transformation.base import Transformation
3132
from shutil import copy2
3233

3334
from finn.util.basic import make_build_dir
35+
from finn.util.fpgadataflow import is_hls_node
3436
from finn.util.vivado import out_of_context_synth
3537

3638

39+
def is_hls_float_op(node, model):
40+
if is_hls_node(node):
41+
for inp in node.input:
42+
if model.get_tensor_datatype(inp).name.startswith("FLOAT"):
43+
return True
44+
return False
45+
46+
3747
class SynthOutOfContext(Transformation):
3848
"""Run out-of-context Vivado synthesis on a stitched IP design."""
3949

@@ -58,8 +68,18 @@ def file_to_basename(x):
5868
for file in all_verilog_srcs:
5969
if any([file.endswith(x) for x in verilog_extensions]):
6070
copy2(file, build_dir)
71+
# extract additional tcl commands to set up floating-point ips correctly
72+
float_ip_tcl = []
73+
for node in model.graph.node:
74+
if is_hls_float_op(node, model):
75+
code_gen_dir = getCustomOp(node).get_nodeattr("code_gen_dir_ipgen")
76+
verilog_path = "{}/project_{}/sol1/impl/verilog/".format(code_gen_dir, node.name)
77+
file_suffix = ".tcl"
78+
for fname in os.listdir(verilog_path):
79+
if fname.endswith(file_suffix):
80+
float_ip_tcl.append(verilog_path + fname)
6181
ret = out_of_context_synth(
62-
build_dir, top_module_name, self.part, self.clk_name, self.clk_period_ns
82+
build_dir, top_module_name, float_ip_tcl, self.part, self.clk_name, self.clk_period_ns
6383
)
6484
model.set_metadata_prop("res_total_ooc_synth", str(ret))
6585
return (model, False)

src/finn/util/vivado.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
def out_of_context_synth(
3535
verilog_dir,
3636
top_name,
37+
float_ip_tcl,
3738
fpga_part="xczu3eg-sbva484-1-e",
3839
clk_name="ap_clk_0",
3940
clk_period_ns=5.0,
@@ -48,11 +49,12 @@ def out_of_context_synth(
4849
raise Exception("vivado is not in PATH, ensure settings64.sh is sourced.")
4950
omx_path = os.environ["OHMYXILINX"]
5051
script = "vivadocompile.sh"
51-
# vivadocompile.sh <top-level-entity> <clock-name (optional)> <fpga-part (optional)>
52-
call_omx = "zsh %s/%s %s %s %s %f" % (
52+
# vivadocompile.sh <top-level-entity> <fp0.tcl#fp1.tcl> <clk-name (opt)> <fpga-part (opt)>
53+
call_omx = "zsh %s/%s %s %s %s %s %f" % (
5354
omx_path,
5455
script,
5556
top_name,
57+
'"%s"' % "#".join(float_ip_tcl),
5658
clk_name,
5759
fpga_part,
5860
float(clk_period_ns),
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
############################################################################
2+
# Copyright (C) 2025, Advanced Micro Devices, Inc.
3+
# All rights reserved.
4+
#
5+
# SPDX-License-Identifier: BSD-3-Clause
6+
#
7+
# ##########################################################################
8+
9+
import pytest
10+
11+
import numpy as np
12+
from onnx import TensorProto, helper
13+
from qonnx.core.datatype import DataType
14+
from qonnx.core.modelwrapper import ModelWrapper
15+
from qonnx.transformation.general import GiveUniqueNodeNames
16+
from qonnx.transformation.infer_datatypes import InferDataTypes
17+
from qonnx.transformation.infer_shapes import InferShapes
18+
from qonnx.util.basic import gen_finn_dt_tensor
19+
20+
import finn.core.onnx_exec as oxe
21+
import finn.transformation.fpgadataflow.convert_to_hw_layers as to_hw
22+
from finn.transformation.fpgadataflow.create_stitched_ip import CreateStitchedIP
23+
from finn.transformation.fpgadataflow.hlssynth_ip import HLSSynthIP
24+
from finn.transformation.fpgadataflow.prepare_ip import PrepareIP
25+
from finn.transformation.fpgadataflow.prepare_rtlsim import PrepareRTLSim
26+
from finn.transformation.fpgadataflow.set_exec_mode import SetExecMode
27+
from finn.transformation.fpgadataflow.set_fifo_depths import InsertAndSetFIFODepths
28+
from finn.transformation.fpgadataflow.specialize_layers import SpecializeLayers
29+
from finn.transformation.fpgadataflow.synth_ooc import SynthOutOfContext
30+
31+
fpga_part = "xczu7ev-ffvc1156-2-e"
32+
clk_ns = 10
33+
34+
35+
def generate_random_threshold_values(data_type, num_input_channels, num_steps):
36+
if data_type.is_integer():
37+
return np.random.randint(
38+
data_type.min(),
39+
data_type.max() + 1,
40+
(num_input_channels, num_steps),
41+
).astype(np.float32)
42+
else:
43+
return (np.random.randn(num_input_channels, num_steps) * 1000).astype(
44+
data_type.to_numpy_dt()
45+
)
46+
47+
48+
def create_test_model():
49+
W = gen_finn_dt_tensor(DataType["INT4"], (16, 32))
50+
T = np.sort(
51+
generate_random_threshold_values(
52+
DataType["FLOAT32"],
53+
1,
54+
DataType["INT8"].get_num_possible_values() - 1,
55+
),
56+
axis=1,
57+
)
58+
MulParam = gen_finn_dt_tensor(DataType["FLOAT32"], [1])
59+
AddParam = gen_finn_dt_tensor(DataType["FLOAT32"], [1, 4, 32])
60+
61+
# Initialize a new graph
62+
nodes = []
63+
64+
# Add nodes
65+
mt_op = helper.make_node(
66+
"MultiThreshold",
67+
inputs=["inp", "thresh"],
68+
outputs=["mt_output"],
69+
domain="qonnx.custom_op.general",
70+
out_dtype="INT8",
71+
out_bias=float(DataType["INT8"].min()),
72+
)
73+
nodes.append(mt_op)
74+
75+
matmul_op = helper.make_node(
76+
"MatMul",
77+
inputs=["mt_output", "matmul_weight"],
78+
outputs=["matmul_output"],
79+
)
80+
nodes.append(matmul_op)
81+
82+
scalar_mul_op = helper.make_node(
83+
"Mul",
84+
inputs=["matmul_output", "scalar_input"],
85+
outputs=["scalar_output"],
86+
)
87+
nodes.append(scalar_mul_op)
88+
89+
channel_add_op = helper.make_node(
90+
"Add",
91+
inputs=["scalar_output", "channelwise_bias"],
92+
outputs=["final_output"],
93+
)
94+
nodes.append(channel_add_op)
95+
96+
# Define inputs
97+
inputs = [
98+
helper.make_tensor_value_info("inp", TensorProto.FLOAT, [1, 4, 16]),
99+
]
100+
101+
# Define outputs
102+
outputs = [helper.make_tensor_value_info("final_output", TensorProto.FLOAT, [1, 4, 32])]
103+
104+
value_info = [
105+
helper.make_tensor_value_info("mt_output", TensorProto.FLOAT, [1, 4, 16]),
106+
helper.make_tensor_value_info("thresh", TensorProto.FLOAT, [1, 255]),
107+
helper.make_tensor_value_info("matmul_output", TensorProto.FLOAT, [1, 4, 32]),
108+
helper.make_tensor_value_info("matmul_weight", TensorProto.FLOAT, [16, 32]),
109+
helper.make_tensor_value_info("scalar_input", TensorProto.FLOAT, [1]),
110+
helper.make_tensor_value_info("scalar_output", TensorProto.FLOAT, [1, 4, 32]),
111+
helper.make_tensor_value_info("channelwise_bias", TensorProto.FLOAT, [1, 4, 32]),
112+
]
113+
114+
# Create the graph
115+
graph = helper.make_graph(
116+
nodes=nodes, name="TestModelGraph", inputs=inputs, outputs=outputs, value_info=value_info
117+
)
118+
119+
# Create the model
120+
model = helper.make_model(graph, opset_imports=[helper.make_opsetid("", 11)])
121+
model = ModelWrapper(model)
122+
123+
# Set initializers and datatypes
124+
model.set_initializer("matmul_weight", W)
125+
model.set_initializer("thresh", T)
126+
model.set_initializer("scalar_input", MulParam)
127+
model.set_initializer("channelwise_bias", AddParam)
128+
129+
model.set_tensor_datatype("inp", DataType["FLOAT32"])
130+
model.set_tensor_datatype("matmul_weight", DataType["INT4"])
131+
model.set_tensor_datatype("thresh", DataType["FLOAT32"])
132+
model.set_tensor_datatype("scalar_input", DataType["FLOAT32"])
133+
model.set_tensor_datatype("channelwise_bias", DataType["FLOAT32"])
134+
135+
return model
136+
137+
138+
@pytest.mark.end2end
139+
@pytest.mark.vivado
140+
@pytest.mark.slow
141+
def test_ooc_synthesis():
142+
model = create_test_model()
143+
model = model.transform(InferShapes())
144+
model = model.transform(InferDataTypes())
145+
146+
# generate reference output
147+
x = gen_finn_dt_tensor(DataType["FLOAT32"], (1, 4, 16))
148+
y_dict = oxe.execute_onnx(model, {model.graph.input[0].name: x})
149+
y_ref = y_dict[model.graph.output[0].name]
150+
151+
# infer and specialize layers
152+
model = model.transform(to_hw.InferThresholdingLayer())
153+
model = model.transform(to_hw.InferElementwiseBinaryOperation())
154+
model = model.transform(to_hw.InferQuantizedMatrixVectorActivation())
155+
model = model.transform(SpecializeLayers(fpga_part))
156+
157+
# node-by-node rtlsim
158+
model = model.transform(GiveUniqueNodeNames())
159+
model = model.transform(SetExecMode("rtlsim"))
160+
model = model.transform(PrepareIP(fpga_part, clk_ns))
161+
model = model.transform(HLSSynthIP())
162+
model = model.transform(PrepareRTLSim())
163+
164+
y_dict = oxe.execute_onnx(model, {model.graph.input[0].name: x})
165+
y_prod = y_dict[model.graph.output[0].name]
166+
assert (y_prod == y_ref).all()
167+
168+
# FIFO sizing
169+
model = model.transform(InsertAndSetFIFODepths(fpga_part, clk_ns))
170+
171+
# stitched IP rtlsim
172+
model = model.transform(PrepareIP(fpga_part, clk_ns))
173+
model = model.transform(HLSSynthIP())
174+
model = model.transform(CreateStitchedIP(fpga_part, clk_ns))
175+
model = model.transform(SynthOutOfContext(fpga_part, clk_ns))
176+
ret = model.get_metadata_prop("res_total_ooc_synth")
177+
assert ret is not None
178+
# example expected output: (details may differ based on Vivado version etc)
179+
# "{'vivado_proj_folder': ...,
180+
# 'LUT': 708.0, 'FF': 1516.0, 'DSP': 0.0, 'BRAM': 0.0, 'WNS': 0.152, '': 0,
181+
# 'fmax_mhz': 206.27062706270627}"
182+
ret = eval(ret)
183+
assert ret["LUT"] > 0
184+
assert ret["FF"] > 0
185+
assert ret["DSP"] > 0
186+
assert ret["BRAM"] > 0
187+
assert ret["fmax_mhz"] > 100

0 commit comments

Comments
 (0)