Skip to content

Commit be66ac5

Browse files
feat(planner): asap-planner --planner saved writes a sketch-bench eval plan without re-solving
Reads a plan from sketch-bench's autosketch_vs_asap plan file and the eval table's `e2e` block (PromQL queries, the plan RQEs each reads, metric labels), and writes streaming and inference YAML through the same converter as --planner milp. experiment_run_e2e.py gains controller.prebuilt_output_dir to deploy those configs instead of running the planner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent b5ecd1b commit be66ac5

11 files changed

Lines changed: 600 additions & 34 deletions

File tree

‎Cargo.lock‎

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

‎asap-planner-rs/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ indexmap.workspace = true
3737
chrono.workspace = true
3838
promql-parser = "0.5.0"
3939
reqwest = { version = "0.11", features = ["blocking", "json"] }
40+
flate2 = "1.1"
4041
rqe-optimizer = { git = "https://github.com/ProjectASAP/sketch-bench", branch = "main" }
4142

4243
[dev-dependencies]

‎asap-planner-rs/src/main.rs‎

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use asap_planner::optimizer::{
2-
parse_weight, plan_milp, plan_to_planner_output, reject_unwritable_queries, MilpInputs,
2+
parse_weight, plan_milp, plan_to_planner_output, reject_unwritable_queries, saved_plan_output,
3+
MilpInputs, PlanSelector,
34
};
45
use asap_planner::{
56
Controller, ControllerConfig, ElasticController, ElasticRuntimeOptions, RuntimeOptions,
@@ -57,10 +58,33 @@ struct Args {
5758
clickhouse_database: Option<String>,
5859

5960
/// `milp` plans with sketch-bench's rqe-optimizer: PromQL with
60-
/// --input_config only, labels from its `metrics:` hints.
61+
/// --input_config only, labels from its `metrics:` hints. `saved` writes
62+
/// a plan sketch-bench's `autosketch_vs_asap` already solved.
6163
#[arg(long, value_enum, default_value = "legacy")]
6264
planner: PlannerArg,
6365

66+
/// Saved only. sketch-bench `autosketch_vs_asap` plan file
67+
/// (`<table>.plans.json.gz`).
68+
#[arg(long, required_if_eq("planner", "saved"))]
69+
plan: Option<PathBuf>,
70+
71+
/// Saved only. The eval workload table the plan was solved for; its
72+
/// `e2e` block gives the PromQL queries and metric labels.
73+
#[arg(long = "workload-table", required_if_eq("planner", "saved"))]
74+
workload_table: Option<PathBuf>,
75+
76+
/// Saved only. The plan's method, e.g. `asap`.
77+
#[arg(long, required_if_eq("planner", "saved"))]
78+
method: Option<String>,
79+
80+
/// Saved only. The plan's objective weights, e.g. `cpu` or `fargate`.
81+
#[arg(long, required_if_eq("planner", "saved"))]
82+
weights: Option<String>,
83+
84+
/// Saved only. The plan at this latency SLA; without it, the cheapest.
85+
#[arg(long = "sla-ms")]
86+
sla_ms: Option<f64>,
87+
6488
/// MILP only. YAML workload facts: per metric, positive `value_range` and
6589
/// `cardinality` per label set, including all labels (the series count),
6690
/// plus the `shape` of each grouping sketches may serve.
@@ -99,6 +123,7 @@ enum EngineArg {
99123
enum PlannerArg {
100124
Legacy,
101125
Milp,
126+
Saved,
102127
}
103128

104129
fn main() -> anyhow::Result<()> {
@@ -116,8 +141,22 @@ fn main() -> anyhow::Result<()> {
116141
EngineArg::Precompute => StreamingEngine::Precompute,
117142
};
118143

119-
if args.planner == PlannerArg::Milp {
120-
run_milp(&args)?;
144+
if args.planner != PlannerArg::Saved {
145+
anyhow::ensure!(
146+
args.plan.is_none()
147+
&& args.workload_table.is_none()
148+
&& args.method.is_none()
149+
&& args.weights.is_none()
150+
&& args.sla_ms.is_none(),
151+
"--plan, --workload-table, --method, --weights and --sla-ms require --planner saved"
152+
);
153+
}
154+
if args.planner != PlannerArg::Legacy {
155+
match args.planner {
156+
PlannerArg::Milp => run_milp(&args)?,
157+
PlannerArg::Saved => run_saved(&args)?,
158+
PlannerArg::Legacy => unreachable!(),
159+
}
121160
println!("Generated configs in {}", args.output_dir.display());
122161
return Ok(());
123162
}
@@ -274,3 +313,52 @@ fn run_milp(args: &Args) -> anyhow::Result<()> {
274313
.write_to_dir(&args.output_dir)?;
275314
Ok(())
276315
}
316+
317+
fn run_saved(args: &Args) -> anyhow::Result<()> {
318+
anyhow::ensure!(
319+
args.input_config.is_none() && args.query_log.is_none(),
320+
"--planner saved reads its queries from --workload-table, not --input_config or --query-log"
321+
);
322+
anyhow::ensure!(
323+
matches!(args.query_language, QueryLanguage::promql)
324+
&& args.prometheus_url.is_none()
325+
&& !args.enable_punting
326+
&& args.range_duration_ms == 0
327+
&& args.step_ms == 0
328+
&& args.clickhouse_url.is_none()
329+
&& args.clickhouse_database.is_none(),
330+
"--planner saved takes only PromQL and none of --prometheus-url, --enable-punting, \
331+
--range-duration-ms, --step-ms, --clickhouse-url, --clickhouse-database"
332+
);
333+
anyhow::ensure!(
334+
args.workload_facts.is_none()
335+
&& args.atomic_costs.is_none()
336+
&& args.saturation_dir.is_none()
337+
&& args.w_cpu.is_none()
338+
&& args.w_mem.is_none(),
339+
"--workload-facts, --atomic-costs, --saturation-dir, --w-cpu and --w-mem require \
340+
--planner milp"
341+
);
342+
let scrape_interval_ms = args
343+
.data_ingestion_interval_ms
344+
.filter(|&ms| ms > 0)
345+
.ok_or_else(|| {
346+
anyhow::anyhow!("--planner saved requires a positive --data-ingestion-interval-ms")
347+
})?;
348+
saved_plan_output(
349+
args.workload_table
350+
.as_deref()
351+
.expect("clap requires --workload-table with --planner saved"),
352+
args.plan
353+
.as_deref()
354+
.expect("clap requires --plan with --planner saved"),
355+
&PlanSelector {
356+
method: args.method.as_deref().expect("clap requires --method"),
357+
weights: args.weights.as_deref().expect("clap requires --weights"),
358+
sla_ms: args.sla_ms,
359+
},
360+
scrape_interval_ms,
361+
)?
362+
.write_to_dir(&args.output_dir)?;
363+
Ok(())
364+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ fn normalized_f64_bits(value: f64) -> u64 {
132132
/// dropped — they contribute no AQE. Only arithmetic operators are split;
133133
/// comparison and set operators are left as-is (treated as opaque leaves).
134134
/// An `avg` leaf becomes its sum and count leaves (see `rewrite_avg`).
135-
fn decompose_to_leaves(query: &str) -> Vec<String> {
135+
pub(crate) fn decompose_to_leaves(query: &str) -> Vec<String> {
136136
let (lhs, rhs) = match parse_binary_arms(query) {
137137
Some(arms) => arms,
138138
None => return rewrite_avg(query),

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,17 @@ pub fn build_milp_workload(
243243
config: &ControllerConfig,
244244
facts: &WorkloadFacts,
245245
scrape_interval_ms: u64,
246+
) -> Result<MilpWorkload, MilpError> {
247+
let workload = workload_from_config(config, scrape_interval_ms)?;
248+
validate_facts(&workload.raqes, facts).map_err(MilpError::InvalidInputs)?;
249+
Ok(workload)
250+
}
251+
252+
/// [`build_milp_workload`] without the facts check, for a plan solved
253+
/// elsewhere.
254+
pub fn workload_from_config(
255+
config: &ControllerConfig,
256+
scrape_interval_ms: u64,
246257
) -> Result<MilpWorkload, MilpError> {
247258
// The MILP chooses windows and sketch parameters itself.
248259
let unsupported: Vec<&'static str> = [
@@ -298,10 +309,8 @@ pub fn build_milp_workload(
298309
tracing::debug!(
299310
items = items.len(),
300311
raqes = raqes.len(),
301-
metrics_with_facts = facts.len(),
302312
"milp inputs: built raqes"
303313
);
304-
validate_facts(&raqes, facts).map_err(MilpError::InvalidInputs)?;
305314
Ok(MilpWorkload {
306315
items,
307316
raqes,

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

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -92,14 +92,36 @@ pub fn plan_to_planner_output(
9292
workload: &MilpWorkload,
9393
solution: &MilpSolution,
9494
) -> Result<PlannerOutput, MilpOutputError> {
95+
let deployments: Vec<Deployment> = solution
96+
.deployments
97+
.iter()
98+
.map(|planned| planned.deployment.clone())
99+
.collect();
100+
let served_by: Vec<usize> = solution.raqes.iter().map(|r| r.deployment).collect();
101+
deployments_to_planner_output(config, workload, &deployments, &served_by)
102+
}
103+
104+
/// [`plan_to_planner_output`] for a plan given as its deployments and, per
105+
/// Raqe of `workload`, the index of the deployment serving it.
106+
pub fn deployments_to_planner_output(
107+
config: &ControllerConfig,
108+
workload: &MilpWorkload,
109+
deployments: &[Deployment],
110+
served_by: &[usize],
111+
) -> Result<PlannerOutput, MilpOutputError> {
112+
assert_eq!(
113+
served_by.len(),
114+
workload.raqes.len(),
115+
"one deployment per Raqe"
116+
);
95117
reject_unwritable_queries(config)?;
96118
// sketch-bench lets a deployment serve a RAQE grouped by a subset of its
97119
// grouping (a roll-up, sketch-bench#190). The engine reads a deployment
98120
// only at its own grouping, and `aggregation_config` takes the labels
99121
// from the first RAQE served, so refuse a roll-up rather than write
100122
// configs at the wrong grouping.
101-
for (raqe, planned) in workload.raqes.iter().zip(&solution.raqes) {
102-
let deployment = &solution.deployments[planned.deployment].deployment;
123+
for (raqe, &index) in workload.raqes.iter().zip(served_by) {
124+
let deployment = &deployments[index];
103125
if raqe.grouping_labels != deployment.grouping_labels {
104126
return Err(MilpOutputError::RollupUnsupported {
105127
query: raqe.id.clone(),
@@ -111,26 +133,26 @@ pub fn plan_to_planner_output(
111133

112134
let item_of = |raqe: usize| &workload.items[workload.raqe_items[raqe]];
113135
// Each item once per deployment, however many occurrences it has.
114-
let mut served: Vec<Vec<&OptimizerItem>> = vec![Vec::new(); solution.deployments.len()];
136+
let mut served: Vec<Vec<&OptimizerItem>> = vec![Vec::new(); deployments.len()];
115137
let mut seen = HashSet::new();
116-
for (raqe, planned) in solution.raqes.iter().enumerate() {
117-
if seen.insert((planned.deployment, workload.raqe_items[raqe])) {
118-
served[planned.deployment].push(item_of(raqe));
138+
for (raqe, &index) in served_by.iter().enumerate() {
139+
if seen.insert((index, workload.raqe_items[raqe])) {
140+
served[index].push(item_of(raqe));
119141
}
120142
}
121143

122144
let mut aggregations: IndexMap<String, IntermediateAggConfig> = IndexMap::new();
123145
let mut deployment_keys = Vec::with_capacity(served.len());
124-
for (planned, items) in solution.deployments.iter().zip(&served) {
125-
let aggregation = aggregation_config(&planned.deployment, items)?;
146+
for (deployment, items) in deployments.iter().zip(&served) {
147+
let aggregation = aggregation_config(deployment, items)?;
126148
let key = aggregation.identifying_key();
127149
aggregations.entry(key.clone()).or_insert(aggregation);
128150
deployment_keys.push(key);
129151
}
130152

131153
let mut queries: IndexMap<String, QueryPlanEntry> = IndexMap::new();
132-
for (raqe, planned) in solution.raqes.iter().enumerate() {
133-
let key = &deployment_keys[planned.deployment];
154+
for (raqe, &index) in served_by.iter().enumerate() {
155+
let key = &deployment_keys[index];
134156
for query in &item_of(raqe).query_strings {
135157
match queries.entry(query.clone()) {
136158
Entry::Vacant(entry) => {

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,21 @@ pub mod atomic_costs;
33
pub mod error;
44
pub mod milp;
55
pub mod milp_output;
6+
pub mod saved_plan;
67
pub mod solution;
78
pub mod workload_facts;
89

910
pub use aqe_extractor::{extract_aqes, RQE};
1011
pub use atomic_costs::{load_flat_atomic_cost_table, AtomicCostEntry, AtomicCostTable};
1112
pub use error::OptimizerError;
1213
pub use milp::{
13-
build_milp_workload, parse_weight, plan_milp, solve_milp, MilpError, MilpInputs, MilpPlan,
14-
MilpWorkload,
14+
build_milp_workload, parse_weight, plan_milp, solve_milp, workload_from_config, MilpError,
15+
MilpInputs, MilpPlan, MilpWorkload,
1516
};
16-
pub use milp_output::{plan_to_planner_output, reject_unwritable_queries, MilpOutputError};
17+
pub use milp_output::{
18+
deployments_to_planner_output, plan_to_planner_output, reject_unwritable_queries,
19+
MilpOutputError,
20+
};
21+
pub use saved_plan::{saved_plan_output, PlanSelector};
1722
pub use solution::OptimizerItem;
1823
pub use workload_facts::{load_workload_facts, parse_workload_facts, WorkloadFactsError};

0 commit comments

Comments
 (0)