Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
67686d9
Add Pareto distribution to datagen and --dataset pareto
zzylol Sep 29, 2026
a12801d
Add saturation study script
zzylol Sep 29, 2026
c72e9d4
Widen the saturation band by seed standard error
zzylol Sep 29, 2026
a18ab4b
Document the saturation study and add its plotting scripts
zzylol Sep 29, 2026
4b6e19c
Allow --dataset pareto --dtype i64 (floored draws)
zzylol Sep 29, 2026
61ff74e
Saturation study: config grid, exact-baseline crossover, phase split
zzylol Sep 29, 2026
29aace0
Saturation study: --resume and --cost-rows
zzylol Sep 29, 2026
51cb65a
Saturation plots: draw one config per sketch from grid output
zzylol Sep 29, 2026
36b10a5
Saturation study: config-grid and 1e9 results
zzylol Sep 30, 2026
3253393
Saturation study: recommend a sketch config per query from worst-case…
zzylol Sep 30, 2026
5109343
Add saturation study figures with reading guides
zzylol Oct 1, 2026
3b4236e
Saturation study: grid cost results and a per-query crossover N*
zzylol Oct 1, 2026
c0780ad
Add the grid crossover and recommendation tables
zzylol Oct 1, 2026
05115ed
Document the saturation study's conclusions and the math behind them
zzylol Oct 2, 2026
2c9a83e
Update merge conclusions with the full top-k and KLL grids
zzylol Oct 2, 2026
19c8419
Key saved cost records and keep out-of-grid curves on resume
milindsrivastava1997 Oct 4, 2026
19fd917
Cancel queued accuracy runs when one fails
milindsrivastava1997 Oct 4, 2026
e4fa66c
Match cardinality as a number in point_key
milindsrivastava1997 Oct 4, 2026
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ In this case, `hll` takes a `lg_k` which describes the size of register list.

**`--zipf-s`:** Some distribution requires parameter, like `zipf-s`.
`zipf-s` means skewness, a parameter for zipf distribution.
`--dataset pareto` instead takes `--pareto-alpha` (shape) and `--pareto-scale`
(minimum value, default 1.0); it is unbounded, so it needs `--dtype f64` (or
`i64`, which floors each draw) and ignores `--cardinality`.

