What a Waveflow component is

A common first reaction to “Python-native hardware” is: you write a kernel in Python and transpile it to HLS. That is not what Waveflow does, and the difference is the whole point.

A Waveflow component is a structured specification, not a loop to be transpiled. It declares four things — the data it consumes and produces, the interfaces it moves that data over, the parameters that size it, and a compute hook — and from that one declaration Waveflow derives the simulation, the synthesizable C++, the testbench, the host glue, and the documentation. The Python is the source of truth; the HLS kernel is one of its outputs.

The four parts

Here is the polynomial accelerator (examples/stream_inband/poly.py) at altitude — a real component, trimmed to its skeleton:

# 1. DATA — typed schemas for what crosses the interface
class PolyCmdHdr(DataList):                 # a command header
    elements = {
        "cmd_type": {"schema": PolyCmdTypeField},   # DATA or END
        "tx_id":    {"schema": TxIdField},          # transaction id
        "nsamp":    {"schema": NsampField},         # sample count
    }

class CoeffArray(DataArray):                # the polynomial coefficients
    element_type = Float32
    max_shape = (4,)

@dataclass
class PolyAccelComponent(HwComponent):
    # 3. PARAMETERS — the knobs that size the hardware
    in_bw:  HwParam[int] = 32
    out_bw: HwParam[int] = 32
    clk:    Clock = ...

    def __post_init__(self):
        # 2. INTERFACES — typed ports, with direction
        self.s_in   = StreamIFSlave(...)    # command + samples in
        self.m_out  = StreamIFMaster(...)   # response + samples out
        self.s_lite = VitisRegMapMMIFSlave(..., regmap={"coeffs": ...})  # AXI-Lite config

    # 4. HOOK — the behavior, as plain Python over the typed values
    @synthesizable
    def evaluate(self, cmd_hdr, s_in, m_out, coeffs):
        samp_in = yield from s_in.get(Float32, count=cmd_hdr.nsamp)
        y, power = np.zeros_like(samp_in), np.ones_like(samp_in)
        for c in coeffs.val:                # y = c0 + c1·x + c2·x² + ...
            y += c * power
            power *= samp_in
        yield from m_out.write(array(Float32, y))

Read it top to bottom and the four parts are right there: typed data (PolyCmdHdr, CoeffArray), a declared interface (a stream in, a stream out, an AXI-Lite register map), parameters (in_bw, out_bw, the clock), and a compute hook (evaluate) that is just NumPy over the typed values. Nothing here is a magic kernel — it is a description a machine can read.

“Isn’t that a lot of boilerplate for a three-line function?”

The polynomial math is three lines; the component around it is a few dozen. A fair question — with a four-part answer.

1. You’re comparing the tip; the cost is the iceberg. Those three lines aren’t deployable hardware. To actually ship them you also need a typed input/output contract, the packing logic that moves data over a 32- or 64-bit bus, the datapath, a testbench, the build script, the host-side driver, and a fast model to explore with — all kept consistent with one another as the design changes. Hand-written, that is hundreds of lines across half a dozen languages, re-synchronized by hand on every edit. Waveflow generates that iceberg from the one declaration. Past a toy, it is less total work — and far less re-work.

2. The “boilerplate” is the specification you were writing anyway. The schemas, the interface, the bit widths — every hardware project pins these down. The only question is whether they live implicitly, scattered and duplicated across a notebook, a spreadsheet, a C++ header, and a testbench — or explicitly, in one executable, checkable place. Waveflow makes you write the spec once.

3. It buys what an HLS kernel alone cannot. Because the component is structured, you get a NumPy-speed, bit-exact simulation (no toolchain in the loop), parameter sweeps over the bw knobs for design-space exploration, composition with other components, and a golden to check the generated hardware against. A bare HLS function gives you none of these.

4. It is the substrate AI needs. An agent asked to “write the polynomial kernel” against a blank HLS file produces something plausible. An agent asked to fill the evaluate hook against an explicit interface contract, with a bit-exact golden to check against, produces something verifiable. Structure is what turns AI output from a guess into a checkable artifact — see the harness for AI.

The real tradeoff

Waveflow optimizes the lifecycle and the system, not the one-off. For a script you will run once and throw away, the three-line function wins — write it and move on. The value appears the moment the design must be simulated, swept, composed, verified, and regenerated — which is to say, the moment it becomes real hardware.

It is the familiar typed-library-versus-throwaway-script tradeoff, brought to hardware — except the “library” here also generates its own simulation, its RTL inputs, its tests, and its documentation.


Next: how you iterate on a component — the fast all-Python inner loop and the Vitis-calibrated outer loop.


This site uses Just the Docs, a documentation theme for Jekyll.