Skip to content

refactor(stage1-pass2): reuse rules over groups, logical edges, metric rule, validity and pruning caps (#1207 PR 4) - #1234

Draft
zzylol wants to merge 6 commits into
refactor/1207-pass1from
refactor/1207-pass2
Draft

zzylol wants to merge 6 commits into
refactor/1207-pass1from
refactor/1207-pass2

Conversation

@zzylol

@zzylol zzylol commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor

Merge unit. PR 4 of #1207's merge unit (PRs 0–9, with 0b), stacked on PR 3 (#1232, refactor/1207-pass1). The PRs merge into main together, after PR 9. This PR fills in Stage 1, Pass 2, which PR 0 left as todo!(). Planning now gets through Pass 2 and stops at Stage 2's first todo!() (PR 5); see "Test status".

Why

#1207's Order, row "4. Pass 2": rules 1–7 over memo groups (with #1160's tumbling layouts), the logical edges $(u,c,i)$ with $\alpha^{\log}$ (#1201), the metric rule, and validity and pruning caps (concepts doc §3 "Stage 1, Pass 2", §4). The user decided on 2026-11-06 that the validity-cap floor, the metric rule and the θ pruning cap belong here. Node trees carry no θ since PR 0, so node-tree guarantee propagation is replaced by per-edge error propagation (ErrorPropagation), as agreed.

What

  • Rules over groups (crates/stage1-pass2-reuse-candidates/src/memo/). Each rule adds groups (find_or_add_group) or candidates (add_candidate). No rule copies a consumer: every consumer of a group sees what a rule adds.
    • Rule 1, tumbling layouts (window_candidates.rs, Pass 2: tumbling-window builds as candidates of the state group, and merge-then-aggregate over raw tumbling windows (window-specific; memo representation is #1203) #1160). A mergeable summary state over TimeRange(W) gets, for each of its builds (each listed θ), the same build with $\omega$ = tumbling width $w$. The widths $w$ are $\gcd(W, c)$ for each $c$ in the gcd closure of the evaluation grids of the queries reaching the group. There is none at an @ anchor or for a family without a merge (an instant rate).
    • Rule 2, scan widening (rules/scan_labels.rs). Scans of one metric that list different labels get the union scan. Every group above that passes labels on (time range, time shift, a per-series build without a filter, a readout, a finalize, a merge) gets a widened image, with the original's builds (θ, ω and input requirements). Each original non-state group gets Project(widened image). Two quantiles over m{job} and m{host,job} then read one widened KLL build.
    • Rule 4, top-k reuse (rules/topk.rs). A top-k reads TopK(k) over the largest top-k group of the same input and partition. A top-k has no error contract, so only an exact producer gives it an edge: this is the doc's "exact-input requirement".
    • Rule 5, finer aggregate (rules/finer_aggregate.rs). A SQL aggregate reads a finer aggregate's result through a compensating Filter group, with each measure's rollup (sum→sum, count→sum, min, max; min when each query group is one finer row).
    • Rule 6, rollup (rules/rollup.rs). A grouped state (exact accumulator or sketch) whose family merges across groups reads SummaryMerge(group_by = its keys) over a finer state of the same family, update and filter, over the same rows. The finer group's windowed builds serve the coarse state through it.
    • Rules 3 and 7 deleted. Rule 3 (summary inputs as groups) is Pass 1's: every input is already a group. Rule 7 (window rollup) over the same range is rule 6 reading the finer group's windowed builds. A longer range read from a shorter range's windows is not offered: it needs a window requirement on the input (spec question 2).
  • Caps, floors, edges (logical_edges.rs, parameter_floors.rs), after the last rule:
    1. Error caps, top down. Each query's root group may be off by $\varepsilon_q$. A consumer passes $\min(b_{c,i}, \text{cap}/\beta_{c,i})$ to input $i$, through its contract in the query's target metric (in every metric, taking the largest, when the target names none). A group keeps the largest cap per query (the bottleneck over paths). A scalar root's operands get no cap.
    2. Parameter floors. A readout of a sketch state gets $\boldsymbol\theta^{\min}$: the per-parameter minimum of the listed θ whose $\varepsilon$ for its statistic is within its group's applicable cap. A query's cap applies only when its target names no metric or the readout's metric. The new floor is combined per parameter with the floor the input already has (a top-k's heap_size ≥ k). A merge of states gets the weakest floor of the consumers reading its group. Stored with Memo::set_input_requirements.
    3. Output errors, bottom up, per candidate: Exact, Bound(metric) or WithoutGuarantee. A sketch read has the guarantee of its θ (local_guarantee); a windowed read keeps it only where the family's merged error is exact or as if built over the union (merged_tumbling_windows_guarantee).
    4. Logical edges. Every $(u,c,i)$ with $u \in G_{c,i}$ is listed, grouped by $(c,i)$. $\alpha^{\log}$ = floor satisfied and the metric rule: an input error in a metric the consumer's contract does not carry gives no edge. An exact producer always may; no guarantee passes only through value-preserving consumers.
  • Error contracts (types): LogicalRequirements::error_propagation(consumer, metric) -> Option<ErrorPropagation>.
  • Accuracy: local_guarantee(family, parameters, statistic) reads θ and is public. The node-tree guarantee propagation is deleted (table below).

Deleted

Item Why
asap_accuracy::propagation (whole module: bind_guarantees, raw_data_samples_of_metric, composed_guarantee, root_guarantee, recompose_guarantees, with_guarantees, approximate_inputs, distinct_bound_source, merged_tumbling_windows_guarantee (node version), source_distinct_guarantee, fold_stats, estimates_raw_data_samples_of, stream_norm, scalar_guarantee, project_column_guarantee, operand_of, operand_guarantee, operand_domain_statistic, split_and, pinned_column, …) Node trees carry no θ (PR 0), so a node's guarantee cannot be derived. Replaced by local_guarantee(θ) + per-edge error_propagation.
asap_accuracy::composition (propagate and its operator rules, statistic constants, StreamNorm, sign/norm statistic helpers), function_rules Used only by node-tree propagation.
asap_accuracy::budget (per_input_target) Budget shares are gone (#1207); no caller.
PropagationStats Only node-tree propagation read it.
asap_accuracy::test_support::{composed_guarantee, scalar_guarantee, stream_norm, fold_stats, estimates_raw_data_samples_of, source_distinct_guarantee, PropagationStats, QUOTIENT_OPERAND_DOMAINS, STREAM_ROW_COUNT, source_norm_statistic, raw_data_samples_sign_statistic, sign_statistic} Wrappers of the deleted functions.
Tests: 23 in asap-accuracy (propagation, composition, budget); promql_numeric_regressions::ddsketch_interpolation_requires_a_nonnegative_domain and the bind_guarantees block of checked_ratio_must_not_certify_cross_zero_interpolation Tested the deleted node-tree propagation. The DDSketch interior-quantile sign condition must come back with $\varepsilon^{\text{loc}}$ in PR 8 (pending 7).
stage1-pass2 rules/summary_inputs.rs (factor_summary_inputs, rule 3); add_window_rollup_candidates (rule 7) See What.
Pass2RuleError::{InvalidGroupReference, TooManyGroups} No longer produced. Added: LogicalProperties (a rule bug).

New abstractions not in #1207 or the doc (pending approval)

Item Where Why
error_propagation(consumer, metric: AccuracyMetric) -> Option<ErrorPropagation> (doc: (consumer) -> ErrorPropagation) types/src/stages/candidates.rs The metric rule needs the contract per metric (doc §4: "β is defined for one input metric and one output metric"), and "an operator without an error contract is reported as unsupported": None. Every contract keeps the metric.
pub const RELATIVE_OPERAND_VALIDITY_CAP: f64 = 0.1 ($b$ of a ratio's denominator and a product's right operand) same The doc leaves $b$ open. 0.1 is the list's weakest ε, so it never removes a listed θ by itself.
Memo::set_input_requirements(id, Vec<LogicalRequirements>) types/src/stages/candidates.rs Floors are tightened after the rules ("each input's floor tightened by its pruning cap"); Memo has no other mutable access to a candidate.
asap_accuracy::estimators::local_guarantee made pub, with a parameters: Option<&SketchParams> argument accuracy/src/estimators/mod.rs θ from LogicalProperties::parameters (the PR 0 todo!).
pass2_after_local_window_composition(memo, entries) gains entries stage1-pass2/src/logical_memo_candidates.rs The caps read each query's target.
Private: OutputError {Exact, Bound(AccuracyMetric), WithoutGuarantee}, ErrorCap {query, cap}, logical_edges_with_capped_floors, error_caps, passed_cap, set_floors, readout_floor, weakest_floor_of_consumers, output_errors, candidate_errors, read_errors, merged_tumbling_windows_guarantee, merge_keeps_guarantee, carries, value_preserving; floor_for_cap, weakest, strongest, weakest_of; add_candidate, expression, produces_state; tumbling_window_widths, splittable_window_ms; union_schemas, passes_labels_on, narrowed; match_finer, Rows, value_rollup, pinned_column, split_and; grouped_state, rollup_keys, same_rows stage1-pass2-reuse-candidates The implementation. OutputError is a concept not in the formulation: the possible output error of a candidate, which the metric rule reads.
Test fixtures in stage1-pass2/src/tests.rs tests Build memos through Memo methods.

Spec questions (conservative choice taken)

  1. Unsupported operator ⇒ $\alpha^{\log} = 0$ for approximate inputs. This follows the doc's metric rule literally. Filters, sum, functions and comparisons over approximate values have no contract here (the old node rules used symbolic row counts and sign statistics, which a constant β cannot hold). Their approximate edges are flagged, not removed. Stage 3 (PR 6/8) decides what IgnoreAccuracyGuarantees does with $\alpha^{\log}$, and the same for the heap-size floor.
  2. Rule 7 across ranges ([1h] from [5m] windows, the old shared nested chains) needs a window requirement on the input (LogicalRequirements has only the parameter floor) or chain groups. It is not offered.
  3. Rule 1's widths come from the queries that reach a state when rule 1 runs. A coarse query that reads a finer state only through rule 6 does not add its grid to the finer state's widths (the doc fixes the rule order 1 → 2…). A TimeShift offset is not folded into the gcd.
  4. Old capabilities not ported: the filtered sketch rollup (filtered_finer: a compensating Filter over finer states), and the exact rollup that read the finer query's finalized result (rule 6 merges states instead). Shall I file issues?

How

generate_logical_asap_memo_candidates(pass1, entries) runs rule 1, then rules 2, 4, 5, 6, then logical_edges_with_capped_floors (above). The memo's own checks (add_candidate: node rules and the group's output) end a rule's shape match: a rejected candidate is skipped.

Key code interfaces added / modified

types/src/stages/candidates.rs

impl LogicalRequirements {
    /// g_c's linear bound for errors in `metric` (each input's and the output's);
    /// None when the consumer has no error contract for it (metric rule).
    pub fn error_propagation(consumer: &Operator<MemoGroupId>, metric: AccuracyMetric)
        -> Option<ErrorPropagation>; // was (consumer) -> ErrorPropagation, todo!()
}
/// Unchanged type (approved 2026-11-05).
pub struct ErrorPropagation {
    /// β_{c,i} per input.
    pub factor_per_input: Vec<f64>, // unchanged
    /// b_{c,i} per input; None when the bound holds everywhere.
    pub validity_cap_per_input: Vec<Option<f64>>, // unchanged
}
/// b of a ratio's denominator and of a product's right operand.
pub const RELATIVE_OPERAND_VALIDITY_CAP: f64 = 0.1; // new
impl Memo {
    /// Replaces candidate `id`'s input requirements, one per input group (Pass 2's floors).
    pub fn set_input_requirements(&mut self, id: CandidateForMemoGroupId,
        input_requirements: Vec<LogicalRequirements>); // new
}

accuracy/src/estimators/mod.rs

/// The guarantee of reading `statistic` from a summary of `family` with θ `parameters`.
pub fn local_guarantee(family: &FieldDataType, parameters: Option<&SketchParams>,
    statistic: &SketchStatistic) -> Option<AccuracyGuarantee>; // was pub(crate) (family, query), todo!() for sketches

stage1-pass2-reuse-candidates/src/logical_memo_candidates.rs, memo/rules.rs

/// Stage 1, Pass 2: rule 1, the other rules, then floors and logical edges.
pub fn generate_logical_asap_memo_candidates(pass1: Memo, entries: &[QueryWorkloadEntry])
    -> Result<LogicalASAPMemoCandidates, Pass2RuleError>; // unchanged
/// Rule 1 alone (asap_planner prints this step).
pub fn pass2_local_window_composition(pass1: Memo, entries: &[QueryWorkloadEntry])
    -> Result<Memo, Pass2RuleError>; // unchanged
/// Rules 2, 4, 5, 6, then floors from caps and the logical edges.
pub fn pass2_after_local_window_composition(memo: Memo, entries: &[QueryWorkloadEntry])
    -> Result<LogicalASAPMemoCandidates, Pass2RuleError>; // was (memo)
/// Why Pass 2 failed (stays).
pub enum Pass2RuleError {
    /// The memo's reads form a cycle: bad input.
    CyclicDependencies, // unchanged
    /// A rule built invalid logical properties: a bug.
    LogicalProperties(LogicalPropertiesError), // new
    // InvalidGroupReference, TooManyGroups: removed
}

Usage (the doc's running example, checked by a_ratio_reads_no_rank_error and a_ratio_divides_its_target_by_its_propagation_factor):

// Query B: quantile_over_time(0.99, m[1m]) / quantile_over_time(0.5, m[1m]),
// relative-error target 0.05, every 60 s.
let target = AccuracyTarget::max_error(0.05).accuracy_metric(AccuracyMetric::Relative);
let entries = [entry(60_000, target)];            // the workload entry the caps read
// pass1: the Pass 1 memo (Scan → TimeRange → SummaryAgg[KLL | DDSketch], one build
// per listed θ → SummaryEstimate → Divide), built with Memo methods in the tests.
let logical = generate_logical_asap_memo_candidates(pass1, &entries)?;
// Rule 1: each state gets one 60 s windowed build per θ.
// Caps: Divide passes 0.05 · (1 − 0.1) = 0.045 to both operands (denominator also ≤ b = 0.1).
// Floors: each DDSketch readout input gets alpha ≤ 0.045 → floor DDSketch { alpha: 0.02 }.
// Edges: (KLL readout → Divide) α^log = false (rank error, no relative contract);
//        (DDSketch readout → Divide) true; builds with alpha 0.1 or 0.05 → readout false.

End-to-end planning stops at Stage 2 (PR 5), so the selected plan is not shown yet.

Major algorithm changes

Pass 2 rules. Problem: the old rules rewrote whole node trees per candidate (private chains, shared-build copies, per-size groups); PR 0 removed that representation. Before: node-tree rewrites, then add_shared_summary_state_groups. After: rules over group expressions (above), with sharing only by hash-consing. Correctness: every candidate passes Memo::add_candidate (the node rules and the group's exact output); tests per rule. Complexity: rules 5 and 6 compare pairs of aggregate or state groups, $O(G_a^2)$; edges are $O(\sum_c \sum_i |G_{c,i}|)$. Limits: see the spec questions.

Pruning caps and the metric rule. Problem: the old Pass 1 sized each summary for an equal share of its query's error budget. With predefined θ lists, nothing removed hopeless θ or rank-error inputs into relative-error consumers. After: caps, floors and $\alpha^{\log}$ (What, 1–4). Correctness (safety): a cap is a maximum over paths of the per-path minimum of $b$ and $\varepsilon_q/\prod\beta$, so a producer with $\varepsilon$ under the cap is never excluded. A floor is the per-parameter minimum over the listed θ that meet the cap, so it admits all of them. A target in another metric than the read gives no floor. Merge floors are the weakest of their consumers'. Tests: a_query_target_caps_the_readout_input_with_a_parameter_floor, the_loosest_query_sets_a_shared_inputs_cap, a_target_in_another_metric_sets_no_floor, a_rollup_merge_takes_its_readouts_floor, a_count_of_approximate_values_is_exact. Complexity: linear in groups × candidates × queries reaching them.

Before / After

  • Before: planning stopped at Pass 2's first todo!() (rule 1). Accuracy guarantees were composed on node trees, whose sketch guarantees were todo!() (no θ on nodes).
  • After: Pass 2 returns LogicalASAPMemoCandidates { memo, edges }, with floors and $\alpha^{\log}$. Planning stops at Stage 2 (PR 5).

Representation, rules, enumeration

  • (a) Representation: unchanged types. Edges are now filled, and input floors are tightened in place.
  • (b) Rules: rules 1, 2, 4, 5, 6 over groups. Rules 3 and 7 are deleted (subsumed).
  • (c) Enumeration: none in this PR. $\alpha^{\log}$ restricts Stage 3's $y_{u,c,i}$.
Notation Code
$\omega_v$ (tumbling) WindowLayout::Tumbling { width_ms } on rule 1's builds
$g_c$, $\beta_{c,i}$, $b_{c,i}$ LogicalRequirements::error_propagation(c, metric) → ErrorPropagation
$\text{cap}_{c,i}$ passed_cap / ErrorCap (private, per group and query)
$\boldsymbol\theta^{\min}_{c,i}$ LogicalRequirements::parameter_floor_for_required_accuracy, set by set_floors / floor_for_cap
$\alpha^{\log}_{u,c,i}$ LogicalEdge::satisfies_logical_requirement
$P_{c,i}$, $\mathcal E$ (logical) LogicalASAPMemoCandidates::edges

Verification

  • cargo test -p asap-stage1-pass2-reuse-candidates: 20 passed. Coverage: rule 1 (widths; windowed builds per θ; none without a merge or at @), rule 2 (union scan; shared widened build), rule 4, rule 5 (rollup; compensating filter), rule 6 (merge; finer windowed builds serve the coarse state; merge floor), edges, metric rule, caps, floors, the count factor, merged-window guarantees.
  • asap-types: error_propagation_follows_each_operators_error_contract. asap-accuracy: local_guarantee_needs_a_statistic_the_family_answers_and_its_parameters.
  • cargo fmt --check; cargo clippy --workspace --all-targets --all-features -D warnings: clean. crate_graph: passes.
  • An independent review found 2 correctness bugs (a count over approximate-only input lost its edges; rule 6 compared filters across different children). Both are fixed and the first has a test. Its other notes became spec questions 2–3 or were fixed (dedup, naming).

Test status

cargo test -p <crate> --all-features --no-fail-fast, per crate, passed / failed / ignored. PR 3 is #1232 at d4bde07; PR 4 is this PR (all crates at e9630ad; types, accuracy and Pass 2 re-run at 72a4037).

Crate PR 3 PR 4 Where failures stop now
asap-stage1-pass2-reuse-candidates 0 / 0 / 0 20 / 0 / 0 —
asap-types 196 / 0 / 0 197 / 0 / 0 +1 error-contract test
asap-accuracy 76 / 1 / 0 54 / 0 / 0 −23 tests deleted with node-tree propagation, composition and budget (incl. the one PR 4 failure)
asap-planner 9 / 19 / 1 9 / 19 / 1 19 at PR 5 (were PR 4)
asap-devtools 40 / 54 / 3 40 / 54 / 3 44 at PR 5 (42, plus 2 asap_planner subprocesses), 10 at PR 9
asap-integration-tests 101 / 70 / 4 101 / 69 / 4 49 at PR 5; 19 at PR 6; 1 as before (materialized_views). −1 test (deleted with bind_guarantees)
asap-executor 210 / 63 / 1 210 / 63 / 1 unchanged (PR 6; one PR 5)
asap-cost 31 / 1 / 0 31 / 1 / 0 unchanged (PR 6)
Pass 1, Stage 0, frontends, sql-function-catalog, metricsql_common, Stage 2/3 33, 13, 218/175/49/15, 10, 3, 0, 3 same —
Total 1182 / 208 / 9 1181 / 206 / 9 No failure stops at PR 4

…propagation (#1207 PR 4)

local_guarantee reads the candidate's parameters (LogicalProperties::parameters)
and is public: Pass 2 derives each listed θ's error from it. Node trees carry no
θ since PR 0, so a node's guarantee cannot be composed any more: the node-tree
propagation (propagation.rs with bind_guarantees and raw_data_samples_of_metric),
its operator rules (composition.rs, function_rules.rs), the budget split
(budget.rs) and PropagationStats are deleted. Error propagation is per edge
through each consumer's error contract (LogicalRequirements::error_propagation).
…ts (#1207 PR 4)

LogicalRequirements::error_propagation(consumer, metric) gives g_c's linear
bound (beta and validity cap per input) for errors in one metric, or None when
the consumer has no contract for it (the metric rule). The ratio's denominator
validity cap is RATIO_DENOMINATOR_VALIDITY_CAP. Memo::set_input_requirements
lets Pass 2 tighten each input's parameter floor after the rules.
…le, caps (#1207 PR 4)

Rules over memo groups: 1 tumbling layouts as candidates of the state group
(gcd closure of the evaluation grids, dividing the window); 2 scan widening
(widened scan and label-passing images, Project candidates in the original
groups); 4 top-k reuse (TopK over the largest top-k group); 5 finer aggregate;
6 rollup (SummaryMerge with group_by over the finer state). Rule 3 is Pass 1's
(every input is a group) and rule 7 is rule 6 over windowed builds: both deleted.

After the rules: error caps per group (query targets passed down through each
consumer's error contract, min with validity caps), parameter floors from the
caps for every readout or merge of a sketch state, each candidate's possible
output errors, and the logical edges with alpha^log (floor and metric rule).
…capped; tests (#1207 PR 4)

A merge of sketch states takes the weakest floor of the consumers reading
its group (the merged state has its inputs' θ), which also carries a top-k's
heap size; caps no longer track statistics. A scalar root's operands get no
cap (its expression is not read). Scan widening keeps each mirrored build's
input requirements and skips fused builds. planner-layering.md: Pass 2's
rules and edges.
…ollup filters over one child, states without an error model (#1207 PR 4)

A count's factor 0 makes an approximate input exact before the all-exact check.
Rule 6 compares two states' filters only over one child (filter columns are
positional). A readout of a sample, wavelet or model state has no guarantee.
Shared produces_state, merge_keeps_guarantee and weakest_of; measures compared
with ==; RATIO_DENOMINATOR_VALIDITY_CAP -> RELATIVE_OPERAND_VALIDITY_CAP; rule 7
wording: a longer range from a shorter range's windows is not offered.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

optimizer Workload optimization and plan selection refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant