Skip to content

perf(evolution): ⚡ halve the follower-marking epoch stamp - #259

Merged
robertodr merged 6 commits into
mainfrom
perf/epoch-stamp-noshrink-v2
Aug 24, 2026
Merged

perf(evolution): ⚡ halve the follower-marking epoch stamp#259
robertodr merged 6 commits into
mainfrom
perf/epoch-stamp-noshrink-v2

Conversation

@diagonal-hamiltonian

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

Copy link
Copy Markdown
Collaborator

🤖 AI text below 🤖

Halve the follower-marking epoch stamp (u32 → u16)

Narrows detail::MatchedEpochSet's per-term stamp to uint16_t — 2 B/term of operator row storage — with wrap handled explicitly and no shrink of the array anywhere. perf/epoch-stamp-noshrink-v2 1f350ff: one commit of substance (8dbe2a5) plus merges of main and two review commits, 6 files +103/−11 against origin/main.

  1. The stamp width. One stamp per term, for a counter that never leaves the struct — never serialised, never exchanged between ranks, never compared against anything but cur_. The wrap branch is now live (once per 65535 gate applications instead of once per 2^32) and the std::fill is what keeps it correct; matched_epoch_stamp_wrap_reached_by_gate_count cycles a whole period rather than assigning to cur_, which is the only way to prove the fill runs rather than that the branch is reachable.
  2. matched_scratch_bytes joins the memory breakdown, inside total_bytes(). The stamp array is propagator-owned, so estimate_memory_usage() cannot see it and only MonomialPropagator::operator_memory_usage() can fill it in — the partitioned path sums per-partition breakdowns, so each picks up its own array.

Memory: main 48cadcb vs this branch, both ENABLE_PROFILE=OFF

Kernel /usr/bin/time -v peak RSS, summed over the ranks on the node, produced outside the code under test. 16 cells × 10 interleaved reps in one allocation, order flipped per (rep, cell); paired per-rep ratios, median of ratios; agree = reps pointing the same way.

cell main GiB port GiB port/main agree
hubbard · A 1x128 · N=1 · fresh 9.54 9.49 0.9948 10/10
hubbard · A 1x128 · N=2 · fresh 10.33 10.02 0.9702 10/10
hubbard · B 8x16 · N=1 · fresh 11.06 10.96 0.9911 10/10
hubbard · B 8x16 · N=2 · fresh 14.18 13.92 0.9813 10/10
hubbard rung · A 1x128 · N=1 · fresh 26.61 26.05 0.9788 10/10
hubbard rung · A 1x128 · N=2 · fresh 31.23 30.46 0.9750 10/10
hubbard rung · B 8x16 · N=1 · fresh 28.41 27.84 0.9797 10/10
hubbard rung · B 8x16 · N=2 · fresh 34.83 34.04 0.9772 10/10
pauli · A 1x128 · N=1 · fresh 10.39 10.31 0.9922 10/10
pauli · A 1x128 · N=1 · graph 18.13 17.93 0.9890 10/10
pauli · A 1x128 · N=2 · fresh 10.90 10.88 0.9987 10/10
pauli · A 1x128 · N=2 · graph 36.70 36.53 0.9952 10/10
pauli · B 8x16 · N=1 · fresh 12.17 12.03 0.9891 9/10
pauli · B 8x16 · N=1 · graph 19.68 19.48 0.9902 10/10
pauli · B 8x16 · N=2 · fresh 14.94 14.92 0.9969 8/10
pauli · B 8x16 · N=2 · graph 40.78 40.57 0.9951 10/10

