Skip to main content

differential_dataflow/operators/int_proxy/
reduce.rs

1//! The proxy reduce framework.
2//!
3//! A conventional differential reduce against `(u64, u64)`, where the backend supplies the
4//! implementation of the interpretation of the integers.
5
6use std::collections::{BTreeMap, BTreeSet};
7
8use timely::PartialOrder;
9use timely::progress::{Antichain, Timestamp};
10use timely::progress::frontier::AntichainRef;
11
12use crate::difference::Semigroup;
13use crate::lattice::Lattice;
14use crate::trace::{BatchReader, Description};
15use super::ProxyBridge;
16use crate::operators::reduce::ReduceTactic;
17
18use super::history::IdHistory;
19use crate::operators::common::{discover_times, tile_descriptions, DiscoverScratch, KeyView};
20
21/// A unit of proxied reduce work, presented to the backend.
22pub struct ReduceInstance<'a, B1: BatchReader, B2: BatchReader<Time = B1::Time>> {
23    /// The accumulated input history.
24    pub source_batches: &'a [B1],
25    /// The freshly arrived input delta.
26    pub input_batches: &'a [B1],
27    /// The accumulated output history.
28    pub output_batches: &'a [B2],
29    /// The compaction frontier for loading (the retire's lower bound).
30    pub lower: AntichainRef<'a, B1::Time>,
31}
32
33/// One window of a retire's changed keys: a bounded, hash-contiguous snip the backend sizes.
34///
35/// The window has the input (old and new) and output histories, restricted to the window's keys.
36pub struct ReduceWindow<T, RIn, ROut> {
37    /// The window's key hashes: a contiguous, ascending slice of the retire's `changed` keys.
38    pub keys: Vec<u64>,
39    /// Input presentation for `keys`, sorted & consolidated by `((key_hash, value_id), time)`.
40    pub input: ProxyBridge<T, RIn>,
41    /// Output-history presentation for `keys`, same ordering.
42    pub output: ProxyBridge<T, ROut>,
43}
44
45/// The reduce backend: value semantics for a proxy-space reduction, driven by [`ProxyReduceTactic`].
46///
47/// The protocol is currently (temporarily) for each round of invocation:
48/// `seed_times begin [ next_window reduce_correction* emit ]* finish`
49/// This should be improved to put the `seed_times` in the per-window loop, or remove it entirely.
50pub trait ProxyReduceBackend<B1: BatchReader, B2: BatchReader<Time = B1::Time>> {
51    /// Diff type presented for the input.
52    type RIn: Semigroup;
53    /// Diff type of the output.
54    type ROut: Semigroup + 'static;
55
56    /// Hash keys and associated times in the instance's novel input batches.
57    ///
58    /// This is used (with held times) to seed the interesting times for each key.
59    fn seed_times(&self, instance: &ReduceInstance<'_, B1, B2>) -> Vec<(u64, B1::Time)>;
60
61    /// Initiate a session to create batches for these descriptions, which span `[lower, upper)`.
62    ///
63    /// It is the backend's job to prepare output batches for each of these descriptions.
64    /// The computation proceeds in windows of keys, where only the backend maintains this
65    /// work in progress, until `finish()` is called.
66    fn begin(&mut self, tiles: &[Description<B1::Time>]);
67
68    /// Produce the next window, resticted to `changed[cursor..]`, and update `cursor` to track.
69    ///
70    /// The size of the window is up to the backend, where the window should be large enough to
71    /// amortize the crossings between the harness and the backend. The proxy bridges for the
72    /// whole window will be active at the same time, so tighter windows reduce the required state.
73    fn next_window(&mut self, instance: &ReduceInstance<'_, B1, B2>, changed: &[u64], cursor: &mut usize) -> Option<ReduceWindow<B1::Time, Self::RIn, Self::ROut>>;
74
75    /// A wave of input-output reconciliation, in which the backend supplies necessary edits.
76    ///
77    /// Multiple keys are provided concurrently, for each an accumulated input and tentative output.
78    /// The backend should provide for each key the necessary output updates to bring the output in
79    /// with its desires. The `usize` integers upper bound the range for the corresponding key.
80    fn reduce_corrections(
81        &mut self,
82        keys: &[u64],
83        in_ends: &[usize],
84        input: &[(u64, Self::RIn)],
85        out_ends: &[usize],
86        output: &[(u64, Self::ROut)],
87    ) -> (Vec<(u64, Self::ROut)>, Vec<usize>);
88
89    /// Commit to a collection of updates at a specific batch in progress.
90    ///
91    /// The `tile: usize` indexes the list of descriptions provided to `begin()`, and these updates
92    /// are aimed at that batch in progress.
93    fn emit(&mut self, tile: usize, records: &[((u64, u64), B1::Time, Self::ROut)]);
94
95    /// Complete the session matching `begin`. The outputs correspond to the descriptions it was provided.
96    fn finish(&mut self) -> Vec<B2>;
97}
98
99/// A proxy-space [`ReduceTactic`]: matches input and output records by `key_hash`.
100pub struct ProxyReduceTactic<T, Bk> {
101    backend: Bk,
102    /// Pending interesting times beyond the upper frontier, keyed by key hash.
103    pending: BTreeMap<u64, Vec<T>>,
104}
105
106impl<T, Bk> ProxyReduceTactic<T, Bk> {
107    /// A tactic deferring all value semantics to `backend`.
108    pub fn new(backend: Bk) -> Self {
109        ProxyReduceTactic { backend, pending: BTreeMap::new() }
110    }
111}
112
113impl<B1, B2, Bk> ReduceTactic<B1, B2> for ProxyReduceTactic<B1::Time, Bk>
114where
115    B1: BatchReader,
116    B2: BatchReader<Time = B1::Time>,
117    Bk: ProxyReduceBackend<B1, B2>,
118{
119    fn retire(
120        &mut self,
121        source_batches: Vec<B1>,
122        output_batches: Vec<B2>,
123        input_batches: Vec<B1>,
124        lower: &Antichain<B1::Time>,
125        upper: &Antichain<B1::Time>,
126        held: &Antichain<B1::Time>,
127    ) -> (Vec<(B1::Time, B2)>, Antichain<B1::Time>) {
128        if held.elements().iter().all(|t| upper.less_equal(t)) {
129            return (Vec::new(), held.clone());
130        }
131
132        let instance = ReduceInstance {
133            source_batches: &source_batches,
134            input_batches: &input_batches,
135            output_batches: &output_batches,
136            lower: lower.borrow(),
137        };
138
139        let seeds = self.backend.seed_times(&instance);
140        debug_assert!(seeds.windows(2).all(|w| w[0].0 <= w[1].0), "seed_times must be sorted by key_hash");
141        let mut changed: BTreeSet<u64> = seeds.iter().map(|(k, _)| *k).collect();
142        changed.extend(self.pending.keys().copied());
143        if changed.is_empty() {
144            self.pending.clear();
145            return (Vec::new(), Antichain::new());
146        }
147        let changed: Vec<u64> = changed.into_iter().collect();
148
149        // The output tiling (identical to the Abelian tactic): one tile per held time, keeping
150        // non-degenerate intervals; `tile_of[i]` maps held time `i` to its tile.
151        let held_elems: Vec<B1::Time> = held.elements().to_vec();
152        let (tile_descs, tile_held, tile_of) = tile_descriptions(lower, upper, &held_elems);
153        self.backend.begin(&tile_descs);
154
155        let mut new_pending: BTreeMap<u64, Vec<B1::Time>> = BTreeMap::new();
156
157        let mut cursor = 0usize;
158        let mut ns = 0usize;
159
160        // Retire-wide reusable scratch (cleared per window/round/moment, not reallocated). See the
161        // profiling note on `DiscoverScratch`: fresh per-key/per-round `Vec`s were the dominant cost.
162        let mut discover_scratch: DiscoverScratch<B1::Time, Bk::RIn> = DiscoverScratch::new();
163        let mut states: Vec<KeyState<B1::Time, Bk::RIn, Bk::ROut>> = Vec::new();
164        let mut tile_deltas: Vec<Vec<((u64, u64), B1::Time, Bk::ROut)>> = (0..held_elems.len()).map(|_| Vec::new()).collect();
165        let mut batch_keys: Vec<u64> = Vec::new();
166        let mut in_ends: Vec<usize> = Vec::new();
167        let mut in_all: Vec<(u64, Bk::RIn)> = Vec::new();
168        let mut out_ends: Vec<usize> = Vec::new();
169        let mut out_all: Vec<(u64, Bk::ROut)> = Vec::new();
170        let mut active: Vec<(usize, B1::Time)> = Vec::new();
171        let mut in_accum: Vec<(u64, Bk::RIn)> = Vec::new();
172        let mut cur_out: Vec<(u64, Bk::ROut)> = Vec::new();
173        let mut moments_scratch: Vec<B1::Time> = Vec::new();
174        let mut pended_scratch: Vec<B1::Time> = Vec::new();
175
176        while let Some(window) = self.backend.next_window(&instance, &changed, &mut cursor) {
177            let p_in = &window.input;
178            let p_out = &window.output;
179            super::debug_assert_sorted_bridge(p_in, "next_window.input");
180            super::debug_assert_sorted_bridge(p_out, "next_window.output");
181
182            for deltas in tile_deltas.iter_mut() { deltas.clear(); }
183
184            // Phase 1 (determination): for every key in the window, discover its interesting times
185            // (times only — no accumulation) and stand up its per-moment replays. Peak state is
186            // O(window presentation), bounded by the window `next_window` already materialized.
187            // `states` is a long-lived buffer reloaded slot-by-slot (not cleared/rebuilt): a slot's
188            // `Vec`s and replays are allocated once and reused, so keys cost no per-key alloc/free.
189            // `n_states` is the live prefix this window; higher slots persist (retaining capacity).
190            let mut n_states = 0usize;
191            let (mut is, mut os) = (0usize, 0usize);
192            for &key in &window.keys {
193                while is < p_in.len() && p_in[is].0.0 < key { is += 1; }
194                let i0 = is;
195                while is < p_in.len() && p_in[is].0.0 == key { is += 1; }
196                let i1 = is;
197                while os < p_out.len() && p_out[os].0.0 < key { os += 1; }
198                let o0 = os;
199                while os < p_out.len() && p_out[os].0.0 == key { os += 1; }
200                let o1 = os;
201                while ns < seeds.len() && seeds[ns].0 < key { ns += 1; }
202                let n0 = ns;
203                while ns < seeds.len() && seeds[ns].0 == key { ns += 1; }
204                let n1 = ns;
205
206                moments_scratch.clear();
207                pended_scratch.clear();
208                {
209                    let pending = self.pending.get(&key).map(|p| &p[..]).unwrap_or(&[]);
210                    let seed_times = seeds[n0..n1].iter().map(|(_, t)| t.clone());
211                    let out_times = (o0..o1).map(|o| p_out[o].1.clone());
212                    discover_times(
213                        KeyView { p_in: &p_in[..], i0, i1, pending },
214                        seed_times, out_times, upper,
215                        &mut discover_scratch,
216                        &mut moments_scratch, &mut pended_scratch,
217                    );
218                }
219                if !pended_scratch.is_empty() {
220                    new_pending.insert(key, std::mem::take(&mut pended_scratch));
221                }
222                if moments_scratch.is_empty() {
223                    continue;
224                }
225
226                // Reload slot `n_states` in place (grow the buffer by one only when a window is wider
227                // than any before). `drain` moves the discovered moments in without copy or realloc.
228                if n_states == states.len() {
229                    states.push(KeyState::empty());
230                }
231                let st = &mut states[n_states];
232                st.key = key;
233                st.cursor = 0;
234                st.produced.clear();
235                st.moments.clear();
236                st.moments.append(&mut moments_scratch);
237                st.meets.clear();
238                st.meets.extend(st.moments.iter().cloned());
239                for i in (1..st.meets.len()).rev() {
240                    let m = st.meets[i].clone();
241                    st.meets[i - 1].meet_assign(&m);
242                }
243                st.in_replay.load_iter((i0..i1).map(|i| (p_in[i].0.1, p_in[i].1.clone(), p_in[i].2.clone())), st.meets.first());
244                st.out_replay.load_iter((o0..o1).map(|o| (p_out[o].0.1, p_out[o].1.clone(), p_out[o].2.clone())), st.meets.first());
245                n_states += 1;
246            }
247
248            // Phase 2 (application): walk all keys' moments in ROUNDS. Each round assembles every
249            // active key's one-moment-deep input and current-output accumulations and crosses them in
250            // a SINGLE `reduce_corrections` — batching across keys (a key's own moments stay
251            // sequential, each seeing its earlier corrections via `produced`). This caps the backend
252            // call count at O(max moments over keys), not O(sum of moments), with peak materialization
253            // one moment deep per key. `produced` is meet-collapsed each round, exactly like the
254            // reference — bounded, not the O(times × values) delta history.
255            loop {
256                batch_keys.clear();
257                in_ends.clear();
258                in_all.clear();
259                out_ends.clear();
260                out_all.clear();
261                active.clear();
262                let mut advanced = false;
263                for (si, st) in states[..n_states].iter_mut().enumerate() {
264                    if st.cursor >= st.moments.len() {
265                        continue;
266                    }
267                    advanced = true;
268                    let j = st.cursor;
269                    st.cursor += 1;
270                    let t = st.moments[j].clone();
271                    st.in_replay.step_through(&t);
272                    st.out_replay.step_through(&t);
273                    st.in_replay.advance_buffer_by(&st.meets[j]);
274                    st.out_replay.advance_buffer_by(&st.meets[j]);
275                    for ((_, et), _) in st.produced.iter_mut() {
276                        *et = et.join(&st.meets[j]);
277                    }
278                    crate::consolidation::consolidate(&mut st.produced);
279
280                    in_accum.clear();
281                    for ((vid, et), d) in st.in_replay.buffer().iter() {
282                        if et.less_equal(&t) {
283                            in_accum.push((*vid, d.clone()));
284                        }
285                    }
286                    crate::consolidation::consolidate(&mut in_accum);
287                    cur_out.clear();
288                    for ((vid, et), d) in st.out_replay.buffer().iter().chain(st.produced.iter()) {
289                        if et.less_equal(&t) {
290                            cur_out.push((*vid, d.clone()));
291                        }
292                    }
293                    crate::consolidation::consolidate(&mut cur_out);
294
295                    if in_accum.is_empty() && cur_out.is_empty() {
296                        continue;
297                    }
298                    batch_keys.push(st.key);
299                    in_all.append(&mut in_accum);
300                    in_ends.push(in_all.len());
301                    out_all.append(&mut cur_out);
302                    out_ends.push(out_all.len());
303                    active.push((si, t));
304                }
305                // Terminate only when every key is EXHAUSTED — not merely when this round produced no
306                // crossing. A round can be empty because every key's current moment is empty-gated
307                // while keys still have later (non-empty) moments; breaking here would drop them.
308                if !advanced {
309                    break;
310                }
311                if batch_keys.is_empty() {
312                    continue;
313                }
314
315                let (corr, corr_ends) = self.backend.reduce_corrections(&batch_keys, &in_ends, &in_all, &out_ends, &out_all);
316                let mut cstart = 0usize;
317                for (bi, (si, t)) in active.iter().enumerate() {
318                    let cend = corr_ends[bi];
319                    if cstart != cend {
320                        let idx = held_elems.iter().rposition(|h| h.less_equal(t)).expect("no held capability <= active time");
321                        for (vid, d) in &corr[cstart..cend] {
322                            states[*si].produced.push(((*vid, t.clone()), d.clone()));
323                            tile_deltas[idx].push(((states[*si].key, *vid), t.clone(), d.clone()));
324                        }
325                    }
326                    cstart = cend;
327                }
328            }
329
330            for (held_index, deltas) in tile_deltas.iter_mut().enumerate() {
331                if deltas.is_empty() {
332                    continue;
333                }
334                if let Some(tile) = tile_of[held_index] {
335                    crate::consolidation::consolidate_updates(deltas);
336                    self.backend.emit(tile, &deltas[..]);
337                }
338            }
339        }
340
341        self.pending = new_pending;
342        let produced: Vec<(B1::Time, B2)> = tile_held.into_iter().zip(self.backend.finish()).collect();
343        let mut frontier = Antichain::new();
344        for times in self.pending.values() {
345            for t in times {
346                frontier.insert_ref(t);
347            }
348        }
349        (produced, frontier)
350    }
351}
352
353/// Per-key application state for [`ProxyReduceTactic`]'s round-batched walk: the key's ordered
354/// interesting `moments` and their suffix `meets`, its input and output replays (meet-collapsed),
355/// the corrections `produced` this round so far, and a `cursor` into `moments`. Held for all of a
356/// window's keys at once so each round's crossing batches across keys — a key's own moments stay
357/// sequential (each sees its earlier corrections via `produced`), but distinct keys are independent.
358struct KeyState<T, RIn, ROut> {
359    key: u64,
360    moments: Vec<T>,
361    meets: Vec<T>,
362    in_replay: IdHistory<T, RIn>,
363    out_replay: IdHistory<T, ROut>,
364    produced: Vec<((u64, T), ROut)>,
365    cursor: usize,
366}
367
368impl<T: Timestamp + Lattice, RIn: Semigroup, ROut: Semigroup> KeyState<T, RIn, ROut> {
369    /// An empty slot, to be filled by [`ProxyReduceTactic`]'s phase 1 (`reload`-style). The `states`
370    /// vector holds these across windows and reloads them in place, so a key's buffers are allocated
371    /// once (per slot) and reused — never dropped per key (which was ~18% of load in `free`).
372    fn empty() -> Self {
373        KeyState { key: 0, moments: Vec::new(), meets: Vec::new(), in_replay: IdHistory::new(), out_replay: IdHistory::new(), produced: Vec::new(), cursor: 0 }
374    }
375}
376