Repository navigation
feat(plan-selection): selection MILP solved per independent part, LP gap diagnosis, tighter big-M bounds (#1095 §5 3a, §6.1) - #1155
Merged
Conversation
…art (#1095 §5, 3a) The model splits into the connected components of the graph joining each row's variables; each part is solved alone with its share of the deadline, and the joined solution goes through the one completion, decode, realization and plan_cost check.
… feature (#1095 §6.1) With ASAP_STAGE3_LP_DIAGNOSIS=<path>, a build with the feature appends, per solve, the LP relaxation and root-node bounds, the LP with each row family dropped, the LP with cover rows on configuration and window terms, and the objective split by variable family at the LP, the warm start and the incumbent. Measurement only; production builds leave it out.
…accuracy shares (#1095 §6.1) Ē[m] in V ≥ E − Ē (x[m] − z) is the sum over the context's slots of its costliest run, not over every candidate; N̄_q in the accuracy row is the number of places (context, slot) q can depend on an approximate value at, not the number of runs. Both stay valid upper bounds, so the objective still equals plan_cost at Optimal.
…P diagnosis (#1095) - The LP diagnosis pushes the MILP's deadline back by its own time. - A part without binaries reports its LP optimum as its dual bound (HiGHS leaves mip_dual_bound at 0 for an LP). - The comment on a row without variables names the part it goes to. - A test pins the tighter context execution bound against the old one (same plan and cost); a test-only switch rebuilds the old bounds. - lp_diagnosis reuses objective_scale and HIGHS_THREADS.
This was referenced Oct 31, 2026
Open
zzylol
added a commit
that referenced
this pull request
Nov 2, 2026
…gap diagnosis, tighter big-M bounds (#1095 §5 3a, §6.1) (#1155) Each connected component of the selection MILP is solved alone and joined before the single decode, realization and plan_cost check. An lp-diagnosis feature (devtools only) attributes the LP gap by row and variable family. Two big-M bounds are tightened. No selected plan or cost changes.
zzylol
added a commit
that referenced
this pull request
Nov 2, 2026
…gap diagnosis, tighter big-M bounds (#1095 §5 3a, §6.1) (#1155) Each connected component of the selection MILP is solved alone and joined before the single decode, realization and plan_cost check. An lp-diagnosis feature (devtools only) attributes the LP gap by row and variable family. Two big-M bounds are tightened. No selected plan or cost changes.
zzylol
added a commit
that referenced
this pull request
Nov 2, 2026
… Stage 3's default search (#1095) (#1144) * feat(plan-selection): group selection MILP over execution contexts behind ASAP_STAGE3_SEARCH=milp (#1095) Step 1 of #1095's solving strategy: one MILP with a warm start. The owner-model pieces from tag stage3-owner-milp-redesign are restored and the owner/instance model is replaced by execution contexts: each query evaluation, the ingestion pipeline I (NotMaterialized ingestion-time candidates) and each shareable (materialized) candidate's own inputs. - groups.rs (was owners.rs): RunKind, ResultSlot (group roots split by self-read depth; private nodes their own slot), execution contexts and their options; dominance, per-candidate usage and accuracy weights kept. Scan windows are costed at every union of the window kinds read within one context, as plan_cost prices them. - milp/model.rs: p[ctx, c], x[m], a[q, m], rows 1-9, slot links for inputs over several result slots, order ranks within and across contexts, latency in scaled units. - milp/cost_terms.rs: run terms (plain, producer-dependent, exact scan window sets), schedule-group rates, held memory, raw retention and pane retention levels. - milp/warm_start.rs: each query's cheapest plan, then greedy sharing, costed exactly by the completion. - milp/decode.rs: one node per (context, computed candidate) and per selected shareable candidate. - asap-cost: a NotMaterialized ingestion-time node runs once per second for every consumer; a NotMaterialized node a materialized node computes is held until that node's slowest reader finishes. The old engine stays the default. * fix(plan-selection): a shareable candidate reads a scan at its own window; window-independent scans keep their usage per window (#1095) * fix(plan-selection): adopt main's Stage 1 exports and cost_inputs name after rebasing onto the Stages 0–2 milestone (#1095) * feat(plan-selection): selection MILP solved per independent part, LP gap diagnosis, tighter big-M bounds (#1095 §5 3a, §6.1) (#1155) Each connected component of the selection MILP is solved alone and joined before the single decode, realization and plan_cost check. An lp-diagnosis feature (devtools only) attributes the LP gap by row and variable family. Two big-M bounds are tightened. No selected plan or cost changes. * perf(plan-selection): keep accuracy builds across pair checks and queries (#1095 §8) (#1156) Measured that candidates of one operator node never share an input context on the measured workloads (no same-timing pairs), so the §8 shared-input-context step has nothing to share. The 44 s build of rangeheavy-t8-t5 largest round was 96% accuracy checks; a candidate built outputs are now kept across pair checks and queries (except outputs built across a cycle of links), and duplicate exact stand-ins are skipped. Weights, excluded pairs, plans and costs unchanged; that round builds in 7.6 s. * feat(plan-selection): one ingestion context per refresh window in the selection MILP (#1095) (#1157) Brings feat/1095-stage3 (#1151 refresh groups and cost rule) into the prototype and drops its once-per-second ingestion hook. The MILP has one ingestion context I_r per refresh window (unbounded views alone), so a shared stateless ingestion node is charged once per refresh window, as asap-cost charges it. workload-01 costs 1.6% more: its old plan under-charged a scan read by chains of three refresh windows. Part of #1095. * fix(plan-selection): use the tumbling-window names (#1163) in the group MILP after rebasing onto main The group MILP commits were written before #1163 renamed PaneChain to SummaryTumblingWindowChain and pane_width_ms / pane_count / newest_panes / pane_retention_ms to their tumbling-window names; rename their uses, and rustfmt the lines the rebase resolved. * feat(plan-selection): the group MILP is Stage 3's default search (#1095 C6) select_plan runs the group MILP unless ASAP_STAGE3_SEARCH=old. Inputs the MILP does not price yet (cost models 2 and 3, or a measured operation cost table without a frozen calibration factor) are an UnsupportedCostModel error; there is no fallback to the old engine. * fix(plan-selection): use the remaining tumbling-window names (#1163 part 2) in the group MILP #1176 renamed the remaining pane helpers (pane_candidates, kept_pane_chains, panes_of, retained_panes, the pane test helper, ...); rename their uses in the group MILP's code and tests.
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
#1095 §6.1 left two failures on the prototype (#1144): workload-01's largest pane-width round ends at a 70.9 % gap with the incumbent equal to the warm start, and rangeheavy-t8-t5's largest round (114,225 variables) has no dual bound after a 45 s build. §5 lists the steps to try in order. This PR takes the first one (3a, connected components) and measures where the LP relaxation loses its bound, so the next step (shared input contexts, 3b or 3c) is chosen from data.
What
plan_cost= objective check. Queries end up in one part when they may share a shareable candidate, a run ofI, a source's raw retention, or anything else a row couples.lp-diagnosiscargo feature (off in production builds;asap-devtoolsforwards it). WithASAP_STAGE3_LP_DIAGNOSIS=<path>, each solve appends: the pure LP bound, the root-node bound, the LP with each row family dropped, the LP with cover rows added, the objective split by variable family (at the LP, the warm start and the incumbent), and thep/xruns with the largest LP-to-incumbent difference. The MILP's deadline is pushed back by the diagnosis's time. (The diagnosis numbers below come from runs made before that change, with a 3,000 s budget.)Ē[m]inV ≥ E − Ē (x[m] − z)is now the sum over the context's slots of its costliest run, not a sum over every candidate.N̄_qin the accuracy row is now the number of (context, slot) places, not the number of runs. The other big-M constants the task listed were already per reader in the prototype:H[m]uses each reader'sL̄[q],WusesH̄[m]overm's readers, andH_queryusesL̄[q]= min(bound, most).How
independent_partsruns a union-find over columns, joining the columns of each row. A component is keyed by the first query whose output-reference choice falls in it. Components without a query are merged into one last part.solve_selection_milprenames each part's variables into its ownProblemVariables, rebuilds its rows and objective terms, and solves the parts smallest first. Each part gets (time left) × (its variables / variables not yet solved), so time a small part leaves goes to the larger ones. Each part has its own objective scale from its share of the warm start. The status isOptimalonly if every part is optimal. The dual bound is the sum of the parts' bounds. With a single part, the original model is solved unchanged. The parts run one after another (HiGHS uses 4 threads per solve).Key code interfaces added / modified
crates/plan-selection/src/milp/model.rscrates/plan-selection/Cargo.toml,crates/devtools/Cargo.toml,crates/plan-selection/src/milp/lp_diagnosis.rsUsage (devtool run, as in §6.1):
Major algorithm changes
3a, independent parts.
plan_costcheck.independent_parts→ one HiGHS solve per part (smallest first, each with a share of the deadline proportional to its variables) → join the columns → one completion → decode → realize →plan_costcheck.plan_cost= objective check is unchanged. The testindependent_query_sets_solve_apart_to_the_same_planuses two queries overmand one overn. It checks that the split gives parts[[0, 1], [2]], that every column and row lands in exactly one part, and that solving apart and solving whole give the same DAG and the same cost.NoSolution, even when the other parts finished. With a warm start (every measured run), each part keeps the start's plan at worst.Big-M bounds. Both new bounds are still valid upper bounds, so the objective still equals
plan_costatOptimal(every exactness test inmilp::model::testspasses).Ē[m]is smaller in 229 shared contexts of workload-01, by 12–16 %. It equals the old bound on every other workload.N̄_qequals the old bound on every measured workload, and on every fixture I tried: no query there may depend on two approximate runs of one slot in one context.a_tighter_context_execution_bound_keeps_the_planuses a rollup read every 20 s and every 30 s. It checks that theĒcoefficients are at most the old ones and strictly smaller in some row, and that the plan and cost equal those of the model built with the old bounds. The old bounds come from a test-only thread-local switch,tests_loose_big_m.Before this PR
§6.1: workload-01's largest round ends at a 70.9 % gap with the incumbent equal to the warm start (3.5429e-6). The only explanation on record was "weak LP bound".
After this PR
Re-measured with the §6.1 settings (budget 300 s, accuracy enforced,
aws-fargate-us-east-1, raw data retained, workload-01 underulimit -v 8388608). Rows show the selected round, and the largest round where it differs. "Parts" is the number of independent parts (queries in each). "Build" is the model build without the warm start. "Root bound" is HiGHS's bound after its root node, from a separate diagnosis run (pure LP in brackets).plan_cost= objectiveAll costs at
Optimalequal §6.1 (and a same-host rerun of the base commit). No plan changed. On workload-01's largest round, the 2-query part solves to optimality in 0.3 s, and the 7-query part finds the 1.4171e-6 plan within its ~200 s. I have not separated how much of that comes from the split and how much from HiGHS's search path; the tightening leaves the LP unchanged. The t8-t5 largest round is still limited by its 44–52 s build, repeated in four rounds of 114k variables. 63 % of that model's variables (71,676) sit in shareable contexts.LP gap attribution
From
ASAP_STAGE3_LP_DIAGNOSISon the base model (before the big-M change; after it, every LP value is identical). Cost per second. "Gap share" splits (warm start − LP) by the family of the charged variable. The warm start is the largest round's incumbent in §6.1.Σ terms ≥ runon configuration / window termsp(query-context andIwork)x(standing cost of shareable candidates)t_producers)H_query,H,W,V, accuracy, latency, π, raw retentionz(schedule groups)H/W/H_query,V, 7 latency, 8 accuracy, ANDsz/ 9 reader constraintsMechanism. The pure LP avoids the expensive bottom of each plan: the raw scans on workload-01 (
p[q2] c0 Scan, 1.01e-6 each at 1, 0 in the LP), and the ingestion-time pane chain on t8-t5 (x c109 PaneChain, 6.39e-6 at 1, 0 in the LP). It does this by spreading each query's top over many alternative paths (on t8-t5 round 1, about 60 query-timeMatAtQueryresults at 0.017 each), all reading one shared bottom (aMatAtQueryscan atx = 0.017). Row 3 asksx[m] ≥ p[κ, m]per reader, so the shared bottom pays only the largest single read (1/k), not the query's full demand of 1. The ANDs of reach (a ≥ a + p − 1) are 0 at these fractions. None of the big-M families matters: the warm start charges them almost nothing, and dropping them leaves the LP where it is. Step 3's tightening is therefore correct but LP-neutral here. HiGHS's root cuts recover 98–100 % of the best known cost on every diagnosed round except workload-01's largest (72 %), and given about 220 s HiGHS closes that round to proven optimality (1.4171e-6, the same cost as the selected plan).Recommendation for the next step. The measured failures are size and search time more than the root bound. workload-01's largest round now ends at 4.1 % in 201 s and proves optimality at 221 s.
Verification
cargo test -p asap-plan-selection --lib: 314 passed, and 314 passed with--features lp-diagnosis. This includes the newindependent_query_sets_solve_apart_to_the_same_plananda_tighter_context_execution_bound_keeps_the_plan.cargo test -p asap-devtools --test devtools stage_pipeline: 51 passed.cargo fmt --all -- --check: clean.cargo clippy -p asap-plan-selection --all-targets -- -D warnings: clean, with and without--features lp-diagnosis.stage_pipelineruns above, base commit rerun on the same host (it reproduces §6.1 exactly), then this branch; the LP diagnosis comes from separate runs with--features lp-diagnosisand a 3,000 s budget, so their solve times are not comparable.🤖 Generated with Claude Code