perf(evolution): ⚡ settle a mutual pair from the leader's record alone - #355
Draft
diagonal-hamiltonian wants to merge 3 commits into
Draft
diagonal-hamiltonian wants to merge 3 commits into
diagonal-hamiltonian wants to merge 3 commits into
Conversation
…eisenberg `ContractSink::out_unanswered` is called once per record whose key missed everywhere -- once per mint of every gate -- and pushed a 16-byte `HalfRotationRec` carrying `c0`, the absent partner's pre-gate coefficient. Only the Schrödinger picture has such a coefficient: `sent_c0` is windowed exactly when the scan was handed a `state_mask`, and `absence_pass` itself substitutes the literal 0.0 otherwise. In Heisenberg the buffered half is therefore `c += sin·(∓1)·0.0` -- 16 bytes in the halves buffer, a slot in the gate's reserve, and one random-access read-modify-write of a coefficient the apply would not otherwise touch, to add nothing. Skip it there. The sink carries `absences_carry_c0`, set from `schrodinger` at the one construction site; the Schrödinger path is untouched. ROUNDING ORDER: the dropped operation is an addition of an exact zero, `x + (±0.0)`. For every finite x != 0 that is x bit for bit, and for x = +0.0 it is +0.0. The single observable difference is x = -0.0, where `-0.0 + 0.0` is `+0.0` and skipping the add leaves `-0.0`; those compare equal, so it is 0 ULP by any comparison of values. Measured on the 35 golden cells, no coefficient sits there: the raw dump's `+-0only` column is 0 and the arm is bitwise identical. `GraphSink::out_unanswered` still records every absence -- the graph needs the row and the phase regardless of the value -- the halves reserve is an upper bound and stays valid, and the absence pass's own iteration order is unchanged. Assisted-by: ClaudeCode:claude-opus-5
A mutual pair -- both endpoints rotating, both records staged on the same slot -- was resolved
twice: each record probed the term table for the other's row and pushed its own half. When the
LEADER's record (the endpoint without the pivot bit) is resolved first it already has both rows in
hand, so it pushes both halves and the follower's record is skipped, at its probe as well as at its
resolve. One probe, one confirm and one resolve pass per local mutual pair.
Exact, not approximate. The follower's record would have delivered {source, -phase,
fl(fl(v·cos)·inv_cos)}; the leader's arm pushes {source, -phase, op_coeffs[row]·inv_cos}, and the
fused sweep's invariant op_coeffs[row] == fl(v·cos) plus phase_foll == -phase_lead make the two the
same double. Without the sweep both are the partner's own coefficient. In the follower-first order
both records take the ordinary arm, so only the saving depends on the order. Mint order is
untouched: a skipped query counts as a hit, and hits never mint.
`TermTable::find_batch` takes the caller's skip predicate and reports `BatchStats{hits, skipped}`;
`TableJoin` records a skipped query under its own sentinel, one below the missing row, so the
resolve can tell a skip from a miss. The probe-time test reads `matched` and the resolve-time one
`received`: the probe runs over the whole gate before the resolve does, which is why the two marks
are kept apart. `ContractSink` opts in with `pairs_once`; the graph sink, having no values to
recover, does not.
Also here, one load per self query: `join.hit(q)` is read once for both arms instead of once per
arm.
Assisted-by: ClaudeCode:claude-opus-5
Two paragraphs in the parallelism page's protocol section: the leader-first settlement of a mutual pair on one slot, with the bit-identity argument and what the follower-first order costs, and why the absent partner's half is nothing at all in the Heisenberg picture. Assisted-by: ClaudeCode:claude-opus-5
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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 cases the one-round join (PR 5) still did more work than it needed to.
A mutual pair — both endpoints rotating, both records staged on the same slot — was resolved twice:
each record probed the term table for the other's row and pushed its own half. This PR settles it
once, from the leader's record alone (the endpoint without the pivot bit): when the leader's record is
resolved first it already has both rows in hand, so it pushes both halves and the follower's record is
skipped, at its probe as well as at its resolve.
TermTable::find_batchtakes the caller's skippredicate and reports
BatchStats{hits, skipped};TableJoinrecords a skipped query under its ownsentinel, one below the missing row, so the resolve can tell a skip from a miss.
Separately,
ContractSink::out_unansweredpushed a 16-byte record for every absent partner, carryingc0, the partner's pre-gate coefficient — but only the Schrödinger picture has such a coefficient; inHeisenberg the buffered half is
c += sin·(∓1)·0.0, added for nothing. This PR skips it there(
absences_carry_c0, set fromschrodingerat the one construction site);GraphSinkis unaffected,since the graph needs the row and the phase regardless of the value.
This is PR 6 of 7, based on
perf/stack-5-one-round-join. Both changes are exact, not approximate —see Measurements. The next PR (
perf/stack-7-pair-exchange) replaces the collective exchange with adirect pairwise one when a gate's window names exactly one peer rank.
Changes
Engine
cpp/monoprop/detail/operator/TermTable.h(318 lines):find_batchgains aSkippredicate and askip sentinel, returns
BatchStats{hits, skipped}; the existing empty-table early return now alsoconsults
skip().cpp/monoprop/detail/evolution/layer_build/TableJoin.h(125 lines):kSkippedRow,skipped(q),skipped_count(),hits()= confirmed + skipped.cpp/monoprop/detail/evolution/layer_build/Resolve.h(405 lines):sink_pairs_once<Sink>()andjoin_self's pair-once arm (skip-continue on the follower, leader arm settles both halves); onejoin.hit(q)load per query, hoisted above the leader arm but not above the skip test.cpp/monoprop/detail/evolution/layer_build/GateSinks.h(251 lines):ContractSink::pairs_once,absences_carry_c0,out_unanswered's skip.cpp/monoprop/detail/evolution/layer_build/GateScratch.h:RowMarksdoc on whymatched(probetime) and
received(resolve time) are read separately — the probe runs over the whole gate beforethe resolve does.
cpp/monoprop/detail/evolution/layer_build/Engine.h:pair_once = sink_pairs_once<Sink>() && n_self != 0 && base < kSkippedRow; the probe's skip lambda onmarks.matched/foll;.absences_carry_c0 = schrodingerat the one construction site.Tests
cpp/tests/pair_once_tests.cpp(new, 411 lines): aGatefixture (20 leader-first pairs, onefollower-first pair, two silent partners, two absent partners) with halves/marks/rows compared bit
for bit against a two-record-path oracle; skip-bookkeeping cases for
TermTable/TableJoin,including the empty-table skip path.
cpp/tests/evolution_detail_tests.cpp:.absences_carry_c0 = trueon the existingContractSinkcase, plus
contract_sink_skips_the_absence_half_when_no_c0_travels.cpp/tests/sparse_resolve_tests.cpp,term_table_tests.cpp: a never-skip predicate at thejoin/
find_batchcall sites;term_table_testspinsskipped == 0on the unaffected paths.Docs
docs/content/docs/features/parallelism.mdx: two paragraphs — the leader-first settlement of amutual pair and its bit-identity argument, and why the absent partner's half is nothing at all in
the Heisenberg picture.
Measurements
Gated positional (
--raw-only) against the x2 raw-bit reference, the same reference PR 5's tail gatesagainst: gate record md5
543c105e043a329e52ddd3047c8db5c7.Paired A/B, 3 interleaved reps against origin/main c5e88c8 and the predecessor, ratios only:
Pair-once is neutral on one node: every rung is within the noise floor of its predecessor. Its
effect is on the cross-rank pair path, which PR 7 introduces.
Notes for reviewers
{source, -phase, fl(fl(v·cos)·inv_cos)}; the leader's arm instead pushes{source, -phase, op_coeffs[row]·inv_cos}. Under the fused sweep,op_coeffs[row] == fl(v·cos)andphase_foll == -phase_lead, so the two are the same double; without the sweep both are simply thepartner's own coefficient. Only in the follower-first order do both records take the ordinary
(two-probe) arm, so the saving — not the correctness — depends on record order. Mint order is
unaffected: a skipped query counts as a hit, and hits never mint.
x + (±0.0). For every finitex != 0that isxbit for bit; the one observable case isx = -0.0, where-0.0 + 0.0rounds to+0.0and skipping the add leaves-0.0— values that compareequal, so this is 0 ULP by any value comparison. The implementer's own dump over the 35 golden cells
(
runs/stack/golden-st6-x2.log) reports the raw diff's+-0onlycolumn at 0: no coefficient in thatset sits at
-0.0where the dropped half would have flipped its sign bit, so the skip isbit-identical in fact here, not only 0 ULP on values. The theoretical case (an operator that does
hold such a
-0.0) is a live caveat, recorded in04c9662's commit message rather than guarded incode.
pair_once_tests.cpp(only0 < skipped <= kPairs): PR 7's transport change moves them, and the bit-identity checks against the two-recordoracle do not depend on the count.
TableJoin::live_bytes()stays absent and noTimeProfcounters (n_mutual,n_skipped) areadded: diagnostics stay out of this stack, per the decision recorded in PR 2's Notes.
Checklist
docs/,CONTRIBUTING.md) if neededCHANGELOG/ release notes updated if applicable (n/a — the repository has noCHANGELOG)AI/LLM disclosure
Important
By opening this PR I confirm that I have read CONTRIBUTING.md and I agree to the terms of the Contributor License Agreement.
Warning
If you're contributing on behalf of your employer, contact cla@algorithmiq.fi to arrange a Corporate CLA.