The bus-transfer model
The first level of the two-level split:
how long the m_axi interconnect takes to move n words in k bursts. This is a property of the
platform — the memory system and AXI adapter — so it is fit once and every accelerator on that
platform reuses it. BusCalib is that fit; mm_bus.json is where it lands.
The law
A contiguous transfer of n words is issued as k = ceil(n / MEM_AXI_MAX_BURST) bursts (HLS’s
max_burst_length is 16). BusCalib fits a per-direction span from those two features:
span(num_trans, nwords) = a·num_trans + b·nwords + c # cycles
On the reference BFM platform the measured laws are nwords + (num_trans − 1) (read) and
nwords + 2·(num_trans − 1) (write) — one cycle per word, plus a per-burst-boundary gap read straight
off the trace. The intercept and coefficients capture whatever the real interconnect does; an
idealized BFM gives these clean laws, a real DDR controller would widen them — which is the point of
measuring rather than assuming.
Measuring, component-independently
The bus law is read off the memory port, not any kernel’s firing — so it is genuinely a platform
property, independent of which accelerator generated the traffic. measure_bus_span does this:
from waveflow.calib.bus_model import measure_bus_span
point = measure_bus_span(bound_trace, "gmem1", "write")
# -> {"num_trans": 32, "nwords": 512, "span": 574}
It samples the bundle’s AR/AW/R/W handshakes from the trace, groups the beats into per-transfer runs by
idle gaps (a gap wider than idle_gap ends a transfer), and returns the median transfer’s
{num_trans, nwords, span}. Measuring one transfer — not the whole run — keeps inter-firing idle out of
the bus span.
Fitting and storing
BusCalib accumulates a per-run corpus across a sweep, then fits:
from waveflow.calib.bus_model import BusCalib
bus = BusCalib(platform_dir="calib/work/zynq7020_bfm_100mhz", clk_freq=100e6)
bus.add_run("n128", write={"num_trans": 8, "nwords": 128, "span": 142}) # -> points/n128.json
bus.add_run("n512", write={"num_trans": 32, "nwords": 512, "span": 574}) # -> points/n512.json
bus.fit() # no args -> read the corpus
add_run(run_id, read=…, write=…)writes one distilledpoints/<run_id>.json— a re-run overwrites its own point, so the corpus is concurrency-safe and a sweep isadd_run-per-size then onefit().fit()with no arguments reads the accumulated corpus;fit(read_points=…, write_points=…)takes points directly (the one-shot case). A direction with no points is simply absent — a write-only accelerator calibrates only the write channel.- The result is written to
mm_bus.json:
{ "clk_freq": 100000000.0, "basis": ["num_trans", "nwords"],
"models": { "read": { "num_trans": 0.066, "nwords": 1.058, "intercept": -1.0 },
"write": { "num_trans": 0.070, "nwords": 1.121, "intercept": -2.0 } } }
A bus law needs ≥2 distinct sizes to fit the slope, so a sweep is the natural unit. (With only two
proportional sizes num_trans and nwords are collinear — see the
workflow's note.)
Deploying: bus_timing()
The calibration side fits and persists; the runtime side is a BusTiming the memory slave consults
during pysim. bus_timing() bridges them — load_or_default:
bt = BusCalib(platform_dir="waveflow/calib/platforms/zynq7020_bfm_100mhz").bus_timing()
- On a calibrated platform it returns a
BusTimingconfigured frommm_bus.json, so pysim charges the real burst cost. - On an uncalibrated platform (no
mm_bus.json) it returns an unconfiguredBusTiming— both directionsNone— so a slice degrades to the plain per-word span rather than crashing.
Charging the bus term in pysim is what lets the component residual shrink to the component’s own control cost: on the reference platform the writer’s residual drops from ~36 (bus + control lumped) to ~22 (control only), and the 14-cycle burst term moves onto this shared model.
Automating it: CalibBusStep
In a build DAG, CalibBusStep does the measure → add_run → refit loop from a traced run — no design
factory, because the bus law is in the trace, not a pysim run:
… -> RtlSimStep -> trace (manifest + vcd) -> CalibBusStep (per m_axi bundle: measure_bus_span -> add_run -> fit)
It consumes the trace manifest + VCD, walks each boundary m_axi bundle,
measures the direction(s) it carries, adds the run to the platform corpus, and refits mm_bus.json.
Across a sweep (one step per size) the corpus grows until the model fits.
See also
- Platforms — where
mm_bus.jsonlives and how the platform is keyed. - Component residuals — the second level, which assumes the bus term is already charged.
BusTiming/ AXI-MM timing — the runtime model the slave applies.- Tracing a kernel run — where
measure_bus_span’s input comes from.