The compute stage’s timing model
The framework mem-streams ship their timing; il_compute does not — it is the design’s own kernel, so
the design gives it a timing model. This page declares that model: where it lives on the stage, the
formula it evaluates, and what its parameters mean. Fitting the timing model then
recovers those parameters from RTL.
The general facility is Adding a timing model to a component; this is the interleaver’s worked instance of it.
Why the stage needs one
In pysim the gather is instantaneous — run_iter computes Y[i] = X[P[i]] in one Python loop and the
result exists at once. Hardware takes cycles. So the stage must charge the time its computation would
take: predict a cycle count for this firing and wait it out before committing the output. Without the
model the compute would look free and the pysim timeline would run ahead of the RTL.
Where it lives
IlComputeInband holds a model object and charges it once per firing. It builds the model in
__post_init__ and, in run_iter, predicts the firing’s cycles and yields a matching timeout right
after the (instant) functional gather:
def _build_timing_model(self):
seed = {"n": IL_COMPUTE_II_SEED, "intercept": IL_COMPUTE_LATENCY_SEED - IL_COMPUTE_II_SEED}
path = None if self.calib_dir is None else Path(self.calib_dir) / "params.json"
model = LinCalibModel(basis=["n"], target="cycles", fit_intercept=True,
coeff_names=["n"], seed=seed, path=path)
model.load_or_default() # a fitted params.json if calib_dir has one, else the seed
return model
def run_iter(self):
...
yblock.val[:n] = xblock.val[pblock.val[:n]] # the gather — one vectorized numpy call
cycles = float(self.timing.predict({"n": n})) # what it would take in hardware, for this n
yield self.timeout(cycles * self.clk.period) # charge it
...
The charged delay is only the compute’s own cost. The reads and writes are charged elsewhere — the reader and writer stages block for their transfers — so the model owns just the gather loop, and any stall grows the firing on its own (see the general pattern).
The formula
The gather is a pipelined loop, so its cycle count is the loop model — a line in the trip count:
cycles = latency + ii · (n − 1)
n— the trip count: the number of output elements. The typed blocks hold one 32-bit element per slot (ap_uint<32>[N]), so the loop trips once per element — the basis isn, not the word countnw.ii— the initiation interval: cycles between successive loop iterations.ii = 1means one output element per cycle (a fully pipelined loop).latency— the fixed per-firing cost: pipeline fill plus constant setup, independent ofn.
LinCalibModel(basis=["n"], fit_intercept=True) stores this as a coefficient on n and an intercept.
Matching latency + ii·(n − 1) = ii·n + (latency − ii), the coefficient is ii and the intercept is
latency − ii. So two numbers describe the stage’s timing, and fitting means finding them.
The seed, and loading a fit
IL_COMPUTE_LATENCY_SEED / IL_COMPUTE_II_SEED (in interleaver.py) give a no-calib fallback, so the
stage charges a plausible cost before any fit is loaded. Point calib_dir (via
InterleaverInband(compute_calib_dir=…)) at a fitted params.json and load_or_default uses that
instead. Both currently agree — cycles = n (ii = 1, latency = 1) — because
the fit measured exactly that from RTL and the seeds were set to match.
See also
- Adding a timing model to a component — the general pattern this instantiates.
- Timing models for loops — the
latency + ii·(n − 1)law. - Fitting the timing model — recovering
latencyandiifrom RTL (the next page).