Parameterization
Both flows build on a HwModule, and either kind of component can be parameterized — the synthesis
surface lives on HwModule itself, so this page applies to a HostActivated kernel and a
FreeRunMod alike.
Why parameterize
A HwModule is a template for hardware, not a single fixed block. Parameterizing
it lets one class describe a family of hardware: the same datapath sized for a 32-bit bus or a
64-bit bus, an accelerator built for 8-bit or 16-bit operands, a coefficient bank with 4 or 8 taps.
That buys three things:
- One source, many concrete kernels. A single class emits several Vitis tops at build time, one
per configuration — see
param_supportsbelow. - Sizing the datapath. Bus widths and lane counts ripple through the generated port signatures and the lane geometry, so the parameter is the hardware size.
- Reusable IP. A component parameterized on its widths/shapes is reusable across designs without editing its body.
Waveflow’s parameter markers are all one axis — when does the value bind? — and where a knob sits on it decides the thing you actually care about: whether changing it means building a new artifact.
| marker | binds at | one artifact per value? |
|---|---|---|
HwConst[T] |
class definition | fixed structurally |
HwParam[T] |
build / elaboration | no — distinct values are distinct artifacts (mem_r_stream_32 vs _64) |
DynParam[T] |
init / pre-sim | yes |
regmap / s_axilite |
runtime, over AXI-Lite | yes — one bitstream serves every value |
HwParam is the one synthesizable code can take, and most of this page is about it. The bottom two
rows are the same idea at different times: a value set on a built thing rather than baked into it.
HwParam[T] — per-instance synthesis parameters
HwParam[T] marks a dataclass field as a synthesis parameter
bound at instantiation — comp = MyComp(in_bw=64) — and potentially varied per generated kernel
variant. From examples/stream_inband/poly.py:
in_bw: HwParam[int] = 32
out_bw: HwParam[int] = 32
aximm_bw: HwParam[int] = 32
(VMAC parameterizes its operand / accumulator / output widths the same way.) Three properties matter:
- Int-like in simulation. A
HwParamfield behaves as a normal Python integer — arithmetic, comparison, indexing all work — so the SimPy model runs with the concrete value. - Identity-preserving for codegen.
HwModule.__post_init__wraps eachHwParamvalue as anHwParamValue— anintsubclass that remembers the.param_nameit came from. That lets the emitter decide between a C++ template parameter name and a literal value (the realization page). - Immutable after construction.
HwModule.__setattr__raises if you reassign aHwParamfield once__post_init__has finished — the value is frozen for the instance’s hardware identity.
HwConst[T] — class-level structural constants
HwConst[T] marks a class attribute that is fixed for the
class — the same for every instance — typically a structural extent like a static array size. From its
docstring:
class CoeffArray(DataArray):
ncoeff: HwConst[int] = 4
max_shape = (ncoeff,)
In Python simulation a HwConst is just a regular class attribute (the framework does not enforce
immutability — the marker signals intent). discover_hw_const(cls)
walks the MRO and returns every HwConst field so codegen can find them.
DynParam[T] — init-time knobs on a fixed artifact
DynParam[T] marks a field bound at init / pre-sim rather
than at build. The value is set on the instance and, for a generated model, emitted as a member
assignment — so one artifact serves every value, where a HwParam would have forced a second.
Its first and still-primary use is configuring XSI testbench models:
class StreamDriver(...):
in_bundle: DynParam[str] = "" # which recorded bundle this driver plays
s_cmd.in_bundle = "vectors/s_cmd"; // what the generated harness emits
discover_dyn_params(obj) returns {field: value} for every
DynParam whose value differs from the class default, and the generator emits one assignment per
entry. A field left at its default emits nothing — which is what lets a knob be added without
every existing harness growing a line.
The four declared today are all testbench-side configuration:
| field | on | says |
|---|---|---|
in_bundle |
StreamDriver |
which bundle to play into the DUT |
out_bundle |
StreamSink |
where to capture what comes out |
load_segs |
MemoryMod |
regions to load from bundles at pre_sim |
dump_segs |
MemoryMod |
regions to dump to bundles at post_sim |
The axis is binding time, not synthesizable-vs-not. It is tempting to read
DynParamas “the marker for non-synthesized blocks” because every current user is a testbench model — but that is a fact about what has been built, not about the marker. Its synthesizable cousin is a regmap /s_axiliteregister: also set on a finished artifact, just at runtime over a bus rather than at init in a C++ constructor.
Bound once at
pre_sim, and constant for the run. ADynParamis not a per-cycle value and not a stream. Something that changes during a run is state, a stream payload, or a regmap write — and each of those is a different mechanism with a different cost.
Choosing between them
HwParam[T] |
HwConst[T] |
DynParam[T] |
|
|---|---|---|---|
| Scope | per-instance field | class-level attribute | per-instance field |
| Set when | at instantiation (MyComp(in_bw=64)) |
at class definition | at init, before pre_sim |
| Varies | per instance, and per kernel variant | never — fixed for the class | per instance, on one built artifact |
| In simulation | int-like value (wrapped HwParamValue) |
plain class attribute | a plain field |
| In codegen | template argument or literal | static constexpr |
<model>.<field> = <value>; |
| Use for | configurable knobs: bus widths, datapath sizing | fixed structure: static array extents | config of a generated model: which vectors to play |
Rules of thumb:
- the value sizes the hardware →
HwParam; - it is a fixed structural fact of the class →
HwConst; - it configures an already-built thing →
DynParam, or a regmap register if that thing is synthesized.
param_supports — declaring kernel variants
To emit more than one concrete kernel from a class, declare
param_supports: a map of variant key → HwParam overrides.
class MyKernel(HwModule):
cpp_kernel_name = "my_kernel"
in_bw: HwParam[int] = 32
param_supports = {
"bw64": {"in_bw": 64},
"bw128": {"in_bw": 128},
}
This declares that the build should emit my_kernel (defaults), my_kernel_bw64, and
my_kernel_bw128. validate_param_supports checks the keys
(valid C identifiers) and that every override names a real HwParam field. How those variants are
generated — concrete top functions per key — is the realization page:
Module Code Generation: Templating.
See also
- Module Code Generation: Templating — the C++ realization: how
HwParamlowers into kernel signatures,HwConstintostatic constexpr, andparam_supportsinto variant kernels. - Hardware modules — where these fields are declared on the class.
- Module structure — the generated kernel these parameters shape.
- XSI testbenches — where
DynParamassignments are emitted. - Register maps — the runtime binding site, for synthesized blocks.
Quick reference
- One axis: when does the value bind? — class definition (
HwConst), build (HwParam), init (DynParam), runtime (regmap). HwParam[T]= per-instance synthesis knob; int-like in sim, wrappedHwParamValue, immutable after construction. Distinct values mean distinct artifacts. The only kind synthesizable code takes.HwConst[T]= class-level fixed structural constant; a plain class attribute in sim.DynParam[T]= init-time knob on a fixed artifact; emitted as<model>.<field> = <value>;viadiscover_dyn_params. Bound once atpre_sim, constant for the run.- Sizes the hardware →
HwParam; fixed fact of the class →HwConst; configures an already-built thing →DynParam, or a regmap register if it is synthesized. param_supportsdeclares variant kernels (key →HwParamoverrides); realization is Templating.