Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
558c530
feat(benches): :sparkles: per-operation time and memory profiling
diagonal-hamiltonian Aug 15, 2026
0802401
feat(benches): :sparkles: split the fixed models into four timed oper…
diagonal-hamiltonian Aug 15, 2026
62c4f38
fix(benches): :bug: skip the model benchmarks whose graph cannot fit
diagonal-hamiltonian Aug 15, 2026
1725b83
fix(benches): 🐛 take the entry barrier out of the timed region
diagonal-hamiltonian Aug 17, 2026
1d7cb04
feat(benches): ✨ identify an A/B arm by its binary, not by its versio…
diagonal-hamiltonian Aug 17, 2026
bffd781
test(cpp): ⚡ skip fabric init in the single-process test variants
diagonal-hamiltonian Aug 13, 2026
5542242
docs(test): 📝 size the fabric-init win on the gate hardware, not the …
diagonal-hamiltonian Aug 17, 2026
c48f015
bench: record which CPUs each rank's threads were pinned to
diagonal-hamiltonian Aug 18, 2026
cd58352
benches: reduce affinity_cpus over ranks, not rank 0 only
diagonal-hamiltonian Aug 18, 2026
5e97929
Merge origin/main into pr/bench-and-test-harness
diagonal-hamiltonian Aug 19, 2026
a330d71
docs: 📝 trim the bench and fabric notes to what the code cannot say
diagonal-hamiltonian Aug 19, 2026
302767e
refactor(bench): ♻️ cut comments to invariants, drop the unread THP f…
diagonal-hamiltonian Aug 19, 2026
7566da6
Merge branch 'main' into pr/bench-and-test-harness
diagonal-hamiltonian Aug 19, 2026
0d3c278
Merge branch 'main' into pr/bench-and-test-harness
Panadestein Aug 19, 2026
2638b2b
refactor(bench): ♻️ cut every docstring to one line, drop the unread …
diagonal-hamiltonian Aug 19, 2026
58e8648
fix(benches): ⚡ give the four model benchmarks their untimed entry ba…
diagonal-hamiltonian Aug 19, 2026
bc6ebf6
fix(bench-tools): 🐛 keep per-operation term counts out of Bencher's o…
diagonal-hamiltonian Aug 19, 2026
f6d447f
test(benches): ✅ assert the benchmarked operation produced terms
diagonal-hamiltonian Aug 19, 2026
16b9024
Merge remote-tracking branch 'origin/pr/bench-and-test-harness' into …
diagonal-hamiltonian Aug 19, 2026
c6e37d3
Merge branch 'main' into pr/bench-and-test-harness
diagonal-hamiltonian Aug 19, 2026
a2f543e
style(bench): 🎨 take the reviewer's wording for the two condensed com…
diagonal-hamiltonian Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ Key files:
change to the recorded sections has to land on both sides of the package boundary. Benchmark
names are Bencher's history key, so renaming or moving a `bench_*` test orphans its tracked
series.
- **`rounds > 1` overlaps two rounds' live memory** (`setup=` runs before the prior round's
teardown) — pin `--bench-rounds=1`; `record_memory` measures that construction transient,
not per-op cost (use `op_memory`).
- **Peak memory is `HighWaterMark`, not sampled PSS** — exact, unlike `/proc/self/smaps_rollup`.

### Core abstractions (the propagation backbone)

Expand Down Expand Up @@ -137,6 +141,8 @@ mp = MajoranaPropagator(operator, initial_state, cutoff=4)
- Fixture msgpack schema is documented in `tests/data/README.md`
- Tests validate against exact solutions for small systems
- Heavy use of `@parametrize_with_cases` decorators
- **pytest's fd-level capture hides C++ stderr** (e.g. `COMMPROF`) — rerun with `-s` to see it.
- **A slow CTest run on an MPI build is `MPI_Init` fabric probing, not slow tests** — see `monoprop_TEST_EXCLUDE_MPI_FABRIC` in `cpp/tests/CMakeLists.txt`.

## Key Dependencies & Integration

Expand Down Expand Up @@ -176,5 +182,7 @@ When changing behavior, APIs, build/test workflows, paths, or developer conventi
- Check `build/*/compile_commands.json` for compilation flags
- Use `rm -rf build` to clear environment-specific builds
- Verify `monoprop_MAX_NUM_MODES` matches your use case (default: 250)
- **`uv sync` does not relink `bin/monoprop_unit_tests.x`** — check its mtime against the
source's; recipe for a standalone C++ build tree in `docs/content/docs/building.mdx`.

This is a sophisticated scientific computing project requiring careful attention to template instantiation, build system configuration, and the C++/Python boundary.
151 changes: 148 additions & 3 deletions benches/bench_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,37 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Fixed-model benchmarks: heavy, Heisenberg-only, in-place simulations.
"""Fixed-model benchmarks: heavy, Heisenberg-only simulations.

The 120-qubit Fermi-Hubbard trajectory and the 127-qubit Pauli-basis kicked-Ising
circuit, at fixed sizes. The registry (config class, builder, steps-per-run) lives
in :data:`monoprop_bench_tools.models.MODELS`; each config field is overridable via
``--<model>-<field>``.

Run one group per pytest process: build_graph/propagate and energy/gradient do not fit a node together.
"""

from __future__ import annotations

import os
from typing import Any

import pytest
from monoprop_bench_tools.memory.cpu import resting_rss_bytes
from monoprop_bench_tools.models import MODELS, barriered
from monoprop_bench_tools.models import MODELS, barrier_setup, barriered

# `build_graph` extends the graph, so a driver that re-applies its circuit retains one layer-set per step
MAX_GRAPH_STEPS = 2


def skip_if_graph_will_not_fit(model: str, steps: int) -> None:
"""Skip a graph-holding benchmark whose retained graph is known not to fit."""
allow = os.environ.get("monoprop_BENCH_ALLOW_BIG_GRAPH") # noqa: SIM112
if steps > MAX_GRAPH_STEPS and allow != "1":
pytest.skip(
f"{model}: {steps} successive build_graph calls retain {steps} layer-sets, "
f"measured to exceed 242 GiB. Set monoprop_BENCH_ALLOW_BIG_GRAPH=1 to run it."
)


@pytest.mark.slow
Expand Down Expand Up @@ -59,9 +75,138 @@ def run(built, n_steps):
return propagator.expectation_value()

result = benchmark.pedantic(
barriered(run, bench_comm), setup=setup, rounds=1, iterations=1
barriered(run, bench_comm),
setup=barrier_setup(bench_comm, setup),
rounds=1,
iterations=1,
)
assert isinstance(result, float)

propagator, _circuit = state["built"]
record_model_stats(model, propagator, state["baseline_rss"])


@pytest.mark.slow
@pytest.mark.parametrize("model", list(MODELS))
def test_model_build_graph(
benchmark,
bench_comm,
bench_rounds,
model_configs,
model,
record_model_config,
op_memory,
record_opsize,
):
"""Benchmark building a fixed model's propagation graph, from a fresh propagator."""
_config_cls, build_fn, steps_fn = MODELS[model]
config = model_configs[model]
steps = steps_fn(config)
skip_if_graph_will_not_fit(model, steps)
record_model_config(model, config)

last = []

def setup():
built = build_fn(config, comm=bench_comm)
last[:] = [built[0]]
op_memory.open() # inside setup, so the window excludes the construction
return (built, steps), {}

def build(built, n_steps):
propagator, circuit = built
for _ in range(n_steps):
propagator.build_graph(circuit)

benchmark.pedantic(
barriered(build, bench_comm),
setup=barrier_setup(bench_comm, setup),
rounds=bench_rounds,
iterations=1,
)
op_memory.close(last[0])
assert record_opsize(last[0]) > 0
assert last[0].graph_layers > 0


@pytest.mark.slow
@pytest.mark.parametrize("model", list(MODELS))
def test_model_propagate(
benchmark,
bench_comm,
bench_rounds,
model_configs,
model,
record_model_config,
op_memory,
record_opsize,
):
"""Benchmark a fixed model's in-place evolution alone -- no expectation value, no graph."""
_config_cls, build_fn, steps_fn = MODELS[model]
config = model_configs[model]
steps = steps_fn(config)
record_model_config(model, config)

last = []

def setup():
built = build_fn(config, comm=bench_comm)
last[:] = [built[0]]
op_memory.open()
return (built, steps), {}

def run(built, n_steps):
propagator, circuit = built
for _ in range(n_steps):
propagator.propagate(circuit)

benchmark.pedantic(
barriered(run, bench_comm),
setup=barrier_setup(bench_comm, setup),
rounds=bench_rounds,
iterations=1,
)
op_memory.close(last[0])
assert record_opsize(last[0]) > 0


@pytest.mark.slow
@pytest.mark.parametrize("model", list(MODELS))
def test_model_energy(
benchmark, model_graph, model, model_configs, bench_comm, bench_rounds, op_memory
):
"""Benchmark evaluating a fixed model's expectation-value functional."""
skip_if_graph_will_not_fit(model, MODELS[model][2](model_configs[model]))
propagator, parameters = model_graph(model)
functional = propagator.expectation_value_functional()
op_memory.open()
result = benchmark.pedantic(
barriered(functional, bench_comm),
args=(parameters,),
setup=barrier_setup(bench_comm),
rounds=bench_rounds,
iterations=1,
)
op_memory.close(propagator)
assert isinstance(result, float)


@pytest.mark.slow
@pytest.mark.parametrize("model", list(MODELS))
def test_model_gradient(
benchmark, model_graph, model, model_configs, bench_comm, bench_rounds, op_memory
):
"""Benchmark evaluating a fixed model's expectation-value-and-gradient functional."""
skip_if_graph_will_not_fit(model, MODELS[model][2](model_configs[model]))
propagator, parameters = model_graph(model)
functional = propagator.expectation_value_and_gradient_functional()
op_memory.open()
_value, gradient = benchmark.pedantic(
barriered(functional, bench_comm),
args=(parameters,),
setup=barrier_setup(bench_comm),
rounds=bench_rounds,
iterations=1,
)
op_memory.close(propagator)
assert len(gradient) == len(parameters)
87 changes: 77 additions & 10 deletions benches/bench_random.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,73 @@

from __future__ import annotations

from monoprop_bench_tools.models import barriered
from monoprop_bench_tools.models import barrier_setup, barriered

PARE_THRESHOLD = 1e-10
INPLACE_LOWER_ATOL = 1e-5


def test_random_build_graph(
benchmark, make_random_propagator, bench_comm, bench_rounds
benchmark,
make_random_propagator,
bench_comm,
bench_rounds,
op_memory,
record_opsize,
):
"""Benchmark building the propagation graph from a fresh propagator."""
last = []

def setup():
return (make_random_propagator(),), {}
built = make_random_propagator()
last[:] = [built[0]]
op_memory.open() # inside setup, so the window excludes the construction
return (built,), {}

def build(built):
propagator, circuit = built
propagator.build_graph(circuit)

benchmark.pedantic(
barriered(build, bench_comm), setup=setup, rounds=bench_rounds, iterations=1
barriered(build, bench_comm),
setup=barrier_setup(bench_comm, setup),
rounds=bench_rounds,
iterations=1,
)
op_memory.close(last[0])
assert record_opsize(last[0]) > 0
assert last[0].graph_layers > 0


def test_random_propagate(
benchmark,
make_random_propagator,
bench_comm,
bench_rounds,
op_memory,
record_opsize,
):
"""Benchmark in-place evolution alone, with no expectation value and no graph."""
last = []

def setup():
built = make_random_propagator()
last[:] = [built[0]]
op_memory.open()
return (built,), {}

def run(built):
propagator, circuit = built
propagator.propagate(circuit)

benchmark.pedantic(
barriered(run, bench_comm),
setup=barrier_setup(bench_comm, setup),
rounds=bench_rounds,
iterations=1,
)
op_memory.close(last[0])
assert record_opsize(last[0]) > 0


def test_random_pare(benchmark, built_graph, bench_comm, bench_rounds):
Expand All @@ -47,34 +93,52 @@ def pare():
pare_threshold=PARE_THRESHOLD,
)

benchmark.pedantic(barriered(pare, bench_comm), rounds=bench_rounds, iterations=1)
benchmark.pedantic(
barriered(pare, bench_comm),
setup=barrier_setup(bench_comm),
rounds=bench_rounds,
iterations=1,
)


def test_random_energy(
benchmark, built_graph, random_problem, bench_comm, bench_rounds
benchmark, built_graph, random_problem, bench_comm, bench_rounds, op_memory
):
"""Benchmark evaluating the expectation-value functional."""
functional = built_graph.expectation_value_functional()
op_memory.open()

# The entry barrier must stay untimed, so it lives in a setup, and a setup that returns args forbids args=.
def setup():
return (random_problem.parameters,), {}

result = benchmark.pedantic(
barriered(functional, bench_comm),
args=(random_problem.parameters,),
setup=barrier_setup(bench_comm, setup),
rounds=bench_rounds,
iterations=1,
)
op_memory.close(built_graph)
assert isinstance(result, float)


def test_random_gradient(
benchmark, built_graph, random_problem, bench_comm, bench_rounds
benchmark, built_graph, random_problem, bench_comm, bench_rounds, op_memory
):
"""Benchmark evaluating the expectation-value-and-gradient functional."""
functional = built_graph.expectation_value_and_gradient_functional()
op_memory.open()

def setup():
return (random_problem.parameters,), {}

_value, gradient = benchmark.pedantic(
barriered(functional, bench_comm),
args=(random_problem.parameters,),
setup=barrier_setup(bench_comm, setup),
rounds=bench_rounds,
iterations=1,
)
op_memory.close(built_graph)
assert len(gradient) == len(random_problem.parameters)


Expand All @@ -90,6 +154,9 @@ def run(built):
return propagator.expectation_value()

result = benchmark.pedantic(
barriered(run, bench_comm), setup=setup, rounds=bench_rounds, iterations=1
barriered(run, bench_comm),
setup=barrier_setup(bench_comm, setup),
rounds=bench_rounds,
iterations=1,
)
assert isinstance(result, float)
Loading
Loading