Peak RSS falls in 16 of 16 cells, 14 at unanimous 10/10, and 15 of 16 clear Holm across the memory family — largest adjusted p = 0.043 (pauli-B-N1 fresh, 9/10); the only failure is pauli-B-N2 fresh (8/10), the smallest effect present. That unanimity is node-sum only: on worst-rank RSS two cells reverse direction (pauli-A-N2 fresh 1.0000, pauli-B-N2 fresh 1.0027). Hubbard beating pauli is what a per-term saving must do — but 2 B/term is a floor on the array's saving, not on the peak-RSS delta, because peak RSS is a maximum over time and the stamp array need not be at its own maximum at that instant: hubbard propagate at N=1 implies only 0.54 and 1.05 B/term of node-sum delta, below the array's own floor, which is a statement about when the peak lands and not about the width.

Timing: a null

1 of 24 tests resolved; the other 23 span 0.964–1.014x. The one survivor is gradient[pauli], layout A 1x128, N=1: 11355.2 → 11172.4 ms, 0.9836x, 10/10, Holm-adjusted p = 0.0469 — exactly at the boundary, and on an operation that reaches only the allreduce and never touches a table this diff changes, so it reads as a null either way. Absolute medians at N=1 below; the N=2 half and the full 24-row breakdown will follow in a comment. Grid: layout A = 1 rank/node × 128 partitions, B = 8 × 16, N = 1 and 2 nodes; hubbard --hubbard-cutoff=10 --hubbard-lower-atol=1.25e-05 (96,981,051 terms), pauli --pauli-cutoff=14 --pauli-lower-atol=5e-05 (91,273,861 terms), plus a build_graph[hubbard] rung at --hubbard-trotter-steps=2 with --hubbard-lower-atol=1e-04, not the main grid's 1.25e-05.

cell operation main ms port ms port/main agree
hubbard · A 1x128 propagate[hubbard] 17624.0 17600.3 0.9950 7/10
hubbard · B 8x16 propagate[hubbard] 19194.4 19150.7 0.9974 6/10
hubbard rung · A 1x128 build_graph[hubbard] 3976.1 3917.2 0.9928 6/10
hubbard rung · B 8x16 build_graph[hubbard] 3093.7 3089.3 0.9988 6/10
pauli · A 1x128 build_graph[pauli] 13947.9 14100.9 1.0065 9/10
pauli · A 1x128 propagate[pauli] 11852.8 11755.8 0.9937 5/10
pauli · A 1x128 energy[pauli] 2473.0 2418.7 0.9867 6/10
pauli · A 1x128 gradient[pauli] 11355.2 11172.4 0.9836 10/10
pauli · B 8x16 build_graph[pauli] 13300.9 13270.3 0.9984 8/10
pauli · B 8x16 propagate[pauli] 10585.8 10533.2 0.9962 8/10
pauli · B 8x16 energy[pauli] 2860.2 2860.8 1.0046 7/10
pauli · B 8x16 gradient[pauli] 11583.9 11549.9 0.9988 7/10

Caveats and scope

  • total_bytes() is not comparable across this commit: a build without matched_scratch_bytes reports a total lower by roughly the stamp array while holding the same or more resident memory, so subtract the field or re-measure the baseline.
  • Not reproducible from this diff: Deucalion, 2× AMD EPYC 7742 / 128 cores / SMT off / NPS4 / 242 GiB per node; a private benchmark harness that does not ship; two prebuilt venvs (main 1250a27e…, port 6f14ecb1…).
  • The measured binaries carry one mechanism this branch's own diff does not — and main now carries it too. They were built from a cut that also released init_op_map in get_operator(). That mechanism shipped separately as perf(operator): ⚡ release the init_op_map buckets once its terms bind #268 and is in this branch's base as of the merge below, so get_operator() here is main's in both arms. Either way it is worth 1,189 B (8357 → 7168 B in this campaign's own ledger) against cells of 9.5–40 GiB — order 1e-7, far below the smallest resolved effect. Re-gated, not re-measured.

Gates

