Skip to content

UnloadChunk: bulk probe extraction for chunk batches#782

Open
DAlperin wants to merge 3 commits into
TimelyDataflow:master-nextfrom
DAlperin:unload-chunk
Open

UnloadChunk: bulk probe extraction for chunk batches#782
DAlperin wants to merge 3 commits into
TimelyDataflow:master-nextfrom
DAlperin:unload-chunk

Conversation

@DAlperin

@DAlperin DAlperin commented Jul 3, 2026

Copy link
Copy Markdown

UnloadChunk is a second way to read a chunk, alongside NavigableChunk: given a sorted set of probe keys, copy the matching updates out into caller-owned staging. A cursor navigates a chunk in place and hands out references into it — Cursor::key<'a>(&self, storage: &'a Storage) -> Key<'a> lets the caller hold borrows for the batch's lifetime — so the only sound way to read a paged chunk under it is ColChunk's fetch-and-cache-forever OnceCell, and a probe-heavy consumer progressively re-materializes the trace it probes. Extraction is finished with a chunk's body when the call returns: any fetch a paged implementor needs is scoped inside one method, the cache stays empty, and probing a paged trace does not accrete it back into memory.

The trait keeps Chunk's discipline: key/val/time/diff opinions stay off it (Staging and Probes are opaque associated types), and the one comparison the batch driver needs is delegated to the chunk via locate, answered from resident metadata like len. Chunk boundaries are carried as a protocol rather than an index: a chunk consumes every probe strictly below its last key and extracts-but-does-not-consume a probe equal to it, so the driver re-offers that probe to the next chunk and the continuation lands in staging as a legal straddle — the same invariant chunk sequences carry everywhere else. Times are copied verbatim; since-advancement stays with the consumer, exactly as on the cursor path.

For the in-tree families, Staging is ChunkBatchBuilder<Self>: extraction emits its hit runs as resident chunks and the builder settles as it goes, so staged data is graded, coalesced, and — when a spiller is installed — paged under the same bounded-footprint discipline as any other chunk sequence. Extraction is thereby chunks-in, chunks-out, its results consumable by everything chunk-shaped (including, since done yields a ChunkBatch, the existing cursors — soundly, over storage the consumer owns). A probe run covering a whole resident chunk, and every fetch_into of one, stages as a handle push: a refcount bump, no copy.

Contents:

  • trace/chunk: the UnloadChunk trait, a provided ChunkBatch driver (gallop the chunk list by locate, open only the chunks a probe touches, fetch_into as the scan path), and the VecChunk implementation.
  • columnar: UpdatesBuilder::meld_keys, melding a key-index range of a borrowed UpdatesView by bulk column-range copies (meld now delegates to it). ColChunk's extraction melds each source chunk's partial hit runs into one staged chunk through it; because it works on views, one primitive serves typed tries and wire bytes, and the paged arm reads a zero-copy view over the loaded bytes (spill::read) for the scope of the call — no trie rebuild, no cache population.
  • Tests: filter-oracle property tests over random chains for vec and col, resident and paged (asserting caches stay empty); exhaustive (chunk cut × probe placement) boundary coverage including a key spanning several chunks; a meld_keys property test against filtered form; paged extraction and scan round-trips with spill stats.
  • chunk_bench: extraction vs the straddle cursor, both consuming into owned staging. At 1M updates: dense String probes 201ms cursor vs 1.8ms extraction (the whole-chunk handle path; the design doc's 21ms-class admission with two orders of headroom), sparse (1%) probes at 1.2x over cursor parity, fat 4x256B values staged as handle pushes. Paged cold reads: dense 23x, sparse 4.3x — where the cursor path must decode-and-cache while extraction views the spilled bytes.

This is the counterpart of #781 one layer down. That PR's backend presentations (present0, present_input) have exactly this contract — sorted probe keys in, an owned restricted run out, nothing borrowed across the boundary — and leave their implementation to the backend; extract_into and meld_keys are the machinery a chunked, spillable backend would implement them with. Both sit on the same two seams: tactics as the consumption dispatch point (#773, public since #789#791), and cursor-less chunks as full trace citizens (#778). Rebased over all of the above.

🤖 Generated with Claude Code

@DAlperin
DAlperin force-pushed the unload-chunk branch 3 times, most recently from 63324cc to 90a0811 Compare July 13, 2026 16:01
Comment thread differential-dataflow/src/columnar/trace/chunk.rs Outdated
Comment thread differential-dataflow/src/columnar/trace/chunk.rs
DAlperin and others added 3 commits July 15, 2026 17:00
UnloadChunk is the read surface for chunks whose bodies may not be
resident: probe hits are copied into caller-owned staging, and no borrow
of chunk contents crosses the trait boundary. Key/val/time/diff opinions
stay off the trait (Staging and Probes are opaque); the one comparison
the batch driver needs is delegated to the chunk via locate(), answered
from resident metadata like bounds() on the cursor path.

Staging, for the in-tree families, is ChunkBatchBuilder<Self>:
extraction emits its hit runs as resident chunks and the builder settles
as it goes, so staged data is graded, coalesced, and -- with a spiller
installed -- paged under the same bounded-footprint discipline as any
other chunk sequence. Extraction is thereby chunks-in, chunks-out, and
its output is consumable by everything chunk-shaped. A run covering a
whole resident chunk (and every fetch_into of one) stages as a handle
push: a refcount bump, no copy.

ChunkBatch gains a provided driver: gallop the chunk list by locate,
open only the chunks a probe touches, and re-offer a probe left
unconsumed at a chunk's last key to the next chunk (the consume-index
protocol); its continuation lands in staging as a legal straddle, the
same invariant chunk sequences carry everywhere else. fetch_into is the
scan path.

VecChunk is the worked implementation. Tests: a random-chain property
test against the filter oracle, exhaustive (chunk cut x probe placement)
boundary coverage including a key spanning several chunks, and a fetch
round-trip.

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

UpdatesBuilder gains meld_keys: meld a key-index range of a borrowed
UpdatesView -- the keys with their complete val/time/diff subtrees -- by
bulk column-range copies, with the same boundary-stitching semantics as
meld (which now delegates to it over the full range). The view may sit
over typed columns or directly over wire bytes, so one primitive serves
resident and spilled sources.

ColChunk's extraction gallops the probe column against the key column
and melds maximal runs of consecutive hits into a per-source-chunk carry
(one staged chunk per chunk touched); a run covering the whole resident
chunk stages the handle instead, no copy. On the paged arm a fetch is
scoped to the call and reads a zero-copy view over the loaded bytes
(spill::read) -- no trie rebuild, and the OnceCell cache stays
unpopulated, unlike the cursor path's fetch-and-cache-forever. locate
answers from resident metadata (ChunkMeta when paged).

Tests: a filter-oracle property test over random chains for resident
and paged chunks (asserting caches stay empty), a straddle fixture, a
paged extraction + scan test with spill stats, and a meld_keys property
test against filtered form.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A read-side section for the UnloadChunk path: both paths consume the
probed updates of a columnar ChunkBatch into owned staging -- the
straddle cursor by per-probe seeks and owned copies, extraction by one
extract_into into chunk staging. Shapes: dense String-keyed probes (the
design doc's headline case), sparse (1%) u64 probes, dense u64 probes,
and fat 4x256B values; the first two also run against paged batches
read cold, where the cursor path decodes-and-caches and extraction
views the spilled bytes.

At 1M updates on an M-series laptop: dense String probes 201ms cursor
vs 1.8ms extraction (the whole-chunk handle fast path; the design doc's
21ms-class admission, cleared with two orders of headroom), sparse
probes at 1.2x over cursor parity, fat values staged as handle pushes.
Paged cold reads: dense 23x, sparse 4.3x.

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.

2 participants