Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/asap-aware-mapping/src/accuracy/reconciliation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//!
//! ## The gap this closes
//!
//! `asap_types::pre_asap::cse::share_common_sub_dags` (pre-ASAP CSE) only
//! `asap_types::ir::cse::share_common_sub_dags` (pre-ASAP CSE) only
//! ever merges two sub-DAGs that are *exactly* [`PartialEq`]-equal,
//! including their [`AggIntent`]'s `accuracy: AccuracyTarget` field. Two
//! otherwise-identical aggregates that differ *only* in how tight an
Expand Down Expand Up @@ -303,7 +303,7 @@ impl AccuracyReconciliationStrategy {
///
/// Also requires the candidate's own *output* schema to carry a provable
/// unique key ([`Schema::has_unique_key`]) — the exact legality gate
/// `pre_asap::cse::share_common_sub_dags` already applies to its own
/// `ir::cse::share_common_sub_dags` already applies to its own
/// sharing decisions, and [`crate::rollup::RollupStrategy`] already
/// reuses verbatim for the identical reason (see that module's
/// `is_legal_rollup_source` doc, point 4): a producer's output is only
Expand Down
4 changes: 1 addition & 3 deletions crates/asap-aware-mapping/src/analytical_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2164,8 +2164,6 @@ pub enum AnalyticalCostError {
UnsupportedDataArrival(DataArrival),
#[error("ingestion rate must be finite and non-negative, got {0}")]
InvalidIngestionRate(f64),
#[error("summary lifecycle, maintenance mode, and evaluation schedule are inconsistent")]
IncompatibleLifecycleGuarantee,
#[error("bootstrap row and byte evidence must either both be zero or both be non-zero")]
InconsistentBootstrapEvidence,
#[error("required summary operation cost {0} must be finite and positive, got {1}")]
Expand Down Expand Up @@ -2196,7 +2194,7 @@ pub enum AnalyticalCostError {
UnsupportedQueryOperator,
#[error("inconsistent operator statistics: {0}")]
InconsistentOperatorStatistics(&'static str),
#[error("summary operation {0} has no lifecycle-aware cost formula")]
#[error("summary operation {0} has no cost formula")]
UnsupportedSummaryOperation(&'static str),
#[error("required comparison-scope field {0} is missing")]
MissingComparisonScope(&'static str),
Expand Down
145 changes: 1 addition & 144 deletions crates/asap-aware-mapping/src/cost_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,10 @@ use std::rc::Rc;
use crate::exact_composition::ExactOperation;
use asap_types::ir::{ASAPOp, Operator, OperatorNode};
use asap_types::post_asap::{
FieldDataType, GroupingStrategy, HydraParams, ResultGuarantee, SketchAlgorithm, SketchParams,
SketchStatistic, SummaryMaintenanceLifecycleGuarantee, SummaryWindowFramework,
FieldDataType, GroupingStrategy, HydraParams, SketchAlgorithm, SketchParams, SketchStatistic,
};
use asap_types::pre_asap::agg_intent::AggIntent;
use asap_types::pre_asap::expr_ir::ColumnRef;
use asap_types::types::AccuracyTarget;

use crate::exact_composition::{ExactComposition, OperationPlacement};
use crate::recurrence::{
Expand All @@ -66,9 +64,6 @@ use crate::recurrence::{
use crate::replacement::{
realize_child, Realization, Replacement, ReplacementProvenance, ReplacementSubDAG, TargetSubDAG,
};
use crate::summary_maintenance_lifecycle::{
SummaryMaintenanceCapabilities, SummaryMaintenanceLifecycleCostInputs,
};

// ── Recurring-cost vocabulary for mixed exact/summary plans (issue #171) ──

Expand Down Expand Up @@ -298,38 +293,6 @@ pub struct CseCandidate<'a> {
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Cost(pub f64);

/// One physical summary state and the lifecycle selected for that exact DAG
/// node. Node identity is preserved so whole-DAG models can bind per-state
/// evidence without relying on traversal order.
pub struct CostedSummaryDeployment<'a> {
pub summary: &'a OperatorNode,
pub guarantee: &'a SummaryMaintenanceLifecycleGuarantee,
pub selected_cost: Cost,
}

/// Complete candidate estimate returned to lifecycle and global plan search.
///
/// A deployment-aware model may compare abstract summary-window primitives
/// using evidence supplied by downstream implementations. `window_frameworks`
/// is planner IR: it records the selected semantic realization contract.
/// `physical_plan_id` is separate provider-owned provenance for the concrete
/// implementation whose evidence won; it is not interpreted as planner IR.
#[derive(Debug, Clone, PartialEq)]
pub struct CompleteSummaryCandidateEstimate {
pub cost: Cost,
/// Stable provider identity of the complete implementation whose evidence
/// produced this estimate.
pub physical_plan_id: Option<String>,
/// Window choice for each entry of the `deployments` slice passed to the
/// complete-cost hook. `None` explicitly means that deployment does not
/// use a summary-window framework.
pub window_frameworks: Vec<Option<SummaryWindowFramework>>,
/// End-to-end guarantee supplied by the selected window realization.
/// `None` means that the complete model supplied no window-specific
/// guarantee; `Some` may be exact or approximate.
pub window_accuracy_guarantee: Option<ResultGuarantee>,
}

impl Cost {
/// The cost of an operation that costs nothing at all.
pub const ZERO: Cost = Cost(0.0);
Expand Down Expand Up @@ -770,112 +733,6 @@ pub trait CostModel {
f64::NAN
}

/// Primitive build, update, read, retention, and retirement costs used to
/// compare physical summary-state lifecycles. Unknown values stay
/// unknown, preventing long-lived deployments from winning through
/// optimistic zeroes.
fn summary_maintenance_lifecycle_cost_inputs(
&self,
_summary: &OperatorNode,
) -> SummaryMaintenanceLifecycleCostInputs {
SummaryMaintenanceLifecycleCostInputs::default()
}

/// Horizon-aware form used by lifecycle planning. Models whose retention
/// objective is capacity rather than byte-seconds can normalize their
/// rate so the horizon integral equals one peak-capacity charge.
fn summary_maintenance_lifecycle_cost_inputs_for_horizon(
&self,
summary: &OperatorNode,
_horizon: Option<Horizon>,
) -> SummaryMaintenanceLifecycleCostInputs {
self.summary_maintenance_lifecycle_cost_inputs(summary)
}

/// Physical update/merge/delete support for one concrete summary. The
/// conservative default advertises no long-lived maintenance capability.
fn summary_maintenance_capabilities(
&self,
_summary: &OperatorNode,
) -> SummaryMaintenanceCapabilities {
SummaryMaintenanceCapabilities::default()
}

/// Replace the sum of selected per-state lifecycle costs with a complete
/// root-DAG cost. The default preserves legacy models. Evidence-strict
/// models return `None` when any root operation is unavailable; callers
/// must not then reuse the partial per-state sum.
fn complete_summary_candidate_cost(
&self,
_root: &OperatorNode,
_target: Option<&OperatorNode>,
deployments: &[CostedSummaryDeployment<'_>],
_horizon: Option<Horizon>,
_expected_reads: Option<f64>,
_required_accuracy: &[AccuracyTarget],
) -> Option<Cost> {
Some(Cost(
deployments
.iter()
.map(|deployment| deployment.selected_cost.0)
.sum(),
))
}

/// Complete cost together with selected implementation provenance and
/// planner-visible window primitives. The default preserves cost models
/// that do not perform either decision.
fn complete_summary_candidate_estimate(
&self,
root: &OperatorNode,
target: Option<&OperatorNode>,
deployments: &[CostedSummaryDeployment<'_>],
horizon: Option<Horizon>,
expected_reads: Option<f64>,
required_accuracy: &[AccuracyTarget],
) -> Option<CompleteSummaryCandidateEstimate> {
self.complete_summary_candidate_cost(
root,
target,
deployments,
horizon,
expected_reads,
required_accuracy,
)
.map(|cost| CompleteSummaryCandidateEstimate {
cost,
physical_plan_id: None,
window_frameworks: vec![None; deployments.len()],
window_accuracy_guarantee: None,
})
}

/// Whether the complete-candidate hook is authoritative for lifecycle
/// costs. When true, lifecycle alternatives rejected only because their
/// legacy per-state cost is missing remain eligible for complete-DAG
/// evaluation. Semantic and runtime-capability rejections still apply.
fn complete_summary_candidate_estimate_covers_lifecycle_costs(&self) -> bool {
false
}

/// Cost of evaluating `target` directly from its logical/raw inputs once.
/// When known, lifecycle-aware materialization compares this fallback with
/// the aggregate cost of the selected summary deployments.
fn raw_query_recompute_cost(&self, _target: &OperatorNode) -> Option<Cost> {
None
}

/// Complete raw cost over the comparison context. The default preserves
/// per-read models; context-aware models override this when raw input
/// cardinality changes between evaluations.
fn raw_query_recompute_total_cost(
&self,
target: &OperatorNode,
expected_reads: f64,
) -> Option<Cost> {
self.raw_query_recompute_cost(target)
.map(|per_read| Cost(per_read.0 * expected_reads))
}
/// Physical feasibility evidence for a complete summary candidate.
/// `None` defers admission to physical/deployment compilation; `Some(false)`
/// excludes the candidate without changing its computation or parameters.
Expand Down
92 changes: 5 additions & 87 deletions crates/asap-aware-mapping/src/empirical_cost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@
//! configuration and environment; they are neither runtime feedback nor proofs
//! of an accuracy guarantee. CPU quantities are nanoseconds, never CPU operations.

use asap_types::ir::{ASAPOp, Operator, OperatorNode};
use asap_types::post_asap::{FieldDataType, GroupingStrategy, SketchAlgorithm, SketchParams};
use asap_types::post_asap::{SketchAlgorithm, SketchParams};
use asap_types::pre_asap::AggIntent;
use serde::{Deserialize, Serialize};

use crate::cost_model::{Cost, CostModel, DefaultCostModel};
use crate::cost_model::{CostModel, DefaultCostModel};
use crate::replacement::{
accuracy_budget, accuracy_target, default_size_params, ReplacementSubDAG, TargetSubDAG,
};
use crate::summary_maintenance_lifecycle::SummaryMaintenanceLifecycleCostInputs;

pub const EVIDENCE_SCHEMA_VERSION: u32 = 1;
pub const EVIDENCE_MODEL_VERSION: &str = "empirical-update-cpu-v1";
Expand Down Expand Up @@ -207,40 +205,6 @@ impl EmpiricalEvidenceProvider {
costs.sort_by(|a, b| a.1.total_cmp(&b.1));
costs.into_iter().map(|(algorithm, _)| algorithm).collect()
}

/// Costs for one independently instantiated sketch state, in CPU ns.
/// Unknown retention/retirement remain unavailable; CPU time must not be
/// mixed with an existing deployment's unitless or CPU-operation costs.
pub fn lifecycle_cost_inputs(
&self,
summary: &OperatorNode,
) -> SummaryMaintenanceLifecycleCostInputs {
let Operator::ASAP(ASAPOp::SummaryAgg {
family: FieldDataType::Sketch(kind, GroupingStrategy::PerSubpopulationInstance),
grouping: GroupingStrategy::PerSubpopulationInstance,
..
}) = &summary.operator
else {
return SummaryMaintenanceLifecycleCostInputs::default();
};
let Ok(row) = self.lookup(kind.algorithm(), kind.params()) else {
return SummaryMaintenanceLifecycleCostInputs::default();
};
SummaryMaintenanceLifecycleCostInputs {
build_cost: snapshot_build_cpu(row).map(Cost),
maintenance_cost_per_update: row
.metrics
.resources
.cpu
.update_cpu_ns
.as_ref()
.map(|m| Cost(m.value)),
// A point-frequency benchmark read does not price a total-count
// or quantile read. There is no query request in this hook.
summary_read_cost: None,
..Default::default()
}
}
}

/// Standalone adapter for the existing planner boundary. Empirical data changes
Expand Down Expand Up @@ -276,13 +240,6 @@ impl CostModel for EmpiricalCostModel {
fn estimate_cost(&self, candidate: &ReplacementSubDAG, target: &TargetSubDAG<'_>) -> f64 {
DefaultCostModel.estimate_cost(candidate, target)
}

fn summary_maintenance_lifecycle_cost_inputs(
&self,
summary: &OperatorNode,
) -> SummaryMaintenanceLifecycleCostInputs {
self.provider.lifecycle_cost_inputs(summary)
}
}

impl EvidenceArtifact {
Expand Down Expand Up @@ -360,14 +317,6 @@ fn nonnegative(value: f64) -> bool {
value.is_finite() && value >= 0.0
}

fn snapshot_build_cpu(row: &OfflineMeasurement) -> Option<f64> {
let cpu = row.metrics.resources.cpu.build_cpu_ns.as_ref()?.value
+ row.metrics.resources.cpu.update_cpu_ns.as_ref()?.value
* row.distribution.sample_count as f64
+ snapshot_prepare_cpu(row)?;
nonnegative(cpu).then_some(cpu)
}

/// The existing fixed-snapshot CMS/CountSketch contract needs no separate
/// preparation. Other families must measure that phase, including an explicit
/// zero when no preparation is necessary; absence is not free work.
Expand Down Expand Up @@ -490,30 +439,6 @@ mod tests {
);
}

/// Lifecycle build includes all measured snapshot updates, not just an empty
/// allocation. A missing update measurement cannot become free ingestion.
#[test]
fn lifecycle_build_requires_complete_snapshot_ingestion() {
let (mut artifact, _, _) = fixture();
let row = &mut artifact.records[0];
row.metrics.resources.cpu.build_cpu_ns = Some(Measurement {
value: 10.0,
stddev: None,
samples: 1,
method: None,
});
assert_eq!(snapshot_build_cpu(row), Some(20010.0));
row.metrics.resources.cpu.prepare_cpu_ns = Some(Measurement {
value: 17.0,
stddev: None,
samples: 1,
method: None,
});
assert_eq!(snapshot_build_cpu(row), Some(20027.0));
row.metrics.resources.cpu.update_cpu_ns = None;
assert_eq!(snapshot_build_cpu(row), None);
}

/// Newly shared optional dimensions receive the same numeric validation.
#[test]
fn optional_prepare_and_scan_measurements_are_validated() {
Expand All @@ -537,28 +462,21 @@ mod tests {

/// Only the established frequency-sketch contract can omit preparation.
#[test]
fn unmeasured_preparation_for_other_families_keeps_build_unknown() {
fn unmeasured_preparation_for_other_families_stays_unknown() {
let (mut artifact, _, _) = fixture();
let row = &mut artifact.records[0];
row.metrics.resources.cpu.build_cpu_ns = Some(Measurement {
value: 10.0,
stddev: None,
samples: 1,
method: None,
});
assert_eq!(snapshot_build_cpu(row), Some(20010.0));
row.algorithm = SketchAlgorithm::CountSketch;
assert_eq!(snapshot_prepare_cpu(row), Some(0.0));
row.algorithm = SketchAlgorithm::Kll;
row.params = SketchParams::Kll { k: 269 };
assert_eq!(snapshot_build_cpu(row), None);
assert_eq!(snapshot_prepare_cpu(row), None);
row.metrics.resources.cpu.prepare_cpu_ns = Some(Measurement {
value: 17.0,
stddev: None,
samples: 1,
method: None,
});
assert_eq!(snapshot_build_cpu(row), Some(20027.0));
assert_eq!(snapshot_prepare_cpu(row), Some(17.0));
}

fn fixture() -> (EvidenceArtifact, EvidenceContext, AggIntent) {
Expand Down
Loading