Repository navigation
Stage 1 Pass 1 review: per-computation candidates over memo groups #1012
Description
Part of #1010. Line numbers are on main at 958778400. Abbreviations: loa/ = crates/logical-asap-optimization/src/, pcc.rs = loa/per_computation_candidates.rs.
Example: what Pass 1 produces
Same query as the tracking issue (avg by (job) (quantile_over_time(0.5, data[5m]) / quantile_over_time(0.5, data[5m]) + quantile_over_time(0.9, data[5m])), every minute, max_error 0.01, failure_probability 0.001). In the production planner, read from the stage-pipeline document tools/dag-viewer/examples/memo-ratio-of-quantiles.json (main at d728d05c) (#1002):
Corrected 2026-10-28: production numbers from the stage-pipeline document; earlier numbers came from a test helper without the series-identity column. The whole-workload column is the earlier helper measurement and has not been re-measured in production.
Memo path (try_populate_pass1) |
Whole-workload path (compose_logical_candidate over every inventory assignment) |
|
|---|---|---|
| Memo groups | 7 (same as Stage 0) | – |
| Implementation candidates | 19: G2 and G4 have 7 each, the other 5 groups 1 each | – |
| Candidate DAGs | 49 = 7 × 7; 16 use only budget_share 1 |
27 (helper) |
Where the 7 candidates of a quantile group come from (read from the code, not from a run):
computation_candidates_for_intent(pcc.rs:495) givesQuantileno exact accumulator, so its choices areExact,Summary(Kll),Summary(DDSketch)(families fromsummary_candidates,loa/realization.rs:30).pass1_candidates(loa/memo/groups.rs:91) counts, for each query, the targets it reaches that offer a summary (here 2: G2, G4), and takes the maximum over the group's queries asmax_budget_share(groups.rs:118-151).target_candidatesemits one candidate per budget share1..=max_budget_sharefor every sketch choice (groups.rs:204-215); the exact choice stops at share 1. In production the 7 are: exact; KLLk345 and DDSketchalpha0.01 for the whole budget; KLLk746 and DDSketchalpha0.004963 for half of it; and KLLk567 (whole budget) andk1184 (half) sized for the whole result over the source's 10,000 series (per-valuefailure_probability1e-7 and 5e-8).
Open question (#1002): why the memo stands for more DAGs than the whole-workload path composes (production: 49, of which 16 whole-budget; helper: 343 vs 27). One hypothesis from reading the code: the whole-workload path splits the budget by the sketch states actually present in a composed candidate (budget_shares, pcc.rs:1302), while the memo offers every share up to the maximum, so the memo includes over-split variants that the whole-workload path never builds. Not confirmed; #1002 adds the memo-to-candidate-DAG enumeration that can check it.
Functionality
- Find each computation target (an
Aggregatewhose intent has an exact accumulator or a summary family), including fused computations (whole-expression top-k, nested sum). - For each target, list its candidates on its own: exact, exact accumulator state, each summary family that can meet its accuracy requirement. Prune only what provably cannot, with a reason.
- Turn the targets' candidates into each memo group's
ImplementationCandidates (CandidateMemo).
Concepts (design doc term ↔ code)
| planner-layering.md | Code on main |
|---|---|
| eligible sub-DAG / target | ComputationTarget (pcc.rs:259) |
| fused computation | FusedComputation (pcc.rs:290) |
| Replacements(s) | ComputationCandidate { Exact, ExactAccumulator(Vec<ExactKind>), Summary(SketchAlgorithm) } (pcc.rs:141) |
| Pass 1 inventory | PerComputationCandidates { roots, targets } (pcc.rs:343) |
| PRUNE(c, reason) | PruneReason (pcc.rs:171) |
summary-family capabilities F |
FamilyCapability (types/src/ir/schema/family_capability.rs), but Pass 1 reads the hand-written summary_candidates |
| rewrite rule (pattern + templates), Algorithm 1.1 | no rule type; per-intent code in pcc.rs |
| implementation candidate | ImplementationCandidate { implements, node, children, origin, budget_share } (loa/memo/candidates.rs:24), origin = CandidateOrigin::Pass1 |
| Memo after Pass 1 | CandidateMemo (candidates.rs:15) |
Interfaces: designed vs implemented
| Designed | Implemented | |
|---|---|---|
| Input | logical roots; the deployment's supported summaries | generate_per_computation_candidates_with_memo(Vec<(Id, QueryRoot)>, &LogicalMemo) (pcc.rs:608), called from Stage 3's driver (plan-selection/src/search/pipeline.rs) |
| Deployment filter | part of Pass 1 | PerComputationCandidates::without_unsupported_summaries (pcc.rs:413), called by plan-selection (pipeline.rs:214), with DeploymentCapabilities defined in this crate (loa/deployment_capabilities.rs:48) |
| Output | CandidateMemo: each group's candidates |
try_populate_pass1(LogicalMemo, &PerComputationCandidates) -> Result<CandidateMemo, _> (groups.rs:48), called from Stage 3 (plan-selection/src/search/route.rs, search_domain) |
| Sizing | "Summary nodes are created unsized" (doc); pcc.rs:22 says the same |
memo Pass 1 composes and sizes each candidate per budget share (groups.rs:204-215 → compose_alone → sized) |
Interface decided in #1047 (2026-10-29)
Decided in #1047, the stage-boundary types. Its sub-issue is #1048. Names follow #1050 within #1047's scope. Principles: a stage or pass reads only the previous step's output type and calls no other stage; the asap-planner facade runs the stages in order; each stage prunes only what its own knowledge rules out and records it as PrunedCandidate<C> { group, candidate, reason: PruneReason }; queries are entry indices. Planning stays as fast as on main: P6 and P7 report before/after stage_pipeline runs.
| Decided | |
|---|---|
| Functionality | For every aggregate measure with a realization besides plain exact: list how it can be computed, turn each way into sized CandidateForMemoGroups on its group, and prune |
| Input | DagOfLogicalMemoGroups (moved in), accuracy targets (one per query), AccuracyModel, AccuracyEvidenceProvider, DataWorkload, supported summaries (the Stage 1 part of DeploymentCapabilities) |
| Output | PerComputationLogicalMemoCandidates { memo (private: groups + candidates_by_group), query_roots, computation_targets, pruned_candidates: Vec<PrunedCandidate<HowToComputeAggregateMeasure>> }. This is also the Pass 2 input |
| Function | generate_per_computation_logical_memo_candidates(...); one entry replacing generate_per_computation_candidates{,_with_memo}, with try_populate_pass1, add_scoped_candidates and without_unsupported_summaries inside Pass 1 |
ComputationTarget |
aggregate_group, measure_index, ways_to_compute: Vec<HowToComputeAggregateMeasure>, measure_filter, having_filters, fused_computation (absorbed_targets, ways_to_compute); raw_data_samples_of leaves the output |
| Prunes | summaries the deployment does not support; candidates provably unable to run or to meet the requirement; new: candidates whose composed guarantee cannot meet the query's accuracy target in any combination within that query (moved from prune_query_candidates) |
| Decided here | Pass 1 walks memo groups (an Aggregate group is a target) instead of whole roots. #1047 P3 implements it, and reviewing P3 settles this question. Whether computation_targets can be deleted is open here; the evidence is in the comment above |
| PR | #1047 P3 (and P6 for the accuracy pruning) |
Review checklist
User's suspected problems
- (a) The intent → summary family table is duplicated. Places that encode intent ↔ family / exact state:
summary_candidates(loa/realization.rs:30): intent → families, written by hand; its comment says "one home". Theta is excluded by hand.of_per_instance_sketch(types/src/ir/schema/family_capability.rs:150): family → readouts. This is the table the executor reads (executor/src/supported_summaries.rs).summary_candidatescould be derived from it ([Refactor] Derive Pass 1's intent → family list (summary_candidates) from the family-capability table #953).statistic(pcc.rs:3875): intent →SketchStatistic, used to check deployment support (pcc.rs:418); the executor maps readout →SketchStatisticagain (executor/src/supported_summaries.rs:119).computation_candidates_for_intent(pcc.rs:495) andloa/function_rules.rs:10: the same intent set (Sum, Min, Max, Avg, Rate, IRate, Increase) for exact accumulators and for accuracy composition.sizing_target(realization.rs:72) andintent_metric(types/src/ir/properties/accuracy.rs:319): two intent → accuracy metric tables.keeps_every_value(plan-selection/src/lib.rs:4963): intents whose exact state grows, in Stage 3.agg_is_mergeable,agg_is_exact,agg_accuracy(types/src/ir/operator/agg_intent.rs:670,:682): no non-test caller found;agg_is_exactsays Count has no sketch benefit, whichsummary_candidatescontradicts.
- (b) Stages mixed in Pass 1. Only about the first 1,200 lines of
pcc.rs(6,154 lines) generate candidates. The rest is whole-workload composition (compose_logical_candidate,pcc.rs:1203), sizing (budget_shares,sized), guarantee propagation including Pass 2 / Stage 2 shapes (PaneChain, rollup, merged panes), and substitution. Stage 3 imports guarantee functions from it (bind_guarantees,with_resized_summaries,root_guarantee, …). - (b) Stage 3 drives Pass 1.
try_populate_pass1andaccuracy_configurations::add_scoped_candidates(a plan-selection module that adds Stage 1 candidates,plan-selection/src/search/candidates/accuracy_configurations.rs:8) run inside Stage 3'ssearch_domain(route.rs). Unsupported summaries are removed by the Stage 3 driver, not by Pass 1 (Reorganize by stage: one memo-based Pass 2, Stage 1 finishes the memo, deployment inputs split by stage, asap-planner as the only entry point #958 decisions 2–3). [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P3:try_populate_pass1,add_scoped_candidatesandwithout_unsupported_summariesmove inside Pass 1; Stage 3 reads only Stage 2's output. - (c) Not general. There is no pattern/template rule as in Algorithm 1.1. Families come from a fixed list; per-intent prunes are inline (nullable avg, relational count, quantile φ, top-k heap over one row per item); fused computations are bespoke passes (
add_whole_expression_targets,add_nested_sum_targets); substitution has per-intent builders (ranked_as_topk,nested_sum,sql_avg). Adding an intent or family means editing several tables ([Refactor] Derive Pass 1's intent → family list (summary_candidates) from the family-capability table #953, Design the extension contract for new ASAP primitives and sketch algorithms #548).
1. Boundaries
- Decide whether budget-share variants are Pass 1 candidates or a sizing coupling decided later (memo-milp: size summaries once (final config per reader set and summary count; Stage 3 never resizes) #929 "size summaries once"). Today Pass 1 sizes, against its own module doc (
pcc.rs:22). -
DeploymentCapabilitiesis defined here but read by Stages 1, 2 and 3 (Reorganize by stage: one memo-based Pass 2, Stage 1 finishes the memo, deployment inputs split by stage, asap-planner as the only entry point #958 decision 3). [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047: each stage receives only its part (see the table above).
2. Naming and over-abstraction
-
ComputationCandidatevsImplementationCandidatevsPerComputationCandidates("inventory" also names a Stage 3 type, [Refactor] Repo-wide naming survey: overloaded and misleading names #948). Decided:HowToComputeAggregateMeasureandCandidateForMemoGroup([Refactor] Rename memo-named caches and the memo types by stage #1050);PerComputationLogicalMemoCandidates([Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047). -
computation_candidates_for_intentispubbut used only insidepcc.rs.
3. Stale code
-
generate_per_computation_candidates(non-memo,pcc.rs:599): callers found only in devtools (sketch_coverage) and tests. [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P3: one entry,generate_per_computation_logical_memo_candidates, replaces both. - Comments still refer to "legacy realization rules" (
pcc.rs:3787). -
try_populate_pass1's doc says it "does not yet generate rules per group" (groups.rs:46-47). [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P3:try_populate_pass1is folded into Pass 1.
4. "memo" / "cache"
-
_with_memouses the memo only as a cache key;LocalCandidateCache(pcc.rs:618) cachescomputation_candidates_for_intent, a pure function of the intent;type Memoinpcc.rsis a rewrite cache. [Refactor] Rename memo-named caches and the memo types by stage #1050 renamesLocalCandidateCache→MemoGroupMeasureToComputationChoicesCacheandtype Memo→NodeToRewrittenNodeCache; [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047 P3 removes_with_memo.
5. Rules over memo groups
- Pass 1 still runs on whole query roots and maps targets to groups afterwards (
targets_ofingroups.rs). Decide whether Pass 1 should run per memo group, so a group shared by two queries is visited once. Decided ([Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047): Pass 1 walks memo groups; P3 implements it, and reviewing P3 settles this item.
Correctness and efficiency
- Resolve Memo: convert memo-group candidates into the vector of all candidate DAGs #1002's memo-vs-whole-workload count (production: 49 memo DAGs, 16 whole-budget) and decide which candidate set is intended.
- Each pruned candidate carries a reason that the viewer can show. [Refactor] One named type per stage boundary: PerComputationLogicalMemoCandidates → LogicalASAPMemoCandidates → PhysicalASAPMemoCandidates #1047:
pruned_candidates: Vec<PrunedCandidate<HowToComputeAggregateMeasure>>with aPruneReasonshared by all stages.
Input/output check with the DAG viewer
- Input: the Stage 0 memo of the example. Output: each group's Pass 1 candidates (origin, family, budget share), and the candidate DAGs they stand for (DAG viewer: draw the memo after each step (groups with their candidates) #1005, DAG viewer: list the enumerated candidate DAGs and draw any of them #1006).
- Check: the 7 candidates of G2 and G4 are the intended set; the enumeration matches the whole-workload path or the difference is explained (Memo: convert memo-group candidates into the vector of all candidate DAGs #1002).
Related issues
Sub-issues: #953, #735, #727, #703, #681, #664, #639, #750, #751, #743, #739, #711, #689, #798, #807, #788, #944, #459, #410, #422, #444, #443, #350, #172, #297.
Referenced: #1002 (memo → candidate DAGs; parent #1008), #958 (Stage 1 layout, under #1013), #929 (size once, under #1015), #548 and #131 (extension contract).
🤖 Generated with Claude Code
Activity
3 remaining items
zzylol commentedon Oct 29, 2026
Evidence for removing
computation_targetsfrom the Pass 1 output (#1047, decided 2026-10-29): after Pass 1 builds the implementation candidates, most ofComputationTargetis duplicated there.measure_filterisSummaryAgg.filter,having_filtersareFilternodes above the aggregate,candidatesare the sized implementation candidates, andFusedComputation.absorbed_targetsisImplementationCandidate.fused_groups. Readers left after #1047: devtools display andcombinations()(plan-selection's weight check readsraw_data_samples_of, which #1047 already removes from the output). Decide here whethercomputation_targetscan be deleted.zzylol commentedon Oct 29, 2026
From #1047 (decided 2026-10-29):
LogicalMemono longer exposes the node-pointer → group map (group_of). Its public shape is{ groups, query_roots: Vec<QueryRoot<MemoGroupId>> }. Pass 1 should find its targets by walking memo groups (anAggregategroup is a target) instead of walking the frontends' nodes and mapping them back to groups (group_for_nodeinper_computation_candidates.rs:710andgroups.rs). This issue decides how Pass 1 does that.zzylol commentedon Oct 30, 2026
Pass 1 correctness bug found while working on #1047 P3c: #1089. A key-only HAVING is pushed below an aggregate that another query shares unfiltered.