The sweep
The model is already installed, and two of its four counters already work: DSP
and BRAM follow from the declaration alone. LUT and FF do not, and until they have measurements the
model reports them as UNCALIBRATED. This is where they get them.
One synthesis gives you one point. A fit needs a grid, and which grid you choose decides what you are able to learn — a sweep that only samples one regime will validate a law it never tested.
How the sweep is written
The whole of vecmult_sweep.py, minus its docstring, is four declarations and a main:
GRID = ParamGrid(vlen=(512, 1024, 4096, 16384), dwid=(32, 64, 128, 256))
RUNNER = SweepRunner(dag_factory=build_vecmult_dag, root_dir=HERE,
platform=PLATFORM, platforms_root=PLATFORMS_ROOT,
part=PART, clk_freq=CLK_FREQ,
extra_params={"live_output": False})
STAGES = [Stage("resources")]
DRY_RUN_STAGES = [Stage("codegen_dut", use_platform=False)]
def main(argv=None):
return sweep_cli(RUNNER, GRID, description="Sweep vecmult resource points",
stages=STAGES, dry_run_stages=DRY_RUN_STAGES, argv=argv)
Read in order, each line says one thing about this design:
| declaration | what it settles |
|---|---|
ParamGrid |
the points — 4 × 4 = 16, vlen first so it is the outer loop |
SweepRunner |
which DAG to run, and the device and work-tier library the measurements are keyed by and filed into |
Stage("resources") |
run that DAG through its resources step: synthesize, attribute, file |
DRY_RUN_STAGES |
what --dry-run does instead — stop at codegen, and attach no platform |
sweep_cli |
turn the three of them into a program with --help |
Everything a sweep needs beyond those is supplied: resume, a summary written after every point, a failing point recorded rather than aborting the run, progress output and an exit code. None of it is written here, and none of it can be forgotten. See Sweeping a design for the API in full.
force=Trueis why each point is honest — and you do not pass it. Without it the DAG would see up-to-date artifacts from the previous point and skip, and the report on disk would then be attributed to the wrong configuration.SweepRunnerforces every run for exactly that reason.
Running it
sweep_cli derives one flag per axis from the grid, so the two axes above are also the CLI:
python -m examples.vecmult.vecmult_sweep --dry-run # codegen only, no Vitis — a cheap pre-flight
python -m examples.vecmult.vecmult_sweep # all 16 points, ~15 minutes
python -m examples.vecmult.vecmult_sweep --vlen 512 --resume # one row, continuing a stopped run
Each point re-runs the whole DAG with different params, so every measurement comes from a design
that was re-generated, re-simulated, re-checked against its golden and re-synthesized at that
configuration. Nothing is reused across points except the source.
At a quarter of an hour the grid is easily long enough to be interrupted, which is why the summary is
written after every point rather than at the end, and why --resume skips the (point, stage) pairs
already recorded as ok — both belong to the runner
rather than to this script.
The grid, and what it was designed to separate
DWIDS = (32, 64, 128, 256) # LW = 2, 4, 8, 16
VLENS = (512, 1024, 4096, 16384)
That is not a uniform box for the sake of coverage. Each vlen row was chosen to put the buffer in a
different regime:
vlen |
bank depth at LW=2 … LW=16 | what it exercises |
|---|---|---|
| 512 | 256 … 32 | banks far shallower than a block — and the LUTRAM corner at the end |
| 1024 | 512 … 64 | every bank rounds up to exactly one block |
| 4096 | 2048 … 256 | straddles the knee |
| 16384 | 8192 … 1024 | banks deeper than a block |
The results
| vlen | LW=2 | LW=4 | LW=8 | LW=16 |
|---|---|---|---|---|
| 512 | 2 | 4 | 8 | 0 |
| 1024 | 2 | 4 | 8 | 16 |
| 4096 | 4 | 4 | 8 | 16 |
| 16384 | 16 | 16 | 16 | 16 |
(BRAM18 blocks. DSP is LW at every one of the 16 points — and both counters are exactly what the
declaration already predicted before this sweep ran. LUT and FF,
the two that needed it, are on the next page.)
The two BRAM regimes
Look at the table twice. The 16384 row is flat at 16 regardless of lane count. The 512 and
1024 rows track LW exactly. Those look like two different laws, and they are one:
BRAM18 = LW × ceil( (vlen / LW) / entries_per_block )
Each of the LW banks is vlen/LW deep, and a bank shallower than one block still occupies a whole
one. That single ceiling produces both behaviours:
- Partition-bound — bank shallower than a block, so the ceiling is 1 and
BRAM = LW. The 512 and 1024 rows: same data, 2 → 4 → 8 → 16 blocks. Partitioning costs everything. - Data-bound — bank deeper than a block, so
LWcancels against the ceiling and BRAM depends only on total size. The 16384 row: 16 at every lane count. Partitioning is free.
Why the grid has to span both. Drop the ceiling and
LWcancels unconditionally, leavingvlen / entries— right on the 16384 row and wrong by up to 4× on the others. The opposite simplification,BRAM = LW, fits 11 of these 16 points and looks convincing until the data-bound column shows up. Neither error is visible from a grid that samples one regime. Choosing the grid is part of choosing the model.
entries_per_block is 1024 for 16-bit elements, not 18432/16 = 1152, because a block has legal port
shapes rather than a bit budget — see bram_estimate.
The corner
vlen=512, dwid=256 gives banks 32 deep and BRAM = 0: HLS declined block RAM and used LUTRAM
instead, with the storage reappearing in fabric. That is a discontinuity, not a bad measurement, and
it is predicted by the device rule rather
than treated as an outlier.
Five extra syntheses pinned where it happens — bank depths 40, 48, 56, 60 and 63 are all LUTRAM, 64 is block RAM — so the threshold sits between 1008 and 1024 bits per bank.
Where the measurements go
Each point’s report is attributed and filed as a record, keyed by the module’s elaborated structure, into an untracked work tier:
examples/vecmult/calib/work/zynq7020_vecmult_sweep/modules/<key>/resource/records.jsonl
A deliberate publish promotes that into the example’s tracked library, which is what the model reads:
waveflow_calib publish examples/vecmult/calib/work/zynq7020_vecmult_sweep \
examples/vecmult/calib/platforms/zynq7020_vecmult --apply
Two tiers because a sweep is exploratory and re-runs freely, while a library is reviewed: the
work → publish flow keeps an interrupted or experimental run from
quietly editing committed measurements. The sweep also writes results/sweep.json, a human-readable
summary; it is untracked, and nothing reads it.
The records are the raw tier — the corpus the fit reads is derived from them on demand, never stored twice.
GRIDinvecmult_corpus.pyis not that corpus. It is the same 16 measurements committed as Python source, and it now serves two narrower jobs: the oracle the tests check predictions against with no toolchain installed, and the fallback corpus foradd_rm(None)when there is no platform to read a store from. Keeping a second independent copy is a feature while one checks the other — it stops being one the moment both claim to be the source.
Next
- How well it fits — what these 16 points do for LUT and FF, checked against points held out of the fit.
- Sweeping a design —
ParamGrid,SweepRunnerandsweep_cliin full, for when you write one of your own.