Custom Hooks

Most of a component lowers to C++ automatically — Component Code Generation walks the synthesizable subset of your Python and emits the kernel structure. But some datapaths are beyond what the extractor can lower: a tight complex MAC, a custom pipeline, anything where you want hand-tuned HLS pragmas and exact ap_fixed intermediates. For those you write the kernel body yourself — a custom hook — and Waveflow drops it into the generated kernel in place of extracted code.

Auto-generated vs. hand-written

This is the hand-written side of hardware generation; the auto-generated side is Component Code Generation. The boundary is one decorator:

  Auto-generated Hand-written (here)
Source the method’s Python body a .cpp / .tpp you write
Lowered by the HwStmt extractor nothing — codegen emits a call to your C++
Use when the body is in the synthesizable subset the datapath needs hand-tuned HLS C++

The mechanism: @synthesizable

A method decorated @synthesizable with no synth_fn is a stub: the extractor does not lower its Python body — codegen instead emits a call to a user-written C++ function. The method’s Python body is still the simulation model (it runs in PySim); only its C++ is hand-written. So the same method is bit-exact-checked in Python and synthesized from your hand-written file.

from waveflow.hw.synth import synthesizable

class SimpFunComponent(HwComponent):
    @synthesizable
    def compute(self, x: Int32, a: Int32, b: Int32) -> Int32:
        return Int32(relu_affine(int(x.val), int(a.val), int(b.val)))   # == the golden

(From examples/regmap/simp_fun.py. The Python body is the bit-exact golden; the C++ contract it must match is the hand-written simp_fun_compute_impl.cpp.) Codegen finds the C++ by the {kernel}_{method}_impl.{cpp,tpp} convention, or you name it explicitly with @synthesizable(impl_file="…"). Writing a hook is the full contract.

Which pattern? A decision guide

The three hook patterns differ by who moves the data — and that is also the order of increasing difficulty. Ask what your operand looks like:

Your operand is… Who moves the data The hook body Pattern
a fixed-size block you can name up front codegen (auto-gen read_array/write_array in run_proc) pure compute over a materialized C++ array Block
a stream that arrives incrementally the hook’s own lane loop read loop + compute + write, with TLAST/framing Stream
a data-dependent memory region (strided rows, gather/scatter) the datapath itself, over the m_axi port strided read_array_lane + the datapath Complex
a resident window/tile (a sliding window needs random access) the hook owns the whole load-compute-store pipeline three HLS functions in a #pragma HLS DATAFLOW region Dataflow

Start at Writing a hook for the mechanism (it uses the simplest case — a scalar hook with no data movement), then jump to the pattern that matches your operand.

In this section

  • Writing a hook — the @synthesizable contract, the namespace, the bit-exact Python sibling, #pragma HLS INLINE, and .cpp vs .tpp, walked through the scalar simp_fun hook.
  • Block — load, compute, store — auto-generated read_array/write_array I/O, a pure-compute hook. The array generalization of the regmap kernel.
  • Stream — process as you read — the lane loop over an AXI-Stream port (read_axi4_stream_lane / write_axi4_stream_lane, pf lanes, TLAST).
  • Complex — data-dependent addressing — driving the m_axi port from the datapath (read_array_lane with a running pointer), and the two VMAC csynth gotchas.
  • Dataflow — load, compute, store pipeline — the hook owns the whole pipeline: three HLS functions in a #pragma HLS DATAFLOW region (partitioned-BRAM ping-pong + FIFO), the synthesizable side of the double-buffered timing model. The rowwise_fir example.
  • Memory command queue — the advanced case: a hook that is the synthesizable half of a transport interface (the queue_get ring dequeue).
  • Kernel transfer reference — the in-kernel transfer-call cheat sheet (read_array_lane / read_array_slice / stream variants) and the Python↔C++ mapping table.

See also


Table of contents

  • Writing a hook - The hook contract, walked through the simplest hook — the scalar simp_fun compute. The @synthesizable stub form (Python body = bit-exact golden, C++ hand-written), the cpp_namespace, #pragma HLS INLINE so the hook fuses into the top, and the .cpp-vs-.tpp rule (non-templated scalar -> .cpp; HwParam-width-templated -> .tpp).
  • Block — load, compute, store - The block hook pattern: the load and store are auto-generated in run_proc (read_array / write_array lower to read_array_slice / write_array_slice bursts), so the hook is pure compute over a materialized C++ array. The array generalization of the scalar regmap kernel, walked through examples/block_scale.
  • Stream — process as you read - The stream hook pattern: data arrives incrementally on an AXI-Stream port, so the hook owns its own lane loop — read pf lanes, UNROLL the compute, write pf lanes — carrying TLAST framing and returning an error status. Walked through the poly evaluator in examples/stream_inband.
  • Complex — data-dependent addressing - The complex hook pattern: the access pattern itself depends on runtime command fields (strided rows, per-row gather), so the datapath drives the m_axi port — a read_array_lane loop over a running word pointer. Walked through VMAC, with the two csynth gotchas (scalar *_core args; #pragma HLS INLINE so m_axi binds to the top).
  • Dataflow — load, compute, store pipeline - The dataflow hook pattern: the hook owns the WHOLE load-compute-store pipeline — three HLS functions wired in a per-row #pragma HLS DATAFLOW region over a partitioned-BRAM ping-pong (load→compute) and an hls::stream FIFO (compute→store), each stage at II=1. Used when a sliding window forces a resident, randomly-addressable row buffer (you cannot stream it). Walked through examples/rowwise_fir.
  • Memory command queue - The synthesizable side of the AXI-MM command queue: AXIMMQueue.get lowers to AXIMMQueueGetStmt, emitted as a call to the hand-written ring-dequeue hook queue_get in aximm_queue_impl.tpp — read (head,tail), poll while empty, read one slot, advance head with a power-of-two mask, deserialize the typed command.
  • Kernel transfer reference - The in-kernel transfer-call cheat sheet: the m_axi memory calls (read_array_slice for a resident range, the read_array_lane lane loop for throughput) and the stream calls (read_stream_lane / read_axi4_stream_lane with TLAST), the common loop shapes, and the Python-to-C++ mapping table the pattern pages draw on.

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