Skip to content

refactor(optimizer): one shared-producer path for top-k reuse, shared sizes and shared panes - #959

Merged
zzylol merged 4 commits into
mainfrom
refactor/shared-producer
Oct 27, 2026
Merged

zzylol merged 4 commits into
mainfrom
refactor/shared-producer

Conversation

@zzylol

@zzylol zzylol commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Why

#954: top-k reuse and the summary-capability rule both share one producer at a need that covers every reader (the largest k; each size parameter's strictest value; a common pane grid with the longest window), and each reader derives its own result from it. They had two separate candidate-generation loops in the memo. This PR gives them one path, so that the next case (#951's shared scan, a group-by union) is a new implementation of that path, not a third loop.

What

There is no behavior change. The memo's top-k reuse and shared-size / shared-pane configurations now generate their candidates through add_shared_producer_candidates. The candidates they produce are identical to main's, which a new test pins with hashes recorded on main.

Case Old code path New covering / derive Status
exact top-k reuse memo/rules/topk.rs add_topk_candidates (its own loop over topk_reuse_candidates) TopKRankings: covering = the ranking Limit(k_max) ▸ Sort; derive = Limit(k) ▸ Project via append_producer / append_reader unified
summary capability (shared sizes) memo/rules/state_configurations.rs expand_configurations, size branch SummaryStates: covering = StateCovering::Size from state_size_configurations; derive = with_resized_build unified
window composition (shared panes) same function, pane branch SummaryStates: covering = StateCovering::Panes from pane_configurations_before; derive = with_pane_chain unified
percentile ranks (#922) ir::cse interning of equal windows over a scan none: equal source windows become one memo group left in ir::cse: the producer is the identical window, so no candidate is generated
identical expression ir::cse / memo group identity none: the trivial case left in ir::cse, as #954 states

Not in this PR:

How

memo/rules/shared_producer.rs holds the driver and the SharedProducer trait (crate-private):

  1. sets lists a kind's readers, already grouped into sets that can share. Each kind groups its own readers.
  2. A set spanning two or more groups gets its coverings.
  3. Each reader gets derive for each covering. derive adds nothing when the covering does not serve the reader, so each kind filters its own readers.

The two implementations are the old code moved into those methods, with the order of reads, writes and deduplication preserved:

  • SummaryStates groups by its compatible check. For a set that shares panes, sets also numbers each reader's normalized pane layout (StateNeed::normalized_layout). The numbers continue across sets, so the request deduplication needs no per-set reset, and coverings takes &self.
  • TopKRankings returns one set per topk_reuse_candidates result, each reader carrying the set's ranking. Its covering is that ranking plus the set's groups. The ranking is interned in the first reader's derive; later readers get the same producer group back, because append_producer is idempotent.

Key code interfaces added / modified

memo/rules/shared_producer.rs (new; crate-private, pub(in crate::memo))

/// One reader of a kind of shared producer: its memo group and its need.
pub(in crate::memo) struct Reader<N> {                                   // new
    pub group: usize,                                                    // new
    pub need: N,                                                         // new
}

/// A kind of producer several readers can share.
pub(in crate::memo) trait SharedProducer {                               // new
    /// What one reader requires of the producer.
    type Need;
    /// A producer's need that serves several readers at once.
    type Covering;
    /// The memo's readers of this kind, in sets whose members can all read
    /// one producer; in memo order.
    fn sets(&mut self, memo: &CandidateMemo, deadline: Option<Instant>)
        -> Result<Vec<Vec<Reader<Self::Need>>>, StateGroupExpansionError>;
    /// The coverings to offer the readers of `set`.
    fn coverings(&self, set: &[Reader<Self::Need>], deadline: Option<Instant>)
        -> Result<Vec<Self::Covering>, StateGroupExpansionError>;
    /// Adds `reader`'s derivation from the producer at `covering`, if that
    /// producer serves it; the number of candidates added.
    fn derive(&mut self, memo: &mut CandidateMemo, reader: &Reader<Self::Need>,
        covering: &Self::Covering) -> Result<usize, StateGroupExpansionError>;
}

/// Whether `set`'s readers belong to two or more groups.
pub(in crate::memo) fn spans_groups<N>(set: &[Reader<N>]) -> bool;     // new

/// Gives every reader of a set spanning two or more groups its derivation
/// from each of the set's coverings.
pub(in crate::memo) fn add_shared_producer_candidates<P: SharedProducer>(
    memo: &mut CandidateMemo, producer: &mut P, deadline: Option<Instant>,
) -> Result<usize, StateGroupExpansionError>;                            // new

Covering is a separate type from Need, unlike the sketch in #954. A pane covering is a whole configuration (grid, longest window, family), not one reader's need.

memo/rules/topk.rs: add_topk_candidates keeps its signature and now calls the driver with TopKRankings (new, private).

memo/rules/state_configurations.rs: add_state_configuration_candidates_in_domain keeps its signature and now calls the driver with SummaryStates (new, private). The private Reader struct is renamed StateNeed and gains normalized_layout; StateCovering is new and private. TopKRankings's need is the set's ranking node and its covering SharedRanking (ranking plus reader_groups); both private.

Usage, end to end:

  1. Input: the PromQL workload topk(3, sum_over_time(m_gauge[2m])) plus topk(10, sum_over_time(m_gauge[2m])), every 60 s, exact.
  2. Pass 1: try_populate_pass1 builds the memo.
  3. Expansion: expand_concrete_summary_state_groups_in_domain runs add_topk_candidates, which calls add_shared_producer_candidates(memo, &mut TopKRankings { .. }, deadline).
    • sets returns one set: the two top-k groups, each with the reuse set's ranking Limit(10) ▸ Sort.
    • coverings returns that ranking with the two groups.
    • derive interns it once and adds Limit(3) ▸ Project and Limit(10) ▸ Project readers with origin TopKReuse.
  4. Output: plan selection receives the same memo as on main: 313 candidates with the same hash. The test top_k_reuse_candidates_are_unchanged checks this.

Major algorithm changes

None in results: candidate generation for these three cases is restructured onto one driver, and its outputs are unchanged. Complexity changes slightly: the top-k path now collects queries and calls append_producer once per reader instead of once per set, so a set of R readers does R linear scans of the memo groups instead of one; repeated calls find the existing producer group. Top-k sets are small, so this is negligible. SummaryStates::derive checks that the covering serves the reader before cloning the candidate, so skipped pairs cost no clone.

Before this PR

Two loops generated shared-producer candidates: add_topk_candidates and expand_configurations. Each had its own grouping, covering and derivation code.

After this PR

One driver generates them. A new case implements SharedProducer (sets, coverings, derive) and calls add_shared_producer_candidates.

Verification

  • Behavior lock (memo/shared_producer_snapshot.rs, new): it hashes each expanded memo in memo order: every group's node, children, parents, queries and roots, and every candidate's origin, budget share, implemented groups, child groups and node. Order matters because the search's tie-breaks depend on it.
    • Recorded on main (d4152bce): top-k (313 candidates, 16940643818053118362), shared sizes (508, 7457272222287953080), shared panes (729, 15532247561651645380), two top-k sets next to a pane set (994, 9213568444657927922; pins the order sets are expanded in).
    • Branch: identical.
    • The test also checks that the top-k workload yields TopKReuse candidates and the pane workload yields pane chains, so each case is exercised.
  • On the branch (rebased on d4152bce): cargo test -p asap-logical-asap-optimization --lib: 229 passed, snapshot hashes unchanged.
  • Integration tests on the branch, all passed:
  • Independent review: an independent reviewer compared main and this branch on five extra multi-set workloads (two pane sets on different metrics, mixed sum/max panes, quantile panes on two metrics, two size sets, mixed top-k plus panes): same counts and hashes on all five. Numbering normalized layouts per set instead of globally (a deliberate mutation) fails the size and pane snapshots (508 → 348, 729 → 489), so the test catches that risk.
  • Style: cargo fmt --all and cargo clippy -p asap-logical-asap-optimization --all-targets --all-features -- -D warnings are clean (stable toolchain).

Part of #954

🤖 Generated with Claude Code

@zzylol zzylol added CSE optimizer Workload optimization and plan selection refactor labels Oct 27, 2026
zzylol and others added 3 commits October 27, 2026 20:38
…shared panes

Hashes each workload's expanded memo (groups, candidates, origins and
children). The values were recorded on main before the shared-producer
cases move onto one generation path, so that move cannot change them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…and panes

Top-k reuse and the shared-size / shared-pane configurations generate
candidates through add_shared_producer_candidates: readers of one kind
are grouped into sets that can share, each set gets its coverings (the
ranking with the largest k; finite shared sizes; pane layouts), and each
reader derives its candidate from each covering that serves it. The
kind-specific code (TopKRankings, SummaryStates) is the old code moved
into the SharedProducer methods; the generated candidates are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The SharedProducer trait keeps three steps: sets (each kind groups its
readers), coverings (no side effects) and derive (adds nothing when the
covering does not serve the reader). can_share and serves are gone.
SummaryStates numbers pane templates in sets, carried in StateNeed;
top-k readers carry their ranking instead of a set index.

The snapshot test now hashes the memo in order, including group edges,
queries, roots and each candidate's implemented groups; its values are
re-recorded on the base commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the refactor/shared-producer branch from 3fcf05f to fcbc4eb Compare October 27, 2026 20:38
coverings takes &self; the serves check runs before the candidate is
cloned; names say what they hold (configuration_index,
normalized_layout, reader_groups). A snapshot of two top-k sets next to
a pane set pins the order sets are expanded in (hash recorded on main).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zzylol
zzylol merged commit 6a22ec4 into main Oct 27, 2026
1 of 2 checks passed
@zzylol
zzylol deleted the refactor/shared-producer branch October 27, 2026 20:59
@zzylol

