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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/accuracy/src/estimators/cms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pub(super) fn guarantee(
}

/// CMS: over-count ≤ ε·N with width `w = ⌈e/ε⌉` columns.
#[cfg(test)]
pub(crate) fn cms_width(eps: f64) -> u32 {
saturating_ceil(std::f64::consts::E / eps, 2, 1 << 26)
}
Expand Down
1 change: 1 addition & 0 deletions crates/accuracy/src/estimators/count_sketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ fn count_sketch_failure_probability(width: u32, depth: u32) -> Option<f64> {
}

/// CountSketch `L2` point-query width: ε = sqrt(3/w).
#[cfg(test)]
pub(crate) fn count_sketch_width(eps: f64) -> u32 {
saturating_ceil(3.0 / (eps * eps), 2, 1 << 26)
}
Expand Down
1 change: 1 addition & 0 deletions crates/accuracy/src/estimators/ddsketch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ pub(super) fn guarantee(
))
}

#[cfg(test)]
pub(super) fn size_params(epsilon: f64) -> SketchParams {
SketchParams::DDSketch { alpha: epsilon }
}
1 change: 1 addition & 0 deletions crates/accuracy/src/estimators/hll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,7 @@ pub fn min_precision(
/// bound up to `⌈1/ε⌉` (18 when none up to 18 does). Pass 1 then raises
/// the precision by the same rule ([`min_precision`]) when the source's own
/// bound or a smaller failure probability needs it.
#[cfg(test)]
pub(crate) fn hll_precision(eps: f64, delta: f64) -> u8 {
if !(eps.is_finite() && eps > 0.0 && delta.is_finite() && delta > 0.0) {
return 18;
Expand Down
181 changes: 180 additions & 1 deletion crates/accuracy/src/estimators/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Dispatch committed estimator parameters to their accuracy models.
use super::*;
#[cfg(test)]
use asap_types::ir::operator::AggIntent;
use asap_types::ir::schema::FamilyCapability;

Expand Down Expand Up @@ -142,7 +143,10 @@ pub fn with_failure_probability(params: &SketchParams, delta: f64) -> SketchPara
}
}

#[allow(dead_code)] // Read again for the predefined θ lists (`parameter_list`), in #1207 PR 1.
/// Today's sizing: the parameters of family `kind` for error `eps` at failure
/// probability `delta`. Test-only: the predefined lists (`parameter_list`)
/// are this sizing over an error grid, which a test keeps equal.
#[cfg(test)]
pub(crate) fn size_params(
kind: SketchAlgorithm,
intent: &AggIntent,
Expand Down Expand Up @@ -249,6 +253,7 @@ pub(crate) fn binomial_upper_tail(n: u32, p: f64, k: u32) -> f64 {

/// Accuracy-dependent candidate budget, not a completeness theorem. The
/// membership model must still certify the selected set independently.
#[cfg(test)]
pub(crate) fn topk_capacity(k: usize, eps: f64) -> u32 {
u32::try_from(k)
.unwrap_or(u32::MAX)
Expand All @@ -269,6 +274,180 @@ mod tests {
use super::*;
use asap_types::ir::operator::agg_intent::{default_cardinality, default_quantile};

/// Each family's predefined list is today's sizing at failure
/// probability 0.01 over the error grid, weakest first, without repeats
/// (a UnivMon's: its L2-norm, distinct-count and other shapes). KMV's and
/// UnivMon's distinct counts stop at ε = 0.01. A heap variant's width × depth is the top-1
/// sizing, each with every heap size of the 1-2-5 series 10 … 10 000.
#[test]
fn parameter_list_is_the_sizing_over_the_error_grid() {
use asap_types::ir::properties::AccuracyTarget;
use asap_types::stages::parameter_list;
let full_grid = [0.1, 0.05, 0.02, 0.01, 0.005, 0.002, 0.001];
let distinct_grid = [0.1, 0.05, 0.02, 0.01];
let heap_sizes = [10, 20, 50, 100, 200, 500, 1_000, 2_000, 5_000, 10_000];
let l2 = AggIntent::FrequencyL2 {
col: None,
accuracy: AccuracyTarget::max_error(0.01),
};
let top_1 = AggIntent::TopK {
k: 1,
accuracy: AccuracyTarget::max_error(0.01),
};
let quantile = default_quantile(0.5);
let distinct = default_cardinality();
for (family, intents) in [
(SketchAlgorithm::Kll, vec![&quantile]),
(SketchAlgorithm::DDSketch, vec![&quantile]),
(SketchAlgorithm::Hll, vec![&distinct]),
(SketchAlgorithm::Kmv, vec![&distinct]),
(SketchAlgorithm::Cms, vec![&distinct]),
(SketchAlgorithm::CountSketch, vec![&distinct]),
(SketchAlgorithm::CmsWithHeap, vec![&top_1]),
(SketchAlgorithm::CountSketchWithHeap, vec![&top_1]),
(SketchAlgorithm::UnivMon, vec![&l2, &distinct, &quantile]),
] {
let mut sized: Vec<SketchParams> = Vec::new();
for intent in intents {
let grid: &[f64] = match (family, intent) {
(
SketchAlgorithm::Kmv | SketchAlgorithm::UnivMon,
AggIntent::Cardinality { .. },
) => &distinct_grid,
_ => &full_grid,
};
for &eps in grid {
let params = size_params(family, intent, eps, sizing::DEFAULT_DELTA);
let with_heaps: Vec<SketchParams> = match params {
SketchParams::CmsWithHeap { width, depth, .. } => heap_sizes
.iter()
.map(|&heap_size| SketchParams::CmsWithHeap {
width,
depth,
heap_size,
})
.collect(),
SketchParams::CountSketchWithHeap { width, depth, .. } => heap_sizes
.iter()
.map(|&heap_size| SketchParams::CountSketchWithHeap {
width,
depth,
heap_size,
})
.collect(),
other => vec![other],
};
for params in with_heaps {
if !sized.contains(&params) {
sized.push(params);
}
}
}
}
assert_eq!(
format!("{:?}", parameter_list(family)),
format!("{sized:?}"),
"{family:?}"
);
}
}

/// Within each family's list, a configuration that satisfies another's
/// floor (`producer_satisfies_logical_requirements`) has no larger error, at the lists' failure
/// probability 0.01, for every statistic the family answers.
#[test]
fn a_configuration_satisfying_a_listed_floor_is_no_less_accurate() {
use asap_types::ir::operator::operator_properties::Reduction;
use asap_types::ir::schema::{GroupingStrategy, SummaryUpdate};
use asap_types::ir::ASAPOp;
use asap_types::ir::Operator;
use asap_types::stages::{
parameter_list, producer_satisfies_logical_requirements, CandidateExpression,
LogicalProperties, LogicalRequirements, MemoGroupId,
};
let point = SketchStatistic::PointCount {
key: asap_types::ir::scalar::expr_ir::ColumnRef::SampleValue,
value: None,
};
let statistics = [
SketchStatistic::Quantile { q: 0.5 },
SketchStatistic::Cardinality,
SketchStatistic::FrequencyL2,
SketchStatistic::FrequencyEntropy,
SketchStatistic::TopK { k: 1 },
point,
];
for family in [
SketchAlgorithm::Kll,
SketchAlgorithm::DDSketch,
SketchAlgorithm::Hll,
SketchAlgorithm::Kmv,
SketchAlgorithm::Cms,
SketchAlgorithm::CountSketch,
SketchAlgorithm::CmsWithHeap,
SketchAlgorithm::CountSketchWithHeap,
SketchAlgorithm::UnivMon,
] {
let build = CandidateExpression {
operator: Operator::ASAP(ASAPOp::SummaryAgg {
child: MemoGroupId(0),
family: FieldDataType::Sketch(family, GroupingStrategy::default()),
input: SummaryUpdate::column(
asap_types::ir::scalar::expr_ir::ColumnRef::SampleValue,
),
reduction: Reduction::PerEntity,
grouping: GroupingStrategy::default(),
filter: None,
}),
};
let capability =
FamilyCapability::of(&FieldDataType::Sketch(family, GroupingStrategy::default()));
let list = parameter_list(family);
for producer in list {
let properties =
LogicalProperties::new(&build, Some(producer.clone()), None).unwrap();
for floor in list {
let requirement = LogicalRequirements::new(Some(floor.clone()));
if !producer_satisfies_logical_requirements(&properties, &requirement) {
continue;
}
for statistic in statistics.iter().filter(|s| capability.answers(s)) {
let Some(needed) = sketch_guarantee(&family, floor, statistic) else {
continue;
};
let delivered = sketch_guarantee(&family, producer, statistic)
.unwrap_or_else(|| panic!("{producer:?} {statistic:?}"));
// A bound that does not evaluate (an unknown term) is
// compared by its expression.
let error_no_larger = match (
delivered.spec.error.evaluate(),
needed.spec.error.evaluate(),
) {
(Some(error), Some(needed)) => error <= needed * (1.0 + 1e-9),
(_, _) => delivered.spec.error == needed.spec.error,
};
let failure_no_likelier = match (
delivered.spec.failure_probability.evaluate(),
needed.spec.failure_probability.evaluate(),
) {
(Some(delta), Some(needed)) => {
delta <= needed.max(sizing::DEFAULT_DELTA) * (1.0 + 1e-9)
}
(_, _) => {
delivered.spec.failure_probability
== needed.spec.failure_probability
}
};
assert!(
error_no_larger && failure_no_likelier,
"{producer:?} ⪰ {floor:?} for {statistic:?}: {delivered:?} vs {needed:?}"
);
}
}
}
}
}

fn delta_and_error(params: &SketchParams, query: &SketchStatistic) -> (f64, f64) {
let algorithm = match params {
SketchParams::Cms { .. } => SketchAlgorithm::Cms,
Expand Down
7 changes: 7 additions & 0 deletions crates/accuracy/src/estimators/univmon.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,22 @@
//! (OctoSketch aggregators fed promoted deltas) are not covered: the
//! executor rejects them.
use super::*;
#[cfg(test)]
use asap_types::ir::operator::AggIntent;

/// Layers `asap_sketchlib` builds: layer `i` hashes at seed index `i`, and
/// seed 19 picks the layers a key reaches.
#[cfg(test)]
pub(crate) const MAX_LAYERS: u8 =
asap_sketchlib::sketch_framework::univmon::UNIVMON_MAX_LAYERS as u8;
/// The most rows an L2 sketch is sized with: the deepest odd shape within
/// the 20 rows the executor builds ([`hash_independent`]), so the row median
/// is one row's estimate, and within the hash budget at the more than 20
/// columns an L2 guarantee needs (5 column bits: 19 · 6 = 114 ≤ 128).
#[cfg(test)]
const MAX_L2_ROWS: u32 = 19;
/// Each row of a layer reads its column and sign bits from one hash.
#[cfg(test)]
const HASH_BITS: u32 = 128;
/// The per-row failure probability the L2 guarantee fixes: a row misses by
/// more than `ε₂ F2` with probability at most `2q/ε₂² = 0.1`. Fixing it
Expand All @@ -50,6 +54,7 @@ const MAX_DISTINCT: &str = "max_distinct";

/// The shape baseline for readouts without a size model: the total (exact
/// whatever the shape) and entropy (uncertified).
#[cfg(test)]
const BASELINE: SketchParams = SketchParams::UnivMon {
heap_size: 256,
sketch_rows: 5,
Expand Down Expand Up @@ -148,6 +153,7 @@ fn l2_failure_probability(rows: u32) -> f64 {
/// clamped to the hash budget: with more rows than [`MAX_L2_ROWS`], or more
/// columns than `rows` leave bits for, the shape is the largest one built
/// and its guarantee misses the target.
#[cfg(test)]
fn l2_shape(eps: f64, delta: f64) -> (u32, u32) {
let rows = (1..=MAX_L2_ROWS)
.find(|&rows| l2_failure_probability(rows) <= delta)
Expand Down Expand Up @@ -347,6 +353,7 @@ pub(crate) fn with_failure_probability(params: &SketchParams, delta: f64) -> Ske
/// counter;
/// - total and entropy: the baseline shape (the total is exact; entropy
/// has no certified bound).
#[cfg(test)]
pub(super) fn size_params(intent: &AggIntent, eps: f64, delta: f64) -> SketchParams {
match intent {
AggIntent::FrequencyL2 { .. } => {
Expand Down
28 changes: 6 additions & 22 deletions crates/accuracy/src/sizing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use asap_types::ir::operator::AggIntent;
use asap_types::ir::properties::{AccuracyMetric, AccuracyTarget};
use asap_types::ir::scalar::ColumnRef;
use asap_types::ir::schema::{SketchAlgorithm, SketchParams, SketchStatistic};
use asap_types::ir::schema::SketchStatistic;

/// Confidence δ assumed when the target carries only an ε
/// (`AccuracyTarget::epsilon`): the (ε, δ)-parameterised sketches (CMS) need
Expand Down Expand Up @@ -67,7 +67,7 @@ fn target_in(target: &AccuracyTarget, metric: AccuracyMetric) -> AccuracyTarget
}

/// Resolve an [`AccuracyTarget`] into the `(eps, delta)` budget
/// `default_size_params` needs — one place this resolution happens, so
/// today's sizing needs — one place this resolution happens, so
/// nothing can drift apart on it.
///
/// An exact target degrades to the tightest parameters for a caller that
Expand All @@ -82,26 +82,10 @@ pub fn accuracy_budget(accuracy: &AccuracyTarget) -> (f64, f64) {
)
}

/// `asap-plan`'s built-in `SketchParams` sizing, keyed off the resolved
/// `(eps, delta)` accuracy budget.
///
/// Each formula inverts the sketch family's standard error bound to the
/// smallest parameter satisfying the target, clamped to the family's sane
/// range. A non-positive ε saturates to the clamp maximum (tightest
/// allowed).
#[allow(dead_code)] // Read by `test_support` until #1207 PR 1 replaces it with `parameter_list`.
pub(crate) fn default_size_params(
kind: SketchAlgorithm,
intent: &AggIntent,
eps: f64,
delta: f64,
) -> SketchParams {
crate::estimators::size_params(kind, intent, eps, delta)
}

#[cfg(test)]
mod tests {
use super::*;
use asap_types::ir::schema::{SketchAlgorithm, SketchParams};

/// CMS width is `⌈e/ε⌉` and depth `⌈ln(1/δ)⌉`, with an ε-only target
/// taking `DEFAULT_DELTA` (depth 5).
Expand All @@ -111,7 +95,7 @@ mod tests {
accuracy: AccuracyTarget::max_error(0.001).failure_probability(0.001),
};
assert_eq!(
default_size_params(SketchAlgorithm::Cms, &intent, 0.001, 0.001),
crate::estimators::size_params(SketchAlgorithm::Cms, &intent, 0.001, 0.001),
SketchParams::Cms {
width: 2719,
depth: 7
Expand All @@ -122,14 +106,14 @@ mod tests {
assert_eq!((eps, delta), (0.001, DEFAULT_DELTA));
let intent = AggIntent::Count { accuracy: target };
assert_eq!(
default_size_params(SketchAlgorithm::Cms, &intent, eps, delta),
crate::estimators::size_params(SketchAlgorithm::Cms, &intent, eps, delta),
SketchParams::Cms {
width: 2719,
depth: 5
}
);
assert_eq!(
default_size_params(SketchAlgorithm::Cms, &intent, 0.01, 0.01),
crate::estimators::size_params(SketchAlgorithm::Cms, &intent, 0.01, 0.01),
SketchParams::Cms {
width: 272,
depth: 5
Expand Down
Loading