Skip to content

chore(benches): make the benchmark set a fixed, runnable table - #310

Draft
diagonal-hamiltonian wants to merge 6 commits into
mainfrom
chore/bench-rungs
Draft

chore(benches): make the benchmark set a fixed, runnable table#310
diagonal-hamiltonian wants to merge 6 commits into
mainfrom
chore/bench-rungs

Conversation

@diagonal-hamiltonian

@diagonal-hamiltonian diagonal-hamiltonian commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

🤖 AI text below 🤖

What this is

benches/ carried three benchmarks nobody asked for and four tracked measures where three are
read. More importantly, the benchmark set that actually matters — the 10M/100M/1B ladder and the
strong/weak scaling rungs — existed only as flags in a shell script on one cluster, so there was
no fixed set a PR author could run and no standard to report against.

This makes the benchmark set a table in the repository, with a runner, a gate on what it
measured, and a paste-ready block of results. Nothing runs automatically and nothing gates a
PR.
Running the rungs a change could plausibly move, and putting the numbers in the PR, is the
author's job.

Four operations, everywhere

build_graph, propagate, energy, gradient — uniform across both bench modules, both
pictures and both fixed models. Deleted:

removed why
test_random_pare outside the set
test_random_inplace propagate followed by energy, both already benchmarked
test_model test_model_propagate plus a trailing expectation_value

Deletions, never renames. A benchmark name is Bencher's history key; the surviving names are
byte-identical to main's, so no series is orphaned.

Three measures

Tracked: latency, peak-memory, terms (exact match). resting-memory is dropped and its
report section removed. The raw memrest / membase / opbytes / opmemdelta / opmempeak
recordings stay in conftest.py — unrendered here, but read by the out-of-tree A/B harness.
benches/results/README.md now says so.

The benchmark set — benches/rungs.toml

monoprop-bench-rung benches/rungs.toml list               # the set, and what each costs
monoprop-bench-rung benches/rungs.toml <id> --dry-run     # the plan, no allocation spent
monoprop-bench-rung benches/rungs.toml <id> --rep 1       # one rep
monoprop-bench-ladder benches/rungs.toml benches/results  # the block to paste here

monoprop-bench-ladder prints the timings, the peak memory, and the resolved parameters of
every problem measured
— read back from the run rather than from the row's overrides, so a
reviewer can see what was measured without resolving flags against a model's defaults:

Its actual output, from the tiny two-rep run used to check this end to end:

## size

| rung | nodes | R | P | terms | Mterms/node | reps | median s | min s | Mterms/s/node | GiB/node | declared s | vs declared |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| tiny-hubbard-propagate | 1 | 1 | 2 | 137 | 0 | 2 | 0.00 | 0.00 | 0.14 | 0.4 | 0.00 | 0.494x |

## Problems measured

- **tiny-hubbard-propagate** — hubbard / heisenberg, propagate, 1 x 1 x 2 (nodes x ranks/node x partitions)
  - chemical_potential=0.0, cutoff=6, hopping=1.0, interaction=-2.0, lower_atol=0.0001,
    neel_start_spin=down, num_sites=3, observable_site=1, observable_spin=up,
    trotter_dt=0.2, trotter_steps=2

What each row declares

expect_terms is a gate: a result missing it by more than 0.1% is refused (renamed
.refused.json, not deleted), as is one whose node count, rank count, partition count or timing
round count disagrees with the row. A mistyped knob fails the cell instead of quietly measuring a
different problem.

--bench-rounds=1 is forced, not defaulted: pedantic builds round k+1's arguments before
releasing round k's, so a second round holds two propagators and doubles peak RSS. Repetition
is repeated process launches, which is what --rep names.

cost_seconds and cost_gib_per_node are the opposite of a gate — what one rep last cost, so
you can see what a rung takes before you spend it, and so a re-run can be shown against the last
one. Documentation only: a timing is noisy and your machine is not the machine they were taken on.
The 38 scaling rungs carry the measured medians of the 2026-08-27 Deucalion campaign, along with
its provenance and the fitted memory law GiB/node = 3.39 + 0.0634 × Mterms/node. Folding them
into the table replaced the separate baseline JSON, so there is one file to keep in sync instead
of two.

A row nobody has calibrated says so twice — expect_terms = 0 and TBD on every unmeasured
size knob — and refuses to run. TBD rather than 0 because 0 is not neutral:
lower_atol = 0 prunes nothing and is the largest problem the model can pose, so a placeholder
0 would invite exactly the allocation-burning run the gate exists to prevent.

Why the graph rows stop at one node

build_graph extends the graph rather than replacing it, so it retains one layer-set per gate
while propagate releases each layer as it contracts. Measured: Hubbard reaches 97M terms through
propagate in well under 2 GiB, while four successive build_graph calls on the same model
exceed a 242 GiB node. So propagate carries the full 10M/100M/1B ladder and the graph-holding
operations cap at a single node. Adding a multi-node graph rung later is one row.

Running a rung needs a machine, a scheduler and an allocation, none of which belong here. The
table, the runner and the gate are in the repo; the launcher that submits them is not.

How to run them — benches/RUNGS.md

A new guide covering: what each field of a row declares and which of them gate; every parameter
of all three models
with its default and its effect; why lower_atol is the size knob and the
other axes are saturated; how nodes, ranks and partitions map onto a machine and why P is the
number that matters; example Slurm scripts for one node, many nodes and a ladder in one
allocation; the pinning and allocation-sizing traps (--cpu-bind=none measured a 1.45x penalty;
MALLOC_ARENA_MAX set to the partition count cost ~16% of wall); and how to calibrate a TBD row
off the gate's own refusal message.

Every shell example in it is syntax-checked and every documented invocation was run.

The 66 rungs, with their model parameters

The size family is the requested matrix exactly: 10M single-thread, 100M and 1B on one node,
1B across eight
— for propagate on all three models, and for build_graph and
energy+gradient on the random model at 1000 generators in both pictures. 12 propagate rows +
16 graph/eval rows.

Which of the graph rungs actually fit is a question calibration answers. build_graph retains one
layer-set per gate, and at 1000 generators that is an order of magnitude more than the 100-generator
measurement anyone has. A row that does not fit records the node count it needed, or that it
exceeded the machine, and stays uncalibrated — that is a measurement of the graph path's cost, not
a gap.

System-size ceilings, which the option list does not show: monoprop_MAX_NUM_MODES defaults to
250, so --num-modes caps at 250 and --hubbard-num-sites at 125. --pauli-num-qubits is
effectively fixed at 127 — the circuit is built over a hard-coded IBM Eagle heavy-hex map of 144
pairs whose highest index is 126, so a lower value fails and a higher one only adds idle qubits.
Size the Pauli model with cutoff, num-layers and lower-atol instead.

cost/rep is the last measured wall time and peak GiB/node; ? means nobody has timed it yet.

