Repository navigation
feat(plan-selection): experimental MILP search over the sub-expression memo (--search memo-milp) - #915
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
… for the memo's MILP
…ates, latency, cuts)
… and lists the identical-expression and scan-widening variants as the exhaustive search does
# Conflicts: # crates/devtools/src/bin/stage_pipeline.rs # crates/devtools/tests/stage_pipeline.rs # crates/plan-selection/src/pass/stage1_to_stage3.rs
# Conflicts: # crates/devtools/src/bin/stage_pipeline.rs # crates/devtools/src/planning_stages.rs # crates/devtools/tests/stage_pipeline.rs # crates/integration-tests/tests/joint_search_equivalence.rs # crates/plan-selection/src/lib.rs # crates/plan-selection/src/pass/stage1_to_stage3.rs # crates/plan-selection/src/pass/subexpr/mod.rs # crates/plan-selection/src/pass/subexpr/pricing.rs # crates/plan-selection/src/pass/subexpr/scans.rs # crates/plan-selection/src/pass/subexpr/search.rs # crates/plan-selection/src/pass/subexpr/windows.rs
…the merge had dropped; --search memo-milp and --milp-time-limit-s on top
…ap tolerance counts as optimal with its bound lowered by that tolerance (was reported as a time-limit stop)
… plan is still priced and the best plan returned with its gap (relaxation bound when no round finished); infeasibility names the queries with no plan
…iables/constraints, first_model_bound, time_limit_reached
…ound, gap, rounds and effort in a nested MilpReport (search.milp)
…idened scans Slots whose builds are equal over equal inputs may be one node (a site's build at one threshold, or identical expressions): each slot carries its build's structural key, and the model charges the dearest chosen one once. Scan choices of one source may be one widened scan: the model charges the dearest scan choice of each source. The model's input and the per-group-choice lower-bound check are functions (model_input_of, lower_bound_check), and a test asserts the model is a lower bound of every group choice on #840's and #839's shapes.
Shared slots and shareable nodes several groups' choices may have are keyed loosely (kind, family, panes, raw sources) and charged once per key, at the dearest chosen one; their held memory is left out of the per-choice held term. A scan choice is charged a pass over its output per reader plus, once per source, the largest excess over it (a widened scan). rollup_rules reads the finer site the rollup itself requires. Tests: the model is a lower bound of every group choice on #840's shape, its sketch rollup, and #839's shape.
… widening A slot's build and a shareable node are keyed by their payload over their producers' keys, each scan by its source alone (the identical-expression rule may widen a source's scans to one label set), schemas left out. So the model charges the dearest of several groups' choices only for nodes that are one node once shared, and the per-choice sum otherwise.
A scan apart is its reader's own, and a shared one is its scan group's choice: counting absorbed scans as shareable nodes of several groups let the model charge only the dearest of every group's scan.
…oss groups Each query-time node of a choice's direct part (scans aside) carries its key up to scan widening; nodes several groups' choices have with one key are one node once shared (identical expressions over widened scans), and the model charges the dearest chosen one once. A choice's own scans are charged like scan choices: a pass per reader plus the largest excess once per source. A choice lists the children its plan reads first, with their child_via; children its candidate lists without reading (a rollup's own window) stay needed but no consumer passes into them.
…ot consumers only of chosen choices A node several groups' choices have (equal up to scan widening) costs its charge per consumer, or built once at least the dearest chosen one's at its group's slowest rate: the model charges the smaller. A slot's consumer reaches and passes into nothing unless its choice is chosen. A shareable node's part leaves its scans to the scan charges. A debug test dumps a seeded random workload as stage_pipeline JSON.
…once group A slot's consumer reaches, passes into and is charged by nothing unless its choice is chosen; a group's own consumer unless the group is built once. Coefficient tests charge a built-once node's scans once.
…t the union rate The scale came from the largest charge including slots' fixed costs: a slot keeping tens of GB of state shrank every other charge below the solver's dual tolerance, so the solver stopped above the optimum with costly variables left at 1 (seed 43: 0.3% over the exact price). It is now the largest per-evaluation charge at the fastest query's rate. A part several queries of one class read runs at their union rate, so each reader's held memory is charged at union/sum of its rate.
…y test over the 20 smallest random workloads
# Conflicts: # crates/devtools/tests/stage_pipeline.rs
…ich two parallel tests shared
…ver time-limit stop with no solution keeps the incumbent; query latency leaves out nodes another group may share; the lower-bound tests also run without raw data, and over a query reading two groups of one metric
A query over two groups of one metric reads one scan node and one node per group for its schedule class, where the model charged each reader: - a scan's pass is charged once per identical scan (source and price), the largest of its readers' charges; - a query-time slot's raw scans come out of its slot charge and are charged as scans of their source, so two slots over one scan share it; - a choice's direct charge for queries of one class is their largest, not their sum.
…known instead of failing; the report says any solver limit
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.
Experimental.
--search memo-milpis opt-in; the legacy search stays the default. This PR is the MVP of the MILP search over the sub-expression memo, merged under a lighter gate (listed under Verification). The full 60-seed three-way test, the slow ignored tests and whole-workload timing are the gate of the follow-up PR.Part of #879
Why
The legacy and exhaustive searches enumerate whole-workload combinations, which blow up on the evaluation workloads (RangeHeavy
quantiles-24h@60s: 359,970 candidates, 617 s, 3.4 GB; RollupHeavy template 10: no plan within 25 min). The memo (#900) represents the workload as groups of equal logical sub-expressions; this PR searches it with a mixed-integer linear program whose answer is checked exactly by Stage 3, so the selected plan is the exhaustive optimum when the reported gap is 0.What
--search memo-milp(SearchAlgorithm::MemoMilp): under cost model 1, a lazy loop over a MILP built from the memo. Cost models 2 and 3 run the legacy search and say so (routed_to_legacy: cost_model_not_supported).incumbent_cost(the selected plan's cost per second). The MILP search addssearch.milp:lower_bound,first_lower_bound,gap,rounds,variable_count,constraint_count,solver_time_ms,time_limit_reached.--milp-time-limit-s <f64>(Stage1ToStage3Search::milp_time_limit_ms): at the limit the search returns its best evaluated plan withtime_limit_reached: trueand its gap; it is never called optimal unless the gap is 0. Hitting the limit before any valid plan is an error that says so.good_lp =1.15.3with the HiGHS solver (MIT).How
Terms (joint-planning-search design, "Terms"):
once) per shared group, consumer reach through direct parts and slots, union rates of schedule classes for built-once and query-time nodes, held memory over each query's query-time latency (linearized), raw data kept on disk per source, latency bounds, requirements (exact children, coupling sites, rollup rules) and pane/top-k exclusivity as constraints, cross-group nodes and slots charged once per exact key.Major algorithm changes
quantile_over_time(0.5, data[1m]),quantile_over_time(0.9, data[5m]),quantile by (host, job) (0.5, data)): the plans differ in which KLL builds are shared, kept at ingestion or built at query time, and how long one raw scan reaches back. The exhaustive search composes and evaluates every combination.--exhaustiveon the equivalence seeds (feat(plan-selection): memo over logical sub-expressions with a brute-force reference search (--search memo-brute-force) #900).quantiles-24h@60s: 4,094 variables, 5,593 constraints, 129 rounds, 735 s, 194 MB (main 617 s, 3.4 GB).Key code interfaces added / modified
crates/plan-selection/src/lib.rscrates/plan-selection/src/pass/stage1_to_stage3.rscrates/devtools/src/bin/stage_pipeline.rs:--search legacy|memo-brute-force|memo-milp(waslegacy|memo-brute-force),--milp-time-limit-s <f64>(new; needs--search memo-milp).Usage
The planner lowers the three queries (Stage 0), builds per-query candidates (Pass 1), builds the memo of 8 groups, derives coefficients for a model of 1,198 variables, then runs rounds: HiGHS solves the model, Stage 2 and Stage 3 evaluate the group choice it names, and a cut removes that group choice. The document's
stage3_plan_selection.searchthen reads:{"algorithm": "memo-milp", "incumbent_cost": 0.1477615599424, "milp": {"lower_bound": 0.1477615599424, "first_lower_bound": 0.141229058, "gap": 0.0, "rounds": 73, "variable_count": 1198, "constraint_count": 1603, "solver_time_ms": 321471, "time_limit_reached": false}, "memo": {"groups": 8, "candidates": 39, ...}}with the selected plan's cost equal to
--exhaustive's (0.1477616 per second): one raw scan of 200,000 samples (5 minutes) read by both windows, a KLL build per window at query time (40,000 and 200,000 rows into 10,000 states), and a separate 15-second scan with an exact hash aggregate for the third query.Before this PR
--searchofferedlegacyandmemo-brute-force. The brute force evaluates every plan of the memo, so it is no faster than the exhaustive search on large workloads.After this PR
--search memo-milpselects the exhaustive optimum where it certifies gap 0 (below), reports how far from proven it is otherwise, and stays opt-in.Verification
Checks that ran (the lighter MVP gate), on head a1f1a62 (the last commit only changes the error path after a time limit and a doc line); runs share one machine with other jobs:
lower_bound_checkover all group choices)the_model_is_a_lower_bound_*, raw data on and off): #840's shape, a shared group with a rollup, #839's shape, a sketch rollup, a query over two groups of one metric. Ad-hoc runs (not committed): 0 violations on random seed 43, #840 (3 queries), two #840 variants, #839 raw and no raw--exhaustive, 73 rounds, 322 s, 123 MB (exhaustive 4.3 s, 134 MB)quantiles-24h@60s, raw, Fargate, guarantees enforced--search memo-brute-force;--exhaustiveselects 115.66801 (1.3e-5 relative cheaper), a memo-coverage gap, not the MILP's: follow-upthree_way_twenty_smallest_random_workloads_under_cost_model_1, release, 137 s)cargo testof plan-selection and devtoolslower_bound4/4 with raw data on and off; stage_pipeline 45, incl. the time-limit and search-flag tests)-D warningson the changed cratesNot run here (follow-up gate): the full 60-seed three-way under cost model 1, the slow ignored equivalence tests, whole paper workloads.
Also: two
stage_pipelinetests from #908 shared temporary file names and failed when run in parallel; they now take per-call names.