Skip to content

refactor!: rename AccuracyTarget.error, Rejection.valid, accuracy CLI flags, execution_timed_dag and accuracy_target - #740

Merged
zzylol merged 2 commits into
mainfrom
refactor/approved-renames
Oct 18, 2026
Merged

zzylol merged 2 commits into
mainfrom
refactor/approved-renames

Conversation

@zzylol

@zzylol zzylol commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Why

Several public names did not say what they hold. AccuracyTarget.error is the largest error a query accepts, yet its builder is already AccuracyTarget::max_error(ε). Rejection.valid was a bool that only said whether a candidate failed a check, not why it lost. The devtools flags --epsilon / --delta / --metric did not match the target's field names. execution_timed_dag and realization::accuracy_target were named after how the value is made, not what it is. On #734 the user also asked whether a "node" in NodePriceSource means a machine. It does not.

What

Breaking change. Every rename, old → new:

Kind Old New
Rust field AccuracyTarget.error AccuracyTarget.max_error
JSON key (workload requirements.accuracy, AggIntent.accuracy, stage_pipeline document) "error" "max_error"
Rust field Rejection.valid: bool Rejection.cause: RejectionCause (new enum)
JSON key (stage3_plan_selection.rejected[]) "valid": true/false "cause": "<snake_case RejectionCause>"
CLI flag (stage_pipeline) --epsilon --max-error
CLI flag (stage_pipeline) --delta --failure-probability
CLI flag (stage_pipeline) --metric --accuracy-metric
CLI flag (sketch_coverage) --epsilon --max-error
Viewer /api/plan payload key (server.py, editor.js) "epsilon" "max_error"
Viewer /api/plan payload key "metric" "accuracy_metric"
Viewer editor element ids (index.html) plannerEpsilon, plannerMetric plannerMaxError, plannerAccuracyMetric
Rust method PlanOutput::execution_timed_dag() PlanOutput::physical_asap_dag()
Rust fn realization::accuracy_target(intent) realization::sizing_target(intent)

None of the other devtools binaries (show_logical_dag, variant_coverage, analyze_corpora, convert_rqe_atomic_costs) has accuracy flags.

AccuracySpec.error (the guarantee side, AccuracyGuarantee.spec.error) keeps its name. It holds a BoundExpr: the error bound a plan achieves, an expression over the plan's parameters. It is not a maximum someone asked for, and guarantee.spec.max_error would read like a requirement. Its doc now says "the error bound", and the AccuracySpec doc explains how it differs from the target's max_error.

Docs only, no rename: NodePriceSource, NodeCost, CandidateCost.per_node and the Stage 3 module doc now say that a node is one operator node of the candidate's physical plan DAG (PhysicalASAPDAG), keyed by PhysicalASAPNodeId in CandidateCost.per_node, not a machine. The broken intra-doc link NodeCost::per_second now points to NodeCost::cost_per_second.

Test: promql_binary_arithmetic_preserves_both_scalar_operand_orders was deleted with the legacy search in #736. It is ported onto the stage pipeline and now checks every Stage 1 logical candidate.

Behaviour is unchanged apart from the names: the same candidates are selected at the same costs.

How

  • RejectionCause is built from what select_plan actually produces. It has five failed-check variants, one per rejection site: resize for failure probability, accuracy_check, raw-data check, latency bound, cluster capacity. The latency and capacity checks used to be merged with or_else; they now carry their own causes. It also has six cost-model variants, one per Ranking::lost_reason arm: Costlier, Slower, MoreMemory, NotLowestLatencyOnParetoFront, Dominated, TiedLostOnEnumerationOrder. lost_reason now returns (RejectionCause, String). RejectionCause::failed_a_check() gives back the old !valid. I chose the enum over loses_on_cost_only: bool because the cause was already computed at each site and only dropped. The reason strings are unchanged.
  • stage_pipeline serializes cause with serde (snake_case). The viewer (stages.js) checks that cause is one of the eleven names. It refuses a document without one.
  • The committed Example 1 viewer fixture is regenerated: error → max_error in targets, valid → cause, and the reason strings print max_error: from the target's Debug. Nothing else changed.
  • Docs that name the old flags, keys and functions are updated: library-api.md, input-output-workflow.md, extend-asap-aware-mapping.md, asap-aware-mapping-contracts.md, run-a-query.md, stage-viewer.md, physical-planning-and-deployment.md, updated_interface_with_pluggable_optimization.md and the viewer README.md.

Key code interfaces added / modified

crates/types/src/types.rs (at e73fda264e786c83b2bcccfd594b709f7a2b6c35)

/// An accuracy requirement, as a workload, a caller or a frontend states it:
/// Pr[err_metric(estimate, truth) > max_error] ≤ failure_probability. The type stays.
/// JSON: {"accuracy_metric": "rank", "max_error": 0.01, "failure_probability": 0.001, "accuracy_scope": "whole_result"}
pub struct AccuracyTarget {
    /// The metric ε is stated in; None (default) accepts a guarantee in any metric.
    pub accuracy_metric: Option<AccuracyMetric>,          // unchanged
    /// ε: the largest error the query accepts, in the metric's units; required.
    pub max_error: f64,                                    // was error
    /// δ; None (default) imposes no δ requirement.
    pub failure_probability: Option<f64>,                  // unchanged
    /// Which output values the statement covers; default WholeResult.
    pub accuracy_scope: AccuracyScope,                     // unchanged
}

/// The outer shape of a guarantee (AccuracyGuarantee.spec). The type stays.
pub struct AccuracySpec<E, P> {
    /// The metric the bound is stated in.
    pub accuracy_metric: AccuracyMetric,                   // unchanged
    /// ε: the error bound a plan achieves (a BoundExpr in a guarantee); not a requested maximum.
    pub error: E,                                          // unchanged (doc clarified)
    /// δ the bound holds with.
    pub failure_probability: P,                            // unchanged
    /// Which output values the bound covers.
    pub accuracy_scope: AccuracyScope,                     // unchanged
}

crates/asap-aware-mapping/src/stage3_plan_selection.rs (at e73fda264e786c83b2bcccfd594b709f7a2b6c35)

/// A candidate Stage 3 did not select. The type stays.
pub struct Rejection {
    /// The physical candidate's id, e.g. "P2".
    pub id: String,                                        // unchanged
    /// Why it lost; replaces the bool that only said "failed a check or not".
    pub cause: RejectionCause,                             // new (was valid: bool)
    /// One line with the numbers, e.g. "costlier: 8.540 vs 7.940 cost_per_second".
    pub reason: String,                                    // unchanged
}

/// Why Stage 3 did not select a candidate. New type. JSON: the snake_case name.
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum RejectionCause {
    /// Failed check: its summaries, resized for the failure probabilities, do not rebuild into a plan.
    FailureProbabilityResizeFailed,                        // new
    /// Failed check: a query's accuracy target is not met (guarantee misses or does not hold, no evidence).
    AccuracyTargetMissed,                                  // new
    /// Failed check: it scans raw data at query time, and the deployment does not retain raw data.
    RawDataNotRetained,                                    // new
    /// Failed check: a query's latency is over its max_latency_ms bound.
    LatencyBoundExceeded,                                  // new
    /// Failed check (cost model 3): its usage is over a cluster limit.
    OverClusterCapacity,                                   // new
    /// Lost (cost models 1, 2): higher price than the selected candidate.
    Costlier,                                              // new
    /// Lost (cost model 3, lowest-latency): slower than the selected candidate.
    Slower,                                                // new
    /// Lost (cost model 3, least-memory): more memory than the selected candidate.
    MoreMemory,                                            // new
    /// Lost (cost model 3, pareto-front): on the front but not its lowest-latency member.
    NotLowestLatencyOnParetoFront,                         // new
    /// Lost (cost model 3, pareto-front): another candidate dominates it.
    Dominated,                                             // new
    /// Lost: tied with the selected candidate and later in enumeration order.
    TiedLostOnEnumerationOrder,                            // new
}

impl RejectionCause {
    /// Whether the candidate failed a check rather than losing under the cost model (the old !valid).
    pub fn failed_a_check(self) -> bool;                   // new
}

/// One operator node of the candidate's PhysicalASAPDAG (keyed by PhysicalASAPNodeId
/// in CandidateCost.per_node), not a machine: its price per second. The type stays.
pub struct NodeCost { /* fields unchanged; doc only */ }

crates/asap-aware-mapping/src/stage3_plan_selection/measured_operation_costs.rs (at e73fda264e786c83b2bcccfd594b709f7a2b6c35)

/// Which prices one node's CPU operations came from; a node is one operator node of the
/// physical plan DAG, keyed by PhysicalASAPNodeId in CandidateCost::per_node, not a machine.
/// The type stays; doc only.
pub enum NodePriceSource {
    /// Plan selection's formulas.
    Analytical,                                            // unchanged
    /// The MeasuredOperationCost entry for its sketch and operation.
    Measured,                                              // unchanged
}

crates/asap-aware-mapping/src/pass/mod.rs (at e73fda264e786c83b2bcccfd594b709f7a2b6c35) and realization.rs (at e73fda264e786c83b2bcccfd594b709f7a2b6c35)

impl PlanOutput {
    /// The workload as one PhysicalASAPDAG, every node at query time (default MaterializationAssignment).
    pub fn physical_asap_dag(&self) -> Result<PhysicalASAPDAG, ExecutionDataStateError>; // was execution_timed_dag
}

/// The target an approximate-capable intent's summary is sized and checked against:
/// the query's target restated in the intent's own metric; None for other intents.
pub fn sizing_target(intent: &AggIntent) -> Option<AccuracyTarget>; // was accuracy_target

CLI and viewer: crates/devtools/src/bin/stage_pipeline.rs, sketch_coverage.rs, tools/dag-viewer/server.py (stage_pipeline, sketch_coverage, server.py)

stage_pipeline   --max-error <f64>            # was --epsilon: ε for every query
                 --failure-probability <f64>  # was --delta: δ; needs --max-error
                 --accuracy-metric <metric>   # was --metric: the metric ε is stated in; needs --max-error
sketch_coverage  --max-error <f64>            # was --epsilon (default 0.01)
POST /api/plan   {"max_error": 0.01,          # was "epsilon"
                  "accuracy_metric": "rank"}  # was "metric"

Usage

End to end, from the devtool invocation to Stage 3's rejections. I ran this at the head commit:

# The user's query and its accuracy: ε = 0.01 (--max-error), δ = 0.001
# (--failure-probability), no metric set (no --accuracy-metric), so Stage 3
# accepts a guarantee in any metric.
cargo run -p asap-devtools --bin stage_pipeline -- \
    --promql "topk by (job) (10, sum_over_time(http_requests_total[1m]))" \
    --max-error 0.01 --failure-probability 0.001 --out plan.json
// workload.queries[0].requirements.accuracy: the AccuracyTarget the flags built
// (JSON key max_error, was error); Stage 1 sizes from it via sizing_target(intent).
{"accuracy_scope": "whole_result", "failure_probability": 0.001, "max_error": 0.01}

// stage3_plan_selection: P1 (exact) is selected. Each rejected candidate says why
// through cause (was "valid": false); both failed the accuracy check in select_plan.
"selected": "P1",
"rejected": [
  {"cause": "accuracy_target_missed", "id": "P2",
   "reason": "q1: CmsWithHeap needs non-negative update weights, and these are not proven non-negative (samples of `http_requests_total`, which is not declared a counter)"},
  {"cause": "accuracy_target_missed", "id": "P3",
   "reason": "q1: the output has no guarantee, and the deployment has no evidence that it meets target AccuracyTarget { accuracy_metric: None, max_error: 0.01, failure_probability: Some(0.001), accuracy_scope: WholeResult }"}
]

Valid candidates that lose under the cost model carry a loss cause. From the regenerated Example 1 fixture (--example planner-layering-1, P1 selected at 7.940 per second):

{"cause": "costlier", "id": "P4", "reason": "costlier: 8.540 vs 7.940 cost_per_second"}
{"cause": "tied_lost_on_enumeration_order", "id": "P2", "reason": "tied with the selected candidate P1 at 7.940 cost_per_second; lost on enumeration order"}

Before this PR

  • A target's JSON was {"error": 0.01}, and the Rust field was target.error, while the builder was AccuracyTarget::max_error(0.01).
  • stage3_plan_selection.rejected[] had "valid": true|false. Telling "costlier" from "dominated" or "tied", or "accuracy" from "latency" or "capacity", meant parsing reason.
  • stage_pipeline --epsilon 0.01 --delta 0.001 --metric rank.
  • PlanOutput::execution_timed_dag() and realization::accuracy_target(intent).
  • The Stage 3 docs did not say what a "node" is.

After this PR

  • {"max_error": 0.01} and target.max_error. Old JSON with "error" is rejected (deny_unknown_fields).
  • "cause": "costlier" (or one of the other ten names). Rust callers match on RejectionCause or call failed_a_check().
  • stage_pipeline --max-error 0.01 --failure-probability 0.001 --accuracy-metric rank. The old flags are refused as unknown.
  • PlanOutput::physical_asap_dag() and realization::sizing_target(intent).
  • The Stage 3 docs say that a node is an operator node of the PhysicalASAPDAG, not a machine.

Verification

All of these were run locally on the changed code. These are only the tests that cover the change, not the full workspace suite.

  • cargo fmt --all -- --check: clean.
  • cargo clippy -p asap-types -p asap-aware-mapping -p asap-devtools -p asap-integration-tests -p asap-planner -p asap-frontend-promql -p asap-frontend-sql --all-targets --all-features --locked -- -D warnings: clean.
  • asap-types (types::tests, workload::tests, agg_intent::tests): 31 passed. This includes the JSON round trips with max_error and the rejection of "error".
  • asap-aware-mapping (stage3_plan_selection, budget, realization, pass::tests, per_computation_candidates, accuracy::tests): 138 passed. The tests that checked valid now assert the specific RejectionCause.
  • asap-devtools (tests/stage_pipeline.rs and the bins' unit tests): 24 passed. The document test checks that a failed-check cause is exactly an unpriced candidate.
  • asap-integration-tests (every planner_layering_* binary, which pins selections and costs, plus promql_to_post_asap): 99 passed, 20 ignored as before. The ported promql_binary_arithmetic_preserves_both_scalar_operand_orders passes.
  • asap-planner --test summary_sharing: 9 passed. asap-frontend-promql (promql_conformance and accuracy_target_flows_into_quantile_intent): 106 passed. asap-frontend-sql median_threads_the_accuracy_target: passed.
  • Viewer: python3 -m unittest test_server test_viewer with py_mini_racer and a built stage_pipeline: 51 passed, none skipped.
  • Example 1 fixture: regenerated with stage_pipeline --example planner-layering-1. The diff is only the renamed keys and max_error in the reason strings. The selection (P1 at 7.940 per second) and every cost are unchanged.

Limitations: the old JSON key and flags have no compatibility alias, as intended. Saved workload or stage documents with "error" or "valid" must be written again.

- AccuracyTarget.error is max_error (JSON key "max_error"), matching the
  AccuracyTarget::max_error builder. AccuracySpec.error, the guarantee's
  bound expression, keeps its name.
- Rejection.valid is replaced by cause: RejectionCause, which says why
  Stage 3 did not select the candidate (a failed check or a loss under
  the cost model); stage_pipeline writes it as "cause" and the viewer
  validates it.
- stage_pipeline's --epsilon/--delta/--metric are --max-error,
  --failure-probability and --accuracy-metric; sketch_coverage's
  --epsilon is --max-error; the viewer's /api/plan payload keys follow.
- PlanOutput::execution_timed_dag is physical_asap_dag, and
  realization::accuracy_target is sizing_target.
- Stage 3's node docs say a node is an operator node of the physical
  plan DAG, keyed by PhysicalASAPNodeId, not a machine.

Regenerate the Example 1 viewer fixture for the new keys.
… 1 candidates

Port promql_binary_arithmetic_preserves_both_scalar_operand_orders,
deleted with the legacy search: every logical candidate of
rate(a[1m]) / 2 and 2 / rate(a[1m]) keeps the literal on its side.
@zzylol zzylol added accuracy Accuracy requirements, guarantees, and propagation cost-model Cost formulas, statistics, ranking, and selection dag-viewer DAG export and visualization tooling dev-tools Developer tools, diagnostics, and demos enhancement New feature or request testing Corpus, regression, differential, or benchmark tests labels Oct 17, 2026
@zzylol
zzylol merged commit 359e4da into main Oct 18, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

accuracy Accuracy requirements, guarantees, and propagation cost-model Cost formulas, statistics, ranking, and selection dag-viewer DAG export and visualization tooling dev-tools Developer tools, diagnostics, and demos enhancement New feature or request testing Corpus, regression, differential, or benchmark tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant