Skip to content

refactor(cpp)!: ♻️ take the mode count as a runtime width - #308

Open
robertodr wants to merge 1 commit into
split/04-sparse-row-storefrom
split/05-drop-nttp
Open

refactor(cpp)!: ♻️ take the mode count as a runtime width#308
robertodr wants to merge 1 commit into
split/04-sparse-row-storefrom
split/05-drop-nttp

Conversation

@robertodr

@robertodr robertodr commented Aug 29, 2026

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

Summary

The engine no longer carries the mode count as a template parameter. A propagator takes its logical width as a constructor argument and sizes its monomial storage from it at runtime, so there is one compiled MonomialPropagator whatever the mode count and no compile-time ceiling on it.

Bitset is the monomial: the width is data, not a template parameter, so a width exists per value rather than per type. Construct with one (Bitset(num_bits)), take a mode count from mono.size() / 2, recover a width with an instance call. Per-word loops go through detail::with_nwords, which dispatches a runtime word count to a compile-time-unrolled arm, in place of the if constexpr branches a compile-time width allowed.

That removes the binding generators with it. tools/generate-binders.py and tools/generate-dispatch.py existed to emit one instantiation per mode tier; bindings.cpp is a real file now rather than a configured template, so the nanobind version the module reports arrives as monoprop_NANOBIND_VERSION from CMake.

Stacked on #305.

Changes

  • Bitset/Monomial: runtime width, detail::with_nwords dispatch, heap_bytes() for the spilled regime. Only words [0, num_words()) hold a value — the inline tail above them is left indeterminate so a copy costs the operand's own width.
  • MonomialPropagator: num_modes is a constructor argument; detail::storage_modes_for() derives the storage width from it, rounded up to a whole 32-mode block.
  • with_algebra binds a runtime Basis without a width; the algebra policies lose their NumModes parameter.
  • MonomialPropagator.inl becomes MonomialPropagator.cpp — there is nothing left to instantiate per width.
  • Deleted: tools/generate-binders.py, tools/generate-dispatch.py, tools/_binding_layout.py, bindings.cpp.in, tests/test_binding_layout.py.
  • New derived wide-system coverage: ModeEmbedding/WIDE_EMBEDDING (tests/cases.py) and test_utils::embed_case relabel a fixture into 260 logical / 288 storage modes — nine words per monomial, past Bitset's eight inline ones. The map is monotone, so the fixture's exact energy still applies and the wide run owes the narrow run's evolved operator term for term.
  • detail/partition/StagedCollect.h: one result per partition written from the partitions' own masters. The staging vector is there for the bool case only — std::vector<bool> is bit-packed, so concurrent writes to disjoint logical elements can tear the same word.

Verification

Gate Result
capture-baseline vs main's golden, dense rows byte-identical, 34 records
--compare --tol 1e-10, sparse vs dense (incl. the new wide case) 38 records agree, term sets exact, 0 failing
ctest 587/587 (293 sparse-rows)
pytest, auto and monoprop_ROW_STORE=sparse 623 / 591 passed
prek over the changed files pass

The byte-wise dense gate is the load-bearing one: the width stops being a type, and no term, coefficient, iteration order or energy moves.

Two pre-existing monoprop-bench-tools test_memory.py failures are unrelated — they fail identically on a pristine main checkout and pass in isolation.

Not in this PR

The per-gate kernel seam (TermProduct.h, WordKernel<W>, with_kernel_width) and the support-form query record follow in #309. Both are written against the runtime width introduced here, so they cannot precede it. Until they land the per-term instruction count runs above the compile-time-width engine — about a fifth of which reaches cycles.

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: Claude Code (claude-opus-5)
  • I used the following tool to generate or modify code: Claude Code (claude-opus-5)

The engine no longer carries the mode count as a template parameter. A propagator
takes its logical width as a constructor argument and sizes its monomial storage
from it at runtime, so there is one compiled MonomialPropagator whatever the mode
count and no compile-time ceiling on it.

Bitset *is* the monomial: the width is data, so a width exists per value rather
than per type. Per-word loops go through detail::with_nwords, which dispatches a
runtime word count to a compile-time-unrolled arm in place of the if constexpr
branches a compile-time width allowed.

The binding generators go with it -- tools/generate-binders.py and
tools/generate-dispatch.py emitted one instantiation per mode tier, and
bindings.cpp is now a real file rather than a configured template.

Dense-row terms and energies are byte-identical to the compile-time-width engine
across every baseline case, and the support-form backend still agrees with them
as term sets and to rtol 1e-10.

The per-gate kernel seam and the support-form query record that recover the lost
per-term specialization are not here; they follow separately.

Assisted-by: ClaudeCode:claude-opus-5
@github-actions

Copy link
Copy Markdown

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

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.58%. Comparing base (9a6284c) to head (e89a79e).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/monoprop/monomial_propagator.py 66.66% 2 Missing ⚠️
Additional details and impacted files
@@                      Coverage Diff                      @@
##           split/04-sparse-row-store     #308      +/-   ##
=============================================================
- Coverage                      97.70%   97.58%   -0.13%     
=============================================================
  Files                             14       14              
  Lines                            742      746       +4     
  Branches                          98       98              
=============================================================
+ Hits                             725      728       +3     
- Misses                            12       13       +1     
  Partials                           5        5              
Flag Coverage Δ
cpp 97.58% <66.66%> (-0.13%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

@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 dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation python tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant