[Ideas] Implement Robust Predicate Transfer (RPT+) on top of Anser #1963
Unanswered
leborchuk
asked this question in
Ideas / Feature Requests
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Description
Implement Robust Predicate Transfer with dynamic execution (RPT+) on top of
Anser (
gpcontrib/anser/). Readgpcontrib/anser/README.mdfor the currentarchitecture before starting.
The references, precisely
https://github.com/embryo-labs/dynamic-predicate-transferhttps://github.com/robust-sql/robustRead both repos, but know which is which:
robust-sql/robustimplements plainPredicate Transfer (DuckDB's native bloom filter, 12 bits/key; reports 1.76×
geomean on JOB), while the paper is specifically about fixing RPT's overhead
and regressions.
AGENTS.mdin this repo says:"Do not generate or import code with incompatible licensing." Check the license
of both repos before reusing even a snippet, and prefer implementing from the
paper. If you do reuse anything, it needs attribution plus
LICENSE/NOTICEhandling — raise it in the PR rather than deciding alone.
What the paper actually proposes
RPT (the baseline) makes Yannakakis' algorithm practical: model the query as
a join graph
G(V,E)where edge weight = number of shared join attributes,build a maximum spanning tree rooted at the largest table (
LargestRoot),then propagate bloom filters over that tree in a forward pass (leaves→root)
and a backward pass (root→leaves), fully reducing tables before the join
phase. Its weakness: it ignores equivalence relationships between join keys,
producing redundant and oversized filters, and pays heavy scan +
BF-construction overhead — enough to cause real regressions.
RPT+ adds three things:
passes, because they do different jobs. Forward = collect information, so a
deep chaining tree is best (filter table i before building the filter
for i+1, avoiding oversized BFs). Backward = distribute information, so a
wide broadcast tree is best (build the final filter once and reuse it
for every table in the same equivalence class). Chains for different
equivalence classes are linked through "bridge tables" (the root of each
class's induced subtree).
for tuple-level filtering, applied coarse-to-fine.
construction when it will not pay off (Algorithm 2 in §5.2).
Reported speedups over DuckDB v1.3.0: 1.47× JOB, 1.28× SQLStorm, 1.17× TPC-H,
1.01× Appian, "avoid[ing] the significant performance regressions observed
with the original RPT".
Constants from §5.5, use these as starting values, do not invent your own:
VPGATHERDDSampling is sequential, not random — random access breaks streaming
execution. On JOB, τ_sel in 0.1-0.3 gave the best speedups; τ_sel = 0 (drop
almost everything) hurt.
Why Anser fits — and the one place it does not
condition_key—include/anser.hdocuments it as exactly "the optimizer-generated equivalence-class symbol"anser_disp_deliver(),src/anserdispatch.c:372)AnserBloomFoldPartInPlace()OR-folds parts as they arrive (src/anserfilter.c:126)src/anserplan.cAnser's backward pass is essentially free — it is what the extension already
does, and the dispatch-connection transport made it cheaper still: one merge in
the coordinator backend, then one push per consumer, no shared memory and no
worker in between. The forward chaining pass is where this project lives or
dies, for a reason that does not exist in DuckDB:
coordination.
merges → all segments receive.
And we now know what a hop costs. A traced exchange on a 3-segment demo cluster
with a 1 MB filter took 34 ms, of which ~21 ms was the coordinator picking
parts up one at a time (~7.2 ms per part, gated by the interconnect wait loop).
JOB queries join 5-10 tables, i.e. 4-9 sequential hops — on the order of
140-300 ms of pure barrier latency added to queries whose total runtime
may be under a second, on loopback, with the smallest filter Anser can build.
That arithmetic is the single most important input to milestone 0.
chaining locally on each segment and avoid the barrier. That is only correct
when the tables are co-distributed on the join key. Otherwise segment s
holds only part of table j's key set, and filtering table j+1 with it drops
rows whose key lives on another segment — wrong results, silently. Where
tables are not co-located, each hop must be global. Write this rule into the
design note, with the co-location test, before any code.
Milestones
M0 — Read, map, measure the budget (blocking; no implementation)
Design-note deliverable:
both repos.
24 segments.
anser.debugalready logs every step with timestamps, so thisis a measurement, not new instrumentation. Then the projected cost of a 4-hop
and 8-hop chain. State whether M3 is viable.
distributed on the same key, and how many JOB / TPC-DS joins qualify.
pass runs post-plan (
AnserApplyRuntimeFilters,src/anserplan.c), whichsees a finished
PlannedStmt, not the join graph — can equivalence classes berecovered there, or does this need a pre-plan hook? This is the second
make-or-break question. ORCA and the Postgres planner will differ; report
both.
backend now, so there is no
anser.max_channelsto size — but RPT creates onefilter per (table, class), so a 10-table query could hold ten 1 MB
accumulators per query in the QD, plus libpq's per-connection input buffers
(which keep their high-water mark for the session). Budget that, and say what
the cap should be.
Do not proceed past M0 without review. "The forward pass costs more than it
saves in an MPP" is a legitimate and valuable outcome.
M1 — Backward broadcast pass across an equivalence class (start here)
Generalize today's one-producer-set/one-consumer-per-hash-join to: all tables
in an equivalence class publish, the merge happens once, every table in that
class receives it. No ordering, no barrier beyond the one Anser already has,
and it is exactly the pass RPT+ shows is best served by a broadcast tree. Expect
the bulk of the achievable win here.
M2 — Dynamic pipeline (Algorithm 2)
Implement the abandon logic with the paper's constants, on the producer side.
Note this is the same mechanism as task 2.4 of the bloom-performance issue —
coordinate, do not implement it twice; whoever gets there first owns it, the
other reviews. Anser already has the cancel path
(
ExecAnserBloomFilterProduceCancel(),src/anserbloomproduce.c), but mind thesemantics: an Anser cancel kills the channel for every consumer, whereas RPT+
abandons one filter locally. Decide and document which you want.
M3 — Asymmetric transfer plan (forward chaining) — only if M0 allows
Chaining forward tree + broadcast backward tree, bridge tables between classes.
Restrict chain depth by what M0's latency budget supports, and say what depth
you allowed and why.
M4 — Cascade filter
min/max block skipping + a blocked/sectorized bloom layout. Cross-link to the
bloom-performance issue: RPT+ affords 20 bits/key and 7 hashes precisely
because a blocked filter touches one cache line per probe (2.48
cycles/tuple), while Anser's classic bloom does k random accesses. That suggests
the layout matters more than the bits/key knob — and a blocked filter is still
unionable by bitwise OR when both sides share parameters, so the cheap fold
survives. Verify that claim before relying on it.
Benchmarks
workload and this technique's home turf. Loading IMDb into Cloudberry with
sensible distribution keys is part of the task; commit the DDL under
gpcontrib/anser/doc/bench/.bloom-performance issue (injected subset in detail, non-injected regression
check, geomean).
regressions, and RPT+'s main claim is removing them; a report that shows the
geomean win without a per-query regression table does not answer the question
this work exists to answer.
to differ: we are distributed, our hops cost a global barrier, and our filter
implementation is not theirs. Explain your gap rather than matching it.
shared_preload_libraries='anser',anser.enable=on,anser.runtime_filter=on. NoCREATE EXTENSIONneeded — the subsystemcreates no catalog objects.
Definition of done
co-location correctness rule, join-graph feasibility for both optimizers,
per-query memory budget.
visible in
EXPLAIN ANALYZE.row counts) on the regression suite, JOB and TPC-DS.
gpcontrib/anser/doc/performance.md, regressionsreported per query.
Out of scope
queries. Detect and skip the rest; do not try to generalize.
("restricts the forward transfer plan to tables with predicates, plus bridge
tables … trades the theoretical guarantee for significantly reduced
overhead"). We are following it there, not doing better.
bad join order survivable, not to fix it.
Use case/motivation
No response
Related issues
#1942
Are you willing to submit a PR?
All reactions