Repository navigation
perf(plan-selection): accuracy pair checks reuse built outputs; shared input contexts measured (#1095 §8) - #1156
Merged
Conversation
…ks (#1095 §8) The selection MILP's input preparation spent 42–44 s of the 44–46 s build of rangeheavy-t8-t5's 114,225-variable round in accuracy pair checks: each of ~30k pair checks rebuilt every candidate from scratch and walked every output candidate of the query. A candidate's outputs depend only on the approximate members at or below it, so they are now kept across checks and queries (except outputs built across a cycle of links), and repeated exact stand-ins among a reference's output candidates are skipped where `distinct_for_readers` would drop them. Weights and excluded pairs are unchanged (new test against fresh builds).
…f kept accuracy builds (#1095) - `FRESH_BUILDS` now checks with no kept builds and no skipped stand-in, so the comparison test also covers the skip; a scalar ratio whose approximate values sit under one operand only fails if the first exact stand-in of the other reference is skipped. - A synthetic two-candidate link cycle checks that outputs built across the cycle are not kept, for one member and for a pair. - `check` takes `&mut BuiltBefore` (no `RefCell`); the walk's per-candidate entry is a named struct.
Open
16 of 61 tasks
zzylol
added a commit
that referenced
this pull request
Nov 2, 2026
…ries (#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.
zzylol
added a commit
that referenced
this pull request
Nov 2, 2026
…ries (#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.
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 §8's first open point: the
mcontexts are 32–63 % of the prototype's variables, and "candidates of one operator node (e.g.MatAtQuery(Memory)andMatAtQuery(Disk)) could share one input context". #1155 recommended trying this next, because rangeheavy-t8-t5's largest round (114,225 variables, 63 % in shareable contexts) spends 44–52 s per build, in four or more rounds.This PR measures first. The measurements show that §8's sharing has nothing to share on the §6.1 workloads, and that the 44 s build is not model building: 96 % of it goes to the accuracy pair checks in
groups/accuracy_positions.rs. So this PR fixes the pair checks and writes the shared-context result down as data for #1095 §8. It does not implement shared contexts.What
How
A candidate's outputs in a check depend only on the members of the approximate set that lie at or below it.
Checker::builtreaches its producers only throughreaches, which readsbelow[candidate] ∩ S, andbelow[producer] ⊆ below[candidate]. So the outputs are stored per (candidate, members at or below it) inBuiltBefore, which lives for the wholeaccuracy_positionscall, together with thereader_keytable. A pair check then builds only the candidates above both members and reuses everything else from the single checks. Two exceptions:Walkand is not kept.reader_key("exact"), sodistinct_for_readerswould drop them anyway. This turns each pair check's scan over every output candidate (about 560 on t8-t5) from hash lookups into a bit test.Key code interfaces added / modified
None public. Private to
crates/plan-selection/src/groups/accuracy_positions.rs:Usage (the §6.1 devtool run; nothing changes for callers):
Step 1: where the size and the build time go
Measured on #1155's head with a temporary debug build that dumps each shareable context's owner (operator node, timing, materialization, producers, options) and times each build phase.
Context sizes. "Options" counts the
pvariables of a context.IMatAtQuery(Memory))MatAtIngestion(Memory)pane chains)PaneChainMatAtIngestion(Memory),PaneChainMatAtQuery(Memory)}: 1,440 contexts, 2,160 optionspremovable by §8's sharingThe other five §6.1 workloads look the same. Every operator node with two shareable candidates is a pane chain at ingestion time plus at query time. example1 and ratio-of-quantiles have none. Stage 2 emits no
Diskmedium and no pair ofMatAtQueryandMatAtQueryKeptAcrossEvaluationson one node in these workloads.What the shareable contexts are. On t8-t5's 39,505-variable round, the owners of the 1,268 query-time contexts are
Project(344),SummaryEstimate(344),SummaryMerge(320),PaneChain(240) and a few others. They form nested chains such asProject→SummaryEstimate→SummaryAgg→TimeRange→Scan. AMatAtQuerycandidate of each level gets its own context, and that context holds a copy of theNotMaterializedstack below it (2–9 options each). The copies belong to different operator nodes, each with its own result. Sharing within one operator node, as §8 proposes, cannot remove them.Build phases (t8-t5, 114,225 variables, base):
candidate_dataaccuracy_positionschoice_variables/structural_rows/ acyclic rowsreach_rows/reader_constraint_rows/latency_rows/accuracy_rows/encode_objectivebuild_msOn the 80,601-variable round,
accuracy_positionsran 1,212 single checks (0.43 s) and 28,724 pair checks (15.8 s). Those checks built 16.5 M candidate outputs between them, and only 1.1 s of the 15.8 s wasguarantee_check. Each pair check rebuilt every candidate from scratch and looked up all ~560 output candidates of the query. The number of pairs grows with the square of the pane-chain count, so the build grows faster than the model: 0.03 s at 4k variables, 3 s at 40k, 17 s at 81k, 44 s at 114k. The model building itself (choice_variablesthroughencode_objective) takes 0.6 s at 114k. No phase there is worth fixing now.Exactness argument for shared input contexts (#1095 §3 change proposal)
Suppose shareable candidates
m1andm2of one operator node share one input contextS. Both are read across a boundary, and a reader context picks at most one of them because they fill one slot.S's options and the producers of each input are the same for both. That needs the same timing and the samecompatper input (producer_candidates_per_input). Contexts of different timings compute different things. A query-time context computesNotMaterializedquery-time candidates. An ingestion-time context computes nothing and only readsIand other shareable candidates across its boundary. So an ingestion-time and a query-time pane chain can never share.m1=MatAtQuery(Memory)andm2=MatAtQuery(Disk), with readersR1andR2, whereR1'scompator latency bound admits onlym1, andm2has the lower per-execution cost. When the schedule-group rates add (different intervals), selecting both costsr1(E + e1) + r2(E + e2). MovingR2ontom1costs(r1 + r2)(E + e1), which is higher byr2(e1 − e2). With different readers, or different held time (H[m]follows the slowest reader), both copies can be selected in an optimal plan.plan_costcharges two copies of the input subtree, one at each candidate's rate. The current charge (context runs once, at the owner's rate) would charge one. Decode would also produce one input subtree read by two materialized nodes, which is a different plan.MatAtQueryMemory vs Disk of one node), and addΣ_{m ∈ set} x[m] ≤ 1. Then context activity isΣ x[m], reach and rates read the selected owner, and decode is unchanged. This loses exactly the plans in point 2, where both media serve disjoint readers. That is an O5-style loss: one shared result per operator node, with the second copy useful only when readers differ incompat, latency or per-execution cost. Unlike O5's case, this one differs in cost, so O5's cost-model-1 argument does not cover it, and it would have to be listed as a fourth O4 class.S's runs once per selected owner. UseV_i ≥ E[S] − Ē (x[m_i] − z_i)per owneriand rate component, also for owners at a fixed rate. Keep a separatezper owner for its own run, and decodeSinto one copy per selected owner. This touches rows 3–6, row 7 (L[m_i]per owner over one context),y,W/H, the G4 ranks, the warm start, completion and decode.mcontexts comes from nestedMatAtQuerycandidates of different nodes, each copying theNotMaterializedstack below it (Project → SummaryEstimate → SummaryAgg → TimeRange → Scan). That is a 3b or 3c question, not a sharing one."Major algorithm changes
Accuracy pair checks reuse built outputs (
accuracy_positions,Checker::check,Checker::built).accuracy_positionstakes 42.5 s of the 44.2 s build, and the build repeats in every 114k round.memoandkeys. Build every output candidate of every reference, depth first, and rebuild every candidate that reaches a member. Thendistinct_for_readers, the roots, andguarantee_check.BuiltBeforeperaccuracy_positionscall. 2. Inbuilt: a candidate that reaches no member returns its exact stand-in right away. Otherwise, look up (candidate, members at or below it) inBuiltBefore; on a miss, build it as before and store the result unless the walk read a candidate it was still building. 3. Per reference, skip the exact non-state stand-ins after the first one. 4. The roots andguarantee_checkare unchanged.builtis a function of (candidate,S ∩ below[candidate]) when no cycle is involved. Results that involve a cycle stay per check, as before. Stand-ins and outputs keep their addresses for the whole call becauseKeysholds them, soreader_keygives the same values. A skipped stand-in has the keydistinct_for_readerswould have dropped. Testkept_builds_give_the_weights_of_fresh_buildschecks weights and excluded pairs against checks with no kept builds and no skipped stand-in (FRESH_BUILDS). It covers t8-t5's shape (a quotient plus both operands), a quotient of three quantiles (which has excluded pairs), and a scalar ratio of two output references whose approximate values sit under one operand only. The last one fails if the skip also drops the first stand-in: the ratio then has no root to check, and every approximate candidate would weigh nothing. Testoutputs_built_across_a_cycle_are_not_keptbuilds a synthetic two-candidate link cycle above an approximate member, for one member and for a pair. It checks that the output cut off by the cycle read is not kept, and that a later walk from the cycle's other candidate equals a build with no kept builds. Mutations that skip every stand-in, panic on a cycle read, or keep outputs built across a cycle each fail one of these tests. On all seven workloads every round's plan and objective is unchanged (After).accuracy_positionscall and is shared by every query, because candidates below several queries are reused across them. It is not cleared per query and its size is not reported inASAP_STAGE3_REPORT. It grows with the number of pair checks, about 130 MB at 80k variables, and is dropped before the MILP is built. If larger workloads make it matter, clearing it per query is a one-line change.guarantee_checkand the per-check scan), and their number is quadratic. Pairs are not deduplicated by guarantee.Before this PR
#1155's table: t8-t5's largest round (114,225 variables) builds in 44–52 s. In the 300 s budget that leaves each 114k round about 0.5 s of solve and no dual bound. The incumbent stays 2.0661e-5, and round 1's plan (6.4822e-6) is selected.
After this PR
Same host, same §6.1 settings (budget 300 s, accuracy enforced,
aws-fargate-us-east-1, raw data retained, workload-01 underulimit -v 8388608). Base = #1155's head, then this branch, run one after the other. Rows show the selected round, and the largest round where it differs. "Build" isbuild_ms(input preparation plus model build, without the warm start).plan_cost= objectiveplan_cost= objective everywhere. Variables, binaries and the selected round are unchanged on all seven workloads. The weights and excluded pairs, and so the models, are identical.accuracy_positionsis tens of ms there.Verification
cargo test -p asap-plan-selection --lib: 316 passed, including the newgroups::tests::kept_builds_give_the_weights_of_fresh_buildsandgroups::accuracy_positions::tests::outputs_built_across_a_cycle_are_not_kept. The three mutations above each fail them.cargo test -p asap-devtools --test devtools stage_pipeline: 51 passed. An earlier run that overlapped a clippy build on a full disk failedscans_projecting_different_labels_are_sharedonce; it passed alone and in this clean full run.cargo fmt --all -- --check: clean.cargo clippy -p asap-plan-selection --all-targets -- -D warnings: clean.&mut BuiltBefore, named walk entry) changes no production behaviour, so the measurements in "After this PR" were not repeated.stage_pipelinebinaries on the same host, all seven workloads. The step-1 breakdown and phase timings come from a temporary debug build (context dump and phase timers) that is not in the branch.🤖 Generated with Claude Code