Repository navigation
feat(planner): Pass 2 reuse rules (rollup, exact top-k reuse) and nested sum merge in Pass 1 (#680) - #716
Conversation
A grouped sum over the value of a sum beneath it, such as sum by (job) (sum_over_time(m[5m])), gets ComputationCandidate::NestedSumMerge beside its own accumulator: one exact Sum accumulator over the inner sum's input, grouped by the outer keys, which absorbs the inner target like the whole-expression top-k does. Plan selection prices it against the nested accumulators.
Rollup answers a coarse exact accumulator (sum, count, min, max) from a finer one the candidate already computes over the same rows, as an exact accumulator over the finer finalized values grouped by the coarse keys. The family-capability table decides which families merge across groups; counter states, IRate and sketches do not roll up. Exact top-k reuse reads every exact top-k of one input and partition from one ranking, Limit(k_max) over Sort. Sketch heaps are not reused. Stage 1 lists both rules' candidates after the window-composition ones, each followed by its identical-expression candidates.
The nested-sum merge is selected and answers as the nested sums; rollup and exact top-k reuse are selected once the shared finer computation is computed once per evaluation, a stand-in for Stage 2 materialization.
…e, never stacks (#680) A per-series sum drops __name__ while the raw samples keep it, so sum by (__name__) (sum_over_time(m[5m])) merged would group under the metric's name where the nested sums group under none: no merge is offered then. A target that absorbs is now never absorbed, so merges of three nested sums do not stack and combinations() counts what enumeration keeps.
…ites together (#680) Exact accumulators execute only Float64 updates, so integer counts are not summed again. Every rollup subset is combined with every top-k reuse subset, so more than 20 sites together list one error instead of 2^n candidates. Document the reuse sites' and variants' fields.
Every candidate of sum by (__name__) and sum by (job) over sum_over_time answers alike; rollups of sums, minima and maxima and exact top-k reuse answer, query by query, as the candidate they reuse from.
f9910f4 to
b995781
Compare
|
/// One Pass 1 candidate for a computation (one measure of an aggregate). |
|
Where the six variants come from: they were not derived from one scheme. They were added one rule at a time. The design doc's Pass 1 table has two kinds, exact or a summary. The code then grew four more:
So the enum mixes two independent questions:
A cleaner shape would separate the two questions: /// How one Pass 1 target is computed.
pub enum ComputationCandidate {
Exact, // re-run the original sub-DAG
ExactAccumulator(Vec<ExactState>), // one or more exact mergeable states; avg = [Sum, Count]
Summary(SketchAlgorithm), // an unsized summary family
}A fused target (top-k over per-item sum; sum over sum) then becomes its own |
#716's rollup test marked shared nodes MatAtQuery by hand as a stand-in for G8; Stage 2 now offers it for the shared sums, and plan selection picks the rollup unaided. The exact top-k test keeps the stand-in: Stage 2 builds a shared summary once per evaluation, not a shared exact ranking.
#716's rollup test marked shared nodes MatAtQuery by hand as a stand-in for G8; Stage 2 now offers it for the shared sums, and plan selection picks the rollup unaided. The exact top-k test keeps the stand-in: Stage 2 builds a shared summary once per evaluation, not a shared exact ranking.
…fied query requirements (Example 4) (#717) feat(planner): Stage 2 materialization options and the latency check (Example 4) Stage 2 returns one physical candidate per materialization assignment of each logical candidate's summary builds (NotMaterialized, MatAtIngestion when eligible, MatAtQuery when two or more queries read the build). Counter states over continuously ingesting PromQL data are eligible, on the assumption that such data arrives in time order per series. Stage 3 prices a maintained pane chain as the newest pane's update and N + 1 kept panes, charges a MatAtQuery state only for the one evaluation it is held, and rejects a candidate whose query latency is over its ExplicitMaxMs bound. Candidates hold planning data only (id, origin, roots, DAG, the materialization assignment); asap_devtools::candidate_labels derives the viewer labels. MaterializationUnit is crate-private Stage 2 bookkeeping. refactor(planner): rename MaterializationUnit to MaterializationCandidate One summary build Stage 2 may store, with its options; not a whole-workload plan like PhysicalCandidate. Related identifiers follow (materialization_candidates, candidates_below, mat_candidates). test(types): query requirements are a target and an optional latency bound (failing) refactor(types)!: QueryRequirements is an accuracy target and an optional latency bound QueryRequirements { accuracy: AccuracyTarget, max_latency_ms: Option<f64> } replaces AccuracyRequirement { Explicit, ImplicitExact } and LatencyRequirement { ExplicitMaxMs, Unspecified }. JSON: an omitted accuracy is AccuracyTarget::exact(), an omitted max_latency_ms imposes no bound, unknown fields are rejected, and the old tagged forms are not accepted. The viewer document's query requirements use max_latency_ms. refactor(planner)!: planning takes the workload entries; QueryDemand removed optimize_physical_asap, physical_candidate_count, select_plan, run_stage1_to_stage3 (and _from_pass1), run_stage2_and_stage3 and run_planning_stages_over take &[QueryWorkloadEntry], one per query in root order, and read the accuracy target, latency bound, recurrence and predictability from it. The separate targets and recurrence parameters and QueryDemand are removed. Every query now has a target (default exact), so Stage 3's internal per-query targets are no longer optional. QueryWorkloadEntry, Query and QueryRecurrence derive Default (empty query, exact, no latency bound, unknown predictability and recurrence). fix: review findings on Stage 2 materialization - Un-ignore the four summary_sharing tests that now pass; the other three are ignored because plan selection still picks an exact plan there. - validate_ingestion_time_summary's doc: the planner checks filtered builds now. - price computes each query's reach once. - Document QueryLatency's fields and CandidateCost::latency. test(integration): reuse_rules reads Stage 2's own MatAtQuery #716's rollup test marked shared nodes MatAtQuery by hand as a stand-in for G8; Stage 2 now offers it for the shared sums, and plan selection picks the rollup unaided. The exact top-k test keeps the stand-in: Stage 2 builds a shared summary once per evaluation, not a shared exact ranking. docs(planner): SelectionError::EntryCount says it is bad input
Closes #680.
Why
The legacy search could answer a computation from a cheaper related one (rollup, top-k reuse, nested sum merge). The Stage 1 → Stage 3 pipeline had none of these, so it missed cheaper plans (#676 capability audit). This PR follows the decisions recorded for #680:
What
sum by (job) (sum_over_time(m[5m])), or any groupedsumover the value of asumdirectly beneath it, gets a new candidateNestedSumMergenext to its own accumulator. The candidate is one exactSumaccumulator over the inner sum's input rows, grouped by the outer keys. It absorbs the inner target, the same way the whole-expression top-k does.sum,min,maxover Float64 values) is answered from a finer one that the candidate already computes over the same rows (for examplesum by (job)fromsum by (job, instance)). The rollup is an exact accumulator over the finer finalized values, grouped by the coarse keys. The value stays exact. (The table lets counts roll up as the sum of the finer counts, but the executor's exact accumulators take only Float64 updates, so integer counts are refused.)kall read one ranking,Limit(k_max)overSort.How
add_nested_sum_candidatesandnested_sum_inputcheck that the merge applies:sumgroups by listed keys, has no filter orHAVING, and is not itself absorbed;sumhas one consumer and no filter orHAVING;__name__: a per-series sum drops the metric name while the raw samples keep it, so the merge would group undermwhere the nested sums group under"";combinations()invariant).The outer keys are moved onto the inner input by column (
partitions_over).ComputationCandidate::absorbs_innerreplaces the earlierWholeExpressionmatches incombinations,absorbed_choice_variesand the display name.rollup_kindreads the family-capability table. A family qualifies when its states merge over disjoint or overlapping selections with anExactmerged error. Counter states (DisjointInTime), IRate (no merge) and every sketch family returnNone.rollup_sitespairs each coarse build (read only throughFinalizeExactAccumulator) with its coarsest finer build. The finer build must have the same family, update and filter, read the same rows, and have group keys that strictly include the coarse keys. "Same rows" means equal nodes, or PromQL scans of one metric without predicates under the same time ranges and shifts, which may project other labels. A global SQL coarse aggregate is refused, because it has a row even over empty input. The finer values must be Float64, the only update type exact accumulators execute, so integer counts do not roll up. The rollup value is renamed to the coarse columns, and its types must match.topk_reuse_sitesgroups plainAggregatetop-k nodes over an exact, unfiltered input by equal input and partition. In a reuse candidate every member reads oneLimit(k_max, Sort)ranking. Stage 2 still implements each top-k asLimit(Sort)over that ranking.Every variant composes its guarantees again (
with_composed_guarantees).Cap. Reuse candidates are listed eagerly, like every other Pass 2 rule's. Every rollup subset is combined with every top-k reuse subset, so the cap applies to the product: more than 20 rollup and top-k sites together (
MAX_REUSE_SITES) list oneUnsupportederror instead of 2^n candidates.Key code interfaces added / modified
crates/asap-aware-mapping/src/per_computation_candidates.rscrates/asap-aware-mapping/src/asap_aware_cse.rscrates/asap-aware-mapping/src/pass/stage1_to_stage3.rsUsage, end to end (checked with
stage_pipelineatcde2ca3d, built-in models, no--epsilon, so every query is exact):What Stage 1 does with it, for each Pass 1 candidate (here only the all-exact one), after its window-composition candidates:
What the user gets in
run.json: 20 Stage 1 candidates (L1–L5 base, L6–L10 top-k reuse, L11–L15 rollup, L16–L20 both), all implemented by Stage 2. Stage 3 selects P1 (all exact, separate computations) at 0.000119 cost_per_second. P6 (top-k reuse) costs 0.000119 and P11 (rollup) 0.000126, because Stage 2 leaves every nodeNotMaterializedand aNotMaterializednode is charged once per query that reads it (see the pricing limitation below).The nested sum merge is a Pass 1 candidate, so it appears for a single query:
stage_pipeline --promql "sum by (job) (sum_over_time(x[5m]))"lists L1 "Q1 exact (Sum acc, Sum acc)" and L2 "Q1 exact (nested Sum acc)" (ComputationCandidate::NestedSumMerge), and Stage 3 selects P2 at 0.00065003 against P1's 0.00065058.Before this PR
sum by (job) (sum_over_time(m[5m]))was always two accumulators: a per-seriesSum, finalized, then aSumbyjobover those values.sum by (job)besidesum by (job, instance), ortopk(5, X)besidetopk(10, X), was always computed separately from the raw rows.After this PR
stage_pipeline --promql "sum by (job) (sum_over_time(x[5m]))"(default statistics):For
sum by (job) (x),sum by (job, instance) (x)and twotopk by (job)with k = 5 and k = 10 over the finer sums, Stage 1 now also lists candidates such as "Sum acc for Q1 rolled up from Q2" and "top-k for Q3, Q4 from one top-10". All 20 candidates are implemented by Stage 2.Limitation (pricing). Stage 2 currently leaves every node
NotMaterialized, and Stage 3 charges aNotMaterializednode once per query that reads it. Until G8 (Stage 2 materialization) lands, rollup and top-k reuse are therefore priced but never cheaper. The integration test materializes the highest shared computed nodes as a stand-in for G8, and with that the reuse candidates are selected.Verification
asap-aware-mappingunit tests:nested_sum_merges_into_one_exact_accumulator,nested_sum_merge_needs_a_sum_of_sums,nested_sum_merge_never_groups_by_the_dropped_metric_nameandnested_sum_merges_do_not_stack(the last two failed before the review fix);rollup_from_a_finer_exact_sum,extrema_roll_up_and_integer_counts_do_notandrollup_needs_a_finer_computation_of_the_same_rows;rollup_is_refused_for_families_without_an_exact_merge_across_groups: Rate, Increase, IRate, CMS+heap and KLL, plus a grouped KLL pair that gives no site;exact_topk_reuses_the_larger_rankingandtopk_reuse_needs_exact_topk_of_one_input_and_partition(including heap sketches, which are not reused);reuse_sites_past_20_together_are_an_error(hung enumerating 2^21 candidates before the cap).crates/integration-tests/tests/reuse_rules.rs, executing each plan on samples of two jobs of two series:nested_sum_by_the_metric_name_answers_as_the_nested_sumsfailed before the fix (merged__name__="m", nested""); every candidate ofsum by (__name__)andsum by (job)oversum_over_timenow answers alike;b9957817e6f1f4b3f3b563e8d3a3d5c7d1d5dcb7(rebased onf0b07992):cargo nextest run -p asap-aware-mapping -p asap-integration-tests -p asap-devtools: 880 passed;cargo fmt --all -- --check: clean;cargo clippy --workspace --all-targets --all-features --locked -- -D warnings: clean.Limitations and follow-ups
SummaryMergemerges states of one grouping).count by (job) (count_over_time(m[5m]))besidecount by (job, series) (...), are not matched. Integer-valued finer values (counts) do not roll up.Aggregate. A top-k oversum ... sum_over_timelowers toSort+Limit, so the rule does not apply there.rollup.rs(RollupStrategy),topk_reuse.rs(TopKLimitReuseStrategy) andreplacement.rs'squery_time_nested_sum/contains_aggregate. Nothing became unused, so nothing is deleted here. These go with the final legacy-core deletion.