From 190d4d75cee08599c3c9fde108df4b358554605c Mon Sep 17 00:00:00 2001 From: zz_y Date: Sun, 19 Jul 2026 13:56:35 -0600 Subject: [PATCH 1/2] feat(plan): add a CostModel interface for deployment-specific sketch ranking asap-plan intentionally has no cost model implementation of its own -- ranking candidate sketches by real cost (bandwidth budget, memory footprint, site count, observed drift, ...) needs deployment knowledge this crate deliberately doesn't have (it depends only on asap-ir, never on a runtime or deployment model). Until now that meant no extension point at all: boundary::bind_sketch took sketch_candidates(intent) unconditionally by static-list order (.first()), and cost_model.rs was a 7-line stub with no callers. Add the CostModel trait (rank_candidates) plus DefaultCostModel, which preserves the existing static order exactly. boundary::realize_with and bind::bind_with/bind_in_with thread a &dyn CostModel through to bind_sketch's candidate selection; the existing realize/bind/bind_in entry points are unchanged (thin wrappers over the _with variants using DefaultCostModel), so this is purely additive -- no behavioral change for any existing caller. This gives each downstream deployment (ASAPCollector + ASAPQuery-backend, ASAPFusion, ...) one place to plug in its own cost-based sketch selection instead of forking bind_sketch. Co-Authored-By: Claude Sonnet 5 --- crates/plan/src/bind.rs | 82 ++++++++++++++++++++++++++--- crates/plan/src/boundary.rs | 35 ++++++++++--- crates/plan/src/cost_model.rs | 99 +++++++++++++++++++++++++++++++++-- crates/plan/src/lib.rs | 11 ++-- 4 files changed, 204 insertions(+), 23 deletions(-) diff --git a/crates/plan/src/bind.rs b/crates/plan/src/bind.rs index 0dbb1aa6b..23b1995cb 100644 --- a/crates/plan/src/bind.rs +++ b/crates/plan/src/bind.rs @@ -40,7 +40,8 @@ use asap_sketch::{ }; use thiserror::Error; -use crate::boundary::{realize, Realization}; +use crate::boundary::{realize_with, Realization}; +use crate::cost_model::{CostModel, DefaultCostModel}; /// Errors from the L3→L4 binding pass. #[derive(Debug, Error)] @@ -50,7 +51,10 @@ pub enum BindError { Schema(#[from] QueryExprError), } -/// Bind a single query (empty `LetBinding` scope) to the L4 IR. +/// Bind a single query (empty `LetBinding` scope) to the L4 IR. Ranks +/// candidate sketches via [`DefaultCostModel`] (`asap-plan`'s built-in +/// static preference order, unchanged); use [`bind_with`] to plug in a +/// deployment-specific [`CostModel`] instead. pub fn bind(expr: &QueryExpr) -> Result, BindError> { bind_in(expr, &BindingScope::default()) } @@ -58,6 +62,21 @@ pub fn bind(expr: &QueryExpr) -> Result, BindError> { /// Bind with an explicit `LetBinding` scope — for roots that reference /// CSE-hoisted producers via [`QueryExpr::Ref`]. pub fn bind_in(expr: &QueryExpr, scope: &BindingScope) -> Result, BindError> { + bind_in_with(expr, scope, &DefaultCostModel) +} + +/// Like [`bind`], but ranks candidate sketches via `cost_model` (see +/// [`crate::cost_model`]) instead of the built-in static preference order. +pub fn bind_with(expr: &QueryExpr, cost_model: &dyn CostModel) -> Result, BindError> { + bind_in_with(expr, &BindingScope::default(), cost_model) +} + +/// Like [`bind_in`], but ranks candidate sketches via `cost_model`. +pub fn bind_in_with( + expr: &QueryExpr, + scope: &BindingScope, + cost_model: &dyn CostModel, +) -> Result, BindError> { if let QueryExpr::Aggregate { by, aggs, @@ -69,12 +88,16 @@ pub fn bind_in(expr: &QueryExpr, scope: &BindingScope) -> Result, Bin // The bindable shape: exactly one intent, no HAVING. (Multi-intent // nodes and HAVING stay logical — see the module docs.) if let ([intent], None) = (aggs.as_slice(), having) { - match realize(intent) { + match realize_with(intent, cost_model) { Realization::Sketch { kind, params } => { - return bind_summary_agg(expr, by, intent, child, kind, params, scope, true) + return bind_summary_agg( + expr, by, intent, child, kind, params, scope, true, cost_model, + ) } Realization::ExactAccumulator { kind, params } => { - return bind_summary_agg(expr, by, intent, child, kind, params, scope, false) + return bind_summary_agg( + expr, by, intent, child, kind, params, scope, false, cost_model, + ) } Realization::PassThrough => {} } @@ -95,6 +118,7 @@ fn bind_summary_agg( params: SummaryParams, scope: &BindingScope, estimate: bool, + cost_model: &dyn CostModel, ) -> Result, BindError> { let child_schema = child.output_schema_in(scope)?; // The single canonical L3 derivation (per-series vs cross-series, name @@ -113,7 +137,7 @@ fn bind_summary_agg( let agg = Rc::new(L4Node { expr: SummaryExpr::SummaryAgg { - child: bind_in(child, scope)?, + child: bind_in_with(child, scope, cost_model)?, sketch: kind, params, col, @@ -305,6 +329,52 @@ mod tests { if matches!(**e, QueryExpr::Scan { .. }))); } + /// A deployment-supplied [`CostModel`] can override the default KLL + /// choice — `bind_with` must actually consult it, not just accept and + /// ignore it (issue: cost model interface, see `crate::cost_model`). + struct PreferDDSketch; + + impl CostModel for PreferDDSketch { + fn rank_candidates( + &self, + _intent: &AggIntent, + candidates: &[SummaryKind], + ) -> Vec { + let mut v = candidates.to_vec(); + if let Some(pos) = v.iter().position(|k| *k == SummaryKind::DDSketch) { + let ddsketch = v.remove(pos); + v.insert(0, ddsketch); + } + v + } + } + + #[test] + fn bind_with_custom_cost_model_overrides_default_sketch_choice() { + let q = agg(vec![2], default_quantile(0.99), metric_scan(&["job"])); + + // Default: KLL (see `quantile_binds_kll_wrapped_in_estimate` above). + let default_root = bind(&q).unwrap(); + let SummaryExpr::SummaryEstimate { sketch_input, .. } = &default_root.expr else { + panic!("expected SummaryEstimate root, got {:?}", default_root.expr); + }; + let SummaryExpr::SummaryAgg { sketch, .. } = &sketch_input.expr else { + panic!("expected SummaryAgg, got {:?}", sketch_input.expr); + }; + assert_eq!(sketch, &SummaryKind::Kll); + + // With `PreferDDSketch`: DDSketch instead, same query. + let custom_root = bind_with(&q, &PreferDDSketch).unwrap(); + let SummaryExpr::SummaryEstimate { sketch_input, .. } = &custom_root.expr else { + panic!("expected SummaryEstimate root, got {:?}", custom_root.expr); + }; + let SummaryExpr::SummaryAgg { sketch, params, .. } = &sketch_input.expr else { + panic!("expected SummaryAgg, got {:?}", sketch_input.expr); + }; + assert_eq!(sketch, &SummaryKind::DDSketch); + assert_eq!(params, &SummaryParams::DDSketch { alpha: 0.01 }); + } + #[test] fn exact_sum_binds_accumulator_without_estimate() { let q = agg(vec![2], AggIntent::Sum { col: None }, metric_scan(&["job"])); diff --git a/crates/plan/src/boundary.rs b/crates/plan/src/boundary.rs index e0158826e..a5382e485 100644 --- a/crates/plan/src/boundary.rs +++ b/crates/plan/src/boundary.rs @@ -28,6 +28,8 @@ use asap_ir::intent_algebra::agg_intent::{agg_is_mergeable, AggIntent}; use asap_ir::types::AccuracyTarget; use asap_sketch::{SummaryKind, SummaryParams}; +use crate::cost_model::{CostModel, DefaultCostModel}; + /// How an [`AggIntent`] is realised at L4. #[derive(Debug, Clone, PartialEq)] pub enum Realization { @@ -73,8 +75,17 @@ pub fn sketch_candidates(intent: &AggIntent) -> &'static [SummaryKind] { /// /// Exhaustive over the [`AggIntent`] vocabulary — adding a variant without an /// explicit realization is a compile error, and the coverage-matrix test pins -/// each variant's category. +/// each variant's category. Ranks candidate sketches via +/// [`DefaultCostModel`] (`asap-plan`'s built-in static preference order, +/// unchanged); use [`realize_with`] to plug in a deployment-specific +/// [`CostModel`] instead. pub fn realize(intent: &AggIntent) -> Realization { + realize_with(intent, &DefaultCostModel) +} + +/// Like [`realize`], but ranks candidate sketches via `cost_model` (see +/// [`crate::cost_model`]) instead of the built-in static preference order. +pub fn realize_with(intent: &AggIntent, cost_model: &dyn CostModel) -> Realization { match intent { // ── Approximate-capable intents — the AccuracyTarget decides ──────── AggIntent::Quantile { accuracy, .. } @@ -82,7 +93,7 @@ pub fn realize(intent: &AggIntent) -> Realization { | AggIntent::Count { accuracy } | AggIntent::TopK { accuracy, .. } => match accuracy { AccuracyTarget::Exact => exact_realization(intent), - _ => bind_sketch(intent, accuracy), + _ => bind_sketch_with(intent, accuracy, cost_model), }, // ── Exact mergeable accumulators ───────────────────────────────────── @@ -169,8 +180,15 @@ fn accumulator(intent: &AggIntent, kind: SummaryKind, params: SummaryParams) -> Realization::ExactAccumulator { kind, params } } -/// Bind the preferred candidate sketch, with parameters sized to the target. -fn bind_sketch(intent: &AggIntent, accuracy: &AccuracyTarget) -> Realization { +/// Bind the preferred candidate sketch, with parameters sized to the +/// target, ranking [`sketch_candidates`] via `cost_model` (see +/// [`crate::cost_model`]) instead of taking the static-order head +/// unconditionally. +fn bind_sketch_with( + intent: &AggIntent, + accuracy: &AccuracyTarget, + cost_model: &dyn CostModel, +) -> Realization { let (eps, delta) = match accuracy { // Unreachable via `realize` (Exact routes to `exact_realization`); // degrade to the tightest parameters if called directly. @@ -178,10 +196,11 @@ fn bind_sketch(intent: &AggIntent, accuracy: &AccuracyTarget) -> Realization { AccuracyTarget::Epsilon(e) => (*e, DEFAULT_DELTA), AccuracyTarget::EpsilonDelta { epsilon, delta } => (*epsilon, *delta), }; - let kind = sketch_candidates(intent) - .first() - .expect("approximate intent has at least one candidate sketch") - .clone(); + let ranked = cost_model.rank_candidates(intent, sketch_candidates(intent)); + let kind = ranked + .into_iter() + .next() + .expect("approximate intent has at least one candidate sketch"); let params = match kind { SummaryKind::Kll => SummaryParams::Kll { k: kll_k(eps) }, SummaryKind::Cms => SummaryParams::Cms { diff --git a/crates/plan/src/cost_model.rs b/crates/plan/src/cost_model.rs index 87a59937b..39e430d75 100644 --- a/crates/plan/src/cost_model.rs +++ b/crates/plan/src/cost_model.rs @@ -1,7 +1,96 @@ -//! Cost model (stub). +//! Cost model interface (issues #6, #33). //! -//! The cost traits the optimizer consults — and, in particular, the model that -//! credits a hoisted [`cse`](crate::cse) producer once instead of per consumer. +//! `asap-plan` deliberately has no cost model *implementation* of its own — +//! ranking candidate sketches by real cost (bandwidth budget, memory +//! footprint, site count, observed drift, workload-level CSE credit, …) +//! needs knowledge this crate doesn't have and shouldn't acquire: the crate +//! doc's layering invariant is that `asap-plan` depends only on [`asap_ir`], +//! never on a runtime or a deployment model. What it *can* own is the +//! interface every deployment's cost model plugs into, so [`boundary`]'s +//! sketch selection has exactly one extension point instead of forcing each +//! downstream (ASAPCollector + ASAPQuery-backend, ASAPFusion, …) to fork +//! [`boundary::bind_sketch`]. //! -//! TODO(#6): wire workload-level CSE into a cost model. -//! TODO(#33): detect which optimizations are applicable to a query workload. +//! Every entry point that doesn't take an explicit `&dyn CostModel` +//! ([`realize`](crate::boundary::realize), [`bind`](crate::bind::bind), +//! [`bind_in`](crate::bind::bind_in)) runs against [`DefaultCostModel`], so +//! a deployment that never plugs in its own cost model keeps today's +//! static-preference-order behavior exactly, byte for byte. + +use asap_ir::intent_algebra::agg_intent::AggIntent; +use asap_sketch::SummaryKind; + +/// Ranks the candidate sketch families for one [`AggIntent`], best choice +/// first. +/// +/// [`boundary::sketch_candidates`] returns every family that *can* answer an +/// intent, in an arbitrary static preference order (issue #98's "one home" +/// for the candidate set). A `CostModel` re-orders that list under real, +/// deployment-specific cost knowledge this crate has no way to know about — +/// [`boundary::bind_sketch`] binds whichever candidate ends up first after +/// ranking. +pub trait CostModel { + /// Rank `candidates` (as returned by + /// [`sketch_candidates`](crate::boundary::sketch_candidates)) for + /// `intent`, best choice first. + /// + /// Implementations MAY reorder freely and MAY drop entries that aren't + /// available in their deployment, but MUST NOT invent a candidate that + /// wasn't in the input — an unknown [`SummaryKind`] has no + /// [`SummaryParams`](asap_sketch::SummaryParams) sizing logic in + /// [`boundary::bind_sketch`] and binding it will panic. Returning an + /// empty `Vec` means "no candidate is acceptable"; `bind_sketch` treats + /// that the same as `candidates` having been empty to begin with. + fn rank_candidates(&self, intent: &AggIntent, candidates: &[SummaryKind]) -> Vec; +} + +/// The default cost model: preserves [`sketch_candidates`]'s built-in static +/// order unchanged. +/// +/// [`sketch_candidates`]: crate::boundary::sketch_candidates +pub struct DefaultCostModel; + +impl CostModel for DefaultCostModel { + fn rank_candidates(&self, _intent: &AggIntent, candidates: &[SummaryKind]) -> Vec { + candidates.to_vec() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::boundary::sketch_candidates; + use asap_ir::intent_algebra::agg_intent::default_cardinality; + + #[test] + fn default_cost_model_preserves_static_order() { + let intent = default_cardinality(); + let candidates = sketch_candidates(&intent); + assert_eq!( + DefaultCostModel.rank_candidates(&intent, candidates), + candidates.to_vec() + ); + } + + struct AlwaysPreferLast; + + impl CostModel for AlwaysPreferLast { + fn rank_candidates( + &self, + _intent: &AggIntent, + candidates: &[SummaryKind], + ) -> Vec { + let mut v = candidates.to_vec(); + v.reverse(); + v + } + } + + #[test] + fn custom_cost_model_can_reorder_candidates() { + let intent = default_cardinality(); + let candidates = sketch_candidates(&intent); + let ranked = AlwaysPreferLast.rank_candidates(&intent, candidates); + assert_eq!(ranked.first(), candidates.last()); + } +} diff --git a/crates/plan/src/lib.rs b/crates/plan/src/lib.rs index c66ef6bb3..5a127454d 100644 --- a/crates/plan/src/lib.rs +++ b/crates/plan/src/lib.rs @@ -24,8 +24,10 @@ //! - [`bind`] — the L3→L4 binding pass: walks a `QueryExpr` tree, fires the //! [`boundary`] decision per node, and emits the sketch-bound //! [`SummaryExpr`](asap_sketch::SummaryExpr) DAG (issue #98). -//! - [`cost_model`] — stub: cost traits + the model CSE credits a shared -//! producer against (issues #6, #33). +//! - [`cost_model`] — the [`CostModel`](cost_model::CostModel) trait every +//! deployment's cost-based sketch selection plugs into (issues #6, #33). +//! `asap-plan` itself only ships [`DefaultCostModel`](cost_model::DefaultCostModel), +//! which preserves [`boundary`]'s built-in static preference order. pub mod cse; @@ -33,5 +35,6 @@ pub mod bind; pub mod boundary; pub mod cost_model; -pub use bind::{bind, bind_in, BindError}; -pub use boundary::{realize, sketch_candidates, Realization}; +pub use bind::{bind, bind_in, bind_in_with, bind_with, BindError}; +pub use boundary::{realize, realize_with, sketch_candidates, Realization}; +pub use cost_model::{CostModel, DefaultCostModel}; From 0afdd92977761a26fca058ec0913e36b8fc332a8 Mon Sep 17 00:00:00 2001 From: zz_y Date: Sun, 19 Jul 2026 19:50:50 -0600 Subject: [PATCH 2/2] refactor(plan): rename sketch_candidates/bind_sketch_with to summary_ naming sketch_candidates/bind_sketch_with predate this PR's CostModel interface, but the "sketch" in their names is narrower than what CostModel:: rank_candidates is meant to cover: SummaryKind already spans exact accumulators alongside approximate sketches, and non-sketch summary realizations (sampling-based, wavelet-transform, OMP/compressive-sensing) are expected to land as SummaryKind variants over time. Renaming to summary_candidates/bind_summary_with matches the established SummaryKind/ SummaryParams/SummaryExpr convention instead of introducing a competing "sketch" vocabulary for the same concept. Doc comments and one test name updated to match; no behavioral change. Co-Authored-By: Claude Sonnet 5 --- crates/plan/src/bind.rs | 8 +++---- crates/plan/src/boundary.rs | 30 +++++++++++++-------------- crates/plan/src/cost_model.rs | 39 +++++++++++++++++++++-------------- crates/plan/src/lib.rs | 2 +- 4 files changed, 44 insertions(+), 35 deletions(-) diff --git a/crates/plan/src/bind.rs b/crates/plan/src/bind.rs index 23b1995cb..777fc1d51 100644 --- a/crates/plan/src/bind.rs +++ b/crates/plan/src/bind.rs @@ -52,7 +52,7 @@ pub enum BindError { } /// Bind a single query (empty `LetBinding` scope) to the L4 IR. Ranks -/// candidate sketches via [`DefaultCostModel`] (`asap-plan`'s built-in +/// candidate summaries via [`DefaultCostModel`] (`asap-plan`'s built-in /// static preference order, unchanged); use [`bind_with`] to plug in a /// deployment-specific [`CostModel`] instead. pub fn bind(expr: &QueryExpr) -> Result, BindError> { @@ -65,13 +65,13 @@ pub fn bind_in(expr: &QueryExpr, scope: &BindingScope) -> Result, Bin bind_in_with(expr, scope, &DefaultCostModel) } -/// Like [`bind`], but ranks candidate sketches via `cost_model` (see +/// Like [`bind`], but ranks candidate summaries via `cost_model` (see /// [`crate::cost_model`]) instead of the built-in static preference order. pub fn bind_with(expr: &QueryExpr, cost_model: &dyn CostModel) -> Result, BindError> { bind_in_with(expr, &BindingScope::default(), cost_model) } -/// Like [`bind_in`], but ranks candidate sketches via `cost_model`. +/// Like [`bind_in`], but ranks candidate summaries via `cost_model`. pub fn bind_in_with( expr: &QueryExpr, scope: &BindingScope, @@ -350,7 +350,7 @@ mod tests { } #[test] - fn bind_with_custom_cost_model_overrides_default_sketch_choice() { + fn bind_with_custom_cost_model_overrides_default_summary_choice() { let q = agg(vec![2], default_quantile(0.99), metric_scan(&["job"])); // Default: KLL (see `quantile_binds_kll_wrapped_in_estimate` above). diff --git a/crates/plan/src/boundary.rs b/crates/plan/src/boundary.rs index a5382e485..34bbf5dbc 100644 --- a/crates/plan/src/boundary.rs +++ b/crates/plan/src/boundary.rs @@ -56,12 +56,12 @@ pub enum Realization { /// one. `ln(1/0.01) → depth 5`, matching the conventional CMS sizing. pub const DEFAULT_DELTA: f64 = 0.01; -/// The sketch families that can serve an intent, most-preferred first. +/// The summary families that can serve an intent, most-preferred first. /// This is the `AggIntent → SummaryKind` map of issue #98; [`realize`] binds /// the head of the list. The tail entries are the alternatives a future cost /// model (#6/#33) may pick instead — listed here so the candidate set has one /// home. -pub fn sketch_candidates(intent: &AggIntent) -> &'static [SummaryKind] { +pub fn summary_candidates(intent: &AggIntent) -> &'static [SummaryKind] { match intent { AggIntent::Quantile { .. } => &[SummaryKind::Kll, SummaryKind::DDSketch], AggIntent::Cardinality { .. } => &[SummaryKind::Hll, SummaryKind::Theta, SummaryKind::Kmv], @@ -75,7 +75,7 @@ pub fn sketch_candidates(intent: &AggIntent) -> &'static [SummaryKind] { /// /// Exhaustive over the [`AggIntent`] vocabulary — adding a variant without an /// explicit realization is a compile error, and the coverage-matrix test pins -/// each variant's category. Ranks candidate sketches via +/// each variant's category. Ranks candidate summaries via /// [`DefaultCostModel`] (`asap-plan`'s built-in static preference order, /// unchanged); use [`realize_with`] to plug in a deployment-specific /// [`CostModel`] instead. @@ -83,7 +83,7 @@ pub fn realize(intent: &AggIntent) -> Realization { realize_with(intent, &DefaultCostModel) } -/// Like [`realize`], but ranks candidate sketches via `cost_model` (see +/// Like [`realize`], but ranks candidate summaries via `cost_model` (see /// [`crate::cost_model`]) instead of the built-in static preference order. pub fn realize_with(intent: &AggIntent, cost_model: &dyn CostModel) -> Realization { match intent { @@ -93,7 +93,7 @@ pub fn realize_with(intent: &AggIntent, cost_model: &dyn CostModel) -> Realizati | AggIntent::Count { accuracy } | AggIntent::TopK { accuracy, .. } => match accuracy { AccuracyTarget::Exact => exact_realization(intent), - _ => bind_sketch_with(intent, accuracy, cost_model), + _ => bind_summary_with(intent, accuracy, cost_model), }, // ── Exact mergeable accumulators ───────────────────────────────────── @@ -180,11 +180,11 @@ fn accumulator(intent: &AggIntent, kind: SummaryKind, params: SummaryParams) -> Realization::ExactAccumulator { kind, params } } -/// Bind the preferred candidate sketch, with parameters sized to the -/// target, ranking [`sketch_candidates`] via `cost_model` (see +/// Bind the preferred candidate summary, with parameters sized to the +/// target, ranking [`summary_candidates`] via `cost_model` (see /// [`crate::cost_model`]) instead of taking the static-order head /// unconditionally. -fn bind_sketch_with( +fn bind_summary_with( intent: &AggIntent, accuracy: &AccuracyTarget, cost_model: &dyn CostModel, @@ -196,11 +196,11 @@ fn bind_sketch_with( AccuracyTarget::Epsilon(e) => (*e, DEFAULT_DELTA), AccuracyTarget::EpsilonDelta { epsilon, delta } => (*epsilon, *delta), }; - let ranked = cost_model.rank_candidates(intent, sketch_candidates(intent)); + let ranked = cost_model.rank_candidates(intent, summary_candidates(intent)); let kind = ranked .into_iter() .next() - .expect("approximate intent has at least one candidate sketch"); + .expect("approximate intent has at least one candidate summary"); let params = match kind { SummaryKind::Kll => SummaryParams::Kll { k: kll_k(eps) }, SummaryKind::Cms => SummaryParams::Cms { @@ -554,27 +554,27 @@ mod tests { #[test] fn candidate_lists_match_the_issue_map() { assert_eq!( - sketch_candidates(&default_quantile(0.5)), + summary_candidates(&default_quantile(0.5)), &[SummaryKind::Kll, SummaryKind::DDSketch] ); assert_eq!( - sketch_candidates(&default_cardinality()), + summary_candidates(&default_cardinality()), &[SummaryKind::Hll, SummaryKind::Theta, SummaryKind::Kmv] ); assert_eq!( - sketch_candidates(&AggIntent::TopK { + summary_candidates(&AggIntent::TopK { k: 5, accuracy: eps(0.01) }), &[SummaryKind::CmsWithHeap] ); assert_eq!( - sketch_candidates(&AggIntent::Count { + summary_candidates(&AggIntent::Count { accuracy: eps(0.01) }), &[SummaryKind::Cms] ); - assert!(sketch_candidates(&AggIntent::Rate).is_empty()); + assert!(summary_candidates(&AggIntent::Rate).is_empty()); } #[test] diff --git a/crates/plan/src/cost_model.rs b/crates/plan/src/cost_model.rs index 39e430d75..d668aa5d7 100644 --- a/crates/plan/src/cost_model.rs +++ b/crates/plan/src/cost_model.rs @@ -1,15 +1,24 @@ //! Cost model interface (issues #6, #33). //! //! `asap-plan` deliberately has no cost model *implementation* of its own — -//! ranking candidate sketches by real cost (bandwidth budget, memory +//! ranking candidate summaries by real cost (bandwidth budget, memory //! footprint, site count, observed drift, workload-level CSE credit, …) //! needs knowledge this crate doesn't have and shouldn't acquire: the crate //! doc's layering invariant is that `asap-plan` depends only on [`asap_ir`], //! never on a runtime or a deployment model. What it *can* own is the //! interface every deployment's cost model plugs into, so [`boundary`]'s -//! sketch selection has exactly one extension point instead of forcing each -//! downstream (ASAPCollector + ASAPQuery-backend, ASAPFusion, …) to fork -//! [`boundary::bind_sketch`]. +//! summary selection has exactly one extension point instead of forcing +//! each downstream (ASAPCollector + ASAPQuery-backend, ASAPFusion, …) to +//! fork [`boundary::bind_summary_with`]. +//! +//! "Summary" here is deliberately broader than the classic streaming +//! sketches [`SummaryKind`] enumerates today (Kll/DDSketch/Hll/Cms/…): +//! [`CostModel::rank_candidates`] ranks whatever [`SummaryKind`] variants +//! [`boundary::summary_candidates`] offers for an intent, so it already +//! extends to non-sketch realizations — sampling-based summaries, +//! wavelet-transform summaries, OMP/compressive-sensing summaries, … — +//! the day a variant for one lands in [`asap_sketch`]; nothing in this +//! trait or [`boundary`]'s dispatch is sketch-specific. //! //! Every entry point that doesn't take an explicit `&dyn CostModel` //! ([`realize`](crate::boundary::realize), [`bind`](crate::bind::bind), @@ -20,34 +29,34 @@ use asap_ir::intent_algebra::agg_intent::AggIntent; use asap_sketch::SummaryKind; -/// Ranks the candidate sketch families for one [`AggIntent`], best choice +/// Ranks the candidate summary families for one [`AggIntent`], best choice /// first. /// -/// [`boundary::sketch_candidates`] returns every family that *can* answer an +/// [`boundary::summary_candidates`] returns every family that *can* answer an /// intent, in an arbitrary static preference order (issue #98's "one home" /// for the candidate set). A `CostModel` re-orders that list under real, /// deployment-specific cost knowledge this crate has no way to know about — -/// [`boundary::bind_sketch`] binds whichever candidate ends up first after +/// [`boundary::bind_summary_with`] binds whichever candidate ends up first after /// ranking. pub trait CostModel { /// Rank `candidates` (as returned by - /// [`sketch_candidates`](crate::boundary::sketch_candidates)) for + /// [`summary_candidates`](crate::boundary::summary_candidates)) for /// `intent`, best choice first. /// /// Implementations MAY reorder freely and MAY drop entries that aren't /// available in their deployment, but MUST NOT invent a candidate that /// wasn't in the input — an unknown [`SummaryKind`] has no /// [`SummaryParams`](asap_sketch::SummaryParams) sizing logic in - /// [`boundary::bind_sketch`] and binding it will panic. Returning an - /// empty `Vec` means "no candidate is acceptable"; `bind_sketch` treats + /// [`boundary::bind_summary_with`] and binding it will panic. Returning an + /// empty `Vec` means "no candidate is acceptable"; `bind_summary_with` treats /// that the same as `candidates` having been empty to begin with. fn rank_candidates(&self, intent: &AggIntent, candidates: &[SummaryKind]) -> Vec; } -/// The default cost model: preserves [`sketch_candidates`]'s built-in static +/// The default cost model: preserves [`summary_candidates`]'s built-in static /// order unchanged. /// -/// [`sketch_candidates`]: crate::boundary::sketch_candidates +/// [`summary_candidates`]: crate::boundary::summary_candidates pub struct DefaultCostModel; impl CostModel for DefaultCostModel { @@ -59,13 +68,13 @@ impl CostModel for DefaultCostModel { #[cfg(test)] mod tests { use super::*; - use crate::boundary::sketch_candidates; + use crate::boundary::summary_candidates; use asap_ir::intent_algebra::agg_intent::default_cardinality; #[test] fn default_cost_model_preserves_static_order() { let intent = default_cardinality(); - let candidates = sketch_candidates(&intent); + let candidates = summary_candidates(&intent); assert_eq!( DefaultCostModel.rank_candidates(&intent, candidates), candidates.to_vec() @@ -89,7 +98,7 @@ mod tests { #[test] fn custom_cost_model_can_reorder_candidates() { let intent = default_cardinality(); - let candidates = sketch_candidates(&intent); + let candidates = summary_candidates(&intent); let ranked = AlwaysPreferLast.rank_candidates(&intent, candidates); assert_eq!(ranked.first(), candidates.last()); } diff --git a/crates/plan/src/lib.rs b/crates/plan/src/lib.rs index 5a127454d..c69ff114a 100644 --- a/crates/plan/src/lib.rs +++ b/crates/plan/src/lib.rs @@ -36,5 +36,5 @@ pub mod boundary; pub mod cost_model; pub use bind::{bind, bind_in, bind_in_with, bind_with, BindError}; -pub use boundary::{realize, realize_with, sketch_candidates, Realization}; +pub use boundary::{realize, realize_with, summary_candidates, Realization}; pub use cost_model::{CostModel, DefaultCostModel};