Skip to content

perf(cpp): ⚡ bind the per-term kernel and the support-form query record - #309

Open
robertodr wants to merge 1 commit into
split/05-drop-nttpfrom
split/06-term-kernel
Open

perf(cpp): ⚡ bind the per-term kernel and the support-form query record#309
robertodr wants to merge 1 commit into
split/05-drop-nttpfrom
split/06-term-kernel

Conversation

@robertodr

Copy link
Copy Markdown
Member

🤖 AI text below 🤖

Summary

Two things #308 left on the table, both of which need the runtime width it introduced.

The per-term kernel seam. Everything the scan asks about one anticommuting term — the product, the overlap, the rotation sign, the structural cutoff, the owner rank and the query record — goes through the per-gate object TermProductsFor<Store, A, W> selects, so the scan itself names no representation. SparseTermProducts answers the first four off the codes word and falls back to DenseTermProducts per term when there is no row to read (a spilled store row, a product past the scratch capacity) or no codes form of the cutoff. This is what makes #305's support-form store compute products in support form; before it, every product was still dense.

The third thing bound once per layer, beside the algebra and the backend: the storage word count. with_kernel_width turns gen.num_words() into a template parameter W at the same seam in build_layer, so every per-term word loop has a compile-time trip count and every operand's storage pointer is resolved once per gate — which is what a Bitset<NumBits> gave for free.

The support-form query record. A store is queried in the form it keys its rows by, so a resolve never converts. A query is M ⊕ G and a fully paired product escapes the cutoff, so no fixed-stride sparse record can hold every one: a buffer is [nq][record 0]…[record nq-1][dense escape tail]. An escaped record keeps its place and its stride, marks lane 0 with kOverflowLane and carries its tail index where the codes word would go — an index into the tail, never an offset into the buffer, which is what lets the fused sink widen every record without renumbering anything.

Stacked on #308.

Changes

  • layer_build/TermProduct.h: DenseTermProducts, DenseTermProductsW<A, W>, SparseTermProducts, TermProductsFor, with_kernel_width, kNarrowKernelWords.
  • WordKernel<W> (Bitset.h): the four word ops with W fixed that stand in for a Bitset method. Two of the four are the same definitions Bitset's own inline arms use, because one of them decides emitted term signs. splitmix is deliberately a second implementation — that value is monomial_hash, so it routes MPI ownership and must stay bit-identical, which word_kernel_tests.cpp asserts at every W rather than by construction.
  • fully_paired_words<W> lives in algebra/AlgebraCommon.h, beside the cutoff_sums it answers for and the even-bit literal it shares with CutoffMasks::make — it answers a question about the algebra, not the storage.
  • Common.h: QueryKeysFor<Store>, query_payload_words_for(store, capacity), SparseQueryKeys, the escape tail and append_escape_tail.
  • Tests: term_product_tests.cpp (the two kernels compared answer for answer), word_kernel_tests.cpp, sparse_wire_tests.cpp, and cpp/tests/InlineWidths.h, the one width sweep both kernel files use so the range cannot be narrowed in one alone.

Measurements

kNarrowKernelWords is 4. Measured worth ~10% at two and four storage words and nothing at seven or eight, so the cap is where an instantiation stops paying; the cost is ~11% of .text. Two conditions on that number, both measured:

  • It is the Majorana path. The Pauli rotation sign already loops over the generator's non-zero words only, so W binds no trip count there and the 127-qubit kicked-Ising model gains ~1%.
  • It scales with how much of a run is in the per-term product at all, so a loose lower_atol — which rejects a term on its coefficient before the product is computed — sees about a third of it.

Two further bindings past this seam measured at nothing (under 0.05% of the instruction count on either shipping model, pinned single-threaded), because the optimizer already hoists them out of the inlined scan loop: resolving the algebra's per-term sign inputs into a per-gate struct, and writing the query record with the word count bound. Measure any third one the same way before adding it — wall clock cannot see this range, and neither can an instruction count taken with the thread pool live, which spins hard enough to inflate the total ~14x.

Verification

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

The sparse kernel is exact, not an approximation, and the differential tests are the gate: term_product_tests.cpp compares SparseTermProducts against DenseTermProducts answer for answer, and the bound-width kernel against the runtime one over the whole inline regime rather than just the capped widths. Extend it with any new answer, or that answer ships untested.

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)

Everything the scan asks about one anticommuting term -- the product, the
overlap, the rotation sign, the structural cutoff, the owner rank and the query
record -- goes through the per-gate object TermProductsFor<Store, A, W> selects,
so the scan itself names no representation. SparseTermProducts answers the first
four off the codes word and falls back to DenseTermProducts per term when there
is no row to read or no codes form of the cutoff.

The storage word count becomes the third thing bound once per layer, beside the
algebra and the backend: with_kernel_width turns gen.num_words() into a template
parameter W, so every per-term word loop has a compile-time trip count again.
Worth ~10% at two and four storage words and nothing above, so kNarrowKernelWords
caps which widths get an instantiation.

A store is now queried in the form it keys its rows by, so a resolve never
converts. A fully paired product escapes the cutoff and no fixed-stride sparse
record can hold every one, so a buffer carries a dense escape tail behind its
records; an escaped record keeps its place and its stride and carries its tail
index where the codes word would go.

Terms and energies are unchanged: the dense baseline stays byte-identical to
main's and the support-form one still agrees to rtol 1e-10.

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

Copy link
Copy Markdown

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

@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.58%. Comparing base (e89a79e) to head (6727351).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@                 Coverage Diff                 @@
##           split/05-drop-nttp     #309   +/-   ##
===================================================
  Coverage               97.58%   97.58%           
===================================================
  Files                      14       14           
  Lines                     746      746           
  Branches                   98       98           
===================================================
  Hits                      728      728           
  Misses                     13       13           
  Partials                    5        5           
Flag Coverage Δ
cpp 97.58% <ø> (ø)

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

cpp documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant