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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
199 changes: 118 additions & 81 deletions crates/asap-aware-mapping/src/bind.rs

Large diffs are not rendered by default.

232 changes: 133 additions & 99 deletions crates/asap-aware-mapping/src/boundary.rs

Large diffs are not rendered by default.

65 changes: 35 additions & 30 deletions crates/asap-aware-mapping/src/cost_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,19 @@
//! each downstream (ASAPCollector + ASAPQuery-backend, ASAPFusion, …) to
//! fork [`boundary::implementation_for`].
//!
//! "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.
//! This trait is scoped to the approximate-**sketch** family specifically
//! ([`CostModel::rank_candidates`]/[`size_params`](CostModel::size_params)
//! take/return [`SketchKind`]/[`SketchParams`]) — `asap_sketch` also has
//! sibling families for sampling-based, wavelet-transform, and fitted
//! statistical-model summaries
//! ([`asap_types::post_asap::SamplingKind`]/…/[`asap_types::post_asap::StatModelKind`]),
//! each with its own `(Kind, Params)` pair, deliberately *not* folded into
//! this trait: no core `AggIntent` picks one of those families today (only
//! [`CostModel::realize_extension`] can, for a deployment-specific
//! `AggIntent::Extension`), so there is no ranking/sizing decision for this
//! trait to own yet. Should a family other than `Sketch` ever need its own
//! `rank_candidates`/`size_params`, it gets its own trait methods rather
//! than overloading these ones across incompatible `Kind`/`Params` types.
//!
//! Every entry point that doesn't take an explicit `&dyn CostModel`
//! ([`implementation_for`](crate::boundary::implementation_for),
Expand All @@ -27,7 +32,7 @@
//! model keeps today's static-preference-order behavior exactly, byte for
//! byte.

use asap_types::post_asap::{SketchQuery, SummaryKind, SummaryParams};
use asap_types::post_asap::{SketchKind, SketchParams, SketchQuery};
use asap_types::pre_asap::agg_intent::AggIntent;
use asap_types::pre_asap::expr_ir::ColumnRef;

Expand All @@ -49,15 +54,15 @@ pub trait CostModel {
///
/// 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_types::post_asap::SummaryParams) sizing logic in
/// wasn't in the input — an unknown [`SketchKind`] has no
/// [`SketchParams`](asap_types::post_asap::SketchParams) sizing logic in
/// [`boundary::implementation_for_with`] and binding it will panic.
/// Returning an empty `Vec` means "no candidate is acceptable";
/// `implementation_for_with` treats that the same as `candidates`
/// having been empty to begin with.
fn rank_candidates(&self, intent: &AggIntent, candidates: &[SummaryKind]) -> Vec<SummaryKind>;
fn rank_candidates(&self, intent: &AggIntent, candidates: &[SketchKind]) -> Vec<SketchKind>;

/// Size [`SummaryParams`] for `kind` (one of the candidates
/// Size [`SketchParams`] for `kind` (one of the candidates
/// [`rank_candidates`](Self::rank_candidates) put first) under the
/// resolved `(eps, delta)` accuracy budget.
///
Expand All @@ -71,11 +76,11 @@ pub trait CostModel {
/// not resize them, can leave this method unimplemented.
fn size_params(
&self,
kind: SummaryKind,
kind: SketchKind,
intent: &AggIntent,
eps: f64,
delta: f64,
) -> SummaryParams {
) -> SketchParams {
crate::boundary::default_size_params(kind, intent, eps, delta)
}

Expand Down Expand Up @@ -120,7 +125,7 @@ pub trait CostModel {
pub struct DefaultCostModel;

impl CostModel for DefaultCostModel {
fn rank_candidates(&self, _intent: &AggIntent, candidates: &[SummaryKind]) -> Vec<SummaryKind> {
fn rank_candidates(&self, _intent: &AggIntent, candidates: &[SketchKind]) -> Vec<SketchKind> {
candidates.to_vec()
}
}
Expand All @@ -147,8 +152,8 @@ mod tests {
fn rank_candidates(
&self,
_intent: &AggIntent,
candidates: &[SummaryKind],
) -> Vec<SummaryKind> {
candidates: &[SketchKind],
) -> Vec<SketchKind> {
let mut v = candidates.to_vec();
v.reverse();
v
Expand All @@ -170,8 +175,8 @@ mod tests {
fn size_params_default_body_matches_default_size_params() {
let intent = default_cardinality();
assert_eq!(
AlwaysPreferLast.size_params(SummaryKind::Hll, &intent, 0.01, 0.01),
crate::boundary::default_size_params(SummaryKind::Hll, &intent, 0.01, 0.01),
AlwaysPreferLast.size_params(SketchKind::Hll, &intent, 0.01, 0.01),
crate::boundary::default_size_params(SketchKind::Hll, &intent, 0.01, 0.01),
);
}

Expand All @@ -185,22 +190,22 @@ mod tests {
fn rank_candidates(
&self,
_intent: &AggIntent,
candidates: &[SummaryKind],
) -> Vec<SummaryKind> {
candidates: &[SketchKind],
) -> Vec<SketchKind> {
candidates.to_vec()
}

fn size_params(
&self,
kind: SummaryKind,
kind: SketchKind,
intent: &AggIntent,
eps: f64,
delta: f64,
) -> SummaryParams {
) -> SketchParams {
match kind {
SummaryKind::Kll => {
SketchKind::Kll => {
let k = if eps >= 0.01 { 200 } else { 2048 };
SummaryParams::Kll { k }
SketchParams::Kll { k }
}
other => crate::boundary::default_size_params(other, intent, eps, delta),
}
Expand All @@ -213,13 +218,13 @@ mod tests {

let intent = default_quantile(0.99);
assert_eq!(
DiscreteKllRungs.size_params(SummaryKind::Kll, &intent, 0.001, 0.01),
SummaryParams::Kll { k: 2048 },
DiscreteKllRungs.size_params(SketchKind::Kll, &intent, 0.001, 0.01),
SketchParams::Kll { k: 2048 },
);
// Untouched kinds still fall through to the default formula.
assert_eq!(
DiscreteKllRungs.size_params(SummaryKind::Hll, &intent, 0.01, 0.01),
crate::boundary::default_size_params(SummaryKind::Hll, &intent, 0.01, 0.01),
DiscreteKllRungs.size_params(SketchKind::Hll, &intent, 0.01, 0.01),
crate::boundary::default_size_params(SketchKind::Hll, &intent, 0.01, 0.01),
);
}
}
7 changes: 4 additions & 3 deletions crates/asap-aware-mapping/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
//! Two real occupants and one stub:
//!
//! - [`boundary`] — the per-intent sketch-vs-exact (accuracy) decision:
//! `AggIntent → SummaryKind + SummaryParams` sized to the `AccuracyTarget`
//! (issue #98). [`boundary::implementation_for`] is the per-node decision;
//! `AggIntent → Implementation` (a summary family's own `(Kind, Params)`,
//! or an exact accumulator) sized to the `AccuracyTarget` (issue #98).
//! [`boundary::implementation_for`] is the per-node decision;
//! [`bind::implement_tree`] drives it over a whole tree — see the
//! terminology section below for why these two are named around
//! "implementation" rather than "bind".
Expand Down Expand Up @@ -64,7 +65,7 @@
//! compatibility rules (e.g. a heap-bearing top-k sketch also satisfying a
//! bare frequency point-query) turned out to have deployment-specific
//! competitors (e.g. single-vs-multi-population re-aggregation) that
//! don't reduce to a fact about `SummaryKind` alone. `control_plane`'s own
//! don't reduce to a fact about a summary family's kind alone. `control_plane`'s own
//! `sketch_algebra::capability::Capability`/`is_satisfied_by` is the
//! reference downstream implementation.

Expand Down
58 changes: 30 additions & 28 deletions crates/integration-tests/tests/l4_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,23 @@
//!
//! Drives the full pipeline — PromQL text → pre-ASAP `QueryExpr`
//! (`lower_promql`) → post-ASAP `SummaryExpr` DAG
//! (`asap_aware_mapping::implement_tree`) — and pins the sketch-bound shape
//! node by node, including the `(SummaryKind, SummaryParams)` committed on
//! each edge's schema. This is the design doc's §"L4 — sketch algebra"
//! worked example, running for real.
//! (`asap_aware_mapping::implement_tree`) — and pins the summary-bound shape
//! node by node, including the family `(Kind, Params)` committed on each
//! edge's schema. This is the design doc's §"L4 — sketch algebra" worked
//! example, running for real.

use asap_aware_mapping::implement_tree;
use asap_frontend_promql::lower_promql;
use asap_types::post_asap::{
SketchQuery, SummaryDataType, SummaryExpr, SummaryKind, SummaryParams, SummarySchema,
ExactKind, ExactParams, SketchKind, SketchParams, SketchQuery, SummaryExpr, SummaryFamilyType,
SummarySchema,
};
use asap_types::pre_asap::expr_ir::ColumnRef;
use asap_types::pre_asap::query_expr::{QueryExpr, Reduction};
use asap_types::pre_asap::schema::DataType;
use asap_types::types::AccuracyTarget;

fn dtype<'a>(schema: &'a SummarySchema, name: &str) -> &'a SummaryDataType {
fn dtype<'a>(schema: &'a SummarySchema, name: &str) -> &'a SummaryFamilyType {
&schema
.fields
.iter()
Expand All @@ -31,7 +32,7 @@ fn dtype<'a>(schema: &'a SummarySchema, name: &str) -> &'a SummaryDataType {
/// ```text
/// SummaryEstimate { query: Quantile{0.99} } → {quantile_0_99: Float64}
/// └─ SummaryAgg { Kll{k:200}, col: SampleValue } → {quantile_0_99: Sketch(Kll, {k:200})}
/// └─ SummaryAgg { Rate, col: SampleValue } → {ts, value: Sketch(Rate), …}
/// └─ SummaryAgg { Rate, col: SampleValue } → {ts, value: ExactAggregate(Rate), …}
/// └─ Logical(TimeRange{5m} → Scan) → {ts, value}
/// ```
///
Expand All @@ -58,8 +59,8 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() {
assert!(matches!(query, SketchQuery::Quantile { q } if *q == 0.99));
assert_eq!(
dtype(&root.schema, "quantile_0_99"),
&SummaryDataType::Primitive(DataType::Float64),
"the Sketch(…) type must not propagate past the estimate"
&SummaryFamilyType::Plain(DataType::Float64),
"the summary-state type must not propagate past the estimate"
);

// The quantile: KLL committed, k=200 sized from ε=0.01. `quantile(...)`
Expand All @@ -69,16 +70,17 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() {
// same empty `by: []` (issue #163).
let SummaryExpr::SummaryAgg {
child,
summary,
params,
family,
col,
reduction,
} = &summary_input.expr
else {
panic!("expected SummaryAgg, got {:?}", summary_input.expr);
};
assert_eq!(summary, &SummaryKind::Kll);
assert_eq!(params, &SummaryParams::Kll { k: 200 });
assert_eq!(
family,
&SummaryFamilyType::Sketch(SketchKind::Kll, SketchParams::Kll { k: 200 })
);
assert_eq!(col, &ColumnRef::SampleValue);
assert_eq!(
reduction,
Expand All @@ -87,28 +89,29 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() {
);
assert_eq!(
dtype(&summary_input.schema, "quantile_0_99"),
&SummaryDataType::Sketch(SummaryKind::Kll, SummaryParams::Kll { k: 200 })
&SummaryFamilyType::Sketch(SketchKind::Kll, SketchParams::Kll { k: 200 })
);

// The rate: exact counter-reset-aware accumulator, per-series (labels
// and time axis preserved), no estimate wrapper. `rate(...)` has no
// grouping concept at all — every entity stays its own summary.
let SummaryExpr::SummaryAgg {
child: leaf,
summary,
params,
family,
reduction,
..
} = &child.expr
else {
panic!("expected inner SummaryAgg for rate, got {:?}", child.expr);
};
assert_eq!(summary, &SummaryKind::Rate);
assert_eq!(params, &SummaryParams::Rate);
assert_eq!(
family,
&SummaryFamilyType::ExactAggregate(ExactKind::Rate, ExactParams::Rate)
);
assert_eq!(reduction, &Reduction::PerEntity);
assert_eq!(
dtype(&child.schema, "value"),
&SummaryDataType::Sketch(SummaryKind::Rate, SummaryParams::Rate)
&SummaryFamilyType::ExactAggregate(ExactKind::Rate, ExactParams::Rate)
);
assert_eq!(
child.schema.time_index,
Expand All @@ -129,8 +132,8 @@ fn promql_quantile_of_rate_binds_kll_over_rate_accumulator() {
leaf.schema
.fields
.iter()
.all(|f| matches!(f.dtype, SummaryDataType::Primitive(_))),
"logical edges carry only primitive columns"
.all(|f| matches!(f.dtype, SummaryFamilyType::Plain(_))),
"logical edges carry only plain columns"
);
}

Expand All @@ -143,24 +146,23 @@ fn promql_exact_workload_binds_accumulators_not_sketches() {
.expect("lowering failed");
let root = implement_tree(&l3).expect("binding failed");
let SummaryExpr::SummaryAgg {
summary,
params,
reduction,
..
family, reduction, ..
} = &root.expr
else {
panic!("expected SummaryAgg, got {:?}", root.expr);
};
assert_eq!(summary, &SummaryKind::Sum);
assert_eq!(params, &SummaryParams::Sum);
assert_eq!(
family,
&SummaryFamilyType::ExactAggregate(ExactKind::Sum, ExactParams::Sum)
);
assert_eq!(
reduction,
&Reduction::by(vec![2]),
"job is col 2 in [ts, value, job]"
);
assert_eq!(
dtype(&root.schema, "job"),
&SummaryDataType::Primitive(DataType::Utf8),
&SummaryFamilyType::Plain(DataType::Utf8),
"group keys pass through verbatim"
);

Expand Down
Loading
Loading