From fa0be326dd46eeb693d8ecc72209b36adc3f66cf Mon Sep 17 00:00:00 2001 From: Frank McSherry Date: Mon, 10 Aug 2026 11:06:02 -0400 Subject: [PATCH 1/3] Retire seed_times; present the novel run apart from the history MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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` — 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) --- differential-dataflow/src/operators/common.rs | 18 +- .../src/operators/int_proxy/reduce.rs | 190 +++++++++++++----- interactive/src/corgi/reduce.rs | 138 +++++++------ 3 files changed, 234 insertions(+), 112 deletions(-) diff --git a/differential-dataflow/src/operators/common.rs b/differential-dataflow/src/operators/common.rs index aea8e598b..069846f12 100644 --- a/differential-dataflow/src/operators/common.rs +++ b/differential-dataflow/src/operators/common.rs @@ -179,9 +179,10 @@ pub fn tile_descriptions( (tile_descs, tile_held, tile_of) } -/// A one-key view into an input presentation: the read-only arguments [`discover_times`] needs -/// about a single key — its slice `[i0, i1)` of the merged `(id, time, diff)` run `p_in` and -/// the carried `pending` times. +/// A one-key view into the ACCUMULATED input presentation: the read-only arguments +/// [`discover_times`] needs about a single key — its slice `[i0, i1)` of the `(id, time, diff)` run +/// `p_in` and the carried `pending` times. The novel run is not here; it arrives as `seed_times`, +/// and the two are deliberately never merged (see the note on `seed_times` below). pub struct KeyView<'a, T, RIn> { /// The presented `((key_hash, value_id), time, diff)` run the key's records live in. pub p_in: &'a [((u64, u64), T, RIn)], @@ -236,15 +237,18 @@ impl Default for DiscoverScratch /// Determines the times in `[lower, upper)` at which a key's reduction must be re-evaluated /// (`moments`), and the times at or beyond `upper` to carry into the next invocation -/// (`pended`). Replays the key's `seed_times` and `pending` times in ascending order, marking +/// (`pended`). Replays the key's `seed_times` (the novel run) and `pending` times in ascending +/// order, marking /// those that carry updates and closing the set under joins with the input and output /// histories' times and with each other. No input collection is materialized, so peak memory /// is O(times); buffers are advanced by the meet of the times still to come, keeping a key /// with many distinct times linear rather than quadratic. /// -/// `seed_times` must be the novel batch's own time support for this key. Seeding from a -/// consolidated view is unsound: compaction may advance a history record onto a novel time, -/// where consolidation cancels the novel update and its interesting time is missed. +/// `seed_times` must be the novel batch's own time support for this key, and `key.p_in` the +/// accumulated input WITHOUT it. Seeding from a view of the two consolidated together is unsound: +/// compaction may advance a history record onto a novel time, where consolidation cancels the novel +/// update and its interesting time is missed. This is why the caller keeps the two runs apart and +/// combines them only when accumulating. #[allow(clippy::too_many_arguments)] pub fn discover_times( key: KeyView<'_, T, RIn>, diff --git a/differential-dataflow/src/operators/int_proxy/reduce.rs b/differential-dataflow/src/operators/int_proxy/reduce.rs index ee206a904..ae9e90253 100644 --- a/differential-dataflow/src/operators/int_proxy/reduce.rs +++ b/differential-dataflow/src/operators/int_proxy/reduce.rs @@ -3,7 +3,7 @@ //! A conventional differential reduce against `(u64, u64)`, where the backend supplies the //! implementation of the interpretation of the integers. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeMap; use timely::PartialOrder; use timely::progress::{Antichain, Timestamp}; @@ -30,34 +30,51 @@ pub struct ReduceInstance<'a, B1: BatchReader, B2: BatchReader