Repository navigation
feat(plan-selection): memo over logical sub-expressions with a brute-force reference search (--search memo-brute-force) - #900
Merged
Merged
Conversation
…pability, window/rollup/top-k transformations, per-group pricing (behind no flag yet)
…, budget-share variants, topological order Each Pass 1 candidate's private nodes read their child groups through the groups' own nodes and output the group's schema. A sketch candidate is offered once per share of its measure's budget a reader may give it (budget_share), composed by the new compose_logical_candidate_with_budget_share. Memo::topological_order orders groups after every group they or their candidates read.
ScanLabels sites per metric whose scans project different labels, with WidenedScans twins, label thresholds and widening at a threshold; and RawHorizon sites per source whose raw windows two or more groups read. Identical expression tested as group identity.
…nts, sketch rollup over a finer group's state, groups kept in topological order
…(exact-child requirements, sketch rollup) into the memo
price_at_scale's per-node loop body is now price_node, which takes the node's producers' outputs, its computation rate, a query-time scan's look-back, a chain's retained panes and an ingestion-time node's window, and returns the node's output statistics, usage, cost and the charges known only once the whole plan is (held memory, partial copies, raw data). price_at_scale calls it per node; prices are unchanged.
…roups, scan materializations A shared-build site keys its input by structure, so equal windows of two queries (one group each without a unique key) meet. Candidates list the materializations Stage 2 may give them; a scan group read through one window is only built once, one read through different windows may also be read per reader. Equal SQL table scans are one group.
… grouping's pane chain; sketch rollup state resized to a threshold; required sites; one topological order
…eep their own columns A lib test composes every whole-workload Pass 1 choice of the random workloads of joint_search_equivalence and checks each query's plan is a combination of the memo's per-group candidates. It found the fused top-k heap candidates of an ungrouped top-k root dropped because their output columns differ from the logical node's; a candidate keeps its own schema when its columns differ, and every candidate of a group a parent reads outputs the group's schema. Equal SQL table scans are one group.
… memo equivalence test
…); scan materializations in the memo
…erialization candidate
…r context price_at_scale's node loop is split into price_each_node, and lib.rs gains price_private_nodes: Stage 3's per-node pricing of a candidate's private nodes for given consumers (queries, or materialized ancestors with their rate), reading its children's given outputs, with computations()' counting restricted to them. Memory held for an evaluation and raw data on disk stay plan-level terms (HeldMemory, raw scans), priced by plan_cost once each query's latency is known. subexpr/pricing.rs: private_usage, output_stats, plan_cost and frozen_calibration, cached by (candidate key, consumers, materialization, child outputs); frontiers dropped (cost model 1 only). Tests: the groups' local prices add up to price_at_scale of the composed plan for several one-query plans, a shared group not materialized and materialized at query time (equal and different intervals), and a pane chain at ingestion.
…threshold resized_at_threshold resizes a shared-build reader's build to a threshold (with_resized_build, guarantees composed again, child groups' nodes kept). The brute-force reference rewrites SharedBuildSize sites with it and makes resized builds over equal windows one build.
… 1 parity test checks every target's candidates A memo plan built from per-group candidates kept the frontend's empty guarantee on a group's own node (an exact top-k over the per-series sums' exact accumulator), so Stage 3 rejected it for having no guarantee. composed_roots now recomposes every guarantee, as Pass 1 composition does (recompose_guarantees made public). The parity test also checks that each target's group, absorbed or not, has the target's own and fused candidates.
…the chosen implementations
… and lists the identical-expression and scan-widening variants as the exhaustive search does
…eport.memo) MemoStats counts the memo's groups, shared groups, candidates by origin, distinct coupling sites by kind and its build time; the reference search fills it whenever it builds the memo. Replaces the string stats() and the temporary JP_MEMO_STATS hook.
…eader needs Pass 1 fuses an aggregate only into its single consumer within one query. The memo merges equal nodes across queries, so an absorbed group may also be another query's root: the fused candidate now keeps such a group (and what only it reads) outside its implemented groups, so a top-k heap never answers a second query's inner sum, and the plan with the top-k by the heap and the sum by its own build is listed. Doc comments: no default look-back.
#897) into the per-node pricing
…r build A sketch rollup's state site was never sized in the brute force (its thresholds went to the shared-build sizing, which lists no rollup reader), so no plan rolled a coarse quantile up from a finer KLL build; and the requirement check keyed the site by the resized copy, whose recomposed guarantees change the key. The search now sizes rollup state sites by their readers' thresholds and keys the site by the memo's build. On the quantile workload with by (host, job) and by (job) beside a per-series quantile it selects the exhaustive search's plan (0.041256/s, was 0.047030/s).
…nused interfaces - The memo branch names its Stage 1 candidate by the selected plan's from_logical, so the document's Stage 2 candidates read a listed Stage 1 candidate (the viewer's validator rejected L1 vs L3). - Delete share_identical_expressions and materialization_slots, which nothing calls. - stage_pipeline rejects --exhaustive with --search memo-brute-force and says --search legacy is the default search. - Memoize unchanged_below by node; point doc comments at planner-layering's memo section.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The joint search enumerates whole-workload candidates: the product of every query's Pass 1 candidates, every Pass 2 rewrite subset and every materialization. On the paper workloads that is millions of candidates per sharing component, and the whole workloads never finish (#879, #673). The fix (#879) is a memo over logical sub-expressions, solved by a MILP. This PR lands the memo and a brute-force reference search over it. The reference is the second exact oracle the MILP will be checked against.
Part of #879.
What
crates/plan-selection/src/pass/subexpr/). The workload's logical DAG is hash-consed into groups, one per distinct sub-expression. Each group gets implementation candidates: Pass 1's per-group output, then the Pass 2 rules as transformations that add candidates:search::brute_force). It tries every consistent choice of one candidate per needed group and one threshold per coupling site, and lists each composed plan's identical-expression and scan-widening variants as the exhaustive search does. It recomposes guarantees, then has Stage 2 and Stage 3 implement and price each plan.Stage1ToStage3Search::memo_groupsselects the reference search, and so doesstage_pipeline --search memo-brute-force. The default search is unchanged.SearchReport.memogives the memo's size: groups, shared groups, candidates by origin, coupling sites by kind, build time.price_at_scale(price_node,price_each_node,price_private_nodes), with no price change. It is what the MILP's coefficients will come from.How
groups::memo_ofinterns the frontends' roots with the CSE interning. Each Pass 1 target's candidates become its aggregate group's candidates, reading child groups byRc; fused computations implement several groups.capability.rs,windows.rs,scans.rs) add candidates and coupling sites, with threshold enumeration per site and requirement checks (windows::requirements_met) that mirror the joint rules.search::composed_rootsbuilds each query's own nodes as Pass 1 composes them.search::brute_forceenumerates group choices × thresholds, lists the variants and prices them.pricing.rs: a candidate's private nodes priced in a consumer context, for the MILP that follows.Key code interfaces added / modified
crates/plan-selection/src/pass/stage1_to_stage3.rscrates/plan-selection/src/lib.rscrates/devtools/src/planning_stages.rscrates/logical-asap-optimization/src/asap_aware_cse.rsrecompose_guaranteesandsize_for_all_readersare nowpub. CLI:stage_pipeline --search legacy|memo-brute-force(new;legacyis the default search, written as"search": "default";--exhaustivewith--search memo-brute-forceis an error).Staged for the MILP PR (crate-private, read only by tests here):
subexpr/pricing.rs(PriceCache,frozen_calibration, local prices),price_private_nodeswithPrivatePricing,NodeConsumer,HeldMemory,PrivateNodesUsage,LocalUsage,Context, andImplementationCandidate.materializations. The MILP's coefficients read them.Usage
stage_pipelinelowers the queries and callsrun_search_overwithmemo_groups: true.run_stage1_to_stage3_from_pass1then callssearch::brute_force:memobuilds the groups,group_choicesandat_thresholdsenumerate plans, and Stage 2 and Stage 3 price each one.out.jsonhas"search": "memo-brute-force"and the selection, withstage3_plan_selection.search.memo={groups, shared_groups, candidates, candidates_by_origin, coupling_sites_by_kind, build_ms}(4 groups here), and the selected plan's Stage 1 candidate under the id its physical candidates name. The selection, P3-m1 at 5.1116e-05/s, costs what--exhaustiveselects (checked: the same plan at the same price), and the viewer'svalidateStagePipelineaccepts the document.Major algorithm changes
1. The memo over logical sub-expressions.
quantile_over_time(q, data[24h])queries plus their ratio have about 11.9M whole-workload candidates, though they share one input and differ only in their estimates.stage1_logical_asap_optimizationcomposes every product of Pass 1 choices, each with every Pass 2 subset.memo_ofhash-conses the DAG, so the input is one group. Each aggregate group lists its own candidates (exact, KLL, DDSketch, panes, shared builds). The memo is linear in the workload: on whole RangeHeavy (46 queries), 74 groups and 284 candidates; on whole RollupHeavy (62 queries), 158 groups and 584 candidates, built in under 0.3 s.every_composed_pass1_candidate_is_a_combination_of_group_candidates);memo_group_search_selects_the_exhaustive_pricecompares only the selected price with the exhaustive search's, on 60 seeds; it does not check that every exhaustive plan is a memo combination.topk(5, sum by (job) (sum_over_time(m[5m])))and Q2 =sum by (job) (sum_over_time(m[5m])). Before, the heap absorbed Q2's root, so one plan answered Q2 with Q1's top-k and "Q1 by the heap, Q2 by its own sum" was missing. Tests:a_fused_candidate_does_not_absorb_another_querys_root(every composed plan) anda_fused_top_k_beside_its_inner_sum_selects_the_exhaustive_price(memo ==--exhaustive, with and without raw data).state_size_thresholds,resize_state_at), and the requirement check keys the site by the memo's build, not by the resized copy whose recomposed guarantees change its key. Example (Instant quantile by (host, job) next to windowed quantiles: no scan sharing, no panes, no ingest-time variant, 100-group default #840's workload):quantile_over_time(0.5, data[1m]),quantile by (host, job) (0.5, data)andquantile by (job) (0.5, data)every 10 s over 10,000 series.--exhaustiverollsby (job)up from theby (host, job)KLL build at 0.041256/s; before the fix the memo had no such plan and selected two exact aggregates at 0.047030/s. On the whole Instant quantile by (host, job) next to windowed quantiles: no scan sharing, no panes, no ingest-time variant, 100-group default #840 workload the memo now selects 0.150788/s, as--exhaustivedoes (was 0.156562/s). Tests:a_sketch_rollup_beside_another_quantile_meets_its_requirementsanda_sketch_rollup_beside_other_quantiles_selects_the_exhaustive_price(Q1/Q3/Q4 and Q2/Q3/Q4, with and without raw data).2. The brute-force reference.
--exhaustive, about 12 s in total in release.Before this PR
The only searches were the default joint search and
--exhaustive. Neither finishes on whole paper workloads, and neither reports how large the sharing problem really is.After this PR
--search memo-brute-forceplans small workloads through the memo, and a memo-brute-force selection reports the memo's size. The MILP solver (next PR) reads the same memo and is checked against this reference and--exhaustive.Verification
cargo test --release -p asap-integration-tests --test joint_search_equivalence --test sql_group_bound -- --include-ignored(head after merging main): 21 + 7 passed, 0 failed (including the ignored exhaustive runs, memo_group_search_selects_the_exhaustive_price on 60 seeds, and the new top-k and rollup seeds)cargo test --release -p asap-plan-selection -p asap-devtools -p asap-logical-asap-optimization -p asap-physical-asap-optimization: all pass, 0 failed (189 plan-selection lib tests, including main's label-set group-bound tests through the per-node pricing; devtools' new memo document and flag tests)validateStagePipeline(node 22)stage_pipelineon Instant quantile by (host, job) next to windowed quantiles: no scan sharing, no panes, no ingest-time variant, 100-group default #840's workload:--exhaustiveP381-m3 0.150788/s;--search memo-brute-force0.150788/s (was 0.156562/s). Two windowed quantiles over one metric: shared scan costs more, and nested-window KLL panes are never shared #839's: both 0.147762/scargo test --release -p asap-plan-selection -p asap-logical-asap-optimization -p asap-physical-asap-optimization -p asap-planner -p asap-devtools: all pass (0 failed)cargo test --release -p asap-integration-tests(every file): all pass (0 failed)cargo clippy -D warnings(touched crates, all targets) andcargo fmt --check: clean