Skip to content

Commit 847bf1e

Browse files
authored
feat(planner): read MILP accuracy from sketch-bench's saturation curves; heap from the plan (#804)
1 parent 6541f49 commit 847bf1e

6 files changed

Lines changed: 495 additions & 69 deletions

File tree

‎Cargo.lock‎

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎asap-planner-rs/src/bin/optimizer_cli.rs‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
44
use std::path::PathBuf;
55

6+
use anyhow::Context;
67
use asap_planner::optimizer::{
78
build_milp_workload, load_flat_atomic_cost_table, load_workload_facts, plan_to_planner_output,
89
reject_avg_queries, solve_milp, MilpError,
910
};
1011
use asap_planner::ControllerConfig;
1112
use clap::Parser;
1213
use rqe_optimizer::milp::Objective;
14+
use rqe_optimizer::saturation::SaturationCurves;
1315

1416
#[derive(Parser, Debug)]
1517
#[command(
@@ -26,7 +28,8 @@ struct Args {
2628
#[arg(long = "data-ingestion-interval-ms", value_parser = clap::value_parser!(u64).range(1..))]
2729
data_ingestion_interval_ms: u64,
2830

29-
/// The flat cost table `export_rqe_optimizer_costs.sh` writes
31+
/// The flat cost table sketch-bench's
32+
/// `study_saturation.py --phase optimizer-cost` writes
3033
/// (`rqe_atomic_costs.json`).
3134
#[arg(long = "atomic-costs")]
3235
atomic_costs: PathBuf,
@@ -42,10 +45,17 @@ struct Args {
4245
allow_undeployable_families: bool,
4346

4447
/// YAML workload facts: per metric, positive `value_range` and
45-
/// `cardinality` per label set, including all labels (the series count).
48+
/// `cardinality` per label set, including all labels (the series count),
49+
/// plus the `shape` of each grouping sketches may serve.
4650
#[arg(long = "workload-facts")]
4751
workload_facts: PathBuf,
4852

53+
/// sketch-bench's saturation-study directory (`out_grid_1e7_cost/`,
54+
/// `out_1e9/`): sketch accuracy is read off its error-vs-N curves at each
55+
/// grouping's `shape`.
56+
#[arg(long = "saturation-dir")]
57+
saturation_dir: PathBuf,
58+
4959
/// Objective weight on CPU-sec/sec. Default: rqe-optimizer's.
5060
#[arg(long = "w-cpu", value_parser = parse_weight)]
5161
w_cpu: Option<f64>,
@@ -90,6 +100,12 @@ fn run_milp(args: &Args, config: &ControllerConfig) -> anyhow::Result<()> {
90100
return Err(MilpError::MissingMetricHints.into());
91101
};
92102
let facts = load_workload_facts(&args.workload_facts, hints, args.data_ingestion_interval_ms)?;
103+
let curves = SaturationCurves::load(&args.saturation_dir).with_context(|| {
104+
format!(
105+
"loading saturation curves from --saturation-dir {}",
106+
args.saturation_dir.display()
107+
)
108+
})?;
93109
let costs = load_flat_atomic_cost_table(&args.atomic_costs)?;
94110
let Objective::AUCCost { w_cpu, w_mem } = Objective::default();
95111
let (w_cpu, w_mem) = (args.w_cpu.unwrap_or(w_cpu), args.w_mem.unwrap_or(w_mem));
@@ -112,6 +128,7 @@ fn run_milp(args: &Args, config: &ControllerConfig) -> anyhow::Result<()> {
112128
&costs,
113129
objective,
114130
args.allow_undeployable_families,
131+
&|raqe, deployment| curves.accuracy(raqe, deployment, &facts),
115132
)?;
116133

117134
println!("=== Deployments: {} ===", solution.deployments.len());

‎asap-planner-rs/src/optimizer/atomic_costs.rs‎

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
//! The flat atomic-cost table sketch-bench's `export_rqe_optimizer_costs.sh`
2-
//! writes for the MILP.
1+
//! The flat atomic-cost table sketch-bench's
2+
//! `study_saturation.py --phase optimizer-cost` writes for the MILP.
33
44
use std::path::Path;
55

66
pub use rqe_optimizer::{AtomicCostEntry, AtomicCostTable};
77

8-
/// Load the flat cost table `export_rqe_optimizer_costs.sh` writes, for the
9-
/// MILP. An invalid row is an error, not dropped.
8+
/// Load the flat cost table `study_saturation.py --phase optimizer-cost`
9+
/// writes, for the MILP. An invalid row is an error, not dropped.
1010
pub fn load_flat_atomic_cost_table(path: &Path) -> anyhow::Result<AtomicCostTable> {
1111
let raw = std::fs::read_to_string(path)
1212
.map_err(|e| anyhow::anyhow!("reading cost table {}: {e}", path.display()))?;
@@ -37,6 +37,21 @@ fn valid_cost_entry(entry: &AtomicCostEntry) -> bool {
3737
.all(|cost| cost.is_finite() && *cost >= 0.0)
3838
}
3939

40+
/// A cost row's required `measured_at`, for test fixtures: the cost table's
41+
/// shape (sketch-bench `study_saturation.py` COST_*). Generic so the
42+
/// fixture needn't name `aqpbm_core::MeasuredAt`.
43+
#[cfg(test)]
44+
pub(crate) fn test_measured_at<T: serde::de::DeserializeOwned>() -> T {
45+
serde_json::from_value(serde_json::json!({
46+
"items_per_instance": 1_000_000,
47+
"keys_per_instance": 10_000,
48+
"value_range": null,
49+
"merge_operand_items": null,
50+
"distribution": null,
51+
}))
52+
.expect("MeasuredAt fixture")
53+
}
54+
4055
#[cfg(test)]
4156
mod tests {
4257
use super::*;
@@ -51,7 +66,7 @@ mod tests {
5166
fn flat_loader_rejects_non_finite_or_negative_costs() {
5267
let row = |insert: &str| {
5368
format!(
54-
r#"{{"sketch":"hll","sketch_config":null,"mem_bytes_per_instance":1.0,"insert_cpu_secs":{insert},"merge_cpu_secs":1.0,"query_cpu_secs":1.0,"query_accuracy":{{}}}}"#
69+
r#"{{"sketch":"hll","sketch_config":null,"mem_bytes_per_instance":1.0,"insert_cpu_secs":{insert},"merge_cpu_secs":1.0,"query_cpu_secs":1.0,"query_accuracy":{{"relative_error":0.0}},"accuracy_metric":"relative_error","measured_at":{{"items_per_instance":1000000,"keys_per_instance":10000,"value_range":null,"merge_operand_items":null,"distribution":null}}}}"#
5570
)
5671
};
5772
let file = tempfile::NamedTempFile::new().unwrap();
@@ -62,16 +77,21 @@ mod tests {
6277
assert!(err.to_string().contains("negative"), "{err}");
6378
}
6479

65-
/// Rows with `measured_at` load, and older rows without it still do.
80+
/// Rows name their accuracy metric and where they were measured; a row
81+
/// without either is an old table, rejected rather than guessed.
6682
#[test]
67-
fn atomic_cost_entry_accepts_optional_measured_at() {
68-
let base = r#""sketch":"kll-percall","sketch_config":null,"mem_bytes_per_instance":1.0,"insert_cpu_secs":1.0,"merge_cpu_secs":1.0,"query_cpu_secs":1.0,"query_accuracy":{}"#;
69-
let with = format!(
70-
r#"{{{base},"measured_at":{{"items_per_instance":1000000,"keys_per_instance":100000,"value_range":[1.0,100000.0],"merge_operand_items":62500,"distribution":{{"kind":"zipf","skewness":1.1,"population_size":100000,"seed":42}}}}}}"#
71-
);
72-
let entry: AtomicCostEntry = serde_json::from_str(&with).unwrap();
73-
assert_eq!(entry.measured_at.unwrap().items_per_instance, 1_000_000);
74-
let without: AtomicCostEntry = serde_json::from_str(&format!("{{{base}}}")).unwrap();
75-
assert!(without.measured_at.is_none());
83+
fn atomic_cost_entry_requires_accuracy_metric_and_measured_at() {
84+
let base = r#""sketch":"kll-percall","sketch_config":null,"mem_bytes_per_instance":1.0,"insert_cpu_secs":1.0,"merge_cpu_secs":1.0,"query_cpu_secs":1.0,"query_accuracy":{"mean_rank_err":0.01}"#;
85+
let measured_at = r#""measured_at":{"items_per_instance":1000000,"keys_per_instance":null,"value_range":null,"merge_operand_items":62500,"distribution":{"kind":"pareto","alpha":2.0,"scale":1000.0,"seed":42}}"#;
86+
let full = format!(r#"{{{base},"accuracy_metric":"mean_rank_err",{measured_at}}}"#);
87+
let entry: AtomicCostEntry = serde_json::from_str(&full).unwrap();
88+
assert_eq!(entry.measured_at.items_per_instance, 1_000_000);
89+
assert_eq!(entry.accuracy(), Some(0.01));
90+
for partial in [
91+
format!(r#"{{{base},{measured_at}}}"#),
92+
format!(r#"{{{base},"accuracy_metric":"mean_rank_err"}}"#),
93+
] {
94+
assert!(serde_json::from_str::<AtomicCostEntry>(&partial).is_err());
95+
}
7696
}
7797
}

0 commit comments

Comments
 (0)