Repository navigation
refactor: stage boundaries milestone 1 — Stages 0–2 and Stage 3's frozen interface (#1047, #1054) - #1108
Merged
Conversation
X2) (#1098) Pass 1 now sizes each memo candidate's approximate inputs for its readers' failure probability over the values their targets cover: delta / (n * V), n the candidate's accuracy-budget parts, V the values the scope covers (G from the workload bounds for the whole result), the smallest over the queries reaching the group; classic HLL precision is raised for its source's distinct bound. This replaces add_scoped_candidates (one sized candidate per split, in place) and Stage 3's resizing. Pass 2 inherits the sizes or takes the strictest shared reader's (size_for_all_readers). Stage 3 deletes sized_for_failure_probability / resize_for_failure_probability, the DAG recompilation after resizing, and RejectionCause::FailureProbabilityResizeFailed; a candidate whose sizes miss the composed target is rejected with AccuracyTargetMissed. template_price_is_a_floor is now always true. Docs updated. Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…step 8b, part 2) (#1099) asap-types: fields and methods that only tests read outside the crate become pub(crate). Tests reach them through accessors and constructors under `test-support` (SummaryCoverage, SelectionBox, ColumnIdentity, MaterializedView, ViewCoverage, MaterializedViewInputs, RawDataWindow, RawDataRetention, PhysicalASAPDAGEdge, FamilyCapability) or through `asap_types::test_support` wrappers for methods the crate itself uses (output_schema, validate_inputs, matches_node, timed, lookback_ms, is_materialized_view). `Schema::has_unique_key` and `compile_physical_asap_dag` are test-only and go under `test-support`. Frontends: the `error`, `histogram`, `promql`, `sql`, `frequency`, `resolve`, `schema_resolver` and `unresolved` modules become private (one public path per item). `SqlLowerer`, `SchemaResolver`, `SchemaCatalog`, `UsageDerivedCatalog` and `resolve_expr` become pub(crate). Single-query MetricsQL entry points and the SQL unresolved lowering move to new `test_support` modules behind `test-support`. Deleted: `HistogramCatalog::is_empty`, `UnresolvedOp::scalar_exprs`. The executor's and integration-tests' fixture code enable `asap-types/test-support` for the test-only DAG helpers. Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: snapshot the selected plans of sketch-selecting workloads The corpus snapshots are single queries without an accuracy target, so their plans are exact and never exercise sketches, their sizes, sharing, materialization or pane chains. Add a second ignored test to the corpus_snapshots binary (so the nightly run picks it up) that pins the selected plans of 17 workloads with accuracy targets: shared DDSketch pane chains, nested windows, a ratio of two quantile sketches, KLL, DDSketch, KMV and a CMS heap, an `or` composition, and random-mixed subsets, under the default, raw-retained and MVP deployments. Each snapshot reuses the corpus snapshot of the selected plan (DAG with sketch parameters, per-node cost, accepted guarantees, materialized views, pane retention) plus every query's text and the search status. Every kept workload is proven optimal or stopped by an evaluation count, and gave the same plan in three runs. A failure names the workload and the first differing JSON path; UPDATE_SNAPSHOTS=1 regenerates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test: keep only optimal sketch plan snapshots; serialize snapshot tests The seed and pane-width rounds have 5 s wall-clock budgets, so a search that stops at any budget can select another plan on a slower machine: pinned to one core, planner-layering-3a changed plan without a status change. Keep only the ten workloads whose search proves its plan optimal, and fail any workload whose status is not optimal instead of recording it. Drop the single-quantile default and raw-retained, planner-layering 3a/3b, `or` and COUNT(DISTINCT) workloads. Hold one mutex for the whole of each planner snapshot test so the corpus and sketch plan tests in the corpus_snapshots binary never run at once under --include-ignored. Mention sketch_plan_snapshots/ in the planner snapshots doc and the PR template. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Stage 3 now computes no cost of its own: the per-node and per-plan
costing that lived in plan-selection's pricing.rs moves to asap-cost,
which depends only on asap-types.
- pricing.rs -> asap-cost usage.rs, re-exported at the crate root:
price_node -> node_usage, PricedNode -> NodeUsage (work_ms ->
work_ms_per_execution), price_at_scale -> plan_cost, CandidateCost ->
PlanCost; ProducerOutput, panes_of, trailing_window, node_map,
kept_pane_chains, pane_retention_ms, query_reach and family_name move
with them. price_each_node stays private.
- execution_demand::schedule_groups(readers, horizon) returns the
reader groups evaluated together (ScheduleGroup { readers,
evaluations_per_second, schedule }); evaluation_rate is their sum,
added in the same order as before, so every rate is unchanged.
- plan-selection changes only imports and call sites; the test-only
self-calibrated price helper moves into lib.rs tests, and checks.rs
reads dag.scan_window(..).lookback_ms() directly.
Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ses (#1095 Steps 1-2) (#1103) Add `classes.rs`: given `PhysicalASAPMemoCandidates`, fold the candidates that are equal in operator node, timing, materialization, query-output and keep flags, reader constraints (other than ActivationRequires) and producer classes per input into one class, by partition refinement to a fixpoint. Each class records its copies (ascending), its capacity (the copy count), its producer classes per input, and the state it holds (#1095 Step 2), which makes it shareable; the others are stateless. ActivationRequires inside a class is absorbed by the capacity; any other one keeps both of its candidates in classes of their own. Only adds the module and its unit tests; the search does not use it yet. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
) One PrunedCandidate { group, candidate, reason } and one PruneReason enum move to asap_types::stages. Pass 1 fills its pruned_candidates with them. Stage 2 replaces pruning_metrics with pruned_candidates, keyed by PhysicalCandidateKey { operator_node, timing, materialization, scope_copy }, and records: PerEntityAggregateNotExecutable (new Stage 2 catalog rule), RawDataNotRetained, InputHasNoBuildableProducer, ReachesNoQueryOutput, TooFewQueryReaders, PreviousScopeCopyPruned. Stage 3 drops its raw-data and per-entity aggregate checks (and their RejectionCause variants); stage_pipeline and the viewer's Memo view show both stages' pruned_candidates. Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…y pruning (#1047 P6b) (#1105) * refactor(plan-selection): delete early query pruning (#1047 P6) Delete `prune_query_candidates` / `alone_violation` (`query_pruning.rs`), the route block that ran it before Pass 1's memo population, and the `early_query_pruning_ms` / `_counts` / `_examples` report fields. Stage 3 still checks every candidate; its pruning is replaced by #1095's region DP, and the per-query "no combination meets the target" check moves to Pass 1 (#1047). This removes the last caller of the legacy whole-workload Pass 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * refactor(logical): delete the legacy whole-workload Pass 2 path (#1047 P6b) Delete `pass2/workload_candidates.rs` (`stage1_candidates_of_choice`) and the whole-workload Pass 2 entry points the memo rules replace: - identical expression: `identical_expression_pass2_candidates`, `sharing_candidates`, `SharingCandidate`, `IdenticalExpressionPass2Candidate` (memo group identity); - summary capability: `summary_capability_pass2_candidates`, `capability_sharing_candidates`, `CapabilitySharingCandidate`, `SummaryCapabilityPass2Candidate`, `MAX_CAPABILITY_SHARING_MEMBERS` and their private helpers (`memo::rules::shared_producer`, `shared_configuration_readers`); - top-k reuse: `topk_reuse_pass2_candidates`, `TopKReusePass2Candidate` (`memo::rules::topk`). Also delete `explicit_assignment_plans` and `memo_covers_whole_workload_pass2.rs`, which compared the memo against this path, and the unit tests of the deleted functions. `planner_layering_example2`'s shared-UnivMon tests read the memo's shared candidates (`shared_pipeline`); the pairwise test is deleted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * docs: describe the memo path where docs named the legacy Pass 2 path (#1047 P6b) `stage_pipeline`'s Stage 1 description, `planner-layering.md`, `asap-aware-mapping-architecture.md` (stage diagram and component table) and `extend-asap-aware-mapping.md` (adding a Pass 2 rule) now name the memo rules and memo enumeration instead of the whole-workload candidates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#1047 P3c, fixes #1089) (#1106) Pass 1 now finds its targets by walking the memo groups of the DagOfLogicalMemoGroups instead of each query's frontend nodes: one target per (aggregate group, measure), however many queries read the group. - A key-only HAVING goes below an aggregate only when every reader of its group (parent groups and query roots, across queries) applies it. Before, this was decided per occurrence and memo population kept the first target per (group, measure), so query A's HAVING city = 'NYC' filtered the summary query B shares and B returned only NYC (#1089). - Fused computations (whole-expression top-k, nested sum) no longer require the inner aggregate to have a single reader. A fused computation is one more candidate of the outer group; the absorbed groups keep their own candidates for other readers, so private_absorbed is deleted. Pass 1 population skips a candidate whose structural id, input groups, fused groups and budget share equal one the group already has. - generate_per_computation_candidates takes &DagOfLogicalMemoGroups; generate_per_computation_candidates_with_memo, the per-(group, measure) choices cache and PerComputationCandidateError::MemoRootsChanged are deleted. - PerComputationCandidates drops Id; its roots are the DAG's roots over the groups' nodes. ComputationTarget.aggregate becomes aggregate_group, with PerComputationCandidates::aggregate for the node. - PerComputationCandidates::walked replaces DagOfLogicalMemoGroups::matches_root_dependencies; original_roots is deleted. Stage1ToStage3Run and CandidateSearchContext drop Id. - Regression test having_shared_aggregate (fails on the base branch: B's shared group returns only NYC). Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ace (#1047 P7) (#1107) * refactor: the facade runs the stages; Stage 3 gets its frozen interface (#1047 P7) Stage 3 (`asap-plan-selection`) now has one public entry, `select_plan(physical, entries, data, &AccuracyCheckInputs, &CostInputs, &dyn AssignmentRealizer, deadline) -> Result<Selection, ErrorSelectingPlan>`, with today's engine behind it and no dependency on Stages 1 or 2. - Selection / SearchReport / SearchStatus follow the decided interface. Whole-plan rejections, rejected_by_cause, search_rejected, the Pareto front (and ClusterObjective::ParetoFront), per-plan cost/accuracy maps, plan id strings, runner-up, pane_widths_pending, exhaustive and keep_every_candidate are gone from Stage 3. - SelectedCandidates replaces SelectedCandidateGraph (no modeled_cost); AssignmentRealizer has one method, realize -> RealizedPlan {plan, pane_retention_ms, nodes_by_candidate}; Stage2AssignmentRealizer holds the planning context; pane retention moves to asap_cost and is computed by realize. - The engine searches the whole catalog once per call: no component split, no per-query seeding; its heuristics read only PhysicalASAPMemoCandidates. - asap-planner runs Stage 0, Pass 1, the pane-width rounds (Pass 2 -> Stage 2 -> select_plan), keeps the best round by asap_cost::PlanRanking, builds the plan with realize and applies the proven weights. The pass API, PlanningModels, PlanOutput (now with pane_retention_ms) and PlanningRun / PlanningReport live there. - The old whole-workload selector stays in plan-selection behind the reference-pricer feature; stage_pipeline --exhaustive is the devtools reference enumeration. - Docs and the DAG viewer's Stage 3 panel follow the new document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * refactor: ignore_accuracy_guarantees leaves CostInputs; the facade passes the accept-all model (#1047 P7) #1095 §1: `ignore_accuracy_guarantees` is no longer a `CostInputs` field. The facade's `PlanningModels::ignore_accuracy_guarantees` (`with_ignore_accuracy_guarantees`) makes `accuracy_check_inputs()` pass `asap_accuracy::IgnoreAccuracyGuarantees` to Stage 3, which no longer swaps models itself. The engine's accuracy heuristics read `AccuracyModel::may_accept_regardless_of_guarantee` instead of the field. - `IgnoreAccuracyGuarantees` moves from plan-selection to asap-accuracy. - `stage_pipeline` sets the models flag from `--enforce-accuracy-guarantees`; a `--plan-selection-settings` file with the field is refused. - Tests that set the field now set the models flag (facade) or the accept-all model (Stage 3). - `SearchStatus::DeadlineReached` documents that it also covers the engine's 128-evaluation stop until #1095; the reference pricer notes its calibration scale under `--price-enumerated`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cal, executor) (#1109) * test(executor): set the semijoin edges' timing through data_state_mut #1099 rewrote `edge.data_state.timing = phase` as `edge.data_state().timing = phase`, which assigns to a copy: the edges kept QueryTime while the nodes moved to IngestionTime, and bind rejected the DAG. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * test(logical): update Pass 1 expectations for memo-group targets (P3c) #1106 (P3c) walks memo groups: structurally equal aggregates and HAVING filters are one group, with one target per (group, measure) and HAVING placed per group; roots are named by workload index, and an inventory matches any DAG built from structurally equal roots. - having_on_one_consumer_of_a_shared_aggregate_stays_above: two equal HAVING filters are one group, so one entry. - memo_cached_rules_preserve_occurrence_specific_having: the two equal aggregates are one target; its HAVING stays above the output because an unfiltered consumer reads the group. - assigned_physical_annotation_asap_input_and_invalid_accuracy_are_rejected: InvalidAccuracy names the root by workload index ("0"); doc comment updated. - population_rejects_an_inventory_from_other_roots: the other roots now differ structurally (the same query text is the same DAG). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * test(frontend-promql): move planner-facade helpers out of support.rs support.rs is also compiled by the private corpus, which does not depend on asap-planner. run_stage1_to_stage3 and selected_dag (asap-planner since #1107) move to planner_support.rs, used only by this crate's test binary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…utput validation (plan-selection, planner) (#1110) * fix(plan-selection): decoded selections assign each query output once P7 (#1107) made validate_selected_graph check the selection through to_selected_candidates, which sorts the roots by output index and drops the index. A root assigned to a nonexistent output (or two roots to one output) then passed validation. Check that the assigned output indices are exactly 0..n before converting, as the checker did before P7. decoded_graph_is_checked_independently_of_solver_values covers it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * test(plan-selection): kept-pane tests' realizer retains query-time results P7 (#1107) moved the query-time-results-retained capability from the planning models into the Stage 2 realizer. The two kept-pane pricing tests still set it only on their deployment, so the test realizer refused MatAtQueryKeptAcrossEvaluations(Memory). Build the realizer with the deployment's flag. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * test(planner): no-raw avg-of-p99 workloads expect no selected plan, not an infeasibility label Both tests asserted the infeasible_under_configured_model status and the early pruning's rejection text. P6b (#1105) deleted early query pruning, which proved these catalogs infeasible, and P7 (#1107) removed that status from Stage 3's report: today's engine now stops at its budget with no validated plan. The tests now assert that plan selection selected none (no valid candidate, or none by the deadline); the ignore-accuracy test's selected sketch plan is still checked end to end. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ion applies Stage 2's raw-data rule (#1111) * fix(devtools): the reference enumeration drops plans that scan raw data a deployment does not keep #1047 P6 moved the raw-data rule from Stage 3's checks into Stage 2's catalog (PruneReason::RawDataNotRetained). The devtools reference path implements Stage 2 with optimize_physical_asap, which enumerates every assignment, so stage_pipeline --exhaustive, --price-enumerated and the integration tests' explicit fixtures priced query-time raw scans that the deployment cannot run, and selected them. memo_pricing::retain_deployable applies Stage 2's rule; physical_plans_of and run_explicit_candidates use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * test(devtools, dag-viewer): read Stage 3's Selection document #1047 P7 made stage3_plan_selection Stage 3's Selection (the plan P1 with its cost, accepted_accuracy_per_query and search_report) and moved the per-candidate table under --exhaustive to reference_pricing; the facade's PlanningReport is planning_report. The stage_pipeline, memo_pricing and viewer tests, and server.py's log line, read that shape; a Pass 1 failure's Stage 3 section is a Selection without a plan. Deleted: the Pareto-front check (ClusterObjective::ParetoFront is gone) and the component and candidates-checked report fields (the engine has no components). viewer_contract reads a JS array up to its own ']'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * test(integration): fixtures follow P3c's shared composition and P7's planning budget - Pass 1 now composes a candidate from memo groups (P3c), so queries that read an identical input share its node. The Example 1 and 3 fixtures build their independent variant with planner_layering_common::unshare, and materialization_e2e splits a node read at two timings (split_shared_by_timing) as Stage 2 does. - memo_enumeration: one target per quantile group (3 x 3 inventory assignments, P3c); a 90 s budget, since Stage 3 searches until the deadline when it validates no plan. - finer_aggregate_reuse::filtered_kll_states_cost_no_more_than_separate_plans is ignored until #1095: P7 removed the per-query seed that reached the separate plans' cost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ports the planning budget (#1113) * fix(planner): report a Pass 1 deadline as the planning budget running out With a zero planning budget, Pass 1's sizing hits its deadline and the facade reported it as "logical ASAP-aware optimization: candidate generation deadline exceeded". Map PerComputationLogicalMemoCandidatesError::Timeout to SelectionError::DeadlineReached, as before P7; other Pass 1 errors keep LogicalASAPCandidates. Fixes joint_search_equivalence::zero_budget_returns_no_unvalidated_plan and stage_pipeline::milp_time_limit_is_reported. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * fix(plan-selection): count quantile operands independently of node sharing The feasibility seed's DDSketch budget splits a query's relative error over its quantile inputs. It counted distinct quantile nodes by Rc identity. Since P3c (#1106) Pass 1 builds each query's root over the memo groups' nodes, so the two `quantile 0.5` operands of `avg by (job) (q0.5 / q0.5 + q0.9)` are one shared node: the count fell from 3 to 2, the seed admitted sketches sized for half the budget, whose outputs have no accuracy guarantee, and the search spent its whole budget cutting such plans one by one without reaching the exact plan. quantile_operands counts each quantile aggregate as often as the most operands that read it from one path (q / q reads q twice), and a diamond of different parents over one aggregate (an average's sum and count) once, so the count no longer depends on how equal nodes are shared. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ay public API (#1114) tools/verify_metricsql_external_consumer.sh (CI) treats them as a stable external dependency; #1099 had moved them behind test-support. Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…accuracy weights (#1095 C3) (#1115) New `plan-selection/src/owners.rs` (pub(crate), not called by the search yet): `prepare_milp_inputs` computes, from the folded classes, - class dominance pruning (exact under cost model 1), with the pruned classes and the dominating class; - per-class data: approx, accuracy_budget_parts (smallest over a factored node's memo candidates), usage per execution, cost per execution, standing cost, held bytes and own latency, all from asap_cost::node_usage, with the context options (producer outputs, scan windows, pane retention) that change the usage recorded; - query owners and instance owners (m_s = min(cap, reaching queries)) with their regions, links and boundary reads; - accuracy weights w(q, k): 1 ordinary, 2 denominator, unusable for nested quotients and unclear operators; exact divisors scale and count aggregates read no value. asap-cost exposes `retained_panes` and `maintained_window_ms` (visibility only). Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cy target (#1116) * feat(pass1): prune summaries no reading query can use for its accuracy target Pass 1 now removes a summary candidate (own or fused) when, for every query whose root reaches its aggregate group, the query's output composed with that summary (every other target exact) has no error bound that can meet the query's target at any size: guarantee propagation has no rule above it (e.g. a rank error through avg, a value HAVING over an estimate), its bound stays unknown once the data workload's sign declarations are bound (a relative error through avg over an undeclared gauge), or its metric is not one the target accepts. Other symbolic statistics are assumed bindable. A summary one reading query can use is kept; exact candidates are never pruned. The reason is the new PruneReason::NoErrorBoundForQueryTarget, carried in Pass 1's pruned_candidates. The facade prunes only when Stage 3's accuracy model and evidence cannot accept an output without its guarantee (not with ignore_accuracy_guarantees). Stage 3 reports a query root group left with no candidate as no valid candidate rather than a solver failure. The two no-raw avg-of-p99 runtime_install tests assert the reason again, on the facade's MemoSteps::pass1_pruned_candidates. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * feat(pass1): keep summaries Pass 2 may reuse; ask the accuracy model; check whole-result scope Follow-ups to the structural accuracy prune: - Pass 1 cannot see Pass 2's reuse of one group's summary by another (rollup, window rollup, finer-aggregate reuse, top-k reuse). A summary is now kept whenever another target, over another aggregate reading one of the same sources, offers the same family. This keeps more than those rules' preconditions need and relies on no dominance argument. - The metric check asks the deployment's accuracy model through a new default method, AccuracyModel::metric_can_meet. Its default is the built-in rule; the facade passes the model Stage 3 uses. - The guarantee is stated over the target's scope, as Stage 3 states it. A per-value guarantee that may fail cannot meet a whole-result failure probability over a number of values the data does not bound, so it is pruned with the same reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 30, 2026
Merged
Open
…ng guarantees (#1118) #1116 makes Pass 1 prune a summary no reading query can use for its accuracy target when guarantees are enforced. Seven integration tests read the reference enumeration under the built-in strict model to check the structure or execution of a summary whose output Stage 3 already rejected at that target before #1116: a heap-backed Top-K and a value filter or group key over an estimate have no guarantee, and a grouped KLL quantile's per-value guarantee meets no whole-result target over groups the data does not bound. Pass 1 now prunes those summaries, so the tests found no candidate. The tests now plan with every output accepted (logical_candidates_ignoring_guarantees, ignore_accuracy_guarantees), as they check rewrites and execution, not accuracy acceptance. Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…edCandidates (#1095) (#1119) * refactor: Stage 2 emits no scope copies; selections are a DAG of candidate runs (#1095) Stage 2 emits one PhysicalCandidate per (operator node, timing, materialization). ReaderConstraint::ActivationRequires, PhysicalCandidateKey::scope_copy and PruneReason::PreviousScopeCopyPruned are deleted, with catalog/scopes.rs. The selection is DagOfSelectedCandidates { candidate_of_node, edges, node_of_output_reference } with SelectedNodeId and SelectedEdge; its validate(physical) replaces validate_selected_connections and checks each reader constraint per node. SelectedCandidates and ProducerConsumerConnection leave the public API. The realizer builds one plan node per selected node (RealizedPlan::operator_node_of_selected_node); Selection carries selected_dag and nodes_with_proven_non_negative_weights. Today's Stage 3 engine makes the copies it needs itself (search/candidates/scopes.rs, moved from Stage 2) and decodes the selected copies into nodes; its activation constraint and connection type are private. Plans, costs and MILP sizes are unchanged on the memo examples. classes.rs is deleted: owners.rs works on PhysicalCandidateId, m_s is the number of queries that can reach s, and capacity is gone. stage_pipeline writes selected_dag and per-round Stage 2 producer links (PlanningRoundReport::physical_producer_links); the viewer drops scope_copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WviZ6EjGTm6ZuggMKX1mjr * docs: no scope copies in Stage 2; Stage 3 selects a DAG of candidate runs (#1095) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WviZ6EjGTm6ZuggMKX1mjr --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…C4) (#1125) * feat(types): Stage 2's operator nodes carry accuracy_budget_parts (#1095) Each OperatorNodeOfCandidateForMemoGroup now holds how many equal shares of a query's accuracy budget its sketch sizes meet, copied by Stage 2 from its memo candidate (the largest for a factored node). Stage 3's owners.rs reads it and drops its parts_per_memo_candidate parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw * feat(plan-selection): the owner-scoped selection MILP and its cost model 1 terms (#1095 C4) Builds the §2.0 MILP from MilpInputs over Stage 2's candidates (no scope copies, #1119) and solves it once with HiGHS, the deadline as its time limit, returning the raw solution (outputs, each owner's candidates and links, active instances) for C5 to decode into a DagOfSelectedCandidates. - milp/model.rs: x, s, e, b, o/ob, reach a (exact from above where a reader constraint counts readers), latency L with the evaluator's per-node definition, accuracy A1 (one share per approximate instance, denominators need parts >= 2, unbounded positions 0), reader constraints, symmetry breaking. m_s is the number of queries that can reach s; there is no capacity row. - milp/cost_terms.rs: execution at the query's rate or the instance's rate (schedule-group levels for materialized instances, per-consumer rates for not-materialized ones), standing cost, held memory, raw retention levels, usage on producer links with AND rows, scan window levels. Every number comes from asap-cost; request fees are reported apart. Cost models 2/3 return UnsupportedCostModel. Terms plan_cost defines differently are counted as inexact by reason. - owners.rs: per-producer and per-window usage tables; MilpInputs' parts renamed to candidate names (candidate_data, CandidateData, pruned_candidates, DominatedCandidate, producer_candidates_per_input, output_candidates, CandidateUsage). - Tests: objective equals plan_cost after decode + Stage 2 realization (optimum, shared MatAtQuery held by readers of different latency, raw retention over two windows, a reader costed at its shared scan's window), latency bound, accuracy rows, symmetry breaking, cost models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WviZ6EjGTm6ZuggMKX1mjr * feat(plan-selection): the selection MILP finds plans: warm start, tighter rows, usage per producer output (#1095 C4) - Warm start (milp/warm_start.rs): each query planned alone with no sharing (cheapest derivation over the candidates it may use, its own instance of each shareable candidate), repaired for accuracy and latency; HiGHS completes the continuous variables from its binaries. A deadline now ends with a plan. If a query has no plan alone, the solve runs without a start and an infeasible model names that query (NoPlanForQuery). - Accuracy rows: N[q] as one variable and one big-M row per parts value, instead of a dense sum per instance (q05-q09: 1.89M -> 6k nonzeros). - Rates: a fixed rate * x[i] where all readers run an instance at one rate; otherwise rate components (schedule-group levels, consumers, and AND(z_m, y) for materialized consumers instead of the big-M P rows). Region execution is summed once per instance and charged per component (V), replacing Q per node; held time at a fixed rate needs no W. - Latency: an instance region's work is one variable u[i], charged per reaching query (u[q, i]), replacing w per node. - owners.rs: usage tabulated per producer output, transitively (each context's own output is recorded and readers key on it); a reader's term sits on AND(link, the producer node's terms giving that output), with equal outputs merged. "Depends on context" goes from 3,382 to 0 on #1036 workload-01; the remaining gaps are producers on a memo cycle. - Tests: the warm start is a feasible plan costing what plan_cost says; a short deadline yields a plan; no usage-table gap in the fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WviZ6EjGTm6ZuggMKX1mjr * fix(plan-selection): HiGHS runs on 4 threads; objective scaled by the warm start's cost (#1095 C4) - A thread per core gave HiGHS a malloc arena each: on a 56-core host #1036 workload-01 broke the 8 GB `ulimit -v` with std::bad_alloc at 4.8 GB resident. With `threads = 4` it plans within the limit. - The objective was scaled so its largest coefficient is 1,000, which left the costs actually compared near HiGHS's tolerances (workload-01's dual bound came out negative; rollupheavy-t11 stopped at 69.86 as "optimal" though 54.44 exists). It is now scaled so the warm start's cost on its binaries is about 1,000, the largest coefficient at most 10^7. - The HiGHS log switch read from the environment is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WviZ6EjGTm6ZuggMKX1mjr --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…P_STAGE3_SEARCH=milp (#1095 C5) (#1127) * feat(types): Stage 3 prune reasons DominatedBy and UsageNotCostable (#1095 C5) Stage 3 records what it removes before its selection MILP in Selection.pruned_candidates: a candidate another dominates (named), or one asap-cost could not cost, which the model leaves out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WviZ6EjGTm6ZuggMKX1mjr * feat(plan-selection): decode the selection MILP, realize once, check, behind ASAP_STAGE3_SEARCH=milp (#1095 C5) - milp/decode.rs: the raw solution as a DagOfSelectedCandidates. Each owner's use of a stateless candidate is one node, each active instance (s, j) one node; edges follow the selected e and b links (an instance owner's links into its own candidate are the instance node's inputs); outputs follow o / ob. Only what the outputs reach is kept. Replaces the test-only decode in milp/model/tests.rs. - select.rs: the new search. One MILP solve, decode, validate, one realization (a rejection is RealizerRejected), then exact checks: plan_cost minus request fees equals the objective within 1e-6 relative (else ObjectiveDiffersFromPlanCost, or SearchReport.inexact_objective when the model counted inexact terms); accuracy (satisfies) and latency must hold (else SelectedPlanFailedCheck). Fills Selection: count-min weight proofs per node, PlanCost, accepted accuracy, pruned candidates (dominated, uncostable), and the search report (status, dual bound plus request fees, MILP size, timings). No plan: NoValidCandidate with a RejectionCause::Infeasible (or LatencyBoundExceeded) rejection naming the query the warm start found no plan for. - select_plan runs it when ASAP_STAGE3_SEARCH=milp; the old engine stays the default until C6. A measured cost table needs a frozen calibration factor (UnsupportedCostModel otherwise), as do cost models 2 and 3. - The facade keeps the new error variants when it copies a round's error; the viewer lists the new rejection cause. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WviZ6EjGTm6ZuggMKX1mjr * test(devtools): the MILP search's objective equals plan_cost on example 1 and RangeHeavy T5 (#1095 C5) Runs stage_pipeline with ASAP_STAGE3_SEARCH=milp on the child process and documents the switch in the binary's header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WviZ6EjGTm6ZuggMKX1mjr --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…on MILP (#1095) (#1128) - warm start: local search from the unshared, amortized and sequential plans (re-plan a query, move every reader off a candidate, merge two instances), every plan costed exactly by the MILP's own rows (new milp/completion.rs); HiGHS gets every variable's value. - cost rows: the -x[i] form of the y and V AND rows, a slowest-rate row on V, per-query big-M on H and W. - fewer AND rows: producer outputs with equal usage merge when no reader tells them apart; scan levels below a reader's own window count as its own level. - latency rows of a binding bound are divided by the bound. - a deadline the warm start used up ends with it, reported as the deadline; SelectionMilpSolution.warm_start_cost_per_second.
… summaries; parts record the sized shares (#1095) (#1129) The A1 accuracy rows now take each position from asap-accuracy's composition and Stage 3's guarantee_check, so the selection MILP admits exactly the single and pairwise placements satisfies accepts. A failing pair gets a[q,k1] + a[q,k2] <= 1, and AccuracyWeight::Two is gone. Pass 1 changes: - The reuse escape keeps a summary only for a reusing target whose own queries can use the family. - Classic HLL is pruned when the declared distinct bound is outside its contract at precision 18. accuracy_budget_parts now records the shares a sketch was sized for, for both shared builds and state-configuration rewrites. Fixes ratio-of-quantiles, rollupheavy-t11 and rangeheavy-t8-t5 under ASAP_STAGE3_SEARCH=milp with guarantees enforced.
…1130) Candidate links can close cycles (a widened scan's Project lists its own memo group, itself included, as producers), and rows 2-3 of the selection MILP admitted a self-supporting cycle with no source below it, so rangeheavy-t7-t4 failed DagOfSelectedCandidates::validate. Row 5: no class reads itself in an owner, and order ranks on the links inside each cyclic component of an owner's links and of the instances' reads. rangeheavy-t7-t4 now plans at 2.149e-7/s (optimal, equal to today's engine); other memo examples are unchanged.
A test reads cargo metadata and fails on any [dependencies] edge between workspace crates that the target graph forbids (stage crates never depend on each other; asap-accuracy and asap-cost depend only on asap-types; the facade on stages and shared crates; devtools and integration-tests unrestricted), naming the edge and the rule. Exceptions: the optional logical -> frontend-promql edge behind test-support, and planner -> executor (#1016).
…engine is the only engine (#1133) refactor(plan-selection): remove the #1095 Stage 3 redesign; the old engine is the only engine The redesign (owner-scoped selection MILP, its decode, warm start and completion, owners and accuracy positions, the ASAP_STAGE3_SEARCH switch) is deleted so the integration branch can merge to main while Stage 3 is redesigned. It stays recoverable at the tag stage3-owner-milp-redesign. - plan-selection: delete milp/, owners.rs, owners/, select.rs, select/; realize and the count-min weight check move back into search/select.rs; remove InexactObjective, SearchReport.inexact_objective, ErrorSelectingPlan::{UnsupportedCostModel, ObjectiveDiffersFromPlanCost, SelectedPlanFailedCheck} and RejectionCause::Infeasible. - types: remove PruneReason::{DominatedBy, UsageNotCostable}. - planner: remove the matching clone_error arms. - cost: retained_panes and maintained_window_ms are private again. - devtools: remove the MILP search tests and the switch's header lines; dag-viewer drops the infeasible cause label.
… longest reader (#1095) (#1134) plan_cost charged a NotMaterialized node's query-time working memory for the rate-weighted mean latency of every query reaching it, times its computations per second. When a materialized node reads it, the node is computed once per execution of that materialized node and its working memory is held until that node's slowest reader finishes, as the MatAtQuery state itself is. plan_cost now charges each consumer's copy (asap_cost::execution_demand::consumers): a query's for its own latency at its rate, a materialized node's for its longest reader's latency at its rate. computations counts the same consumers. The default search's direct-cost model (selected_cost, encode, general_schedule::encode) certifies its objective against plan_cost, so it charges the same per-consumer term; the mean-lifetime B/Q and equality encodings are replaced by gated per-consumer lifetimes. Stages 0-2 are unchanged; only Stage 3 costs (and so plans) change.
…r behind a feature (#1054) (#1135) The catalog module is private: the planner's items (generate_physical_asap_memo_candidates, Stage2AssignmentRealizer, ErrorGeneratingPhysicalASAPMemoCandidates and its error types) are re-exported at the crate root, test-only catalog items move to test_support::catalog, and the re-exports of asap-types items (PhysicalWorkloadPlan, PhysicalCandidateError, RealizedPlan) are dropped. optimize_physical_asap compiles only under cfg(test) or the new reference-pricer feature, which devtools and integration-tests enable and test-support implies.
#1136) - operator_statistics is a private module; EdgeStatistics, which NodeUsage.output and ProducerOutput.output expose, is re-exported at the crate root. - analytical: ResourceEstimate, PhysicalOperator, the Promql* enums, HashJoinBuildSide and estimate_operator are pub(crate); only AnalyticalCostError stays public. - measured: aggregate_lookup is pub(crate).
…ts, CandidateMemo → MemoCandidates) (#1137) budget_share becomes accuracy_budget_parts in the stage_pipeline JSON (accuracy_budget_parts, accuracy_budget_parts_1), the DAG viewer and its fixtures, and the logical crate's internal names (compose_logical_candidate_with_accuracy_budget_parts, accuracy_budget_parts_per_measure, max_accuracy_budget_parts). CandidateMemo becomes MemoCandidates, the name #1047 gives it, with no alias. It stays pub in asap_types::stages: asap-types defines it as a field type of the stage-boundary outputs and the logical crate builds it, so it cannot be crate-private.
…s own group (#1138) Scan-label widening recomposes the guarantees of every root it returns, including roots it left unchanged. The rule compared each returned root with the stored original, which has no recomposed guarantee annotation, so a scan or time range that already had every label of its metric looked widened. Interning then mapped it back to its own group and wrapped it in an identity Project: a candidate reading the group it implements. Stage 2 wired that candidate to every candidate of its group, itself and its execution-scope copies included, so the physical candidate graph and the class folding of #1095 had cycles. The no-op readers also counted as a second identical build, which made single-reader shared state groups. The rule now compares with the recomposed original. The memo's topological order counts a group reading itself as a cycle, so Pass 2's existing acyclicity checks reject such a memo. Claude-Session: https://claude.ai/code/session_01K1M6bqhAk7WA4MZcfbAoMw Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ing form (#1047 P6) (#1139) generate_per_computation_logical_memo_candidates runs all of Pass 1 (targets, deployment and accuracy pruning, population, sizing); the facade calls it once and PlanningRun.per_computation_candidates holds its output. The target list (per_computation_candidates) is public only with test-support. Removed: stage_pipeline's pass1_pruned, MemoSteps.pass1/pass1_pruned_candidates, ComputationTarget.raw_data_samples_of, and from_roots' Id parameter. Plans, costs and Stage 0-2 counts unchanged on example1, rangeheavy t5-q05-q09 and t8-t5.
PlanSearchSettings gains inspect (default false). Without it run_planning copies no memo: no Stage 0 copy, no Pass 1 clone, no per-round unexpanded-memo clone, no MemoSteps; PlanningRun.per_computation_candidates and memo_steps are None. stage_pipeline and the reference enumeration set it. MemoSteps.stage0 is removed (Stage 0's groups are Pass 1's). Plans, costs and counts unchanged with inspect on or off.
…ingModels.plan_selection to cost_inputs (#1054) (#1141) The CostInputs type was already renamed; this renames the stage_pipeline flag, the PlanningModels / SelectionModels field, the with_plan_selection builders, and the prose that still said "plan selection settings". No alias for the old flag; behavior unchanged.
memo_enumeration::choices_whose_candidates_read_each_other_are_left_out looked for two groups whose candidates read each other. On its workload the only such pair was a scan reading its own group, the self-loop #1138 removed. Pass 2 now rejects any memo whose groups read each other, so the test checks that production's memo has a dependency order and no self-reads, that every enumerated choice composes, and that a cycle injected by hand is still reported as Cycle. sharing_issue_regressions::exact_coarse_sum_reuses_fine_sum is ignored with a pointer to #1142. Stage 2 still offers the cheap plan, but Stage 3 keeps a MatAtQuery build whose NotMaterialized sibling would avoid a second scan. Before #1138 the invalid self-reads made shared-build candidates that hid this. Stage 3 MILP work is paused. asap-cost's held_memory test calls optimize_physical_asap, so its asap-physical-asap-optimization dev-dependency now enables reference-pricer; cargo check -p asap-cost --all-targets builds.
…se (#1054) (#1145) pass1/pass2/deployment_capabilities become crate-private; the crate root re-exports the boundary items once each, and test_support re-exports the internals other crates' tests call. asap_aware_cse is crate-private. computation_candidates_for_intent and the unused pub items become crate-private or test-only. split_shared_by_timing moves from asap-types to Stage 2, its only production caller.
) (#1146) Move DagOfLogicalMemoGroups and PerComputationLogicalMemoCandidates, with the types Pass 1's output holds (ComputationTarget, FusedComputation, HavingEntry, HavingPlacement, HowToComputeAggregateMeasure), from asap-logical-asap-optimization into asap_types::stages, next to the other stage-boundary types. Callers import them from asap_types::stages; the logical crate does not re-export them. Hash-consing stays in the logical crate, so DagOfLogicalMemoGroups::from_roots becomes the free function generate_dag_of_logical_memo_groups; the type gets a constructor DagOfLogicalMemoGroups::new for it. No behavior change.
…3's Selection (#1047 P8) (#1147) stage_pipeline's reference_pricing block now uses Selection's field names: selected -> selected_candidate, costs -> cost_per_candidate, accepted -> accepted_accuracy_per_candidate, rejected -> pruned_candidates (entry id -> candidate), search_rejected -> pruned_candidates_not_enumerated, rejected_by_cause -> pruned_candidate_count_by_cause. The viewer's selection table takes the same names and still reads archived documents' older Stage 3 section; the dead runner-up and editor rejection reads are gone.
…c warning (#1047, #1054) (#1148) Crate headers (logical, physical, per_computation_candidates) and design docs (planner-layering, asap-aware-mapping architecture/contracts, joint-planning implementation plan) match the code after #1133-#1147. cargo doc --workspace --no-deps prints no warnings, with or without --all-features (69 + 2 fixed). Documentation only.
…e facade's slower planning (#1150) test(devtools): snapshot harness reads Stage 3's Selection; regenerate snapshots The corpus and sketch plan snapshot harness still read the old stage3_plan_selection keys (selected, costs, accepted, search.candidate_connections). Since the selection became DagOfSelectedCandidates, stage_pipeline writes the selected plan as Stage 2 candidate P1 with cost, accepted_accuracy_per_query and search_report, so a regeneration wrote every corpus snapshot with a null plan and every sketch workload failed with status null. - plan_snapshot reads P1, cost, accepted_accuracy_per_query. - proven_optimal: search_report.status is optimal and no pane width is pending (planning_report.pane_widths_pending). - hit_time_limit sums planning_report's Pass 1 and per-round times. Snapshot changes against the committed ones: - o11y_bench q02, q06, q22, q23, q26: proven_optimal false -> true. Same plan and cost; the facade now searches every pane-width round up to Complete, where the search proves the plan optimal. - fuzz-issue903-raw-retained: KLL shown at the size that runs (k 345 -> 567, failure probability 1e-3 -> 1e-7, from Pass 1 sizing); cost +1.8e-10 relative from charging working memory for the longest reader. - quantile-single-mvp: KLL shown at the size that runs (k 52 -> 86, failure probability 3e-5, from Pass 1 sizing); status optimal -> deadline_reached. Same cost. - shared-quantiles-1h-mvp: status optimal -> deadline_reached. Same plan and cost. The facade's slower planning is accepted (plans are unchanged): - A corpus query may carry a time_limit_reason; reaching the time limit then records and compares its plan instead of failing. o11y_bench q05 has one: it now reaches the 240 s limit (16 s before) with the same plan and cost. - A sketch workload may carry a deadline_reached_reason; it may then end deadline_reached (after every pane width), and its plan, cost and status are still compared. quantile-single-mvp and shared-quantiles-1h-mvp have one: their MILP stalls numerically about 8e-7 relative below the cost.
This was referenced Nov 2, 2026
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.
Milestone 1 of the #1047 / #1054 refactor: Stages 0–2 are restructured behind stage boundary types, and Stage 3's
select_planinterface is frozen for the #1095 rewrite. Closes #1089.Why
The planner's stages called into each other, and Stage 3 re-sized sketches, carried Stage 2's execution-scope copies and drove the pane-width rounds itself. #1047 and #1054 give each stage one input type and one output type, and let the facade orchestrate them. #1095 can then replace Stage 3 behind a fixed interface. Every step was reviewed and merged into the integration branch
refactor/stage-boundaries. This PR merges that branch intomainafter the merge gates below.What
asap_types::stages. Pass 1 has one entry, walks memo groups, and decides every sketch size (X2). Pass 1 prunes summaries that no reading query can use for its accuracy target. Stage 1's public API is narrowed to what other crates use.pruned_candidates. Each item has one public path, and the reference pricer sits behind a feature.select_planinterface and returnsDagOfSelectedCandidates; it only checks sizes. The experimental [Feature] Stage 3: no execution-scope copies, exact candidate-level MILP, one cost formula #1095 redesign is removed (refactor(plan-selection): remove the #1095 Stage 3 redesign; the old engine is the only engine #1133); the old engine is the only engine.asap-cost. A materialized consumer's working-memory copy is held for its longest reader.budget_sharebecomesaccuracy_budget_parts,CandidateMemobecomesMemoCandidates, and--plan-selection-settingsbecomes--cost-inputs. "Producer" replaces "supplier".cargo docwarnings.Included PRs
git log origin/main..origin/refactor/stage-boundaries, oldest first:How
Each step moved one boundary at a time. Stage 1 hands Stage 2 its candidates through the
asap_types::stagestypes, Stage 2 hands Stage 3 the physical candidates, and no stage calls another stage: the facade (asap-planner) calls them in order. The constituent PRs describe each step in full.Key code interfaces added / modified
The constituent PRs list each interface change with permalinks. The ones that callers outside the planner see:
asap_types::stages: the Stage 0, Pass 1, Stage 2 and Stage 3 boundary types (refactor: Stage 0 and Pass 1 boundary types in asap_types::stages (#1054) #1146, refactor: Stage 2 emits no scope copies; the selection is DagOfSelectedCandidates (#1095) #1119).select_plan, which takes the candidates and a deadline and returnsSelection/DagOfSelectedCandidates(refactor: facade runs the stages; Stage 3's frozen select_plan interface (#1047 P7) #1107, refactor: Stage 2 emits no scope copies; the selection is DagOfSelectedCandidates (#1095) #1119, refactor(devtools, dag-viewer): name reference_pricing keys as Stage 3's Selection (#1047 P8) #1147).stage_pipeline --cost-inputs(was--plan-selection-settings), andPlanningModels.cost_inputs(wasplan_selection) (refactor: rename --plan-selection-settings to --cost-inputs and PlanningModels.plan_selection to cost_inputs (#1054) #1141).stage_pipelineJSON: Stage 3's output shows the selected plan as Stage 2 candidateP1, withcost,accepted_accuracy_per_queryandsearch_report.planning_reportshows the facade's rounds (refactor: Stage 2 emits no scope copies; the selection is DagOfSelectedCandidates (#1095) #1119, refactor(devtools, dag-viewer): name reference_pricing keys as Stage 3's Selection (#1047 P8) #1147).Major algorithm changes
seed_separate_plans) is gone. The plans are unchanged on the snapshot workloads, but planning takes longer (see the waived planning-time threshold) and some Stage 3 searches end worse (see the accepted losses).Merge gates
The gates compared
main(b822553, same planner as the merge base) with the integration branch (7e268d4, before #1148, which is docs only).Plan diffs (22 snapshot workloads: 12 corpus queries, 10 sketch workloads)
No plan or cost changes in Stages 0–2. All 22 selected plans have the same cost; fuzz-903 differs by +1.8e-10 relative. Rows whose plan display, cost or status changed:
#1106, #1113, #1116 and #1138 change none of the 22 snapshot plans. The snapshot harness read Stage 3's old JSON keys after #1119. #1150 fixes it, regenerates the snapshots and accepts the three rows above that the harness used to reject (see "Waived planning-time threshold").
Retro-check (67 runs per mode, integration vs main, 30 s budget)
--enforce-accuracy-guarantees)Guarantees ignored:
Guarantees enforced:
Accepted Stage 3 losses (until #1095)
In each case the cheaper plan's candidates are in Stage 2's output; Stage 3's search does not find them. No Stage 3 code changes before #1095.
#1149 records the root causes: #1107 removed the per-query seed; the joint price profile has no options for KLL candidates; seed weights are log-compressed when guarantees are ignored, which hides a 6× gap; and the joint MILP ends with lower bound 0 under the deadline. It marks these workloads as #1095 acceptance cases.
Waived planning-time threshold
#1107 runs every pane-width round up to Complete, so planning takes 5–15× longer on slow corpus queries: q02/q06/q22/q23 go from 9–11 s to 70–72 s, q19/q25 from 21 s to 194 s, q26 from 8 s to 47 s, and q05 from 16 s to the 240 s budget. The plans are unchanged, so this slowdown is accepted. In the snapshot gate (#1150):
time_limit_reason, so reaching the time limit no longer fails it; its plan and cost are still compared.deadline_reached_reason, so they may enddeadline_reached; their plan, cost and status are still compared.X2 sizing note
#1098 sizes KLL for its failure probability even when a query composes no guarantee, for example with guarantees ignored. KLL state for ex-memo-ratio-of-quantiles grows from 121.8 MB to 174.2 MB (+0.53% cost), and custom-or-topk-quantile-raw switches to DDSketch (+0.0054%). This stays as it is; #1054 records it.
Before this PR
On
main, Stage 3 re-sized sketches, folded Stage 2's execution-scope copies, ran the pane-width rounds, and stopped after a 128-evaluation budget. Stages called each other across crate boundaries.After this PR
Each stage has its own boundary types, and the facade orchestrates the stages. Pass 1 decides sketch sizes. Stage 3 is a replaceable
select_planthat takes only a deadline. Selected plans are the same on the snapshot workloads; the Stage 3 losses above are tracked for #1095.Verification
cargo test --release -p asap-devtools --test corpus_snapshots -- --ignoredpasses on the integration branch after test(devtools): snapshot harness reads Stage 3's Selection; accept the facade's slower planning #1150.testandformat-and-lintpass on this PR's head.🤖 Generated with Claude Code