Repository navigation
Simulation Internals
How FINN+ executes models and checks them. For configuring verification in a build, see Functional Verification.
All verification goes through finn.core.onnx_exec, which walks the
graph and dispatches each node to a handler. That is what allows a graph mixing standard ONNX nodes,
QONNX custom nodes and HLS/RTL hardware nodes to execute at all: standard nodes go to ONNX Runtime,
custom nodes to their CustomOp.execute_node().
Because tensors are stored as float32 carriers for integer values (see
Implementation Details), execution can accumulate
rounding error; qonnx.util.basic.sanitize_quant_values cleans integer tensors during the custom
execution flow.
A hardware node's exec_mode attribute selects its handler, set in bulk by
SetExecMode:
exec_mode |
Path |
|---|---|
"" (unset) |
Python implementation on the abstract HW op |
"cppsim" |
Compiled C++ from finn-hlslib |
"rtlsim" |
XSI RTL simulation |
Important
RTL components do not support cppsim. If cppsim is selected for an RTL component, execution falls back to the abstract HW-op parent. A cppsim pass over a design containing RTL variants therefore does not exercise those variants' implementations โ worth remembering when a design passes cppsim and fails rtlsim.
Preparation is PrepareCppSim (generate C++) then CompileCppSim (compile it). Both are
NodeLocalTransformations, so they parallelize across nodes โ see
Writing Custom Transformations.
CompileCppSim requires code_gen_dir_cppsim to be set and raises FINNUserError telling you to
run PrepareCppSim first otherwise. The generated code lives under FINN_BUILD_DIR; executing a
node is literally running that binary on text files, so it can be debugged by hand โ see
Custom Operations.
finn.xsi wraps Vivado's XSI. Its module docstring documents the
contract precisely:
- Importing it is always safe โ it touches neither FINN settings nor the filesystem, so it can
be imported from
multiprocessingworkers spawned withforkserver/spawn. - The availability check and any Vivado-version-triggered rebuild happen only in
ensure_available(). - That call is made once, early, by
run_finn.py'sprepare_finn()for the CLI and bytests/conftest.py'spytest_configure()for tests โ both before any parallel work is dispatched, so the build is never raced.
If XILINX_VIVADO is unset, is_available() raises
OSError: XILINX_VIVADO environment variable not set. In pytest this surfaces as an
INTERNALERROR during collection, which is an environment problem, not a test failure.
rtlsim_exec.py drives stitched-IP simulation.
verify_step implements every entry in
verify_steps. Its shape:
- Normalize the graph โ
SortGraph,GiveUniqueNodeNamesRecursive,GiveReadableTensorNamesโ so that dumps are readable and stable. - Resolve the verification IO pair via
cfg._resolve_verification_io_pair(), which validates that both.npyfiles exist and asserts matching batch sizes. - For each sample: execute, compare, record.
- Write
verification_output/verify_<step>_<batch>_<SUCCESS|FAIL>.npzwhenverify_save_full_contextis on, plus per-tensor statistics in topological order.
Input shapes that do not match the model are reshaped with a warning rather than rejected.
After step_create_dataflow_partition the model is a parent graph containing a
StreamingDataflowPartition node plus a child graph. Verification of those steps passes
need_parent=True, which:
- asserts
save_intermediate_modelsis enabled, - loads
intermediate_models/dataflow_parent.onnx, - saves the current model as
verify_<step>.onnx, - runs
execute_parent(...)over both.
Tensor names in the dump are then prefixed with the SDP node name.
Three checks, all of which must pass:
res1 = np.isclose(out, exp, atol=cfg.verification_atol, rtol=cfg.verification_rtol).all()
res2 = np.mean(np.abs(out - exp)) <= cfg.verification_mean_atol
res3 = np.mean(np.abs((out - exp) / exp_safe)) <= cfg.verification_mean_rtolexp_safe substitutes np.finfo(float).eps for zeros to avoid division by zero. The mean checks
exist because element-wise closeness alone is brittle โ one near-zero output element can fail
isclose on an otherwise correct model.
verify_step accepts an optional rtlsim_pre_hook, passed through to execute_onnx. This is how
MLO drives a rolled design via verify_mlo. Note that
context saving is skipped whenever a pre-hook is present.
Tracing is controlled by the rtlsim_trace node attribute (node-by-node) or graph metadata property
(stitched IP), and RTLSIM_TRACE_DEPTH:
| Depth | Shows |
|---|---|
| 1 (default) | Top-level input/output streams |
| 2 | Per-layer input/output streams |
| 3 | Per full-layer I/O including FIFO count signals |
step_measure_rtlsim_performance temporarily forces depth 3 when
verify_save_rtlsim_waveforms is set, restoring the original afterwards.
The layer-parallel simulation used for FIFO sizing is the same machinery:
simulation_build.py compiles per-node backends and simulation_connected.py runs them as
communicating processes, optionally across MPI. step_measure_rtlsim_performance constructs a
NodeConnectedSimulation directly with performance_sim=True.
- Functional Verification โ the user-facing guide
- FIFO Sizing Internals โ same simulation infrastructure
- Custom Operations โ how a node provides its execution paths
- Multi-Layer Offload Internals
๐ Home
Using FINN+ to compile a network.
Getting started
Building
Tuning
Checking
Reference
Working on the FINN+ compiler itself.
Getting started
Extending FINN+
Compiler internals
- Network Preparation
- Hardware Build
- Folding
- FIFO Sizing
- Multi-Layer Offload
- Simulation & Verification
Testing
Reference
- FINN+ Repository
- Brevitas - Quantization library
- QONNX - Model format
- finn-examples - Pre-built accelerators