Skip to content

feat(cpp): ✨ select the support-form row store per propagator - #305

Open
robertodr wants to merge 2 commits into
split/02-row-hash-tablefrom
split/04-sparse-row-store
Open

feat(cpp): ✨ select the support-form row store per propagator#305
robertodr wants to merge 2 commits into
split/02-row-hash-tablefrom
split/04-sparse-row-store

Conversation

@robertodr

@robertodr robertodr commented Aug 29, 2026

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

Stacked on #303 — review that first.

Summary

Fourth of five PRs carved out of #226. Adds the support-form row store, the algebra that reads it, and the machinery that selects it: a propagator now picks a row backend once at construction, and everything from the scan to the inserts is templated on it.

SparseRowStore keeps a row as fixed-width mode lanes plus one 2-bit-per-slot codes word, instead of a dense monomial's storage words. CodesAlgebra.h is the structural algebra over that word — one function per dense counterpart — plus sparse_toggle, the product M ⊕ G as a single merge over two ascending lane arrays. It is exact, not an approximation, so both forms have to keep agreeing term for term; codes_algebra_tests.cpp and codes_product_tests.cpp are that comparison, over the fixtures and over randomized rows at several widths.

MPOperator holds one pointer per backend with exactly one non-null and binds the live one via with_storeonce per layer, inside build_layer, never per term, since the scan asks the store for a row per anticommuting term. LayerBuildEngine, fused_find_and_collect and probe_incoming_queries take the store as a template parameter; off that path the forwarding accessors pay one well-predicted branch. There is no accessor handing out a store, because there is no one type to hand out, so MonomialPropagator::indexing() is replaced by for_each_term() and num_local_terms().

Which backend comes from the mode count against monoprop_SPARSE_ROW_MIN_MODES — a build-time constant derived from whether ARCH_FLAG is actually emitted rather than from the option that asks for it, because what moves the crossover is the target ISA. monoprop_ROW_STORE=dense|sparse forces it process-wide, and an unrecognized value throws rather than falling back to auto: the point of setting it is to know which backend ran.

Every C++ case is now registered a second time with the sparse backend forced — the sparse-rows ctest label, 541 cases where there were 270. Every fixture is below the crossover, so without that the support-form backend would ship untested. row_store_selection_tests.cpp is what fails if the variable stops reaching the propagator.

Gates

  • just diff-baseline: the dense capture is byte-identical to main's golden baseline. Nothing on the shipping path moved.
  • just diff-baseline-sparse: the sparse capture agrees with it as term sets plus rtol 1e-10 across all 34 records. The two backends hash differently and so accumulate in a different order by design, which is why this one is a tolerance check.
  • ctest: 541/541, both backends.
  • Python suite: 620 passed under auto and under monoprop_ROW_STORE=sparse (two monoprop-bench-tools memory-measurement failures are pre-existing on main and unrelated).

Changes

  • cpp/monoprop/detail/operator/SparseRowStore.h, cpp/monoprop/algebra/CodesAlgebra.h: new.
  • cpp/monoprop/detail/operator/MPOperator.h: dual store, with_store, set_store, rows_are_sparse, forwarding accessors.
  • cpp/monoprop/detail/operator/RowAccess.h: the four accessors over the third backend.
  • cpp/monoprop/detail/evolution/layer_build/{Engine,Scan,Resolve}.h: templated on the store; build_layer is the single binding site.
  • cpp/monoprop/detail/EnvConfig.h, MonomialPropagator: monoprop_ROW_STORE and the per-propagator choice.
  • cpp/monoprop/Bitset.h: hoist splitmix_finalize into namespace monoprop — the row hash mixes (mode, code) slots with no Bitset in hand. The value routes MPI ownership, so it stays bit-identical.
  • cpp/monoprop/detail/operator/RowHashTable.h: geometric_row_capacity / spilled_rows_bytes, now shared by both stores.
  • CMakeLists.txt: derive and propagate monoprop_SPARSE_ROW_MIN_MODES.
  • cpp/tests/boostAddTests.cmake, boost-test.cmake: the sparse-rows variant pass, with its own MPI rank list so dense coverage growth does not multiply sparse mpiexec launches.
  • Seven new/extended test files plus RandomMonomial.h and TestOperator.h.
  • justfile: diff-baseline-sparse, test-sparse-rows.
  • benches/conftest.py, report.py: record and render monoprop_row_store (asked) beside row_store_effective (ran).
  • AGENTS.md, README.md, docs/{building,testing}.mdx.

Not in this PR

The sparse query recordQueryKeysFor, query_payload_words_for, the escape tail — has no user until the per-gate kernel seam (TermProduct.h) lands, because every product is still computed densely. It goes with PR 05 alongside that seam and the retained-key arena.

Checklist

  • 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 used the following tool to help write this PR description: ClaudeCode (claude-opus-5)
  • I used the following tool to generate or modify code: ClaudeCode (claude-opus-5)

@github-actions github-actions Bot added the cpp label Aug 29, 2026
@github-actions

Copy link
Copy Markdown

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

@robertodr
robertodr force-pushed the split/04-sparse-row-store branch from 942ed08 to 0202e3f Compare August 29, 2026 14:36
@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 (e4b1217) to head (9a6284c).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@                   Coverage Diff                    @@
##           split/02-row-hash-table     #305   +/-   ##
========================================================
  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.

SparseRowStore is a third row representation: each row is a fixed-width list of
the modes it occupies plus one word carrying two bits per occupied mode.
CodesAlgebra.h is the structural algebra over that word -- one function per
dense counterpart, reading the codes word instead of looping over storage words,
plus sparse_toggle, the product M (+) G as one merge over two ascending lane
arrays.

Not yet selected by anything. MPOperator still holds a single OperatorIndex, and
neither RowAccess nor the layer-build scan knows this store exists;
monoprop_SPARSE_ROW_MIN_MODES is defined and preferred_for_modes() reads it, but
no caller asks. Wiring it in is the next change.

Two pieces of shared machinery move to RowHashTable.h beside the index both
stores key rows through: geometric_row_capacity, the 1.5x growth rule, and
spilled_rows_bytes, the side-map accounting. A single number in two stores would
otherwise be correctable in one and not the other, which would skew
operator_memory_breakdown() for one backend only.

splitmix_finalize comes out of SplitmixHash<Bitset<N>>::mix, whose body never
depended on the width: the row hash accumulates over (mode, code) slots and so
needs the mix with no Bitset in hand. That value routes MPI ownership, so having
one definition rather than two matters more than the tidiness.

Verified out of tree against the compile-time-width engine: 3000 random
monomials round-trip through row(), popcount(), find(), find_batch(),
for_each_position(), clone() and resized(); and over 5000 monomials the codes
forms of cutoff_sums, is_paired, pauli_y_count, pauli_anticommutes and
pauli_rotation_sign agree with the dense ones exactly. OperatorIndex comes out
inert after the two helpers move -- identical iteration order, find results,
clone order and memory_bytes.

Assisted-by: ClaudeCode:claude-opus-5
@robertodr
robertodr force-pushed the split/04-sparse-row-store branch from 0202e3f to 3c22661 Compare August 29, 2026 14:59
Wires SparseRowStore and CodesAlgebra into the engine: a propagator now picks a row
backend once at construction and everything from the scan to the inserts is templated
on it.

MPOperator holds one pointer per backend with exactly one non-null and binds the live
one via with_store -- once per layer, inside build_layer, never per term, since the scan
asks the store for a row per anticommuting term. LayerBuildEngine, fused_find_and_collect
and probe_incoming_queries take the store as a template parameter; off that path the
forwarding accessors pay one well-predicted branch. There is no accessor handing out a
store, because there is no one type to hand out, so MonomialPropagator::indexing() is
replaced by for_each_term() and num_local_terms().

The choice comes from the mode count against monoprop_SPARSE_ROW_MIN_MODES;
monoprop_ROW_STORE=dense|sparse forces it process-wide, and an unrecognized value throws
rather than falling back to auto -- the point of setting it is to know which backend ran.

Every C++ case is registered a second time with the sparse backend forced (the
sparse-rows ctest label, 541 cases where there were 270): every fixture is below the
crossover, so without that the support-form backend would ship untested.
row_store_selection_tests.cpp is what fails if the variable stops reaching the
propagator.

Gates: the dense capture is byte-identical to main's golden baseline, and the sparse
capture agrees with it as term sets plus rtol 1e-10 (just diff-baseline-sparse).

Assisted-by: ClaudeCode:claude-opus-5
@github-actions github-actions Bot added documentation Improvements or additions to documentation python ci labels Aug 29, 2026
@robertodr robertodr changed the title feat(cpp): ✨ add the support-form row store and its codes algebra feat(cpp): ✨ select the support-form row store per propagator Aug 29, 2026
@robertodr
robertodr marked this pull request as ready for review August 29, 2026 15:38
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci cpp documentation Improvements or additions to documentation python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant