Skip to content

Token-generic, windowed int-proxy tactics, with reference backends and benchmarks - #808

Open
frankmcsherry wants to merge 18 commits into
TimelyDataflow:master-nextfrom
frankmcsherry:int-proxy-generic-tokens
Open

Token-generic, windowed int-proxy tactics, with reference backends and benchmarks#808
frankmcsherry wants to merge 18 commits into
TimelyDataflow:master-nextfrom
frankmcsherry:int-proxy-generic-tokens

Conversation

@frankmcsherry

@frankmcsherry frankmcsherry commented Jul 23, 2026

Copy link
Copy Markdown
Member

Generalizes the int-proxy tactics from u64-only proxies to backend-chosen tokens and brings both tactics to windowed streaming operation. This is the substrate for the Corgi/DDIR work (#795): its columnar backends are the intended first real consumers, and this PR reshapes the protocol they port onto. Validation here is the harness's own property suite plus measurements recorded below; end-to-end validation lands with the Corgi gate. Both proxy tactics measured faster than the cursor tactics on identical OrdValBatch storage (join 0.90–0.95×, reduce 0.58–0.70×).

Tokens (70d7e288)

ProxyBridge<G, I, T, R>; backends choose Group: Copy + Ord + 'static (the granule name — commonly a u64 key hash, exactly the key for small Copy keys, in which case collisions are impossible) and per-side Token: Copy + Ord (invocation-scoped value tokens). The time-logic library beneath (ValueHistory, bilinear_wave, discover_times) was already generic at this bound; u64 was one instantiation posing as the architecture. 'static sits exactly on the one token that crosses invocations, encoding the memory rule in types.

Streaming protocols (e197207f, b52cac66, 3945cb8e)

  • JOIN replaces present0/present1 + eager whole-instance staging with next_window (bounded group-range presentations of both sides) driven through a genuinely lazy JoinUnit — the driver already drained units under fuel across activations; the tactic now delivers a lazy one. Matches flow one at a time to the backend's absorb, which cuts output containers at its own granularity; flush yields the remainder. No match is ever staged in the harness.
  • REDUCE loses the retire-wide seed_times staging its own doc comment apologized for: seeds arrive per window on ReduceWindow, and next_window receives the pending groups (all the harness knows that the backend does not). Peak state is O(window) for both tactics.
  • Per-unit state has both halves: Cursor (read progress — a resumption state richer than the usize the first draft assumed) and Sink (output staging). Neither can live on &mut self: half-drained units from both input queues interleave against one shared backend. The Sink half fixes a real bug the review found in the first absorb draft — shared staging shipped one unit's matches in another unit's containers, under the wrong capability. The interleaving regression test pauses a unit mid-stream holding a straddling partial container, and was verified to fail against the bug shape.
  • Spent windows return to the backend through next_window(..., reuse) for capacity reclamation.

Harness contract enforcement

The harness now hard-asserts (not debug_asserts) the window contract clauses whose violation is silent corruption rather than a crash: strictly ascending windows (shared checker), pending-group coverage, no stray/trailing records or seeds outside window.keys, and per-window progress. Join's fresh-side coverage clause is documented as uncheckable (the harness is data-oblivious); the reference backends and oracle tests are its enforcement.

Single-moment fast path (9a7a1262)

When a key has no pending times, one distinct seed time, and history at-or-before it, the harness evaluates directly from the bridge slices — no discovery, no replays — with one batched crossing per window. This is the steady-state snapshot shape, and it was the dominant per-key cost. Equivalence is argued against discover_times (under the guard: moments = [t*], nothing pended) and checked by a deterministic fuzz test mixing fast- and slow-path keys against an exact per-time oracle; a multi-time test keeps the slow path exercised.

Property tests (tests/int_proxy.rs)

One consolidated file of oracle-backed property tests over the value-token instantiation (G = src exact, token = the edge): join vs a naive nested-loop oracle (including a fat-key case that runs the ≥16×≥16 bilinear_wave path, which no small-key test reaches); reduce vs exact per-time counting, including a randomized differential fuzz mixing fast- and slow-path keys; the partial-order pending contract (incomparable Pair times whose join lies beyond the frontier — pended, carried, re-windowed as a pending-only group, corrected next retire, checked on a grid oracle); and interleaved-unit isolation (which pinned the Sink bug, and is verified to fail against the bug shape). Tests that demonstrated rather than checked — including a 700-line [u8]-rows recipe backend — were pruned from the tree per volume discipline; they live in the branch history (05c48113), and the compressing-token instantiation's collision handling is a noted coverage gap for a slimmer future test.

Benchmarks (c1b99551, a36dda82; harness pruned from the final tree, recoverable from those commits)

Against hand-written natives over the same batches (the floor), and against the shipping cursor tactics on identical OrdValBatch storage through the same cursors (the decision-relevant comparison; the cursors modules were public while the bench drove them, re-privatized with its pruning). 100k keys, fanout 4, release:

vs native floor vs cursor incumbent
join 1.40× 0.95× (0.90× at fanout 1)
reduce ~12× (vs a non-incremental count) 0.58×

The reduce advantage is the fast path — an optimization that was a few slice scans here and would be an intrusive rewrite in interleaved cursor navigation, which is the decoupling argument in miniature. The remaining join gap to the native floor is the bridge presentation copy (owned, consolidated, harness-checkable presentations), left as a deliberate open item.

In-dataflow validation (recorded in branch history)

An SCC example ran the proxy join inside the stock SCC dataflow (nested iteration, Product times, many fuel-interleaved units) at parity with join_core — load 4.28s vs 4.35s, rounds ~2.67s vs ~2.71s at 100k/200k — oracle-validated for exact agreement, including under the #801 trigger shape (staggered labels, 3 workers, the regression suite's sizes). It was removed from the final tree (1bc07522, ed19240e hold it and its findings): the Corgi differential gate is the stronger version of that check, and this PR defers end-to-end validation to it.

Migration notes for the #795 backends

The Corgi backends bind the pre-reshape protocol; the port is mechanical:

  • Tokens are typed: ProxyBridge<G, I, T, R>; backends declare Group: Copy + Ord + 'static and Token: Copy + Ord (join: Token0/Token1 per side). Corgi's value-as-id / content-hash ids become Group = u64, Token = u64 verbatim.
  • Reduce: seed_times is gone — each ReduceWindow carries its own seeds (novel-batch time support for its keys, sorted by group; the seeding-provenance contract is documented on the field). next_window(&mut self, instance, pending, reuse)pending is the harness's carried groups (must be covered, in group order, even with no novel input); reuse returns the spent window for clear()-and-refill. Progress within a retire lives on &mut self (synchronous begin/finish bracket), as before.
  • Join (if ported from the bespoke tactic to the seam): next_window(&mut self, instance, fresh, cursor, reuse) with type Cursor: Default per-unit resumption state; cross is replaced by absorb(&mut self, instance, sink, left, right, time, diff) -> Option<Output> + flush(&mut self, instance, sink) with type Sink: Default per-unit output staging. Neither Cursor nor Sink state may live on &mut self: half-drained units interleave against the shared backend (a shared sink ships one unit's matches under another's capability — the bug class the property suite pins). Container granularity is the backend's (JOIN_CHUNK left the harness).
  • The harness now hard-asserts (release too): strictly ascending windows, per-window progress (no empty windows — skip fully-cancelled ranges internally), pending coverage, and no stray/trailing bridge records or seeds outside window.keys. A port that violates the contract fails loudly rather than corrupting.
  • Fast path: keys with no pending, one distinct seed time, and history at-or-before it are evaluated by the harness directly from the bridge slices — backends need do nothing, but snapshot-shaped retires get cheaper (measured 48×→20× vs floor; 0.58× vs the cursor tactic).

Every increment was passed through adversarial review (eight finder/verifier agents across two passes); the review's findings — including the Sink bug and a blind regression test — are fixed in-branch.

🤖 Generated with Claude Code

frankmcsherry and others added 18 commits July 22, 2026 16:36
The proxy tactics fixed both bridge coordinates at u64; the time-logic
library beneath them (ValueHistory, bilinear_wave, discover_times) was
already generic at V: Copy + Ord, so u64 was an instantiation posing as
the architecture. This makes the tokens backend-chosen associated types:

* `Group: Copy + Ord + 'static` on both backend traits: the granule
  name, commonly a u64 key hash but exactly the key for small Copy keys
  (collisions then impossible), wider hashes for insurance. `'static`
  because groups are the one token that crosses invocations (pending
  times are held per group).
* `Token: Copy + Ord` (per input side on join: Token0/Token1): the
  ephemeral value token, scoped to one invocation.
* `ProxyBridge<G, I, T, R>`; `bilinear_wave` widened to distinct value
  types per side; `KeyView`, `DiscoverScratch`, `discover_times`, and
  `IdHistory` generalized to match.

Tests are invariant under (u64, u64); no in-tree backend impls existed
to migrate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
JOIN materialized whole-instance bridges (present0/present1) and all
outputs eagerly, returning a Vec costumed as Box<dyn Iterator>; REDUCE
was windowed but staged seed_times for the entire invocation up front
(its own doc comment asked for the fix). Both tactics now stream in
ascending group order with peak state O(window):

* JOIN: present0/present1 are replaced by next_window, yielding a
  JoinWindow of both inputs' presentations for a contiguous group
  range. Units are genuinely lazy: JoinUnit owns its batches and
  produces outputs window by window as the join driver's fuel drains
  it — the driver already held units across activations, the proxy
  tactic just never delivered a lazy one. The backend sits behind
  Rc<RefCell<_>> since half-drained units from both input queues
  interleave; per-unit progress lives in the unit's opaque cursor,
  never on the backend. Fresh is now Copy.

* REDUCE: seed_times is deleted; seeds arrive per window on
  ReduceWindow (novel-batch time support, with the discover_times
  seeding contract documented on the field). next_window now takes the
  pending groups (all the harness knows that the backend does not) and
  must cover novel-input groups union pending; a debug_assert checks
  all pending groups were covered. The empty-invocation fast path
  checks batch lengths instead of staged seeds.

No in-tree backends existed to migrate; validation is compile-level
until the reference backends land (next step).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The value-token instantiation from the seam design as a behavioral test
(tests/int_proxy_graphs.rs): edges (src, dst) grouped by source, with
Group = u32 the source itself (exact, no hashing) and the whole edge as
the value token (self-redeeming: cross and finish build outputs from
tokens alone). Oracle tests drive both tactics directly — join against
a naive nested-loop join, reduce (per-source counts, with corrections
and full retractions) against per-time accumulation — at window sizes
down to one group per window.

Writing the backend surfaced two protocol corrections, applied here:

* JOIN: the per-unit resumption state is now `type Cursor: Default` on
  the backend, owned by the unit. A single usize could not carry real
  resumption state (a position per batch per side), and unit progress
  cannot live on `&mut self` since half-drained units interleave.

* REDUCE: next_window loses its cursor parameter entirely — a retire's
  windows all happen within one synchronous begin/finish-bracketed
  call, so the backend keeps progress in its session state. The
  pending-coverage check moves to the harness, which now verifies
  ascending windows and that no pending group is skipped, from
  window.keys alone.

Not yet exercised: the pending path (carried interesting times), which
requires partially ordered time to arise; that belongs to the
partial-order test alongside the hash and ordinal backends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests/int_proxy_bytes.rs: the compressing-token instantiation over
opaque [u8] rows of unknown shape, stored columnar (bytes + bounds +
aligned time/diff columns) with a stored (G, I) token column computed
at formation — presentation is a column read, never a re-hash. Group
collisions are *resolved*, not detected: join's cross re-checks real
key bytes and drops false pairs; reduce partitions each granule by real
key and reduces the parts separately. Weak-hash tests force both
collision kinds: value collisions panic at their T1 site (formation,
redemption, stash); group collisions must yield byte-identical output
to the naive oracle.

A six-angle adversarial review of the branch produced fixes applied
here:

Harness contract completeness:
* Stray records/seeds — anything presented for a group absent from
  window.keys was silently discarded by the skip loops; now hard
  asserts (per-key strays-below, end-of-window trailing).
* Empty-window progress guards: a backend returning Some with nothing
  presented used to hang the retire loop / scheduler; now asserted.
* Ascending-window watermark: one shared checker (assert_ascending_
  window) replaces two divergent inline copies; pending-coverage and
  watermark checks upgraded from debug_assert to hard asserts — the
  failure class (dropped carried times, released capabilities, lost
  matches) is silent corruption, and the checks are O(1)/window.
* Join's fresh-coverage clause documented as uncheckable (the harness
  is data-oblivious); missing it silently loses matches.

Indirection-tax fixes in JoinUnit:
* Yields per key rather than per window: ready holds at most one key's
  containers instead of a whole window's cross product.
* Match buffers and replay histories hoisted onto the unit; cross now
  takes slices, ending the mem::take capacity destruction each flush.
* Reduce phase 2 sweeps a live-key list with swap-remove rather than
  rescanning exhausted slots under moment-count skew.

Reference-backend upgrades:
* Fresh-side driving restored (the old present0-then-filter behavior):
  both join backends window over the fresh side's groups and skip the
  accumulated side's others — O(fresh) presentations, empty fresh side
  free.
* Interleaved-units test: two half-drained units alternately polled
  against one shared backend, the scenario that motivated type Cursor.
* started flags replaced by idempotent resize; RowBatch::form now uses
  consolidate_updates; test-grade patterns (next_group rescan, redeem
  by search) labeled with the real-backend alternative.

Deferred with notes: window-buffer recycling in the protocol, scratch
pooling vs the Token 'static bound (both for SEAM.md open items).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
examples/int_proxy_bench.rs measures the indirection tax where it is
purest: value tokens (G = src, token = edge), so every gap is protocol,
not tokenization. Hand-written merge-join and streaming count over the
same batches are the baselines. Release mode, best of 5.

Numbers at 100k keys, fanout 4 (400k edges/side), window 1024:

  join    native  7.5ms   proxy 16.9ms   2.26x
  reduce  native  0.36ms  proxy 17.3ms   ~48x

Localization by sweeps:

* Window size (64 / 1024 / 65536): join flat at 2.3-2.7x; reduce flat
  through 1024 and WORSE at 65536 (35ms — oversized phase-2 state).
  Harness crossings are not the bottleneck at sane window sizes.
* Values per key (fanout 1 / 4 / 16): reduce per-key cost 85ns -> 173ns
  -> 632ns. The tax scales with replay volume: per-key ValueHistory
  loads, TimeHistory sorts, and discovery machinery — running on data
  with exactly ONE moment, where discovery is pure overhead.

Reading: the join tax (~2.3x) is bridge materialization plus output
allocation — the window-buffer recycling and resumable-wave items on
SEAM.md's open list. The reduce tax is dominated by running the full
interesting-times apparatus per key on single-moment granules; the top
optimization candidate is a single-moment fast path (after advancing,
all times equal -> skip discovery and replay, evaluate once, subtract
output). Native count is also not an incremental operator, so the fair
gap is smaller than 48x — but the snapshot case is common enough that
the fast path should exist before this number is taken at face value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tests/int_proxy_pending.rs exercises the last unexercised clause of the
windowed reduce contract: carried interesting times. Updates at
incomparable Pair times (0,1) and (1,0) make their join (1,1) an
interesting time beyond the retire's upper frontier; the test asserts
it is pended (returned in the frontier, holding the capability),
carried by the tactic, and handed back as a pending group next retire —
where it must appear in a window with no novel input, merged in group
order with a novel-only group at window size 1. A grid oracle over the
partial order checks exact per-key counts at every probe, confirming
the deferred correction collapses the two partial counts. The hard
pending-coverage asserts run live throughout.

tests/common/mod.rs extracts the shared scaffold the review prescribed
(EdgeBatch<T>, the value-token join/reduce backends generic over the
timestamp, Pair itself) before this third test could become the third
copy; int_proxy_graphs.rs now uses it. The byte-rows test keeps its own
scaffold deliberately — its columnar layout is the point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bench localized the reduce tax to per-key discovery/replay running
on granules with exactly one interesting moment — the steady-state
snapshot shape. This adds the safe fast path: when a key has no pending
times, exactly one distinct seed time t*, and every presented input and
output time at-or-before t*, then discover_times would compute moments
== [t*] with nothing pended (history times before t* pass without
becoming moments; no join can escape t*), so the key is evaluated
directly from its bridge slices — no TimeHistory loads, no ValueHistory
replays, no KeyState. Fast keys batch into one window-wide
reduce_corrections call (round zero); their corrections go straight to
the tiles. Keys with no seeds and no pending are skipped outright.

Bench (100k keys, window 1024, release, best of 5):
  fanout 4:  17.3ms -> 7.6ms   (48x -> 20x vs native count)
  fanout 1:   8.5ms -> 5.2ms   (24x -> 11.5x)

Remaining cost is presentation-side (whole-window consolidate_updates
sorts in the backend, bridge allocation) — the open buffer-recycling
and k-way-merge items, not harness machinery.

tests/int_proxy_graphs.rs gains reduce_multitime_rounds: two distinct
times per retire under total order, so the slow path stays exercised
(a two-seed key cannot take the fast path) and fast and slow keys mix
within one window. The partial-order pending test covers the other
slow-path shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
reduce_fuzz_matches_oracle: deterministic LCG-driven rounds of random
insertions and retractions over a small key space, times randomized
within each round's width-3 interval so every round mixes fast-path
keys (one distinct time) with slow-path keys, at window sizes 1/3/100,
checked against exact per-time counting after every round. This is the
empirical half of the fast-path equivalence argument (the analytic
half: under the guard, discover_times computes moments == [t*] with
nothing pended, and the round-zero accumulation equals the slice
consolidation). Retractions draw only from prior rounds' insertions so
every prefix accumulation stays non-negative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-written natives are a floor; the decision-relevant comparison
is against the shipping cursor tactics on the same real storage. The
bench now builds OrdValBatch inputs and consumes them two ways: the
cursor tactics directly, and the proxy tactics through backends that
present from those same batches via the same cursors — identical
storage access, so the difference is pure protocol.

The cursors modules in join.rs/reduce.rs become public (the tactic
entry points already were; these are the incumbent reference
implementations, and benches and differential tests drive them
directly).

Results (100k keys, window 1024, release, best of 5):

  join    fanout 4:  cursor 14.4ms   proxy 21.6ms   1.50x
          fanout 1:  cursor  3.4ms   proxy  3.9ms   1.16x
  reduce  fanout 4:  cursor 12.0ms   proxy  8.4ms   0.70x
          fanout 1:  cursor  6.8ms   proxy  5.6ms   0.83x

Reading: against the incumbent (rather than the non-incremental native
floor) the join protocol costs 1.2-1.5x — the bridge copy and staging
double-write, partially offset by the proxy's batch-at-a-time group
processing. The proxy REDUCE is 17-30% FASTER than the cursor
incumbent: the single-moment fast path skips per-key interesting-time
machinery the cursor path always runs. The decoupling is not a tax on
reduce at all on snapshot-shaped work; it is currently a modest tax on
join with identified, protocol-level fixes queued (buffer recycling,
cross-by-emit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two measured per-record taxes in the join protocol, removed:

* cross (staged slices) becomes absorb + flush: each match goes
  straight from the merge to the backend, which accumulates into its
  own output representation and yields containers at its own size
  target (JOIN_CHUNK leaves the harness — container granularity is the
  backend's decision). The li/ri/ot/od staging buffers are deleted
  outright: the double-write goes away, not just its allocation.
  join_key emits through a closure; the wave path feeds absorb
  directly.

* next_window (both tactics) gains a reuse parameter returning the
  previous, fully-processed window, so backends clear() and refill
  with retained capacity instead of allocating fresh bridges per
  window — the steady-state allocation churn item from the review.

Bench movement (100k keys, window 1024, release, best of 5):

  join vs native:            2.7x  -> 1.49x  (fanout 4)
  join vs cursor incumbent:  1.50x -> 0.94x  (fanout 4)
                             1.16x -> 0.90x  (fanout 1)
  reduce vs cursor:          0.70x -> 0.68x

Both proxy tactics are now FASTER than the shipping cursor tactics on
identical OrdValBatch storage. The remaining gap to the hand-written
native floor (1.49x) is the bridge presentation copy itself, which is
the present-by-reference / lens territory deliberately parked in
SEAM.md.

All backends updated (graphs/common, bytes, bench x2); the bytes
absorb keeps the group-collision key re-check per match. Tests
unchanged in behavior; test backends use small container targets to
exercise multi-container yields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The adversarial review of the absorb/flush protocol found a real bug
in it: staged output lived on the backend, which all live units share
(Rc<RefCell>, and the driver interleaves half-drained units under
fuel). Unit A could pause holding a partial container in the shared
buffer; unit B's next container would then ship A's matches — under
B's capability, into B's output stream — and a unit dropped at
shutdown would leave residue poisoning every later unit.

The fix is the write-side twin of the Cursor finding: `type Sink:
Default` on ProxyJoinBackend, owned by the unit, passed to absorb and
flush. Per-unit state now has both halves — read progress (Cursor) and
output staging (Sink) — and neither can live on `&mut self`.

The review also caught that the interleaving regression test was
blind: with target = 1 staging empties at every yield, so the bug
shape passes. The test now uses target = 2 with a 3-match key, so a
unit pauses mid-stream holding a straddling partial container; the
bug shape was temporarily re-injected to confirm the test fails
against it, then removed.

Also from the review: docs reconciled to the absorb/flush protocol
(the trait narrative and the bytes/graphs module headers still named
`cross`; the container-boundary parity clause is now stated on absorb;
JoinUnit's peak-state claim now counts one key's containers in
`ready`); reuse vecs route back to the side they served (they
alternated duties under Fresh::Input1, growing both to max capacity);
two rustdoc links introduced by publishing `cursors` fixed; bench
reduce backends hoist a per-key allocation off the proxy side.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
examples/scc_proxy.rs runs the stock SCC computation with trim_edges'
two joins substituted through join_with_tactic and a value-token proxy
backend over the same ValSpine arrangements (Group = the key itself,
tokens = the values; time-generic, so it runs at the nested
Product<usize, usize> of the iterative scope). propagate and all other
stages are stock; SCC_VALIDATE=1 runs stock strongly_connected
alongside and asserts exact agreement at every time (verified through
load plus incremental rounds).

This is the proxy tactics' first run inside a real dataflow, and a
deliberately hostile one: nested iteration, Product timestamps,
regions, and many fuel-interleaved lazy units live at once — the
scenario the unit-owned Cursor/Sink state exists for. The harness's
hard asserts (ascending windows, progress) ran throughout.

Numbers (1 worker, release, batch 1000, 5 rounds), stock vs proxy:

  100k nodes / 200k edges:  load 4.35s vs 4.28s; rounds ~2.71s vs ~2.67s
  200k nodes / 400k edges:  load 10.33s vs 10.04s; rounds ~4.38s vs ~4.31s

Parity with a consistent slight edge — as the microbenchmarks predict
(join at 0.90-0.95x of the cursor tactic) once diluted by the rest of
the dataflow. One comparison hazard worth recording: an earlier draft
accidentally used propagate_at's label-staggering variant and appeared
7-20x faster; the numbers above use |_| 0 exactly as stock, and the
validation mode exists so that mistake class gets caught by an oracle
rather than by suspicion.

The backend needed no cursor-bound vocabulary at all: plain Data
bounds and the projections normalize. (Asserting redundant Key<'a> =
&'a K equalities actually breaks the trait solver — deleting them was
the fix.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The PR carried ~2.7k lines of tests and benches against a ~280-line
net library change. Pruned by the criterion: keep tests that check
general properties against oracles, drop demonstrations and anything
that locks in current behavior.

Removed:
* examples/int_proxy_bench.rs — a benchmark, not an example. Its
  numbers and their analysis are recorded in the branch's commit
  messages; the harness is recoverable from history when next needed.
* tests/int_proxy_bytes.rs — the [u8]-rows recipe was a demonstration
  wearing a test costume. Its genuinely general content (collision
  resolution under compressing tokens) goes with it for now; the
  hashed-token instantiation is currently uncovered, noted as a gap
  for a slimmer future test.

Consolidated tests/{common/mod.rs, int_proxy_graphs.rs,
int_proxy_pending.rs} into one tests/int_proxy.rs (821 lines) keeping
the property tests: join vs a naive oracle, reduce vs exact per-time
counting, the randomized differential fuzz, the multi-time slow-path
oracle, the partial-order pending contract, and interleaved-unit
isolation (which pinned the Sink bug and is verified to fail against
the bug shape).

examples/scc_proxy.rs stays: it is an actual example (how to write a
backend and mount the tactic in a dataflow), and its SCC_VALIDATE
oracle is the deepest integration check we have.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…at proxy joins

Audit of whether the int-proxy join carries the bug fixed in TimelyDataflow#802
(in-flight batches overtaken by advance_upper, silently dropped):

* The bug lived in join_with_tactic's driver — the acknowledged-
  frontier admission guard — which both tactics share, and this branch
  is based past the fix (59dfd3d, f8f1645); its only diffs to
  join.rs are a derive and module visibility. The proxy path inherits
  the fix.
* The tactic-level assumptions survive the fix's new admission rule:
  `meet` is the timely message capability, which lower-bounds the
  fresh batch's times on the overtaken path exactly as elsewhere
  (overtaking changes which batches are admitted and what the opposing
  trace reads through, not the capability relationship), so the
  backend's advance-by-lower algebra and the exactly-once argument
  (driver-level, tactic-agnostic) are unaffected.
* Empirically: SCC_STAGGER=1 switches both the proxy trim and the
  oracle to the prioritized strongly_connected_at shape (labels
  introduced in rounds — the TimelyDataflow#801 trigger), and the oracle-validated
  runs at the regression suite's sizes (10/20 x 1000 single-edge
  rounds, 100/200 x 10, 100/2000) with three workers all pass: exact
  agreement at every time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
They were made public solely so the (since pruned) benchmark could
drive them directly; with that gone, nothing outside the crate names
them, and the public surface shrinks back. The intra-doc link fix
stays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This PR's role is the protocol substrate for the Corgi/DDIR work
(TimelyDataflow#795): the reshaped int-proxy tactics that its columnar backends
port onto. Under that lens the SCC example's job — in-dataflow
validation against a real backend — is superseded by the Corgi
backend's differential gate (whole .ddp programs vs the vec
reference), which is the stronger version of the same check.

The example's findings stay on the record in its commits (1bc0752,
ed19240): proxy joins at parity with join_core end-to-end,
oracle-validated including under the TimelyDataflow#801 trigger shape at the
regression suite's sizes. What remains in-tree is the harness's own
property suite (tests/int_proxy.rs), which guards the contract the
Corgi port will code against and is the one thing the downstream gate
does not cover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two items from downstream (TimelyDataflow#795) review of the substrate:

* retire's final zip would silently truncate a backend returning the
  wrong number of batches from finish(), shipping batches at the wrong
  held times. Now a hard assert (one batch per tile description),
  consistent with the harness's fail-loudly posture.

* join_key's >=16x>=16 bilinear_wave path had no in-tree exercise —
  every key in the property suite was small, and the SCC example that
  covered the path was pruned. join_wave_path_matches_naive builds one
  deliberately fat key (20 presented records per side: spread-time
  insertions plus non-cancelling retractions, so consolidation keeps
  all of them) alongside a small key, so both join_key paths run in
  one unit, against the naive oracle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review feedback: the unit had become a flat 13-field bag with no
visible organization — one field accreted per protocol change, none of
them prompting reorganization. The grouping that was missing is the
protocol itself, so the type now displays it:

* Task — the immutable work description (batches, fresh side,
  capability time), with the JoinInstance construction as its method.
* The shared backend and the two per-unit state halves it interprets:
  cursor (read resumption) and sink (output staging).
* Phase — the window machine, previously three fields in a trench coat
  (done + flushed + current: Option): Fetch, Merge(window, i, j),
  Drained, Spent. next() is now a match over it, and the
  can't-flush-twice property is the state graph rather than a boolean
  to reason about.
* spent + high — the harness's reuse stash and ascending-window
  watermark; h0/h1 merge scratch; ready the output queue.

WindowFor<B0, B1, Bk> names the bridge type that was previously
spelled out six-parameters-long at each use. No behavioral change;
the suite is the witness.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant