diff --git a/Cargo.lock b/Cargo.lock index ac56cfbbf..1d5067e24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,12 +317,9 @@ dependencies = [ name = "asap-aware-mapping" version = "0.1.0" dependencies = [ - "asap-frontend-promql", "asap-logical-optimizer", - "asap-physical-optimizer", + "asap-plan-selection", "asap-types", - "serde", - "serde_json", "thiserror 2.0.18", ] @@ -330,10 +327,10 @@ dependencies = [ name = "asap-devtools" version = "0.1.0" dependencies = [ - "asap-aware-mapping", "asap-frontend-promql", "asap-frontend-sql", "asap-logical-optimizer", + "asap-plan-selection", "asap-types", "serde", "serde_json", @@ -364,9 +361,9 @@ dependencies = [ name = "asap-frontend-promql" version = "0.1.0" dependencies = [ - "asap-aware-mapping", "asap-frontend-common", "asap-logical-optimizer", + "asap-plan-selection", "asap-types", "promql-parser", ] @@ -375,7 +372,6 @@ dependencies = [ name = "asap-frontend-sql" version = "0.1.0" dependencies = [ - "asap-aware-mapping", "asap-frontend-common", "asap-logical-optimizer", "asap-sql-function-catalog", @@ -391,12 +387,12 @@ dependencies = [ name = "asap-integration-tests" version = "0.1.0" dependencies = [ - "asap-aware-mapping", "asap-frontend-promql", "asap-frontend-sql", "asap-logical-optimizer", "asap-physical-operators", "asap-physical-optimizer", + "asap-plan-selection", "asap-planner", "asap-types", "asap_sketchlib 0.3.0 (git+https://github.com/ProjectASAP/asap_sketchlib)", @@ -420,9 +416,9 @@ dependencies = [ name = "asap-physical-operators" version = "0.1.0" dependencies = [ - "asap-aware-mapping", "asap-frontend-promql", "asap-logical-optimizer", + "asap-plan-selection", "asap-types", "asap_sketchlib 0.3.0 (git+https://github.com/ProjectASAP/asap_sketchlib?rev=5f03ccbd798ed5fec62bdd839bcb331123cab369)", "chrono", @@ -445,6 +441,19 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "asap-plan-selection" +version = "0.1.0" +dependencies = [ + "asap-frontend-promql", + "asap-logical-optimizer", + "asap-physical-optimizer", + "asap-types", + "serde", + "serde_json", + "thiserror 2.0.18", +] + [[package]] name = "asap-planner" version = "0.1.0" @@ -454,6 +463,7 @@ dependencies = [ "asap-frontend-promql", "asap-frontend-sql", "asap-logical-optimizer", + "asap-plan-selection", "asap-types", "thiserror 2.0.18", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 07b9f5f39..655de7b59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/asap-aware-mapping", "crates/logical-optimizer", "crates/physical-optimizer", + "crates/plan-selection", "crates/frontend-promql", "crates/frontend-metricsql", "crates/metricsql-common-parser-support", diff --git a/crates/asap-aware-mapping/Cargo.toml b/crates/asap-aware-mapping/Cargo.toml index b158aafac..44dca7631 100644 --- a/crates/asap-aware-mapping/Cargo.toml +++ b/crates/asap-aware-mapping/Cargo.toml @@ -3,16 +3,12 @@ name = "asap-aware-mapping" version = "0.1.0" edition = "2021" -# #509 Stage 3 and the planner facade over the Stage 1 candidates of -# asap-logical-optimizer and the Stage 2 candidates of asap-physical-optimizer. +# The planner facade (pass/) over the #509 stage crates: Stage 1 +# asap-logical-optimizer and Stage 3 asap-plan-selection (which brings Stage 2). # Never depends on a front end. [dependencies] asap-types = { path = "../types" } asap-logical-optimizer = { path = "../logical-optimizer" } -asap-physical-optimizer = { path = "../physical-optimizer" } +asap-plan-selection = { path = "../plan-selection" } thiserror = "2" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -[dev-dependencies] -asap-frontend-promql = { path = "../frontend-promql" } diff --git a/crates/asap-aware-mapping/src/empirical_comparison.rs b/crates/asap-aware-mapping/src/empirical_comparison.rs deleted file mode 100644 index e08bb37c6..000000000 --- a/crates/asap-aware-mapping/src/empirical_comparison.rs +++ /dev/null @@ -1,915 +0,0 @@ -//! Query-matched, fixed-snapshot offline recommendations. Observed error is an -//! explicit acceptance criterion, never a replacement for formal guarantees. - -use asap_types::ir::schema::{SketchAlgorithm, SketchParams}; -use serde::{Deserialize, Serialize}; - -use crate::empirical_cost::{ - DistributionDescriptor, EmpiricalEvidenceProvider, EnvironmentDescriptor, EvidenceArtifact, - EvidenceContext, Measurement, MeasurementProvenance, OfflineMeasurement, -}; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OfflineQueryDescriptor { - pub kind: String, - pub value_type: String, - /// Identifies the exact probe population used for timing and observed error. - pub probe_set: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct MeasurementQueryBinding { - pub record_id: String, - pub query: OfflineQueryDescriptor, -} - -pub use crate::empirical_resources::ExactResourceMeasurements; - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OfflineExactMeasurement { - pub id: String, - pub distribution: DistributionDescriptor, - pub environment: EnvironmentDescriptor, - pub query: OfflineQueryDescriptor, - pub measured_at_unix_seconds: u64, - pub valid_until_unix_seconds: u64, - pub provenance: MeasurementProvenance, - pub metrics: ExactResourceMeasurements, -} - -/// The companion format binds otherwise query-agnostic sketch primitives to -/// their measured evaluation and exact reference. Bindings describe state after -/// ingestion, without merges or intervening updates during the read sequence. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OfflineComparisonEvidence { - pub schema_version: u32, - /// Every CPU phase excludes other phases and destruction of retained state. - pub timing_contract: String, - pub sketch_evidence: EvidenceArtifact, - pub query_bindings: Vec, - pub exact_records: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EmpiricalAccuracyRequirement { - pub metric: String, - pub max_observed_mean: f64, - pub minimum_trials: u32, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OfflineWorkload { - pub input_items_per_state: u64, - /// Reads of the fixed, fully ingested snapshot; no updates between reads. - pub reads_per_state: u64, - pub merges_per_state: u64, - pub state_instances: u64, - pub horizon_seconds: f64, -} - -/// Explicit scalarization: CPU ns × cpu_ns_weight + byte-seconds × -/// retained_byte_seconds_weight. Weights must be nonnegative and not both zero. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ResourceWeights { - pub cpu_ns_weight: f64, - pub retained_byte_seconds_weight: f64, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct SketchConfiguration { - pub algorithm: SketchAlgorithm, - pub params: SketchParams, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct OfflineComparisonRequest { - pub context: EvidenceContext, - pub exact_environment: EnvironmentDescriptor, - pub query: OfflineQueryDescriptor, - pub accuracy: EmpiricalAccuracyRequirement, - pub workload: OfflineWorkload, - pub weights: ResourceWeights, - /// `Some` restricts selection to these algorithms and configurations at - /// least as large as their formally legal deployment parameters. `None` - /// requests a purely offline recommendation, unsuitable for formal binding. - pub formal_minimums: Option>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OfflineResourceEstimate { - pub cpu_ns: f64, - pub retained_bytes: Option, - pub retained_byte_seconds: Option, - /// Conservative sum of per-state construction/ingestion peaks. - pub peak_bytes_upper_bound: Option, - /// Per-state snapshot sizes, not charged as writes in this in-memory model. - pub serialized_bytes_per_state: Option, - pub disk_bytes_per_state: Option, - pub objective_cost: f64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OfflineCandidateEstimate { - pub record_id: String, - pub configuration: Option, - pub observed_error_mean: Option, - pub resources: Option, - pub rejection: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OfflineRecommendation { - pub query: OfflineQueryDescriptor, - pub selected: OfflineCandidateEstimate, - pub exact_baseline: OfflineCandidateEstimate, - pub candidates: Vec, - pub estimated_cpu_savings_ns: f64, - pub estimated_retained_bytes_savings: Option, - /// True only if the request supplied and every selected sketch passed - /// explicit formal minima; observed error alone cannot authorize binding. - pub checked_formal_minimums: bool, - pub assumptions: Vec, -} - -impl OfflineRecommendation { - /// An exact selection deliberately returns no sketch. The caller must - /// preserve its exact path; do not silently substitute a default sketch. - pub fn selected_sketch(&self) -> Option<&SketchConfiguration> { - self.selected.configuration.as_ref() - } -} - -/// Compare complete CPU components for a fixed snapshot and retain every -/// unavailable alternative with its reason. Without an applicable exact -/// baseline no benefit can be established, so this returns an error. -pub fn recommend_offline( - evidence: &OfflineComparisonEvidence, - request: &OfflineComparisonRequest, -) -> Result { - validate_request(request)?; - if evidence.schema_version != 1 { - return Err("unsupported offline comparison schema version".into()); - } - if evidence.timing_contract != "disjoint_live_state_v1" { - return Err( - "comparison requires disjoint CPU phases measured with retained state alive".into(), - ); - } - let provider = - EmpiricalEvidenceProvider::new(evidence.sketch_evidence.clone(), request.context.clone()) - .map_err(|e| e.to_string())?; - let mut bindings = std::collections::HashMap::new(); - for binding in &evidence.query_bindings { - if bindings - .insert(&binding.record_id, &binding.query) - .is_some() - { - return Err("duplicate query binding".into()); - } - if !evidence - .sketch_evidence - .records - .iter() - .any(|r| r.id == binding.record_id) - { - return Err("query binding refers to missing record".into()); - } - } - let exact: Vec<_> = evidence - .exact_records - .iter() - .filter(|r| { - r.distribution == request.context.distribution - && r.environment == request.exact_environment - && r.query == request.query - && r.measured_at_unix_seconds <= request.context.now_unix_seconds - && request.context.now_unix_seconds <= r.valid_until_unix_seconds - }) - .collect(); - if exact.len() != 1 { - return Err("missing, stale, incompatible or ambiguous exact baseline".into()); - } - let exact = exact[0]; - validate_exact(exact)?; - let baseline_resources = estimate_exact(exact, request)?; - let exact_baseline = OfflineCandidateEstimate { - record_id: exact.id.clone(), - configuration: None, - observed_error_mean: Some(0.0), - resources: Some(baseline_resources), - rejection: None, - }; - let mut candidates = Vec::new(); - for row in &evidence.sketch_evidence.records { - let mut candidate = OfflineCandidateEstimate { - record_id: row.id.clone(), - configuration: Some(SketchConfiguration { - algorithm: row.algorithm.clone(), - params: row.params.clone(), - }), - observed_error_mean: row.error.as_ref().and_then(|e| e.mean), - resources: None, - rejection: None, - }; - let result = (|| { - let matched = provider - .lookup(&row.algorithm, &row.params) - .map_err(|e| e.to_string())?; - if matched.id != row.id { - return Err("record belongs to another applicability context".into()); - } - if bindings.get(&row.id).copied() != Some(&request.query) { - return Err("missing or incompatible measured query binding".into()); - } - if let Some(minimums) = &request.formal_minimums { - if !minimums.iter().any(|m| { - m.algorithm == row.algorithm && parameters_at_least(&row.params, &m.params) - }) { - return Err( - "configuration does not meet the deployment's formal minimum".into(), - ); - } - } - let error = row - .error - .as_ref() - .ok_or("missing offline error observation")?; - if error.query.get("kind").and_then(|v| v.as_str()) != Some(request.query.kind.as_str()) - || error.query.get("value_type").and_then(|v| v.as_str()) - != Some(request.query.value_type.as_str()) - { - return Err( - "offline error observation has incompatible evaluation semantics".into(), - ); - } - if error.metric != request.accuracy.metric - || error.trials < request.accuracy.minimum_trials - { - return Err("incompatible error metric or insufficient offline trials".into()); - } - let mean = error.mean.ok_or("missing observed mean error")?; - if mean > request.accuracy.max_observed_mean { - return Err("observed error exceeds explicit offline acceptance budget".into()); - } - estimate_sketch(row, request) - })(); - match result { - Ok(resources) => candidate.resources = Some(resources), - Err(reason) => candidate.rejection = Some(reason), - } - candidates.push(candidate); - } - let mut selected = exact_baseline.clone(); - for candidate in &candidates { - if let Some(resources) = &candidate.resources { - if resources.objective_cost < selected.resources.as_ref().unwrap().objective_cost { - selected = candidate.clone(); - } - } - } - let baseline = exact_baseline.resources.as_ref().unwrap(); - let chosen = selected.resources.as_ref().unwrap(); - Ok(OfflineRecommendation { query: request.query.clone(), - estimated_cpu_savings_ns: baseline.cpu_ns - chosen.cpu_ns, - estimated_retained_bytes_savings: baseline.retained_bytes.zip(chosen.retained_bytes).map(|(a,b)| a-b), - selected, exact_baseline, candidates, checked_formal_minimums: request.formal_minimums.is_some(), - assumptions: vec![ - "Fixed snapshot: construct, ingest all measured input, prepare exact index once, then read without updates or merges".into(), - "Read CPU is the measured average over all distinct keys; individual key latency and error can differ".into(), - "CPU sums disjoint measured construction/update/prepare/read phases while state remains alive; the comparison ends with retained state and excludes retirement".into(), - "No serialization or disk-write CPU is modeled; reported disk/serialized bytes describe one optional persisted snapshot only".into(), - "Offline mean-error acceptance is restricted to the measured input and probe population; it is not a formal or runtime error guarantee".into(), - ] }) -} - -fn validate_request(request: &OfflineComparisonRequest) -> Result<(), String> { - let w = &request.workload; - if request.query.kind != "point_frequency" - || request.query.value_type != "i64" - || request.query.probe_set != "all_distinct_keys" - { - return Err("unsupported offline query contract".into()); - } - if request.accuracy.metric.trim().is_empty() - || !nonnegative(request.accuracy.max_observed_mean) - || request.accuracy.minimum_trials == 0 - { - return Err("invalid empirical accuracy requirement".into()); - } - if w.input_items_per_state != request.context.distribution.sample_count - || w.state_instances == 0 - || !w.horizon_seconds.is_finite() - || w.horizon_seconds <= 0.0 - { - return Err( - "workload must match the measured snapshot and have positive states/horizon".into(), - ); - } - if w.merges_per_state != 0 { - return Err("no post-merge error or exact merge baseline was measured".into()); - } - if !nonnegative(request.weights.cpu_ns_weight) - || !nonnegative(request.weights.retained_byte_seconds_weight) - || request.weights.cpu_ns_weight == 0.0 - && request.weights.retained_byte_seconds_weight == 0.0 - { - return Err("invalid resource objective weights".into()); - } - let a = &request.context.environment; - let b = &request.exact_environment; - if a.cpu != b.cpu || a.os != b.os || a.runtime != b.runtime { - return Err( - "sketch and exact measurements must share hardware, OS and benchmark runtime".into(), - ); - } - Ok(()) -} - -fn validate_exact(row: &OfflineExactMeasurement) -> Result<(), String> { - let p = &row.provenance; - if row.id.trim().is_empty() - || [ - &p.command, - &p.dataset, - &p.source_revision, - &row.environment.id, - &row.environment.implementation, - &row.environment.implementation_version, - ] - .iter() - .any(|s| s.trim().is_empty()) - || p.repetitions == 0 - || row.measured_at_unix_seconds > row.valid_until_unix_seconds - { - return Err("invalid exact baseline provenance".into()); - } - let m = &row.metrics.resources; - for measurement in [ - &m.cpu.build_cpu_ns, - &m.cpu.update_cpu_ns, - &m.cpu.merge_cpu_ns, - &m.cpu.prepare_cpu_ns, - &m.cpu.read_cpu_ns, - &m.retained_memory_bytes, - &m.peak_memory_bytes, - &m.serialized_bytes, - &m.disk_bytes, - &m.scan_bytes, - ] - .into_iter() - .flatten() - { - if !nonnegative(measurement.value) - || measurement.samples == 0 - || measurement.stddev.is_some_and(|s| !nonnegative(s)) - { - return Err("invalid exact baseline measurement".into()); - } - } - Ok(()) -} - -fn charge(measurement: &Option, count: u64, name: &str) -> Result { - if count == 0 { - return Ok(0.0); - } - let value = measurement - .as_ref() - .ok_or_else(|| format!("missing {name}"))? - .value - * count as f64; - if !nonnegative(value) { - return Err(format!("invalid or overflowing {name}")); - } - Ok(value) -} - -fn estimate_sketch( - row: &OfflineMeasurement, - request: &OfflineComparisonRequest, -) -> Result { - let m = &row.metrics.resources; - let w = &request.workload; - let cpu = charge(&m.cpu.build_cpu_ns, 1, "empty sketch construction CPU")? - + charge( - &m.cpu.update_cpu_ns, - w.input_items_per_state, - "sketch update CPU", - )? - + charge( - &m.cpu.read_cpu_ns, - w.reads_per_state, - "query-matched sketch read CPU", - )? - + charge(&m.cpu.merge_cpu_ns, w.merges_per_state, "sketch merge CPU")? - + crate::empirical_cost::snapshot_prepare_cpu(row) - .ok_or("missing or invalid sketch snapshot preparation CPU")?; - estimate_resources( - cpu, - &m.retained_memory_bytes, - &m.peak_memory_bytes, - m.serialized_bytes.as_ref().map(|m| m.value), - m.disk_bytes.as_ref().map(|m| m.value), - request, - ) -} - -fn estimate_exact( - row: &OfflineExactMeasurement, - request: &OfflineComparisonRequest, -) -> Result { - let m = &row.metrics.resources; - let w = &request.workload; - let cpu = charge(&m.cpu.build_cpu_ns, 1, "exact empty construction CPU")? - + charge( - &m.cpu.update_cpu_ns, - w.input_items_per_state, - "exact update CPU", - )? - + charge(&m.cpu.prepare_cpu_ns, 1, "exact snapshot preparation CPU")? - + charge(&m.cpu.read_cpu_ns, w.reads_per_state, "exact read CPU")?; - estimate_resources( - cpu, - &m.retained_memory_bytes, - &m.peak_memory_bytes, - m.serialized_bytes.as_ref().map(|m| m.value), - m.disk_bytes.as_ref().map(|m| m.value), - request, - ) -} - -fn estimate_resources( - per_state_cpu: f64, - retained: &Option, - peak: &Option, - serialized: Option, - disk: Option, - request: &OfflineComparisonRequest, -) -> Result { - let count = request.workload.state_instances as f64; - let cpu_ns = per_state_cpu * count; - let retained_bytes = retained.as_ref().map(|m| m.value * count); - let retained_byte_seconds = retained_bytes.map(|v| v * request.workload.horizon_seconds); - let peak_bytes_upper_bound = peak.as_ref().map(|m| m.value * count); - let memory_cost = if request.weights.retained_byte_seconds_weight == 0.0 { - 0.0 - } else { - retained_byte_seconds.ok_or("missing retained memory for weighted resource objective")? - * request.weights.retained_byte_seconds_weight - }; - let objective_cost = cpu_ns * request.weights.cpu_ns_weight + memory_cost; - if [ - Some(cpu_ns), - retained_bytes, - retained_byte_seconds, - peak_bytes_upper_bound, - Some(objective_cost), - ] - .into_iter() - .flatten() - .any(|v| !nonnegative(v)) - { - return Err("overflowing resource estimate".into()); - } - Ok(OfflineResourceEstimate { - cpu_ns, - retained_bytes, - retained_byte_seconds, - peak_bytes_upper_bound, - serialized_bytes_per_state: serialized, - disk_bytes_per_state: disk, - objective_cost, - }) -} - -fn nonnegative(value: f64) -> bool { - value.is_finite() && value >= 0.0 -} - -/// Conservative componentwise dominance for known planner sizing families. -/// A deployment still checks its own catalog/layout constraints before binding. -pub fn parameters_at_least(candidate: &SketchParams, minimum: &SketchParams) -> bool { - match (candidate, minimum) { - (SketchParams::Cms { width: a, depth: b }, SketchParams::Cms { width: c, depth: d }) - | ( - SketchParams::CountSketch { width: a, depth: b }, - SketchParams::CountSketch { width: c, depth: d }, - ) => a >= c && b >= d, - (SketchParams::Kll { k: a }, SketchParams::Kll { k: b }) - | (SketchParams::Kmv { k: a }, SketchParams::Kmv { k: b }) - | (SketchParams::Theta { k: a }, SketchParams::Theta { k: b }) => a >= b, - (SketchParams::Hll { precision: a }, SketchParams::Hll { precision: b }) => a >= b, - (SketchParams::DDSketch { alpha: a }, SketchParams::DDSketch { alpha: b }) => { - a.is_finite() && *a > 0.0 && a <= b - } - ( - SketchParams::CmsWithHeap { - width: a, - depth: b, - heap_size: c, - }, - SketchParams::CmsWithHeap { - width: d, - depth: e, - heap_size: f, - }, - ) - | ( - SketchParams::CountSketchWithHeap { - width: a, - depth: b, - heap_size: c, - }, - SketchParams::CountSketchWithHeap { - width: d, - depth: e, - heap_size: f, - }, - ) => a >= d && b >= e && c >= f, - _ => false, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn m(value: f64) -> Option { - Some(Measurement { - value, - stddev: None, - samples: 3, - method: Some("synthetic test fixture, not measured".into()), - }) - } - - fn fixture() -> (OfflineComparisonEvidence, OfflineComparisonRequest) { - let mut sketches: EvidenceArtifact = serde_json::from_str(include_str!( - "../tests/data/offline-evidence-synthetic.json" - )) - .unwrap(); - let query = OfflineQueryDescriptor { - kind: "point_frequency".into(), - value_type: "i64".into(), - probe_set: "all_distinct_keys".into(), - }; - let row = &mut sketches.records[0]; - row.metrics.resources.cpu.build_cpu_ns = m(1.0); - row.metrics.resources.cpu.update_cpu_ns = m(1.0); - row.metrics.resources.cpu.read_cpu_ns = m(1.0); - row.metrics.resources.retained_memory_bytes = m(100.0); - row.error.as_mut().unwrap().mean = Some(0.5); - row.error.as_mut().unwrap().query = - serde_json::json!({"kind":"point_frequency","value_type":"i64"}); - let mut wide = row.clone(); - wide.id = "synthetic-wide-cms".into(); - wide.params = SketchParams::Cms { - width: 544, - depth: 5, - }; - wide.metrics.resources.cpu.update_cpu_ns = m(2.0); - wide.metrics.resources.retained_memory_bytes = m(200.0); - wide.error.as_mut().unwrap().mean = Some(0.001); - let context = EvidenceContext { - distribution: row.distribution.clone(), - environment: row.environment.clone(), - now_unix_seconds: 150, - }; - let mut exact_environment = row.environment.clone(); - exact_environment.id = "synthetic-exact".into(); - exact_environment.implementation = "synthetic exact baseline".into(); - let exact = OfflineExactMeasurement { - id: "exact".into(), - distribution: row.distribution.clone(), - environment: exact_environment.clone(), - query: query.clone(), - measured_at_unix_seconds: 100, - valid_until_unix_seconds: 200, - provenance: row.provenance.clone(), - metrics: ExactResourceMeasurements { - resources: asap_types::workload::resources::MeasuredResources { - cpu: asap_types::workload::resources::MeasuredCpu { - build_cpu_ns: m(1.0), - update_cpu_ns: m(5.0), - prepare_cpu_ns: m(1000.0), - read_cpu_ns: m(5.0), - ..Default::default() - }, - retained_memory_bytes: m(1000.0), - ..Default::default() - }, - }, - }; - let metric = row.error.as_ref().unwrap().metric.clone(); - sketches.records.push(wide); - let bindings = sketches - .records - .iter() - .map(|r| MeasurementQueryBinding { - record_id: r.id.clone(), - query: query.clone(), - }) - .collect(); - let request = OfflineComparisonRequest { - context, - exact_environment, - query, - accuracy: EmpiricalAccuracyRequirement { - metric, - max_observed_mean: 0.01, - minimum_trials: 1, - }, - workload: OfflineWorkload { - input_items_per_state: 1000, - reads_per_state: 1000, - merges_per_state: 0, - state_instances: 1, - horizon_seconds: 10.0, - }, - weights: ResourceWeights { - cpu_ns_weight: 1.0, - retained_byte_seconds_weight: 0.0, - }, - formal_minimums: None, - }; - ( - OfflineComparisonEvidence { - schema_version: 1, - timing_contract: "disjoint_live_state_v1".into(), - sketch_evidence: sketches, - query_bindings: bindings, - exact_records: vec![exact], - }, - request, - ) - } - - /// Observed acceptance rejects a cheap inaccurate rung and chooses a larger - /// measured configuration, with every CPU component and state counted. - #[test] - fn accuracy_requirement_changes_selected_configuration_and_cost() { - let (evidence, mut request) = fixture(); - request.formal_minimums = Some(vec![SketchConfiguration { - algorithm: SketchAlgorithm::Cms, - params: SketchParams::Cms { - width: 512, - depth: 5, - }, - }]); - let chosen = recommend_offline(&evidence, &request).unwrap(); - assert_eq!(chosen.selected.record_id, "synthetic-wide-cms"); - assert_eq!(chosen.selected.resources.as_ref().unwrap().cpu_ns, 3001.0); - assert_eq!( - chosen.exact_baseline.resources.as_ref().unwrap().cpu_ns, - 11001.0 - ); - assert_eq!(chosen.estimated_cpu_savings_ns, 8000.0); - assert_eq!(chosen.estimated_retained_bytes_savings, Some(800.0)); - assert!(chosen.checked_formal_minimums); - request.formal_minimums = None; - request.accuracy.max_observed_mean = 1.0; - assert_eq!( - recommend_offline(&evidence, &request) - .unwrap() - .selected - .record_id, - "synthetic-test-cms" - ); - } - - /// Optional sketch preparation is charged once; exact preparation never - /// borrows the independent merge measurement, and snapshot bytes survive. - #[test] - fn preparation_and_exact_resource_dimensions_keep_their_meaning() { - let (mut evidence, request) = fixture(); - let old = recommend_offline(&evidence, &request).unwrap(); - evidence.sketch_evidence.records[1] - .metrics - .resources - .cpu - .prepare_cpu_ns = m(37.0); - let exact = &mut evidence.exact_records[0].metrics.resources; - exact.cpu.merge_cpu_ns = m(1e9); - exact.serialized_bytes = m(256.0); - exact.disk_bytes = m(4096.0); - exact.scan_bytes = m(8000.0); - let changed = recommend_offline(&evidence, &request).unwrap(); - assert_eq!( - changed.selected.resources.as_ref().unwrap().cpu_ns, - old.selected.resources.as_ref().unwrap().cpu_ns + 37.0 - ); - let baseline = changed.exact_baseline.resources.unwrap(); - assert_eq!( - baseline.cpu_ns, - old.exact_baseline.resources.unwrap().cpu_ns - ); - assert_eq!(baseline.serialized_bytes_per_state, Some(256.0)); - assert_eq!(baseline.disk_bytes_per_state, Some(4096.0)); - } - - /// Exact observations validate optional dimensions even when the comparison - /// does not execute the corresponding operation. - #[test] - fn optional_exact_dimensions_cannot_hide_invalid_measurements() { - let selectors: [fn( - &mut asap_types::workload::resources::MeasuredResources, - ) -> &mut Option; 4] = [ - |r| &mut r.cpu.merge_cpu_ns, - |r| &mut r.serialized_bytes, - |r| &mut r.disk_bytes, - |r| &mut r.scan_bytes, - ]; - for select in selectors { - let (mut evidence, request) = fixture(); - *select(&mut evidence.exact_records[0].metrics.resources) = m(-1.0); - assert!(recommend_offline(&evidence, &request).is_err()); - } - } - - /// A family outside the established CMS/CountSketch contract cannot gain - /// an apparently cheap comparison by omitting its preparation measurement. - #[test] - fn sketch_comparison_requires_unknown_preparation_phase() { - let (evidence, request) = fixture(); - let mut row = evidence.sketch_evidence.records[1].clone(); - let original_cpu = estimate_sketch(&row, &request).unwrap().cpu_ns; - row.algorithm = SketchAlgorithm::Kll; - row.params = SketchParams::Kll { k: 269 }; - assert!(estimate_sketch(&row, &request) - .unwrap_err() - .contains("preparation CPU")); - row.metrics.resources.cpu.prepare_cpu_ns = m(37.0); - assert_eq!( - estimate_sketch(&row, &request).unwrap().cpu_ns, - original_cpu + 37.0 - ); - } - - /// Missing required measurements and failing observed-error budgets return - /// the applicable exact baseline, never an optimistically free sketch. - #[test] - fn missing_cost_or_failed_error_acceptance_selects_exact() { - let (mut evidence, mut request) = fixture(); - request.accuracy.max_observed_mean = 0.0; - assert!(recommend_offline(&evidence, &request) - .unwrap() - .selected_sketch() - .is_none()); - request.accuracy.max_observed_mean = 0.01; - evidence.sketch_evidence.records[1] - .metrics - .resources - .cpu - .build_cpu_ns = None; - let chosen = recommend_offline(&evidence, &request).unwrap(); - assert!(chosen.selected_sketch().is_none()); - assert!(chosen.candidates[1] - .rejection - .as_ref() - .unwrap() - .contains("construction")); - evidence.exact_records[0] - .metrics - .resources - .cpu - .prepare_cpu_ns = None; - assert!(recommend_offline(&evidence, &request) - .unwrap_err() - .contains("preparation")); - } - - /// Query, environment, snapshot cardinality, metric, trial count and - /// validity are required independently; nearby evidence is not extrapolated. - #[test] - fn applicability_is_checked_before_recommendation() { - let (evidence, request) = fixture(); - for case in ["query", "environment", "cardinality", "expired", "merges"] { - let mut request = request.clone(); - match case { - "query" => request.query.kind = "total_count".into(), - "environment" => request.exact_environment.cpu = "other CPU".into(), - "cardinality" => request.workload.input_items_per_state = 1001, - "expired" => request.context.now_unix_seconds = 201, - "merges" => request.workload.merges_per_state = 1, - _ => unreachable!(), - } - assert!(recommend_offline(&evidence, &request).is_err(), "{case}"); - } - for case in ["metric", "trials", "binding", "evaluation"] { - let mut evidence = evidence.clone(); - let mut request = request.clone(); - match case { - "metric" => request.accuracy.metric = "rank_error".into(), - "trials" => request.accuracy.minimum_trials = 100, - "binding" => evidence.query_bindings.clear(), - "evaluation" => { - for row in &mut evidence.sketch_evidence.records { - row.error.as_mut().unwrap().query["kind"] = - serde_json::json!("total_count"); - } - } - _ => unreachable!(), - } - assert!( - recommend_offline(&evidence, &request) - .unwrap() - .selected_sketch() - .is_none(), - "{case}" - ); - } - } - - /// Resource weights have explicit dimensions; missing memory blocks a - /// memory-weighted objective but does not become a zero-memory estimate. - #[test] - fn resource_objective_and_unknown_memory_are_explicit() { - let (mut evidence, mut request) = fixture(); - evidence.sketch_evidence.records[1] - .metrics - .resources - .retained_memory_bytes = None; - let chosen = recommend_offline(&evidence, &request).unwrap(); - assert!(chosen.selected.resources.unwrap().retained_bytes.is_none()); - request.weights.retained_byte_seconds_weight = 1.0; - assert!(recommend_offline(&evidence, &request) - .unwrap() - .selected_sketch() - .is_none()); - evidence.sketch_evidence.records[1] - .metrics - .resources - .retained_memory_bytes = m(10000.0); - assert!(recommend_offline(&evidence, &request) - .unwrap() - .selected_sketch() - .is_none()); - request.weights.cpu_ns_weight = f64::NAN; - assert!(recommend_offline(&evidence, &request).is_err()); - } - - /// A measured rung below a deployment's formal sizing floor cannot be - /// selected even if its error happened to be zero on the offline input. - #[test] - fn formal_minimums_cannot_be_relaxed_by_observed_accuracy() { - let (evidence, mut request) = fixture(); - request.formal_minimums = Some(vec![SketchConfiguration { - algorithm: SketchAlgorithm::Cms, - params: SketchParams::Cms { - width: 1024, - depth: 5, - }, - }]); - assert!(recommend_offline(&evidence, &request) - .unwrap() - .selected_sketch() - .is_none()); - assert!(!parameters_at_least( - &SketchParams::Cms { - width: 1024, - depth: 4 - }, - &SketchParams::Cms { - width: 512, - depth: 5 - } - )); - assert!(!parameters_at_least( - &SketchParams::CountSketch { - width: 1024, - depth: 5 - }, - &SketchParams::Cms { - width: 512, - depth: 5 - } - )); - } - - /// Consuming-wrapper CPU phases and ambiguous exact generations cannot - /// establish a complete fixed-snapshot comparison. - #[test] - fn comparison_requires_disjoint_phases_and_one_exact_generation() { - let (mut evidence, request) = fixture(); - evidence.timing_contract = "consuming_upstream_wrappers".into(); - assert!(recommend_offline(&evidence, &request) - .unwrap_err() - .contains("disjoint")); - evidence.timing_contract = "disjoint_live_state_v1".into(); - evidence - .exact_records - .push(evidence.exact_records[0].clone()); - assert!(recommend_offline(&evidence, &request) - .unwrap_err() - .contains("ambiguous")); - } -} diff --git a/crates/asap-aware-mapping/src/erp.rs b/crates/asap-aware-mapping/src/erp.rs deleted file mode 100644 index 6b4ad6737..000000000 --- a/crates/asap-aware-mapping/src/erp.rs +++ /dev/null @@ -1,859 +0,0 @@ -//! Distribution-conditioned Error–Resource Profiles (ERP). -//! -//! This is a discrete, auditable profile selector. It does not infer a formal -//! sketch guarantee from benchmark observations and it does not interpolate -//! across distributions, implementations, or parameter points. - -use std::collections::{BTreeMap, HashSet}; - -use serde::{Deserialize, Serialize}; - -pub const ERP_SCHEMA_VERSION: u32 = 1; - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ErpResourceProfile { - pub memory_bytes: f64, - pub update_cpu_seconds: f64, - pub merge_cpu_seconds: f64, - pub query_cpu_seconds: f64, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ErpRecord { - pub id: String, - pub sketch: String, - pub implementation: String, - pub parameters: serde_json::Value, - /// Opaque but equality-matched sketch-bench workload descriptor. - pub distribution: serde_json::Value, - pub trials: u32, - pub error_metrics: BTreeMap, - pub resources: ErpResourceProfile, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ErpArtifact { - pub schema_version: u32, - pub producer_version: String, - pub records: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum AccuracyMode { - /// ERP may select a smaller empirical configuration. The result is not a - /// formal guarantee and must be labelled accordingly by the deployment. - Empirical, - /// Same selection as empirical mode, but absence of applicable evidence is - /// reported so the caller can fall back to formal sizing or exact execution. - Hybrid, -} - -#[derive(Debug, Clone)] -pub struct ErpSelectionRequest { - pub distribution: serde_json::Value, - pub implementation: Option, - pub allowed_sketches: Vec, - pub error_metric: String, - pub max_error: f64, - pub min_trials: u32, - pub expected_updates: f64, - pub expected_queries: f64, - pub expected_merges: f64, - pub retention_seconds: f64, - pub cpu_weight: f64, - pub byte_second_weight: f64, - pub mode: AccuracyMode, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct ErpSelection<'a> { - pub record: &'a ErpRecord, - pub observed_error: f64, - pub estimated_cost: f64, - pub accuracy_mode: AccuracyMode, -} - -/// Runtime-observable input shape used to match a benchmark scenario. Input -/// volume is a sufficiency gate, not a distance axis: once the benchmark has -/// enough samples, repeating the same stationary distribution adds little -/// information about sketch error. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ErpDataShape { - pub cardinality: u64, - /// Stable distribution family, for example `uniform`, `zipf`, - /// `power_law`, or `empirical`. Different families are never interpolated. - pub family: String, - /// Family-specific numeric parameters. Zipf uses `exponent`; a continuous - /// power law may use `alpha` and `minimum`. Uniform has no parameters. - #[serde(default)] - pub parameters: BTreeMap, - pub benchmark_events: u64, -} - -/// One hypothesis fitted to the same bounded runtime observation. Lower -/// goodness-of-fit is better; confidence is in [0, 1]. Keeping all plausible -/// fits avoids prematurely classifying unknown data as one named family. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ErpShapeFit { - pub family: String, - #[serde(default)] - pub parameters: BTreeMap, - pub goodness_of_fit: f64, - pub confidence: f64, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ErpShapeObservation { - pub cardinality: u64, - pub observed_events: u64, - pub fits: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub empirical_fingerprint: Option, -} - -#[derive(Debug, Clone)] -pub struct ErpMultiFitSelectionRequest { - pub selection: ErpSelectionRequest, - pub observed: ErpShapeObservation, - pub minimum_benchmark_events: u64, - pub max_log2_cardinality_distance: f64, - pub max_parameter_distance: f64, - pub max_goodness_of_fit: f64, - pub minimum_confidence: f64, - /// Required confidence separation between the best and second-best fit. - pub minimum_confidence_margin: f64, -} - -#[derive(Debug, Clone)] -pub struct ErpNearestSelectionRequest { - pub selection: ErpSelectionRequest, - pub observed: ErpDataShape, - pub minimum_benchmark_events: u64, - pub max_log2_cardinality_distance: f64, - /// Maximum normalized distance for every common distribution parameter. - pub max_parameter_distance: f64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ErpWindowWorkload { - pub input_updates: u64, - pub query_executions: u64, - pub panes_per_query: u64, - pub retained_panes: u64, - pub materializations: u64, -} - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ErpOperationCounts { - pub updates: u64, - pub merges: u64, - pub queries: u64, - pub retained_sketches: u64, -} - -#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] -pub enum ErpError { - #[error("unsupported ERP schema version {0}")] - UnsupportedVersion(u32), - #[error("invalid ERP artifact: {0}")] - Invalid(&'static str), - #[error("no applicable ERP configuration satisfies the empirical accuracy requirement")] - NoApplicableConfiguration, -} - -impl ErpArtifact { - pub fn validate(&self) -> Result<(), ErpError> { - if self.schema_version != ERP_SCHEMA_VERSION { - return Err(ErpError::UnsupportedVersion(self.schema_version)); - } - if self.producer_version.trim().is_empty() { - return Err(ErpError::Invalid("missing producer version")); - } - let mut ids = HashSet::new(); - for row in &self.records { - if row.id.trim().is_empty() || !ids.insert(row.id.as_str()) { - return Err(ErpError::Invalid("empty or duplicate record id")); - } - if row.sketch.trim().is_empty() - || row.implementation.trim().is_empty() - || row.trials == 0 - || !row.resources.valid() - || row - .error_metrics - .values() - .any(|value| !value.is_finite() || *value < 0.0) - { - return Err(ErpError::Invalid("invalid profile record")); - } - } - Ok(()) - } - - /// Select the least-cost measured parameter point satisfying the requested - /// empirical error. Distribution and implementation matching are exact by - /// design; a later model may add conservative interpolation explicitly. - pub fn select(&self, request: &ErpSelectionRequest) -> Result, ErpError> { - self.validate()?; - if !request.valid() { - return Err(ErpError::Invalid("invalid selection request")); - } - self.records - .iter() - .filter(|row| row.distribution == request.distribution) - .filter(|row| { - request - .implementation - .as_ref() - .is_none_or(|wanted| &row.implementation == wanted) - }) - .filter(|row| { - request.allowed_sketches.is_empty() - || request - .allowed_sketches - .iter() - .any(|name| name == &row.sketch) - }) - .filter(|row| row.trials >= request.min_trials) - .filter_map(|row| { - let observed_error = *row.error_metrics.get(&request.error_metric)?; - (observed_error <= request.max_error).then(|| ErpSelection { - record: row, - observed_error, - estimated_cost: request.cost(&row.resources), - accuracy_mode: request.mode, - }) - }) - .min_by(|left, right| { - left.estimated_cost - .total_cmp(&right.estimated_cost) - .then_with(|| left.record.id.cmp(&right.record.id)) - }) - .ok_or(ErpError::NoApplicableConfiguration) - } - - /// Select against the nearest compatible measured shape. Shape metadata is - /// read from `distribution.erp_shape`, keeping ERP v1 wire compatibility. - pub fn select_nearest( - &self, - request: &ErpNearestSelectionRequest, - ) -> Result, ErpError> { - self.validate()?; - if !request.selection.valid() || !request.valid() { - return Err(ErpError::Invalid("invalid nearest-shape request")); - } - self.records - .iter() - .filter_map(|row| Some((row, data_shape(&row.distribution)?))) - .filter(|(_, shape)| shape.benchmark_events >= request.minimum_benchmark_events) - .filter_map(|(row, shape)| Some((row, request.distance(shape)?))) - .filter(|(_, distance)| *distance <= 1.0) - .filter(|(row, _)| { - request - .selection - .implementation - .as_ref() - .is_none_or(|wanted| &row.implementation == wanted) - && (request.selection.allowed_sketches.is_empty() - || request - .selection - .allowed_sketches - .iter() - .any(|name| name == &row.sketch)) - && row.trials >= request.selection.min_trials - }) - .filter_map(|(row, distance)| { - let observed_error = *row.error_metrics.get(&request.selection.error_metric)?; - (observed_error <= request.selection.max_error).then_some(( - distance, - ErpSelection { - record: row, - observed_error, - estimated_cost: request.selection.cost(&row.resources), - accuracy_mode: request.selection.mode, - }, - )) - }) - .min_by(|(left_distance, left), (right_distance, right)| { - left_distance - .total_cmp(right_distance) - .then_with(|| left.estimated_cost.total_cmp(&right.estimated_cost)) - .then_with(|| left.record.id.cmp(&right.record.id)) - }) - .map(|(_, selection)| selection) - .ok_or(ErpError::NoApplicableConfiguration) - } - - /// Select using every statistically plausible fit for one observation. - /// Ambiguous and poor fits fail closed so callers can use their - /// theoretical-then-exact fallback policy. - pub fn select_multi_fit( - &self, - request: &ErpMultiFitSelectionRequest, - ) -> Result, ErpError> { - self.validate()?; - if !request.selection.valid() || !request.valid() { - return Err(ErpError::Invalid("invalid multi-fit request")); - } - if let Some(selected) = - request - .observed - .empirical_fingerprint - .as_deref() - .and_then(|wanted| { - self.records - .iter() - .filter(|row| benchmark_fingerprint(&row.distribution) == Some(wanted)) - .filter_map(|row| request.selection.evaluate(row).map(|value| (row, value))) - .min_by(|(left_row, left), (right_row, right)| { - left.estimated_cost - .total_cmp(&right.estimated_cost) - .then_with(|| left_row.id.cmp(&right_row.id)) - }) - .map(|(_, selected)| selected) - }) - { - return Ok(selected); - } - let mut fits = request - .observed - .fits - .iter() - .filter(|fit| { - fit.confidence >= request.minimum_confidence - && fit.goodness_of_fit <= request.max_goodness_of_fit - }) - .collect::>(); - fits.sort_by(|left, right| right.confidence.total_cmp(&left.confidence)); - let Some(best) = fits.first() else { - return Err(ErpError::NoApplicableConfiguration); - }; - if fits.get(1).is_some_and(|second| { - best.confidence - second.confidence < request.minimum_confidence_margin - }) { - return Err(ErpError::NoApplicableConfiguration); - } - // Compare every plausible fit with every compatible benchmark shape. - // Confidence and fit quality contribute to the joint score; neither - // field first collapses the observation to one family. - self.records - .iter() - .filter_map(|row| Some((row, data_shape(&row.distribution)?))) - .filter(|(_, shape)| shape.benchmark_events >= request.minimum_benchmark_events) - .flat_map(|(row, shape)| { - fits.iter().filter_map(move |fit| { - let nearest = ErpNearestSelectionRequest { - selection: request.selection.clone(), - observed: ErpDataShape { - cardinality: request.observed.cardinality, - family: fit.family.clone(), - parameters: fit.parameters.clone(), - benchmark_events: request.observed.observed_events, - }, - minimum_benchmark_events: request.minimum_benchmark_events, - max_log2_cardinality_distance: request.max_log2_cardinality_distance, - max_parameter_distance: request.max_parameter_distance, - }; - let shape_distance = nearest.distance(shape.clone())?; - (shape_distance <= 1.0).then_some((row, fit, shape_distance)) - }) - }) - .filter_map(|(row, fit, shape_distance)| { - let selected = request.selection.evaluate(row)?; - let fit_distance = - fit.goodness_of_fit / request.max_goodness_of_fit.max(f64::EPSILON); - let confidence_distance = 1.0 - fit.confidence; - Some(( - shape_distance.max(fit_distance).max(confidence_distance), - selected, - )) - }) - .min_by(|(left_distance, left), (right_distance, right)| { - left_distance - .total_cmp(right_distance) - .then_with(|| left.estimated_cost.total_cmp(&right.estimated_cost)) - .then_with(|| left.record.id.cmp(&right.record.id)) - }) - .map(|(_, selected)| selected) - .ok_or(ErpError::NoApplicableConfiguration) - } -} - -fn benchmark_fingerprint(distribution: &serde_json::Value) -> Option<&str> { - distribution - .pointer("/erp_shape/empirical_fingerprint") - .or_else(|| distribution.pointer("/workload/external/fingerprint")) - .and_then(serde_json::Value::as_str) -} - -fn data_shape(distribution: &serde_json::Value) -> Option { - serde_json::from_value(distribution.get("erp_shape")?.clone()).ok() -} - -impl ErpNearestSelectionRequest { - fn valid(&self) -> bool { - self.minimum_benchmark_events > 0 - && self.observed.cardinality > 0 - && self.max_log2_cardinality_distance.is_finite() - && self.max_log2_cardinality_distance > 0.0 - && !self.observed.family.trim().is_empty() - && self - .observed - .parameters - .values() - .all(|value| value.is_finite()) - && self.max_parameter_distance.is_finite() - && self.max_parameter_distance > 0.0 - } - - fn distance(&self, candidate: ErpDataShape) -> Option { - if candidate.cardinality == 0 - || candidate.family != self.observed.family - || candidate - .parameters - .keys() - .ne(self.observed.parameters.keys()) - { - return None; - } - let cardinality = ((candidate.cardinality as f64).log2() - - (self.observed.cardinality as f64).log2()) - .abs() - / self.max_log2_cardinality_distance; - let parameters = candidate - .parameters - .iter() - .map(|(name, value)| { - let observed = self.observed.parameters.get(name)?; - (value.is_finite() && observed.is_finite()) - .then_some((value - observed).abs() / self.max_parameter_distance) - }) - .collect::>>()? - .into_iter() - .fold(0.0_f64, f64::max); - Some(cardinality.max(parameters)) - } -} - -impl ErpMultiFitSelectionRequest { - fn valid(&self) -> bool { - self.observed.cardinality > 0 - && self.observed.observed_events > 0 - && (!self.observed.fits.is_empty() || self.observed.empirical_fingerprint.is_some()) - && self.max_goodness_of_fit.is_finite() - && self.max_goodness_of_fit >= 0.0 - && self.minimum_confidence.is_finite() - && (0.0..=1.0).contains(&self.minimum_confidence) - && self.minimum_confidence_margin.is_finite() - && (0.0..=1.0).contains(&self.minimum_confidence_margin) - && self.observed.fits.iter().all(|fit| { - !fit.family.trim().is_empty() - && fit.goodness_of_fit.is_finite() - && fit.goodness_of_fit >= 0.0 - && fit.confidence.is_finite() - && (0.0..=1.0).contains(&fit.confidence) - && fit.parameters.values().all(|value| value.is_finite()) - }) - } -} - -impl ErpOperationCounts { - /// Compose atomic benchmark costs with a pane-based window plan. A query - /// over one pane needs no merge; N panes need N-1 merges. - pub fn from_window(workload: ErpWindowWorkload) -> Self { - Self { - updates: workload - .input_updates - .saturating_mul(workload.materializations), - merges: workload - .query_executions - .saturating_mul(workload.panes_per_query.saturating_sub(1)), - queries: workload.query_executions, - retained_sketches: workload - .retained_panes - .saturating_mul(workload.materializations), - } - } - - pub fn cpu_seconds(self, resources: &ErpResourceProfile) -> f64 { - self.updates as f64 * resources.update_cpu_seconds - + self.merges as f64 * resources.merge_cpu_seconds - + self.queries as f64 * resources.query_cpu_seconds - } -} - -impl ErpResourceProfile { - fn valid(&self) -> bool { - [ - self.memory_bytes, - self.update_cpu_seconds, - self.merge_cpu_seconds, - self.query_cpu_seconds, - ] - .into_iter() - .all(|value| value.is_finite() && value >= 0.0) - } -} - -impl ErpSelectionRequest { - fn evaluate<'a>(&self, row: &'a ErpRecord) -> Option> { - if self - .implementation - .as_ref() - .is_some_and(|wanted| &row.implementation != wanted) - || (!self.allowed_sketches.is_empty() - && !self.allowed_sketches.iter().any(|name| name == &row.sketch)) - || row.trials < self.min_trials - { - return None; - } - let observed_error = *row.error_metrics.get(&self.error_metric)?; - (observed_error <= self.max_error).then(|| ErpSelection { - record: row, - observed_error, - estimated_cost: self.cost(&row.resources), - accuracy_mode: self.mode, - }) - } - - fn valid(&self) -> bool { - !self.error_metric.trim().is_empty() - && self.max_error.is_finite() - && self.max_error >= 0.0 - && self.min_trials > 0 - && [ - self.expected_updates, - self.expected_queries, - self.expected_merges, - self.retention_seconds, - self.cpu_weight, - self.byte_second_weight, - ] - .into_iter() - .all(|value| value.is_finite() && value >= 0.0) - } - - fn cost(&self, resources: &ErpResourceProfile) -> f64 { - self.cpu_weight - * (self.expected_updates * resources.update_cpu_seconds - + self.expected_queries * resources.query_cpu_seconds - + self.expected_merges * resources.merge_cpu_seconds) - + self.byte_second_weight * self.retention_seconds * resources.memory_bytes - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn row(id: &str, width: u64, error: f64, memory: f64) -> ErpRecord { - ErpRecord { - id: id.into(), - sketch: "cms".into(), - implementation: "oxide".into(), - parameters: serde_json::json!({"width": width, "depth": 3}), - distribution: serde_json::json!({"synthetic":{"description":{"kind":"zipf"}}}), - trials: 20, - error_metrics: BTreeMap::from([("relative_error".into(), error)]), - resources: ErpResourceProfile { - memory_bytes: memory, - update_cpu_seconds: 1e-7, - merge_cpu_seconds: 1e-5, - query_cpu_seconds: 1e-6, - }, - } - } - - fn request() -> ErpSelectionRequest { - ErpSelectionRequest { - distribution: serde_json::json!({"synthetic":{"description":{"kind":"zipf"}}}), - implementation: Some("oxide".into()), - allowed_sketches: vec!["cms".into()], - error_metric: "relative_error".into(), - max_error: 0.01, - min_trials: 10, - expected_updates: 1_000.0, - expected_queries: 100.0, - expected_merges: 0.0, - retention_seconds: 60.0, - cpu_weight: 1.0, - byte_second_weight: 1e-9, - mode: AccuracyMode::Hybrid, - } - } - - #[test] - fn selects_cheapest_applicable_accurate_configuration() { - let artifact = ErpArtifact { - schema_version: ERP_SCHEMA_VERSION, - producer_version: "bench-1".into(), - records: vec![ - row("too-small", 256, 0.02, 6_144.0), - row("winner", 512, 0.009, 12_288.0), - row("overprovisioned", 2048, 0.001, 49_152.0), - ], - }; - let selected = artifact.select(&request()).unwrap(); - assert_eq!(selected.record.id, "winner"); - } - - #[test] - fn distribution_mismatch_fails_closed() { - let artifact = ErpArtifact { - schema_version: ERP_SCHEMA_VERSION, - producer_version: "bench-1".into(), - records: vec![row("cms", 512, 0.009, 12_288.0)], - }; - let mut request = request(); - request.distribution = serde_json::json!({"external":{"dataset":"production"}}); - assert_eq!( - artifact.select(&request), - Err(ErpError::NoApplicableConfiguration) - ); - } - - fn multi_fit_request() -> ErpMultiFitSelectionRequest { - ErpMultiFitSelectionRequest { - selection: request(), - observed: ErpShapeObservation { - cardinality: 1_000, - observed_events: 20_000, - fits: vec![ErpShapeFit { - family: "zipf".into(), - parameters: BTreeMap::from([("exponent".into(), 1.2)]), - goodness_of_fit: 0.03, - confidence: 0.95, - }], - empirical_fingerprint: None, - }, - minimum_benchmark_events: 10_000, - max_log2_cardinality_distance: 1.0, - max_parameter_distance: 0.2, - max_goodness_of_fit: 0.1, - minimum_confidence: 0.8, - minimum_confidence_margin: 0.1, - } - } - - #[test] - fn multi_fit_selects_only_confident_well_fitting_family() { - let mut measured = row("zipf", 512, 0.009, 12_288.0); - measured.distribution = serde_json::json!({"erp_shape": { - "cardinality": 1000, "family": "zipf", - "parameters": {"exponent": 1.22}, "benchmark_events": 20000 - }}); - let artifact = ErpArtifact { - schema_version: ERP_SCHEMA_VERSION, - producer_version: "bench-1".into(), - records: vec![measured], - }; - assert_eq!( - artifact - .select_multi_fit(&multi_fit_request()) - .unwrap() - .record - .id, - "zipf" - ); - } - - #[test] - fn multi_fit_rejects_ambiguous_or_poor_observations() { - let mut measured = row("zipf", 512, 0.009, 12_288.0); - measured.distribution = serde_json::json!({"erp_shape": { - "cardinality": 1000, "family": "zipf", - "parameters": {"exponent": 1.2}, "benchmark_events": 20000 - }}); - let artifact = ErpArtifact { - schema_version: ERP_SCHEMA_VERSION, - producer_version: "bench-1".into(), - records: vec![measured], - }; - let mut ambiguous = multi_fit_request(); - ambiguous.observed.fits.push(ErpShapeFit { - family: "normal".into(), - parameters: BTreeMap::from([("mean".into(), 0.0), ("stddev".into(), 1.0)]), - goodness_of_fit: 0.04, - confidence: 0.90, - }); - assert_eq!( - artifact.select_multi_fit(&ambiguous), - Err(ErpError::NoApplicableConfiguration) - ); - ambiguous.observed.fits.truncate(1); - ambiguous.observed.fits[0].goodness_of_fit = 0.5; - assert_eq!( - artifact.select_multi_fit(&ambiguous), - Err(ErpError::NoApplicableConfiguration) - ); - } - - #[test] - fn multi_fit_jointly_ranks_all_plausible_families() { - let mut distant_high_confidence = row("zipf-distant", 512, 0.009, 1.0); - distant_high_confidence.distribution = serde_json::json!({"erp_shape": { - "cardinality": 1000, "family": "zipf", - "parameters": {"exponent": 1.39}, "benchmark_events": 20000 - }}); - let mut close_lower_confidence = row("normal-close", 512, 0.009, 100.0); - close_lower_confidence.distribution = serde_json::json!({"erp_shape": { - "cardinality": 1000, "family": "normal", - "parameters": {"mean": 4.0, "stddev": 1.0}, "benchmark_events": 20000 - }}); - let artifact = ErpArtifact { - schema_version: ERP_SCHEMA_VERSION, - producer_version: "bench-1".into(), - records: vec![distant_high_confidence, close_lower_confidence], - }; - let mut request = multi_fit_request(); - request.observed.fits.push(ErpShapeFit { - family: "normal".into(), - parameters: BTreeMap::from([("mean".into(), 4.0), ("stddev".into(), 1.0)]), - goodness_of_fit: 0.01, - confidence: 0.82, - }); - request.minimum_confidence_margin = 0.1; - assert_eq!( - artifact.select_multi_fit(&request).unwrap().record.id, - "normal-close" - ); - } - - #[test] - fn exact_empirical_fingerprint_precedes_fits_and_requires_identity() { - let mut exact = row("exact-trace", 512, 0.009, 100.0); - exact.distribution = serde_json::json!({"workload": {"external": { - "dataset": "trace-a", "fingerprint": "sha256:abc" - }}}); - let artifact = ErpArtifact { - schema_version: ERP_SCHEMA_VERSION, - producer_version: "bench-1".into(), - records: vec![exact], - }; - let mut request = multi_fit_request(); - request.observed.empirical_fingerprint = Some("sha256:abc".into()); - request.observed.fits.clear(); - assert_eq!( - artifact.select_multi_fit(&request).unwrap().record.id, - "exact-trace" - ); - request.observed.empirical_fingerprint = Some("sha256:different".into()); - assert_eq!( - artifact.select_multi_fit(&request), - Err(ErpError::NoApplicableConfiguration) - ); - } - - /// Verifies the JSON contract emitted by sketch-bench is directly - /// consumable without a backend-specific translation layer. - #[test] - fn deserializes_sketch_bench_wire_format() { - let json = serde_json::json!({ - "schema_version": 1, - "producer_version": "sketch-bench-rev", - "records": [{ - "id": "erp-0", - "sketch": "cms", - "implementation": "oxide", - "parameters": {"width": 512, "depth": 3}, - "distribution": {"synthetic": {"description": {"kind": "zipf"}}}, - "trials": 20, - "error_metrics": {"relative_error": 0.009}, - "resources": { - "memory_bytes": 12288.0, - "update_cpu_seconds": 1e-7, - "merge_cpu_seconds": 1e-5, - "query_cpu_seconds": 1e-6 - } - }] - }); - let artifact: ErpArtifact = serde_json::from_value(json).unwrap(); - let selected = artifact.select(&request()).unwrap(); - assert_eq!(selected.record.id, "erp-0"); - assert_eq!(selected.accuracy_mode, AccuracyMode::Hybrid); - } - - #[test] - fn nearest_shape_prefers_cardinality_and_skew_then_cost() { - let mut close = row("close", 512, 0.009, 12_288.0); - close.distribution = serde_json::json!({"erp_shape": { - "cardinality": 1000, "family": "zipf", "parameters": {"exponent": 1.2}, "benchmark_events": 100000 - }}); - let mut cheap_but_far = row("far", 256, 0.009, 1.0); - cheap_but_far.distribution = serde_json::json!({"erp_shape": { - "cardinality": 8000, "family": "zipf", "parameters": {"exponent": 1.2}, "benchmark_events": 100000 - }}); - let artifact = ErpArtifact { - schema_version: ERP_SCHEMA_VERSION, - producer_version: "bench-1".into(), - records: vec![cheap_but_far, close], - }; - let selected = artifact - .select_nearest(&ErpNearestSelectionRequest { - selection: request(), - observed: ErpDataShape { - cardinality: 1200, - family: "zipf".into(), - parameters: BTreeMap::from([("exponent".into(), 1.1)]), - benchmark_events: 0, - }, - minimum_benchmark_events: 10_000, - max_log2_cardinality_distance: 4.0, - max_parameter_distance: 0.5, - }) - .unwrap(); - assert_eq!(selected.record.id, "close"); - } - - #[test] - fn nearest_shape_rejects_distribution_family_and_small_benchmarks() { - let mut row = row("uniform", 512, 0.009, 12_288.0); - row.distribution = serde_json::json!({"erp_shape": { - "cardinality": 1000, "family": "uniform", "parameters": {}, "benchmark_events": 999 - }}); - let artifact = ErpArtifact { - schema_version: ERP_SCHEMA_VERSION, - producer_version: "bench-1".into(), - records: vec![row], - }; - let nearest = ErpNearestSelectionRequest { - selection: request(), - observed: ErpDataShape { - cardinality: 1000, - family: "zipf".into(), - parameters: BTreeMap::from([("exponent".into(), 1.0)]), - benchmark_events: 0, - }, - minimum_benchmark_events: 1_000, - max_log2_cardinality_distance: 1.0, - max_parameter_distance: 0.5, - }; - assert_eq!( - artifact.select_nearest(&nearest), - Err(ErpError::NoApplicableConfiguration) - ); - } - - #[test] - fn pane_window_composes_atomic_operation_costs() { - let counts = ErpOperationCounts::from_window(ErpWindowWorkload { - input_updates: 1_000, - query_executions: 10, - panes_per_query: 12, - retained_panes: 24, - materializations: 2, - }); - assert_eq!(counts.updates, 2_000); - assert_eq!(counts.merges, 110); - assert_eq!(counts.queries, 10); - assert_eq!(counts.retained_sketches, 48); - assert!((counts.cpu_seconds(&row("cost", 1, 0.0, 0.0).resources) - 0.00131).abs() < 1e-12); - } -} diff --git a/crates/asap-aware-mapping/src/lib.rs b/crates/asap-aware-mapping/src/lib.rs index f67c32c93..0ae67ff63 100644 --- a/crates/asap-aware-mapping/src/lib.rs +++ b/crates/asap-aware-mapping/src/lib.rs @@ -1,11 +1,8 @@ -//! `asap-aware-mapping` — #509 Stage 3 and the planner facade, over the -//! Stage 1 candidates of [`asap_logical_optimizer`] and the Stage 2 -//! candidates of [`asap_physical_optimizer`]. +//! `asap-aware-mapping` — the planner facade over the #509 stage crates: +//! Stage 1 [`asap_logical_optimizer`], Stage 2 `asap-physical-optimizer` and +//! Stage 3 [`asap_plan_selection`]. //! -//! It is being split into one crate per stage (#572): Stage 1 lives in -//! `asap-logical-optimizer` and Stage 2 in `asap-physical-optimizer`; Stage 3 -//! (`plan_selection`, the cost model and its inputs) and the facade (`pass`) -//! remain here for now. +//! It is being retired (#572): the facade (`pass`) moves to `asap-planner`. //! //! ## Planning workflows //! @@ -14,9 +11,9 @@ //! selection, the only stage that prices plans. //! - Legacy: search the workload with //! [`asap_logical_optimizer::search_workload`], then rank with -//! [`cost_sorted`](plan_selection::candidate_selection::cost_sorted) or +//! [`cost_sorted`](asap_plan_selection::candidate_selection::cost_sorted) or //! select with -//! [`global_selection`](plan_selection::candidate_selection::global_selection) +//! [`global_selection`](asap_plan_selection::candidate_selection::global_selection) //! and assemble each query root with //! [`GlobalSelection::assemble_selected_dag`](asap_logical_optimizer::GlobalSelection::assemble_selected_dag). //! Every summary runs at query time until Stage 2 materialization (#509) @@ -24,52 +21,10 @@ //! //! Physical operator binding, placement, storage, deployment, and execution //! remain downstream responsibilities. -//! -//! ## Supporting components -//! -//! - [`cost_model`] — the [`CostModel`](cost_model::CostModel) trait every -//! deployment's cost-based selection plugs into (issues #6, #33). This crate -//! ships [`DefaultCostModel`](cost_model::DefaultCostModel), which keeps the -//! built-in static preference order and exposes a numeric cost per -//! candidate through [`CostModel::estimate_cost`](cost_model::CostModel::estimate_cost). -//! - [`recurrence`] — recurring and one-shot cost rates over a horizon. -//! - [`analytical_cost`], [`physical_plan_cost_model`], [`empirical_cost`] — -//! analytical and evidence-based pricing for Stage 3. -pub mod analytical_cost; -pub mod cost_model; -pub mod empirical_comparison; -pub mod empirical_cost; -pub mod empirical_resources; -pub mod erp; -pub mod pane_sharing; pub mod pass; -pub mod physical_handoff_cost; -pub mod physical_operator_statistics; -pub mod physical_plan_cost_model; -pub mod query_physical_lowering; -pub mod recurrence; -pub mod storage_io; -#[cfg(test)] -mod test_support; -pub use cost_model::{ - maintenance_operation_plan_cost_rate, raw_recompute_cost_rate, read_operation_plan_cost_rate, - CostModel, CostProvenance, CostUnit, DefaultCostModel, ExactCompositionCostInputs, - ExactCompositionCostRequest, ValueOperationCapabilities, -}; pub use pass::{ optimize, OptimizationInput, OptimizationInputError, OptimizationPass, OptimizeError, - PassNameConflict, PassRegistry, PlanOutput, PlanningModels, QueryPlan, StagePipeline, + PassNameConflict, PassRegistry, PlanOutput, QueryPlan, StagePipeline, }; -pub use plan_selection::candidate_selection::{ - CompositionDecision, CostedGlobalSelection, RankedTargetSubDAGCandidates, RecurrenceProfileMap, -}; -pub use recurrence::{ - evaluation_rate_of, total_cost, update_rate_from_data_workload, CostRate, EvaluationRate, - Horizon, RecurrenceCostExplanation, RecurrenceError, RecurrenceProfile, RootRecurrence, - UpdateRate, -}; - -/// #509 Stage 3 MVP: accuracy check, analytical pricing, cheapest valid plan. -pub mod plan_selection; diff --git a/crates/asap-aware-mapping/src/pane_sharing.rs b/crates/asap-aware-mapping/src/pane_sharing.rs deleted file mode 100644 index 0d944b2da..000000000 --- a/crates/asap-aware-mapping/src/pane_sharing.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Costed reuse of compatible physical pane producers. The executor supplies -//! an equality key covering source, state, phase and evidence. This pass never -//! changes logical evaluation windows or assumes compatibility from metric names. - -/// A concrete mergeable-pane implementation and its horizon costs. -#[derive(Debug, Clone)] -pub struct PaneReuseCandidate { - pub compatibility: K, - pub lookback_ms: u64, - /// Build, update, residency and retirement for this producer. Candidates - /// with the same key must use the same unit costs and pane width, making - /// the longest-lived producer sufficient for every evaluation in the group. - pub producer_cost: f64, - /// Evaluation cost for all consumers of this distinct producer. - pub read_cost: f64, -} - -#[derive(Debug, PartialEq)] -pub struct SharedPaneGroup { - pub members: Vec, - pub lookback_ms: u64, - pub cost: f64, -} - -/// Select build-once reuse when it is cheaper than independent producers. -/// Nonfinite quotes are ineligible, not zero-cost alternatives. The caller -/// retains independent implementations for candidates absent from the result. -pub fn select_shared_panes(candidates: &[PaneReuseCandidate]) -> Vec { - let mut groups: Vec> = Vec::new(); - for (index, candidate) in candidates.iter().enumerate() { - if candidate.lookback_ms == 0 - || ![candidate.producer_cost, candidate.read_cost] - .into_iter() - .all(|cost| cost.is_finite() && cost >= 0.0) - { - continue; - } - if let Some(group) = groups - .iter_mut() - .find(|group| candidates[group[0]].compatibility == candidate.compatibility) - { - group.push(index); - } else { - groups.push(vec![index]); - } - } - groups - .into_iter() - .filter_map(|members| { - if members.len() < 2 { - return None; - } - let mut independent = 0.0; - let mut producer = 0.0_f64; - let mut reads = 0.0; - let mut lookback_ms = 0; - for &index in &members { - let c = &candidates[index]; - independent += c.producer_cost + c.read_cost; - producer = producer.max(c.producer_cost); - reads += c.read_cost; - lookback_ms = lookback_ms.max(c.lookback_ms); - } - let cost = producer + reads; - (independent.is_finite() && cost.is_finite() && cost < independent).then_some( - SharedPaneGroup { - members, - lookback_ms, - cost, - }, - ) - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - fn offer(key: &str, lookback_ms: u64, producer_cost: f64) -> PaneReuseCandidate<&str> { - PaneReuseCandidate { - compatibility: key, - lookback_ms, - producer_cost, - read_cost: 2.0, - } - } - // Share source work once while retaining both evaluation charges and longest history. - #[test] - fn shares_compatible_windows() { - assert_eq!( - select_shared_panes(&[ - offer("a", 60_000, 10.0), - offer("a", 600_000, 15.0), - offer("b", 600_000, 15.0) - ]), - vec![SharedPaneGroup { - members: vec![0, 1], - lookback_ms: 600_000, - cost: 19.0 - }] - ); - } - // Invalid evidence and free producers must not fabricate a sharing benefit. - #[test] - fn excludes_invalid_and_nonbeneficial_quotes() { - for cost in [0.0, f64::NAN, f64::INFINITY, -1.0, f64::MAX] { - assert!( - select_shared_panes(&[offer("a", 60_000, cost), offer("a", 600_000, cost)]) - .is_empty() - ); - } - } -} diff --git a/crates/asap-aware-mapping/src/pass/mod.rs b/crates/asap-aware-mapping/src/pass/mod.rs index d894aa993..54ecf451a 100644 --- a/crates/asap-aware-mapping/src/pass/mod.rs +++ b/crates/asap-aware-mapping/src/pass/mod.rs @@ -24,10 +24,10 @@ use asap_types::ir::{apply_materialization_timings, MaterializationAssignment, T use asap_types::workload::parsed_workload::ParsedWorkload; use asap_types::workload::WorkloadError; -use crate::plan_selection::{Selection, SelectionError}; use asap_logical_optimizer::pass1::logical_candidates::LogicalCandidateError; +use asap_plan_selection::{Selection, SelectionError}; -pub use crate::plan_selection::PlanningModels; +use asap_plan_selection::PlanningModels; pub use stage_pipeline::StagePipeline; // ── Input ──────────────────────────────────────────────────────────────── diff --git a/crates/asap-aware-mapping/src/pass/stage_pipeline.rs b/crates/asap-aware-mapping/src/pass/stage_pipeline.rs index d974b30c2..2ff56a9a2 100644 --- a/crates/asap-aware-mapping/src/pass/stage_pipeline.rs +++ b/crates/asap-aware-mapping/src/pass/stage_pipeline.rs @@ -14,8 +14,8 @@ use asap_types::ir::{OperatorNode, QueryRoot}; use asap_types::workload::QueryLanguage; use super::{OptimizationInput, OptimizationPass, OptimizeError, PlanOutput, QueryPlan}; -use crate::plan_selection::select_plan; use asap_logical_optimizer::pass1::logical_candidates::enumerate_local_logical_candidates; +use asap_plan_selection::select_plan; #[derive(Debug, Default, Clone, Copy)] pub struct StagePipeline; diff --git a/crates/asap-physical-operators/Cargo.toml b/crates/asap-physical-operators/Cargo.toml index b8e063127..ec420fc98 100644 --- a/crates/asap-physical-operators/Cargo.toml +++ b/crates/asap-physical-operators/Cargo.toml @@ -17,6 +17,6 @@ regex = "1" [dev-dependencies] rmp-serde = "1" -asap-aware-mapping = { path = "../asap-aware-mapping" } +asap-plan-selection = { path = "../plan-selection" } asap-logical-optimizer = { path = "../logical-optimizer" } asap-frontend-promql = { path = "../frontend-promql" } diff --git a/crates/asap-physical-operators/src/physical_planner/candidates.rs b/crates/asap-physical-operators/src/physical_planner/candidates.rs index ec765bd9b..5fba246ae 100644 --- a/crates/asap-physical-operators/src/physical_planner/candidates.rs +++ b/crates/asap-physical-operators/src/physical_planner/candidates.rs @@ -365,9 +365,9 @@ mod tests { .remove(0); let root = promql_rows::with_series_identity(&root).unwrap(); let space = asap_logical_optimizer::search_workload(vec![("q", root)]); - let selected = asap_aware_mapping::plan_selection::candidate_selection::global_selection( + let selected = asap_plan_selection::candidate_selection::global_selection( &space, - &asap_aware_mapping::cost_model::DefaultCostModel, + &asap_plan_selection::cost::cost_model::DefaultCostModel, ) .assemble_selected_dag(&space.roots[0].1) .unwrap() diff --git a/crates/asap-physical-operators/tests/deployment_computation.rs b/crates/asap-physical-operators/tests/deployment_computation.rs index c11b6bd4c..a9ff1f95d 100644 --- a/crates/asap-physical-operators/tests/deployment_computation.rs +++ b/crates/asap-physical-operators/tests/deployment_computation.rs @@ -54,9 +54,9 @@ fn exact_dag(query: &str) -> PhysicalASAPDAG { let expression = lower(query); let root = promql_rows::with_series_identity(&expression).unwrap_or(expression); let space = asap_logical_optimizer::search_workload(vec![("q", root)]); - let selected = asap_aware_mapping::plan_selection::candidate_selection::global_selection( + let selected = asap_plan_selection::candidate_selection::global_selection( &space, - &asap_aware_mapping::DefaultCostModel, + &asap_plan_selection::DefaultCostModel, ) .assemble_selected_dag(&space.roots[0].1) .unwrap() diff --git a/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs b/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs index d1d4f72fe..9e843adfa 100644 --- a/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs +++ b/crates/asap-physical-operators/tests/planspace_series_identity_heap.rs @@ -6,8 +6,6 @@ mod common; use common::compile_physical_asap_dag; use planner_types::ir::OperatorNode; -use asap_aware_mapping::cost_model::DefaultCostModel; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; use asap_logical_optimizer::{ accuracy::AccuracyEvidenceProvider, accuracy::DefaultAccuracyModel, accuracy::PropagationStats, pass1::replacement::default_strategies_with_evidence, @@ -17,6 +15,8 @@ use asap_logical_optimizer::{ use asap_physical_operators::physical_planner::promql_rows::{ compile_current_series_evaluation, SERIES_IDENTITY_COLUMN, }; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::cost::cost_model::DefaultCostModel; use planner_types::{ ir::properties::*, ir::schema::*, diff --git a/crates/asap-physical-operators/tests/precompute_candidates.rs b/crates/asap-physical-operators/tests/precompute_candidates.rs index de3290227..d5aa35e57 100644 --- a/crates/asap-physical-operators/tests/precompute_candidates.rs +++ b/crates/asap-physical-operators/tests/precompute_candidates.rs @@ -1,7 +1,5 @@ //! Materialized frontiers are compiled by Planner, never rewritten by deployment. mod common; -use asap_aware_mapping::cost_model::DefaultCostModel; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; use asap_logical_optimizer::search_workload; use asap_physical_operators::{ factory::create_planner_accumulator, @@ -14,6 +12,8 @@ use asap_physical_operators::{ runtime::{Limits, RunContext, Scope}, values::{Batch, Value}, }; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::cost::cost_model::DefaultCostModel; use common::compile_physical_asap_dag; use futures::{executor::block_on, StreamExt}; use planner_types::ir::physical_export::{PhysicalASAPDAG, PhysicalASAPOperatorPayload}; diff --git a/crates/asap-physical-operators/tests/promql_fallback.rs b/crates/asap-physical-operators/tests/promql_fallback.rs index 293e4d3d0..c7dab96a5 100644 --- a/crates/asap-physical-operators/tests/promql_fallback.rs +++ b/crates/asap-physical-operators/tests/promql_fallback.rs @@ -1224,11 +1224,11 @@ fn histogram_quantile_rejects_equal_output_label_sets() { // for an approximate target, and the selected DAG compiles and executes. #[test] fn histogram_quantile_selection_keeps_the_exact_fallback() { - use asap_aware_mapping::cost_model::DefaultCostModel; use asap_logical_optimizer::{ accuracy::DefaultAccuracyModel, default_strategies, search_workload_with_targets, Replacement, }; + use asap_plan_selection::cost::cost_model::DefaultCostModel; let samples = buckets(&[("job=a", HISTOGRAM)]); for target in [AccuracyTarget::Exact, AccuracyTarget::Epsilon(0.01)] { for query in [ @@ -1249,14 +1249,13 @@ fn histogram_quantile_selection_keeps_the_exact_fallback() { Replacement::SubDAG(node) if !node.contains_asap() && node.operator == root.operator)), "{query}: {candidates:?}" ); - let selected = - asap_aware_mapping::plan_selection::candidate_selection::global_selection( - &space, - &DefaultCostModel, - ) - .assemble_selected_dag(planned) - .unwrap() - .unwrap(); + let selected = asap_plan_selection::candidate_selection::global_selection( + &space, + &DefaultCostModel, + ) + .assemble_selected_dag(planned) + .unwrap() + .unwrap(); let dag = compile_physical_asap_dag(&selected).unwrap(); let rows = evaluate_dag(&root, &dag, &[("x_bucket", &samples)], 60).unwrap(); let values: Vec<_> = rows.iter().map(|(_, _, v)| *v).collect(); diff --git a/crates/devtools/Cargo.toml b/crates/devtools/Cargo.toml index d9b88a4ba..484ce7991 100644 --- a/crates/devtools/Cargo.toml +++ b/crates/devtools/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" [dependencies] asap-frontend-promql = { path = "../frontend-promql" } asap-frontend-sql = { path = "../frontend-sql" } -asap-aware-mapping = { path = "../asap-aware-mapping" } +asap-plan-selection = { path = "../plan-selection" } asap-logical-optimizer = { path = "../logical-optimizer" } # Used by the show_ir / dag_export / variant_coverage bins (catalog schemas, diff --git a/crates/devtools/src/bin/dag_export.rs b/crates/devtools/src/bin/dag_export.rs index 65f86c97d..f94a40781 100644 --- a/crates/devtools/src/bin/dag_export.rs +++ b/crates/devtools/src/bin/dag_export.rs @@ -60,7 +60,7 @@ // - `--planner-cost-json ` supplies complete deployment-owned // physical-plan evidence. It both ranks the candidates and is exported: // every decision carries a calibrated `CostUnits` annotation. -// - `--default-cost` ranks with `asap_aware_mapping::cost_model:: +// - `--default-cost` ranks with `asap_plan_selection::cost::cost_model:: // DefaultCostModel` — structural node counts, owning no deployment // evidence. The structure of the result is real (which replacements the // search found, which one won per group, what the merged post-ASAP DAG @@ -76,23 +76,23 @@ use std::collections::HashMap; use std::rc::Rc; use std::time::Instant; -use asap_aware_mapping::analytical_cost::{ - cache_hit_ratios, AnalyticalCostError, EvidenceBackedPhysicalDAG as PhysicalDAG, - PhysicalNodeEvidence, ResourceCalibration, ANALYTICAL_COST_MODEL_VERSION, -}; -use asap_aware_mapping::cost_model::DefaultCostModel; -use asap_aware_mapping::cost_model::{Cost, CostModel}; -use asap_aware_mapping::physical_operator_statistics::ComparisonScope; -use asap_aware_mapping::physical_plan_cost_model::{ - PhysicalEvidenceSnapshot, PhysicalPlanCostModel, PlannerPhysicalPlanProvider, -}; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; -use asap_aware_mapping::query_physical_lowering::PhysicalNodeRequest; use asap_logical_optimizer::pass1::replacement::{ default_strategies_with_evidence, is_logical_rewrite, search_workload, search_workload_with, Replacement, ReplacementSubDAG, }; use asap_logical_optimizer::{AccuracyEvidenceProvider, PropagationStats}; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::cost::analytical_cost::{ + cache_hit_ratios, AnalyticalCostError, EvidenceBackedPhysicalDAG as PhysicalDAG, + PhysicalNodeEvidence, ResourceCalibration, ANALYTICAL_COST_MODEL_VERSION, +}; +use asap_plan_selection::cost::cost_model::DefaultCostModel; +use asap_plan_selection::cost::cost_model::{Cost, CostModel}; +use asap_plan_selection::cost::physical_operator_statistics::ComparisonScope; +use asap_plan_selection::cost::physical_plan_cost_model::{ + PhysicalEvidenceSnapshot, PhysicalPlanCostModel, PlannerPhysicalPlanProvider, +}; +use asap_plan_selection::cost::query_physical_lowering::PhysicalNodeRequest; use asap_types::cost::{BaselineRef, CostAnnotation, CostInput, CostSource, CostUnit}; use asap_types::dag_export::{ self, DAGDecision, DAGNote, ExportDAG, NamedDAG, PostAsapSubstitution, TargetRejection, @@ -110,10 +110,10 @@ use asap_types::workload::resources::CacheProfile; #[serde(deny_unknown_fields)] struct PlannerCostDocument { #[serde(default, skip_serializing_if = "Option::is_none")] - storage_io: Option, + storage_io: Option, #[serde(default, skip_serializing_if = "Option::is_none")] #[serde(rename = "boundaries")] - handoffs: Option, + handoffs: Option, /// Immutable catalog/runtime evidence generation shared by this file. evidence_version: String, calibration: ResourceCalibration, @@ -152,7 +152,7 @@ struct ComparisonScopeEvidence { time_scope: String, lookback_ms: Option, as_of_ms: Option, - sources: Vec, + sources: Vec, #[serde(default = "CacheProfile::no_cache")] cache_profile: CacheProfile, } @@ -193,7 +193,7 @@ impl ComparisonScopeEvidence { #[serde(deny_unknown_fields)] struct QueryNodePhysicalEvidence { logical_node: OperatorNode, - operator: asap_aware_mapping::analytical_cost::PhysicalOperator, + operator: asap_plan_selection::cost::analytical_cost::PhysicalOperator, occurrence: usize, synthetic: bool, evidence: PhysicalNodeEvidence, @@ -302,8 +302,8 @@ fn plan_values_match_inner( } struct ExportPhysicalProvider<'a> { - storage_io: Option<&'a asap_aware_mapping::storage_io::StorageIoProfile>, - handoffs: Option<&'a asap_aware_mapping::physical_handoff_cost::PhysicalHandoffProfile>, + storage_io: Option<&'a asap_plan_selection::cost::storage_io::StorageIoProfile>, + handoffs: Option<&'a asap_plan_selection::cost::physical_handoff_cost::PhysicalHandoffProfile>, evidence_version: &'a str, target: &'a TargetPhysicalEvidence, candidate: &'a CandidatePhysicalEvidence, @@ -516,7 +516,7 @@ impl ExportPlannerCostModel<'_> { cache_inputs.push(input("result_cache_invalidation_ratio", ratio, "ratio")); } } - let inputs = |resources: asap_aware_mapping::analytical_cost::ResourceEstimate| { + let inputs = |resources: asap_plan_selection::cost::analytical_cost::ResourceEstimate| { let mut inputs = vec![ CostInput { name: "estimated_cpu_ops".into(), @@ -537,7 +537,7 @@ impl ExportPlannerCostModel<'_> { inputs.extend(cache_inputs.iter().cloned()); inputs }; - let storage_inputs = |storage: &asap_aware_mapping::storage_io::StorageEstimate| { + let storage_inputs = |storage: &asap_plan_selection::cost::storage_io::StorageEstimate| { let mut terms: Vec<_> = storage .total .terms() @@ -571,7 +571,7 @@ impl ExportPlannerCostModel<'_> { candidate_inputs.extend(storage_inputs(candidate)); } let handoff_inputs = - |estimate: &asap_aware_mapping::physical_handoff_cost::PhysicalHandoffEstimate| { + |estimate: &asap_plan_selection::cost::physical_handoff_cost::PhysicalHandoffEstimate| { let mut terms: Vec<_> = estimate .total .terms() @@ -1699,14 +1699,14 @@ async fn main() { mod tests { use super::*; - use asap_aware_mapping::analytical_cost::{ + use asap_devtools::PromqlError; + use asap_plan_selection::cost::analytical_cost::{ ExecutionMultiplicity, PhysicalDAGNode, PhysicalOperator, }; - use asap_aware_mapping::physical_operator_statistics::{ + use asap_plan_selection::cost::physical_operator_statistics::{ EdgeStatistics, OperatorStatistics, ScanSelection, UnaryEdgeStatistics, }; - use asap_aware_mapping::query_physical_lowering::lower_query_physical_dag; - use asap_devtools::PromqlError; + use asap_plan_selection::cost::query_physical_lowering::lower_query_physical_dag; use asap_types::ir::operator::{Reduction, Source}; use asap_types::ir::schema::{DataType, Field, Schema}; use asap_types::ir::NonASAPOp; @@ -1758,7 +1758,7 @@ mod tests { // JSON evidence reaches calibrated ranking and structured annotation inputs. #[test] fn storage_requests_export_and_change_plan_selection() { - use asap_aware_mapping::storage_io::*; + use asap_plan_selection::cost::storage_io::*; let (query, candidate, mut document) = cost_fixture(); let raw = fixture_raw_dag(&query, &candidate, &document); let candidate_dag = cheap_candidate_dag(); @@ -1921,7 +1921,7 @@ mod tests { // Declared handoffs survive JSON and affect the selected physical plan. #[test] fn handoff_bytes_export_and_change_plan_selection() { - use asap_aware_mapping::physical_handoff_cost::*; + use asap_plan_selection::cost::physical_handoff_cost::*; let (query, candidate, mut document) = cost_fixture(); let raw = fixture_raw_dag(&query, &candidate, &document); let candidate_dag = cheap_candidate_dag(); @@ -2035,7 +2035,7 @@ mod tests { // Independent supplemental objectives add once, and either one can // price a zero-base plan while retaining both sets of export evidence. { - use asap_aware_mapping::storage_io::*; + use asap_plan_selection::cost::storage_io::*; let mut joint = handoff_only.clone(); let mut storage = StorageIoProfile { evidence_version: joint.evidence_version.clone(), diff --git a/crates/devtools/src/bin/stage_pipeline.rs b/crates/devtools/src/bin/stage_pipeline.rs index 18c84c11b..0062f5a1b 100644 --- a/crates/devtools/src/bin/stage_pipeline.rs +++ b/crates/devtools/src/bin/stage_pipeline.rs @@ -25,12 +25,12 @@ use std::rc::Rc; -use asap_aware_mapping::plan_selection::{select_exhaustive, Selection, MAX_ENUMERATED_CANDIDATES}; -use asap_aware_mapping::PlanningModels; use asap_logical_optimizer::pass1::logical_candidates::{ choice_index, enumerate_local_logical_candidates, LocalLogicalCandidates, }; use asap_logical_optimizer::Realization; +use asap_plan_selection::PlanningModels; +use asap_plan_selection::{select_exhaustive, Selection, MAX_ENUMERATED_CANDIDATES}; use asap_types::ir::flat::{flatten, FlatDag}; use asap_types::ir::schema::SketchAlgorithm; use asap_types::ir::schema_support::with_promql_series_identity; diff --git a/crates/frontend-promql/Cargo.toml b/crates/frontend-promql/Cargo.toml index 985bbee59..0ac525de5 100644 --- a/crates/frontend-promql/Cargo.toml +++ b/crates/frontend-promql/Cargo.toml @@ -16,7 +16,7 @@ asap-frontend-common = { path = "../frontend-common" } promql-parser = { git = "https://github.com/ProjectASAP/promql-parser", rev = "9fede7eecca923c9882fe256484d00d37f8706cb" } [dev-dependencies] -asap-aware-mapping = { path = "../asap-aware-mapping" } +asap-plan-selection = { path = "../plan-selection" } asap-logical-optimizer = { path = "../logical-optimizer" } # Corpus tests live in domain folders (observability) but must still be declared diff --git a/crates/frontend-promql/tests/count_planning.rs b/crates/frontend-promql/tests/count_planning.rs index 486d49145..76b527fdd 100644 --- a/crates/frontend-promql/tests/count_planning.rs +++ b/crates/frontend-promql/tests/count_planning.rs @@ -1,11 +1,11 @@ //! Query text through summary selection: counts use observations, never value weights. -use asap_aware_mapping::cost_model::DefaultCostModel; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; use asap_logical_optimizer::accuracy::DefaultAccuracyModel; use asap_logical_optimizer::{ default_strategies, search_workload_with_targets, ASAPStrategies, Replacement, ReplacementStrategy, TargetSubDAG, }; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::cost::cost_model::DefaultCostModel; mod support; use asap_types::ir::physical_export::PhysicalASAPOperatorPayload; use asap_types::ir::schema::{ diff --git a/crates/frontend-promql/tests/univmon_candidates.rs b/crates/frontend-promql/tests/univmon_candidates.rs index 9917ff380..3fbf8afb0 100644 --- a/crates/frontend-promql/tests/univmon_candidates.rs +++ b/crates/frontend-promql/tests/univmon_candidates.rs @@ -1,7 +1,5 @@ use std::rc::Rc; -use asap_aware_mapping::cost_model::DefaultCostModel; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; use asap_logical_optimizer::accuracy::{ AccuracyModel, DefaultAccuracyModel, EqualSplitAllocator, PropagationStats, }; @@ -9,6 +7,8 @@ use asap_logical_optimizer::pass1::replacement::{ default_strategies, search_workload_with_targets, }; use asap_logical_optimizer::{ASAPStrategies, Replacement, ReplacementStrategy, TargetSubDAG}; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::cost::cost_model::DefaultCostModel; mod support; use asap_types::ir::cse::share_common_sub_dags; use asap_types::ir::properties::{ diff --git a/crates/frontend-sql/Cargo.toml b/crates/frontend-sql/Cargo.toml index 03adb84dd..3e15bee42 100644 --- a/crates/frontend-sql/Cargo.toml +++ b/crates/frontend-sql/Cargo.toml @@ -17,7 +17,6 @@ datafusion = "54" serde_json = "1" [dev-dependencies] -asap-aware-mapping = { path = "../asap-aware-mapping" } asap-logical-optimizer = { path = "../logical-optimizer" } tokio = { version = "1", features = ["rt", "macros", "rt-multi-thread"] } # bgp_jan2024_workload corpus is sourced verbatim as YAML (ASAPQuery PR #561) diff --git a/crates/integration-tests/Cargo.toml b/crates/integration-tests/Cargo.toml index 8f7084802..0d8c79229 100644 --- a/crates/integration-tests/Cargo.toml +++ b/crates/integration-tests/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" asap-types = { path = "../types" } asap-frontend-promql = { path = "../frontend-promql" } asap-frontend-sql = { path = "../frontend-sql" } -asap-aware-mapping = { path = "../asap-aware-mapping" } +asap-plan-selection = { path = "../plan-selection" } asap-logical-optimizer = { path = "../logical-optimizer" } asap-physical-optimizer = { path = "../physical-optimizer" } diff --git a/crates/integration-tests/tests/exact_composition.rs b/crates/integration-tests/tests/exact_composition.rs index 9a7893d37..57ddfb14b 100644 --- a/crates/integration-tests/tests/exact_composition.rs +++ b/crates/integration-tests/tests/exact_composition.rs @@ -12,13 +12,6 @@ use std::rc::Rc; -use asap_aware_mapping::cost_model::{ - CostProvenance, CostUnit, ExactCompositionCostInputs, ExactCompositionCostRequest, -}; -use asap_aware_mapping::plan_selection::candidate_selection::{ - global_selection, runtime_support_evidence, -}; -use asap_aware_mapping::{CostModel, DefaultCostModel, EvaluationRate}; use asap_integration_tests::fixtures::lower_promql; use asap_integration_tests::post_asap::{maintained, post_asap_dag, timed}; use asap_logical_optimizer::pass1::exact_composition::ExactOperation; @@ -27,6 +20,11 @@ use asap_logical_optimizer::pass1::replacement::{ ReplacementStrategy, TargetSubDAG, }; use asap_logical_optimizer::{ExplanationKind, OperationPlacement}; +use asap_plan_selection::candidate_selection::{global_selection, runtime_support_evidence}; +use asap_plan_selection::cost::cost_model::{ + CostProvenance, CostUnit, ExactCompositionCostInputs, ExactCompositionCostRequest, +}; +use asap_plan_selection::{CostModel, DefaultCostModel, EvaluationRate}; use asap_types::dag_export; use asap_types::ir::data_state; use asap_types::ir::operator::{default_quantile, AggIntent}; diff --git a/crates/integration-tests/tests/operator_design_examples.rs b/crates/integration-tests/tests/operator_design_examples.rs index 2cb14663c..18e02d81b 100644 --- a/crates/integration-tests/tests/operator_design_examples.rs +++ b/crates/integration-tests/tests/operator_design_examples.rs @@ -262,12 +262,12 @@ async fn sql_window_and_filtered_aggregate_types() { /// constructed by the test. #[tokio::test] async fn batch_planning_selects_and_executes_each_plan() { - use asap_aware_mapping::pass::PlanningModels; use asap_physical_operators::{ physical_planner::{compile, InputContract}, runtime::Scope, values::{Batch, Value}, }; + use asap_plan_selection::PlanningModels; use asap_planner::{e2e_plan, FrontendInput, UserInput}; use asap_types::workload::*; use std::{collections::BTreeMap, sync::Arc}; diff --git a/crates/integration-tests/tests/operator_sharing.rs b/crates/integration-tests/tests/operator_sharing.rs index d43520882..790c28b67 100644 --- a/crates/integration-tests/tests/operator_sharing.rs +++ b/crates/integration-tests/tests/operator_sharing.rs @@ -9,11 +9,11 @@ use std::rc::Rc; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; -use asap_aware_mapping::DefaultCostModel; use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_integration_tests::post_asap::post_asap_dag; use asap_logical_optimizer::search_workload; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::DefaultCostModel; use asap_types::ir::schema::{DataType, Field, Schema}; use asap_types::ir::{ASAPOp, NonASAPOp, Operator, OperatorNode}; use asap_types::types::AccuracyTarget; diff --git a/crates/integration-tests/tests/planner_layering_example1.rs b/crates/integration-tests/tests/planner_layering_example1.rs index a66a8cbf4..bbce6336b 100644 --- a/crates/integration-tests/tests/planner_layering_example1.rs +++ b/crates/integration-tests/tests/planner_layering_example1.rs @@ -14,10 +14,10 @@ use std::collections::{BTreeMap, BTreeSet, HashSet}; -use asap_aware_mapping::PlanningModels; +use asap_plan_selection::PlanningModels; use asap_types::ir::flat::{flatten, FlatDag, NodeId}; +use asap_types::ir::schema::state_type::{GroupingStrategy, HydraKind, SketchAlgorithm}; use asap_types::ir::schema::FieldDataType; -use asap_types::ir::schema::{GroupingStrategy, HydraKind, SketchAlgorithm}; use asap_types::ir::{ASAPOp, Operator}; use asap_types::types::AccuracyTarget; use asap_types::workload::{ @@ -220,7 +220,7 @@ mod stages { .map(|entry| Some(entry.requirements.accuracy.target())) .collect(); let candidates: Vec<_> = physical.iter().map(|p| p.stage2.clone()).collect(); - let selection = asap_aware_mapping::plan_selection::stage3_select( + let selection = asap_plan_selection::stage3_select( &candidates, &targets, workload.data_workload.as_ref().expect("data workload"), diff --git a/crates/integration-tests/tests/precompute_raw_samples.rs b/crates/integration-tests/tests/precompute_raw_samples.rs index 72d17c174..2cc311bfe 100644 --- a/crates/integration-tests/tests/precompute_raw_samples.rs +++ b/crates/integration-tests/tests/precompute_raw_samples.rs @@ -7,8 +7,6 @@ use asap_types::ir::OperatorNode; use physical_common::compile_maintained_physical_asap_dag; use std::{collections::BTreeMap, collections::BTreeSet, rc::Rc, sync::Arc}; -use asap_aware_mapping::cost_model::DefaultCostModel; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; use asap_integration_tests::fixtures::lower_promql; use asap_logical_optimizer::{ search_workload, ASAPStrategies, Replacement, ReplacementStrategy, ReplacementSubDAG, @@ -23,6 +21,8 @@ use asap_physical_operators::{ values::{Batch, Value}, AggregateCore, KeyByLabelValues, Statistic, }; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::cost::cost_model::DefaultCostModel; use asap_types::ir::operator::Reduction; use asap_types::ir::scalar::ColumnRef; use asap_types::ir::schema::{ diff --git a/crates/integration-tests/tests/promql_to_post_asap.rs b/crates/integration-tests/tests/promql_to_post_asap.rs index f77c8cc60..2e15c7491 100644 --- a/crates/integration-tests/tests/promql_to_post_asap.rs +++ b/crates/integration-tests/tests/promql_to_post_asap.rs @@ -8,8 +8,6 @@ use std::rc::Rc; -use asap_aware_mapping::cost_model::DefaultCostModel; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; use asap_integration_tests::fixtures::lower_promql; use asap_integration_tests::post_asap::{ maintained, maintained_post_asap_dag, post_asap_dag, timed, @@ -25,7 +23,9 @@ use asap_logical_optimizer::{ search_workload, search_workload_with_targets, ASAPStrategies, AccuracyModel, Replacement, ReplacementStrategy, ReplacementSubDAG, TargetSubDAG, }; -use asap_types::ir::operator::Reduction; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::cost::cost_model::DefaultCostModel; +use asap_types::ir::operator::operator_properties::Reduction; use asap_types::ir::physical_export::PhysicalASAPOperatorPayload; use asap_types::ir::properties::CompositionOperator; use asap_types::ir::scalar::ColumnRef; diff --git a/crates/integration-tests/tests/sql_to_physical.rs b/crates/integration-tests/tests/sql_to_physical.rs index 8402ef0c3..bfac9650f 100644 --- a/crates/integration-tests/tests/sql_to_physical.rs +++ b/crates/integration-tests/tests/sql_to_physical.rs @@ -1,7 +1,5 @@ //! SQL frontend, candidate selection, physical compilation and fresh-run execution. mod physical_common; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; -use asap_aware_mapping::DefaultCostModel; use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_logical_optimizer::search_workload; use asap_physical_operators::{ @@ -10,9 +8,10 @@ use asap_physical_operators::{ sources::{DataSources, MemorySource}, values::{Batch, Value}, }; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::DefaultCostModel; use asap_types::ir::physical_export::PhysicalASAPOperatorPayload; -use asap_types::ir::schema::FieldDataType; -use asap_types::ir::schema::{DataType, Field, Schema}; +use asap_types::ir::schema::{DataType, Field, FieldDataType, Schema}; use asap_types::types::AccuracyTarget; use futures::StreamExt; use physical_common::compile_physical_asap_dag; diff --git a/crates/integration-tests/tests/sql_to_post_asap.rs b/crates/integration-tests/tests/sql_to_post_asap.rs index 02b89c55f..b9c7e2dfe 100644 --- a/crates/integration-tests/tests/sql_to_post_asap.rs +++ b/crates/integration-tests/tests/sql_to_post_asap.rs @@ -19,8 +19,6 @@ use std::rc::Rc; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; -use asap_aware_mapping::DefaultCostModel; use asap_frontend_sql::{lower_sql, lower_sql_dialect, SqlCatalog}; use asap_integration_tests::post_asap::post_asap_dag; use asap_logical_optimizer::pass1::replacement::{retain_exact, RealizationError}; @@ -28,7 +26,9 @@ use asap_logical_optimizer::{ search_workload, ASAPStrategies, Replacement, ReplacementStrategy, ReplacementSubDAG, TargetSubDAG, }; -use asap_types::ir::operator::Reduction; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::DefaultCostModel; +use asap_types::ir::operator::operator_properties::Reduction; use asap_types::ir::physical_export::{EdgeRole, PhysicalASAPNodeId, PhysicalASAPOperatorPayload}; use asap_types::ir::scalar::ColumnRef; use asap_types::ir::schema::{DataType, Field, Schema}; diff --git a/crates/integration-tests/tests/summary_coverage_e2e.rs b/crates/integration-tests/tests/summary_coverage_e2e.rs index 1098e4ff8..6cf8e108b 100644 --- a/crates/integration-tests/tests/summary_coverage_e2e.rs +++ b/crates/integration-tests/tests/summary_coverage_e2e.rs @@ -6,11 +6,11 @@ use std::ops::Bound; use std::rc::Rc; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; -use asap_aware_mapping::DefaultCostModel; use asap_frontend_sql::{lower_sql, SqlCatalog}; use asap_integration_tests::fixtures::lower_promql; use asap_logical_optimizer::search_workload; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::DefaultCostModel; use asap_types::ir::properties::summary_coverage::{ColumnIdentity, Constraint, SelectionBox}; use asap_types::ir::scalar::ScalarValue; use asap_types::ir::schema::{DataType, Field, FieldDataType, Schema, SketchAlgorithm}; diff --git a/crates/logical-optimizer/tests/stage1_cost_independence.rs b/crates/logical-optimizer/tests/stage1_cost_independence.rs index 8b11dd81b..ca28321dc 100644 --- a/crates/logical-optimizer/tests/stage1_cost_independence.rs +++ b/crates/logical-optimizer/tests/stage1_cost_independence.rs @@ -9,6 +9,7 @@ const FORBIDDEN_MODULES: &[&str] = &["cost_model", "recurrence"]; /// Crates Stage 1 must not depend on: later stages, the facade and the executor. const FORBIDDEN_CRATES: &[&str] = &[ "asap-physical-optimizer", + "asap-plan-selection", "asap-aware-mapping", "asap-physical-operators", ]; diff --git a/crates/plan-selection/Cargo.toml b/crates/plan-selection/Cargo.toml new file mode 100644 index 000000000..c4e01e1d5 --- /dev/null +++ b/crates/plan-selection/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "asap-plan-selection" +version = "0.1.0" +edition = "2021" + +# #509 Stage 3: plan selection, the only stage that uses the cost model. +# Depends on asap-types, Stage 1 and Stage 2; never on the facade or the +# executor. tests/stage3_dependencies.rs checks this manifest. +[dependencies] +asap-types = { path = "../types" } +asap-logical-optimizer = { path = "../logical-optimizer" } +asap-physical-optimizer = { path = "../physical-optimizer" } +thiserror = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dev-dependencies] +asap-frontend-promql = { path = "../frontend-promql" } diff --git a/crates/asap-aware-mapping/src/plan_selection/candidate_selection.rs b/crates/plan-selection/src/candidate_selection.rs similarity index 99% rename from crates/asap-aware-mapping/src/plan_selection/candidate_selection.rs rename to crates/plan-selection/src/candidate_selection.rs index 72d3a06b4..5a86a6903 100644 --- a/crates/asap-aware-mapping/src/plan_selection/candidate_selection.rs +++ b/crates/plan-selection/src/candidate_selection.rs @@ -13,11 +13,11 @@ use std::rc::Rc; use asap_types::ir::schema::{FieldDataType, GroupingStrategy, SketchAlgorithm}; use asap_types::ir::{ASAPOp, NonASAPOp, Operator, OperatorNode}; -use crate::cost_model::{ +use crate::cost::cost_model::{ raw_recompute_cost_rate, CostModel, CseCandidate, ExactCompositionCostInputs, ExactCompositionCostRequest, ShareDecision, }; -use crate::recurrence::{ +use crate::cost::recurrence::{ CostRate, Horizon, RecurrenceError, RecurrenceProfile, RootRecurrence, UpdateRate, }; use asap_logical_optimizer::pass1::exact_composition::OperationPlacement; @@ -261,22 +261,22 @@ pub fn recurrence_profiles( space: &CandidateLogicalASAPDAGs, root_recurrence: &[RootRecurrence], update_rate: Option, -) -> Result { +) -> Result { if root_recurrence.len() != space.roots.len() { - return Err(crate::recurrence::RecurrenceError::RootCountMismatch { - expected: space.roots.len(), - got: root_recurrence.len(), - }); + return Err( + crate::cost::recurrence::RecurrenceError::RootCountMismatch { + expected: space.roots.len(), + got: root_recurrence.len(), + }, + ); } if let Some(rate) = update_rate { - crate::recurrence::validate_update_rate(rate)?; + crate::cost::recurrence::validate_update_rate(rate)?; } for recurrence in root_recurrence { if let RootRecurrence::Repeating(rate) = recurrence { if !rate.0.is_finite() || rate.0 < 0.0 { - return Err(crate::recurrence::RecurrenceError::InvalidEvaluationRate( - *rate, - )); + return Err(crate::cost::recurrence::RecurrenceError::InvalidEvaluationRate(*rate)); } } } @@ -328,7 +328,7 @@ pub fn recurrence_profiles( let mut profiles = HashMap::with_capacity(space.order().len()); for ptr in space.order() { let rate = rates.get(ptr).copied().unwrap_or(0.0); - let evaluation_rate = (rate > 0.0).then_some(crate::recurrence::EvaluationRate(rate)); + let evaluation_rate = (rate > 0.0).then_some(crate::cost::recurrence::EvaluationRate(rate)); let one_shot_consumers = one_shot_counts.get(ptr).copied().unwrap_or(0); // Bug 2 fix (see "Unreachable sites" above): only a reached // site carries the caller-supplied `update_rate`. @@ -480,7 +480,8 @@ fn rank_group<'a>( }) .collect(); if let Some(kinds) = kinds { - let order = crate::cost_model::validated_candidate_ranking(cost_model, intent, &kinds); + let order = + crate::cost::cost_model::validated_candidate_ranking(cost_model, intent, &kinds); ranked.sort_by_key(|c| { let kind = match &c.replacement { Replacement::SubDAG(node) => sketch_kind_of(node), @@ -547,7 +548,7 @@ fn cse_preference(group: &TargetSubDAGCandidates, cost_model: &dyn CostModel) -> /// [`ASAPStrategies::replacements`] returns — so this just reuses /// [`realize_child`], the same rank-and-take-first helper /// `construct_summary_agg`'s own recursion and -/// [`crate::cost_model::DefaultCostModel::estimate_cost`] already use, +/// [`crate::cost::cost_model::DefaultCostModel::estimate_cost`] already use, /// wrapped to swallow the (here, uninteresting) error into `None`. fn realize_one(target: &Rc) -> Option> { realize_child(target).ok() @@ -1355,7 +1356,7 @@ fn topological_order( #[cfg(test)] mod tests { use super::*; - use crate::cost_model::{Cost, DefaultCostModel}; + use crate::cost::cost_model::{Cost, DefaultCostModel}; use crate::test_support::{agg, lower_promql, metric_scan}; use asap_logical_optimizer::accuracy::{ AccuracyModel, DefaultAccuracyModel, EqualSplitAllocator, PropagationStats, diff --git a/crates/asap-aware-mapping/src/analytical_cost.rs b/crates/plan-selection/src/cost/analytical_cost.rs similarity index 99% rename from crates/asap-aware-mapping/src/analytical_cost.rs rename to crates/plan-selection/src/cost/analytical_cost.rs index f1574be24..46c89f4eb 100644 --- a/crates/asap-aware-mapping/src/analytical_cost.rs +++ b/crates/plan-selection/src/cost/analytical_cost.rs @@ -14,7 +14,7 @@ pub use asap_types::workload::resources::{ use asap_types::workload::DataArrival; use serde::{Deserialize, Serialize}; -use crate::physical_operator_statistics::{ +use crate::cost::physical_operator_statistics::{ validate_comparison_scopes, ComparisonScope, EdgeStatistics, OperatorStatistics, OperatorStatisticsProvider, PromqlEdgeStatistics, PromqlValueKind, ScanSelection, }; @@ -968,7 +968,7 @@ fn checked_cpu_product(rows: u64, operations_per_row: u64) -> Result Result { @@ -989,7 +989,7 @@ fn partitioned_order_estimate( fn validate_partitioning( input: EdgeStatistics, - partitioning: &crate::physical_operator_statistics::PartitionStatistics, + partitioning: &crate::cost::physical_operator_statistics::PartitionStatistics, partitioned: bool, ) -> Result<(), AnalyticalCostError> { let inconsistent = |reason| Err(AnalyticalCostError::InconsistentOperatorStatistics(reason)); @@ -1882,8 +1882,9 @@ pub(crate) fn validate_operator_semantics( } fn require_promql_unary( - edges: &crate::physical_operator_statistics::UnaryEdgeStatistics, -) -> Result { + edges: &crate::cost::physical_operator_statistics::UnaryEdgeStatistics, +) -> Result +{ let promql = edges.promql.ok_or(AnalyticalCostError::MissingOrStale( "promql_edge_statistics", ))?; @@ -1893,8 +1894,11 @@ fn require_promql_unary( } fn require_promql_binary( - edges: &crate::physical_operator_statistics::BinaryEdgeStatistics, -) -> Result { + edges: &crate::cost::physical_operator_statistics::BinaryEdgeStatistics, +) -> Result< + crate::cost::physical_operator_statistics::PromqlBinaryEdgeStatistics, + AnalyticalCostError, +> { let promql = edges.promql.ok_or(AnalyticalCostError::MissingOrStale( "promql_edge_statistics", ))?; @@ -1905,7 +1909,7 @@ fn require_promql_binary( } fn validate_promql_cardinality_preserving_shape( - promql: crate::physical_operator_statistics::PromqlUnaryEdgeStatistics, + promql: crate::cost::physical_operator_statistics::PromqlUnaryEdgeStatistics, ) -> Result<(), AnalyticalCostError> { validate_promql_edge(promql.input)?; validate_promql_edge(promql.output)?; @@ -1919,7 +1923,7 @@ fn validate_promql_cardinality_preserving_shape( } fn validate_promql_filter_shape( - promql: crate::physical_operator_statistics::PromqlUnaryEdgeStatistics, + promql: crate::cost::physical_operator_statistics::PromqlUnaryEdgeStatistics, ) -> Result<(), AnalyticalCostError> { validate_promql_edge(promql.input)?; validate_promql_edge(promql.output)?; @@ -1985,7 +1989,7 @@ fn validate_promql_edge_shape( fn validate_promql_bridge( operator: PhysicalOperator, - edges: &crate::physical_operator_statistics::UnaryEdgeStatistics, + edges: &crate::cost::physical_operator_statistics::UnaryEdgeStatistics, ) -> Result<(), AnalyticalCostError> { let promql = require_promql_unary(edges)?; let valid = match operator { @@ -2019,7 +2023,7 @@ fn validate_promql_binary( cardinality: PromqlVectorCardinality, build_side: Option, matching_key_bytes: u64, - edges: &crate::physical_operator_statistics::BinaryEdgeStatistics, + edges: &crate::cost::physical_operator_statistics::BinaryEdgeStatistics, ) -> Result<(), AnalyticalCostError> { let promql = require_promql_binary(edges)?; validate_instant_vector_rows(edges.output, promql.output)?; @@ -2110,7 +2114,7 @@ fn validate_promql_series_sample( grouping_key_count: u64, group_count: u64, key_bytes: u64, - edges: &crate::physical_operator_statistics::UnaryEdgeStatistics, + edges: &crate::cost::physical_operator_statistics::UnaryEdgeStatistics, ) -> Result<(), AnalyticalCostError> { let promql = require_promql_unary(edges)?; validate_instant_vector_rows(edges.output, promql.output)?; @@ -2234,7 +2238,7 @@ fn checked_bytes(parts: &[u64]) -> Result { #[cfg(test)] mod tests { use super::*; - use crate::physical_operator_statistics::{ + use crate::cost::physical_operator_statistics::{ validate_comparison_scopes, BinaryEdgeStatistics, ComparisonScope, EdgeStatistics, OperatorStatistics, PartitionStatistics, PromqlBinaryEdgeStatistics, PromqlEdgeStatistics, PromqlUnaryEdgeStatistics, PromqlValueKind, ScanSelection, UnaryEdgeStatistics, diff --git a/crates/asap-aware-mapping/src/cost_model.rs b/crates/plan-selection/src/cost/cost_model.rs similarity index 99% rename from crates/asap-aware-mapping/src/cost_model.rs rename to crates/plan-selection/src/cost/cost_model.rs index c9a7c88b1..5f42fb2ef 100644 --- a/crates/asap-aware-mapping/src/cost_model.rs +++ b/crates/plan-selection/src/cost/cost_model.rs @@ -31,7 +31,7 @@ //! `docs/design_docs/cse-cost-model-decision.md` for the full design discussion (why //! cost-based, why not a full plan-search engine, the layering constraint //! that forces detection to stay cost-agnostic). -//! [`cost_sorted`](crate::plan_selection::candidate_selection::cost_sorted) +//! [`cost_sorted`](crate::candidate_selection::cost_sorted) //! (via [`asap_logical_optimizer::pass1::replacement`]'s own `cse_preference`) and //! [`DefaultCostModel::estimate_cost`] are this crate's own callers. @@ -44,7 +44,7 @@ use asap_types::ir::schema::{ }; use asap_types::ir::{ASAPOp, Operator, OperatorNode}; -use crate::recurrence::{ +use crate::cost::recurrence::{ self, CostRate, EvaluationRate, Horizon, RecurrenceCostExplanation, RecurrenceError, RecurrenceProfile, }; @@ -251,7 +251,7 @@ fn finite_rate(units_per_second: f64) -> Option { /// A CSE-detected, legality-gated shared sub-DAG with two or more consumers /// — the unit [`CostModel::cse_share_decision`] decides over. Built by -/// [`cost_sorted`](crate::plan_selection::candidate_selection::cost_sorted) +/// [`cost_sorted`](crate::candidate_selection::cost_sorted) /// (via [`asap_logical_optimizer::pass1::replacement`]'s own `cse_preference`) the first time it /// needs a representative bound node for a sub-DAG that /// [`asap_types::ir::cse::share_common_sub_dags`] already collapsed @@ -506,7 +506,7 @@ pub trait CostModel { // ── Recurrence-aware costing (issue #287) ─────────────────────────── // - // See `crate::recurrence`'s module docs for the full cost model + // See `crate::cost::recurrence`'s module docs for the full cost model // (`maintained_cost_rate`/`recompute_cost_rate` formulas, units, // provenance of every new input). The three hooks below are the // per-update-event/per-read/per-recomputation cost primitives that @@ -517,7 +517,7 @@ pub trait CostModel { /// Cost of maintaining `candidate`'s bound summary for a single ingest /// update event. Units: cost units per update — the /// `maintenance_cost_per_update` term of `maintained_cost_rate` - /// (`crate::recurrence`), where it is multiplied by an `UpdateRate` in + /// (`crate::cost::recurrence`), where it is multiplied by an `UpdateRate` in /// **Hz** (`update_rate * maintenance_cost_per_update`). /// /// Default: a small nominal constant, `Cost(0.01)` — deliberately @@ -587,7 +587,7 @@ pub trait CostModel { /// `Share`/`RecomputeIndependently` choice, weighted by how *often* /// `candidate`'s consumers actually run (`recurrence`) instead of only /// how many structurally exist (`candidate.consumer_count`). See - /// `crate::recurrence`'s module docs for the full design. + /// `crate::cost::recurrence`'s module docs for the full design. /// /// - `recurrence.is_empty()` (no [`RepeatingEntry`]/[`DataWorkload`]-derived /// metadata available): delegates to @@ -619,7 +619,7 @@ pub trait CostModel { /// [`ReplacementSubDAG`] candidate at `target` — a real `f64`, not just a /// relative rank, meant for a caller that wants to *display* "candidate A /// costs ≈ X, candidate B costs ≈ Y" (e.g. a DAG-visualization view built - /// on [`cost_sorted`](crate::plan_selection::candidate_selection::cost_sorted)), + /// on [`cost_sorted`](crate::candidate_selection::cost_sorted)), /// not just order candidates against each other — that ordering job /// already belongs to [`rank_candidates`](Self::rank_candidates) (for a /// [`ASAPStrategies`](asap_logical_optimizer::pass1::replacement::ASAPStrategies) @@ -1006,7 +1006,7 @@ mod tests { // 2 * 100 assert_eq!(raw_recompute_cost_rate(&inputs).unwrap().0, 200.0); assert_eq!( - crate::recurrence::total_cost(CostRate(5.0), Horizon(10.0), Cost(3.0)), + crate::cost::recurrence::total_cost(CostRate(5.0), Horizon(10.0), Cost(3.0)), Cost(53.0) ); } diff --git a/crates/asap-aware-mapping/src/empirical_cost.rs b/crates/plan-selection/src/cost/empirical_cost.rs similarity index 93% rename from crates/asap-aware-mapping/src/empirical_cost.rs rename to crates/plan-selection/src/cost/empirical_cost.rs index eda0ee977..fc77bb85a 100644 --- a/crates/asap-aware-mapping/src/empirical_cost.rs +++ b/crates/plan-selection/src/cost/empirical_cost.rs @@ -6,7 +6,7 @@ use asap_types::ir::operator::AggIntent; use asap_types::ir::schema::{SketchAlgorithm, SketchParams}; use serde::{Deserialize, Serialize}; -use crate::cost_model::{CostModel, DefaultCostModel}; +use crate::cost::cost_model::{CostModel, DefaultCostModel}; use asap_logical_optimizer::pass1::replacement::{ accuracy_budget, accuracy_target, default_size_params, ReplacementSubDAG, TargetSubDAG, }; @@ -14,7 +14,7 @@ use asap_logical_optimizer::pass1::replacement::{ pub const EVIDENCE_SCHEMA_VERSION: u32 = 1; pub const EVIDENCE_MODEL_VERSION: &str = "empirical-update-cpu-v1"; -pub use crate::empirical_resources::ResourceMeasurements; +pub use crate::cost::empirical_resources::ResourceMeasurements; pub use asap_types::workload::resources::Measurement; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] @@ -317,23 +317,6 @@ fn nonnegative(value: f64) -> bool { value.is_finite() && value >= 0.0 } -/// 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. -pub(crate) fn snapshot_prepare_cpu(row: &OfflineMeasurement) -> Option { - match &row.metrics.resources.cpu.prepare_cpu_ns { - Some(measurement) => nonnegative(measurement.value).then_some(measurement.value), - None if matches!( - row.algorithm, - SketchAlgorithm::Cms | SketchAlgorithm::CountSketch - ) => - { - Some(0.0) - } - None => None, - } -} - fn validate_context( distribution: &DistributionDescriptor, environment: &EnvironmentDescriptor, @@ -416,7 +399,7 @@ mod tests { #[test] fn checked_in_synthetic_example_is_valid() { let artifact: EvidenceArtifact = serde_json::from_str(include_str!( - "../tests/data/offline-evidence-synthetic.json" + "../../tests/data/offline-evidence-synthetic.json" )) .unwrap(); artifact.validate().unwrap(); @@ -425,7 +408,7 @@ mod tests { .implementation .contains("SYNTHETIC")); let schema: serde_json::Value = serde_json::from_str(include_str!( - "../../../docs/develop_docs/offline-sketch-evidence.schema.json" + "../../../../docs/develop_docs/offline-sketch-evidence.schema.json" )) .unwrap(); assert_eq!( @@ -459,25 +442,6 @@ mod tests { } } - /// Only the established frequency-sketch contract can omit preparation. - #[test] - fn unmeasured_preparation_for_other_families_stays_unknown() { - let (mut artifact, _, _) = fixture(); - let row = &mut artifact.records[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_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_prepare_cpu(row), Some(17.0)); - } - fn fixture() -> (EvidenceArtifact, EvidenceContext, AggIntent) { let distribution = DistributionDescriptor { id: "unit-test-uniform".into(), diff --git a/crates/asap-aware-mapping/src/empirical_resources.rs b/crates/plan-selection/src/cost/empirical_resources.rs similarity index 100% rename from crates/asap-aware-mapping/src/empirical_resources.rs rename to crates/plan-selection/src/cost/empirical_resources.rs diff --git a/crates/plan-selection/src/cost/mod.rs b/crates/plan-selection/src/cost/mod.rs new file mode 100644 index 000000000..f057ad296 --- /dev/null +++ b/crates/plan-selection/src/cost/mod.rs @@ -0,0 +1,18 @@ +//! Stage 3 pricing: the [`CostModel`](cost_model::CostModel) trait every +//! deployment's cost-based selection plugs into, with the built-in +//! [`DefaultCostModel`](cost_model::DefaultCostModel); recurring and one-shot +//! cost rates ([`recurrence`]); analytical and evidence-based pricing +//! ([`analytical_cost`], [`physical_plan_cost_model`], [`empirical_cost`]); and +//! the physical lowering and storage I/O profiles they price +//! ([`query_physical_lowering`], [`storage_io`]). + +pub mod analytical_cost; +pub mod cost_model; +pub mod empirical_cost; +pub mod empirical_resources; +pub mod physical_handoff_cost; +pub mod physical_operator_statistics; +pub mod physical_plan_cost_model; +pub mod query_physical_lowering; +pub mod recurrence; +pub mod storage_io; diff --git a/crates/asap-aware-mapping/src/physical_handoff_cost.rs b/crates/plan-selection/src/cost/physical_handoff_cost.rs similarity index 98% rename from crates/asap-aware-mapping/src/physical_handoff_cost.rs rename to crates/plan-selection/src/cost/physical_handoff_cost.rs index f99e03f26..a5a442aa5 100644 --- a/crates/asap-aware-mapping/src/physical_handoff_cost.rs +++ b/crates/plan-selection/src/cost/physical_handoff_cost.rs @@ -1,10 +1,10 @@ //! Byte estimates at deployment-declared physical handoffs. -use crate::analytical_cost::{ +use crate::cost::analytical_cost::{ estimate_physical_dag, AnalyticalCostError, EvidenceBackedPhysicalDAG, ExecutionMultiplicity, PhysicalDAGNode, }; -use crate::physical_operator_statistics::{ComparisonScope, OperatorStatistics}; +use crate::cost::physical_operator_statistics::{ComparisonScope, OperatorStatistics}; pub use asap_types::workload::resources::{PhysicalHandoffBytes, PhysicalHandoffKind}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; diff --git a/crates/asap-aware-mapping/src/physical_operator_statistics.rs b/crates/plan-selection/src/cost/physical_operator_statistics.rs similarity index 99% rename from crates/asap-aware-mapping/src/physical_operator_statistics.rs rename to crates/plan-selection/src/cost/physical_operator_statistics.rs index 14317081e..c63d77bf8 100644 --- a/crates/asap-aware-mapping/src/physical_operator_statistics.rs +++ b/crates/plan-selection/src/cost/physical_operator_statistics.rs @@ -15,7 +15,7 @@ use asap_types::workload::{ }; use serde::{Deserialize, Serialize}; -use crate::analytical_cost::AnalyticalCostError; +use crate::cost::analytical_cost::AnalyticalCostError; /// The semantic and workload boundary within which two resource estimates /// may be compared. Canonical workload and query-IR types remain authoritative; @@ -276,7 +276,7 @@ pub struct PartitionStatistics { } /// Workload-dependent evidence for one operator in an already-lowered -/// physical DAG. [`PhysicalOperator`](crate::analytical_cost::PhysicalOperator) +/// physical DAG. [`PhysicalOperator`](crate::cost::analytical_cost::PhysicalOperator) /// is the authoritative operator vocabulary: every one of its variants has a /// matching statistics variant here. /// diff --git a/crates/asap-aware-mapping/src/physical_plan_cost_model.rs b/crates/plan-selection/src/cost/physical_plan_cost_model.rs similarity index 96% rename from crates/asap-aware-mapping/src/physical_plan_cost_model.rs rename to crates/plan-selection/src/cost/physical_plan_cost_model.rs index a3e337495..5e1835f33 100644 --- a/crates/asap-aware-mapping/src/physical_plan_cost_model.rs +++ b/crates/plan-selection/src/cost/physical_plan_cost_model.rs @@ -7,14 +7,14 @@ use asap_types::ir::schema::SketchAlgorithm; use asap_types::ir::OperatorNode; use asap_types::workload::resources::CacheProfile; -use crate::analytical_cost::{ +use crate::cost::analytical_cost::{ estimate_physical_dag_comparison, AnalyticalCostError, EvidenceBackedPhysicalDAG as PhysicalDAG, PhysicalDAGComparisonEstimate, PhysicalDAGEstimateRequest, PhysicalNodeEvidence, ResourceCalibration, }; -use crate::cost_model::{Cost, CostModel, DefaultCostModel}; -use crate::physical_operator_statistics::ComparisonScope; -use crate::query_physical_lowering::{ +use crate::cost::cost_model::{Cost, CostModel, DefaultCostModel}; +use crate::cost::physical_operator_statistics::ComparisonScope; +use crate::cost::query_physical_lowering::{ lower_query_physical_dag, PhysicalNodeEvidenceProvider, PhysicalNodeRequest, }; use asap_logical_optimizer::pass1::replacement::{Replacement, ReplacementSubDAG, TargetSubDAG}; @@ -30,8 +30,8 @@ pub struct PhysicalEvidenceSnapshot { pub version: String, pub scope: ComparisonScope, pub cache_profile: CacheProfile, - pub storage_io: Option, - pub handoffs: Option, + pub storage_io: Option, + pub handoffs: Option, } /// Deployment evidence needed to price one planner alternative. @@ -70,12 +70,12 @@ pub struct PhysicalPlanComparison { pub raw_cost: Cost, pub candidate_cost: Cost, pub storage_io: Option<( - crate::storage_io::StorageEstimate, - crate::storage_io::StorageEstimate, + crate::cost::storage_io::StorageEstimate, + crate::cost::storage_io::StorageEstimate, )>, pub handoffs: Option<( - crate::physical_handoff_cost::PhysicalHandoffEstimate, - crate::physical_handoff_cost::PhysicalHandoffEstimate, + crate::cost::physical_handoff_cost::PhysicalHandoffEstimate, + crate::cost::physical_handoff_cost::PhysicalHandoffEstimate, )>, } @@ -219,13 +219,13 @@ impl<'a> PhysicalPlanCostModel<'a> { .as_ref() .map(|profile| { Ok(( - crate::storage_io::estimate_storage_io( + crate::cost::storage_io::estimate_storage_io( &raw, scope, profile, &snapshot.version, )?, - crate::storage_io::estimate_storage_io( + crate::cost::storage_io::estimate_storage_io( &replacement, scope, profile, @@ -239,13 +239,13 @@ impl<'a> PhysicalPlanCostModel<'a> { .as_ref() .map(|profile| { Ok(( - crate::physical_handoff_cost::estimate_physical_handoffs( + crate::cost::physical_handoff_cost::estimate_physical_handoffs( &raw, scope, profile, &snapshot.version, )?, - crate::physical_handoff_cost::estimate_physical_handoffs( + crate::cost::physical_handoff_cost::estimate_physical_handoffs( &replacement, scope, profile, @@ -356,7 +356,7 @@ impl CostModel for PhysicalPlanCostModel<'_> { #[cfg(test)] mod tests { use super::*; - use crate::plan_selection::candidate_selection::global_selection; + use crate::candidate_selection::global_selection; use std::cell::Cell; use std::collections::HashMap; @@ -368,8 +368,8 @@ mod tests { DataArrival, DurationMs, QueryRecurrence, QueryTimeScope, TimeSelection, TimestampMs, }; - use crate::analytical_cost::{ExecutionMultiplicity, PhysicalDAGNode, PhysicalOperator}; - use crate::physical_operator_statistics::{ + use crate::cost::analytical_cost::{ExecutionMultiplicity, PhysicalDAGNode, PhysicalOperator}; + use crate::cost::physical_operator_statistics::{ EdgeStatistics, OperatorStatistics, ScanSelection, UnaryEdgeStatistics, }; use asap_logical_optimizer::pass1::replacement::ReplacementStrategy; @@ -456,10 +456,10 @@ mod tests { } struct TestProvider { - storage_io: Option, + storage_io: Option, summary_available: bool, candidate_scan_bytes: u64, - handoffs: Option, + handoffs: Option, snapshot_calls: Cell, raw_evidence_calls: Cell, } @@ -636,7 +636,7 @@ mod tests { // zero, or invalid supplemental calibration. #[test] fn storage_only_objective_requires_positive_valid_storage_calibration() { - use crate::storage_io::*; + use crate::cost::storage_io::*; let root = query(); let target = TargetSubDAG::new(&root); let mut provider = TestProvider::new(true, 800); @@ -756,7 +756,7 @@ mod tests { // Explicit byte pricing can rank complete plans without pricing CPU or scans. #[test] fn handoff_only_objective_ranks_complete_plans() { - use crate::physical_handoff_cost::*; + use crate::cost::physical_handoff_cost::*; let root = query(); let target = TargetSubDAG::new(&root); let mut provider = TestProvider::new(true, 800); @@ -875,7 +875,7 @@ mod tests { // Ranking must retain the uncovered byte of a nearly resident buffer cache. #[test] fn tiny_buffer_misses_still_affect_global_selection() { - use crate::analytical_cost::{CacheCapacityEvidence, CacheEvidence}; + use crate::cost::analytical_cost::{CacheCapacityEvidence, CacheEvidence}; const WORKING_SET: u64 = 1_u64 << 63; struct AlmostResident(TestProvider); impl PlannerPhysicalPlanProvider for AlmostResident { diff --git a/crates/asap-aware-mapping/src/query_physical_lowering.rs b/crates/plan-selection/src/cost/query_physical_lowering.rs similarity index 99% rename from crates/asap-aware-mapping/src/query_physical_lowering.rs rename to crates/plan-selection/src/cost/query_physical_lowering.rs index 848bf0fd4..096935978 100644 --- a/crates/asap-aware-mapping/src/query_physical_lowering.rs +++ b/crates/plan-selection/src/cost/query_physical_lowering.rs @@ -4,13 +4,13 @@ use std::rc::Rc; use asap_types::ir::{NonASAPOp, OperatorNode, ScalarExpr}; -use crate::analytical_cost::{ +use crate::cost::analytical_cost::{ validate_operator_semantics, AnalyticalCostError, EvidenceBackedPhysicalDAG, ExecutionMultiplicity, HashJoinBuildSide, PhysicalDAGNode, PhysicalNodeEvidence, PhysicalOperator, PromqlBinaryOperandMode, PromqlBinaryOperation, PromqlPresenceKind, PromqlSeriesSampleKind, PromqlVectorCardinality, }; -use crate::physical_operator_statistics::{ +use crate::cost::physical_operator_statistics::{ ComparisonScope, EdgeStatistics, OperatorStatistics, ScanSelection, }; @@ -1260,10 +1260,10 @@ fn fixed_state_per_series_intent(intent: &asap_types::ir::operator::AggIntent) - #[cfg(test)] mod tests { use super::*; - use crate::analytical_cost::{ + use crate::cost::analytical_cost::{ estimate_physical_dag, estimate_physical_dag_comparison, PhysicalDAGEstimateRequest, }; - use crate::physical_operator_statistics::{ + use crate::cost::physical_operator_statistics::{ validate_comparison_scopes, BinaryEdgeStatistics, PartitionStatistics, PromqlEdgeStatistics, PromqlUnaryEdgeStatistics, PromqlValueKind, UnaryEdgeStatistics, }; @@ -1718,7 +1718,7 @@ mod tests { }) }; let shared_scope = scope(vec![scan_selection]); - let no_cache = crate::analytical_cost::CacheProfile::no_cache(); + let no_cache = crate::cost::analytical_cost::CacheProfile::no_cache(); let shared_dag = lower_query_physical_dag(&root, &shared_scope, &shared_provider).unwrap(); assert_eq!(shared_dag.nodes.len(), 2); assert_eq!( @@ -2593,7 +2593,7 @@ mod tests { inputs: [left_edge, right_edge], output: output_edge, promql: Some( - crate::physical_operator_statistics::PromqlBinaryEdgeStatistics { + crate::cost::physical_operator_statistics::PromqlBinaryEdgeStatistics { inputs: [left_promql, right_promql], output: output_promql, }, diff --git a/crates/asap-aware-mapping/src/recurrence.rs b/crates/plan-selection/src/cost/recurrence.rs similarity index 98% rename from crates/asap-aware-mapping/src/recurrence.rs rename to crates/plan-selection/src/cost/recurrence.rs index 15e865926..fa88830bf 100644 --- a/crates/asap-aware-mapping/src/recurrence.rs +++ b/crates/plan-selection/src/cost/recurrence.rs @@ -20,10 +20,10 @@ //! |---|---|---| //! | [`UpdateRate`] | Hz (updates/second) | how often the *raw* data underlying a maintained summary changes (ingest rate) | //! | [`EvaluationRate`] | Hz (evaluations/second) | how often a target is *read* — `sum(1 / query_interval_i)` over every repeating consumer | -//! | [`CostRate`] | cost units / second | a steady-state cost rate — never comparable to a bare [`Cost`](crate::cost_model::Cost) without going through [`total_cost`] | +//! | [`CostRate`] | cost units / second | a steady-state cost rate — never comparable to a bare [`Cost`](crate::cost::cost_model::Cost) without going through [`total_cost`] | //! | [`Horizon`] | seconds | the explicit evaluation window a caller supplies to compare a rate-valued cost against a one-shot cost | //! -//! `Cost` (bare, from [`crate::cost_model`]) stays a one-time, unitless +//! `Cost` (bare, from [`crate::cost::cost_model`]) stays a one-time, unitless //! magnitude — exactly what it was before this module existed, preserved //! for [`CostModel::cse_share_decision`] and everything else that already //! uses it. `CostRate` is a *new*, distinct type specifically so a rate and @@ -75,7 +75,7 @@ //! //! - [`EvaluationRate`]: derived from [`asap_types::workload::RepeatingEntry::demand`] //! values of every repeating consumer reaching a target (via -//! [`evaluation_rate_of`], or [`recurrence_profiles`](crate::plan_selection::candidate_selection::recurrence_profiles) +//! [`evaluation_rate_of`], or [`recurrence_profiles`](crate::candidate_selection::recurrence_profiles) //! for a whole workload). A one-shot ([`asap_types::workload::BatchEntry`]) //! consumer contributes to [`RecurrenceProfile::one_shot_consumers`] //! instead, never to this rate. @@ -106,7 +106,7 @@ use std::fmt; use asap_types::workload::{DataWorkload, RepetitionInterval}; -use crate::cost_model::{Cost, CostModel, CseCandidate, ShareDecision}; +use crate::cost::cost_model::{Cost, CostModel, CseCandidate, ShareDecision}; // ── Units ──────────────────────────────────────────────────────────────── @@ -216,7 +216,7 @@ pub enum RecurrenceError { CostRate with a one-shot Cost without distorting the comparison" )] InvalidHorizon(Horizon), - /// [`recurrence_profiles`](crate::plan_selection::candidate_selection::recurrence_profiles) was called + /// [`recurrence_profiles`](crate::candidate_selection::recurrence_profiles) was called /// with a `root_recurrence` slice whose length doesn't match the /// `CandidateLogicalASAPDAGs`'s own root count — a caller error, but recoverable /// (this method's whole signature promises a `Result`, so this is @@ -239,7 +239,7 @@ pub enum RecurrenceError { /// applied at every point an `UpdateRate` enters a [`RecurrenceProfile`] /// ([`RecurrenceProfile::with_update_rate`], /// [`update_rate_from_data_workload`], -/// [`recurrence_profiles`](crate::plan_selection::candidate_selection::recurrence_profiles)'s own parameter) +/// [`recurrence_profiles`](crate::candidate_selection::recurrence_profiles)'s own parameter) /// *and*, as a backstop that can't be bypassed by constructing a /// `RecurrenceProfile` via its public fields directly, inside [`decide`] /// itself before any comparison uses it. @@ -373,7 +373,7 @@ impl RecurrenceProfile { } /// How one workload root recurs — the opaque per-root tag -/// [`recurrence_profiles`](crate::plan_selection::candidate_selection::recurrence_profiles) threads down to +/// [`recurrence_profiles`](crate::candidate_selection::recurrence_profiles) threads down to /// every target reachable from that root. Mirrors /// [`asap_types::workload::QueryWorkload`]'s own `query_batch` (one-shot) /// vs. `repeating_queries` (an interval each) split, but at the @@ -632,10 +632,10 @@ pub(crate) fn decide( #[cfg(test)] mod tests { use super::*; - use crate::cost_model::DefaultCostModel; - use crate::plan_selection::candidate_selection::cost_sorted_with_recurrence; - use crate::plan_selection::candidate_selection::global_selection_with_recurrence; - use crate::plan_selection::candidate_selection::recurrence_profiles; + use crate::candidate_selection::cost_sorted_with_recurrence; + use crate::candidate_selection::global_selection_with_recurrence; + use crate::candidate_selection::recurrence_profiles; + use crate::cost::cost_model::DefaultCostModel; fn interval(ms: u32) -> RepetitionInterval { RepetitionInterval(ms) @@ -781,7 +781,7 @@ mod tests { // ── decide (structural fallback) ───────────────────────────────────── - use crate::cost_model::CseCandidate; + use crate::cost::cost_model::CseCandidate; use asap_types::ir::operator::operator_properties::{Reduction, Source}; use asap_types::ir::properties::ResultGuarantee; use asap_types::ir::scalar::ColumnRef; @@ -1294,7 +1294,7 @@ mod tests { cost_sorted_with_recurrence(&space, &DeterministicUnitCostModel, &infrequent, None) .unwrap(); let first_provenance = - |ranked: &[crate::plan_selection::candidate_selection::RankedTargetSubDAGCandidates<'_>]| { + |ranked: &[crate::candidate_selection::RankedTargetSubDAGCandidates<'_>]| { ranked .iter() .find(|group| Rc::ptr_eq(group.target, &shared.target)) diff --git a/crates/asap-aware-mapping/src/storage_io.rs b/crates/plan-selection/src/cost/storage_io.rs similarity index 98% rename from crates/asap-aware-mapping/src/storage_io.rs rename to crates/plan-selection/src/cost/storage_io.rs index 5b2d37cae..ba2363ffe 100644 --- a/crates/asap-aware-mapping/src/storage_io.rs +++ b/crates/plan-selection/src/cost/storage_io.rs @@ -7,11 +7,11 @@ use serde::{Deserialize, Serialize}; // Keep the original public import path while sharing the sole type definition. pub use asap_types::workload::resources::StorageResources; -use crate::analytical_cost::{ +use crate::cost::analytical_cost::{ estimate_physical_dag, AnalyticalCostError, EvidenceBackedPhysicalDAG, ExecutionMultiplicity, PhysicalDAGNode, PhysicalOperator, }; -use crate::physical_operator_statistics::{ComparisonScope, OperatorStatistics}; +use crate::cost::physical_operator_statistics::{ComparisonScope, OperatorStatistics}; pub const STORAGE_IO_MODEL_VERSION: &str = "storage-requests-v1"; diff --git a/crates/asap-aware-mapping/src/plan_selection/mod.rs b/crates/plan-selection/src/lib.rs similarity index 97% rename from crates/asap-aware-mapping/src/plan_selection/mod.rs rename to crates/plan-selection/src/lib.rs index 6bf5dc87f..cd53d8d76 100644 --- a/crates/asap-aware-mapping/src/plan_selection/mod.rs +++ b/crates/plan-selection/src/lib.rs @@ -1,4 +1,11 @@ -//! #509 Stage 3 (MVP): plan selection, the only stage that computes cost. +//! `asap-plan-selection` — #509 Stage 3 (MVP): plan selection, the only stage +//! that computes cost. Cargo enforces the stage order: this crate depends on +//! `asap-types`, Stage 1 and Stage 2, never on the facade or the executor. +//! +//! - [`cost`] — the [`CostModel`] trait, analytical and evidence-based +//! pricing, recurrence, and the physical lowering and storage I/O they price. +//! - [`candidate_selection`] — the legacy cost-ranked selection over a Stage 1 +//! search (deleted under #580). //! //! Each Stage 2 candidate is checked against every query's accuracy target //! with the accuracy model, and Count-Min is admitted only over weights proven @@ -9,7 +16,7 @@ //! cannot be built or priced is rejected with its reason; it does not fail //! the selection. //! -//! Prices come from [`crate::analytical_cost::estimate_operator`] over edge +//! Prices come from [`crate::cost::analytical_cost::estimate_operator`] over edge //! statistics derived from the [`DataWorkload`] and a fixed default group //! count; summary build and estimation are priced as rows × sketch depth and //! rows read out. These numbers are illustrative, not calibrated. Latency @@ -20,6 +27,23 @@ //! [`select_exhaustive`] builds and prices every combination, for display and //! for checking the program. pub mod candidate_selection; +pub mod cost; +#[cfg(test)] +mod test_support; + +pub use candidate_selection::{ + CompositionDecision, CostedGlobalSelection, RankedTargetSubDAGCandidates, RecurrenceProfileMap, +}; +pub use cost::cost_model::{ + maintenance_operation_plan_cost_rate, raw_recompute_cost_rate, read_operation_plan_cost_rate, + CostModel, CostProvenance, CostUnit, DefaultCostModel, ExactCompositionCostInputs, + ExactCompositionCostRequest, ValueOperationCapabilities, +}; +pub use cost::recurrence::{ + evaluation_rate_of, total_cost, update_rate_from_data_workload, CostRate, EvaluationRate, + Horizon, RecurrenceCostExplanation, RecurrenceError, RecurrenceProfile, RootRecurrence, + UpdateRate, +}; use asap_types::ir::NonASAPOp; use std::collections::{BTreeMap, HashMap}; @@ -39,11 +63,10 @@ use asap_types::types::AccuracyTarget; use asap_types::workload::DataWorkload; use thiserror::Error; -use crate::analytical_cost::{ +use crate::cost::analytical_cost::{ estimate_operator, AnalyticalCostError, PhysicalOperator, ResourceCalibration, ResourceEstimate, }; -use crate::cost_model::{CostModel, DefaultCostModel}; -use crate::physical_operator_statistics::{ +use crate::cost::physical_operator_statistics::{ EdgeStatistics, OperatorStatistics, PartitionStatistics, UnaryEdgeStatistics, }; use asap_logical_optimizer::accuracy::{ diff --git a/crates/asap-aware-mapping/src/test_support.rs b/crates/plan-selection/src/test_support.rs similarity index 100% rename from crates/asap-aware-mapping/src/test_support.rs rename to crates/plan-selection/src/test_support.rs diff --git a/crates/asap-aware-mapping/tests/data/offline-evidence-synthetic.json b/crates/plan-selection/tests/data/offline-evidence-synthetic.json similarity index 100% rename from crates/asap-aware-mapping/tests/data/offline-evidence-synthetic.json rename to crates/plan-selection/tests/data/offline-evidence-synthetic.json diff --git a/crates/asap-aware-mapping/tests/physical_handoff_cost.rs b/crates/plan-selection/tests/physical_handoff_cost.rs similarity index 98% rename from crates/asap-aware-mapping/tests/physical_handoff_cost.rs rename to crates/plan-selection/tests/physical_handoff_cost.rs index de7a6f44c..79fa1bcf3 100644 --- a/crates/asap-aware-mapping/tests/physical_handoff_cost.rs +++ b/crates/plan-selection/tests/physical_handoff_cost.rs @@ -1,8 +1,8 @@ -use asap_aware_mapping::analytical_cost::{ +use asap_plan_selection::cost::analytical_cost::{ EvidenceBackedPhysicalDAG, ExecutionMultiplicity, PhysicalDAGNode, PhysicalNodeEvidence, PhysicalOperator, }; -use asap_aware_mapping::physical_operator_statistics::{ +use asap_plan_selection::cost::physical_operator_statistics::{ ComparisonScope, EdgeStatistics, OperatorStatistics, ScanSelection, UnaryEdgeStatistics, }; use asap_types::ir::operator::operator_properties::Source; @@ -136,7 +136,7 @@ fn fixture() -> (EvidenceBackedPhysicalDAG, ComparisonScope) { ) } -use asap_aware_mapping::physical_handoff_cost::*; +use asap_plan_selection::cost::physical_handoff_cost::*; // Legacy mapping imports and the shared resource API are the very same Rust types. #[test] @@ -387,7 +387,7 @@ fn missing_stale_and_non_finite_evidence_is_rejected() { // Individual actions may fit while accumulation across actions or nodes overflows. #[test] fn handoff_accumulation_and_calibrated_cost_overflow_are_rejected() { - use asap_aware_mapping::analytical_cost::AnalyticalCostError; + use asap_plan_selection::cost::analytical_cost::AnalyticalCostError; let (mut dag, mut scope) = fixture(); scope.recurrence = QueryRecurrence::OneTime { invocations: 1, diff --git a/crates/plan-selection/tests/stage3_dependencies.rs b/crates/plan-selection/tests/stage3_dependencies.rs new file mode 100644 index 000000000..f221ec892 --- /dev/null +++ b/crates/plan-selection/tests/stage3_dependencies.rs @@ -0,0 +1,29 @@ +//! Stage 3 (plan selection) depends on neither the facade nor the executor: Cargo enforces +//! the one-way #509 stage flow (#572). + +use std::path::Path; + +/// Crates Stage 3 must not depend on: the facade and the executor. +const FORBIDDEN_CRATES: &[&str] = &[ + "asap-aware-mapping", + "asap-planner", + "asap-physical-operators", +]; + +/// The manifest names neither the facade nor the executor crate, so Cargo +/// rejects any import of them. +#[test] +fn stage3_manifest_has_no_path_to_the_facade_or_executor() { + let manifest = + std::fs::read_to_string(Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml")).unwrap(); + let offenders: Vec<&str> = manifest + .lines() + .filter(|line| !line.trim_start().starts_with('#')) + .filter(|line| FORBIDDEN_CRATES.iter().any(|name| line.contains(name))) + .collect(); + assert!( + offenders.is_empty(), + "asap-plan-selection must not depend on the facade or the executor:\n{}", + offenders.join("\n") + ); +} diff --git a/crates/asap-aware-mapping/tests/storage_io.rs b/crates/plan-selection/tests/storage_io.rs similarity index 96% rename from crates/asap-aware-mapping/tests/storage_io.rs rename to crates/plan-selection/tests/storage_io.rs index 837391826..e2bddff52 100644 --- a/crates/asap-aware-mapping/tests/storage_io.rs +++ b/crates/plan-selection/tests/storage_io.rs @@ -1,8 +1,8 @@ -use asap_aware_mapping::analytical_cost::{ +use asap_plan_selection::cost::analytical_cost::{ EvidenceBackedPhysicalDAG, ExecutionMultiplicity, PhysicalDAGNode, PhysicalNodeEvidence, PhysicalOperator, }; -use asap_aware_mapping::physical_operator_statistics::{ +use asap_plan_selection::cost::physical_operator_statistics::{ ComparisonScope, EdgeStatistics, OperatorStatistics, ScanSelection, UnaryEdgeStatistics, }; use asap_types::ir::operator::operator_properties::Source; @@ -136,7 +136,7 @@ fn fixture() -> (EvidenceBackedPhysicalDAG, ComparisonScope) { ) } -use asap_aware_mapping::storage_io::*; +use asap_plan_selection::cost::storage_io::*; // The compatibility import and shared resource namespace expose one Rust type. #[test] @@ -144,7 +144,7 @@ fn storage_estimates_use_the_shared_resource_type_without_wire_changes() { let (dag, scope) = fixture(); let estimate = estimate_storage_io(&dag, &scope, &profile(&dag), "evidence-v1").unwrap(); let shared: asap_types::workload::resources::StorageResources = estimate.total; - let legacy: asap_aware_mapping::storage_io::StorageResources = shared; + let legacy: asap_plan_selection::cost::storage_io::StorageResources = shared; assert_eq!(shared, legacy); let wire = serde_json::to_value(&estimate).unwrap(); assert_eq!( @@ -319,7 +319,7 @@ fn multiplicity_and_cross_node_overflow_are_unavailable() { .push(large.clone()); assert_eq!( estimate_storage_io(&dag, &scope, &profile, "evidence-v1"), - Err(asap_aware_mapping::analytical_cost::AnalyticalCostError::Overflow) + Err(asap_plan_selection::cost::analytical_cost::AnalyticalCostError::Overflow) ); scope.recurrence = QueryRecurrence::OneTime { invocations: 1, @@ -336,7 +336,7 @@ fn multiplicity_and_cross_node_overflow_are_unavailable() { }); assert_eq!( estimate_storage_io(&dag, &scope, &profile, "evidence-v1"), - Err(asap_aware_mapping::analytical_cost::AnalyticalCostError::Overflow) + Err(asap_plan_selection::cost::analytical_cost::AnalyticalCostError::Overflow) ); } diff --git a/crates/planner/Cargo.toml b/crates/planner/Cargo.toml index 0a73eecc3..56ff74f50 100644 --- a/crates/planner/Cargo.toml +++ b/crates/planner/Cargo.toml @@ -12,6 +12,7 @@ edition = "2021" [dependencies] asap-types = { path = "../types" } asap-aware-mapping = { path = "../asap-aware-mapping" } +asap-plan-selection = { path = "../plan-selection" } asap-frontend-sql = { path = "../frontend-sql" } asap-frontend-promql = { path = "../frontend-promql" } asap-frontend-metricsql = { path = "../frontend-metricsql" } diff --git a/crates/planner/src/lib.rs b/crates/planner/src/lib.rs index e67baa553..3e2650a0e 100644 --- a/crates/planner/src/lib.rs +++ b/crates/planner/src/lib.rs @@ -26,8 +26,9 @@ use asap_frontend_sql::{lower_sql_dialect, SqlCatalog, SqlError}; // `e2e_plan` or straight to `optimize`. pub use asap_aware_mapping::pass::{ optimize, OptimizationInput, OptimizationPass, OptimizeError, PassRegistry, PlanOutput, - PlanningModels, QueryPlan, StagePipeline, + QueryPlan, StagePipeline, }; +pub use asap_plan_selection::PlanningModels; // ── Input ──────────────────────────────────────────────────────────────── diff --git a/crates/planner/tests/e2e_plan.rs b/crates/planner/tests/e2e_plan.rs index 57c1eb4b4..59d92ac53 100644 --- a/crates/planner/tests/e2e_plan.rs +++ b/crates/planner/tests/e2e_plan.rs @@ -3,12 +3,10 @@ use std::rc::Rc; -use asap_aware_mapping::pass::{ - OptimizationInput, OptimizationPass, OptimizeError, PlanOutput, PlanningModels, -}; -use asap_aware_mapping::plan_selection::{select_exhaustive, MAX_ENUMERATED_CANDIDATES}; +use asap_aware_mapping::pass::{OptimizationInput, OptimizationPass, OptimizeError, PlanOutput}; use asap_frontend_sql::{lower_sql_dialect, SqlCatalog}; use asap_logical_optimizer::pass1::logical_candidates::enumerate_local_logical_candidates; +use asap_plan_selection::{select_exhaustive, PlanningModels, MAX_ENUMERATED_CANDIDATES}; use asap_planner::{e2e_plan, FrontendInput, PlanError, UserInput, UserInputError}; use asap_types::ir::schema::{DataType, Field, Schema}; use asap_types::ir::QueryRoot; diff --git a/crates/planner/tests/stage_pipeline_selection.rs b/crates/planner/tests/stage_pipeline_selection.rs index fa76496b7..6f49e13b5 100644 --- a/crates/planner/tests/stage_pipeline_selection.rs +++ b/crates/planner/tests/stage_pipeline_selection.rs @@ -3,14 +3,14 @@ //! the combination it picks is the one that building and pricing every //! combination picks. -use asap_aware_mapping::pass::PlanningModels; -use asap_aware_mapping::plan_selection::{ - select_exhaustive, select_plan, SelectionMethod, MAX_ENUMERATED_CANDIDATES, -}; use asap_frontend_sql::{lower_sql_dialect, SqlCatalog}; use asap_logical_optimizer::pass1::logical_candidates::{ enumerate_local_logical_candidates, LocalLogicalCandidates, }; +use asap_plan_selection::PlanningModels; +use asap_plan_selection::{ + select_exhaustive, select_plan, SelectionMethod, MAX_ENUMERATED_CANDIDATES, +}; use asap_planner::{e2e_plan, FrontendInput, UserInput}; use asap_types::ir::cse::share_common_sub_dags; use asap_types::ir::schema::{DataType, Field, Schema}; diff --git a/crates/planner/tests/summary_sharing.rs b/crates/planner/tests/summary_sharing.rs index b53c9178a..654d6c4bb 100644 --- a/crates/planner/tests/summary_sharing.rs +++ b/crates/planner/tests/summary_sharing.rs @@ -5,9 +5,7 @@ use asap_types::ir::cse::share_common_sub_dags; use asap_types::ir::{ASAPOp, OperatorNode}; use std::rc::Rc; -use asap_aware_mapping::pass::{PlanOutput, PlanningModels, QueryPlan}; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; -use asap_aware_mapping::{CostModel, DefaultCostModel}; +use asap_aware_mapping::pass::{PlanOutput, QueryPlan}; use asap_frontend_promql::lower_promql_workload; use asap_frontend_sql::SqlCatalog; use asap_logical_optimizer::accuracy::{ @@ -18,6 +16,9 @@ use asap_logical_optimizer::{ search_workload_with_targets, ASAPStrategies, Replacement, ReplacementStrategy, ReplacementSubDAG, TargetSubDAG, }; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::PlanningModels; +use asap_plan_selection::{CostModel, DefaultCostModel}; use asap_planner::{e2e_plan, FrontendInput, UserInput}; use asap_types::ir::operator::agg_intent::default_quantile; use asap_types::ir::operator::AggIntent; diff --git a/docs/design_docs/decisions/cse-cost-model.md b/docs/design_docs/decisions/cse-cost-model.md index d4d0496ae..7ad7afbd7 100644 --- a/docs/design_docs/decisions/cse-cost-model.md +++ b/docs/design_docs/decisions/cse-cost-model.md @@ -28,7 +28,7 @@ decides the framework for stage 4, "wire workload-level CSE credit into ## Decision: cost-based (Volcano/Cascades), implemented for real This lands as an actual cost comparison, not a documented-but-unimplemented -shape. [`CostModel::cse_share_decision`](../../../crates/asap-aware-mapping/src/cost_model.rs) +shape. [`CostModel::cse_share_decision`](../../../crates/plan-selection/src/cost/cost_model.rs) compares two real, overridable cost estimates for every CSE candidate with two or more consumers: @@ -78,7 +78,7 @@ gate) and the cost-aware decision is applied downstream, in ## Where it hooks in -[`candidate_selection::cost_sorted`](../../../crates/asap-aware-mapping/src/plan_selection/candidate_selection.rs) +[`candidate_selection::cost_sorted`](../../../crates/plan-selection/src/candidate_selection.rs) is where this hooks in today. `search_workload_with` computes each shared sub-DAG's true `consumer_count` across the whole workload up front (the same role `implement_workload_with`'s pre-pass used to play, before that function diff --git a/docs/design_docs/proposals/asap-aware-mapping/analytical-resource-cost.md b/docs/design_docs/proposals/asap-aware-mapping/analytical-resource-cost.md index 0b8651545..1524bb811 100644 --- a/docs/design_docs/proposals/asap-aware-mapping/analytical-resource-cost.md +++ b/docs/design_docs/proposals/asap-aware-mapping/analytical-resource-cost.md @@ -1,7 +1,7 @@ # Analytical resource cost model > Status: implemented model with explicit support limits. The -> [analytical estimator](../../../../crates/asap-aware-mapping/src/analytical_cost.rs) +> [analytical estimator](../../../../crates/plan-selection/src/cost/analytical_cost.rs) > and physical-plan adapter implement supported evidenced comparisons. > Unsupported operators, arrival modes and missing evidence remain unavailable; > proposed extensions are not implied by the implemented formulas. diff --git a/docs/design_docs/proposals/error-resource-profile.md b/docs/design_docs/proposals/error-resource-profile.md index d4ccd4a04..f7ef87206 100644 --- a/docs/design_docs/proposals/error-resource-profile.md +++ b/docs/design_docs/proposals/error-resource-profile.md @@ -1,16 +1,16 @@ # Error–Resource Profile (ERP) -> Status: implemented ERP v1 with proposed extensions. The -> [ERP module](../../../crates/asap-aware-mapping/src/erp.rs) consumes discrete -> empirical profiles; benchmark observations are not worst-case accuracy proofs. -> See the status, supported selection modes and remaining work below. +> Status: ERP v1 was implemented in `asap-aware-mapping::erp` and deleted under +> #572 because the planner never called it. Benchmark observations are not +> worst-case accuracy proofs. ## Status ERP v1 is a discrete, shape- and distribution-conditioned profile exchanged between `sketch-bench` and ASAPPlanner. It is an empirical planning input, not a proof -of a worst-case sketch guarantee. The implementation lives in -`asap-aware-mapping::erp`; `approxbench erp` exports the producer artifact. +of a worst-case sketch guarantee. The planner-side implementation +(`asap-aware-mapping::erp`) was deleted under #572; `approxbench erp` exports +the producer artifact. ## Motivation diff --git a/docs/develop_docs/library-api.md b/docs/develop_docs/library-api.md index d1d2f9370..2936bf9c6 100644 --- a/docs/develop_docs/library-api.md +++ b/docs/develop_docs/library-api.md @@ -206,8 +206,8 @@ use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, Query, PlanningWorkload, QueryLanguage, QueryRequirements, QueryWorkload, }; -use asap_aware_mapping::plan_selection::candidate_selection::cost_sorted; -use asap_aware_mapping::DefaultCostModel; +use asap_plan_selection::candidate_selection::cost_sorted; +use asap_plan_selection::DefaultCostModel; use asap_logical_optimizer::{ default_strategies, search_workload_with_targets, DefaultAccuracyModel, }; @@ -256,7 +256,7 @@ fn main() -> Result<(), Box> { } ``` -| API (`asap_logical_optimizer`; `candidate_selection` is `asap_aware_mapping::plan_selection::candidate_selection`) | Inputs | Output and limits | +| API (`asap_logical_optimizer`; `candidate_selection` is `asap_plan_selection::candidate_selection`) | Inputs | Output and limits | | --- | --- | --- | | `search_workload` | `(query_id, Rc)` roots | `CandidateLogicalASAPDAGs` with built-in strategies/model; no explicit per-root target argument | | `search_workload_with` | Roots, strategy slice | `CandidateLogicalASAPDAGs`; callers choose context-free replacement strategies | @@ -355,8 +355,8 @@ use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, Query, PlanningWorkload, QueryLanguage, QueryRequirements, QueryWorkload, }; -use asap_aware_mapping::plan_selection::candidate_selection::cost_sorted; -use asap_aware_mapping::DefaultCostModel; +use asap_plan_selection::candidate_selection::cost_sorted; +use asap_plan_selection::DefaultCostModel; use asap_logical_optimizer::{ search_workload_with_targets, DefaultAccuracyModel, ReplacementStrategy, ASAPStrategies, SharedSubDAGStrategy, @@ -588,8 +588,8 @@ use asap_types::workload::{ AccuracyRequirement, BatchEntry, DataWorkload, DurationMs, Evidence, Query, PlanningWorkload, QueryLanguage, QueryRequirements, QueryWorkload, }; -use asap_aware_mapping::plan_selection::candidate_selection::global_selection; -use asap_aware_mapping::DefaultCostModel; +use asap_plan_selection::candidate_selection::global_selection; +use asap_plan_selection::DefaultCostModel; use asap_logical_optimizer::search_workload; use asap_types::types::AccuracyTarget; @@ -654,6 +654,6 @@ cargo doc -p asap-aware-mapping -p asap-types --no-deps - [Frontend PromQL](../../crates/frontend-promql/src/lib.rs), [SQL](../../crates/frontend-sql/src/lib.rs), [MetricsQL](../../crates/frontend-metricsql/src/lib.rs) - [Search, ranking and selection](../../crates/logical-optimizer/src/pass1/replacement.rs) -- [Cost models](../../crates/asap-aware-mapping/src/cost_model.rs) +- [Cost models](../../crates/plan-selection/src/cost/cost_model.rs) - [Workload types](../../crates/types/src/workload/mod.rs) - [Planner-runtime contract](../design_docs/architecture/planner-runtime-contract.md) diff --git a/docs/develop_docs/offline-sketch-evidence.md b/docs/develop_docs/offline-sketch-evidence.md index 558af4f88..fd0e5c416 100644 --- a/docs/develop_docs/offline-sketch-evidence.md +++ b/docs/develop_docs/offline-sketch-evidence.md @@ -1,14 +1,14 @@ # Consuming offline sketch measurements This document is for developers integrating sketch-bench with the planner. The -Rust schema is `asap_aware_mapping::empirical_cost::EvidenceArtifact`; its JSON +Rust schema is `asap_plan_selection::cost::empirical_cost::EvidenceArtifact`; its JSON schema version is `1`. Required artifact-level `benchmark_version` and `model_version` identify the producer and cost interpretation independently of the serialization schema. Producers export offline benchmark measurements using this contract; benchmark tooling is delivered separately from the core provider. The checked-in [JSON Schema](offline-sketch-evidence.schema.json) describes the -wire format. `crates/asap-aware-mapping/tests/data/offline-evidence-synthetic.json` +wire format. `crates/plan-selection/tests/data/offline-evidence-synthetic.json` is an explicitly fabricated format fixture, never benchmark evidence. Runtime validation additionally checks cross-field constraints and matching context. @@ -102,9 +102,11 @@ parameters solely because one dataset had low observed error. ## Query-matched offline recommendations -`empirical_comparison::recommend_offline` consumes the companion -[`OfflineComparisonEvidence` JSON format](offline-comparison-evidence.schema.json). -This combines the sketch artifact with explicit query bindings and a separately +The companion +[`OfflineComparisonEvidence` JSON format](offline-comparison-evidence.schema.json) +has no planner consumer: its former consumer, +`empirical_comparison::recommend_offline`, had no callers and was deleted under +#572. The contract below describes the format only. It combines the sketch artifact with explicit query bindings and a separately identified exact implementation measured on the same machine, OS, runtime and input distribution. Its `disjoint_live_state_v1` timing contract requires construction, ingestion, exact preparation and read CPU to be timed separately diff --git a/docs/develop_docs/storage-operation-costs.md b/docs/develop_docs/storage-operation-costs.md index af7ad604c..87cedd08b 100644 --- a/docs/develop_docs/storage-operation-costs.md +++ b/docs/develop_docs/storage-operation-costs.md @@ -45,7 +45,7 @@ can be bound to other physical nodes. Their sole data type is `asap_types::workload::resources::StorageResources`, defined in the shared resources module alongside CPU and byte dimensions. The mapping -crate re-exports it at `asap_aware_mapping::storage_io::StorageResources` for +crate re-exports it at `asap_plan_selection::cost::storage_io::StorageResources` for source compatibility; the four integer JSON fields are unchanged. Pure term enumeration and checked addition live with the shared type. Access profiles, request-count estimation, calibration, and ranking remain in the mapping