zzylol commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

@milindsrivastava1997 FYI

milindsrivastava1997 added a commit that referenced this pull request Oct 28, 2026
Same selected plans and costs. Only the search report changed: about
twice the joint-price solves, and the direct-cost path now validates
plans for q19, q22, q23, q25 and q26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKYtMxgJTLtPW9uAqPpiEG
zzylol pushed a commit that referenced this pull request Oct 29, 2026
Same selected plans and costs. Only the search report changed: about
twice the joint-price solves, and the direct-cost path now validates
plans for q19, q22, q23, q25 and q26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKYtMxgJTLtPW9uAqPpiEG
zzylol pushed a commit that referenced this pull request Oct 29, 2026
Same selected plans and costs. Only the search report changed: about
twice the joint-price solves, and the direct-cost path now validates
plans for q19, q22, q23, q25 and q26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKYtMxgJTLtPW9uAqPpiEG
zzylol added a commit that referenced this pull request Oct 29, 2026
* test(devtools): add planner corpus snapshot harness

The planner has no reference answer, so pin its output instead: each corpus
query's stage_pipeline document (default search) is compared with a committed
snapshot. Timings are stripped and a run stopped by the wall-clock budget
fails, so snapshots do not depend on machine speed. UPDATE_SNAPSHOTS=1
regenerates them. Part of #941.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKYtMxgJTLtPW9uAqPpiEG

* test(devtools): snapshot 14 o11y-bench queries

The o11y-bench queries that default search plans in under 10 s and 500 MB,
each on its own, with declared source facts for the corpus metrics. Queries
05, 06 and 11 select no plan and are recorded as failure snapshots. Checked
deterministic across two runs and a single-core run. Part of #941.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKYtMxgJTLtPW9uAqPpiEG

* ci: check planner snapshots in a release-build job

Runs the corpus snapshot test against a release build, since the corpus
queries take minutes in debug, and uploads the regenerated snapshots when a
plan changed. Part of #941.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKYtMxgJTLtPW9uAqPpiEG

* docs: explain planner snapshots and require Snapshot changes in PRs

A developer note on what the snapshots pin, how to regenerate them and how to
add queries, plus a Snapshot changes section in the PR template and AGENTS.md
so every changed plan is explained. Part of #941.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKYtMxgJTLtPW9uAqPpiEG

* test(devtools): regenerate o11y-bench snapshots after #943, #961, #959

Same selected plans and costs. Only the search report changed: about
twice the joint-price solves, and the direct-cost path now validates
plans for q19, q22, q23, q25 and q26.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CKYtMxgJTLtPW9uAqPpiEG

* test(devtools): snapshot only the selected plan, not the search

The corpus snapshots compared the whole stage_pipeline document, including
the search report's counters, status and candidate ids. Those vary from run
to run because pane-width rounds have wall-clock budgets, so every snapshot
failed on main without any plan change.

- Snapshot only the selected Stage 2 candidate (display name, DAG), its
  cost and accepted accuracy, materialized views, pane retention, warnings
  and whether the search proved it optimal; for a query that does not plan,
  the error up to its search report and the rejection causes. Floats are
  rounded to 12 significant digits. The memo is not built (--no-memo).
- Fail on a search report with status time_limit, not only on a run that
  used the whole budget.
- 120 s per query instead of 600 s; the CI job has a 45 min timeout and
  caches only the cargo registry next to sccache.
- Mark q18 (#1023) and q21 (a pane round ends on its wall-clock budget)
  known_slow and skip them.
- Clear the regenerated directory at the start of a run, and write each
  failing query's full document next to its regenerated snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(devtools): 240 s per query, skip_reason, document the final-round status check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: zzylol <50204836+zzylol@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CSE optimizer Workload optimization and plan selection refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant