From 7bc1a4625f89334a1304cea84cddeee2cfad3376 Mon Sep 17 00:00:00 2001 From: zz_y Date: Fri, 9 Oct 2026 21:37:11 +0000 Subject: [PATCH 1/2] kll: report asap_sketchlib's allocation as the lib rows' footprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The kll-percall lib rows reported a nominal 4·k·sizeof(T), which is not what asap_sketchlib::KLL allocates: init_internal boxes compute_max_capacity(k, m) slots once, plus a level index and a merge buffer of capacity k (5536 B at k = 50, not 1600 B). Report that, from the capacity replica the hydra-kll wrapper already had, now shared in the kll module. The oxide rows keep the nominal size: sketch_oxide's KLL grows as it fills. Closes #191. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FhcWJExZcmVqS6r6rEtjis --- sketch-bench/src/wrappers/hydra_kll/mod.rs | 42 ++--------- .../src/wrappers/hydra_kll/sketchlib.rs | 4 +- sketch-bench/src/wrappers/kll/mod.rs | 69 ++++++++++++++++++- sketch-bench/src/wrappers/kll/sketchlib.rs | 4 +- 4 files changed, 76 insertions(+), 43 deletions(-) diff --git a/sketch-bench/src/wrappers/hydra_kll/mod.rs b/sketch-bench/src/wrappers/hydra_kll/mod.rs index 206d4452..e6758577 100644 --- a/sketch-bench/src/wrappers/hydra_kll/mod.rs +++ b/sketch-bench/src/wrappers/hydra_kll/mod.rs @@ -11,39 +11,9 @@ pub(crate) use super::hydra_shared::{ check_grid, grid_overhead_bytes, label_columns, labels, new_hydra, query, update, }; -// Level count and decay of an `asap_sketchlib::KLL`. Both are private constants -// in the library, reproduced here because a cell's footprint is a function of -// them and the library exposes no accessor for its own capacity. -const KLL_MAX_LEVELS: usize = 61; - -const KLL_CAPACITY_DECAY: f64 = 2.0 / 3.0; - -/// The `m` the library's `init_kll` passes, its minimum level capacity, and the -/// floor it silently raises a smaller `k` to. Same cell as the `kll` rows, so -/// the bound is theirs: see [`crate::wrappers::kll::LIB_K_MIN`]. -const KLL_MIN_LEVEL: usize = crate::wrappers::kll::LIB_K_MIN as usize; - -/// The library clamps `k` to this before sizing, so a larger `k` buys nothing. -const KLL_MAX_CACHEABLE_K: usize = crate::wrappers::kll::LIB_K_MAX as usize; - -/// Retained slots one KLL cell allocates at construction — `KLL::init` boxes a -/// slice of this length once and never grows it. A line-for-line copy of the -/// library's private `compute_max_capacity` (unchanged from 0.2.2 to 0.3.0), so -/// a claim about the pinned version, not a bound. -fn kll_cell_slots(k: u32) -> usize { - // `init_internal` normalises before sizing: `m` floors `k`, and `k` is - // capped. Reproduced so an out-of-range `k` reports the footprint the - // library actually allocates. - let m = KLL_MIN_LEVEL; - let k = (k as usize).max(m).min(KLL_MAX_CACHEABLE_K) as f64; - let mut total = 0usize; - let mut scale = 1.0f64; - for _ in 0..KLL_MAX_LEVELS { - total += (k * scale).ceil().max(m as f64) as usize; - scale *= KLL_CAPACITY_DECAY; - } - total -} +pub(crate) use crate::wrappers::kll::{kll_lib_slots as kll_cell_slots, KLL_LIB_MAX_LEVELS}; +#[cfg(test)] +use crate::wrappers::kll::{KLL_LIB_MAX_CACHEABLE_K, KLL_LIB_MIN_LEVEL}; #[cfg(test)] mod tests { @@ -125,11 +95,11 @@ mod tests { assert_eq!(kll_cell_slots(200), 580 + 424); // Monotone in k, and never below the floor times the level count. assert!(kll_cell_slots(400) > kll_cell_slots(200)); - assert_eq!(kll_cell_slots(1), KLL_MIN_LEVEL * KLL_MAX_LEVELS); + assert_eq!(kll_cell_slots(1), KLL_LIB_MIN_LEVEL * KLL_LIB_MAX_LEVELS); // Past the library's clamp, a larger k buys no more slots. assert_eq!( - kll_cell_slots(KLL_MAX_CACHEABLE_K as u32), - kll_cell_slots(KLL_MAX_CACHEABLE_K as u32 + 5_000) + kll_cell_slots(KLL_LIB_MAX_CACHEABLE_K as u32), + kll_cell_slots(KLL_LIB_MAX_CACHEABLE_K as u32 + 5_000) ); } diff --git a/sketch-bench/src/wrappers/hydra_kll/sketchlib.rs b/sketch-bench/src/wrappers/hydra_kll/sketchlib.rs index 1c5060ae..47c5ae51 100644 --- a/sketch-bench/src/wrappers/hydra_kll/sketchlib.rs +++ b/sketch-bench/src/wrappers/hydra_kll/sketchlib.rs @@ -53,11 +53,11 @@ impl HydraKll { /// Retained slots per cell times the grid area, plus the level index every /// cell carries. Analytic because the cell allocates once — see -/// `kll_cell_slots` in this crate's `hydra_kll` module for the per-cell count. +/// `kll_lib_slots` in this crate's `kll` module for the per-cell count. pub fn memory_hydra_kll(sketch: &HydraKll) -> usize { let p = &sketch.params; let per_cell = kll_cell_slots(p.cell_k) * std::mem::size_of::() - + (KLL_MAX_LEVELS + 1) * std::mem::size_of::(); + + (KLL_LIB_MAX_LEVELS + 1) * std::mem::size_of::(); p.rows * p.cols * per_cell + grid_overhead_bytes(p.rows, p.cols) } diff --git a/sketch-bench/src/wrappers/kll/mod.rs b/sketch-bench/src/wrappers/kll/mod.rs index a9f15aee..f0679172 100644 --- a/sketch-bench/src/wrappers/kll/mod.rs +++ b/sketch-bench/src/wrappers/kll/mod.rs @@ -13,13 +13,59 @@ pub mod oxide; pub mod polars; pub mod sketchlib; -/// figure is `heap_bytes_net`, which the tracking allocator measures; this is -/// the derived upper bound beside it, and the two are meant to be compared. -/// One rule for all four rows, so the column answers one question. +/// A nominal footprint for the `oxide` rows, `4 · k` items: `sketch_oxide`'s +/// KLL grows its buffers as it fills, so there is no fixed allocation to +/// report. The `lib` rows report the allocation instead ([`kll_lib_bytes`]). fn kll_footprint(k: u32) -> usize { (k as usize) * std::mem::size_of::() * 4 } +// Level count and decay of an `asap_sketchlib::KLL`. Both are private constants +// in the library, reproduced here because its footprint is a function of them +// and the library exposes no accessor for its own capacity. +pub(crate) const KLL_LIB_MAX_LEVELS: usize = 61; + +const KLL_LIB_CAPACITY_DECAY: f64 = 2.0 / 3.0; + +/// The `m` the library's `init_kll` passes, its minimum level capacity, and the +/// floor it silently raises a smaller `k` to. +pub(crate) const KLL_LIB_MIN_LEVEL: usize = LIB_K_MIN as usize; + +/// The library clamps `k` to this before sizing, so a larger `k` buys nothing. +pub(crate) const KLL_LIB_MAX_CACHEABLE_K: usize = LIB_K_MAX as usize; + +/// The `k` the library sizes with: `init_internal` floors it at `m` and caps it. +fn kll_lib_k(k: u32) -> usize { + (k as usize).clamp(KLL_LIB_MIN_LEVEL, KLL_LIB_MAX_CACHEABLE_K) +} + +/// Retained slots an `asap_sketchlib::KLL` allocates at construction: +/// `KLL::init` boxes a slice of this length once and never grows it. A +/// line-for-line copy of the library's private `compute_max_capacity` +/// (unchanged from 0.2.2 to 0.3.0), so a claim about the pinned version, not a +/// bound. +pub(crate) fn kll_lib_slots(k: u32) -> usize { + let m = KLL_LIB_MIN_LEVEL; + let k = kll_lib_k(k) as f64; + let mut total = 0usize; + let mut scale = 1.0f64; + for _ in 0..KLL_LIB_MAX_LEVELS { + total += (k * scale).ceil().max(m as f64) as usize; + scale *= KLL_LIB_CAPACITY_DECAY; + } + total +} + +/// Bytes an `asap_sketchlib::KLL` allocates at construction, whatever it +/// holds later (`init_internal`): the retained-item slice, the level index, +/// and the merge buffer's capacity of `k` items. Independent of the item count, +/// so a window's sketch costs the same however many values it sees. +fn kll_lib_bytes(k: u32) -> usize { + kll_lib_slots(k) * std::mem::size_of::() + + (KLL_LIB_MAX_LEVELS + 1) * std::mem::size_of::() + + kll_lib_k(k) * std::mem::size_of::() +} + /// The range `asap_sketchlib::KLL::init` keeps a `k` in. Below the floor it /// raises `k` to `m`, above the ceiling it caps; both silently. Reproduced here /// so the two `lib` rows refuse instead, which is the only way the `k` in the @@ -48,3 +94,20 @@ fn query_cdf(table: &[(f64, f64)], phi: f64, min: f64, max: f64) -> f64 { let idx = table.partition_point(|(_, cum)| *cum < phi); table[idx.min(table.len() - 1)].0 } + +#[cfg(test)] +mod tests { + use super::*; + + /// The `lib` footprint is what `init_internal` allocates. At k = 269 that + /// is 9648 + 496 + 2152 = 12296 B, the figure `aqpbm-planeval` derives for + /// the same library independently. + #[test] + fn lib_footprint_is_the_library_allocation() { + assert_eq!(kll_lib_bytes::(269), 9648 + 496 + 2152); + // ceil(50 · (2/3)^i) is 50, 34, 23, 15, 10, then 56 levels at m = 8. + assert_eq!(kll_lib_slots(50), 132 + 56 * 8); + // Below the floor, the library sizes as k = m. + assert_eq!(kll_lib_bytes::(1), kll_lib_bytes::(LIB_K_MIN)); + } +} diff --git a/sketch-bench/src/wrappers/kll/sketchlib.rs b/sketch-bench/src/wrappers/kll/sketchlib.rs index 0bb7467c..851fabeb 100644 --- a/sketch-bench/src/wrappers/kll/sketchlib.rs +++ b/sketch-bench/src/wrappers/kll/sketchlib.rs @@ -48,7 +48,7 @@ pub fn memory_kll_lib_per_call(sketch: &KllLibPerCall) -> usize where T: asap_sketchlib::common::numerical::NumericalValue + QuantileValue, { - kll_footprint::(sketch.k) + kll_lib_bytes::(sketch.k) } pub struct KllLibCdf { @@ -73,7 +73,7 @@ pub fn memory_kll_lib_cdf(sketch: &KllLibCdf) -> usize where T: asap_sketchlib::common::numerical::NumericalValue + QuantileValue, { - kll_footprint::(sketch.k) + kll_lib_bytes::(sketch.k) } pub fn insert_kll_lib_per_call( From bcff1b553bcea11275c4b5caaa6d37ad08b7feb6 Mon Sep 17 00:00:00 2001 From: zz_y Date: Sat, 10 Oct 2026 02:11:54 +0000 Subject: [PATCH 2/2] kll: state what the lib footprint leaves out; pin m = 8 The lib footprint is the sketch's own allocation; say that the cdf row's prepared CDF table is not counted. KLL_LIB_MIN_LEVEL is the m init_kll passes, 8, with a const assertion that LIB_K_MIN agrees, instead of being defined through it. hydra-kll's footprint notes it omits each cell's merge buffer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FhcWJExZcmVqS6r6rEtjis --- sketch-bench/src/wrappers/hydra_kll/sketchlib.rs | 4 +++- sketch-bench/src/wrappers/kll/mod.rs | 14 ++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/sketch-bench/src/wrappers/hydra_kll/sketchlib.rs b/sketch-bench/src/wrappers/hydra_kll/sketchlib.rs index 47c5ae51..672de091 100644 --- a/sketch-bench/src/wrappers/hydra_kll/sketchlib.rs +++ b/sketch-bench/src/wrappers/hydra_kll/sketchlib.rs @@ -53,7 +53,9 @@ impl HydraKll { /// Retained slots per cell times the grid area, plus the level index every /// cell carries. Analytic because the cell allocates once — see -/// `kll_lib_slots` in this crate's `kll` module for the per-cell count. +/// `kll_lib_slots` in this crate's `kll` module for the per-cell count. Unlike +/// the `kll-percall` lib rows (`kll_lib_bytes`), each cell's merge buffer of +/// `cell_k` items is not counted. pub fn memory_hydra_kll(sketch: &HydraKll) -> usize { let p = &sketch.params; let per_cell = kll_cell_slots(p.cell_k) * std::mem::size_of::() diff --git a/sketch-bench/src/wrappers/kll/mod.rs b/sketch-bench/src/wrappers/kll/mod.rs index f0679172..92521c08 100644 --- a/sketch-bench/src/wrappers/kll/mod.rs +++ b/sketch-bench/src/wrappers/kll/mod.rs @@ -27,9 +27,13 @@ pub(crate) const KLL_LIB_MAX_LEVELS: usize = 61; const KLL_LIB_CAPACITY_DECAY: f64 = 2.0 / 3.0; -/// The `m` the library's `init_kll` passes, its minimum level capacity, and the -/// floor it silently raises a smaller `k` to. -pub(crate) const KLL_LIB_MIN_LEVEL: usize = LIB_K_MIN as usize; +/// The `m` the library's `init_kll` passes (`init(k, 8)`), its minimum level +/// capacity, and the floor it silently raises a smaller `k` to. +pub(crate) const KLL_LIB_MIN_LEVEL: usize = 8; + +// The lib rows refuse a `k` below the library's floor rather than let it be +// raised silently, so the two must agree. +const _: () = assert!(LIB_K_MIN as usize == KLL_LIB_MIN_LEVEL); /// The library clamps `k` to this before sizing, so a larger `k` buys nothing. pub(crate) const KLL_LIB_MAX_CACHEABLE_K: usize = LIB_K_MAX as usize; @@ -59,7 +63,9 @@ pub(crate) fn kll_lib_slots(k: u32) -> usize { /// Bytes an `asap_sketchlib::KLL` allocates at construction, whatever it /// holds later (`init_internal`): the retained-item slice, the level index, /// and the merge buffer's capacity of `k` items. Independent of the item count, -/// so a window's sketch costs the same however many values it sees. +/// so a window's sketch costs the same however many values it sees. The +/// sketch's own allocation only: the cdf row's prepared CDF table, built for +/// queries, is not counted (nor is the oxide cdf row's). fn kll_lib_bytes(k: u32) -> usize { kll_lib_slots(k) * std::mem::size_of::() + (KLL_LIB_MAX_LEVELS + 1) * std::mem::size_of::()