Re-gated on the merged tree (09c5e67, extension md5 4a32d5c3…; the two commits since are a doc line and a code comment, neither reaching the binary, ENABLE_PROFILE=OFF): ctest -L unit 224/224 and -L serial 223/223, the four Python MPI layouts 1×1, 1×16, 2×8 and 8×16 at 592 passed each, and clang-format --output-replacements-xml 0 replacements on every changed C++/binding file.

main has moved under this branch since the 48cadcb measurement, and two of those commits are C++ rather than docs and build config: #267 bounds the epoch-stamp mark by combined_size, and #268 is the init_op_map release named in the caveat above. Neither is in the measured main arm. #268 is the 1,189 B already bounded there; #267 adds one compare per self-resolve hit and allocates nothing. The figures stand.

Merging main took three conflicts, all additive. Main modernised MPOperatorMemoryBreakdown's member initialisers to {0uz}, so matched_scratch_bytes joins in that form; and each side had appended a test case at the same point in evolution_detail_tests.cpp and in mp_operator_tests.cpp, so both survive in each file. The wrap test and #267's guard test both pass on the merged build.

@github-actions github-actions Bot added documentation Improvements or additions to documentation python cpp labels Aug 20, 2026
@github-actions

Copy link
Copy Markdown

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

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.70%. Comparing base (6c3e1bd) to head (1f350ff).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #259   +/-   ##
=======================================
  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.

@diagonal-hamiltonian
diagonal-hamiltonian changed the base branch from perf/profile-graph-coverage to main August 20, 2026 18:08
@diagonal-hamiltonian
diagonal-hamiltonian force-pushed the perf/epoch-stamp-noshrink-v2 branch from f1f3067 to b01e6fa Compare August 20, 2026 18:08
@diagonal-hamiltonian
diagonal-hamiltonian marked this pull request as draft August 21, 2026 07:20
@diagonal-hamiltonian
diagonal-hamiltonian force-pushed the perf/epoch-stamp-noshrink-v2 branch from b01e6fa to 2be9966 Compare August 21, 2026 08:12
`detail::MatchedEpochSet` carried a `uint32_t` stamp per term for a counter that never leaves the
struct -- never serialised, never exchanged, only ever compared to `cur_`. This narrows it to
`uint16_t`, keeps the wrap discipline explicit, and does NOT shrink the array anywhere.

Two mechanisms:
- the stamp width, with the wrap branch now live (once per 65535 gates) and the `std::fill` the
  thing that keeps it correct -- `epoch_` retains stale stamps for row indices reused after a
  truncation, so deleting the fill aliases marks;
- `matched_scratch_bytes` joins `MPOperatorMemoryBreakdown` inside `total_bytes()`, with
  the stamp array being propagator-owned, so only `MonomialPropagator::operator_memory_usage()` can
  fill it in. The partitioned path sums per-partition breakdowns, so each picks up its own array.

NO `shrink_to_fit()` ANYWHERE, not even an uncalled method. An earlier revision called
`matched_scratch_.shrink_to_fit()` from `initialize_operator_caches_()` on the premise that the array
"grows only when the term count does". That is false on Hubbard: the function runs after EVERY
`build_graph()` and `propagate()`, so 29 Trotter steps reach it 29 times against a term count that
grows at each step. Its A/B read +0.2821 GiB peak RSS (+3.12 B/term), 6/6, non-overlapping, at the
1x16 hubbard cell; removing it read -0.24 GiB, 6/6. And the shrink cannot free resident bytes at
all -- `resize(n, 0)` never writes past `n`, so the capacity it releases was never faulted in. A
retained-but-uncalled method plus a comment asking the next reader not to call it is a trap; the
rejected experiment lives in the PR body instead.

Re-cut twice and rebased onto main: the original was authored on a base carrying the profiling
instrument, then briefly on the graph-memory PR whose key-set test is not on main. The mechanism
never changed across any of it.

It contains no profiling code: monoprop_PROF, profile:: and Profile.h appear zero times in the diff.

