Testbench
VecMult has two implementations of one behaviour: run_iter in Python and vec_mult_task.h in
C++. That is a liability until something checks them against each other — and the check is easy to
believe you have when you do not.
pysimproves the Python body is right.csynthproves the C++ body compiles and schedules.- Neither proves they compute the same thing.
Every rung can be green while the two disagree. Closing that is what this page is about.
The pysim harness
An in-memory graph: a source that issues one job, the DUT, and a capture.
@dataclass
class VecMultTB(FreeRunMod):
"""source -> DUT -> capture, over one stream in and one out."""
Both ends use the endpoint’s own schema methods (write / get), never a hand-rolled word split, so
the packing under test is the same generated serializer on both sides of the comparison.
got, exp, echoed = run_one(dwid=64, vlen=4096, n=4093, seed=0, tx_id=0x5A5A)
n = vlen - 3 by default, which is deliberately ragged: not a multiple of LW, so the partial
final beat is exercised. A full-length job never reaches it.
This example writes its vectors in memory, not as burst bundles.
state_toyandmem_copywrite bundles because the same bytes have to drive an RTL testbench too;vecmulthas no RTL rung, so a file round-trip would buy nothing and cost the reader a level of indirection.
The csim twin check
The csim rung replays that exact job through the C++ and compares:
source -> pysim -> codegen_dut -> csim -> csynth -> resources
pysim writes the stimulus and the expectation to data/; vec_mult_tb.cpp reads them back, packs
them onto an hls::stream, calls the task, and checks every sample plus the tx_id echo.
vec_mult_task<VM_DWID, VM_VLEN>(s_in, z_out);
Three details that matter:
It calls the task function, not the generated top. vec_mult is ap_ctrl_none with an
hls::task — it never returns, so csim of it would spin forever. The task body is the artifact under
test anyway; the top is generated and carries no arithmetic.
The expectation comes from Python. A testbench that computed its own expected output would be
checking the C++ against itself. data/ holds x, y and z_expected, all written by the pysim
rung, so there is exactly one definition of the job under test.
csynth consumes the verdict. A design whose C++ disagrees with its golden cannot reach synthesis
and therefore cannot contribute a resource measurement:
consumes = ["vec_mult_cpp", "run_tcl", "csim_verdict"]
The gate can actually fail
A green gate that cannot go red is worse than no gate, because it buys false confidence. This one was verified by corrupting a single expected sample:
TB MISMATCH at 7: got 26649 expected 26650
WAVEFLOW_CSIM_FAIL: 1 mismatch(es) over n=4093
Worth doing once for any check you intend to rely on.
What is not checked
Cycle behaviour. csim runs the C++ as software: it proves the arithmetic and the framing, not the schedule, the back-pressure, or the handshakes in RTL. The resource counters this example exists for are settled at C-synthesis, so nothing here needs RTL — but the gap is real and worth naming rather than leaving implied.
For a design that closes it, see fir_block and its
XSI gate, which drives the elaborated RTL cycle by cycle through a BFM.
The toolchain-free gates
tests/examples/test_vecmult.py runs with no Vitis installed, and covers what would otherwise rot
silently:
| test | what it pins |
|---|---|
test_pysim_matches_golden |
every lane width, LW=2 … 16 |
test_ragged_lengths |
n ∈ {1, 7, 63, 64, 65, 253} — the partial-beat off-by-ones |
test_response_echoes_the_transaction_id |
the transaction closes |
test_runtime_length_does_not_change_the_hardware |
n is runtime, vlen is structure |
test_golden_wraps_rather_than_saturates |
the golden truncates like the C++, not saturates |
test_dsp_prior_is_exact_on_every_point |
the DSP rule against the committed corpus |
test_bram_prior_is_exact_on_every_point |
the BRAM rule, all 16 |
test_crossbar_basis_predicts_lut_and_ff_held_out |
the fit generalizes, leave-one-out |
test_naive_linear_basis_is_not_good_enough |
why the basis has quadratic terms |
The last one is the unusual entry: it asserts that the obvious feature set is >30% wrong. Without it the quadratic terms would read as unexplained curve-fitting, and a later “simplification” back to linear-in-width would look harmless.
Next
- DUT codegen — the
ap_ctrl_nonetop this rung synthesizes, and the headers the body includes. - The sweep — 16 design points, and the grid that separates the two BRAM regimes.