Vector Multiply — measuring and modelling what a design costs

This example builds on memcpy. That one introduced the free-running kernel — a block that is never started and never returns, paced only by back-pressure. vecmult is the smallest design in the tree that takes such a kernel all the way to a number: how many LUTs, flip-flops, DSPs and block RAMs it occupies, and how that changes as you turn its knobs.

That is the subject here. The arithmetic is deliberately trivial — z = x * y, element-wise — because the interesting content is not the compute. It is how little you have to say to get a resource model you can trust: you describe what the design contains — this many multipliers, this many memory banks, this many lanes — and the library chooses the features, prices the device geometry and fits whatever is left over.

fir_block also models its resources, but it does so while simultaneously teaching cross-firing state, fixed point, a four-module composite and RTL verification. This example does one thing.

The design

One firing carries a command, both operands, and a response:

s_in:   [ cmd(tx_id, n) | x_0 .. x_{n-1} | y_0 .. y_{n-1} ]
z_out:  [ z_0 .. z_{n-1} | resp(tx_id) ]

Two parameters, and the difference between them is load-bearing:

     
dwid stream word width sets the lane count LW = dwid / 16
vlen compile-time bound on the buffer sets the BRAM cost
n runtime length in the command costs nothing in area — but changes the shape of the logic

Because x and y share one port they arrive sequentially, so the kernel buffers x while y streams past. That buffer has to be read LW samples per cycle to sustain II=1, which forces a cyclic ARRAY_PARTITION — and that is what turns a throughput requirement into a memory cost.

It is worth knowing what does not force the buffer, because the plausible reason is false. Two separate input streams would need no buffer: distinct FIFOs are independent ports and their reads schedule in the same beat. Measured, that design runs at II=1 with zero BRAM. A shared port is what makes storage unavoidable — the buffer is a consequence of the interface, not the arithmetic.

Learning Objectives

In going through this example, you will learn to:

  1. Build a standalone free-running module — one FreeRunMod, one stream in, one out, with an in-band command and a response that echoes a transaction id.
  2. Hand off a kernel body the extractor cannot write — declare a hand-written hls::task through kernel_task(), and keep the Python run_iter as the golden.
  3. Prove the twin — replay the pysim job through the C++ in Vitis C-simulation, so “Python golden, C++ twin” is a checked claim rather than an intention.
  4. Write a parameter sweep with sweep_cli — declare the points as a ParamGrid, the run as a SweepRunner and one Stage, and get a program with --dry-run, --resume and a flag per axis; then collect an attributed utilization report at each of the 16 points.
  5. Describe a design’s structure and install a model on the moduleresource_structure and get_rm — reading each declared number straight off the kernel body, and getting DSP and BRAM exactly right before any synthesis has run.
  6. Sweep for the measurements the declaration cannot give you, and let VitisResourceModel choose the features and fit the coefficients for LUT and FF.
  7. Compose an estimate over a hierarchyadd_rm, compose — and read the confidence it reports, which is the weakest link rather than the best one.

The build

python -m examples.vecmult.vecmult_build --list-steps
#   vecmult_source -> pysim -> codegen_dut -> csim -> csynth -> resources

python -m examples.vecmult.vecmult_build --through pysim        # no toolchain, seconds
python -m examples.vecmult.vecmult_build --through resources    # needs Vitis, ~40 s
python -m examples.vecmult.vecmult_sweep                        # the 16-point grid, ~15 min

csynth consumes the csim verdict, so a design whose C++ disagrees with its Python golden cannot reach synthesis and cannot contribute a resource measurement.

There is no RTL rung here. The resource counters are settled at C-synthesis, so measuring them needs no RTL simulation — but that also means this example verifies function (csim) and cost (csynth) without verifying cycle behaviour. For a design that closes that too, see fir_block and its XSI gate.

In this section

  • The module — the standalone FreeRunMod, its ports, and the command/response protocol.
  • The kernel — the hand-written task: why it buffers, why it partitions, and the ragged final beat.
  • Testbench — the pysim golden and the csim twin check, driven from one set of vectors.
  • DUT codegen — how one leaf lowers to an ap_ctrl_none top, what template_args reaches, and the two kinds of file in include/.
  • Testbench codegen — what is generated for the csim rung and what deliberately is not.
  • The resource model — what VitisResourceModel does for you, the six terms it asks for and where each number comes from, and what it already predicts with nothing measured.
  • The sweep — how the sweep script is written, 16 design points through the DAG, and the committed corpus.
  • How well it fits — the model checked against points held out of its own fit, plus an appendix on why it is built the way it is.

See also


Table of contents

  • The module - VecMult as a standalone free-running module: two stream ports, an in-band command carrying a transaction id and a runtime length, a response that echoes the id, two parameters whose difference is load-bearing — vlen is the compile-time bound the area is priced against, n is the runtime length that costs nothing — and one shared golden() so the twin check has something real to disagree with.
  • The kernel - The hand-written vec_mult_task.h: why a shared input port forces a buffer (and why two separate streams would not), why the buffer is firing-local rather than declared state, why sustaining II=1 forces a cyclic ARRAY_PARTITION, how the ragged final beat is handled, and why the product wraps rather than saturates. This is the file every resource number on the example traces back to.
  • Testbench - Two bodies, one behaviour, and the rung that makes them agree. The pysim testbench drives one job through the SimPy model; the csim rung replays that exact job through the hand-written C++ and compares element for element. csynth consumes the verdict, so a design whose twin check failed cannot contribute a resource measurement.
  • DUT codegen - How one standalone FreeRunMod lowers to an ap_ctrl_none hls::task top: a leaf walks the same path a composite does and simply has nothing to wire, template_args become the instantiation and the RTL entity name resource attribution matches on, and include/ ends up holding two kinds of file — generated headers that are overwritten every build and the hand-written body that is copied in and must never be.
  • Testbench codegen - What is generated for the csim rung and what deliberately is not. The C++ testbench is hand-written, because a csim TB is a main() and not a graph; what is generated is vec_mult_params.h, the one header that stops the testbench drifting from the DUT's knobs and points it at the vectors the pysim rung wrote. Also why the TB calls the task rather than the top, and why there are two TCLs.
  • The resource model - VecMult's resource model, before anything is measured. Its two knobs map onto a structure declaration — LW multipliers and LW memory banks of vlen/LW, both read straight off vec_mult_task.h — plus a LutFfBasis saying which quantities LUT and FF are allowed to grow in. get_rm installs a VitisResourceModel that prices the first two exactly with zero syntheses run and fits the last two; the basis is chosen by declare-validate-add rather than by reasoning alone.
  • The sweep - Driving 16 design points through the build DAG to get an attributed resource report per point. How the sweep script is written — a ParamGrid, a SweepRunner, one Stage and sweep_cli — and what the grid was designed to separate: the two BRAM regimes that look like different laws and are one ceiling. Also the committed corpus, and why it is Python source rather than the sweep's JSON.
  • How well it fits - The model with both halves, checked against points held out of its own fit. DSP, BRAM and LUT are all exact at every one of the 16 measurements — LUT including the point where the buffer leaves block RAM, because a device rule prices the distributed RAM it became. FF is 1.7% in-regime and 3.5% at that corner, which is the one gap left and is reported rather than hidden. Then an appendix: why the obvious basis fails by 43%, and how the LUTRAM rule was predicted before it was measured.