Managing a platform

Two things you do to an existing platform: look inside one, and publish into one. Creating and seeding one is Creating a platform.

waveflow_calib show    calib/platforms/myboard
waveflow_calib publish calib/work/myboard calib/platforms/myboard --apply

Looking inside one

waveflow_calib show waveflow/calib/platforms/zynq7020_bfm_100mhz
platform 'zynq7020_bfm_100mhz'   waveflow/calib/platforms/zynq7020_bfm_100mhz
  part xc7z020clg484-1   clock 100.0 MHz
  measured in: lut ff dsp bram uram srl

bus law      : fitted   (2 corpus points)

timing residuals (2):
  mem_r_stream_framed_task                   fitted      3 corpus row(s)
  mem_w_stream_framed_done_task              fitted      2 corpus row(s)

module records (4 configuration(s)):
  MemRStream               2 config(s)   hls_estimate=29 record(s)
  MemWStream               2 config(s)   hls_estimate=29 record(s)

synthesis time these records represent: 12.9 min

Worth reading that last line: it is the cost the library saves you, recorded from the runs that actually happened rather than estimated. -v lists every module configuration and its counters.

Note what is not there: this is the shipped library, so it carries only the framework’s own modules. A project’s own modules live in its own library — see Creating a platform.

The two timing residuals above are keyed by function name alone — they predate the configuration-qualified key, so they are not known to describe any particular memory width. A model that loads one reports that in its confidence rather than implying a match. See Directory layout.

Publishing into one

Calibration parameters are infra-wide: once a shared component is calibrated, every project reuses it. That makes two things matter — a stray run must never clobber shared parameters, and a re-run that produces the same fit must not churn git. Both fall out of a two-tier storage split.

Two tiers: work vs. tracked

calib/work/<name>/                  untracked (gitignored).  Sweeps, tests, and the DAG calib steps
                                    write here freely — it churns.
        │  publish_calib
        ▼
waveflow/calib/platforms/<name>/    tracked (committed), and shipped as package data so a
                                    pip-installed user resolves it. EXACTLY ONE writer: publish.

A sweep points the BusCalib / StreamTimingModel fits at the work dir. When you are satisfied, one command promotes the result into the tracked library — waveflow/calib/platforms/ for the shipped reference platforms, or a user/project overlay for a platform you calibrate locally (see Platforms). Because the tracked dir has a single writer, a test can’t reach it; because a re-fit on the same corpus is deterministic, an unchanged promotion writes nothing.

The command

publish_calib calib/work/<name> calib/platforms/<name>            # dry-run: print the plan, write nothing
publish_calib calib/work/<name> calib/platforms/<name> --apply    # write only the changed files

The target is a tracked library — usually your project’s (calib/platforms/). Promoting into the shipped one (waveflow/calib/platforms/) is for framework modules only; see what belongs where.

  • Dry-run by default. It prints a plan — + created, ~ updated, = unchanged — and writes nothing. --apply performs it.
  • No-op when unchanged. Each artifact is byte-compared against the tracked copy; identical files are never rewritten, so a deterministic re-publish produces 0 files written and no git diff.
  • Only the stable artifacts are promoted; the churny raw firing trees stay behind:

    Published Left in the work dir
    platform.json (identity) components/<c>/rtl/
    mm_bus.json + points/*.json components/<c>/pysim/
    components/<c>/params.json + corpus.csv  
  • Coverage-regression guard. publish refuses (exit 1) to replace a fit with one built from fewer datapoints — bus point files, or a component’s corpus.csv rows — so a thin re-sweep can’t silently clobber a richer library. --force overrides it.

Under the hood build_plan(work, tracked) computes the plan (pure inspection) and apply_plan(plan, force=…) writes the changed files; the CLI is a thin wrapper.

Why publish is not a DAG step

Promotion to shared infra is a deliberate “I’m satisfied” act, not a build side effect. The DAG steps (CalibBusStep, CollectTimingStep / FitTimingStep) populate the work dir; publish_calib is the manual gate to the tracked one.

What is committed, and what is not

The work tier is ignored; a tracked library is not. Getting those rules right is part of setting a project up, so they live with the setup — see what to .gitignore, including the anchoring subtlety that decides whether a nested project’s work tier is actually ignored.

The reference platform, end to end

waveflow/calib/platforms/zynq7020_bfm_100mhz/ is built reproducibly by examples/mem_copy/calibrate_platform.py:

  1. Seed the identity — creating the platform writes platform.json (xc7z020clg484-1, 100 MHz).
  2. Fit the bus lawadd_run the measured burst laws (write nwords + 2·(num_trans−1), read nwords + (num_trans−1)) at two sizes, then fit()mm_bus.json.
  3. Fit the writer residualcollect_rtl the measured RTL spans (183 cyc at n=128, 615 at n=512) and collect_pysim a run with the bus law active, so the residual is control-only (~22 cyc), then fit()components/mem_w_stream_framed_done_task/.
  4. Publishpublish_calib calib/work/zynq7020_bfm_100mhz waveflow/calib/platforms/zynq7020_bfm_100mhz --apply.

The pysim runs live (no toolchain); only the RTL spans are measured constants (gated for real by the -m xsi run that produced 0.0% error). Loading the committed platform reproduces the writer RTL period to 0.0% at both sizes — a test pins it, so the committed params can’t silently drift.

A known limitation: collinear sizes

The reference platform was swept at two sizes whose num_trans and nwords are proportional (num_trans = nwords / 16), so the two features are collinear: the fit reproduces the measured points exactly but its split between a per-word and a per-burst slope is underdetermined (the writer’s residual reads as 23 at n=128, 0 at n=512). More sizes do not fix this if they are all multiples of 16 — num_trans stays nwords / 16 and the collinearity persists. What breaks it is a size where ceil(nwords / 16) departs from nwords / 16 — a small or non-multiple-of-16 transfer (e.g. nwords = 17 → 2 bursts, nwords = 16 → 1) that changes the burst-to-word ratio. Adding those to the grid identifies the per-word (1) and per-burst (2 write / 1 read) costs cleanly; it is the follow-up before trusting the model to extrapolate beyond the swept range.

See also