MEASURED, campaign pr7v2off, both arms ENABLE_PROFILE=OFF, 12 cells x 10 interleaved reps in one
allocation with order flipped per cell: kernel peak RSS (`/usr/bin/time -v`, node-sum) falls in 16 of
16 memory cells, 14 at unanimous 10/10, 15 of 16 clearing Holm across the memory family at largest
adjusted p = 0.043. Hubbard `propagate` 0.97-0.99x, the hubbard `build_graph` rung 0.975-0.980x,
pauli 0.989-0.999x -- hubbard beating pauli is what a per-term saving must do. Timing is a null: 1 of
24 tests resolved (`gradient[pauli]`, A 1x128, N=1, 0.9836x, 10/10, adjusted p = 0.0469, on an
operation this diff cannot reach), the other 23 inside 0.964-1.014x.

2 B/term is a floor on the ARRAY's saving, not on the peak-RSS delta: peak RSS is a maximum over
time and the stamp array need not be at its own maximum then. Hubbard `propagate` at N=1 implies
0.54 and 1.05 B/term of node-sum delta, below the array's own floor, which says something about WHEN
the peak lands and nothing about the width.

SPLIT: an earlier cut of this branch also released `init_op_map` in `get_operator()`. That is now a
separate change so this PR carries one story, which means the measured binaries contain a release this
branch does not. It was worth 1,189 B (8357 -> 7168 B in this campaign's own ledger) against cells of
9.5-40 GiB -- order 1e-7, far below the smallest resolved effect -- so the figures stand.
Re-gated, not re-measured.

An earlier instrumented pair (both arms ENABLE_PROFILE=ON) agrees on the `build_graph` rung and on
pauli but read hubbard `propagate` at 0.96-0.97x -- it overstated exactly the number being
advertised. The uninstrumented pair above is what certifies the shipping code, and is what is quoted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@diagonal-hamiltonian
diagonal-hamiltonian force-pushed the perf/epoch-stamp-noshrink-v2 branch from 2be9966 to 8dbe2a5 Compare August 21, 2026 08:20
@diagonal-hamiltonian

diagonal-hamiltonian commented Aug 21, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 AI text below 🤖

Measured at b01e6fa, which also carried an init_op_map release in get_operator(). That mechanism has since been split out of this PR, so the branch is now 8dbe2a5 and get_operator() matches main; the release was worth 1,189 B against cells of 9.5–40 GiB, so these figures stand. See the body's caveats.

Full timing breakdown — all 24 tests, OFF/OFF

Campaign pr7v2off: main src/mp-main-48cadcb (1250a27e…, 48cadcb) vs port src/pr7v2 (6f14ecb1…, b01e6fa), 10 interleaved reps per cell. port/main is the median of the per-rep paired ratios, not the ratio of the two side medians, so it can disagree in sign with the main ms/port ms columns — propagate[hubbard] at A / N=2 is the one row where it does. raw p is one two-sided sign test in isolation, uncorrected.

cell N operation main ms port ms port/main agree raw p
hubbard · A 1x128 1 propagate[hubbard] 17624.0 17600.3 0.9950 7/10 0.344
hubbard · A 1x128 2 propagate[hubbard] 46373.7 46670.9 0.9996 5/10 1.000
hubbard · B 8x16 1 propagate[hubbard] 19194.4 19150.7 0.9974 6/10 0.754
hubbard · B 8x16 2 propagate[hubbard] 13761.7 13843.4 1.0032 7/10 0.344
hubbard rung · A 1x128 1 build_graph[hubbard] 3976.1 3917.2 0.9928 6/10 0.754
hubbard rung · A 1x128 2 build_graph[hubbard] 8298.2 8312.4 1.0027 6/10 0.754
hubbard rung · B 8x16 1 build_graph[hubbard] 3093.7 3089.3 0.9988 6/10 0.754
hubbard rung · B 8x16 2 build_graph[hubbard] 2935.8 2807.5 0.9643 8/10 0.109
pauli · A 1x128 1 build_graph[pauli] 13947.9 14100.9 1.0065 9/10 0.021
pauli · A 1x128 1 propagate[pauli] 11852.8 11755.8 0.9937 5/10 1.000
pauli · A 1x128 1 energy[pauli] 2473.0 2418.7 0.9867 6/10 0.754
pauli · A 1x128 1 gradient[pauli] 11355.2 11172.4 0.9836 10/10 0.002
pauli · A 1x128 2 build_graph[pauli] 28878.9 28858.4 0.9988 6/10 0.754
pauli · A 1x128 2 propagate[pauli] 23310.2 23467.5 1.0142 7/10 0.344
pauli · A 1x128 2 energy[pauli] 4659.1 4756.0 1.0120 8/10 0.109
pauli · A 1x128 2 gradient[pauli] 15958.2 15720.6 0.9858 6/10 0.754
pauli · B 8x16 1 build_graph[pauli] 13300.9 13270.3 0.9984 8/10 0.109
pauli · B 8x16 1 propagate[pauli] 10585.8 10533.2 0.9962 8/10 0.109
pauli · B 8x16 1 energy[pauli] 2860.2 2860.8 1.0046 7/10 0.344
pauli · B 8x16 1 gradient[pauli] 11583.9 11549.9 0.9988 7/10 0.344
pauli · B 8x16 2 build_graph[pauli] 9248.5 9250.4 1.0007 5/10 1.000
pauli · B 8x16 2 propagate[pauli] 7197.5 7162.5 0.9944 8/10 0.109
pauli · B 8x16 2 energy[pauli] 756.5 760.1 1.0010 5/10 1.000
pauli · B 8x16 2 gradient[pauli] 4612.5 4607.9 0.9996 5/10 1.000

Holm across all 24 leaves one survivor, gradient[pauli] at A 1x128 / N=1, adjusted p = 0.0469. The next smallest raw p — build_graph[pauli] at A 1x128 / N=1, 0.021 at 9/10 — adjusts to 0.494.

Worst-rank peak RSS, for the OOM question

Node-sum is the only cross-layout figure and is what the body tables; worst-rank is what an OOM is decided by. At layout A the two are equal (1 rank/node). The direction holds on worst-rank everywhere except two pauli cells, which reverse: pauli-A-N2 fresh 5.5067 → 5.5141 GiB (1.0000x, 5/10) and pauli-B-N2 fresh 0.9682 → 0.9703 GiB (1.0027x, 7/10). Both are unresolved and neither contradicts the node-sum result, but the body's "16 of 16" is a node-sum claim and does not transfer.

@diagonal-hamiltonian
diagonal-hamiltonian marked this pull request as ready for review August 21, 2026 08:58
robertodr pushed a commit that referenced this pull request Aug 24, 2026
🤖 _AI text below_ 🤖

## Summary

Bounds the epoch-stamp mark in `resolve_self_queries` by `combined_size`
so it can no longer write
past the end of `epoch_`. `fix/epoch-stamp-bounds-guard` `92e352b`, 1
commit on `e0c528e`, 2 files
+69/−2.

1. **The guard.** `resolve_self_queries` marks under `found[j] <
op_size` — the store size read at
`resolve_range_` entry — but `epoch_` is sized to `combined_size`, the
store size at engine
construction. Once the store has grown, `op_size > combined_size` and
the mark writes past the
end of `epoch_`, into vector slack. Silently. This matches what the
cross-rank twin
   `resolve_incoming` (`Resolve.h:153`) already does.
2. **Latent, not live.** The only marking pass is `is_leader_pass`, and
`run_exchange(true, …)`
resolves self queries before both insert paths, so today the two sizes
coincide. Reordering the
   passes breaks it. Pre-dates #259 and is kept out of it deliberately.
3. **Dropping such a mark is result-neutral, not merely in-bounds.**
Both `is_marked` readers
(`:406`, `:482`) read scan-source indices out of `fused`, built at
`:562-565` *before* the engine
is constructed at `:592-598` — so every index reaching `is_marked` is `<
combined_size`, and a
   mark at or above it is unreadable.
4. **The test constructs the out-of-bounds ordering rather than
asserting today's accident.**
`begin_gate(6)` then `combined_size=4`, so query 2 resolves to `found=5`
against `op_size=6` and
the unfixed code writes `epoch_[5]`. Mutation-verified — reverting the
guard fails the case
   deterministically.

## Changes

- Bound the epoch-stamp mark in `resolve_self_queries` by
`combined_size`, matching
  `resolve_incoming`'s existing guard.
- No behavior change today: the only marking pass, `is_leader_pass`,
never presents
`op_size > combined_size`; the guard protects against reordering the
passes, not against a live
  bug.
- Add a regression test that constructs the out-of-bounds ordering
directly (`begin_gate(6)`,
  `combined_size=4`) instead of relying on today's coincidental sizing.

## Checklist

- [x] Tests added or updated to cover the changes
- [ ] Documentation updated (docstrings, `docs/`, `CONTRIBUTING.md`) if
needed
- [ ] `CHANGELOG` / release notes updated if applicable

## AI/LLM disclosure

- [ ] I did not use LLM tooling, or used it only privately for ideation
- [x] I used the following tool to help write this PR description:
Claude Code
- [x] I used the following tool to generate or modify code: Claude Code

> [!IMPORTANT]
> By opening this PR I confirm that I have read
[CONTRIBUTING.md](https://github.com/Algorithmiq/monoprop/blob/fa820c3e5a90f773417c367fedbf2bd50683b496/CONTRIBUTING.md)
and I agree to the terms of the [Contributor License
Agreement](https://github.com/Algorithmiq/monoprop/blob/fa820c3e5a90f773417c367fedbf2bd50683b496/CLA.md).

> [!WARNING]
> If you're contributing on behalf of your employer, contact
[cla@algorithmiq.fi](mailto:cla@algorithmiq.fi) to arrange a Corporate
CLA.

Co-authored-by: Ludmila Botelho <39570941+ludmilaasb@users.noreply.github.com>
Three conflicts, all additive:

- `MPOperatorMemoryBreakdown`: main modernised the member initialisers to `{0uz}` (#268); keep
  that form and add `matched_scratch_bytes{0uz}` in it.
- `evolution_detail_tests.cpp`: both sides appended a case at the same point --
  `matched_epoch_stamp_wrap_reached_by_gate_count` (this branch) and
  `self_resolve_mark_bounded_by_combined_size` (#267). Keep both.
- `mp_operator_tests.cpp`: same shape -- the two `matched_scratch_bytes` cases here and
  `mp_operator_breakdown_keeps_init_operator_entries_out_of_total` from main. Keep both.

#268 landed the `init_op_map` release the measured binaries also carried, so `get_operator()` is
main's again with that mechanism now in the base rather than split out. #267's `combined_size`
bound is orthogonal to the stamp width: it constrains the index marked, not the stamp stored.
Comment on lines +135 to +139
// matched_scratch_ is propagator-owned, so estimate_memory_usage() cannot see it; on a facade the
// partitioned path above already picks it up per partition.
auto breakdown = detail::estimate_memory_usage(mp_op_);
breakdown.matched_scratch_bytes = matched_scratch_.memory_bytes();
return breakdown;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really don't understand what is happening here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI text below 🤖

Fair — it was unexplained, and it looks like the line above it is being overwritten. Comment rewritten in 1f350ff.

What it is: matched_scratch_ is a member of this class, not of the operator. detail::estimate_memory_usage(mp_op_) takes the operator, so it has no way to see the stamp array and returns matched_scratch_bytes == 0; this level is the only one that holds both the number and the struct to put it in, so it fills the field in after the fact.

Why the field lives in the operator's breakdown struct at all, rather than being returned separately: the array is one stamp per operator row, so it belongs with the other per-term figures, and total_bytes() has to include it or the total under-reports resident memory. The partitioned branch two lines up needs no such patch — partitioned_operator_memory_usage_() sums each partition's own breakdown, and each partition is a propagator with its own array, so each summand already carries its own bytes.

The two tests pinning it are mp_operator_breakdown_counts_matched_scratch_in_total_and_sum (the field is summed by total_bytes() and accumulated by operator+=, and an operator on its own reports 0) and mp_operator_breakdown_matched_scratch_nonzero_after_a_gate (0 before any gate, nonzero after one, and the delta in total_bytes() equals the field).

Comment thread docs/content/docs/benchmarks.mdx Outdated
Signed-off-by: Roberto Di Remigio Eikås <robertodr@users.noreply.github.com>
robertodr
robertodr previously approved these changes Aug 24, 2026

@robertodr robertodr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't understand one bit, but otherwise seems harmless. It's good to be thrifty with type widths

Review comment on #259: the assignment in `operator_memory_usage()` was unexplained. It reads as an
after-the-fact patch of a field the line above it just computed, which is what it is, and the reason
is ownership -- `matched_scratch_` is a member of the propagator while `estimate_memory_usage()`
takes the operator, so the field comes back 0 and only this level knows the number.
@sonarqubecloud

Copy link
Copy Markdown

@diagonal-hamiltonian

Copy link
Copy Markdown
Collaborator Author

Are you satisfied
@robertodr

@robertodr
robertodr merged commit cb9a033 into main Aug 24, 2026
24 checks passed
@robertodr
robertodr deleted the perf/epoch-stamp-noshrink-v2 branch August 24, 2026 13:46
robertodr pushed a commit that referenced this pull request Aug 24, 2026
Main's side of every C++ conflict was the pre-NTTP-removal code, so its changes
were ported onto this branch's runtime-width structure rather than either side
being taken whole. Main's three functional imports are preserved:

- the `found[j] < combined_size` guard on the self-resolve mark (#267), which
  auto-merged into this branch's `with_store`-bound engine;
- the `init_op_map` bucket release (#268). Its `rehash(0)` supersedes this
  branch's `init_op_map = MonomialMap{}`, so this branch's partial-drain test
  relaxes from `==` to `<=` on the bucket count -- main's version shrinks on any
  erase, where this branch's released only on a full drain;
- `matched_scratch_bytes` (#259), beside this branch's
  `inverted_index_columns_bytes`.

Main's nanobind 3 split mode (`BACKEND_MODULE nanobind_backend`, hence no
`STABLE_ABI`/`NB_STATIC`) applies to this branch's real `bindings.cpp`. With
`bindings.cpp.in` deleted there is nowhere to substitute `@nanobind_VERSION@`,
so `__nanobind_version__` -- which `src/monoprop/__init__.py` imports -- now
arrives as a `monoprop_NANOBIND_VERSION` compile definition from CMake, and
pyproject's build cache-keys point at `bindings/*.cpp` instead of the two gone
template files.

Main's repo-wide `class` -> `typename` style (#240) is applied to the 15
branch-only template sites the merge left behind, leaving the same single
`template <class>` main keeps in MPICompat.h.

Verified byte-identical to the pre-merge tip d37c408 (`capture-baseline`, 38
records over 10 cases), so the merge moves no term and no energy.
`.baseline-capture/golden` predates 9b43667 and d37c408 and differs from both
in `majorana_lattice_layer_30` by term order alone; it needs a refresh.

ctest 590/590 (295 under the sparse-rows label), pytest 620 passed, sparse-row
pytest 583 passed, sparse-vs-dense baselines agree to rtol 1e-10.

Assisted-by: ClaudeCode:claude-opus-5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cpp documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants