Skip to content

Compose exact operators with summary plans across explicit update/readout boundaries #171

Description

@zzylol

Design status (updated)

This issue defines a required planning capability, not a settled post-ASAP IR design. The implementation on the feat/exact-composition-planner family of branches is a prototype for evaluation and has not been merged into main. Its ExactTransform / ExactPostProcess vocabulary must not be treated as the accepted architecture.

The semantic requirement is:

ASAPPlanner must be able to represent, validate, cost, and select plans that compose exact computation with summary computation without hiding an independently realizable summary subtree inside an opaque fallback.

In particular, rate, irate, and increase are exact functions. They should not be collectively named deriv, because PromQL already has a distinct deriv() function, and they should not be classified as “transforms” as a semantic identity. Function semantics and execution placement are separate concerns: an exact function may have update-time, query-time, or multiple legal physical implementations, and the planner should compare those alternatives rather than encode one phase into the function name.

Any proposed design must satisfy these implementation requirements:

  • Extensible: adding another exact function, aggregate, or relational operation must not require another pair of phase-specific IR variants or a new one-off planner path.
  • General: the representation and selection mechanism must cover the general exact/summary composition problem in either nesting direction, rather than only the example queries that exposed this issue.
  • No issue-specific special nodes: do not introduce a special node solely to handle rate, max over quantile, or any other single reproducer. Nodes must represent stable semantic or physical concepts with invariants that apply across operators.
  • Open to downstream extension: deployments must be able to register capabilities, costs, and legal execution modes without changing the core IR for every new exact function.
  • Conservative and compositional: unsupported combinations must retain an explicit fallback without swallowing independently selectable child candidates.

Any proposed design should therefore evaluate at least these options before choosing an IR:

  • an ExactFunction / ExactAggregate semantic vocabulary with execution phase represented separately;
  • a general exact-operation node with independently selected physical execution modes;
  • expressing the composition only in a lower physical/stage DAG rather than adding specialized post-ASAP logical nodes; and
  • retaining multiple legal placements for cost-based selection instead of assigning functions such as rate permanently to the update path.

The existing branch is useful as a prototype and source of tests, especially for phase legality, shared-child selection, conservative fallback, and cost accounting. It is not evidence that its node names or architectural boundary are final. Acceptance of this issue requires an explicit design decision followed by an implementation merged into main.


Context

Same migration as issues #150/#151 (ASAPQuery-backend's control_plane adopting asap_plan::bind::implement_tree_in_with for L3→L4 binding, replacing its own locally-defined L4 IR).

We hit a real coverage gap that shows up in both nesting orders of "mix an exact computation with a summary/sketch computation in the same query": an outer exact op wrapping an inner realized summary, and an outer summary op wrapping an inner exact computation. Neither direction has an L4 representation today.

Direction 1 — outer exact fold over an inner realized summary

max by (zone) (quantile_over_time(0.99, http_latency_ms[5m]))
avg by (zone) (quantile_over_time(0.99, http_latency_ms[5m]))

Structurally this is two nested Aggregate nodes: an outer AggIntent::Max/Avg whose child is itself an Aggregate carrying an inner, independently-realizable AggIntent::Quantile. Semantically, the query wants: read the inner quantile's per-zone answer (from whatever summary/accumulator actually answers it), then fold same-zone rows by max/avg. It is not asking for a real, independently-materialized "max" or "avg" accumulator over raw samples — in our deployment this shape is typically the identity fold (the inner summary already emits one row per zone), but it can be a real multi-row fold too (e.g. the inner summary is keyed more granularly than the outer by).

crates/plan/src/boundary.rs's implementation_for_with has no representation for this composition:

AggIntent::Min { .. } | AggIntent::Max { .. } => {
    accumulator(intent, SummaryKind::MinMax, SummaryParams::MinMax)
}
// ...
AggIntent::Avg { .. } | AggIntent::StdDev { .. } | AggIntent::Variance { .. } => {
    Implementation::PassThrough
}
  • Max/Min unconditionally commit to SummaryKind::MinMax — this requires a real, independently-registered MinMax-family sid to exist for the metric. One never does for this shape (there's no reason to plan/register a MinMax accumulator alongside a Quantile sketch just so an outer max by (...) can wrap it) — so find_candidates (our SummaryExecutor impl) always returns no candidates for the outer node, and the whole tree fails to execute even though the inner Quantile would answer fine on its own.
  • Avg/StdDev/Variance go straight to PassThrough — per implement_tree_in_with's conservative-fallback behavior (a logical parent above a bindable aggregate subsumes it unbound), this wraps the entire tree, including the otherwise-realizable inner Quantile, as one opaque Logical blob.

Either way, the outer wrapper poisons an inner sub-tree that would otherwise realize correctly on its own.

Direction 2 — outer summary op over an inner exact computation

The mirror image: rate(...)/irate(...) are themselves exact, per-sample computations over raw counter data (counter-reset-aware delta ÷ time). A query can wrap one in an operator that would otherwise be summary-realizable, e.g.

sum_over_time(rate(metric[5m])[10m:])

or stack a top-k summary on top of an exact rate, e.g. our own already-excluded

topk(5, sum by (zone) (rate(metric[5m])))

Here the inner rate(...) is the exact computation, and the outer sum_over_time/topk is the one that could in principle be answered from a summary (CMS-with-heap for the top-k case) — but there's no way to express "realize the outer as a summary over the OUTPUT of an inner exact per-sample transform" either. This deployment doesn't currently attempt this composition at all: any query containing rate(...)/irate(...) anywhere in its tree is unconditionally excluded from our SummaryExecutor binding before we ever try (see "What we did instead" below) — direction 2 is why.

Both directions are the same underlying gap, just which side of the composition is exact vs. summary-realized.

The composition should consider when a query DAG, part of the DAG computes from raw data, part of the DAG computes from summaries as well.

Request

Is there room in the L4 model for a third answer shape, alongside "realized as a summary/accumulator" and "opaque Logical, compute from raw data": an L4 node that composes an exact computation with a nested realized summary, in either nesting order — outer-exact-over-inner-summary (direction 1) or outer-summary-over-inner-exact (direction 2)? Concretely, something like the L4Node DAG carrying an explicit "apply this function to the child's per-group output, independent of whether the child bound as a summary or stayed exact" node, so implement_tree_in_with can commit whichever sub-tree can realize independently, and let the caller (or a CostModel-style hook) apply the outer/inner composition at readout — instead of one side either requiring its own unrelated accumulator/summary sid or poisoning the other side's binding.

We don't have a strong opinion on the right shape for this (a new L4Node/SummaryExpr variant vs. a CostModel hook similar to realize_extension/readout_extension vs. something else) — flagging the gap for discussion rather than proposing a specific API.

What we did instead (for now)

Nothing routed around locally for direction 1 yet. We're treating it the same way we treat #151's TopK { accuracy: Exact } gap: a known, accepted "falls over to the exact/archive tier" limitation for now, rather than forking implement_tree_in_with's tree walk to special-case it in control_plane.

For direction 2, we already have a standing local exclusion: any candidate whose PromQL contains rate(...)/irate(...) anywhere in its tree is excluded from our SummaryExecutor binding before it's ever attempted (a RateShape skip in our serving-time lowering), and topk(K, sum by (...) (rate(...))) is excluded the same way. We hadn't connected these to direction 1 until writing this up — they're the same gap.

If a CostModel-reachable hook (or a new IR shape) lands upstream for either direction, we'd rather adopt that than build a parallel local mechanism for either one.

References

  • crates/plan/src/boundary.rs — implementation_for_with's AggIntent::{Min,Max} / AggIntent::{Avg,StdDev,Variance} arms (direction 1), and the Rate/Increase accumulator arms (direction 2)
  • ASAPController#151 — same "deployment wants a different answer than core's default realization for one intent shape" pattern, one layer over (ranking within an already-offered candidate set, vs. no candidate set being offered at all here)
  • ASAPController#150 — the CostModel-style extension-point precedent (realize_extension/readout_extension) this request is loosely modeled after

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    cost-modelCost formulas, statistics, ranking, and selectionenhancementNew feature or requestirPre-ASAP or post-ASAP intermediate representationnested queriesreplacement-strategyASAP replacement candidates and physical strategies

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions