Rowwise FIR
Rowwise FIR is a per-matrix-row FIR filter accelerator — Y[i,j] = Σ_t h[t]·X[i, j−t] over every
row of an input matrix. It is the example where Waveflow turns inward: it reuses
shared_mem’s AXI-stream control (the command on s_in, the response on
m_out) over AXI-MM data, and instead studies the accelerator’s internal structure — the
load-compute-store dataflow — and how to give it a physical, cosim-calibrated timing model. It
adds no new interface (control is the stream you already know); its new ideas are all internal.
The system
flowchart LR
host["host<br/>(fir_sim.py)"]
subgraph fir["FIRAccel — fir.py"]
direction LR
load["load"] --> compute["compute"] --> store["store"]
end
mem[("shared memory<br/>X · h · Y")]
host -->|"AXI-stream control"| fir
fir -->|"m_axi (gmem)"| mem
A host sends FIRCmds over the control stream (filter taps h, the address of X, n_rows,
n_cols); the accelerator reads each row of X, computes the FIR, writes Y back over a single
full-duplex m_axi bundle, and returns a response on m_out. Internally it is three concurrent
stages — load, compute, store — overlapping across rows, the
double-buffered timing model made concrete.
What it adds
The earlier examples answer “are the numbers right?”. Rowwise FIR is the culmination of the timing and calibration arc, adding two things:
- The load-compute-store dataflow — a sliding window forces the input resident and randomly addressable, so the design cannot be a pure stream. It must load a row, compute over it, and store the result, with the three stages pipelined. This is the canonical dataflow custom hook.
- A physical, near-fit-free timing model — the whole-kernel latency decomposes into deterministic
channel occupancy + an exact II=1 compute + a single calibrated curve (
row_depth, the per-row pipeline depth), measured from a Vitis cosim sweep. The loosely-timed sim then reproduces the RTL to ≤1.3% across the grid (0.11% on the held-out point). That study is the fit page.
Walkthrough
- What we’re building — the per-row FIR function, the
validedge, and why FIR forces a resident row buffer. - The load-compute-store dataflow — why a sliding window can’t stream, and how the three stages overlap.
- The Python model —
FIRAccel: three concurrent stage processes, the fictitious inter-stage messages, and the one shared golden. - The timing model — what is timed and how: deterministic occupancy, II=1
compute, the calibrated
row_depth, the early-anchored store, and where to log events. - Writing the dataflow hook — the hand-written
fir_dataflow.tpp: three HLS functions in a#pragma HLS DATAFLOWregion. - C and RTL simulation — Vitis csim/cosim, bit-exact against the golden, through the build DAG.
- Extracting timing from cosim — measuring the X-read / Y-write spans from the VCD, counting transfer beats, and sweeping sizes into a corpus.
- Fitting the model — the physical decomposition, the one fitted curve, the gates, and the result figure.
Table of contents
- What we're building - The computation: a real-valued FIR filter applied independently to every row of a matrix, with a 'valid' edge. The sliding T-tap window is what forces the input to be resident and randomly addressable — the reason this is a load-compute-store dataflow rather than a pure stream.
- The load-compute-store dataflow - Why the rowwise FIR is a load-compute-store dataflow — the sliding window can't stream, so each row is loaded resident, computed over, and stored, with the three stages pipelined across rows (load(n+1) ∥ compute(n) ∥ store(n-1)). The two channel kinds: a partitioned-BRAM ping-pong for windowed input, a FIFO for sequential output.
- The Python model - FIRAccel as a HwComponent: three persistent SimPy stage processes (load / compute / store) handing off through fictitious inter-stage messages, a host-facing run_proc that pulls commands off the queue and kicks the pipeline without blocking, one shared golden (execute), and element-coordinate Region slices for memory access.
- The timing model - FIRTiming: which parts of the accelerator are timed and how. The whole-kernel decomposes into deterministic channel occupancy (transfer beats), an exact II=1 compute body, and a single calibrated curve row_depth(n_col). The store is early-anchored so it hides under compute; the sim composes whole = fill + max(write_occ, compute_body). Plus the per-stage log events used to validate against cosim.
- Writing the dataflow hook - The hand-written fir_dataflow.tpp: the whole load-compute-store pipeline as three HLS functions (load_row / compute_row / store_row) wired in a per-row #pragma HLS DATAFLOW region over a partitioned-BRAM ping-pong and an hls::stream FIFO, each at II=1. The interface pragmas live in the generated m_axi top; the core is a reusable fir_accel_core.
- C and RTL simulation - Vitis C-simulation and RTL co-simulation of the generated FIR kernel, bit-exact against the one golden, driven by the build DAG (codegen / csim / cosim steps). The Phase-1 hand-HLS sandbox is the ground-truth that first proved compute II=1 and the full-duplex single-bundle read+write.
- Extracting timing from cosim - Measuring the FIR timing from the RTL co-simulation VCD: extract the X-read and Y-write bursts, count transfer beats for true channel occupancy, read off the per-stage spans and the whole-kernel, and sweep over a (n_row, n_col) grid to build the calibration corpus that the fit consumes.
- Fitting the model - Fitting the FIR timing model from the cosim corpus: channel occupancy and II=1 compute need no fit, so the only calibrated term is row_depth(n_col) — a saturating InterpCalibModel lookup. The composed loosely-timed model then reproduces the RTL whole-kernel to ≤1.3% across the grid (0.11% on the interior held-out point, ≤0.60% on untrained columns). Shown graphically; the figure regenerates without Vitis.