Skip to content
Closed
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
19 changes: 18 additions & 1 deletion aqpbm-cli/src/rows/materialise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,31 @@ pub(super) fn peel<T: ColumnItem>(
pub(super) fn peel_labeled<V: ColumnItem>(
description: &TableDescription,
table: GeneratedTable,
) -> Result<Rc<Vec<(String, V)>>, RunError> {
peel_joined(description, table, ';')
}

/// The same, joined with [`SERIES_SEPARATOR`], which `Hydra::update` does not
/// split on: each record is one key, the full label set, and not every subset.
pub(super) fn peel_series<V: ColumnItem>(
description: &TableDescription,
table: GeneratedTable,
) -> Result<Rc<Vec<(String, V)>>, RunError> {
peel_joined(description, table, SERIES_SEPARATOR)
}

fn peel_joined<V: ColumnItem>(
description: &TableDescription,
table: GeneratedTable,
separator: char,
) -> Result<Rc<Vec<(String, V)>>, RunError> {
let (labels, values) = label_columns::<V>(description, table)?;
let mut items = Vec::with_capacity(values.len());
for (row, value) in values.into_iter().enumerate() {
let mut key = String::new();
for (column, labels) in labels.iter().enumerate() {
if column > 0 {
key.push(';');
key.push(separator);
}
key.push_str(&labels[row]);
}
Expand Down
1 change: 1 addition & 0 deletions aqpbm-cli/src/rows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ use aqpbm_core::measure::{
use aqpbm_core::metrics::{Metric, Operation};
use aqpbm_core::{ColumnItem, GeneratedTable, TableDescription};
use sketch_bench::wrappers::frequency_value::FrequencyValue;
use sketch_bench::wrappers::hydra_kll::SERIES_SEPARATOR;

/// The label column every subpopulation comparator scores over.
const SCORED_LABEL_COLUMN: usize = 0;
Expand Down
5 changes: 3 additions & 2 deletions aqpbm-cli/src/rows/statistic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,11 @@ pub(super) fn subpop_quantile_row<V: ColumnItem + 'static>(
table,
want,
SubpopRankErrorGT {
group_columns: vec![SCORED_LABEL_COLUMN],
// Every label column: the sketch holds one key per full label set.
group_columns: (0..value_column(description)).collect(),
value_column: value_column(description),
},
peel_labeled::<V>,
peel_series::<V>,
insert,
insert_step,
query,
Expand Down
38 changes: 38 additions & 0 deletions configs/datagen/hydra_kll_disjoint_labels_100.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `hydra_columns_f64.yaml` with a separate alphabet per label column, so a key1
# value can never render the same as a key2 value (#74). The sketch keys each
# record by its full label set, so a group is a distinct (key1, key2) pair:
# key1's population times key2's ~50, about 100 here. The `_1k` and `_10k`
# specs differ only in key1's population (#165). The `polars` baseline scores
# zero, so any rank error is the sketch's. `export_rqe_optimizer_costs.sh`
# prices `hydra-kll` on all three.

column_num: 3
column_label: [key1, key2, value]
row_num: 200000

column_spec:
# key1: its population sets the group count.
- data_type: string
distribution:
kind: uniform
lower_bound: 0.0
upper_bound: 2.0
seed: 1
string: {alphabet: "abcdefghijklm"}

# key2: a second label, skewed, so groups are not all the same size.
- data_type: string
distribution:
kind: zipf
skewness: 1.1
population_size: 50
seed: 2
string: {alphabet: "nopqrstuvwxyz"}

# value: KLL orders these inside each group.
- data_type: f64
distribution:
kind: zipf
skewness: 1.2
population_size: 1000
seed: 3
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
# `hydra_columns_f64.yaml` with a separate alphabet per label column, so a key1
# value can never render the same as a key2 value (#74). On the shared alphabet
# a key1 query also picks up the matching key2 group, and that collision shows
# up as rank error; here the `polars` baseline scores zero and any rank error
# is the sketch's. `export_rqe_optimizer_costs.sh` prices `hydra-kll` on it.
# value can never render the same as a key2 value (#74). The sketch keys each
# record by its full label set, so a group is a distinct (key1, key2) pair:
# key1's population times key2's ~50, about 10k here. The `_100` and `_1k`
# specs differ only in key1's population (#165). The `polars` baseline scores
# zero, so any rank error is the sketch's. `export_rqe_optimizer_costs.sh`
# prices `hydra-kll` on all three.

column_num: 3
column_label: [key1, key2, value]
row_num: 200000

column_spec:
# key1: the column the comparator groups by.
# key1: its population sets the group count.
- data_type: string
distribution:
kind: uniform
Expand All @@ -18,7 +20,7 @@ column_spec:
seed: 1
string: {alphabet: "abcdefghijklm"}

# key2: a second label, skewed, so the subset fan-out is not uniform.
# key2: a second label, skewed, so groups are not all the same size.
- data_type: string
distribution:
kind: zipf
Expand All @@ -27,7 +29,7 @@ column_spec:
seed: 2
string: {alphabet: "nopqrstuvwxyz"}

# value: KLL orders these inside each subpopulation.
# value: KLL orders these inside each group.
- data_type: f64
distribution:
kind: zipf
Expand Down
38 changes: 38 additions & 0 deletions configs/datagen/hydra_kll_disjoint_labels_1k.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# `hydra_columns_f64.yaml` with a separate alphabet per label column, so a key1
# value can never render the same as a key2 value (#74). The sketch keys each
# record by its full label set, so a group is a distinct (key1, key2) pair:
# key1's population times key2's ~50, about 1k here. The `_100` and `_10k`
# specs differ only in key1's population (#165). The `polars` baseline scores
# zero, so any rank error is the sketch's. `export_rqe_optimizer_costs.sh`
# prices `hydra-kll` on all three.

column_num: 3
column_label: [key1, key2, value]
row_num: 200000

column_spec:
# key1: its population sets the group count.
- data_type: string
distribution:
kind: uniform
lower_bound: 0.0
upper_bound: 20.0
seed: 1
string: {alphabet: "abcdefghijklm"}

# key2: a second label, skewed, so groups are not all the same size.
- data_type: string
distribution:
kind: zipf
skewness: 1.1
population_size: 50
seed: 2
string: {alphabet: "nopqrstuvwxyz"}

# value: KLL orders these inside each group.
- data_type: f64
distribution:
kind: zipf
skewness: 1.2
population_size: 1000
seed: 3
11 changes: 5 additions & 6 deletions rqe-optimizer/src/candidates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,11 @@ fn subset_gcds(values: impl IntoIterator<Item = Millis>) -> BTreeSet<Millis> {
}

/// Whether a fixed-size sketch shared by all groups was measured holding at
/// least `groups` of them. Its accuracy degrades as groups grow, and the row
/// holds one measured point (`subpopulations`). Models a sketch keyed by the
/// joined `G` value alone, so it holds exactly `card(G)` subpopulations; the
/// current rows fan out to every label subset (sketch-bench#165).
// ponytail: one point, so larger groupings are never planned. Rows at more
// group counts (sketch-bench#143 S5) loosen this with no code change.
/// least `groups` of them. Its accuracy degrades as groups grow, so a row
/// measured at more groups than `G` has is conservative. The sketch is keyed
/// by the joined `G` value alone, so it holds exactly `card(G)` subpopulations,
/// and each config is measured at several group counts, one row each.
// ponytail: groupings past the largest measured count are never planned.
fn measured_at_group_count(config: &AtomicCostEntry, groups: u64) -> bool {
config
.query_accuracy
Expand Down
16 changes: 10 additions & 6 deletions scripts/export_rqe_optimizer_costs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ HYDRA_KLL_CONFIGS=(
"rows=5 cols=256 cell_k=200"
"rows=3 cols=256 cell_k=500"
)
# Suffixes of the hydra-kll specs: about this many groups each.
HYDRA_KLL_GROUP_COUNTS=(100 1k 10k)

# One row: a cost pass, an accuracy pass, then one merge-accuracy pass per
# merge count, over the same data. Arguments
Expand Down Expand Up @@ -151,19 +153,21 @@ point_spec exact-delta-set exact "" key-set configs/datagen/hydra_columns.yaml i
# and merge cost are not divided per group (`subpop-rank-error` reports no
# `groups_per_instance`). The optimizer prices them as one shared fixed-size
# sketch, not card(G) instances, and plans them only up to the `subpopulations`
# they were measured at. The keys fan out to every label subset, unlike the
# single-key sketch the optimizer models (#165). The spec gives each label its
# own alphabet, so rank error is the sketch's, not label collisions' (#74).
# they were measured at, so every config runs at ~100, ~1k and ~10k groups
# (#165). Each record is one key, its full label set. The specs give each label
# its own alphabet, so rank error is the sketch's, not label collisions' (#74).
echo "==> Quantiles: KLL, DDSketch, HydraKLL" >&2
for k in "${KLL_KS[@]}"; do
point kll-percall "k=$k" rank-error
done
for alpha in "${DD_ALPHAS[@]}"; do
point dd "alpha=$alpha" rank-error
done
for config in "${HYDRA_KLL_CONFIGS[@]}"; do
point_spec hydra-kll lib "$config" subpop-rank-error \
configs/datagen/hydra_kll_disjoint_labels.yaml f64
for groups in "${HYDRA_KLL_GROUP_COUNTS[@]}"; do
for config in "${HYDRA_KLL_CONFIGS[@]}"; do
point_spec hydra-kll lib "$config" subpop-rank-error \
"configs/datagen/hydra_kll_disjoint_labels_$groups.yaml" f64
done
done

echo "==> Cardinality: HLL and UnivMon" >&2
Expand Down
39 changes: 28 additions & 11 deletions sketch-bench/src/wrappers/hydra_kll/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Hydra wrappers — `asap_sketchlib::Hydra` (Manousis et al., VLDB 2022). Their
//! item is a **record**; insert fans out into every non-empty label subset, so
//! `d` labels cost `2^d - 1` cells and throughput is records per second.
//! Hydra over KLL cells — `asap_sketchlib::Hydra` (Manousis et al., VLDB 2022).
//! The item is a **record** whose key is its full label set joined with
//! [`SERIES_SEPARATOR`]. `Hydra::update` only fans out across `;`, so each
//! record is one insert and the sketch holds one subpopulation per label set.

use crate::params::*;

Expand All @@ -9,6 +10,10 @@ pub mod sketchlib;

pub(crate) use super::hydra_shared::{check_grid, grid_overhead_bytes, labels};

/// Joins a record's label values into its key. Not `;`, which `Hydra::update`
/// splits on to insert every label subset.
pub const SERIES_SEPARATOR: char = '\u{1f}';

// 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.
Expand Down Expand Up @@ -59,8 +64,8 @@ mod tests {
.expect("canonical dimensions build")
}

fn frecord(key: &str, value: f64) -> (String, f64) {
(key.to_string(), value)
fn frecord(labels: &[&str], value: f64) -> (String, f64) {
(labels.join(&SERIES_SEPARATOR.to_string()), value)
}

fn fed_kll(sketch: &mut HydraKll, r: &(String, f64)) {
Expand All @@ -80,29 +85,41 @@ mod tests {
fn kll_quantiles_are_taken_inside_the_group() {
let mut h = built_kll();
for v in 1..=101 {
fed_kll(&mut h, &frecord("a;x", v as f64));
fed_kll(&mut h, &frecord(&["a", "x"], v as f64));
}
for _ in 0..500 {
fed_kll(&mut h, &frecord("b;x", 10_000.0));
fed_kll(&mut h, &frecord(&["b", "x"], 10_000.0));
}
let median = h.estimate_subpop_quantile(&["a"], 0.5);
let median = h.estimate_subpop_quantile(&["a", "x"], 0.5);
assert!(
(1.0..=101.0).contains(&median),
"group `a` spans 1..=101, median estimated {median}"
);
}

/// A record lands in its full label set's cells only, not in any subset's.
/// An untouched KLL cell answers 0.0, so `a` alone must see nothing.
#[test]
fn kll_record_is_not_inserted_under_its_label_subsets() {
let mut h = built_kll();
for v in 1..=101 {
fed_kll(&mut h, &frecord(&["a", "x"], v as f64));
}
assert_eq!(h.estimate_subpop_quantile(&["a"], 0.5), 0.0);
assert_eq!(h.estimate_subpop_quantile(&["x"], 0.5), 0.0);
}

/// `k` is well past the group size here, so the cell retains everything and
/// the answer is exact. Pinned because it is what makes a rank error at a
/// larger dataset attributable to compaction and not to the grid.
#[test]
fn kll_is_exact_below_k() {
let mut h = built_kll();
for v in 1..=101 {
fed_kll(&mut h, &frecord("a;x", v as f64));
fed_kll(&mut h, &frecord(&["a", "x"], v as f64));
}
assert_eq!(h.estimate_subpop_quantile(&["a"], 0.0), 1.0);
assert_eq!(h.estimate_subpop_quantile(&["a"], 1.0), 101.0);
assert_eq!(h.estimate_subpop_quantile(&["a", "x"], 0.0), 1.0);
assert_eq!(h.estimate_subpop_quantile(&["a", "x"], 1.0), 101.0);
}

/// The library allocates a cell's retained slots once, so the footprint is
Expand Down
7 changes: 2 additions & 5 deletions sketch-bench/src/wrappers/hydra_kll/polars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ impl<T: QuantileValue + PolarsColumnItem> PolarsSubpopQuantile<T> {
/// rank interval contains `phi * n`, which is what the rank-error
/// comparator scores against, so an exact answer scores zero.
pub fn estimate_subpop_quantile(&self, labels: &[&str], phi: f64) -> f64 {
let Some(values) = self.sorted.get(&labels.join(";")) else {
let Some(values) = self.sorted.get(&labels.join(&SERIES_SEPARATOR.to_string())) else {
return f64::NAN;
};
if values.is_empty() {
Expand Down Expand Up @@ -75,10 +75,7 @@ impl<T: QuantileValue + PolarsColumnItem> PolarsSubpopQuantile<T> {
/// The exact answer, over the stream as buffered. Its own step because the
/// runner times it as `prepare`: this row's cost is the pass, not the ask.
fn finalize(&mut self) {
let (mut keys, mut values) = (Vec::new(), Vec::new());
for r in &self.buf {
fan_out(&r.0, &r.1, &mut keys, &mut values);
}
let (keys, values): (Vec<String>, Vec<T>) = self.buf.iter().cloned().unzip();
if keys.is_empty() {
return;
}
Expand Down
4 changes: 2 additions & 2 deletions sketch-bench/src/wrappers/hydra_kll/sketchlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ impl HydraKll {
/// answers the inverse question.
#[inline]
pub fn estimate_subpop_quantile(&self, labels: &[&str], phi: f64) -> f64 {
self.inner
.query_key(labels.to_vec(), &HydraQuery::Quantile(phi))
let key = labels.join(&SERIES_SEPARATOR.to_string());
self.inner.query_key(vec![&key], &HydraQuery::Quantile(phi))
}
}

Expand Down
Loading