**`--cardinality`:** Another parameter required by zipf distribution.
`cardinality` means the size of key-space of this synthesized zipf dataset.
Expand Down
12 changes: 10 additions & 2 deletions aqpbm-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ pub struct SketchbenchArgs {
/// A *list* of specs is a multi-column stream, which only `hydra-*` targets take.
#[arg(long, help_heading = "Dataset")]
pub spec: Option<String>,
/// Inline shape: "uniform" or "zipf". Ignored when `--spec` is
/// Inline shape: "uniform", "zipf" or "pareto". Ignored when `--spec` is
/// set. Required unless `--spec` or `--list-impls` is given: a run either
/// names its shape or names its file, never falls back to one unasked.
#[arg(
Expand All @@ -106,7 +106,8 @@ pub struct SketchbenchArgs {
help_heading = "Dataset"
)]
pub size: Option<usize>,
/// Cardinality (uniform: max key; zipf: key-space size). Required unless
/// Cardinality (uniform: max key; zipf: key-space size; pareto: ignored,
/// it is unbounded). Required unless
/// `--spec` or `--list-impls` is given.
#[arg(
long,
Expand All @@ -118,6 +119,13 @@ pub struct SketchbenchArgs {
/// otherwise.
#[arg(long, required_if_eq("dataset", "zipf"), help_heading = "Dataset")]
pub zipf_s: Option<f64>,
/// Pareto shape `alpha`. Required when `--dataset pareto` is given;
/// ignored otherwise.
#[arg(long, required_if_eq("dataset", "pareto"), help_heading = "Dataset")]
pub pareto_alpha: Option<f64>,
/// Pareto scale (the minimum value). Only read by `--dataset pareto`.
#[arg(long, default_value_t = 1.0, help_heading = "Dataset")]
pub pareto_scale: f64,
/// Item type the value column is generated at: `i64`, `u64`, `f64` or
/// `string`
/// Required unless `--list-impls` is given: read regardless of `--spec`.
Expand Down
24 changes: 21 additions & 3 deletions aqpbm-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ use aqpbm_core::measure::MeasureConfig;
use aqpbm_core::measure::MIN_MERGE_SHARDS;
use aqpbm_core::metrics::{Metric, MetricsMask, Operation, OperationMask};
use aqpbm_datagen::{
ColumnSpec, DataDistribution, GeneratedTable, StringOpts, TableDescription, UniformParameter,
ZipfParameter, RULE_NONE,
ColumnSpec, DataDistribution, GeneratedTable, ParetoParameter, StringOpts, TableDescription,
UniformParameter, ZipfParameter, RULE_NONE,
};
use clap::Parser;
use sketch_bench::params::ParamSet;
Expand Down Expand Up @@ -205,7 +205,25 @@ fn dataset_spec(args: &SketchbenchArgs) -> Result<InputDataSetSpec> {
population_size: cardinality,
seed: args.seed,
}),
other => bail!("unknown dataset shape: {other} (expected uniform|zipf, or use --spec)"),
// Unbounded and continuous, so `--cardinality` has nothing to name. An
// f64 column keeps the draws as drawn; an i64 column floors them (every
// draw is >= scale > 0, so the renderer's truncation is a floor), which
// is what the i64-only exact quantile baseline needs.
"pareto" => {
if !matches!(args.dtype.as_deref(), Some("f64" | "i64")) {
bail!("--dataset pareto needs --dtype f64 or i64 (i64 floors each draw)");
}
DataDistribution::Pareto(ParetoParameter {
alpha: args
.pareto_alpha
.expect("clap requires --pareto-alpha when --dataset pareto"),
scale: args.pareto_scale,
seed: args.seed,
})
}
other => {
bail!("unknown dataset shape: {other} (expected uniform|zipf|pareto, or use --spec)")
}
};
// Only the rows that ingest text read this. Left `None` at the defaults
// so a run that did not ask for the axis keeps the descriptor — and so
Expand Down
79 changes: 79 additions & 0 deletions aqpbm-core/schema/merged_record.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,38 @@
],
"x-schema-version": 5,
"$defs": {
"BurstSpec": {
"type": "object",
"properties": {
"burst_intervals": {
"type": "integer",
"format": "uint",
"minimum": 0
},
"extra_fraction": {
"description": "Additional rows per selected interval, relative to its base size.",
"type": "number",
"format": "double"
},
"interval_rows": {
"type": "integer",
"format": "uint",
"minimum": 0
},
"seed": {
"type": "integer",
"format": "uint64",
"minimum": 0
}
},
"additionalProperties": false,
"required": [
"interval_rows",
"burst_intervals",
"extra_fraction",
"seed"
]
},
"ColumnSpec": {
"description": "One column of a [`crate::TableDescription`].",
"type": "object",
Expand Down Expand Up @@ -453,6 +485,19 @@
"required": [
"kind"
]
},
{
"type": "object",
"properties": {
"kind": {
"type": "string",
"const": "pareto"
}
},
"$ref": "#/$defs/ParetoParameter",
"required": [
"kind"
]
}
]
},
Expand Down Expand Up @@ -607,6 +652,30 @@
"seed"
]
},
"ParetoParameter": {
"description": "Pareto: heavy-tailed, `P(X > x) = (scale / x)^alpha` for `x >= scale`.\nUnbounded above, so like Normal it has no domain.",
"type": "object",
"properties": {
"alpha": {
"type": "number",
"format": "double"
},
"scale": {
"type": "number",
"format": "double"
},
"seed": {
"type": "integer",
"format": "uint64",
"minimum": 0
}
},
"required": [
"alpha",
"scale",
"seed"
]
},
"RunStats": {
"description": "Aggregate across N samples: mean / stddev / optional 95% CI, plus the raw\nsamples the aggregate was computed from.",
"type": "object",
Expand Down Expand Up @@ -781,6 +850,16 @@
"synthetic": {
"type": "object",
"properties": {
"burst": {
"anyOf": [
{
"$ref": "#/$defs/BurstSpec"
},
{
"type": "null"
}
]
},
"description": {
"$ref": "#/$defs/TableDescription"
}
Expand Down
7 changes: 7 additions & 0 deletions aqpbm-core/src/erp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ fn distribution_descriptor(workload: &WorkloadDescription) -> serde_json::Value
("standard_deviation".into(), value.standard_deviation),
]),
),
DataDistribution::Pareto(value) => (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This Pareto arm never runs. The cardinality lookup just before the match (column.cardinality.or_else(|| column.distribution.domain()...)?) returns None for Pareto, so the function exits first: domain() is None for Pareto, --dataset pareto sets cardinality: None, and ColumnSpec validation rejects any cardinality on an unbounded distribution. As a result, every Pareto record (including the KLL/DDSketch cost runs) is exported with no erp_shape, and nothing warns about it. Fix: give unbounded families a path that doesn't need a cardinality, or remove this arm.

"pareto",
BTreeMap::from([
("alpha".into(), value.alpha),
("scale".into(), value.scale),
]),
),
};
Some(ErpDataShape {
cardinality,
Expand Down
4 changes: 2 additions & 2 deletions aqpbm-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ pub mod metrics;
pub use accuracy::GroundTruth;
pub use aqpbm_datagen::{
ColumnData, ColumnItem, ColumnSpec, DataDistribution, DataGenError, GeneratedTable,
NormalParameter, StringOpts, TableDescription, UniformParameter, ZipfParameter,
RULE_MONOTONIC_INCREASE, RULE_NONE,
NormalParameter, ParetoParameter, StringOpts, TableDescription, UniformParameter,
ZipfParameter, RULE_MONOTONIC_INCREASE, RULE_NONE,
};
pub use atomic_costs::{reduce_all, reduce_one, AtomicCostEntry, AtomicCostTable, SkipReason};
pub use benchmark_result::{
Expand Down
33 changes: 30 additions & 3 deletions aqpbm-datagen/src/dist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! fixes it — so two columns are independent unless given the same one. Every
//! distribution draws an `f64`, so [`crate::column`]'s pipeline is written once.

use rand_distr::{Distribution as _, Normal, Uniform, Zipf};
use rand_distr::{Distribution as _, Normal, Pareto, Uniform, Zipf};
use rand_xoshiro::Xoshiro256PlusPlus;
use serde::{Deserialize, Serialize};

Expand Down Expand Up @@ -40,6 +40,16 @@ pub struct NormalParameter {
pub seed: u64,
}

/// Pareto: heavy-tailed, `P(X > x) = (scale / x)^alpha` for `x >= scale`.
/// Unbounded above, so like Normal it has no domain.
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParetoParameter {
pub alpha: f64,
pub scale: f64,
pub seed: u64,
}

/// A column's distribution. Serialises internally tagged, so a spec file reads
/// `distribution: {kind: zipf, skewness: 1.1, population_size: 200, seed: 1}`.
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
Expand All @@ -49,6 +59,7 @@ pub enum DataDistribution {
Zipf(ZipfParameter),
Uniform(UniformParameter),
Normal(NormalParameter),
Pareto(ParetoParameter),
}

/// The half-open span a bounded distribution draws over: the lowest value it can
Expand All @@ -68,6 +79,7 @@ impl DataDistribution {
DataDistribution::Zipf(_) => "zipf",
DataDistribution::Uniform(_) => "uniform",
DataDistribution::Normal(_) => "normal",
DataDistribution::Pareto(_) => "pareto",
}
}

Expand All @@ -77,10 +89,11 @@ impl DataDistribution {
DataDistribution::Zipf(p) => p.seed,
DataDistribution::Uniform(p) => p.seed,
DataDistribution::Normal(p) => p.seed,
DataDistribution::Pareto(p) => p.seed,
}
}

/// The bounded span, or `None` for Normal — which is unbounded, so a
/// The bounded span, or `None` for Normal and Pareto — which are unbounded, so a
/// `cardinality` over it would be a claim this crate cannot keep.
pub fn domain(&self) -> Option<Domain> {
match self {
Expand All @@ -93,7 +106,7 @@ impl DataDistribution {
lower: p.lower_bound,
size: (p.upper_bound - p.lower_bound) as u64,
}),
DataDistribution::Normal(_) => None,
DataDistribution::Normal(_) | DataDistribution::Pareto(_) => None,
}
}

