DUT codegen — one leaf becomes an hls::task top
The module is a graph with exactly one node. Generating the DUT walks it and lowers it to a free-running Vitis HLS top, alongside every header the hand-written body includes.
python -m examples.vecmult.vecmult_build --through codegen_dut
# generated gen\vec_mult.cpp + vec_mult.tcl (dwid=64, vlen=4096, LW=4)
The top
The whole generated file, at the default point:
void vec_mult(
hls::stream<ap_uint<64> >& s_in,
hls::stream<ap_uint<64> >& z_out
) {
#pragma HLS INTERFACE axis port=s_in
#pragma HLS INTERFACE axis port=z_out
#pragma HLS INTERFACE ap_ctrl_none port=return
hls_thread_local hls::task t0(vec_mult_task<64, 4096>, s_in, z_out);
}
Twenty lines, and none of them are VecMult-specific in the sense of having been written for it.
Every one is read off the graph:
| what the generator emits | where it comes from |
|---|---|
| the top’s name | cpp_kernel_name = "vec_mult" |
| the two port names, in that order | add_endpoint order in __post_init__ |
hls::stream<ap_uint<64> > and the direction |
the endpoint types — StreamIFSlave in, StreamIFMaster out |
INTERFACE axis rather than a framed_word FIFO |
has_tlast=False on both endpoints |
ap_ctrl_none port=return |
it is a FreeRunMod |
vec_mult_task<64, 4096> |
kernel_task()’s template_args |
A leaf walks the same path a composite does.
render_topis the same function that emitsmem_copy’s three-task top (DUT codegen); nothing branches on whether the module has children. A leaf is simply a graph with no internal interfaces to lower and one task to instantiate, so the internal-FIFO section of the emitted file is empty and the boundary ports are the leaf’s own. This is worth knowing because it is what makes the two examples comparable: the difference you see between the two generated tops is a difference in the graphs, not in two code paths that happen to agree.
template_args reaches further than the instantiation
Both knobs are baked into the C++ template, and they have to be. VLEN sizes buf, which must be a
compile-time extent for ARRAY_PARTITION to mean anything, and DWID sets the lane count the
partition factor is derived from. A runtime vlen would not be a different-sized buffer — it would
be no partition at all.
The instantiation then names the RTL entity, which is the part that matters downstream:
vec_mult_task<64, 4096> -> vec_mult_task_64_4096_s
That name is what resource attribution matches on to assign a
synthesis report’s counters to this module at this configuration. It is derived from the template
arguments rather than tabulated anywhere, which is why a sweep can add a new (dwid, vlen) point
without anybody registering it.
Two kinds of artifact in include/
The directory the top and the body both include from holds two populations, and confusing them is how a hand-written body gets silently overwritten:
Generated, rewritten on every build — regenerate rather than edit:
| file | from |
|---|---|
vec_cmd.h, vec_resp.h |
DataSchemaStep over VecCmd / VecResp |
int16_array.h, int16_array_utils.h |
the array-utils generator for Samp |
streamutils_hls.h, memmgr.hpp |
the framework’s stream / memory utilities |
vecmult_types.h |
render_types_h — see below |
vec_mult_params.h |
the knobs the testbench needs |
Hand-written, copied in — vec_mult_task.h, listed in HAND_WRITTEN_TASKS. The build copies it
from the example root into include/ and raises if it is missing rather than proceeding without
it, because a body that silently failed to arrive would surface as a link error hundreds of lines
into a Vitis log.
vecmult_types.h — one alias, and why it exists
// GENERATED by vecmult_build.render_types_h -- do not edit.
#include "int16_array_utils.h"
namespace vm_au = int16_array_utils;
That is the entire file, and the indirection earns its keep. The array-utils namespace is named after
the element type — Samp is a 16-bit signed int, so the generator emits int16_array_utils. A
body that spelled that name directly would compile today and break the moment the sample width
changed, in a file nobody would think to look at. vm_au is stable under that change, so
vec_mult_task.h never names a generated symbol whose spelling it does not control.
This is the same discipline as never hand-rolling .range() packing — the generated serializer is
the interface, and the body’s only job is to call it by a name that will keep meaning the same thing.
The TCL, and what csynth needs
render_tcl emits vec_mult.tcl beside the top: open a project, add gen/vec_mult.cpp with
-Iinclude, set the part and the clock, csynth_design. It is csynth-only — there is no
testbench hook in it at all, which is why the csim rung has a
second, hand-written TCL.
The DAG makes the ordering explicit:
codegen_dut -> csim -> csynth -> resources
csynth consumes csim_verdict, so a body whose C++ disagrees with the Python golden cannot reach
synthesis and cannot contribute a resource measurement. A wrong design that synthesizes cleanly would
otherwise produce a perfectly good-looking row in the corpus.
Next
- Testbench codegen — what is generated for the csim rung, and what deliberately is not.
- The sweep — running this codegen at 16 points.