size — the 10M / 100M / 1B / multinode-1B ladder (28 rungs)
rung model / picture ops N x R/node x parts terms knobs cost/rep
st-10m-hubbard-propagate hubbard / heisenberg propagate 1x1x1 TBD hubbard-cutoff=10, hubbard-lower-atol=TBD ?
st-10m-pauli-propagate pauli / heisenberg propagate 1x1x1 TBD pauli-cutoff=12, pauli-lower-atol=TBD ?
st-10m-random-propagate random / heisenberg propagate 1x1x1 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-100m-hubbard-propagate hubbard / heisenberg propagate 1x8x16 96,981,051 hubbard-cutoff=10, hubbard-lower-atol=1.25e-05 ?
n1-100m-pauli-propagate pauli / heisenberg propagate 1x8x16 91,273,861 pauli-cutoff=14, pauli-lower-atol=5e-05 ?
n1-100m-random-propagate random / heisenberg propagate 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-1b-hubbard-propagate hubbard / heisenberg propagate 1x8x16 TBD hubbard-cutoff=10, hubbard-lower-atol=TBD ?
n1-1b-pauli-propagate pauli / heisenberg propagate 1x8x16 TBD pauli-cutoff=14, pauli-lower-atol=TBD ?
n1-1b-random-propagate random / heisenberg propagate 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n8-1b-hubbard-propagate hubbard / heisenberg propagate 8x8x16 TBD hubbard-cutoff=10, hubbard-lower-atol=TBD ?
n8-1b-pauli-propagate pauli / heisenberg propagate 8x8x16 TBD pauli-cutoff=14, pauli-lower-atol=TBD ?
n8-1b-random-propagate random / heisenberg propagate 8x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
st-10m-random-graph-heisenberg random / heisenberg build_graph 1x1x1 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
st-10m-random-eval-heisenberg random / heisenberg energy, gradient 1x1x1 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
st-10m-random-graph-schrodinger random / schrodinger build_graph 1x1x1 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
st-10m-random-eval-schrodinger random / schrodinger energy, gradient 1x1x1 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-100m-random-graph-heisenberg random / heisenberg build_graph 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-100m-random-graph-schrodinger random / schrodinger build_graph 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-100m-random-eval-heisenberg random / heisenberg energy, gradient 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-100m-random-eval-schrodinger random / schrodinger energy, gradient 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-1b-random-graph-heisenberg random / heisenberg build_graph 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-1b-random-graph-schrodinger random / schrodinger build_graph 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-1b-random-eval-heisenberg random / heisenberg energy, gradient 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n1-1b-random-eval-schrodinger random / schrodinger energy, gradient 1x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n8-1b-random-graph-heisenberg random / heisenberg build_graph 8x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n8-1b-random-graph-schrodinger random / schrodinger build_graph 8x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n8-1b-random-eval-heisenberg random / heisenberg energy, gradient 8x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
n8-1b-random-eval-schrodinger random / schrodinger energy, gradient 8x8x16 TBD num-generators=1000, num-modes=250, cutoff=6, obs-terms=TBD ?
weak — fixed terms per node, 1 to 64 nodes (21 rungs)
rung model / picture ops N x R/node x parts terms knobs cost/rep
weak-97m-n1 hubbard / heisenberg propagate 1x8x16 96,981,051 hubbard-cutoff=10, hubbard-lower-atol=1.25e-05 10.8s 10.8GiB/node
weak-97m-n2 hubbard / heisenberg propagate 2x8x16 184,124,520 hubbard-cutoff=10, hubbard-lower-atol=8.8e-06 10.6s 9.6GiB/node
weak-97m-n4 hubbard / heisenberg propagate 4x8x16 377,482,074 hubbard-cutoff=10, hubbard-lower-atol=5.9e-06 13.1s 11.5GiB/node
weak-97m-n8 hubbard / heisenberg propagate 8x8x16 781,669,404 hubbard-cutoff=10, hubbard-lower-atol=3.9e-06 14.5s 10.9GiB/node
weak-97m-n16 hubbard / heisenberg propagate 16x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 18.1s 11GiB/node
weak-97m-n32 hubbard / heisenberg propagate 32x8x16 3,104,527,573 hubbard-cutoff=10, hubbard-lower-atol=1.73e-06 24.4s 11GiB/node
weak-97m-n64 hubbard / heisenberg propagate 64x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 36.6s 11.4GiB/node
weak-385m-n1 hubbard / heisenberg propagate 1x8x16 377,482,074 hubbard-cutoff=10, hubbard-lower-atol=5.9e-06 47.7s 26.6GiB/node
weak-385m-n2 hubbard / heisenberg propagate 2x8x16 781,669,404 hubbard-cutoff=10, hubbard-lower-atol=3.9e-06 50.9s 28GiB/node
weak-385m-n4 hubbard / heisenberg propagate 4x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 53.3s 27.9GiB/node
weak-385m-n8 hubbard / heisenberg propagate 8x8x16 3,104,527,573 hubbard-cutoff=10, hubbard-lower-atol=1.73e-06 54.7s 27.5GiB/node
weak-385m-n16 hubbard / heisenberg propagate 16x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 58s 27.4GiB/node
weak-385m-n32 hubbard / heisenberg propagate 32x8x16 12,255,330,837 hubbard-cutoff=10, hubbard-lower-atol=7.35e-07 64.6s 26.3GiB/node
weak-385m-n64 hubbard / heisenberg propagate 64x8x16 24,419,998,198 hubbard-cutoff=10, hubbard-lower-atol=4.68e-07 79.9s 26.9GiB/node
weak-1529m-n1 hubbard / heisenberg propagate 1x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 216s 99.9GiB/node
weak-1529m-n2 hubbard / heisenberg propagate 2x8x16 3,104,527,573 hubbard-cutoff=10, hubbard-lower-atol=1.73e-06 222s 100GiB/node
weak-1529m-n4 hubbard / heisenberg propagate 4x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 224s 101GiB/node
weak-1529m-n8 hubbard / heisenberg propagate 8x8x16 12,255,330,837 hubbard-cutoff=10, hubbard-lower-atol=7.35e-07 227s 99.9GiB/node
weak-1529m-n16 hubbard / heisenberg propagate 16x8x16 24,419,998,198 hubbard-cutoff=10, hubbard-lower-atol=4.68e-07 236s 102GiB/node
weak-1529m-n32 hubbard / heisenberg propagate 32x8x16 48,317,129,677 hubbard-cutoff=10, hubbard-lower-atol=2.94e-07 250s 101GiB/node
weak-1529m-n64 hubbard / heisenberg propagate 64x8x16 94,684,031,363 hubbard-cutoff=10, hubbard-lower-atol=1.82e-07 257s 84.3GiB/node
strong — fixed total terms, 1 to 64 nodes (17 rungs)
rung model / picture ops N x R/node x parts terms knobs cost/rep
strong-1569m-n1 hubbard / heisenberg propagate 1x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 216s 99.9GiB/node
strong-1569m-n2 hubbard / heisenberg propagate 2x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 106s 51.5GiB/node
strong-1569m-n4 hubbard / heisenberg propagate 4x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 53.3s 28GiB/node
strong-1569m-n8 hubbard / heisenberg propagate 8x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 28.6s 15.9GiB/node
strong-1569m-n16 hubbard / heisenberg propagate 16x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 18.1s 11GiB/node
strong-1569m-n32 hubbard / heisenberg propagate 32x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 16.1s 6.6GiB/node
strong-1569m-n64 hubbard / heisenberg propagate 64x8x16 1,569,152,761 hubbard-cutoff=10, hubbard-lower-atol=2.6e-06 24s 4.1GiB/node
strong-6126m-n2 hubbard / heisenberg propagate 2x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 450s 200GiB/node
strong-6126m-n4 hubbard / heisenberg propagate 4x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 224s 101GiB/node
strong-6126m-n8 hubbard / heisenberg propagate 8x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 110s 51.3GiB/node
strong-6126m-n16 hubbard / heisenberg propagate 16x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 58.8s 27.4GiB/node
strong-6126m-n32 hubbard / heisenberg propagate 32x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 38.1s 16.1GiB/node
strong-6126m-n64 hubbard / heisenberg propagate 64x8x16 6,125,805,627 hubbard-cutoff=10, hubbard-lower-atol=1.14e-06 37s 11.4GiB/node
strong-24420m-n8 hubbard / heisenberg propagate 8x8x16 24,419,998,198 hubbard-cutoff=10, hubbard-lower-atol=4.68e-07 471s 201GiB/node
strong-24420m-n16 hubbard / heisenberg propagate 16x8x16 24,419,998,198 hubbard-cutoff=10, hubbard-lower-atol=4.68e-07 235s 102GiB/node
strong-24420m-n32 hubbard / heisenberg propagate 32x8x16 24,419,998,198 hubbard-cutoff=10, hubbard-lower-atol=4.68e-07 127s 52.2GiB/node
strong-24420m-n64 hubbard / heisenberg propagate 64x8x16 24,419,998,198 hubbard-cutoff=10, hubbard-lower-atol=4.68e-07 79.6s 26.9GiB/node

