RTL timing and the comparison

The previous page is the pysim’s modeled timeline — 300 cyc/job. This page measures the actual RTL timing from a trace and puts the two side by side. They agree on the steady-state period to ≈1%, and the small gap is a story worth telling: it is the reader stage the interleaver forced into being calibrated.

What the RTL measures

A traced XSI run gives cycle-accurate per-firing timing — every task’s ap_done, beat by beat. The built-in component_firings reads each stage’s firings from the waveform; the cadence (the steady-state gap between consecutive firings) is the throughput:

  • every stage fires once per job at the pipeline period — except MemRStream, which fires twice (P then X, ≈151 cycles each), so the pipeline is reader-bound at ≈302 cyc/job: moving P and X over one m_axi bus is the critical path.

The RtlTimingStep

InterleaverRtlTimingStep produces this comparison. It consumes the pysim timeline (the previous page), builds and traces the RTL, extracts each stage’s cadence, and writes the table — both a results/interleaver_timing_compare.json artifact and, in place, the table below:

python examples/interleaver/interleaver_figures.py --through rtl_timing

It is toolchain-gated — csynth + xsim, so an -m xsi-class rung — unlike the pysim figure, which is committed and CI-regenerable. That asymmetry is deliberate: the pysim path is the fast, always-available model, and this rung is the ground-truth check you run when the toolchain is present.

The comparison

The table compares each stage’s cadence (firings/job and the steady-state period), generated by the step above:

| stage | firings/job | RTL cadence | PySim cadence | |——-|:———–:|:———–:|:————-:| | cmd_rx (framer) | 1 | 302 | 300 | | MemRStream (gmem0) | 2 | 151 | 150 | | il_load → SOB | 1 | 302 | 300 | | il_compute (gather) | 1 | 302 | 300 | | il_store (framer) | 1 | 302 | 300 | | MemWStream (gmem1) | 1 | 302 | 300 | | period (cyc/job) | | 302 | 300 |

Overall period agreement: 99.3% (RTL 302 vs PySim 300 cyc/job).

Why cadence and not per-firing span: the two sides define a firing’s span differently. RTL component_firings anchors a span at the first input handshake (excluding the wait for input), while a pysim fire_log starts before the input get (including it) — so for a paced stage the two span definitions diverge (il_store, say, reads ≈139 in RTL but ≈300 in the pysim occupancy). The firing cadence — the rate a stage completes — is well-defined and comparable on both sides, and it reflects the calibrated residuals (the reader’s 150 includes the ≈15-cycle residual the fire_log window omits).

The agreement — and what it took

RTL 302 vs pysim 300, ≈0.7% off. It holds because every stage’s cost is now a loaded model — but getting there took calibrating the reader, and that is the interesting part. mem_copy is writer-bound, so it only ever needed the writer’s residual; the reader’s was never fit. The interleaver is the first reader-bound design, and it ran ≈10% under the RTL until a reader residual was fit (the mem-stream residual — the fixture the interleaver forced into existence). The lesson: you calibrate the stage a design actually bottlenecks on; an un-fit residual only shows where it lands on the critical path.

The compute stage’s own per-firing span, meanwhile, matches exactly — 256 pysim == 256 RTL. It is not the bottleneck, so it does not set the period, but its cost is faithful: a variant that made the gather the critical path would already have the right model.

Next: where the compute parameters came from

The compute model’s cycles = n did not come from nowhere. Fitting the timing model shows the measure → fit → ship recipe — reading il_compute’s per-firing span off a full-pipeline XSI run, fitting the loop law, and storing it in the platform library. That is the custom-component half of the calibration story, the one this example exists to teach.

See also