Expand Down Expand Up @@ -139,6 +152,18 @@ impl DataDistribution {
.map_err(|e| bad(format!("normal: {e}")))?,
))
}
DataDistribution::Pareto(p) => {
if !p.alpha.is_finite() || p.alpha <= 0.0 || !p.scale.is_finite() || p.scale <= 0.0
{
return Err(bad(format!(
"pareto: alpha and scale must be finite and > 0, got alpha {} scale {}",
p.alpha, p.scale
)));
}
Ok(Sampler::Pareto(
Pareto::new(p.scale, p.alpha).map_err(|e| bad(format!("pareto: {e}")))?,
))
}
}
}
}
Expand All @@ -149,6 +174,7 @@ pub enum Sampler {
Zipf(Zipf<f64>),
Uniform(Uniform<f64>),
Normal(Normal<f64>),
Pareto(Pareto<f64>),
}

impl Sampler {
Expand All @@ -158,6 +184,7 @@ impl Sampler {
Sampler::Zipf(d) => d.sample(rng),
Sampler::Uniform(d) => d.sample(rng),
Sampler::Normal(d) => d.sample(rng),
Sampler::Pareto(d) => d.sample(rng),
}
}
}
46 changes: 45 additions & 1 deletion aqpbm-datagen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ pub mod value;
pub use burst::{inject_interval_bursts, BurstSpec};
pub use column::{ColumnSpec, DATA_TYPES};
pub use dist::{
DataDistribution, Domain, NormalParameter, Sampler, UniformParameter, ZipfParameter,
DataDistribution, Domain, NormalParameter, ParetoParameter, Sampler, UniformParameter,
ZipfParameter,
};
pub use error::DataGenError;
pub use rule::{RULE_MASK_ALL, RULE_MONOTONIC_INCREASE, RULE_NONE};
Expand Down Expand Up @@ -48,6 +49,10 @@ mod tests {
})
}

