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

  1. What we’re building — the per-row FIR function, the valid edge, and why FIR forces a resident row buffer.
  2. The load-compute-store dataflow — why a sliding window can’t stream, and how the three stages overlap.
  3. The Python modelFIRAccel: three concurrent stage processes, the fictitious inter-stage messages, and the one shared golden.
  4. 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.
  5. Writing the dataflow hook — the hand-written fir_dataflow.tpp: three HLS functions in a #pragma HLS DATAFLOW region.
  6. C and RTL simulation — Vitis csim/cosim, bit-exact against the golden, through the build DAG.
  7. Extracting timing from cosim — measuring the X-read / Y-write spans from the VCD, counting transfer beats, and sweeping sizes into a corpus.
  8. 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.

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