Skip to content

feat(asap-physical-operators): KMV distinct-count kernel (#697) - #704

Merged
zzylol merged 2 commits into
mainfrom
feat/kmv-kernel-697
Oct 15, 2026
Merged

zzylol merged 2 commits into
mainfrom
feat/kmv-kernel-697

Conversation

@zzylol

@zzylol zzylol commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Closes #697.

Why

Pass 1 offers KMV as a distinct-count candidate and Stage 3 prices it, but asap-physical-operators had no KMV kernel, so a Stage 3-selected plan that uses KMV could not execute. #694 plans to make KMV the certified distinct-count summary at realistic scale, so the executor needs it.

What

  • New KmvAccumulator summary kernel (summary_kernels/kmv.rs) that wraps asap_sketchlib::KMV (no local copy): build over sample values, merge, and the Cardinality estimate.
  • KMV is registered wherever HLL is: summary-kernel validation (k >= 2), native-family validation, sketch-evaluation validation, stored-state validation, and the accumulator factory.
  • asap_sketchlib is now built with its experimental feature (KMV is gated behind it) at the existing pin e6b0314.

How

Each sample value is hashed by its bits (-0.0 folded into 0.0, NaN skipped), the same convention as the HLL kernel, through the library's shared CANONICAL_HASH_SEED. KMV::merge keeps the k smallest distinct hashes of both states, so a merged state equals the union build. A merge across different k is rejected. The kernel answers Cardinality and a bare PointCount (both read the distinct count); other statistics return an error.

Key code interfaces added / modified

crates/asap-physical-operators/src/summary_kernels/kmv.rs (new), mod.rs

/// The KMV (k minimum values) distinct-count kernel module.
pub mod kmv;          // new
/// Re-exports `KmvAccumulator` and `KMV_MIN_K` from `summary_kernels`.
pub use kmv::*;       // new

/// The smallest `k` the estimate `(k - 1) / U(k)` is defined for; smaller `k` is rejected at validation.
pub const KMV_MIN_K: u32 = 2;   // smallest k the executor accepts

/// The executor's KMV summary state; merging two equals one build over the union.
pub struct KmvAccumulator {
    /// The library sketch holding the `k` smallest distinct hashes.
    pub inner: KMV,   // asap_sketchlib::KMV
}

impl KmvAccumulator {
    /// An empty KMV keeping the `k` smallest hashes.
    pub fn new(k: u32) -> Self;
    /// Observes one sample value, hashed by its bits.
    pub fn insert_sample(&mut self, value: f64);   // NaN skipped, -0.0 hashed as 0.0
}

/// KMV as a summary state the executor merges and reads (new impl).
impl AggregateCore for KmvAccumulator {
    /// A boxed copy of this state, used when a state is shared or snapshotted.
    fn clone_boxed_core(&self) -> Box<dyn AggregateCore>;   // new
    /// This state as `Any`, for downcasting to `KmvAccumulator` (merge, `validate_state`).
    fn as_any(&self) -> &dyn std::any::Any;                 // new
    /// Mutable `Any` view, for in-place downcasts.
    fn as_any_mut(&mut self) -> &mut dyn std::any::Any;     // new
    /// Merges with another KMV state; errors (bad input) on a non-KMV state or a different `k`.
    fn merge_with(&self, other: &dyn AggregateCore) -> Result<Box<dyn AggregateCore>, KernelError>;  // new: same k only
    /// The distinct count (a bare `PointCount` reads it too); other readouts are errors (bad plan).
    fn estimate(&self, query: &SketchStatistic) -> Result<f64, KernelError>;  // new: Cardinality, or PointCount { value: None, .. }
    /// The struct size plus 8 bytes per kept hash; used for memory reservations.
    fn approx_memory_bytes(&self) -> usize;                 // new
    // is_empty, shared_state: trait defaults (false, None)
}
/// A KMV state is its own updater: it observes unkeyed sample values (new impl).
impl AccumulatorUpdater for KmvAccumulator {
    /// `false`: KMV counts sample values, not keyed items.
    fn is_keyed(&self) -> bool;                                                   // new
    /// Same as `approx_memory_bytes`; the executor reserves it per state.
    fn memory_usage_bytes(&self) -> usize;                                        // new
    /// Observes one sample value through `insert_sample`; the timestamp is ignored.
    fn update_single(&mut self, value: f64, timestamp_ms: i64);                   // new
    /// Same as `update_single`; the key is ignored.
    fn update_keyed(&mut self, key: &KeyByLabelValues, value: f64, timestamp_ms: i64);   // new
    /// Returns the built state and resets this one to an empty KMV with the same `k`.
    fn take_accumulator(&mut self) -> Box<dyn AggregateCore>;                     // new
    /// Returns a copy of the built state; this one keeps building.
    fn snapshot_accumulator(&self) -> Box<dyn AggregateCore>;                     // new
    /// Returns the updater itself as the state.
    fn into_accumulator(self: Box<Self>) -> Box<dyn AggregateCore>;               // new
    /// Empties the state, keeping `k`.
    fn reset(&mut self);                                                          // new
    // validate_single_input: trait default (finite values only)
}

crates/asap-physical-operators/src/capability.rs, summary_kernels/factory.rs, values.rs

/// Checks, before a plan is accepted, that the executor can build this
/// summary; the same contract `create_planner_accumulator` uses.
pub fn validate_summary_kernel(
    family: &SummaryFamilyType,
    input: &SummaryUpdate,
    grouping: &GroupingStrategy,
) -> Result<(), String>;   // accepts (Kmv, SketchParams::Kmv { k }) with k >= KMV_MIN_K
/// Errors unless the executor has a native kernel for `family`.
pub fn validate_native_family(family: &SummaryFamilyType) -> Result<(), Error>;   // Kmv is now native
/// Errors unless the executor can read `query` from `family` directly.
pub fn validate_sketch_evaluation(
    family: &SummaryFamilyType,
    query: &SketchStatistic,
) -> Result<(), Error>;   // (Kmv, Cardinality) answers; other Kmv readouts only as a bare count

/// Builds the kernel a Planner `SummaryAgg` declares; unsupported payloads are errors.
pub fn create_planner_accumulator(
    family: &SummaryFamilyType,
    input: &planner_types::post_asap::SummaryUpdate,
    grouping: &planner_types::post_asap::GroupingStrategy,
) -> Result<Box<dyn AccumulatorUpdater>, String>;   // (Kmv, SketchParams::Kmv { k }) => KmvAccumulator::new(k)

/// Checks a stored state matches its declared family and parameters before it is used.
fn validate_state(family: &SummaryFamilyType, state: &dyn AggregateCore) -> Result<(), Error>;   // checks a KMV state's k

crates/asap-physical-operators/Cargo.toml

# The pinned sketch library for the executor's kernels.
asap_sketchlib = { git = "https://github.com/ProjectASAP/asap_sketchlib", rev = "e6b03142628a3109b6482ebe8512bff7e75f5b35", features = ["experimental"] }  # was: no features; KMV needs "experimental"

Usage, end to end (Example 2's Q1 through its PromQL stand-in; the numbers come from stage3_plan_selection_selected_kmv_plan_executes_end_to_end, run at 2357ecec):

// 1. What the user writes. `workload` is Example 2: Q1 counts distinct source IPs over the
//    last minute every 10 s, within ε = 0.02 at δ = 0.01 (Q2 and Q3, entropy and L2, ride
//    along). Every stage below reads it.
let workload = standin_workload();   // Q1: "distinct_over_time(flows_src_ip[1m])",
                                     // requirements.accuracy: Explicit(epsilon_delta(0.02, 0.01))
// `entries` holds the three normalized query entries; targets and recurrence are read from them.
let entries: Vec<_> = workload.query_workload.entries().collect();
// `data` holds the source facts (10M distinct source IPs, 1M samples per 15 s); Stage 3
// prices row counts from it.
let data = workload.data_workload.as_ref().unwrap();

// 2. Stages 0–2. The frontend lowers Q1 to a distinct count. Pass 1 offers exact, HLL, UnivMon
//    and KMV for it and sizes KMV from ε (k = 250,002 here). Stage 2 makes one physical
//    candidate per logical one. `run` holds all of them; step 3 picks from it.
let run = run_stages(&workload, lower_promql(&workload));

// 3. Stage 3. `kmv` holds the 3 physical candidates that answer Q1 with KMV and Q2, Q3 exactly.
//    Over all candidates Stage 3 selects the all-exact P1 (8.64 cost_per_second), so this
//    restriction is what puts a KMV plan in front of the executor.
let kmv: Vec<PhysicalCandidate> = /* candidates whose Q1 option is "Kmv" and Q2, Q3 "exact" */;
// `targets` holds one AccuracyTarget per query, from each entry's requirements.accuracy:
// [ε 0.02 δ 0.01, ε 0.05 δ 0.01, ε 0.01 δ 0.01]. Stage 3 checks each guarantee against it.
let targets: Vec<_> = entries.iter().map(|e| Some(e.requirements.accuracy.target())).collect();
// `recurrence` holds each query's 10 s repetition; Stage 3 turns cost per run into cost per second.
let recurrence: Vec<_> = entries.iter().map(|e| e.recurrence.clone()).collect();
// `selection` holds the chosen candidate, each valid candidate's cost and each rejection.
let selection = select_plan(&kmv, &targets, &recurrence, data, PlanningModels::builtin())?;
// -> selected "P25" at 9.04 cost_per_second; P26 and P27 tie at 9.04 and lose on enumeration order.
// `p` is P25's physical DAG. Its node 2 is Q1's SummaryAgg { Kmv, SketchParams::Kmv { k: 250002 } }.
let p = run.physical(&selection.selected);

// 4. The executor compiles Q1's sub-DAG. `q1` is Q1's root node id; `inputs` maps the
//    time_range node to a bounded input contract for flows_src_ip. Compiling node 2 calls this
//    PR's validate_summary_kernel (Kmv with k >= KMV_MIN_K = 2 is accepted) and
//    create_planner_accumulator, which returns KmvAccumulator::new(250_002).
//    `plan` is the compiled operator graph that step 5 runs.
let q1 = p.query_roots[0] as u64;
let plan = compile(p.dag(), inputs, &[q1])?;

// 5. Run it. `batches` holds 1,000 flow records over 50 s with 300 distinct source IPs. Each
//    sample goes through KmvAccumulator::insert_sample (hashed by its bits). Q1's estimate node
//    reads estimate(&SketchStatistic::Cardinality). `result` holds Q1's output: what the user gets.
let result = execute(&plan, batches, Scope::Query { evaluation_time_ms: 55_000, revision: 1 });
// -> [300.0]: fewer than k distinct values, so the KMV estimate is exact.

Before this PR

A plan with a KMV summary failed when it was bound to a kernel.

After this PR

A Stage 3-selected KMV plan for Example 2 Q1 executes end to end and returns the exact distinct count when the input has fewer than k distinct values. Raw-sample precompute checks KMV against its kernel.

Verification

  • cargo nextest run -p asap-physical-operators: 200 passed, 3 skipped. New kernel tests: fewer-than-k distinct items are counted exactly; many distinct items are estimated within the KMV error; merge equals the union build; different-k merges and quantile queries are rejected; -0.0 equals 0.0 and NaN is not an item.
  • cargo nextest run -p asap-integration-tests: 245 passed, 33 skipped (already #[ignore]d on main). New: stage3_plan_selection_selected_kmv_plan_executes_end_to_end.
  • cargo clippy --workspace --all-targets --all-features --locked -- -D warnings and cargo fmt --all -- --check: clean.
  • Screenshots and measurements: not applicable.

Limitations and follow-ups

@zzylol
zzylol force-pushed the feat/kmv-kernel-697 branch from d48f391 to 7fe09f4 Compare October 15, 2026 04:30
Wrap asap_sketchlib's KMV (enabling its experimental feature at the pinned
rev): build over sample values hashed with CANONICAL_HASH_SEED, merge keeping
the k smallest distinct hashes, and the distinct-count estimate. Register it
with the summary-kernel and native-family capability checks, the sketch
evaluation check, stored-state validation and the accumulator factory, the
same way as HLL.
…ple 2 Q1

Raw-sample precompute now checks KMV against its kernel, and Stage 3 over
Example 2's KMV candidates selects a plan whose Q1 executes and returns the
exact distinct count below k.
@zzylol
zzylol force-pushed the feat/kmv-kernel-697 branch from 7fe09f4 to a723dd8 Compare October 15, 2026 05:30
@zzylol
zzylol merged commit 2357ece into main Oct 15, 2026
3 of 4 checks passed
@zzylol zzylol added binding Logical-to-physical binding and implementation selection enhancement New feature or request labels Oct 16, 2026
@zzylol
zzylol deleted the feat/kmv-kernel-697 branch November 3, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

binding Logical-to-physical binding and implementation selection enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Executor has no KMV kernel although Pass 1 offers KMV for distinct counts

1 participant