Skip to content

Stamps at the batch-to-records seams, leave_dynamic under multi-element stamps, and unconsolidated feedback - #855

Open
frankmcsherry wants to merge 6 commits into
master-nextfrom
dd-dynamic-stamps
Open

Stamps at the batch-to-records seams, leave_dynamic under multi-element stamps, and unconsolidated feedback#855
frankmcsherry wants to merge 6 commits into
master-nextfrom
dd-dynamic-stamps

Conversation

@frankmcsherry

@frankmcsherry frankmcsherry commented Sep 2, 2026

Copy link
Copy Markdown
Member

Two bugs found while running DDIR programs in steady state, the seam that let the first one through, and the driver edit mode and corgi pin that surfaced them.

Where multi-element stamps come from, and where they were leaking. Since timely #813 a message's stamp is a multiset, and DD's batch-shipping operators make multi-element ones: arrange ships each batch under the set of capabilities it retires, reduce likewise, join forwards its input batch's set. In an iterative scope with two epochs in flight, an arrange holding (1, [18]) (epoch 1, round 18) and (2, []) (epoch 2, just entered) retires both in one batch when its input frontier passes both at once. Instrumenting the operators on the run that panicked (a DDIR program at 20k nodes, four workers, epochs fed without waiting) shows the chain: an arrange minted the first multi-element stamp, then a join, then a reduce, then an arrange whose batch reached the scope's exit as (1, [19]), (2, [1]).

The leak was the seam from batches to records: as_collection forwarded a batch's records under the batch's set, so a collection carried messages whose capability spanned two epochs though no record did, and anything reading a message's time as one, cap.time(), panicked with "expected a singleton stamp". StampRouter (collection.rs) now gives one capability per element of a message's stamp and routes each record to the first element at or before its time; it is applied at every seam that makes records from batches: the vec as_collection, count, threshold, the columnar as_recorded_updates, and DDIR's corgi as_collection. With that, collection streams carry one timestamp per message and only batch streams carry sets. Not yet routed: join_with_tactic's output, whose tactics build containers the operator cannot split (the fix belongs in the tactics, a builder per element), and as_container (opaque containers, no users found). tests/dynamic.rs checks the seam with a hand-built batch under two epochs' capabilities.

leave_dynamic itself. All three (DD's row and columnar collections, DDIR's corgi backend) read cap.time() to truncate it. Each now holds a capability per stamp element, truncated exactly as the records are, and opens its session on the set, so it is safe whatever reaches it; a test makes the two-epoch message directly, panics before the fix and passes with it. An audit of DD, DDIR, and dogsdogsdogs found no other InputCapability::time() reads.

Unconsolidated feedback. A DDIR var was set to its step unconsolidated. An iteration quiesces when its feedback carries no records, not when it carries nothing net, so a step whose additions and retractions cancel only logically (a negate under a concat) never quiesced: the vec backend ran forever on a program whose fixpoint was reached in one round. Every program in the suite fed its vars through a reduce, which consolidates by nature, so nothing had seen it. Each var is now set to its consolidated step (Backend::consolidate, through the arrangement by default); the suite's timings are unchanged. The regression test is DD's identifiers algorithm written as a DDIR program (interactive/tests/identifiers.rs): hash, min, negate, and a var give 32 records unique ids over 64 slots.

The ddir driver's rounds edit file-fed rows. With EDGES_FILE, the round loop used to churn synthetic rows the dataflow never saw. Each update now retracts one of the file's rows and re-inserts it with its last field rewired, so a round is an edit to data the dataflow holds.

corgi pinned at WIP 3c4b0d6f, where compare_idx and gather read sum discriminants in place (43s -> 23s on a 200k-node saturation).

🤖 Generated with Claude Code

https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK

frankmcsherry and others added 4 commits September 2, 2026 05:43
Both the row and the corgi `leave_dynamic` read `cap.time()`, which now
panics ("expected a singleton stamp") when a message carries timestamps
from two epochs — late iterations of one alongside early ones of the next,
which any steady-state run of an iterative program produces. Hold a
capability per stamp element, each truncated exactly as the records are,
and open the session on the set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
The round loop churned synthetic rows from `gen_row`, which with a file-fed
input meant retracting rows the dataflow never saw and inserting random
ones (duplicate ids, cycles). Each update now retracts one of the file's
rows and re-inserts it with its last field rewired below its first, so a
round is an edit to data the dataflow holds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
compare_idx and gather read sum discriminants in place (WIP master
3c4b0d6f), which is what took the e-graph's 200k-node saturation from 43s
to 23s on the corgi backend.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
…a program

An iteration quiesces when its feedback carries no records, so a var whose
step's additions and retractions cancel only logically — a `negate` under
a `concat` — never quiesced: the vec backend ran forever on a program
whose fixpoint was reached in one round. Every var is now set to its
consolidated step (`Backend::consolidate`, through the arrangement by
default). The suite's timings are unchanged; the programs there all fed
their vars through a reduce, which consolidated by nature.

tests/identifiers.rs writes DD's `identifiers` algorithm in DDIR: each
record proposes hash(round, record); among the proposers of an id the
least (round, record) wins and the losers try the next round; with hash,
min, negate and a var, over 64 slots for 32 records, every record gets
its own id (25 settle at round 0, the rest by round 4). It is the
primitive an e-graph rule needs to mint a node for a new term inside the
program.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
@frankmcsherry frankmcsherry changed the title leave_dynamic under multi-element stamps, a driver edit mode, and the corgi pin Two DDIR footguns: leave_dynamic under multi-element stamps, and unconsolidated feedback Sep 2, 2026
frankmcsherry and others added 2 commits September 2, 2026 08:01
…messages come from

Instrumenting the batch-shipping operators on the run that panicked (a
DDIR program at 20k nodes, four workers, epochs fed without waiting) shows
the multi-element stamps being minted: an arrange retiring (1, [18]) and
(2, []) — epoch 1 at round 18 alongside epoch 2 just entered — in one
batch, then a join forwarding its input batch's set, then a reduce
retiring the same pair, and finally an arrange whose batch, forwarded by
as_collection under the retiring set and delayed element-wise by the
feedback, reached the scope's exit as (1, [19]), (2, [1]).

Which retirements coincide depends on scheduling, so the test makes the
message directly: an operator that ships its input under a capability for
epoch e round 3 and one for epoch e + 1 round 0, into leave_dynamic. It
panics with "expected a singleton stamp" before this branch's fix and
passes with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
…ount, threshold route by time

A batch is shipped under the set of capabilities it retires, so a message
of batches carries several timestamps when two epochs retire together.
The operators that turn batches back into records forwarded the records
under that same set, which is how a multi-element stamp reached
`leave_dynamic` and anything else that reads a message's time as one.

`StampRouter` (collection.rs) gives one capability per element of a
message's stamp and routes each record to the first element at or before
its time (a batch's times are at or beyond one of the capabilities it
retired under, so every record has one). It is applied at every seam
that makes records from batches: the vec `as_collection`
(`flat_map_batches`), `count`, `threshold`, the columnar
`as_recorded_updates`, and DDIR's corgi `as_collection` (a chunk under one
capability goes whole, as before; under several it is split by row). With
that, a collection's messages carry one timestamp each; only streams of
batches carry sets.

Not routed: `join_with_tactic`'s output, whose tactic builds containers the
operator cannot split by time (the fix belongs in the tactics, a builder
per element of the fresh batch's set), and `as_container`, which forwards
opaque containers. `leave_dynamic` keeps holding a capability per element,
so it is safe either way.

tests/dynamic.rs: a hand-built batch under capabilities for epoch 0 round
3 and epoch 1 round 0, through `as_collection` into an operator that reads
each message's time as one, lands its records in their two epochs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012k2GSwxmvD2LvckkoXi6GK
@frankmcsherry frankmcsherry changed the title Two DDIR footguns: leave_dynamic under multi-element stamps, and unconsolidated feedback Stamps at the batch-to-records seams, leave_dynamic under multi-element stamps, and unconsolidated feedback Sep 2, 2026
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