Repository navigation
Add saturation study: error vs N, N_saturation and Pareto value data - #130
Conversation
DataDistribution::Pareto(ParetoParameter { alpha, scale, seed }) samples
rand_distr::Pareto. Like Normal it is unbounded, so it has no domain and
refuses a cardinality or string rendering. The CLI adds --pareto-alpha and
--pareto-scale (default 1.0) and requires --dtype f64. Includes an MLE test
for alpha and the regenerated merged_record schema.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/study_saturation.py sweeps stream length N per sketch, config and distribution point, averages the error over seeds, and reports N_sat: the smallest checkpoint from which the error stays within a relative tolerance of its tail plateau. A final-N run records insert/merge/query CPU and memory_bytes. scripts/test_study_saturation.py covers N_sat. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A checkpoint is in band when it is within max(10% of the plateau, 2 SE) of the plateau, so seed noise alone no longer breaks a small plateau. The curve CSV gains a seed_se column, and child runs read stdin from /dev/null so a detached study is never stopped for terminal input. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The doc gives the N_sat definition, the reduced-run results, a theoretical bound per sketch with how the empirical curves relate to it, and what is not covered yet (accuracy after merging, config scaling). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The datagen renderer already truncates each f64 draw into an i64 column, and Pareto draws are >= scale > 0, so truncation is a floor; only the CLI refused i64. Allowing it lets the i64-only polars exact quantile baseline score a Pareto stream (e.g. --pareto-scale 1000 keeps three digits). Adds a datagen test that the i64 column is the floored f64 draws. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Configs step 4x: CMS/CountSketch/CMS-heap cols 256..16384 x rows 3,5; HLL lg_k 12,14,16 (asap HLL has no lg_k 10); KLL k 50,200,800; DDSketch alpha 0.005..0.05. - Quantile points use Pareto(alpha, scale 1000) floored to i64. - --phase accuracy|cost|all: cost reads saturation_curve.csv from --out and runs only the serial cost runs, so CPU can be timed separately. - The cost phase also runs each family's exact polars baseline at every checkpoint and writes crossover.csv: the smallest N where exact memory_bytes (and insert+prepare+query CPU) reach 10x/100x the sketch's. - --points-from CSV selects a subset of the grid (for the 1e9 runs). - Tests for n_star, read_curve, --points-from and the cost phase against a fake approxbench. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- --resume (accuracy): keep the points whose curve over the requested sizes is already in saturation_curve.csv, write them back first, and run only the rest; a point cut off mid-write is dropped and rerun. Raw records are appended. - --cost-rows N (cost): time only the rows=N configs of the Vector2D sketches; other rows keep blank cost columns and get no crossover row. - Tests for both against the fake approxbench. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The curve and cost files now hold the whole config grid; each figure keeps the config its bound is written for (rows=3 cols=1024, lg_k=12, k=200, alpha=0.01). The caption no longer hard-codes the seed count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Document the 595-point config grid to 1e7 (N_sat per config, plateau scaling against theory) and the 185-point 1e9 subset: which unsaturated points saturate by 1e9, which keep growing (uniform data at K=1e7, top-k miss rate), and how far 1e7 N_sat verdicts move with a longer curve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… data parameters scripts/recommend_config.py reads ASAPQuery's skew_summary.csv (new worst_* / min_N / max_N / target_* schema, or the older per-window one), rounds each query's worst case to the measured grid toward the harder side (theta down, K up, rank alpha up), reads the error at max_N from the grid (or 1e9) saturation curves, optionally from the merged curve for top-k and KLL, and writes the smallest config per family that meets the target to out/recommendations.csv, with n_sat, flags and cost columns once the cost phase exists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Error-vs-N figures with theoretical bounds and the serial cost figure for the reduced run, plus the 1e9-subset figures, each with a README. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
N* is now the size from which exact stays >= f x the sketch, so polars' fixed start-up cost no longer marks N = 1e3 as a crossover, and both sides are charged one query instead of their differently sized probe sets. --phase crossover recomputes crossover.csv from the saved cost JSONL. Adds the grid cost and exact-vs-sketch figures, the crossover and recommendation CSVs, and the results to the doc. The recommender's query column is renamed query_phase_us, since it is the whole query phase. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conclusions across the trace analysis, the sketch study and merging, plus derivations: why KLL's rank error is distribution-free, why DDSketch's rank error grows and its memory shrinks with the Pareto index, and why merging is exact for linear sketches but adds variance for KLL. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Proposed fixes for the two medium review findings1. Crossover matches saved cost records by order (
|
--phase crossover matched saved cost records to points by position, so a different --cost-rows than the cost phase used gave silently wrong numbers or a StopIteration. Cost and exact records now carry their key and are looked up by it; a missing one exits with a clear message. --resume rewrote saturation_curve.csv with only the current grid's curves, so a narrower resume deleted finished curves. Rows outside the grid are now written back unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
milindsrivastava1997
left a comment
There was a problem hiding this comment.
Two low-severity findings from code review.
| ("standard_deviation".into(), value.standard_deviation), | ||
| ]), | ||
| ), | ||
| DataDistribution::Pareto(value) => ( |
There was a problem hiding this comment.
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.
| with ThreadPoolExecutor(max_workers=args.jobs) as pool: | ||
| futures = [ | ||
| None if point_key(p[1], p[2], *p[6:]) in done else | ||
| [[pool.submit(accuracy, p, n, s) for s in seeds] for n in ns] for p in points | ||
| ] | ||
| print(f"{len(done)} points kept, {sum(f is not None for f in futures)} to run", | ||
| file=sys.stderr) | ||
| results = [] | ||
| for point, per_n in zip(points, futures): | ||
| family, variant, config, _, metric, _, dist, param, k = point | ||
| if per_n is None: | ||
| kept = done[point_key(variant, config, dist, param, k)] | ||
| means = [mean for _, mean, _ in kept] | ||
| n_sat = n_saturation(ns, means, args.tolerance, args.plateau_tail, | ||
| [se for _, _, se in kept]) | ||
| results.append((point, n_sat, means[-1])) | ||
| continue | ||
| means, ses = [], [] | ||
| for n, per_seed in zip(ns, per_n): | ||
| records = [f.result() for f in per_seed] |
There was a problem hiding this comment.
One failed run is only reported after every queued run finishes. All accuracy futures are submitted at the start. If one approxbench run fails, f.result() raises CalledProcessError, but leaving the with ThreadPoolExecutor block calls shutdown(wait=True). That waits for every remaining queued run and then throws their results away, since none are written to the curve CSV. On the default grid this can waste hours before the error appears. Fix: cancel the pending futures in an except block, or call pool.shutdown(cancel_futures=True) before re-raising.
A failed approxbench run (or Ctrl-C) used to surface only after every queued run finished, and those results were discarded anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
||
| def point_key(sketch, config, dist, param, cardinality): | ||
| """How a point is matched across CSVs: param compared as a number.""" | ||
| return (sketch, config, dist, float(param), str(cardinality)) |
There was a problem hiding this comment.
point_key compares param as a number but cardinality as a raw string. --points-from asks for "a filtered saturation.csv". If that file is filtered with pandas, the Pareto rows' empty cardinality turns the column into floats, so the Zipf values come back as 1000.0. "1000.0" never equals str(1000), so:
--points-fromexits with "names N points but only 0 are in the grid".recommend_config.py:256raisesValueErroronint(k[4])when given a curve CSV saved the same way.
Fix: normalise cardinality with int(float(c)) when it isn't empty.
A saturation CSV filtered with pandas writes Zipf cardinalities as 1000.0 (the Pareto rows' empty cells make the column float), which never matched the grid's "1000". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…130) (#133) * Add Pareto distribution to datagen and --dataset pareto DataDistribution::Pareto(ParetoParameter { alpha, scale, seed }) samples rand_distr::Pareto. Like Normal it is unbounded, so it has no domain and refuses a cardinality or string rendering. The CLI adds --pareto-alpha and --pareto-scale (default 1.0) and requires --dtype f64. Includes an MLE test for alpha and the regenerated merged_record schema. * Add saturation study script scripts/study_saturation.py sweeps stream length N per sketch, config and distribution point, averages the error over seeds, and reports N_sat: the smallest checkpoint from which the error stays within a relative tolerance of its tail plateau. A final-N run records insert/merge/query CPU and memory_bytes. scripts/test_study_saturation.py covers N_sat. * Widen the saturation band by seed standard error A checkpoint is in band when it is within max(10% of the plateau, 2 SE) of the plateau, so seed noise alone no longer breaks a small plateau. The curve CSV gains a seed_se column, and child runs read stdin from /dev/null so a detached study is never stopped for terminal input. * Document the saturation study and add its plotting scripts The doc gives the N_sat definition, the reduced-run results, a theoretical bound per sketch with how the empirical curves relate to it, and what is not covered yet (accuracy after merging, config scaling). * Allow --dataset pareto --dtype i64 (floored draws) The datagen renderer already truncates each f64 draw into an i64 column, and Pareto draws are >= scale > 0, so truncation is a floor; only the CLI refused i64. Allowing it lets the i64-only polars exact quantile baseline score a Pareto stream (e.g. --pareto-scale 1000 keeps three digits). Adds a datagen test that the i64 column is the floored f64 draws. * Saturation study: config grid, exact-baseline crossover, phase split - Configs step 4x: CMS/CountSketch/CMS-heap cols 256..16384 x rows 3,5; HLL lg_k 12,14,16 (asap HLL has no lg_k 10); KLL k 50,200,800; DDSketch alpha 0.005..0.05. - Quantile points use Pareto(alpha, scale 1000) floored to i64. - --phase accuracy|cost|all: cost reads saturation_curve.csv from --out and runs only the serial cost runs, so CPU can be timed separately. - The cost phase also runs each family's exact polars baseline at every checkpoint and writes crossover.csv: the smallest N where exact memory_bytes (and insert+prepare+query CPU) reach 10x/100x the sketch's. - --points-from CSV selects a subset of the grid (for the 1e9 runs). - Tests for n_star, read_curve, --points-from and the cost phase against a fake approxbench. * Saturation study: --resume and --cost-rows - --resume (accuracy): keep the points whose curve over the requested sizes is already in saturation_curve.csv, write them back first, and run only the rest; a point cut off mid-write is dropped and rerun. Raw records are appended. - --cost-rows N (cost): time only the rows=N configs of the Vector2D sketches; other rows keep blank cost columns and get no crossover row. - Tests for both against the fake approxbench. * Saturation plots: draw one config per sketch from grid output The curve and cost files now hold the whole config grid; each figure keeps the config its bound is written for (rows=3 cols=1024, lg_k=12, k=200, alpha=0.01). The caption no longer hard-codes the seed count. * Saturation study: config-grid and 1e9 results Document the 595-point config grid to 1e7 (N_sat per config, plateau scaling against theory) and the 185-point 1e9 subset: which unsaturated points saturate by 1e9, which keep growing (uniform data at K=1e7, top-k miss rate), and how far 1e7 N_sat verdicts move with a longer curve. * Saturation study: recommend a sketch config per query from worst-case data parameters scripts/recommend_config.py reads ASAPQuery's skew_summary.csv (new worst_* / min_N / max_N / target_* schema, or the older per-window one), rounds each query's worst case to the measured grid toward the harder side (theta down, K up, rank alpha up), reads the error at max_N from the grid (or 1e9) saturation curves, optionally from the merged curve for top-k and KLL, and writes the smallest config per family that meets the target to out/recommendations.csv, with n_sat, flags and cost columns once the cost phase exists. * Add saturation study figures with reading guides Error-vs-N figures with theoretical bounds and the serial cost figure for the reduced run, plus the 1e9-subset figures, each with a README. * Saturation study: grid cost results and a per-query crossover N* N* is now the size from which exact stays >= f x the sketch, so polars' fixed start-up cost no longer marks N = 1e3 as a crossover, and both sides are charged one query instead of their differently sized probe sets. --phase crossover recomputes crossover.csv from the saved cost JSONL. Adds the grid cost and exact-vs-sketch figures, the crossover and recommendation CSVs, and the results to the doc. The recommender's query column is renamed query_phase_us, since it is the whole query phase. * Add the grid crossover and recommendation tables * Document the saturation study's conclusions and the math behind them Conclusions across the trace analysis, the sketch study and merging, plus derivations: why KLL's rank error is distribution-free, why DDSketch's rank error grows and its memory shrinks with the Pareto index, and why merging is exact for linear sketches but adds variance for KLL. * Update merge conclusions with the full top-k and KLL grids * Key saved cost records and keep out-of-grid curves on resume --phase crossover matched saved cost records to points by position, so a different --cost-rows than the cost phase used gave silently wrong numbers or a StopIteration. Cost and exact records now carry their key and are looked up by it; a missing one exits with a clear message. --resume rewrote saturation_curve.csv with only the current grid's curves, so a narrower resume deleted finished curves. Rows outside the grid are now written back unchanged. * Cancel queued accuracy runs when one fails A failed approxbench run (or Ctrl-C) used to surface only after every queued run finished, and those results were discarded anyway. * Match cardinality as a number in point_key A saturation CSV filtered with pandas writes Zipf cardinalities as 1000.0 (the Pareto rows' empty cells make the column float), which never matched the grid's "1000". --------- Co-authored-by: Zeying Zhu <50204836+zzylol@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
- rqe-optimizer/results/autosketch-vs-asap-synthetic: 66 runs of the ProjectASAP/ASAPQuery#777 workload grid (default point, every dimension alone, replicas x series, windows x card), model A and model B per family, every SLA. 0 sanity violations, the peak bound exact everywhere, no solve or job timed out. Raw results as sweep-json.tar.gz; sweep.csv, summary, figures and per-shard provenance (34 runs at ba52fd8 with verified identical tables, 32 at 0a45c48). - rqe-optimizer/data/profiling-time.json: ASAP's one-time profiling, 35.6 h of sketch-bench runs (#130, #131, #140), from record timestamps. - The synthetic summary and figures order each dimension's values by the grid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decided for ProjectASAP/ASAPQuery#777: - Q1. One evaluation's query CPU per sketch instance is q_r queries at the benchmark's CPU per query: card * (q_r * CPU per query + (S/x - 1) * merge CPU). The exporter adds query_us_per_query (query-phase CPU over its query count, study_saturation.query_secs, from --cost-records; keys derived for #130's unkeyed records) and queries_per_instance per RQE: a frequency query reads every key (C, C*s, or traces' worst_K), top-k and each quantile issue one. query_phase_us stays for reference. - Q2. Estimated latency remains that CPU time on one core, serial across instances; the summaries say so. - Q3. AutoSketch's benchmark lower bound charges each distinct probed config N = 1e8 inserts (sketch-bench's measurement size) plus one query phase: benchmark_secs_lower_bound_nbench1e8. The paper-rate estimate (60 s per distinct probe) stays. - The traces table is re-exported with both new fields. Tests: q_r for every template, CPU per query from a hand-computed record, and the benchmark bound. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conclusions (task 3: sketch accuracy, saturation and cost)
No sketch's theoretical error bound depends on N. Error that still moves with N comes from two small-N effects: a noisy ground truth when N is small, and a tail of keys still arriving while N < K. Past N_sat, error is set by the config alone, so a profile measured on a small window extrapolates to larger (merged) windows.
N_sat is mostly 1e3–1e6, with two exceptions:
So an N_sat measured only up to 1e7 is a lower bound when K is large.
Error scales with config size as theory predicts, so configs can be interpolated:
The data distribution decides which sketch to use:
Merging (Measure accuracy after merging m shards #131):
So single-sketch curves are optimistic for top-k and KLL in merged windows.
Cost:
Sketch vs exact computation (exact = buffer the samples, compute at query time):
Worst-case parameters → configuration is automated (
scripts/recommend_config.py). Examples:by (user)by (service)Limits:
Summary
Measures, for each sketch type, configuration and data distribution:
Full method and results:
docs/saturation_study.md. Figures, each folder with a README:docs/figures/saturation/.What's added
--dataset pareto(--pareto-alpha,--pareto-scale, f64 or floored i64)scripts/study_saturation.py--phase accuracy|cost|crossover|all. Supports--resume,--cost-rows 3,--points-fromscripts/recommend_config.pyscripts/plot_saturation_{curves,cost,crossover}.pyN_sat is the first checkpoint after which every later seed-mean error stays within max(10% of the plateau, 2 standard errors) of the mean of the last 3 points.
N*(f) is the first N from which exact cost stays ≥ f × sketch cost at every larger N. CPU is counted as "ingest N items + answer one query" on both sides.
Grid
Key results
Every theoretical bound is N-independent. Error that still changes with N comes from small-N effects: a noisy ground truth, or a tail of keys still filling in while N < K.
Plateau error vs sketch size (log-log slope against each sketch's size parameter: width w for CMS/CountSketch/top-k, register count m = 2^lg_k for HLL, k for KLL, α for DDSketch):
Run to 1e9, 67 of the 108 points not saturated by 1e7 saturate:
Cost per item at N = 1e7, flat in N and in the distribution unless noted:
DDSketch memory shrinks from 6.3 KB to 2.3 KB as the Pareto α grows from 1.1 to 3.
Crossover vs exact (exact = buffer the samples, compute at query time):
Recommendations, with the cost data:
by (user)by (service)by (msname)Known limits
cpu_rate) is clamped to the edge of the grid.Validation
cargo test -p aqpbm-datagenpasses, including a Pareto α recovery test.-D warningsis clean on the touched crates.python3 -m unittest scripts/test_study_saturation.py scripts/test_recommend_config.py: 22 + 11 tests pass.🤖 Generated with Claude Code