Repository navigation
Memo MILP search: clusters, solution decoding, warm start, accuracy rules and term cuts (opt-in --search memo-milp) - #930
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
… for the memo's MILP
…ates, latency, cuts)
… and lists the identical-expression and scan-widening variants as the exhaustive search does
…custom accuracy models A group a plan may read through a node exact whatever its input (a count of rows) gets no failure-probability target, so an uncertified input of such a node is no longer ruled out. Both accuracy rules apply only under the built-in accuracy model's semantics: a model that may accept an output whatever its guarantee (also when guarantees are ignored) turns them off.
… the largest size Stage 3 can give Stage 3 enlarges summaries for their readers' failure probabilities before it checks guarantees (classic HLL precision, Count-Min/CountSketch/KLL depth, UnivMon heap). The rule now evaluates each composition with every summary at the most that enlargement can reach (classic HLL at precision 18, others for a vanishing failure probability), so it rules out only what fails at every size Stage 3 can give.
…at the largest size as unknown
…y size rules nothing out A division by a UnivMon distinct count loses its guarantee only when the UnivMon is sized for a vanishing failure probability (its heap cap lets the relative error grow). The memo MILP's accuracy rule must treat that as unknown and keep the UnivMon choices.
…alone, cache it per plan, and skip candidates that cannot change it (same rules, RollupHeavy 662 s -> 15 s)
Each round decodes the MILP solution to the plan it describes and runs Stage 3 on it (plus its widened-scans variant and the materializations of nodes no variable sets, at most 16), instead of enumerating the group choice's thresholds, identical-expression variants and materializations. The cut excludes exactly the decoded choice set (choices, groups built once or not). Model changes that make every solution describe one plan: - one threshold per coupling site (site threshold variables); - a group is needed only as a root, a chosen choice's child or a fused choice's implemented group; - a scan shared at a horizon has two or more readers (one reader reads it apart, the same plan); - a build built once only because another group's equal build reads it needs such a choice. Decoding: scans read apart are the reader's own copy; identical non-scan nodes are one node; widened scans are evaluated as a variant (a decoding gap until scan labels are a model variable).
…elity, decoder failures reported - A choice set cut with combinations of free nodes' options not evaluated (more than the cap) or that failed to decode keeps its solution's objective as the lower bound, so the loop cannot certify past it; the last round leaves such a set uncut for the relaxation. - A group built once whose own node is a build is built once in the decoded plan (built once overrides the slot), and the model builds a group once only through its own choice, never a fused one. - Decoding failures are counted in the MILP report (debug_assert). - Threshold variables keyed by (site, threshold); lower_bound_check clears excluded_once; Decoded::deviations removed. - Tests: site threshold, need <= readers, a shared scan needs two readers, cuts on built once, fused groups not built once, the bound with unevaluated plans, built once over a build node, decode.
…at their ingestion price A consumer that is an ingestion-time slot reaches its child groups at rate 1, and the model charged each reached choice's direct part at its query-time price per evaluation (scans of the whole look-back, absorbed scans, class and cross-group charges). Stage 3 computes that part at ingestion time, once per second over one second of rows (LazyTerms::direct_at_ingestion), so the model overestimated such plans and was no lower bound: an exact top-k of 5-minute sums without raw data was modeled at 0.150 against an exact price of 0.0145. The model now charges a choice reached through an ingestion-time slot its direct part's ingestion-time price (0 when it cannot run there). Test: the model is a lower bound for that top-k below an ingestion-time slot (failed before: model 0.150 over exact 0.0145).
…ed slot node Slots that may be one shared node (one shared site, threshold and materialization) are charged once, at the dearest; their ingestion-time inputs were still charged once per slot consumer, so the model was above Stage 3's price when two ingestion-time slots shared one build (two quantiles within 1%: 26.094 vs 26.042; max by job of a 5-minute sum and the sum: 0.2701 vs 0.2181). Each such node's inputs are now charged at the dearest of its slot consumers' charges. A part that cannot run at ingestion time (no ingestion-time price) is no longer reached by an ingestion-time slot for free: that reach is fixed at 0. Test: the model is a lower bound for children of two ingestion-time slots, at 300 s/300 s and 60 s/300 s (both cases failed before).
…every group Held memory of a part is charged per reader at union / sum of its readers' rates. The model took the readers from the choice's own group only, but a node several groups compute is one node read by all their queries: two quantiles every 10 s within 1% over one shared build were modeled at 33.886070 against an exact 33.886035 (about 1e-6 relative, one reader's held memory too many). The share now ranges over the queries of every group with a choice computing one of the part's nodes. Only a part that Stage 3 cannot place at ingestion time (a grouping with no window below it) is kept from an ingestion-time slot; any other pricing error of its ingestion-time copy charges it 0. Test: the two-ingestion-slot lower-bound test also runs at 10 s/10 s (failed before).
Before round 1, each cluster loop plans every query alone with the legacy search (no sharing between queries), composes the plans side by side with each query's own materializations, and offers the result to the loop's PlanSelector as the starting incumbent. A time-limited run then reports that plan and its gap instead of "no plan". Certification is unchanged: the loop still stops when the MILP lower bound reaches the incumbent. No HiGHS MIP start: mapping a legacy plan onto the model's choice and slot variables is not cheap. Test: a memo-milp run with a 0 ms time limit returns the warm-start plan, priced at the sum of the queries' own plans.
…e stopped at the time limit, so a stopped search reports its gap without an unlimited relaxation solve
…shared-chain candidates, not every subset's
… group joins A node several groups compute may be read by queries of classes that no single group has together, so no union_rate key covers their class set and the share fell back to a single class's ratio (1 for one reader per class): every 10 s and every 20 s, the model charged 0.15 against Stage 3's 0.10. The share now also takes, for each possible fastest reader, its rate over the sum of the readers no faster than it: their union rate is at least the fastest's, so this bounds the share from below. Test: a node read from two classes keyed only one by one (model 0.15 over 0.10 before); the two-ingestion-slot lower-bound test also runs at 10 s/20 s.
…lows into chosen choices; requirement-coupled consumer flows
…ances in the lower bound when no plan is left below the incumbent; certified means gap within GAP_TOLERANCE
… shared builds possibly apart; test each cut kind
…4 tolerance; a time-limited run returns the warm-start plan with its gap
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.
Part of #879
Why
--search memo-milp(experimental, #915) searches the sub-expression memo with a MILP lower bound and a lazy loop, but on main it could not plan whole evaluation workloads: every round enumerated every plan of a group choice in Stage 3 (q24h60 checked 1.31M candidates; RangeHeavy grew past 59 GB), coefficient derivation spent ~10 min in the rollup rules on RollupHeavy, a time-limited run with no evaluated round returned no plan (RollupHeavy t11-5m and t10: "time limit ... before finding a valid plan" after 1800 s), and the bound was weak enough that small rows took hundreds of rounds (q24h60 689 s, rh8-q1h60 772 s, i840 not certified after 1800 s). #879 needs each whole workload to return a plan with an honest gap within 300 s, and the components to certify.What
--search memo-milp(still opt-in; the default search is unchanged and stilllegacy) now:search.milp.clusters;The selection report gains
search.milp.decode_failuresandsearch.milp.clusters.How
search::milp::model::searchbuilds the memo,clusters::splitpartitions its queries by union-find, andsearch_clustersruns oneClusterLoopper cluster (new->offer_independent_planwarm start ->next_round/take_solution->finish).run_roundssolves the clusters' models on scoped threads; incumbent evaluation stays on the main thread.take_solutioncallsdecode::decode, which composes the chosen choice versions into one CandidateLogicalASAPDAG (identical non-scan nodes one node viawith_shared_candidates, plus the widened-scans variant), materializes it withoptimize_physical_asap_chosenfrom the solution's built-once and slot choices, and offers each variant to the Stage 3 selector. The loop then addscuts::term_cuts_of/held_cuts_of/ingestion_copy_cuts_ofand the no-good cut of the decoded choice set.clusters::combineputs the clusters' plans back in workload order and checks that the whole-workload cost equals the sum (else the memo is searched as one).Key code interfaces added / modified
crates/plan-selection/src/lib.rs
crates/physical-asap-optimization/src/lib.rs
crates/logical-asap-optimization/src/asap_aware_cse.rs
No CLI flag changes:
--search memo-milpand--milp-time-limit-sare as on main.Usage
Major algorithm changes
Per-cluster MILP
model_input_of+ one lazy loop. After: (1)clusters::splitunion-finds queries over shared groups, candidate reads, coupling sites and disk-charged raw sources; (2) oneClusterLoopper cluster, solves on threads; (3)clusters::combinemerges plans and checks cost = sum.combinereturnsNone(search as one) unless the combined cost equals the sum within 1e-9. Test:three_way_on_two_independent_clusters, clusters unit tests.measured_operation_costsput everything in one cluster.Solution decoding and incumbent evaluation
offer_group_choice): q24h60 1,310,528 candidates; RangeHeavy 59 GB in identical-expression variants.decode::decodebuilds one logical DAG from the chosen versions, shares identical nodes the solution shares (with_shared_candidates), materializes per the solution (optimize_physical_asap_chosen), plus the widened-scans variant; nodes no variable fixes get every Stage 2 option combination up to 16.unevaluated_boundand the reported bound is min(bound, that), so no false certificate. Tests: decode and model unit tests (site threshold, shared-scan readers, built-once fidelity, unevaluated bound), 20-seed three-way.Warm start and time-limit bound
ClusterLoop::offer_independent_planoffers every query's own legacy plan before round 1; a solve stopped at the time limit keeps HiGHS's proven bound lowered by its tolerance.Nonewhen no valid bound exists. Test:a_time_limited_memo_milp_run_returns_the_warm_start_plan.Accuracy rules as constraints
guarantee_requirementsand the rollup/pair rules require child choices that keep each choice's guarantee, read at the largest summary size Stage 3 can give (resized_to_the_largest); exact compositions and custom accuracy models are skipped; a guarantee lost only at the largest size rules nothing out.d990e77a's largest-size test.Tighter lower bound and term cuts
ChoiceCoefficients::query_work), held-memory shares, ingestion prices, reach flows, strict cutoff; then per roundterm_cuts_of(SourceScans),held_cuts_of(HeldPerReader, HeldLongest, SharedBuildHeld),ingestion_copy_cuts_of:charge(term) >= bound * (sum of conditions - (n-1)).lower_boundtests), plusterm_cuts_certify_the_brute_force_price_on_839.Rollup and pane-chain rules per coarse build
rollup_rulesrebuilt every subset's rollup candidates (RollupHeavy coefficient derivation 662 s).rollup_candidate_forper coarse build, cached per plan, skipping candidatesmay_change_rollup_candidatesays cannot change it; the pane rule builds only the pair's shared-chain candidates (window_composition_shared_chain_candidates).72a873aecompares shared-chain roots).Before this PR
main (
--search memo-milp --milp-time-limit-s 1800, shared host):Whole workloads (DRYRUN, 180 s / 30 min): 12 of 12 time out with no plan.
After this PR
Merged head 0f8bf09,
--milp-time-limit-s 280, same host (shared, load ~15):Fixed regression set (component rows):
Whole workloads (raw retained, fargate, enforced; each returns a plan and its gap within 300 s, none certifies):
decode_failures is 0 in every row. q24h60 and rh8-q1h60 are worse than main at a 280 s limit (main needed 689 s / 772 s to certify them); see Known open problems.
Known open problems
fix/memo-q24h-ingestion, an exact root pair rule that restores 8.738845e-05 certified in 113 s. A review found that it can judge scalar-root queries at the operator group; a WIP fix for that is on the same branch, not gated yet. The per-size form of the same rule is tracked in memo-milp misses rh8-q1h60 at 280 s (1.34e-05 vs 3.83e-06): suspected decode cut of a valid stricter site threshold #928.fix/memo-rh8) certifies 3.832840e-06 in 17.5 s. memo-milp misses rh8-q1h60 at 280 s (1.34e-05 vs 3.83e-06): suspected decode cut of a valid stricter site threshold #928 stays open until a test settles the decode question.feat/memo-sizing-once.The following are not in this PR:
fix/memo-q24h-ingestion,fix/memo-rh8,perf/memo-warm-start-cheap,w2/held-once,feat/memo-sizing-onceandfeat/memo-validity.Verification
On the merged head (main merged in, 0f8bf09), on the remote build host:
cargo fmt --all -- --check: clean.cargo clippy --release -p asap-plan-selection -p asap-integration-tests -p asap-logical-asap-optimization -p asap-physical-asap-optimization --all-targets -- -D warnings: clean.cargo test --release -p asap-plan-selection --lib: 223 passed.cargo test --release -p asap-integration-tests --test joint_search_equivalence three_way_twenty_smallest -- --ignored: passed.cargo test --release -p asap-devtools: all pass, after updating two stalestage_pipelineexpectations to PR-B's intended behavior (search_flag_picks_the_search_algorithm: a certified gap is within 1e-4, not 1e-9;milp_time_limit_is_reported_and_needs_the_milp_search: a run stopped before round 1 now returns the warm-start plan with its gap instead of an error).cargo test --release -p asap-integration-tests -p asap-logical-asap-optimization -p asap-physical-asap-optimization(non-ignored): 556 passed.