Repository navigation
feat(planner): physical ASAP-aware optimization and plan selection for candidate logical ASAP DAGs - #576
Draft
zzylol wants to merge 13 commits into
Draft
feat(planner): physical ASAP-aware optimization and plan selection for candidate logical ASAP DAGs#576zzylol wants to merge 13 commits into
zzylol wants to merge 13 commits into
Conversation
This was referenced Oct 3, 2026
Merged
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
3 times, most recently
from
October 10, 2026 14:32
d613b0f to
243c791
Compare
zzylol
force-pushed
the
stack/509-c2-stage1-workload
branch
2 times, most recently
from
October 11, 2026 17:11
0150354 to
db2f3c4
Compare
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
from
October 11, 2026 17:11
243c791 to
bd70f05
Compare
zzylol
force-pushed
the
stack/509-c2-stage1-workload
branch
from
October 13, 2026 02:58
db2f3c4 to
ec00b3c
Compare
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
… values A comparison of approximate operands (quantile(0.99, …) > quantile(0.5, …)) now gets the same BinaryOp candidate over summary operands as arithmetic does, so no combination loses its summary candidates. Automatic selection still needs evidence about the candidate's output: a root guarantee that meets the target, which composition (#669) supplies, or the deployment's empirical output-level accuracy model (Stage 3, #576). Certified inputs are not enough, since a difference or ratio of them can be unbounded, so such candidates are kept but not selected under the built-in models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
force-pushed
the
stack/509-c2-stage1-workload
branch
from
October 13, 2026 04:12
ec00b3c to
03ebf0e
Compare
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
… values A comparison of approximate operands (quantile(0.99, …) > quantile(0.5, …)) now gets the same BinaryOp candidate over summary operands as arithmetic does, so no combination loses its summary candidates. Automatic selection still needs evidence about the candidate's output: a root guarantee that meets the target, which composition (#669) supplies, or the deployment's empirical output-level accuracy model (plan selection, #576). Certified inputs are not enough, since a difference or ratio of them can be unbounded, so such candidates are kept but not selected under the built-in models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
from
October 13, 2026 04:46
bd70f05 to
fd2e79f
Compare
…ction (MVP)
Stage 2 implements one logical candidate physically: exact Aggregate{[TopK]}
becomes Limit(Sort) per group, summaries stay build -> estimate, every node
runs at query time, and the timed roots export as one PhysicalASAPDAG.
Stage 3 is the only stage that computes cost. It rejects candidates whose
summary misses its query's accuracy target (accuracy model, analytical
guarantee) or uses Count-Min over weights not proven non-negative, prices
every valid candidate node by node with analytical_cost (illustrative
statistics), selects the cheapest and reports the rest as costlier.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add stage2_physical_asap (no cost) and stage3_selection (costs, selected, every other candidate rejected as invalid or costlier) per the v2 contract. PromQL rows carry the series identity from Stage 0, as runtime state needs. Regenerate tools/dag-viewer/examples/planner-layering-example1.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the export removal Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…#667 materialization Stage 2 takes query roots, scalar ones included (#542), times every sub-DAG with Materialization::NotMaterialized (#667) and keeps the guarantee Stage 1 composed on every node it implements; a MaintainPopulation is no longer rejected, since it now runs at query time when not materialized. Stage 3 accepts a query's output when its root guarantee meets the target, or when the deployment's accuracy model shows this candidate's output does (AccuracyModel::output_guarantee, none by default). A certified estimate inside the plan is not enough: a difference of two quantile estimates stays listed as invalid, with a reason, under the built-in models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…#575 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…th its reason A Stage 2 error no longer fails the whole candidate set: stage_pipeline lists that candidate under stage2_physical_asap.pruned and plans the rest. Full materialization enumeration is left to #604. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
… in Stage 3 The output-level hook moves from AccuracyModel to AccuracyEvidenceProvider: AccuracyModel stays theoretical, Stage 1 composes root guarantees with the built-in model, and deployment evidence enters only in Stage 3. A scalar root that is exactly scalar(v) inherits v's guarantee; any other scalar expression over an approximate operand has none until #669. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…age names physical_candidates -> physical_asap_optimization (stage2_physical -> optimize_physical_asap, Stage2Error -> PhysicalASAPOptimizationError), stage3_select -> select_plan, and the stage_pipeline keys stage2_physical_asap -> physical_asap_optimization and stage3_selection -> plan_selection. The fixture is regenerated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…bound #575 gives Top-K and signed Count-Min estimates guarantee None, so the CountSketch+heap candidate of the top-k fixture no longer carries an analytical guarantee: plan selection rejects it for missing evidence rather than for missing the target. The "misses target" case now uses a quantile summary planned at a looser target, and the per-node cost test admits the CountSketch+heap candidate through deployment evidence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…imization
Rank a one-measure grouped child by its measure (the SQL heavy-hitter and
PromQL `topk(k, sum by ..)` shapes) and per-series rows by the sample
value, instead of looking up a `value` column. A node reading the top-k
gets a Project restoring the logical [keys.., topk_k] columns over the
rows. An Aggregate{[TopK]} with other measures, a filter or HAVING now
fails, so its candidate is pruned with the reason.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
With no valid candidate the document lists the rejections with a null `selected` (and the Stage 2 pruned list as before); any other selection error is reported under `error`, instead of failing the run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
…ote MVP costing limits Reword the Count-Min test to the decided behavior and test that a query without a target admits the Count-Min candidate. Document that a cost error aborts selection (only from bad declared rates) and that binary, join and concat nodes are priced from their first input only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW
zzylol
force-pushed
the
stack/509-c3-stage2-3
branch
from
October 13, 2026 05:24
fd2e79f to
99a1b3c
Compare
zzylol
added a commit
that referenced
this pull request
Oct 13, 2026
…mputation candidates (#575) * feat(planner): compose a whole-workload candidate from Pass 1 choices Replays #575's composition on the #561 Pass 1 API: choice[i] indexes targets[i].choices.candidates, several targets (measures) of one aggregate are grouped, an exact accumulator becomes SummaryAgg → FinalizeExactAccumulator and a summary SummaryAgg → SummaryEstimate. Pass 1 summaries are unsized, so composition sizes each for its measure's accuracy requirement. Summary coverage is derived from the subtree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(ir): name a finalized exact accumulator after the aggregate it realizes FinalizeExactAccumulator kept the state column's name (`state`), so a consumer reading the sample value (e.g. sum by (job) over a finalized per-series rate) found no `value` column at runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(planner): rank Top-K summary items by the PromQL series identity column Rows that carry the full series identity are closed; Pass 1 rejected them and the runtime needs explicit item columns for keyed summaries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(devtools): stage_pipeline writes real frontend and logical ASAP DAG documents Replays #575's devtools commits on the #561 Pass 1 API: the frontends section is the frontends' workload DAG and logical_asap_optimization holds one composed whole-workload candidate per choice of a per-computation candidate for every target, as flat DAGs, capped by --max-candidates. Example 1 has three candidates (one exact option per target), so the cap test cuts at two. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * feat(planner): compose measure filters, HAVING, avg and multi-measure targets Composition substitutes every #561 Pass 1 choice. A measure filter becomes SummaryAgg.filter (the realization rule #608 carried, with its filtered-output nullability in ASAPOp schemas); a key-only HAVING that every consumer applies becomes a Filter below the state (SummaryAgg.filter would keep the dropped groups); a value HAVING stays a Filter above the output. avg divides its finalized sum and count accumulators, and the measures of a multi-measure aggregate are substituted one by one and joined back on the group keys. ScalarExpr::map_columns renumbers a predicate's columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * feat(planner): give composed logical ASAP candidates theoretical guarantees Every node of a composed candidate gets a guarantee: exact over exact inputs, a summary estimate's sized family bound over exact input, and None for anything computed from an approximate value, including a value HAVING or any other predicate at any depth (#669 defines composition; plan selection's accuracy model decides). The Example 1 fixture shows them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): keep summary candidates for combinations of approximate values A comparison of approximate operands (quantile(0.99, …) > quantile(0.5, …)) now gets the same BinaryOp candidate over summary operands as arithmetic does, so no combination loses its summary candidates. Automatic selection still needs evidence about the candidate's output: a root guarantee that meets the target, which composition (#669) supplies, or the deployment's empirical output-level accuracy model (plan selection, #576). Certified inputs are not enough, since a difference or ratio of them can be unbounded, so such candidates are kept but not selected under the built-in models. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * docs(planner): composition of Pass 1 candidates in the module doc The composition of per-computation candidates into whole-workload candidates is Algorithm 1's SUBSTITUTE; its helpers are named so. The module doc describes it and states that summaries are sized here per measure because Pass 2 sharing is not implemented yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): no frequency bound on Top-K or signed Count-Min estimates A heap sketch's bound is per item frequency, not Top-K membership, and a Count-Min bound assumes non-negative weights. A composed Top-K estimate, and a Count-Min estimate over weights not proven non-negative, now get no guarantee; plan selection's accuracy model decides them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): rank Top-K over a closed PromQL child by its labels topk(3, sum by (job) (rate(m[5m]))) listed heap candidates that composition could not substitute: the closed child [job, value] has no series identity. Its item is now the tuple of labels outside the partition, as in the legacy current-series and counter rules. stage_pipeline records a candidate it cannot compose with its error instead of aborting. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): substituted outputs keep the aggregate's column names An estimate or accumulator names its value after the operation (quantile_0_99 became quantile, avg sum, a per-series count value count). A single-measure substitution is now renamed to the aggregate's columns, as join_measures already did; a renaming projection keeps its child's guarantee. The logical_asap_optimization test compares column names too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): substitute subqueries inside measure filters and HAVING A subquery in a substituted aggregate's measure filter or own HAVING now reads its rewritten plan, and a summary state's guarantee requires every input, its filter's subqueries included, to be exact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(ir): a filtered quantile aggregate is nullable As its summary estimate is: no row passing the filter gives NULL. The filtered quantile queries join the SQL candidate composition test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): compose SQL avg as a keyed join of sum and count A PromQL BinaryOp matches Utf8 labels: it merges NULL and '' groups and rejects other key types. For SQL rows the finalized sum and count are joined on the group keys (NULL keys match) and divided as SQL, NULL where the count is 0 (a filtered-empty group, an empty global input). PromQL keeps the BinaryOp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * fix(planner): count states read a column non-null on every counted row The count state behind avg-as-sum/count and SQL count read the sample value, which for SQL rows is `value` or the only numeric column: avg(c) divided by count(value), count(*) counted a nullable column, and with several numeric columns the composed DAG was invalid. The runtime skips NULL updates, so: - avg's count reads the averaged column (non-null, or the candidate is pruned); - SQL count reads a column its filter proves non-null (count(col)), else a non-nullable column; with none it is Unsupported; - an unresolvable state input is Unsupported, and a composed DAG is validated before it is returned. PromQL counts still read the sample value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW * test(planner): a subquery in an aggregate's own HAVING is substituted Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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
#509 has four stages. Before this PR the planner stopped after logical ASAP-aware optimization (#575): it produced candidate logical ASAP DAGs but no physical plans, and it selected no plan. This PR adds Stage 2 (physical ASAP-aware optimization) and Stage 3 (plan selection) as an MVP, so a workload goes from the frontends to one selected plan.
What
physical_asap_optimization::optimize_physical_asap): builds one candidate physical ASAP DAG for each candidate logical ASAP DAG.Materialization::NotMaterializedand runs at query time. EnumeratingMatAtIngestionandMatAtQueryis left to Stage 2 materialization: ingestion vs query time per summary #604.Aggregate{[TopK]}becomes a per-groupSortfollowed by a per-groupLimit, the shape the PromQL frontend uses fortopk. A summary staysSummaryAgg→SummaryEstimate.PhysicalASAPOptimizationError. The caller prunes that candidate alone and records the error as its reason.plan_selection::select_plan): the only stage that computes cost.AccuracyEvidenceProvider::output_guarantee(new,Noneby default), shows that it does. Checks on individual estimates inside the plan never accept on their own: a difference of two quantile estimates is rejected under the built-in models.scalar(v)inheritsv's guarantee. Any other scalar expression over approximate inputs has no guarantee until Compose accuracy guarantees through arithmetic and comparisons over approximate values #669.analytical_cost, and the per-node costs are summed. The cheapest is selected. Every other candidate is listed with a reason: invalid (with the failed check) or valid but costlier.stage_pipelinedevtool emits four documents:language_specific_frontends,logical_asap_optimization,physical_asap_optimization(candidates plusprunedwith reasons, no cost), andplan_selection(costs, selected, rejected). PromQL rows carry the series identity, as per-series state needs at runtime. The Example 1 fixturetools/dag-viewer/examples/planner-layering-example1.jsonis regenerated.How
Physical ASAP-aware optimization rewrites bottom-up with one memo for the whole workload, so sharing between query roots is kept. It then applies
apply_materialization_timingswithMaterializationAssignment::with_default(NotMaterialized)and exports the timed roots as onePhysicalASAPDAG.Plan selection walks each query root, first applying the Count-Min weight check, then the output-level accuracy check. Deployment evidence is read only here; #575's composition never reads it. Pricing charges each node of the physical DAG once, so a shared node costs the same whatever the number of queries reading it.
Before this PR
stage_pipelineon Example 1 stops at 3 candidate logical ASAP DAGs. There is no physical plan, no cost, and no selected plan.After this PR
Example 1 gives 3 → 3 → 1:
sort (partition by job) → limit 10.Q1 exact · Q2 exact) is selected, at 79.4 cpu_ms per workload evaluation.CmsWithHeapneeds non-negative update weights, and these are not proven non-negative.Evidence
physical_asap_optimizationtests: exact top-k becomes sort then limit and a shared child stays shared; every node runs at query time.plan_selectiontests:scalar(v)inheritsv's guarantee and no other scalar expression does.crates/devtools/tests/stage_pipeline.rs: the four-document output validates; physical ASAP-aware optimization implements or prunes each composed logical candidate exactly once with no cost; plan selection accounts for every physical candidate once and costs exactly the valid ones; the committed fixture is current.Verification
cargo fmt --all -- --check: OKcargo clippy --workspace --all-targets -- -D warnings: OKcargo test --workspace: 1593 passed, 0 failed, 2 ignoredtest_render: 29 OKArchitectural decisions
AccuracyModelstays theoretical; the deployment's empirical accuracy enters throughAccuracyEvidenceProvider::output_guaranteeand only in plan selection.Limitations and follow-up
NotMaterialized. Enumeration ofMatAtIngestion/MatAtQueryand the timing split across query roots: Stage 2 materialization: ingestion vs query time per summary #604.prunedlist yet: feat(dag-viewer): redesign the viewer around the stage pipeline #574.Stack: tracker #528 · main → #575 → #576 → #577 → …
Human review — do not complete with an agent
🤖 Generated with Claude Code
https://claude.ai/code/session_01W7qG9aFyPij5uWsyAJCxDW