chore: update the benchmark scripts and documentation - #148
Open
ludmilaasb wants to merge 109 commits into
Open
Conversation
…untime Re-homes the performance work from perf/align-with-paper onto the current public interface (#40). The diff vs main is performance-only; the propagator Python/C++ interface is unchanged. What lands: - Rebuilt evolution/layer-build engine (detail/evolution/layer_build/*, CosineRecompute, LayerBuilder) aligning the build path with the paper, plus the operator store rework (detail/operator/{MPOperator,OperatorIndex, InvertedIndex}) and cosine-recompute graph encoding, replacing the old masked_execution_plan / MPGraphEncodingCompression / EvolutionMajorana. - Operator sharding runtime (detail/shard/{CpuTopology,ShardGroup}, detail/mpi/*): one single-threaded shard per physical core is the default parallelism (shards=0 => auto), composing MPI ranks and shards into one flat hybrid world; query (two-pass) is the sole cross-rank exchange protocol. - In-repo std::thread pool (run_static) replacing oneTBB; drop the oneTBB dependency from CMake, packaging, and the Config export. - Additive-only public surface: basis/shards ctor kwargs (defaulted), evolved_operator_terms, __deepcopy__ — main's interface otherwise untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Migrate the C++ unit suite to the ported engine internals (OperatorIndex / MPOperator / cosine-recompute, the shard runtime, the align mpi:: transport). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two engine-behavior adaptations, no interface changes: - Sharding is now the default parallelism, so white-box tests that inspect raw per-partition internals (graph_data()) opt out via a new @pytest.mark.unsharded marker (autouse fixture sets monoprop_SHARDS=off before construction). - upper_atol in the rebuilt build path RESCUES over-cutoff partner terms (it can only add terms, never drop). At cutoff 6 the LiH molecule has no over-cutoff partners, so exercise the rescue at cutoff 2 where it observably changes the size. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oops Every parallel_for_*/parallel_reduce_* wrapper and the layer-build chunk helpers (for_each_chunk / append_*_in_order) took a serial-fallback branch on the default shard runtime — each shard master sets gate_serial_override, so effective_parallelism() reports 1 and the pool threads never start. Only the non-default SHARDS=off path ever engaged them. Rewrite all ~46 call sites (13 files) as the plain serial loop each wrapper's fallback already ran, so results are bit-identical on the default path. The in-repo thread pool (Threading.h/.cpp) and Parallel.h still exist after this commit; they lose their last callers and are deleted next. Bit-exactness: reductions were already chunk-order-deterministic, so the serial folds reproduce them exactly. FP association differs ONLY on the demoted paths (SHARDS=off, explicit shards=1 off a master, pure-MPI unbound ranks): inner_product, accumulate_cos_*, and the endpoint-contrib folds get serial association there; those paths' tests are tolerance-based. Also collapses the now-single-table OperatorIndex bulk_insert (deletes the counting-sort branch), the InvertedIndex row-block-parallel fill + its persistent staging members, and folds the single cosine block set in the layer-build scan directly into the result. Validated: MPI-OFF C++ unit tests 102/102 "No errors detected". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ue tests The rescue invariant is independent of the shard/thread count (each shard runs its partition serially), so the thread-mode axis is meaningless. Drop the ScopedParallelismCap guard and the thread_mode_values() axis: 4 tests per (fixture, comm) instead of 8. Removes this suite's dependency on Threading.h ahead of deleting the pool. snapshot_invariance.cpp: reword the comment off the now-gone parallel_reduce_indices. Validated: MPI-OFF C++ unit tests 98/98 "No errors detected" (was 102). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e only parallelism
The pool primitive (run_static), the parallel_for_*/parallel_reduce_* wrappers,
the gate_serial_override TLS, ScopedParallelismCap, and the layer-build chunk
helpers now have no callers (all rewritten to serial loops). Delete them:
- src/monoprop/Threading.{h,cpp}
- src/monoprop/detail/evolution/layer_build/Parallel.h
and every #include of them, plus the two dead detail:: wrappers in
EvolutionHelpers.h, the gate_serial_override write in ShardGroup, the
init_from_env() calls in MPICompat and the nanobind module init, and
Threading.cpp from the library sources.
monoprop_NUM_THREADS is unaffected: resolve_shard_count_ reads
config::get().num_threads directly, never the (now-gone) pool. std::thread is
still used by ShardGroup and the ShmComm/HybridComm barriers, so Threads::Threads
stays linked. Breaking (!) because installed public headers/symbols (Threading.h,
run_static_impl, init_from_env, ScopedParallelismCap, current_max_parallelism)
are removed.
Validated: MPI-OFF C++ unit tests 98/98 "No errors detected".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
busy_ns/tasks and the capture()/TaskScope machinery existed to attribute parallel-task time to a dispatch region across pool worker threads. With the pool gone and each shard running its region serially, they have no producers. Drop capture(), TaskScope, profiling_current(), RegionAcc::busy_ns/tasks, and ScopedRegion's prev_ save/restore. The monoprop_PHASE dump is now wall_ms + calls per region. Validated: MPI-OFF C++ unit tests 98/98 "No errors detected". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y shim
CpuTopology.h is now the ONE platform-specific file: its whole body (the /sys
parse, L3-domain interleave placement, pthread_setaffinity_np) sits under
`#if defined(__linux__)` with a `using CpuSet = cpu_set_t;`, and a portable
`#else` provides an empty CpuSet plus no-op enumerate/shard_cpusets/pin — so no
Linux type or header leaks out (ShardGroup now holds std::vector<CpuSet>). The
portable shard-count fallback (hardware_concurrency()/2) is unchanged; macOS
additionally reports its physical-core count via sysctl for accurate auto
sharding even though it cannot pin.
Two correctness/portability fixes a topology package (hwloc) would have given —
done in ~15 lines instead of a bundled C dependency:
- enumerate_physical_cores() intersects with sched_getaffinity(): a
cgroup-restricted / partial Slurm allocation now reports and pins only ITS
cores, instead of enumerating all 112 and oversubscribing / pinning outside
the mask (previously masked only because the sbatch scripts set NUM_THREADS).
- monoprop_SHARD_PINNING moves into EnvConfig (config::get().shard_pinning),
parsed once with the shared parse_flag; semantics identical.
hwloc was evaluated and rejected: it replaces only the ~75-line /sys parse and
the 3-line pin call (the ~80-line placement policy survives on top of it),
cannot pin threads on macOS regardless, and would re-add a bundled autotools C
dependency right after TBB/quill were dropped.
Validated: MPI-OFF C++ unit tests 98/98 "No errors detected".
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TBB is neither used nor a build dependency. Remove the leftover mentions the engine's TBB removal never swept: the libtbb-dev / brew tbb installs in CI (test.yml, docpages.yml, copilot-setup-steps.yml), the README and AGENTS.md dependency lines, and a stale benches comment. No build/behaviour change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
parallelism.mdx claimed monoprop used oneTBB with a monoprop_NUM_THREADS worker count. Rewrite it around the real model: one serial shard per physical core by default, composing with MPI into an R×S world. Document all five runtime env vars (NUM_THREADS, SHARDS, SHARD_PINNING, PHASE_TIMERS, RECOMPUTE_CACHE_MAX_MB), the measured 6-9x (Pauli) / ~2x (Hubbard) win over the former pool, the non-Linux/partial-allocation fallbacks, and why there is no hwloc dependency. Fix the "oneTBB worker count" line in benchmarks.mdx and the sharding-controls link blurb in building.mdx. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a `topology` field to KickedIsingConfig with a `_topology_edges` helper: "heavy-hex" (default, byte-identical to today's 127q map) or "chain" (1D nearest-neighbour), so the qubit count can be swept for scaling studies. Record for fixed-model runs the same non-timing quantities the random benches capture (term count, operator/graph storage, resting PSS) keyed by model name, plus `membase` (resting PSS before the build) so the operator's persistent footprint can be isolated as memrest - membase. Fall back to serial when mpi4py is present but cannot dlopen libmpi (the ABI wheel on a serial node), not only when it is absent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
operator_memory_bytes()/graph_memory_bytes() require an unsharded propagator and raise once the operator shards under multi-thread parallelism, which failed the fixed-model point (and tripped the sweep's ratchet). The byte count is thread-count-independent, so the serial run captures it; skip it when sharded rather than fail, keeping time/PSS/term-count recording intact for every point. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-width cap Expose a per-component operator memory breakdown plus popcount / support-window / diagonal-count histograms and the inline width via MonomialPropagator and the Python bindings, to attribute per-term byte cost in the scaling study. Split OperatorIndex's single inline-width constant into kDefaultInlinePositions=11 (byte-identical default for Schrödinger/opaque-cutoff rows) and kMaxInlinePositions=32, and derive packed_inline_width_ from the cutoff (2*cutoff for a Pauli support bound) so cutoff-bounded terms stay inline instead of spilling to the mutex-guarded overflow arena. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…local-symplectic Fold the sweepable pauli-chain bench topology (per-model stats, sharded storage accounting) and the operator memory/popcount diagnostics onto the JW-free native-Pauli fix branch so it can drive its own memory+speed A/B. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…frame
Route PauliPropagator onto the engine's native Basis::Pauli path instead of the
Jordan-Wigner Majorana image. JW packed a weight-1 X_q as a Majorana monomial of
popcount 2q+1 = O(N), so operator memory grew with the qubit count at fixed cutoff.
The local packing (X_q->slot 2q, Y_q->slot 2q+1, Z_q->{2q,2q+1}) gives popcount
<= 2*weight, independent of N (verified: X on the top qubit is popcount 1 at every N).
- dispatch generator + _init_simulator: thread a `basis` argument through to the core
(the binding already accepted it); PauliPropagator passes basis="pauli".
- conversion_utils: _pauli_to_local_slots / _local_slots_to_pauli (encode/decode,
mirroring slots_of_string / letter_from_bitset in the C++ tests).
- PauliOperator.get_local_operator(): pack terms into local slots with real coeffs.
- circuit._gate_layers: native branch emits (local slots, real g) directly -- no
jw_coeff, no antihermitian normalization (the Pauli rotation kernel handles phase).
- PauliPropagator: ingest via get_local_operator, cutoff_type="support", no basis_change;
evolved_operator() now decodes to a PauliOperator (was a raw index dict).
No C++ engine changes: the native path is already implemented and validated
(tests/cpp/pauli_build_layer_tests.cpp T6/T7). Full Python suite green (365 passed);
the qiskit-reference evolution test matches at 1e-6.
BREAKING CHANGE: PauliPropagator.evolved_operator() returns a PauliOperator (Pauli-keyed)
instead of a dict keyed by Jordan-Wigner Majorana index tuples.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route PauliPropagator onto the engine's native local-symplectic (Basis::Pauli) path instead of Jordan-Wigner Majorana storage. Fixes the O(N) memory growth (JW popcount artifact): operator store is now N-flat (~54.7 B/term, 0 overflow) with popcount O(cutoff). Arch-native compute A/B (job 49844575) vs a same-build JW shim: energies bit-identical, term counts identical, build_graph 1.98x-7.30x faster single-thread. C++ arbiter tests (pauli_build_layer_* T6/T7, pauli_algebra_*, shard_equivalence_*) green. Python-only change; no C++ algebra touched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-layer fold cache retained a (sum of mask_words * 8B) buffer per functional and was gated by monoprop_RECOMPUTE_CACHE_MAX_MB. A functional-path A/B (2026-07-20) showed it bought <=5% per eval -- and LOST for large operators, its buffer going cold faster than the L1-blocked recompute rebuilds it -- while costing 0.3-1.0 GB per 1.7M-term operator and creating a silent mid-sweep regime change in scaling benchmarks. Time-only break-even was 22-70 evals: trading GB of memory for a shrinking (then negative) speedup is backwards for a scaling-focused library. build_cos_callbacks now always takes the recompute (LazyFold) path. make_fold_cache / scale_cos_cached / fold_to_cos_mask survive only as the one-shot pare materializer and the C++ recompute-equivalence-test oracle (no retained buffer, no scale cost). Validated: C++ suite 106/106 cases, 1,413,155 assertions pass (incl. *cache_equals_recompute*); Python results bit-identical to pre-change; the cache-vs- recompute peak-RSS gap collapses from +600-950 MB to +/-1 MB; pytest functional/exact slice 33 passed, 1 skipped. BREAKING CHANGE: the monoprop_RECOMPUTE_CACHE_MAX_MB environment variable is removed; cosine folds are always recomputed on the fly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Matteo AC Rossi <9745862+matteoacrossi@users.noreply.github.com>
Attacks the anticommutation fold, measured at ~60-70% of per-gate cost, using
the bimodal gate-participation structure (most gates anticommute with almost
nothing yet the fold streamed all K/64 operator words regardless).
- Zero-postings early-out: a generator whose inverted-index fold columns hold
no postings provably anticommutes with nothing (even |G|; Pauli is always
even in this path), so pass 1 + pass 2 are skipped entirely. A dense column
always holds >=1 posting, so the emptiness test is O(|G|) sparse-size reads.
Fires on ~70% (hubbard) / ~64% (pauli) of per-shard gate scans, removing that
share of fold busy-CPU at high shard counts. The g_odd guard is load-bearing:
an odd Majorana generator anticommutes with the odd-weight terms it is
disjoint from, so zero overlap does not imply commutation there.
- Lazy sparse-pivot expansion: a sparse pivot column is scatter-expanded only
for fold blocks that produced a nonzero overlap word; empty blocks (the common
case on low-participation gates) no longer pay the expansion stream. Dense
pivots stay folded inline (deferring them regressed hubbard +7.6%).
- combine_columns_block seeds its scratch from the first dense column by memcpy
(XOR-commutative) instead of memset + XOR-all, saving one pass over the block.
New monoprop_FOLD_STATS env knob (default OFF, zero-overhead when disabled,
RegionProfiler atexit dump): per-gate {all-sparse?, sum postings, K/64, n_anti,
structural rejects} + a log2 postings/(K/64) histogram. The sizing data it
produced ruled out two further proposals (candidate-merge discovery and a
bit-sliced structural prefilter) as sub-single-digit on the real workloads.
All changes bit-identical (verified: full C++ suite + real-MPI R=2 green;
AB probe byte-for-byte vs baseline, hex-float 0x1.2a7a10767e528p+0 preserved).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove ~150 lines of copy-pasted Pauli oracle code duplicated between pauli_algebra_tests.cpp and pauli_build_layer_tests.cpp into a shared PauliTestOracle.h (native/JW encoding, dense Pauli-matrix brute force, string helpers). Factor the ShmComm/HybridComm thread-spawn harness into ThreadHarness.h (run_comm_threads), and hoist the duplicated near() float comparison and LihFixture into TestUtilities.h; slots_of_string, the near() constant, and RandomExactFixture now come from the shared headers across the equivalence suites. Merge the two single-case files into their neighbours: utilities.cpp (even/odd-bit helpers) -> mpfunctions.cpp, snapshot_invariance.cpp -> the recompute-equivalence file that exercises the same machinery. Rewrite the stale tests/cpp/README.md (wrong binary name, deleted-file list). Behaviour-preserving: C++ 94/94 serial + MPI green, no test cases removed beyond the two folded ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Remove validators with zero callers anywhere in the codebase (verified by repo-wide grep, incl. bindings and Python): determine_evolution_mode, validate_evolution_parameters, validate_graph_state_for_mode, validate_params, validate_propagation_params, validate_propagation_contraction, validate_param_map_gen_coeffs_majoranas_match, the EvolutionMode enum, and the private has_complete_evolution_parameters helper. The live surface (validate_coefficient_lengths, validate_gate_indices, validate_parameters_length, validate_functional_call, validate_expected_graph_layers, validate_equal_sizes) is untouched. No behaviour change: the deleted symbols had no callers, so nothing in the hot path is affected. Library relinks clean; C++ + Python suites green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add direct unit coverage for structures that were only exercised indirectly through the engine end-to-end: - bitset_tests.cpp: Bitset ctor top-mask, cross-word count_and/parity_and, multi-word shift and find_first/find_next, position-sensitive hash, plus a randomized differential fuzz against std::bitset. - majorana_cutoff_tests.cpp: length/support cutoff (incl. logical_num_modes masking on single- and multi-word paths), CutoffEvaluator dispatch + popcount fast path + max_positions_bound, interleave_phase vs its masked- parity form, encode/decode_coeff round-trip and non-Hermitian throw. - validation_tests.cpp: the live parameter validators (accept + reject paths). - mpi_utils_tests.cpp: find_rank determinism/range/hash-mod and the Majorana word (de)serialization round-trip. - evolution_detail_tests.cpp: MatchedEpochSet O(1)-clear / tail-grow / u32 wrap and the CutoffContext atol/upper-atol gating predicates. - row_accessor_tests.cpp: dense-vector vs OperatorIndex backend differential for materialize_row/assign_row/row_popcount/for_each_row_position. - ctor_validation_tests.cpp: MonomialPropagator ctor guards (logical_num_modes, Pauli+cutoff/basis_change, atol ordering, operator index range), propagate()-on-nonempty-graph, and MPGraph::get_layer bounds. 40 new cases; full suite 134 serial + MPI green. Validation.cpp coverage 28% -> 95%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The monoprop_WIDE_TERM_INDEX=ON configuration (64-bit TermIndex) was never built anywhere, so the wide `#if defined(monoprop_WIDE_TERM_INDEX)` branches in operator_index_tests / large_cosine_storage_tests were dead in CI. Add: - CMakePresets: release-gcc-wide configure/build/test presets. - justfile: `test-cpp-wide` (build + serial ctest under wide) and a `coverage-cpp` convenience recipe encoding the gcovr invocation. - CI: a `cpp-wide` job (gcc-14, Release + WIDE_TERM_INDEX=ON, serial ctest) so the wide branches are compiled and run on every push. Verified locally: wide build is green (serial 128/128) and the wide-only term-index-width case executes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… backbone Make the code's names and types state the essence: one monomial container, two algebra models, and a propagation backbone that is generic over the algebra. Clean break — old names are removed outright, no back-compat aliases. Bit-exact and perf-neutral: each policy method wraps the existing kernel, and the runtime Basis is bound to a compile-time model exactly once via with_algebra, replacing the compile-time IsPauli flag and every scattered `if (basis == Basis::Pauli)`. - core/Monomial.h: split the monomial vocabulary out of TypeAliases.h. MajoranaSet<N> -> Monomial<N> (Bitset<2N>: a Majorana product, or its Jordan-Wigner-image Pauli string); MajoranaVector -> MonomialList; MajoranaOperator (C++ map alias) -> MonomialMap; MPHash/MPEqual/majorana_hash -> MonomialHash/MonomialEqual/monomial_hash. The Python class MajoranaOperator is unchanged. - algebra/: MajoranaAlgebra.h and PauliAlgebra.h move here as true siblings over a new AlgebraCommon.h (shared structural primitives); PauliAlgebra no longer includes MajoranaAlgebra. - algebra/Algebra.h: the Algebra concept, the MajoranaAlgebra/PauliAlgebra policy models, the with_algebra(Basis, fn) dispatcher, and point-dispatch helpers for the cold sites that carry a runtime Basis. - backbone: fused_find_and_collect and the cosine fold are templated on the algebra policy; every basis branch outside the policy is gone, including the last two in MPOperator (HF scoring via algebra_score_hf, coeff codec via algebra_encode_coeff). - tests: reference-only helpers moved to tests/cpp/AlgebraReference.h. - docs: AGENTS.md core-abstractions section + an essence doc on core/Monomial.h. BREAKING CHANGE: the C++ core type names change (MajoranaSet -> Monomial and friends) and the two algebra headers move under algebra/. Out-of-tree subclasses such as MonomialPropagatorExtra must update to the renamed types and paths. Verified on host bebe (gcc-14): C++ 127/127, MPI 135/135 (+ 4-rank clean), wide-index 128/128, Python 447 passed / 8 skipped; exact-value suites bit-identical. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Matteo AC Rossi <9745862+matteoacrossi@users.noreply.github.com>
Member
Author
|
Ok, the merging got messed up. Fixing it |
robertodr
reviewed
Jul 29, 2026
Co-authored-by: Roberto Di Remigio Eikås <robertodr@users.noreply.github.com> Signed-off-by: Ludmila Botelho <39570941+ludmilaasb@users.noreply.github.com>
The per-backend `memory` series mixed four incommensurable quantities: monoprop's operator accounting, an RSS ratchet for ppvm and Qiskit, and a device pool for the GPU. Take the peak from the kernel's VmHWM in `_run_steps` instead, so every engine is measured by one instrument over the timed region, and keep each library's own figure in a separate `operator_memory` series. `run_scaling.jl` follows suit. Resetting VmHWM also resets `ru_maxrss` and `Sys.maxrss()` (both read `mm->hiwater_rss`), so the whole-run ceilings now take the maximum over the windows. Drop the per-backend runner scripts: `run_one.py` already isolates each backend in its own process, and `model.py` defines the circuit once rather than once per runner. Assisted-by: GitHubCopilot:claude-opus-5
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



By opening this PR I confirm that I have read CONTRIBUTING.md and I agree to the terms of the Contributor License Agreement.
Summary
This PR updates the 3rd party benchmark scripts. The SPECS are the same, the number of quibits is increased for Pauli Propagation and the scripts for comparing with MajoranaPropagation.jl were improved and simplified.
Changes
Checklist
docs/,CONTRIBUTING.md) if neededCHANGELOG/ release notes updated if applicableAI/LLM disclosure