Retire seed_times; visit only the keys with work due - #824
Merged
Conversation
`seed_times` existed because `next_window` merged the accumulated input and the novel delta into one bridge, which loses which records were novel. A novel record that nets to zero against a history record compaction has advanced onto its time vanishes, and its interesting time with it, so the seeds had to be derived from a second, separate read of the delta. The window now carries the two runs apart, mirroring the three cursors of the conventional reduce, which has always held them apart for exactly this reason. The seeds are the novel run's own times, read where they already are. The two runs combine only in the per-moment accumulation, never as presentations. `next_window` also takes `from: &mut Option<u64>` — a key hash rather than an index into a harness-built list — on the same contract as the join tactic's `advance`: strictly increase it or report the key space exhausted, and report a key entirely within the window that first mentions it. The window is caller-owned and refilled, so its three runs keep their allocations. Neither side now scans the whole key space. The harness contributes the keys it knows must be revisited, and the backend adds the ones its novel batches touch, which it discovers in the scan the presentation needs anyway. That change to `changed` is where the time went, and it was a real redundancy rather than a constant factor. `changed` was every pending key; it is now only the keys with an interesting time DUE in this interval. A key whose times all lie at or beyond `upper` was being presented and walked through determination on every retire, only to produce no moments — and its history and output rows were gathered into the presentation to do it. The conventional tactic never did this: it filters to the due times and takes the no-work branch when there are none. The closure is not lost by skipping, because a join of two not-yet-due times is itself not yet due, and is synthesized when they become due. Carrying the times of unvisited keys is now explicit, since a key that is not `changed` is never presented and would otherwise be forgotten. The split against `upper` happens up front: due times drive determination, carried times seed the next round's pending set directly, and a key with both unions them. Measured on the corgi backend, both binaries built in the same target directory: scc n=500 e=1000 batch=100 rounds=60 4.07s -> 1.92s scc n=1000 e=2000 batch=100 rounds=100 12.33s -> 4.95s scc n=2000 e=4000 batch=100 rounds=60 18.83s -> 5.72s reach n=20000 e=40000 batch=200 rounds=100 flat The gap grows with n, as it should if the old cost tracked the whole pending set and the new one tracks the due set. Program output is byte-identical across scc, reach, tour, adt, binders and unnest, on both the corgi and vec backends. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Narrowing `changed` to the due keys made the old early return unsafe, so it was
dropped along with `seed_times`.
It read
if changed.is_empty() { self.pending.clear(); return (Vec::new(), Antichain::new()) }
which was sound only because `changed` then held every pending key, so an empty
`changed` implied an empty pending set.
It no longer does: times at or beyond `upper` remain when nothing is due, and
clearing them while returning an empty frontier releases the capabilities that
were holding them.
The guarded form also requires the novel batches to be empty, and returns the
frontier over the untouched pending set rather than an empty one.
Without it a retire with nothing to do still opened a tiled output session and
shipped empty batches.
Also measured and rejected: advancing the accumulated input and output
presentations by `instance.lower` in the corgi backend, as its join backend
already does in `stage_runs`.
The reasoning is sound — every moment is at or above `lower`, so advancing
preserves each record's downset while letting the consolidation net more — but
it costs more than it saves, because the driver already sets logical compaction
to `upper` on both traces each activation, so the times largely arrive advanced
and the pass is work for nothing.
On scc (n=1000, corgi) it was a stable 5.09-5.10s against 4.85-4.89s, about 4%
worse, and flat everywhere else.
`ReduceInstance::lower` therefore remains unread by the only backend.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
frankmcsherry
marked this pull request as ready for review
August 10, 2026 20:13
frankmcsherry
added a commit
that referenced
this pull request
Aug 10, 2026
…mark A ProxyReduceBackend over hash-keyed VecChunk storage, the plain-rows counterpart to the corgi backend. It covers the key space in bounded windows, so the harness's multi-window path — the code #824 noted had never executed — now runs under its contract assertions in every test. Input value ids are per-run ordinals from a shared pool: the history and novel runs mint independently, and a value present in both gets two ids, reconciled because reduce_corrections resolves ids to values and consolidates by value before applying logic. Output ids are interned, sharing the namespace corrections mint into. Collisions re-group each hash bracket by real key, so a 64-bit collision is an inefficiency rather than an error (tested by forcing every key to one hash). tests/int_proxy.rs gives the proxy tactic its first in-repo tests: a direct retire, collision correctness, cursor-reduce comparisons (flat, String values, multi-moment, one-key windows), and reduce inside iterate. tests/int_proxy_bench.rs benchmarks the cursor tactic against the proxy tactic over the SAME hash-keyed arrangement (with the inherent reduce as reference) across three regimes: streaming churn, multi-time batches, and label propagation inside iterate with churn — the carried-interesting-times shape of #824. Current readings, single worker, release: churn 1.03x cursor-same; multimoment 1.84x; propagate 3.39x (output checksums asserted equal across all three modes). Also adds VecChunk::as_slice, a read-only accessor for the sorted records, which the backend's merge walks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frankmcsherry
added a commit
that referenced
this pull request
Aug 10, 2026
…mark A ProxyReduceBackend over hash-keyed VecChunk storage, the plain-rows counterpart to the corgi backend. It covers the key space in bounded windows, so the harness's multi-window path — the code #824 noted had never executed — now runs under its contract assertions in every test. Input value ids are per-run ordinals from a shared pool: the history and novel runs mint independently, and a value present in both gets two ids, reconciled because reduce_corrections resolves ids to values and consolidates by value before applying logic. Output ids are interned, sharing the namespace corrections mint into. Collisions re-group each hash bracket by real key, so a 64-bit collision is an inefficiency rather than an error (tested by forcing every key to one hash). tests/int_proxy.rs gives the proxy tactic its first in-repo tests: a direct retire, collision correctness, cursor-reduce comparisons (flat, String values, multi-moment, one-key windows), and reduce inside iterate. tests/int_proxy_bench.rs benchmarks the cursor tactic against the proxy tactic over the SAME hash-keyed arrangement (with the inherent reduce as reference) across three regimes: streaming churn, multi-time batches, and label propagation inside iterate with churn — the carried-interesting-times shape of #824. Current readings, single worker, release: churn 1.03x cursor-same; multimoment 1.84x; propagate 3.39x (output checksums asserted equal across all three modes). Also adds VecChunk::as_slice, a read-only accessor for the sorted records, which the backend's merge walks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
frankmcsherry
added a commit
that referenced
this pull request
Aug 11, 2026
A DDIR variant column becomes a corgi `Sum`: a tag column plus one lane per constructor. Nothing carries the declared universe, so each shape derivation infers the arity from what it sees -- `infer_shape_cols` from the batch's tags, `infer_term_shape` from `Inject(tag, _)` as `tag + 1`. A collection holding only `Rare(_)` therefore gets one lane and one holding only `Common(_)` gets two, for the same DDIR type. Concatenating them into one arrangement panicked corgi while vec rendered it fine. frankmcsherry/WIP#10 fixed that where it lives: `gather_lanes` took the output Sum's arity from `sums[0]` while gathering out tags from every source, dropping lanes the tags still named. This bumps the pin onto it and keeps the two programs that found it as gate coverage -- one on the row-wise fallback, one on the compiled path, since both derivations under-approximate. Earlier revisions of this PR reconciled arities DDIR-side in `chunk.rs` at the four sites that read two independently-inferred columns. That is all gone: the corgi fix subsumes it, and dropping the per-merge shape walk is worth ~4% on scc steady-state (4.54s vs 4.72s, median of 3) now that #824/#826 have taken the baseline down to where it shows. Gate 14/14 debug and release; both new programs panic without the pin bump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0168NJWzHwrLGW2RXToGooYP
frankmcsherry
added a commit
that referenced
this pull request
Aug 12, 2026
A DDIR variant column becomes a corgi `Sum`: a tag column plus one lane per constructor. Nothing carries the declared universe, so each shape derivation infers the arity from what it sees -- `infer_shape_cols` from the batch's tags, `infer_term_shape` from `Inject(tag, _)` as `tag + 1`. A collection holding only `Rare(_)` therefore gets one lane and one holding only `Common(_)` gets two, for the same DDIR type. Concatenating them into one arrangement panicked corgi while vec rendered it fine. frankmcsherry/WIP#10 fixed that where it lives: `gather_lanes` took the output Sum's arity from `sums[0]` while gathering out tags from every source, dropping lanes the tags still named. This bumps the pin onto it and keeps the two programs that found it as gate coverage -- one on the row-wise fallback, one on the compiled path, since both derivations under-approximate. Earlier revisions of this PR reconciled arities DDIR-side in `chunk.rs` at the four sites that read two independently-inferred columns. That is all gone: the corgi fix subsumes it, and dropping the per-merge shape walk is worth ~4% on scc steady-state (4.54s vs 4.72s, median of 3) now that #824/#826 have taken the baseline down to where it shows. Gate 14/14 debug and release; both new programs panic without the pin bump. Claude-Session: https://claude.ai/code/session_0168NJWzHwrLGW2RXToGooYP Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Removes
seed_timesfromProxyReduceBackendby presenting the novel input run apart from the accumulated one, and narrows the set of keys a retire visits to those with work actually due in the interval.The second of those is worth 2-3x on SCC over the corgi backend, growing with
n.Why
seed_timesexistednext_windowhanded back one input bridge holding the accumulated history and the novel delta consolidated together, which loses which records were novel. A novel record that nets to zero against a history record compaction has advanced onto its time disappears, and its interesting time with it. The seeds therefore had to come from a second, separate read of the delta.The conventional reduce has never had this problem: it keeps three cursors, and the batch history is loaded unadvanced and is the only thing that makes a time interesting. The window now carries the same three runs, so the seeds are the novel run's own times, read where they already are. The two input runs combine only in the per-moment accumulation, never as presentations.
Protocol
next_windownow takesfrom: &mut Option<u64>— a key hash rather than an index into a list the harness built — on the same contract asProxyJoinBackend::advance: strictly increase it or report the key space exhausted, and report a key entirely within the window that first mentions it. Both aredebug_asserted. The window is caller-owned and refilled, so its three runs keep their allocations.Neither side now scans the whole key space. The harness contributes the keys it knows must be revisited; the backend adds the ones its novel batches touch, which it discovers in the scan the presentation needs anyway.
Where the time went
Not the seeding pass.
changedwas every pending key and is now only the keys with an interesting time due in[lower, upper). A key whose times all lie at or beyondupperwas being gathered into the presentation — history rows, output rows — and walked through determination on every retire, only to produce no moments.The conventional tactic never did this:
cursors::CursorTacticfilters to!upper.less_equal(time)and takes the no-work branch when nothing is left, and the model-derivedreference::ReferenceTacticdoes the same. This change brings the proxy tactic in line with both.Carrying the times of unvisited keys is now explicit, since a key that is not
changedis never presented and would otherwise be forgotten. The split againstupperhappens up front: due times drive determination, carried times seed the next round's pending set directly, and a key with both unions them. The old no-work early return returned an empty frontier, which was safe only whilechangedheld every pending key; the restored form additionally requires the novel batches to be empty and returns the frontier over the untouched pending set.Measurements
ddiron the corgi backend, single worker, both binaries built in the same target directory. Binary layout between build trees moves these programs by more than the effect, sovecis carried as a control — it uses the cursor tactic and is untouched by this PR.The corgi/vec spread across these programs was
[0.97x, 3.17x]and is now[0.98x, 1.62x]. SCC was the outlier and it collapsed; nothing else moved, which is what to expect if the redundancy only bit where a large pending set is carried across retires.adtis now the widest gap and did not move, so whatever it is, it is not this.This bears on
int-proxy-columnar-findings.md, which attributed the 2.24-2.59x SCC corgi/vec gap toPointStampallocation and corgi's structural compare and concluded "framework is a tie". On this shape most of it was the tactic re-presenting and re-walking every pending key.Testing
DD has no
int_proxytest, so the gate iscargo test -p interactive --test corgi_backend— 12 programs asserting the corgi andvecbackends agree — run in debug, wheredebug_assert_sorted_bridgeand thereduce_with_tacticcontract checks are live. Green, along with the DD suite.Given the size of the speedup I also compared program output directly rather than relying on the gate: byte-identical to
master-nextacross scc, reach, tour, adt, binders and unnest, on both backends, at several sizes (5,624 lines on tour, 2,332 on unnest).Also measured and rejected
Advancing the accumulated input and output presentations by
instance.lowerin the corgi backend, as its join backend already does instage_runs. The reasoning holds — every moment is at or abovelower, so advancing preserves each record's downset while letting the consolidation net more — but the driver already sets logical compaction toupperon both traces each activation, so the times largely arrive advanced and the pass is work for nothing. On scc it was a stable 5.09-5.10s against 4.85-4.89s, about 4% worse, and flat elsewhere.ReduceInstance::loweris consequently still unread by the only backend.Open
The multi-window path is unexercised. corgi covers the key space in a single call, so the harness code that runs on a second window — the cursor walking
changedacross windows, the merge that forms each window's key list, and both newdebug_asserts — has never executed. AProxyReduceBackendover a conventionalOrdValBatchtrace would exercise it and would also let the tactic be benchmarked against the cursor tactic on identical storage, which nothing currently does.Co-authored-by: Claude Opus 5 (1M context) noreply@anthropic.com