CI

One workflow change, and it is not a gate: bench.yml drops the resting-memory threshold and
gains an artifact upload of bmf.json and the raw results, so a failed Bencher upload no longer
loses the measurement. bench_main.yml and bench_bare_metal.yml are untouched and still just
track the main branch.

Drive-by fix

report.py's _display_op collapsed every test_model_*[hubbard] and test_model_*[pauli] row
to the same label, so the two models were indistinguishable in the report. A parameter that is not
a picture now names the model and replaces the group: hubbard / propagate, pauli / build_graph.
Regression test updated in both directions.

Verification

Run on a Deucalion login node against this branch:

  • uv run pytest662 passed, 8 skipped (the 8 need --with-mpi).
  • prek run --from-ref origin/main --to-ref HEAD — every hook clean on the changed files.
  • Bench smoke — 8 benchmarks collect and run after the deletions.
  • Full bench run + monoprop-bench-report — the resting-footprint section is gone; the hubbard
    and pauli rows are now distinct.
  • monoprop-bench-bmf — exactly latency, peak-memory, terms; 20 benchmark names, all
    unchanged from main.
  • Every args entry of all 58 rungs checked against pytest --help's option list — no unknown
    flags.
  • Two reps of a tiny Hubbard rung end to end: the gate first refused a wrong expect_terms
    (137 measured vs 2,276 declared), then passed once corrected, and the ladder rendered both the
    table and the resolved model parameters.
  • The ladder-ordering fix was mutation-tested — reverting the sort key fails the new test.

Not done here

The 18 uncalibrated rows need a cluster allocation to measure; they refuse to run until then. The
private harness is updated separately to call monoprop-bench-rung instead of its own launchers,
and keeps working unchanged until it is.

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci labels Aug 29, 2026
@github-actions

Copy link
Copy Markdown

Docs preview: https://pr-310.monoprop-docs.pages.dev

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.70%. Comparing base (665508e) to head (712ddd9).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #310   +/-   ##
=======================================
  Coverage   97.70%   97.70%           
=======================================
  Files          14       14           
  Lines         742      742           
  Branches       98       98           
=======================================
  Hits          725      725           
  Misses         12       12           
  Partials        5        5           
Flag Coverage Δ
cpp 97.70% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

`benches/` carried three benchmarks nobody asked for and four measures where
three are read, while the benchmark set that actually matters -- the
10M/100M/1B ladder and the strong/weak scaling rungs -- existed only as flags
in a private shell script, so there was no fixed set a PR author could run and
report against.

Trim to four operations, uniform across both bench modules, both pictures and
both fixed models: `build_graph`, `propagate`, `energy`, `gradient`. Drop
`test_random_pare`, `test_random_inplace` (which is `propagate` followed by
`energy`) and `test_model` (`test_model_propagate` plus a trailing
`expectation_value`). These are deletions, never renames: a benchmark name is
Bencher's history key.

Drop `resting-memory` as a tracked measure, leaving latency, peak memory and an
exact-match term count. The raw `memrest`/`membase` recordings stay in
`conftest.py` -- unrendered here, but read by the out-of-tree A/B harness.

Add `benches/rungs.toml`: one row per cell of the benchmark set, giving the
picture, model, operations, geometry, size knobs, the exact term count that
configuration produces, and what one rep last cost in wall seconds and GiB per
node. `monoprop-bench-rung` runs one rep; `monoprop-bench-ladder` collates them
into a markdown block -- the timings, the peak memory, and the resolved
parameters of every problem measured -- to paste into a pull request.

Nothing runs automatically and nothing gates. Running the rungs a change could
plausibly move, and putting the numbers in the PR, is the author's job.

The runner refuses a result whose term count, node count, rank count, partition
count or round count disagrees with the row, so a mistyped knob fails the cell
instead of quietly measuring a different problem. A row nobody has calibrated
says so twice -- `expect_terms = 0` and `TBD` on the unmeasured knob -- and
refuses to run; `TBD` rather than `0` because `lower_atol = 0` prunes nothing
and is the largest problem the model can pose. `cost_seconds` and
`cost_gib_per_node` are the opposite: documentation, never a gate.

`propagate` carries the full ladder to a billion terms; `build_graph`, `energy`
and `gradient` stop at one node, because `build_graph` extends the graph rather
than replacing it and retains one layer-set per gate.

`bench.yml` loses the `resting-memory` threshold and gains an artifact upload,
so a failed Bencher upload no longer loses the measurement. No other CI change.

Also fixes a pre-existing bug in `report.py`, where every `test_model_*[hubbard]`
and `test_model_*[pauli]` row rendered under the same label.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
@diagonal-hamiltonian diagonal-hamiltonian changed the title chore(benches): make the benchmark set a table, and gate performance PRs on it chore(benches): make the benchmark set a fixed, runnable table Aug 29, 2026
`rungs.toml` says what the benchmark set is; nothing said how to run it. A reader
had the row's `args` but no way to learn what `--hubbard-observable-site` means or
what a sensible `srun` line looks like, and the parameters were only discoverable
by reading the model dataclasses.

`benches/RUNGS.md` covers: the three commands; what each field of a row declares,
and which of them gate and which only document; every parameter of all three
models with its default and its effect; why `lower_atol` is the size knob and the
other axes are saturated; how nodes, ranks and partitions map onto a machine and
why P is the number that matters; example Slurm scripts for one node, many nodes
and a ladder in one allocation; the pinning and allocation-sizing traps measured
on Deucalion; and how to calibrate a TBD row off the gate's own refusal message.

Every shell example is syntax-checked and every documented invocation was run.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
…pped rung

The graph and eval rows stopped at a single `n1-cap-*` rung per picture, so the
100M, 1B and multinode-1B points the set is supposed to cover were absent for
`build_graph`, `energy` and `gradient` — present only for `propagate`.

That cap rested on an extrapolation from a 100-generator measurement (~1234 B per
term for build_graph), applied to rows that run 1000 generators, where the
retained layer count is an order of magnitude larger. It was a guess standing in
for the measurement, and naming the rung `n1-cap` hid the question rather than
answering it.

Declare the graph and eval rows at the same 10M / 100M / 1B / multinode-1B points
as propagate, in both pictures: 4 cap rows become 12, and the size family goes
from 20 rungs to 28 (12 propagate + 16 graph/eval). Calibration now answers which
of them fit. A row that does not fit is a result: record the node count it needed,
or that it exceeded the machine, in its `note` and leave it uncalibrated.

Also document two system-size ceilings that the option list does not show.
`monoprop_MAX_NUM_MODES` defaults to 250, so `--num-modes` is capped at 250 and
`--hubbard-num-sites` at 125. `--pauli-num-qubits` is effectively fixed at 127:
the circuit is built over a hard-coded IBM Eagle heavy-hex map of 144 pairs whose
highest index is 126, so a lower value fails and a higher one adds idle qubits.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
RUNGS.md showed one rung and one ladder within an allocation, but not how to run
a family end to end: a family needs one allocation per node count, and nothing
said where that job list comes from.

Add a `Running everything` section that generates the job list from the table
itself — the table is the only place that knows which rungs share a node count —
then submits one `sbatch` per line and collates once over every job's artifacts.
The generator's output is reproduced verbatim from a real run of it.

Add a `What you get out` section showing the collated `strong` table and the
`Problems measured` block. The medians are the shipped campaign's own, replayed
through the collator, so `vs declared` reads `1.000x` by construction; that is
stated inline so nobody reads it as a fresh measurement. The point is the shape,
and the shape is the answer: `Mterms/s/node` goes 7.28 at one node, 6.86 at
eight, 1.02 at sixty-four, so the ladder turns over well before 64 nodes.

Every shell block is syntax-checked and the job-list generator was run.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
RUNGS.md carried the memory law but no way to answer "what will this cost me":
the map from the size knob to terms, memory and time existed only inside the rung
rows.

Add the measured hubbard/propagate table -- eleven `lower_atol` values against
terms, GiB/node, median seconds and Mterms/s/node, from the campaign the table
ships. Two things fall out: the knob is steep, roughly doubling terms per halving,
and throughput is nearly flat at ~7 Mterms/s/node across three orders of
magnitude, which makes it a usable planning number.

Add planning rules for node count and wall time, each checked against the
measured rows and the check shown: 1569 Mterms predicts 1.0 nodes at 100 GiB
against 1 measured, 24420 predicts 8.1 at 200 GiB against 8, and the time rule
lands within ~15%.

Refit the memory law on the shipped costs: GiB/node = 3.40 + 0.0634 x
Mterms/node, marginal 68.0 B/term. Note that the constant dominates below about
1000 Mterms/node, where apparent bytes-per-term reads ~180 rather than ~68 -- a
per-term figure taken from a small rung overestimates a large one badly.

Replace the build_graph assertion with a measurement. At identical parameters
(cutoff 6, lower_atol 1e-4) propagate runs 29 Trotter steps in 0.38 GiB while
build_graph was killed above 20 GiB at 2 steps: at least fiftyfold, on the same
problem, rather than an extrapolation from a 100-generator estimate.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
…ze knob

The sizing data covered hubbard/propagate only -- all 38 shipped rungs are that
one configuration. Nothing said what pauli costs, what the random model costs, or
what any model costs at its defaults, which is the first thing anyone runs.

Sweep 63 cells: three models, four operations, both pictures, each at its
defaults and along a size-knob ladder. 42 produced numbers; 21 were killed at the
login node's 20 GiB per-user cap, which is recorded as `> 20 GiB` -- a lower
bound and a fact about that node, not about the model.

Findings worth the table space:

- At the defaults only `propagate` fits in 20 GiB on hubbard; `build_graph`,
  `energy` and `gradient` all hold a graph and all died.
- The random model is 159x larger in the Schrodinger picture than the Heisenberg
  one at identical knobs, `schrodinger_cutoff` being `cutoff + 2`.
- Marginal cost per term, fitted over each ladder so the ~380 MiB process floor
  drops out: hubbard/propagate 63.2 B/term, pauli/propagate 79.4,
  pauli/build_graph 134.0, random 134-150 across all four operations.
- Hubbard's 63.2 B/term here against 68.0 fitted over 38 compute-node rungs is
  the only independent check available on any of these numbers: two machines, two
  partition counts, two harnesses, 7% apart.
- `--obs-terms` maps to propagated terms at a flat x33 over the range measured,
  so a random rung can be calibrated by starting at `target / 33` instead of
  guessing. That starting point now sits in each of the 20 random rows' `note`,
  with the extrapolation flagged: the ratio was measured up to 0.66M terms and
  the largest target is 1B.

Assisted-by: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@diagonal-hamiltonian
diagonal-hamiltonian marked this pull request as draft August 30, 2026 13:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant