Repository navigation
feat(planner): collapse topk(sum by (l) (sum_over_time)) into one CMS heap - #849
milindsrivastava1997 wants to merge 3 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ketchWithHeap For f linear under both sum and topk (sum_over_time, count_over_time), plan one heap keyed by the inner sum's labels instead of an exact sum anchor with a query-time topk. Both the legacy and MILP planners use it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… heap Register the shared topk-of-sum patterns so the engine matches the planner's collapsed CountMinSketchWithHeap and answers with the inner sum's labels. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| )); | ||
| let r = &w.raqes[0]; | ||
| assert_eq!(r.capability, Capability::TopKByValue); | ||
| assert_eq!(r.grouping_labels, LabelSet::new()); |
There was a problem hiding this comment.
MILP: topk-of-sum Raqes are indistinguishable from other topk Raqes on the same metric, so one deployment can serve queries needing different heap keys.
build_raqe sets grouping_labels to topk_by_labels or [], and Raqe carries no aggregated/key labels. topk(32, sum by (label_0) (sum_over_time(data[15m]))) and topk(5, sum by (job) (sum_over_time(data[15m]))) (or a plain topk(5, sum_over_time(data[15m]))) all become TopKByValue, grouping [], same metric/lookback/filter. The solver can put them on one deployment, and the roll-up guard in plan_to_planner_output only compares grouping_labels, so it passes. aggregation_config then takes aggregated_labels from items.first(), so the heap is keyed by [label_0] and the by (job) query is routed to it and returns wrong results with no error. Before this PR every topk Raqe with the same grouping needed the same keys, so sharing was safe. Fix: add the key labels (req.grouping_labels) to the Raqe identity, or reject a deployment whose items disagree on them.
| @@ -237,7 +311,7 @@ pub fn get_topk_by_labels( | |||
| AggregationModifierType::By => KeyByLabelNames::new(modifier.labels.clone()), | |||
There was a problem hiding this comment.
topk by (l) over an inner sum uses l as heap buckets even when the inner sum already removed l.
topk by (instance) (3, sum by (job) (sum_over_time(m[5m]))): in Prometheus every input series has no instance, so there is a single bucket and the result is the global top 3 jobs. The Without branch now intersects with topk_input_labels, but the By branch still returns [instance]. set_subpopulation_labels then plans grouping [instance] and aggregated [job], so precompute builds one heap per raw instance value holding per-(instance, job) partial sums. The query gets per-instance partial sums instead of per-job totals. Fix: intersect the by labels with topk_input_labels(match_result, all_labels).
| )); | ||
| let r = &w.raqes[0]; | ||
| assert_eq!(r.capability, Capability::TopKByValue); | ||
| assert_eq!(r.grouping_labels, LabelSet::new()); |
There was a problem hiding this comment.
The MILP prices and sizes the topk-of-sum CMS heap from the grouping [] data shape, not from the cardinality of the inner sum by keys.
require_shapes and the accuracy curves look up data_shape[raqe.grouping_labels], which is [] for every global topk. The CMS for topk(32, sum by (label_0) ...) holds one key per label_0 value, not one per series. The accuracy/cost estimate, and so the chosen depth/width/heap, is the same as for a per-series heap. That can over-provision a few-key heap or under-provision a many-key heap without any warning.
| // Patching: When the query is topk, we should always return all labels | ||
| // topk is a selector: it keeps its input's labels, which are all labels | ||
| // unless an inner `sum by (...)` reduced them first. | ||
| if aggregation_token.op.parse::<AggregationOperator>() == Ok(AggregationOperator::Topk) { |
There was a problem hiding this comment.
Doc comments still say topk output labels are always all labels.
get_topk_by_labels's doc (line ~288) still says get_spatial_aggregation_output_labels "always returns all labels for topk regardless of any modifier", and the engine's extract_topk_param doc says topk is "excluded from every collapsable spatial-of-temporal pattern". Both are wrong after this PR and will mislead the next person who changes the topk label logic.
| /// CountMinSketchWithHeap keyed by `l`. | ||
| pub fn build_topk_of_sum_patterns() -> Vec<PromQLPattern> { | ||
| [ | ||
| PromQLFunction::Rate, |
There was a problem hiding this comment.
build_topk_of_sum_patterns adds a third copy of the temporal-function list and the pattern-building code that the planner and engine already duplicate.
planner/patterns.rs and simple_engine/mod.rs each hard-code the same [Rate, Increase, SumOverTime, ...] list and the same collapsable aggregation(function(matrix_selector)) builder. This PR moves only the new pattern into promql_utilities (and into query_logics/parsing, a module for parsing helpers, not pattern builders), leaving three lists to keep in sync. Moving the full ONE_TEMPORAL_ONE_SPATIAL and TOPK_OF_SUM builders into one shared promql_utilities function would let the planner and engine share them, so they can't drift apart on which shapes they accept.
| /// than the bucketing labels (#714). | ||
| /// *output* label set (all labels, or an inner `sum by (...)`'s labels; | ||
| /// unaffected by topk's own modifier) rather than the bucketing labels | ||
| /// (#714). |
There was a problem hiding this comment.
Changed comments keep issue-number references, which AGENTS.md (pulled in by CLAUDE.md) forbids.
The repo CLAUDE.md says "See AGENTS.md for process conventions". AGENTS.md says: "Code comments should not include git history, or issue numbers or historical facts." The rewritten comments still carry (#714) (query_requirements.rs:49) and (collapsable, see #508) (query_requirements.rs:101).
Before this PR
topk(k, sum by (l) (f(m[w])))had no single-sketch plan.--planner saved: they rejected it withno supported query pattern matched. That includes the docs(planner): plan the AutoSketch vs. ASAPQuery planner evaluation (§6.3) #777 classic workload's 4topk(32, sum by (label_0) (sum_over_time(data[w])))queries, so the saved plan'scms-heap-topk-fastpath-vector2ddeployment couldn't be converted.sum by (l)anchor with a query-timetopk.After this PR
When
fis linear under bothsumandtopk(sum_over_time,count_over_time), the query collapses into oneCountMinSketchWithHeapkeyed byl. The weighting is sum forsum_over_timeand count forcount_over_time, and no query-time stage runs.promql_utilities:build_topk_of_sum_patterns()matches the three-level shape. The innersumis collected asinner_aggregation.topk's output labels, and the labels atopk without (...)buckets over, now come from the inner sum instead of all labels. Every other shape is unchanged, because only these patterns produce the inner token.build_patternsincludes the new patterns, so both the legacy and MILP planners emit the heap. The MILP Raqe is a globalTopKByValue(grouping[]) withkfrom the outertopk. Its aggregated labels arel.Behavior change: the legacy planner no longer plans this shape as exact sum plus query-time
topk. It's now approximate (CMS) and uses less memory. #744 described this collapse as the optimization.Still unsupported (scope agreed on #827):
sum by (job) (rate(data_total[w]))andtopk(32, sum by (label_0) (rate(data_total[w]))). Both need per-series deltas of the cumulative counter before grouping (#496). They still fail loudly.Validation
promql_utilities: output and bucket labels forby,without, and no inner modifier.sum_over_timeandcount_over_timematch;rateshapes and a non-suminner reducer don't.[job], planned subquery equal to the query, empty query-time pipeline.e2e_topk_of_sum_ranks_groups_from_one_planned_heap): planner config → precompute → query. The data makes the per-series top (c= 4) differ from the per-job top (api= 2 + 3). The test fails without the engine change.cargo testforpromql_utilities,asap_types,asap_plannerandquery_engine_rust: all pass. clippy adds no warnings andcargo fmtis clean.--planner savedend to end: I merged this branch with feat(planner): asap-planner --planner saved writes a sketch-bench eval plan without re-solving #828 in a scratch worktree and ran it on sketch-bench'ssynthetic-templatesclassic-shared1(ASAP, cpu, cheapest), with only the 8ratequeries removed (46 of 54). It wrote 6 aggregations, versus 5 in feat(planner): asap-planner --planner saved writes a sketch-bench eval plan without re-solving #828 with all 12 removed. The new one isCountMinSketchWithHeapsum, depth 3, width 1024, heap 46080, aggregated[label_0], matching the plan's deployment. All 4sum_over_timetopk queries route to it.Part of #827. Blocks #824.
🤖 Generated with Claude Code