Runtime benchmark campaigns¶
Audience: operators deciding whether a target-optimized build of a program earns its place next to the portable generic one.
vq benchmark plans a generic-versus-optimized comparison, runs it through
the queue, and analyzes the recorded runs. plan and analyze are local and
read-only. run submits the plan’s runs, one at a time per node class, and
collects their results; nothing runs unless an operator starts it. The real
pilot measurements are separate, admitted work, and nothing here claims a
speedup.
vq benchmark plan plan.json # validate; print digest and run order
vq benchmark plan plan.json --json # the same, for a runner
vq benchmark run plan.json binding.json RUN_DIR # submit and collect; safe to stop and rerun
vq benchmark analyze plan.json RUN_DIR/results.json # the report as a table
vq benchmark analyze plan.json results.json --json --csv cells.csv
vq benchmark analyze plan.json results.json --svg speedups.svg
The plan: vq.benchmark-plan/1¶
A plan holds everything that must stay fixed for a comparison to mean anything, and the analysis refuses results recorded against any other plan.
{
"schema_name": "vq.benchmark-plan/1",
"plan_id": "isa-pilot",
"baseline": "generic",
"variants": [
{"name": "generic", "runtime_digest": "<sha256>", "build_flags": "-O2 -march=x86-64-v2"},
{"name": "tuned", "runtime_digest": "<sha256>", "build_flags": "-O2 -march=sapphirerapids",
"libraries": {"openblas": "0.3.28"},
"build_cost_core_seconds": 36000, "deployment_cost_core_seconds": 1800,
"storage_bytes": 400000000}
],
"workloads": [
{"name": "h2o-ccsd", "input_digest": "<sha256>", "method": "CCSD",
"algorithm": "conventional", "nbf": 58,
"checks": [{"quantity": "total_energy", "abs_tolerance": 1e-8}]}
],
"node_classes": ["class-a"],
"layout": {"mpi_ranks": 1, "threads_per_rank": 8},
"warmup": 1,
"repeats": 5,
"seed": 20261012,
"confidence": 0.95,
"margin": 0.02
}
Variants are identified by runtime digest; two variants with one digest are refused. Record build flags and loaded numerical libraries. Compare one factor at a time: a different BLAS or compiler is its own plan, not a third variant mixed into an ISA comparison. Keep fast-math or reduced-precision builds out of the primary comparison; they need their own scientific validation first.
Workloads are identified by input digest and need at least one correctness check. Record Nbf, the auxiliary basis size, method and algorithm where they apply.
Node classes are operator-chosen labels for one compute-node type, not host names. Do not put private host names in a plan or results file.
repeatsis at least 5.warmupblocks run first and never count.marginis the practical-significance band: a ratio within1 ± marginis no difference worth a separate artifact.
vq benchmark plan prints the plan’s plan_sha256. The digest covers the
plan’s content, not its formatting or key order. It also prints the run order.
For each node class the blocks run in sequence, and each block contains
every workload/variant pair once, shuffled by seed, so no variant always
runs first or last. The same plan always gives the same order.
The results: vq.benchmark-results/1¶
{
"schema_name": "vq.benchmark-results/1",
"plan_sha256": "<from vq benchmark plan>",
"runs": [
{"workload": "h2o-ccsd", "variant": "tuned", "node_class": "class-a", "block": 1,
"status": "completed", "runtime_digest": "<sha256>",
"hardware": {"cpu_model": "Example CPU 8480", "mpi_ranks": 1, "threads_per_rank": 8},
"steady_seconds": 80.2, "startup_seconds": 2.1, "cpu_seconds": 640.9,
"peak_rss_mb": 1012.0, "scf_iterations": 12,
"values": {"total_energy": -76.241123456}}
]
}
One run per workload, variant, node class and block; a duplicate, or a run
the plan does not contain, is an error. status is one of completed,
failed, unconverged, time_exceeded, oom_killed and killed.
steady_seconds is the execution time after startup, and the comparison is
made on it. Cold startup, CPU time, peak memory and SCF iterations are
reported separately. Keep scheduler waiting, build and transfer time out of
all of them.
Running a plan: vq benchmark run¶
vq benchmark run plan.json binding.json ~/benchmarks/isa-pilot
The plan says what is compared; the binding says where and how, and stays with the operator, so the plan and its results can be shared without a host name in them.
{
"schema_name": "vq.benchmark-binding/1",
"plan_sha256": "<from vq benchmark plan>",
"node_classes": {"class-a": {"host": "cluster-a"}},
"variants": {
"generic": {"command": ["/opt/vibeqc/generic/bin/python", "run.py"]},
"tuned": {"command": ["/opt/vibeqc/spr/bin/python", "run.py"]}
},
"workloads": {"h2o-ccsd": {"sources": "inputs/h2o-ccsd", "args": []}},
"mem_mb": 16000,
"wall_time_seconds": 7200
}
node_classesmaps each node class to a configured host or scheduler lane; withouthost, the class runs on this machine. Every node class, variant and workload of the plan must be bound, and nothing else.variantsgives each variant’s command (and optionally a registeredprogram). A run’s command is the variant’s command followed by the workload’sargs.workloadsnames each workload’s input directory, relative to the binding file. Its content must match the plan’sinput_digest, the same digestvq campaign validatecomputes for node sources: every entry’s path, type, size and SHA256, with symbolic links and special files refused. It is copied into each run’s payload, so it arrives in the job’s workspace.Each run asks for the layout’s cores (
mpi_ranks × threads_per_rank) and the binding’smem_mbandwall_time_seconds.
Every problem with the binding is listed before anything is submitted: an unbound or extra name, a host that is neither this machine nor configured, or an input whose content does not match its pin.
Order and isolation. Runs follow vq benchmark plan’s seeded order. On
one node class they never overlap: the next run is submitted only after the
previous one finished, so no run competes with another for its node.
Different node classes run side by side.
If a run hits a problem, such as an unrecognised queue state, later runs on its node class stay blocked on every pass. A problem does not prove the accepted job has stopped. Independent node classes continue in their own order. Resolve the reported problem and restart with the same run directory; the runner checks the existing accepted job before continuing that class. It does not cancel or replace the job to make progress.
What the job reports. A benchmark job writes vq-benchmark-run.json in
its workspace (vq.benchmark-run-report/1):
{"schema_name": "vq.benchmark-run-report/1", "runtime_digest": "<sha256>",
"converged": true, "steady_seconds": 80.2, "startup_seconds": 2.1,
"scf_iterations": 12, "values": {"total_energy": -76.241123456},
"cpu_model": "Example CPU 8480", "mpi_ranks": 1, "threads_per_rank": 8}
Only the job can time its own steady state and know its result, and it
reports the runtime it actually ran: a portable runtime’s artifact ID, for
example. vq does not vouch for these values. The analysis excludes a run
whose runtime or layout differs from the plan, and converged: false records
the run as unconverged. The layout defaults to the planned one when the
report leaves it out.
vq benchmark measure writes the report for you for vibe-qc and ORCA.
Use it as the variant’s command:
"variants": {
"generic": {"command": ["vq", "benchmark", "measure", "--program", "vibe-qc",
"--runtime-root", "/opt/runtimes/vibeqc-generic",
"--", "{runtime}/bin/python", "run.py"]},
"tuned": {"command": ["vq", "benchmark", "measure", "--program", "vibe-qc",
"--runtime-root", "/opt/runtimes/vibeqc-spr",
"--", "{runtime}/bin/python", "run.py"]}
}
It runs the command, passes its output through to the job’s log unchanged,
keeps its exit code, and writes vq-benchmark-run.json in the workspace:
Startup and steady time. Startup is the time to the first SCF iteration line of the output; steady time is the rest of the run. A program that prints no iteration line is timed as steady throughout and reports no startup, and is not counted as converged.
Values and convergence. The total energy (as
total_energy), the gap where printed (gap_ev), and the SCF iteration count are read by the same parsers as the console’s calculation preview. AFATALline, an optimisation that did not converge, a missing SCF result or a non-zero exit makes the runconverged: false. The wrapper must observe an SCF iteration and the final cycle’s result (SCF converged ...for vibe-qc,FINAL SINGLE POINT ENERGYfor ORCA). A new SCF header, geometry cycle or iteration 1 clears the previous cycle’s completion. An energy value alone or an earlier successful cycle cannot prove convergence. Completion and fatal-error evidence are tracked across the output stream, even when those lines scroll out of the bounded preview tail. Unconverged runs stay in the results with their timings and are excluded from speedup pairs.Runtime.
--runtime-rootnames a portable runtime deployment (vq runtime-bundle deploy). The wrapper reads its verified current selection, expands{runtime}in the command to that generation’s payload, and reports the selection’s artifact ID. The digest therefore names the code that ran, and it is what the plan’s variantruntime_digestshould be. A selection that changes during the run is refused, and no report is written. For a runtime not deployed as a bundle,--runtime-digest SHA256records an identity you declare instead.CPU model, from the platform where it exposes one.
The command needs vq where the job runs, as on any vq host or scheduler
cluster with the vq helper.
What the runner records. For each finished run the runner brings the
report back with vq fetch HOST JOB --name vq-benchmark-run.json, and takes
peak RSS and CPU time from the job’s measured resource_usage in
vq status --json. It then writes one row to RUN_DIR/results.json
(vq.benchmark-results/1), rewritten after every run.
A completed job without a valid report is left missing, and the analysis lists it as missing. vq never fills in a timing.
A job that failed, ran out of memory or time, or was killed is recorded with that status. Its
runtime_digestis the planned one, which the analysis never compares for a run that did not complete.
Stopping and restarting. Each run is submitted with
vq submit HOST --idempotency-key, its key derived from the plan digest and
the run’s place in the order, scoped to a random campaign identity in the
journal. That identity is recorded before the first request is sent. A fresh
run directory starts a separate campaign and submits new jobs even when the
plan is unchanged. Keep the existing directory to resume its campaign;
moving or renaming that directory preserves its identity and submission keys.
Older journals without an identity retain their original keys when resumed.
The run directory is locked. Rerunning the same command after a crash or a
lost answer resends under the same key and the queue returns the job it
already accepted. A run’s staged input is checked against its pin before
every send, and a changed one is never sent. --once makes one pass and
exits. The exit status is 0 when every run is recorded (or, with --once,
while runs remain without a problem), and 1 if a run was left missing or
hit a problem.
The runner checks the plan digest in both the journal and any existing
results.json before contacting the queue or writing a checkpoint. Results
from another plan stop the run with an error. Restore this campaign’s
matching results before resuming; use a separate run directory to start a
new campaign.
Every run the journal marks recorded must have its measurement row in
results.json. A missing file or missing recorded row stops the runner
before queue contact or checkpoint writes, rather than reporting success
with lost measurements. Restore the campaign’s matching results before
resuming. Existing job identities and saved files are preserved.
Result rows are saved before the journal marks their runs recorded. If a checkpoint write is interrupted, restart the runner with the same run directory. A job whose journal entry still says accepted is collected again; its existing result row, if any, is replaced by the same planned run key. Recovery preserves the accepted job and records its measurement once before starting the next run on that node class.
The analysis¶
For each node class, workload and non-baseline variant, each block’s variant run is paired with the baseline run of the same block. A pair counts only when:
both runs completed, with their planned runtime digest and the planned layout, and recorded
steady_seconds;both ran on the same CPU model, when it is recorded;
every declared check agrees:
|variant - baseline| <= abs_tolerance + rel_tolerance * |baseline|. A fast run that fails a check is not a speedup.
Every pair that does not count is listed under excluded with its reason,
and a run that is missing is listed under missing. Nothing is dropped
silently.
Reported |
Meaning |
|---|---|
ratio |
|
interval |
Student t interval at |
verdict |
|
break-even |
Only for |
aggregate |
Per node class and variant, the equal-weight geometric mean of the per-workload ratios. Only workloads with all planned pairs and a comparable node class are included; the others are listed as left out. No interval is given, because workloads are not samples of one population. |
The JSON report (vq.benchmark-report/1) repeats the plan’s identities under
applies_to: runtime digests, build flags, libraries, node classes and
layout. Its results lose applicability when any of these change. The same
plan and results always give the same report, byte for byte, whatever order
the runs were recorded in. It contains no host names. --csv writes one row
per node class, workload and variant.
For break-even, each retained sample is (T_baseline - T_variant) * cores.
Its lower confidence bound is the sample mean minus
t_quantile((1 + confidence) / 2, n - 1) * sample_sd / sqrt(n).
This is a separate interval from the log-ratio interval: consistent relative
speedups do not establish a precise saving in core-seconds when absolute
durations vary. A non-positive mean saving leaves both counts unavailable;
a positive mean with a non-positive lower bound gives only the point count
and an explanatory reason. Both intervals assume independent measured pairs.
The text report prints the same table. Where a cell’s break-even is shown as
237 (244), the second number is the conservative count. 237 (-) means
the conservative count is unavailable, with the reason in the notes. JSON
uses null and CSV an empty field for an unavailable count. The report’s
settings.break_even_interval identifies the paired-saving interval.
Exporting a plot¶
analyze --svg speedups.svg also writes a standalone vector figure, suitable
for a browser or a report, without requiring a plotting package. It can be
combined with --json and --csv. Export destinations must differ from the
input files and from each other, including symbolic links and hard links.
The figure groups workloads by node class and variant on a shared logarithmic
axis. Each point is the reported geometric mean of T_baseline / T_variant;
its bar is the configured Student-t confidence interval. The reference line
at 1 means equal steady execution time. The shaded equivalence band has the
same bounds as the analysis, 1 / (1 + margin) through 1 + margin.
Every row includes retained/planned pairs, verdict, and excluded and missing
pair counts. Inconclusive points are hollow. A single retained pair has no
interval; absent or unplottable ratios read Unavailable. An incomparable
cell has no plotted estimate even when individual ratios exist. Unknowns
never become zero-speedup points. Warm-ups remain excluded from the analysis.
Row tooltips retain the reasons for exclusions and missing pairs. The figure embeds the full JSON report as inert SVG metadata, including input/runtime identities and analysis settings; the plan and results digests are also printed below the plot. Sharing the figure shares that report. The same inputs produce identical SVG bytes, independent of recorded run order.
Not in this increment¶
Any measurement. The pilot on a real compute-node class is a separately admitted execution plan, and nothing here claims a speedup.
Measurement wrappers for programs other than vibe-qc and ORCA; their jobs write
vq-benchmark-run.jsonthemselves.