Repository navigation
feat(plan-selection): one ingestion pipeline per refresh window in the selection MILP (#1095 §2.5 O3(b)) - #1157
Merged
zzylol merged 7 commits intoOct 31, 2026
Conversation
…un (#1151) (#1153) RefreshWindow, PhysicalASAPDAG::refresh_window and ingestion_refresh_groups derive which ingestion-time views share one run; the install compile check and the stand-in runtimes refresh per group, and a counting-store test shows one raw read per window per group. No cost change. Part of #1151.
…1154) asap_cost::execution_demand::computations counts a NotMaterialized ingestion-time node once per refresh window of its MatAtIngestion consumers instead of once per consumer, and the old Stage 3 engine (general_schedule, DirectCostProfile::selected_cost, direct_cost::encode) charges it the same way. Snapshots unchanged. Closes #1151 except the backend follow-up.
…resh window) Conflict in crates/cost/src/execution_demand.rs: keep #1154's rule (a NotMaterialized ingestion-time node counts once per refresh window of its MatAtIngestion consumers) and drop the prototype's hook that charged such a node (1, 1.0), which under-charged chains of different refresh windows.
…e selection MILP (#1095 §2.5 O3(b)) The ingestion context I becomes one I_r per refresh window r of the ingestion-time shareable candidates (ContextKind::Ingestion(IngestionPipeline)); an ingestion-time candidate whose window is unbounded gets its own pipeline. r(m) is PhysicalASAPDAG::refresh_windows on the DAG of the candidates below m, the function asap-cost groups ingestion work by, so the objective keeps equalling plan_cost. Row 3: p[m,g,c] <= p[I_r(m),g,c]; row 4: a candidate of I_r is read by the contexts of the m with r(m) = r; the stateless ingestion work is charged once per I_r. view_windows walks each operator's inputs (a valid DAG's edges) so it also answers for a DAG of nodes alone.
…1095-ingestion-per-refresh-window
…fresh window (#1095) - Debug-assert that every producer candidate of an input reads one view window when computing refresh windows (the per-solve equality check does not catch a mismatch when the model is inexact); probe test on hand-built and Stage 1-2 fixtures. - asap-types test: a non-chain view's refresh window on a DAG without edges. - Test execution_contexts with two windows, a shifted window through NotMaterialized nodes, and an unbounded (Alone) view. - Row 4 reads an inverse map I_r -> contexts of its views, built once. - I_r wording in inexact reasons and test comments; compact LP-diagnosis labels (I[10000ms@0], I[alone m42]).
zzylol
added a commit
that referenced
this pull request
Nov 2, 2026
… 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.
zzylol
added a commit
that referenced
this pull request
Nov 2, 2026
… 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.
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.
Part of #1095
Why
#1151 (merged into
feat/1095-stage3as #1152–#1154) changed the cost rule for stateless ingestion work. The deployment refreshes all ingestion-time views of one refresh window in one run. Soasap_cost::execution_demand::computationsnow charges aNotMaterializedingestion-time node once per distinct refresh window of itsMatAtIngestionconsumers. A consumer with an unbounded window counts alone. #1095 §2.5 O3(b) was rewritten to match: one ingestion pipelineI_rper refresh windowr.The prototype (#1144) still had one ingestion context
I. To keep its objective equal toplan_cost, it also had a hook incomputationsthat charged such a node(1, 1.0), whoever read it. That hook under-charges a scan read by chains of different pane widths (§8 G5). This PR adoptsI_rand removes the hook.What
origin/feat/1095-stage3(docs: refresh the ingestion-time views of one refresh window in one run (#1151) #1152–feat: charge a shared ingestion node once per refresh window (#1151) #1154). On the conflict incrates/cost/src/execution_demand.rs, it keeps feat: charge a shared ingestion node once per refresh window (#1151) #1154's rule and drops the prototype's hook.retained_panesandmaintained_window_msstay public, sincegroups.rsstill uses them.p[m,g,c] ≤ p[I_{r(m)},g,c];p[I_r,g,c'] ≤ Σ_{m: r(m)=r} p[m,g,c'];I_ris chargede(c)once perI_rthat selects it, at rate 1.I_rof the reading context.How
r(m)must be computed exactly asasap-costcomputes it on the realized DAG. Sogroups::refresh_windowsbuilds a nodes-only DAG of the candidates below each ingestion-time shareable candidate, reading each input from its first producer candidate, ascatalog_nodedoes. It then calls the same function,PhysicalASAPDAG::refresh_windows. For that to work on a DAG without edges,PhysicalASAPDAG::view_windowsnow walks each operator's inputs (payload.children()) instead of the edge list.validaterequires these to be identical in a valid DAG, so its results on real DAGs do not change.execution_contextsputs a candidate ofI_rinto the seeds ofI_{r(m)}only when the context of an ingestion-time shareablemreads it. Stage 2'scompatible_timinglets only ingestion-time candidates read aNotMaterializedingestion-time candidate. EachI_ris then expanded through the candidates it computes, asIwas. Contexts are ordered: queries, then theI_r(byIngestionPipeline), then the shareable contexts.MilpInputs::ingestion_context_of_shareablerecordsm → I_{r(m)}.Key code interfaces added / modified
crates/plan-selection/src/groups.rscrates/plan-selection/src/milp/model.rscrates/types/src/ir/physical_export.rscrates/cost/src/execution_demand.rs: no signature change. The hookNone if n.output_state.timing == ExecutionTiming::IngestionTime && count.contains_key(&n.id) => (1, 1.0)is removed (merge resolution).Usage (end to end, as the test
ingestion_scans_under_chains_of_two_refresh_windows_are_charged_twiceruns it):With
ASAP_STAGE3_SEARCH=milp,stage_pipelinereaches the same code. ItsASAP_STAGE3_REPORTlines report the model size; theIngestionscope now holds everyI_r.Major algorithm changes
Ingestion contexts per refresh window (§2.5 O3(b), §3 rows 2–4, §3.5)
I, a scan read by a 1 s chain and a 30 s chain ran once inI, and the MILP charged it once. Under feat: charge a shared ingestion node once per refresh window (#1151) #1154,plan_costcharges it once per refresh window. Before this PR, the hook incomputationshid the difference by under-charging the plan. On random-mixed-seed-862 workload-01, the old selected plan read one ingestion scan from chains of 1 s, 20 s and 30 s panes (node 51, 8.32e-9 per second). It was charged once instead of three times.execution_contextsseeds oneIwith every ingestion-time candidate read across a boundary. 2. Row 3:p[ctx,c] ≤ p[I,c]. 3. Row 4 forI: read by any other context. 4.computationscharged each such node(1, 1.0).refresh_windowscomputesr(m)per ingestion-time shareable candidate withPhysicalASAPDAG::refresh_windows. 2.execution_contextsseedsI_{r(m)}fromm's context only (Alone(m)for an unbounded window). 3. Row 3:p[m,c] ≤ p[I_{r(m)},c]. 4. Row 4: a candidate ofI_ris read by the contexts of themwithr(m) = r. 5. EachI_rrun is charged at rate 1.computationsapplies feat: charge a shared ingestion node once per refresh window (#1151) #1154's rule unchanged.I_rin the decoded DAG is read only by themwithr(m) = r, socomputationscounts it once. The same candidate selected in twoI_ris two nodes, each counted once.r(m)comes from the same functioncomputationscalls. This holds as long as the candidates of one memo group read one window (see limits). The per-solve check (objective =plan_cost) passes on every test and on all seven §6.1 workloads.Q + 1 + |M|toQ + |distinct r| + |M|. A candidate read by chains ofkwindows gets up tokpcolumns instead of 1. Measured: +0 to +4 % variables (workload-01's largest round: 39,907 → 40,251).refresh_windowsis linear in the candidates below the ingestion-time states, built once perprepare_milp_inputs_with.r(m)is computed with each input read from its first producer candidate. That is exact when every producer candidate of an input reads one view window.refresh_windowsdebug-asserts this (producer_window_disagreements), because the per-solve equality check alone would miss a mismatch in a model with inexact terms (it reportsInexactObjective). Tests check it on hand-built and real Stage 1–2 fixtures; no counterexample is known (a widened time range would be one). Only ingestion-time shareable contexts read a candidate of anI_r. If some other context read one, it would have no source and row 3 would force the read to 0 (Stage 2 never links one).Before this PR
The prototype modeled one ingestion pipeline. It relied on a hook in
asap-costthat charged stateless ingestion work once whoever read it, so plans sharing a scan across chains of different pane widths were under-priced (§8 G5).After this PR
The MILP and
plan_costboth use #1151's rule (one run per refresh window), and the hook is gone. A scan shared by chains of one refresh window is charged once; chains ofkwindows pay itktimes. The solver now sees that cost and chooses between sharing and separate scans with it.Verification
milp/model/tests.rs, each with the per-solve objective =plan_costcheck:one_ingestion_scan_under_chains_of_one_refresh_window_is_charged_once: two 10 s chains → one scan;ingestion_scans_under_chains_of_two_refresh_windows_are_charged_twice: 10 s + 60 s chains → two scans, one per window.MILP objective 1.1670e-1 != plan_cost 1.1696e-1.222fbd3):execution_contexts_have_one_ingestion_pipeline_per_refresh_window(hand-built): two views of (10 s, 0) share oneI_r; (60 s, 0) and (10 s, 5 s) get their own (the shifted one goes through aNotMaterializedshift and range); an unbounded view getsAlone.producers_of_an_input_read_one_view_window(hand-built) andproducers_of_an_input_read_one_view_window_through_stages(three Stage 1–2 fixtures).a_view_has_its_refresh_window_on_a_dag_without_edges: a non-chain KLL overTimeRange(1 min)shifted 15 s has window (60 s, 15 s) with and without edges. It fails when theview_windowschange is reverted.I_r → contexts, the debug-assert is debug-only, and the label/wording changes are cosmetic. So no re-measurement.cargo test -p asap-plan-selection --lib: 322 passed.cargo test -p asap-cost,cargo test -p asap-types: pass.cargo test -p asap-devtools --test devtools stage_pipeline: 51 passed.cargo fmt --all;cargo clippy -p asap-types -p asap-cost -p asap-plan-selection --all-targets -- -D warningsandcargo clippy -p asap-plan-selection --all-targets --features lp-diagnosis -- -D warnings: clean.stage_pipeline,ASAP_STAGE3_SEARCH=milp, aws-fargate cost model, accuracy enforced, 300 s limit). Before = feat(plan-selection): selection MILP solved per independent part, LP gap diagnosis, tighter big-M bounds (#1095 §5 3a, §6.1) #1155 head; after = this PR:plan_cost= objectiveSix workloads select the same plan at the same cost. Four of them have small variable increases from the extra
I_rcontexts. Example1, ratio-of-quantiles and rollupheavy-t11 have no ingestion-time node in the plan; t5-q05-q09, t7-t4 and t8-t5 have one chain over its own scan.workload-01 (random-mixed-seed-862) changes by +2.31e-8 per second (+1.6 %). Two causes:
The selected plan's objective equals
plan_cost(checked per solve). Closing workload-01's gap belongs with the §5 steps (3b/3c).🤖 Generated with Claude Code