perf(cpp): ⚡ bind the per-term kernel and the support-form query record - #309
Open
robertodr wants to merge 1 commit into
Open
perf(cpp): ⚡ bind the per-term kernel and the support-form query record#309robertodr wants to merge 1 commit into
robertodr wants to merge 1 commit into
Conversation
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
robertodr
requested review from
diagonal-hamiltonian,
fpietra and
ludmilaasb
as code owners
August 29, 2026 16:44
5 tasks
|
Docs preview: https://pr-309.monoprop-docs.pages.dev |
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. |
|
9 tasks
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.



🤖 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.SparseTermProductsanswers the first four off thecodesword and falls back toDenseTermProductsper 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_widthturnsgen.num_words()into a template parameterWat the same seam inbuild_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 aBitset<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 ⊕ Gand 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 withkOverflowLaneand 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 withWfixed that stand in for aBitsetmethod. Two of the four are the same definitionsBitset's own inline arms use, because one of them decides emitted term signs.splitmixis deliberately a second implementation — that value ismonomial_hash, so it routes MPI ownership and must stay bit-identical, whichword_kernel_tests.cppasserts at everyWrather than by construction.fully_paired_words<W>lives inalgebra/AlgebraCommon.h, beside thecutoff_sumsit answers for and the even-bit literal it shares withCutoffMasks::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 andappend_escape_tail.term_product_tests.cpp(the two kernels compared answer for answer),word_kernel_tests.cpp,sparse_wire_tests.cpp, andcpp/tests/InlineWidths.h, the one width sweep both kernel files use so the range cannot be narrowed in one alone.Measurements
kNarrowKernelWordsis 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:Wbinds no trip count there and the 127-qubit kicked-Ising model gains ~1%.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
capture-baselinevs main's golden, dense rows--compare --tol 1e-10, sparse vs densectestsparse-rows)pytest,autoandmonoprop_ROW_STORE=sparseprekover the changed filesThe sparse kernel is exact, not an approximation, and the differential tests are the gate:
term_product_tests.cppcomparesSparseTermProductsagainstDenseTermProductsanswer 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
docs/,CONTRIBUTING.md) if neededCHANGELOG/ release notes updated if applicableAI/LLM disclosure