compute: bucket temporally on the columnar edge - #38371
Conversation
d7c0cb0 to
b980c29
Compare
e37c599 to
e163635
Compare
e163635 to
8f98052
Compare
8f98052 to
aea209c
Compare
aea209c to
d02623b
Compare
d02623b to
9bd1e54
Compare
9bd1e54 to
dd94790
Compare
dd94790 to
bdf0852
Compare
bdf0852 to
51d7f71
Compare
| permutation.clear(); | ||
| for index in 0..borrowed.len() { | ||
| let update = borrowed.get(index); | ||
| if upper.less_equal(&T::into_owned(update.1)) { |
There was a problem hiding this comment.
I think the trick is to have a one-element container for upper, and compare the Ref type directly. Check if that's possible.
There was a problem hiding this comment.
Not taken yet, and I want to check it properly rather than guess. upper is an Antichain<T>, and less_equal wants a &T, so comparing a Ref directly needs PartialOrder on the Ref type, which columnar does not provide for a Product<Timestamp, PointStamp<u64>>. A one-element container gives us Ref on both sides, but the comparison still has to come from somewhere.
Took the cheaper half of the win in the meantime: the allocation, not the comparison. time_buf.copy_from(update.1) reuses one T for both the upper test and the bucket placement, so the loop no longer allocates a PointStamp per record. What is left is the copy itself, a few words.
Will look at whether a Ref-level less_equal is expressible, most likely as a small trait on our side implemented for the timestamp types the bucketer actually sees, and report back rather than holding this PR for it.
Posted by Claude Code
| let mut buffered_range = None; | ||
| for index in permutation.drain(..) { | ||
| let update = borrowed.get(index); | ||
| let update_time = T::into_owned(update.1); |
There was a problem hiding this comment.
Try to use Columnar::copy_from instead of creating a new owned object.
There was a problem hiding this comment.
Done. One time_buf now serves both reads of a record's time, the upper test and the bucket placement, so the pass allocates nothing per record.
Posted by Claude Code
51d7f71 to
48a7764
Compare
| /// | ||
| /// Columnar throughout. The chain's batcher already holds [`Column`] chunks, so | ||
| /// the reveal path moves containers, and the input side addresses records by | ||
| /// index through a time-ordered permutation rather than moving them. No owned | ||
| /// record is materialized on either path. |
There was a problem hiding this comment.
don't need to comment this anymore
There was a problem hiding this comment.
Dropped, along with the two sibling comments that said the same thing at the call sites in render.rs and top_k.rs. Those now say only when bucketing fires. The implementation's doc paragraph went too: the permutation it described is already noted where the permutation is built, which is where it belongs.
Pushed as doc: drop the comments the columnar bucketing made vacuous.
Posted by Claude Code.
|
|
||
| use super::*; | ||
| use crate::render::columnar::vec_to_columnar; | ||
| use crate::render::columnar::{columnar_to_vec, vec_to_columnar}; |
There was a problem hiding this comment.
Test-only. The bucketing tests capture their output for comparison, and capture() wants a stream of owned records, so columnar_to_vec is how the assertion reads a columnar edge back. No decode is added to a rendered dataflow by this PR.
There is a note on one of those tests saying why the decode is not allowed to hide a bug: agreeing contents alone would not rule out a silent decode on the ok path, so the tests assert on the edge's container type as well.
Posted by Claude Code.
| ); | ||
| } | ||
| // Temporal bucketing is `Vec`-internal, so decode in and encode out. It fires | ||
| // Temporal bucketing is columnar throughout, so no round trip here. It fires |
There was a problem hiding this comment.
Gone. Both this one and the matching line in render.rs now say only when bucketing fires, not what its containers are.
Posted by Claude Code.
| // encode out, keeping this Union input a columnar edge. | ||
| let os = columnar_to_vec(os); | ||
| vec_to_columnar(T::maybe_apply_temporal_bucketing( | ||
| // Temporal bucketing is columnar throughout, so no round trip |
There was a problem hiding this comment.
Gone. Both this one and the matching line in render.rs now say only when bucketing fires, not what its containers are.
Posted by Claude Code.
4e21ab6 to
790d556
Compare
QA LLM Review1. MEDIUM -- the reduce path's
|
|
Both findings are real. Fixed in 1, the The You also caught something I should have caught myself. The doc justified the staging with "a Not taking the second option you offer, moving the reduce call site onto the columnar operator. Its bucketed output feeds 2, the
Posted by Claude Code. |
5ee47da to
963f532
Compare
963f532 to
e483030
Compare
Temporal bucketing consumed and produced a `Vec` stream, so each of its four call sites decoded the columnar edge into it and re-encoded the result. The operator's own state was already columnar, since its bucket chain wraps `ColumnMergeBatcher`, so the round-trips existed only at its boundary. The operator now consumes and produces `Column`, and the four sites hand it the edge directly. Three per-record costs go with the round-trips: * The reveal path handed the builder owned tuples reconstituted from the chain's `Column` chunks. It now gives each chunk as a container. * `MergeBatcherWrapper::push_container` staged its `Vec` argument into a `Column` one record at a time before the chunker could read it. It takes a `Column` now, so the caller's buffer is what the chunker reads. * `Bucket::split` round-tripped the sealed chunks through a `Vec`, cloning every record on the way. It re-chunks the containers. The input side addresses records by index through a permutation rather than moving them, so neither the pass-through nor the bucket routing materializes an owned record. Records are partitioned before that permutation is sorted, because only the retained ones need ordering and in steady state the pass-through share is the larger one. `TemporalBucketing::bucket` returns `Self` rather than a chosen container, which lets a `Vec` stream and a columnar stream both implement it. The reduce key-value path keeps the `Vec` implementation, which stages through the columnar operator so there is one bucketing implementation rather than two. That path feeds an arrangement, and a re-encoding consumer is cheaper to hand moved allocations than copied bytes, so `MaybeBucketByTime` offers it as a separate `_vec` method rather than converting it.
The bucketing pass reads each record's time twice, once to test it against the upper and once to place it in a bucket. An iterative `T` owns a `PointStamp`'s allocation, so `Columnar::into_owned` paid for one per read. `copy_from` refills a buffer instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
With bucketing columnar on both sides there is no round trip left to point at, so the call sites in `render.rs` and `top_k.rs` say only when bucketing fires. The implementation's doc loses its paragraph as well: the permutation it described is already noted where the permutation is built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Staging a `Vec` caller's whole stream through a column copies every pass-through record in, copies it out, and allocates it again on the decode. Records near the frontier never enter the chain, so that work buys nothing. Give the `Vec` stream its own operator body again, which passes those records through as moves, and encode only what the chain stores, which the columnar batcher required anyway. The `split` comment claimed the re-chunk visits no record. The chunker sorts and re-pushes every one, which is what the per-record fuel charge below it accounts for. What the columnar chain removed is the owned-tuple round trip, not the visit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
e483030 to
8780d40
Compare
Makes temporal bucketing columnar throughout, removing the four
Vecround-trips its call sites paid. The operator's state was already columnar, since its bucket chain wrapsColumnMergeBatcher, so the decode and re-encode existed only at its boundary.Three per-record costs go with the round-trips. The reveal path handed the output builder owned tuples reconstituted from the chain's
Columnchunks, and now gives each chunk as a container.MergeBatcherWrapper::push_containerstaged itsVecargument into aColumnone record at a time before the chunker could read it, and now takes aColumn.Bucket::splitround-tripped the sealed chunks through aVec, cloning every record, and now re-chunks the containers.On the input side, records are addressed by index through a permutation rather than moved, so neither the pass-through nor the bucket routing materializes an owned record. The partition happens before the sort, because only retained records need ordering and in steady state the pass-through share is the larger one.
TemporalBucketing::bucketnow returnsSelf, which lets aVecstream and a columnar stream both implement it. The reduce key-value path stays on theVecimplementation, which stages through the columnar operator so there is one bucketing implementation rather than two. It feeds an arrangement, and a consumer that re-encodes what it reads is cheaper to hand moved allocations than copied bytes.Worth knowing for review:
enable_compute_temporal_bucketingis compiled in asfalsebut served on in production, so this is a live path rather than a dormant one. CPU-213 tracks reconciling the default. Verified with the flag defaulted on, since the compiled-in default otherwise leaves the operator untested outsidetemporal_bucketing.slt.Posted by Claude Code