fn pareto(alpha: f64, scale: f64, seed: u64) -> DataDistribution {
DataDistribution::Pareto(ParetoParameter { alpha, scale, seed })
}

fn column(distribution: DataDistribution, data_type: &str) -> ColumnSpec {
ColumnSpec {
distribution,
Expand Down Expand Up @@ -126,6 +131,45 @@ mod tests {
assert!((mean - 1000.0).abs() < 0.2, "mean {mean} not near 1000");
}

/// The MLE of the shape, `n / sum(ln(x / scale))`, recovers `alpha` from a
/// large fixed-seed sample.
#[test]
fn pareto_mle_recovers_alpha() {
let (alpha, scale) = (1.5, 2.0);
let t = one(column(pareto(alpha, scale, 13), "f64"), 200_000)
.generate()
.unwrap();
let values = t.into_column(0).unwrap().into_f64().unwrap();
assert!(values.iter().all(|&x| x >= scale));
let log_sum: f64 = values.iter().map(|x| (x / scale).ln()).sum();
let alpha_hat = values.len() as f64 / log_sum;
assert!(
(alpha_hat - alpha).abs() / alpha < 0.05,
"alpha_hat {alpha_hat} not within 5% of {alpha}"
);
}

/// An i64 column is the f64 draws floored: every draw is >= scale > 0, so
/// the renderer's `as` truncation is a floor. The i64-only exact quantile
/// baseline scores a Pareto stream through this.
#[test]
fn pareto_i64_floors_the_f64_draws() {
let draw = |data_type| {
one(column(pareto(1.1, 1000.0, 7), data_type), 50_000)
.generate()
.unwrap()
.into_column(0)
.unwrap()
};
let floats = draw("f64").into_f64().unwrap();
let ints = draw("i64").into_i64().unwrap();
assert!(ints.iter().all(|&v| v >= 1000));
assert!(ints
.iter()
.zip(&floats)
.all(|(&i, f)| i == f.floor() as i64));
}

// ---------- rendering ----------

/// `data_type` picks the variant, and nothing else about the description
Expand Down
7 changes: 7 additions & 0 deletions docs/aqpbm-datagen.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ enum DataDistribution {
Zipf(ZipfParameter),
Uniform(UniformParameter),
Normal(NormalParameter),
Pareto(ParetoParameter),
}

struct ZipfParameter {
Expand All @@ -60,6 +61,12 @@ struct NormalParameter {
pub standard_deviation: f64,
pub seed: u64,
}

struct ParetoParameter {
pub alpha: f64, // shape
pub scale: f64, // minimum value
pub seed: u64,
}
```

`special_rule` bit map contains the following at this moment:
Expand Down
Loading