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 typesStreamIFSlave 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_top is the same function that emits mem_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 invec_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 typeSamp 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.