Repository navigation
feat(planner): build rqe-optimizer Raqes and facts from the workload config - #798
Conversation
milindsrivastava1997
left a comment
There was a problem hiding this comment.
Automated review findings (Claude Code). These have not been verified by hand.
|
|
||
| fn capability(statistic: Statistic) -> Capability { | ||
| match statistic { | ||
| Statistic::Sum | Statistic::Count => Capability::SumOrCount, |
There was a problem hiding this comment.
Sum and Count both map to Capability::SumOrCount. After avg by (job)(x) is rewritten into sum and count items with the same grouping, both land in one candidate group, so the MILP can serve both Raqes from a single exact-sum deployment. One accumulator can't hold both the sum of values and the count, so the plan under-counts ingest and memory and A3 can't deploy it.
There was a problem hiding this comment.
Agreed, this is real. The fix belongs upstream: split SumOrCount into Sum and Count capabilities (as Min/Max already are), so a sum and a count on the same grouping never share a deployment. Tracked in ProjectASAP/sketch-bench#161; ASAPQuery bumps the rev once it lands. A3 must not deploy an avg plan before then.
| _ => req.grouping_labels.labels.iter().cloned().collect(), | ||
| }; | ||
|
|
||
| Ok(Raqe { |
There was a problem hiding this comment.
The Raqe id is only query_strings.join(" | ") plus a per-item #k. Items that differ only in t_repeat_ms, accuracy_sla or latency_sla_ms get the same ids (for example, sum by (job) (x)#0 with SLA 0.99 and with SLA 0.9). The Unservable error, the CLI's Raqe→deployment output, and anything downstream keyed by id can't tell them apart.
There was a problem hiding this comment.
Fixed in 5561781. Ids are now {item index}:{query}#{k}, so items differing only in T or SLA get distinct ids (same_query_at_different_cadences_gets_distinct_ids).
| let costs_path = args | ||
| .atomic_costs | ||
| .as_deref() | ||
| .expect("clap requires --atomic-costs with --milp"); |
There was a problem hiding this comment.
The MILP path deserializes the cost table without row validation. The greedy loader drops non-finite or negative rows via valid_cost_entry, and rqe-optimizer doesn't check them either. A NaN or negative cost then reaches minimize, where HiGHS either fails with an opaque error or picks the negative-cost deployment. Suggest reusing valid_cost_entry.
There was a problem hiding this comment.
Fixed in 5561781. New load_flat_atomic_cost_table reuses valid_cost_entry and errors on any non-finite or negative row instead of dropping it (flat_loader_rejects_non_finite_or_negative_costs).
| .expect("clap requires --atomic-costs with --milp"); | ||
| let costs: AtomicCostTable = serde_json::from_str(&std::fs::read_to_string(costs_path)?) | ||
| .map_err(|e| anyhow::anyhow!("parsing cost table {}: {e}", costs_path.display()))?; | ||
| let Objective::AUCCost { w_cpu, w_mem } = Objective::default(); |
There was a problem hiding this comment.
--w-cpu and --w-mem accept any f64. A negative value (for example --w-mem -1) rewards memory or makes the problem unbounded, and NaN makes every objective coefficient NaN. Suggest rejecting values that aren't finite and >= 0.
There was a problem hiding this comment.
Fixed in 5561781. --w-cpu/--w-mem go through parse_weight: finite and >= 0 only (weights_must_be_finite_and_non_negative).
| let req = &item.requirements; | ||
| let query = item.query_strings.join(" | "); | ||
| let [statistic] = req.statistics.as_slice() else { | ||
| panic!("optimizer item {query:?} must have exactly one statistic after the avg rewrite"); |
There was a problem hiding this comment.
This panic!s when an item has more than one statistic. get_statistics_to_compute returns [Sum, Count] for any avg, but rewrite_avg only rewrites a top-level avg or avg_over_time, so an avg the rewrite misses crashes the CLI. Suggest returning a MilpError variant instead.
There was a problem hiding this comment.
Fixed in 5561781. Returns MilpError::MultipleStatistics instead of panicking (item_with_two_statistics_is_an_error_not_a_panic).
| } | ||
|
|
||
| pub type AtomicCostTable = Vec<AtomicCostEntry>; | ||
| pub use rqe_optimizer::{AtomicCostEntry, AtomicCostTable}; |
There was a problem hiding this comment.
Re-exporting rqe_optimizer's AtomicCostEntry drops the local #[serde(deny_unknown_fields)]. Unknown or misspelled cost fields (for example a new subtract_cpu_secs) used to fail to parse and are now silently accepted, and the canary test that guarded this was removed.
There was a problem hiding this comment.
Agreed that the strictness is lost, but it belongs to sketch-bench's type now that ASAPQuery uses it directly. Tracked in ProjectASAP/sketch-bench#161 (add deny_unknown_fields to AtomicCostEntry). On the typed measured_at: sketch-bench's reduce_one always writes the full block, so the typed form matches every table it emits. Tables are only produced by sketch-bench.
|
|
||
| /// `query_frequency_hz` is `count * 1000 / t_repeat_ms`. | ||
| fn occurrence_count(item: &OptimizerItem) -> usize { | ||
| (item.query_frequency_hz * item.t_repeat_ms as f64 / 1000.0).round() as usize |
There was a problem hiding this comment.
Recovering the occurrence count by rounding query_frequency_hz counts leaves, not query evaluations. sum(x) / sum(x) adds 1000/t twice to one item and becomes 2 Raqes, so query CPU is charged twice for one evaluation. The same happens with avg by (job)(rate(x[5m])). Storing an integer count on OptimizerItem would be exact.
There was a problem hiding this comment.
Fixed the float round-trip in 5561781: OptimizerItem.occurrences is an exact integer count and query_frequency_hz is derived from it. Keeping per-leaf counting, though: the engine evaluates each arm of a binary query independently, so sum(x) / sum(x) really runs the aggregate twice, and two Raqes charge that correctly.
| cost_rows = costs.len(), | ||
| "milp: built candidates" | ||
| ); | ||
| for (raqe, eligible) in workload |
There was a problem hiding this comment.
Nit: this debug-only loop runs eligible_deployments_for for every Raqe on every solve, even when debug logging is off, and unservable then repeats the same O(R×D) scan. Suggest guarding it with tracing::enabled!(Level::DEBUG), or computing eligibility once.
There was a problem hiding this comment.
Fixed in 5561781. Eligibility is computed once per Raqe and feeds both the debug log and the unservable check (no second scan via unservable).
| /// document. This makes the empirical workload profile explicit and avoids | ||
| /// mixing costs from different traces or time windows. | ||
| #[arg(long = "atomic-cost-workload", requires = "atomic_costs")] | ||
| /// Greedy only. JSON `profiles[].workload` value copied from the |
There was a problem hiding this comment.
Nit: --label-set-facts has no conflicts_with = "milp", while --atomic-cost-workload does, so it is silently ignored under --milp.
There was a problem hiding this comment.
Fixed in 5561781. --label-set-facts now has conflicts_with = "milp".
| Ok(()) | ||
| } | ||
|
|
||
| fn run_milp(args: &Args, config: &ControllerConfig) -> anyhow::Result<()> { |
There was a problem hiding this comment.
Nit: workload facts load against config.metrics.unwrap_or_default() before the MissingMetricHints check runs. A config with no metrics: section fails with "metric X has facts but no metrics: hint" instead of the intended MissingMetricHints error.
There was a problem hiding this comment.
Fixed in 5561781. A missing metrics: section now returns MissingMetricHints before facts are loaded, and the MILP path calls config.warn_default_slas().
…config Converts the workload's optimizer items into sketch-bench `Raqe`s (one per query occurrence), loads per-metric workload facts, and solves with `rqe_optimizer::milp::minimize`. `asap-optimizer-cli --milp` prints the chosen deployments and plan cost; it writes no configs yet. `AtomicCostEntry` is now rqe-optimizer's type instead of a local copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1806411 to
b0ce7a7
Compare
milindsrivastava1997
left a comment
There was a problem hiding this comment.
Automated code review (9 findings).
|
|
||
| fn capability(statistic: Statistic) -> Capability { | ||
| match statistic { | ||
| Statistic::Sum | Statistic::Count => Capability::SumOrCount, |
There was a problem hiding this comment.
Sum and Count share one SumOrCount deployment. Both map to Capability::SumOrCount, and a Deployment is keyed only by (capability, metric, filter, grouping, config, window), so the MILP can serve sum by (job)(x) and count by (job)(x) from one exact-sum accumulator, which can hold only one of sum-of-values or sum-of-1s.
Failure: the avg rewrite (#795) produces a sum item and a count item on the same metric and grouping. Both map to one shared deployment, so ingest and memory are undercounted by about 2x and A3 cannot deploy the plan.
There was a problem hiding this comment.
Agreed, this is real. The fix belongs upstream: split SumOrCount into Sum and Count capabilities (as Min/Max already are), so a sum and a count on the same grouping never share a deployment. Tracked in ProjectASAP/sketch-bench#161; ASAPQuery bumps the rev once it lands. A3 must not deploy an avg plan before then.
| }; | ||
|
|
||
| Ok(Raqe { | ||
| id: query, |
There was a problem hiding this comment.
Raqe ids are not unique. The id is query_strings.join(" | ") plus #k only. Items with the same query string but a different t_repeat_ms, accuracy_sla or latency_sla_ms get identical ids.
Failure: two groups running sum by (job)(x) at 30s and 60s both produce sum by (job)(x)#0. The CLI output, MilpError::Unservable, and the BTreeSet-deduped validate_facts messages then become ambiguous or merge, and A3 keying off raqe ids would see duplicates.
There was a problem hiding this comment.
Fixed in 5561781. Ids are now {item index}:{query}#{k}, so items differing only in T or SLA get distinct ids (same_query_at_different_cadences_gets_distinct_ids).
| } | ||
|
|
||
| fn run_milp(args: &Args, config: &ControllerConfig) -> anyhow::Result<()> { | ||
| let hints = config.metrics.as_deref().unwrap_or_default(); |
There was a problem hiding this comment.
Missing metrics: hints are silently defaulted. config.metrics.as_deref().unwrap_or_default() turns missing hints into an empty slice, which hides the intended MissingMetricHints error (code-design-review §1).
Failure: a config with no metrics: block later fails with metric "..." has facts but no metrics: hint. That points the user at the facts file instead of the config.
Separately, the MILP path never calls config.warn_default_slas(). A group without controller_options gets accuracy_sla = 0.0 (max error 1.0, or precision_at_k >= 0 for TopK), so the coarsest sketch qualifies with no warning.
There was a problem hiding this comment.
Fixed in 5561781. A missing metrics: section now returns MissingMetricHints before facts are loaded, and the MILP path calls config.warn_default_slas().
| } | ||
|
|
||
| pub type AtomicCostTable = Vec<AtomicCostEntry>; | ||
| pub use rqe_optimizer::{AtomicCostEntry, AtomicCostTable}; |
There was a problem hiding this comment.
Re-exporting sketch-bench's AtomicCostEntry drops #[serde(deny_unknown_fields)] and makes measured_at a typed MeasuredAt.
Failure: a misspelled field such as merge_acuracy now loads silently with the data dropped, so merged accuracy is never checked. Conversely, a profile whose measured_at lacks items_per_instance (accepted as opaque in #796) now fails to load.
There was a problem hiding this comment.
Agreed that the strictness is lost, but it belongs to sketch-bench's type now that ASAPQuery uses it directly. Tracked in ProjectASAP/sketch-bench#161 (add deny_unknown_fields to AtomicCostEntry). On the typed measured_at: sketch-bench's reduce_one always writes the full block, so the typed form matches every table it emits. Tables are only produced by sketch-bench.
| } | ||
|
|
||
| /// `query_frequency_hz` is `count * 1000 / t_repeat_ms`. | ||
| fn occurrence_count(item: &OptimizerItem) -> usize { |
There was a problem hiding this comment.
Occurrence count is recovered through a float round-trip (query_frequency_hz * t_repeat_ms / 1000), so the integer count from extract_aqes is lost.
Failure: sum(x) + sum(x) in one dashboard adds 1000/t twice, becomes two Raqes, and charges query CPU twice. Carrying count: usize on OptimizerItem would be exact and simpler.
There was a problem hiding this comment.
Fixed the float round-trip in 5561781: OptimizerItem.occurrences is an exact integer count and query_frequency_hz is derived from it. Keeping per-leaf counting, though: the engine evaluates each arm of a binary query independently, so sum(x) / sum(x) really runs the aggregate twice, and two Raqes charge that correctly.
| for (raqe, eligible) in workload | ||
| .raqes | ||
| .iter() | ||
| .map(|r| (r, eligible_deployments_for(r, &deployments))) |
There was a problem hiding this comment.
An O(R·D) eligibility scan runs only to feed a debug log. eligible_deployments_for runs for every Raqe x candidate pair even when debug logging is off, and unservable then repeats the same scan. Gate it on tracing::enabled!(Level::DEBUG), or derive unservable from the same per-Raqe lists.
There was a problem hiding this comment.
Fixed in 5561781. Eligibility is computed once per Raqe and feeds both the debug log and the unservable check (no second scan via unservable).
| .then(a.accuracy_sla.total_cmp(&b.accuracy_sla)) | ||
| .then( | ||
| a.latency_sla_ms | ||
| .partial_cmp(&b.latency_sla_ms) |
There was a problem hiding this comment.
The sort comparator is inconsistent. It uses partial_cmp(...).unwrap_or(Ordering::Equal) for latency_sla_ms but total_cmp for accuracy_sla. A NaN latency (rejected by validate_facts only after sorting) makes the order non-total, so Raqe order and ids can vary between runs. Using total_cmp through the Option would make it total.
There was a problem hiding this comment.
Fixed in 5561781. Both SLAs now use total_cmp; no latency limit sorts last.
| #[arg(long = "atomic-costs")] | ||
| /// Greedy only. YAML label-set facts: `series_count` per (metric, spatial | ||
| /// filter) and `cardinality` per (metric, spatial filter, grouping labels). | ||
| #[arg(long = "label-set-facts", required_unless_present = "milp")] |
There was a problem hiding this comment.
--label-set-facts is silently ignored with --milp. Clap accepts the flag but the file is never read in MILP mode. Add conflicts_with = "milp", as --atomic-cost-workload already has.
There was a problem hiding this comment.
Fixed in 5561781. --label-set-facts now has conflicts_with = "milp".
- Raqe ids carry the item index, so items differing only in T or SLA get distinct ids. - `OptimizerItem.occurrences` counts merged leaves exactly; `query_frequency_hz` is derived from it. - An item with more than one statistic is a `MilpError`, not a panic. - Eligibility is computed once per Raqe for the debug log and the unservable check; the item sort is total. - The MILP cost table loader rejects non-finite or negative rows; `--w-cpu`/`--w-mem` must be finite and >= 0. - `--label-set-facts` conflicts with `--milp`; missing `metrics:` hints report `MissingMetricHints`; the MILP path warns on default SLAs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
milindsrivastava1997
left a comment
There was a problem hiding this comment.
Automated code review: 2 correctness issues (deployment sharing), 4 behaviour changes, 2 nits.
🤖 Generated with Claude Code
|
|
||
| fn capability(statistic: Statistic) -> Capability { | ||
| match statistic { | ||
| Statistic::Sum | Statistic::Count => Capability::SumOrCount, |
There was a problem hiding this comment.
Sum and Count share a deployment. Statistic::Sum and Statistic::Count both map to Capability::SumOrCount, so the MILP can serve a sum Raqe and a count Raqe from one exact-sum deployment. ASAPQuery deploys them as different configs (MultipleSum with sub_type sum vs count, per map_statistic_to_precompute_operator). The avg→sum+count rewrite from #795 always hits this, and so does sum by (job)(x) next to count by (job)(x). The result is a plan ASAPQuery can't deploy as written, and its cost counts one accumulator where two are needed.
There was a problem hiding this comment.
Same as the earlier Sum/Count thread: tracked in ProjectASAP/sketch-bench#161 (split SumOrCount into Sum and Count). ASAPQuery bumps the rev once it lands; A3 must not deploy avg plans before then.
| item.latency_sla_ms.unwrap_or(f64::INFINITY) | ||
| } | ||
|
|
||
| fn item_to_raqe(item: &OptimizerItem) -> Result<Raqe, MilpError> { |
There was a problem hiding this comment.
topk_count_events is dropped. topk(5, sum_over_time(x[5m])) and topk(5, count_over_time(x[5m])) at the same cadence become identical Raqes (capability, grouping, lookback all match), so they can share one CountMinSketchWithHeap deployment. ASAPQuery needs two configs with different count_events, so one of the two queries gets wrong rankings, and the cost counts one sketch too few.
There was a problem hiding this comment.
Agreed. Raqe has no way to express it, so the fix is upstream: added as §3 of ProjectASAP/sketch-bench#161 (split TopK by value vs count weighting, or add a stream-identity field). That is in progress, so no interim guard here.
| capability: Capability, | ||
| accuracy_sla: f64, | ||
| ) -> (&'static str, f64, AccuracyDirection) { | ||
| let max_error = 1.0 - accuracy_sla; |
There was a problem hiding this comment.
Floating-point error at the SLA boundary. 1.0 - 0.9 = 0.09999999999999998, so a cost row whose relative_error or max_rank_err is exactly 0.1 fails the <= check and is wrongly ineligible. This can make a Raqe Unservable or force a costlier deployment. Adding a small epsilon to the comparison would fix it.
There was a problem hiding this comment.
Fixed in a7515f6. Tolerances get a SLA_EPSILON = 1e-9 slack in the lenient direction (accuracy_exactly_at_the_sla_boundary_passes covers 0.1 at SLA 0.9 for rank error and 0.9 for top-k precision).
|
|
||
| /// Objective weights must be finite and non-negative: a negative weight | ||
| /// rewards cost, and NaN poisons every coefficient. | ||
| fn parse_weight(s: &str) -> Result<f64, String> { |
There was a problem hiding this comment.
An all-zero objective is accepted. 0 passes this check for both weights, and the default w_mem is already 0, so --w-cpu 0 makes every cost coefficient 0. HiGHS then returns an arbitrary feasible plan, which the CLI prints as optimal. Reject the case where w_cpu == 0 && w_mem == 0.
There was a problem hiding this comment.
Fixed in a7515f6. The CLI rejects --w-cpu and --w-mem both being 0.
| } | ||
|
|
||
| pub type AtomicCostTable = Vec<AtomicCostEntry>; | ||
| pub use rqe_optimizer::{AtomicCostEntry, AtomicCostTable}; |
There was a problem hiding this comment.
This also changes the greedy loader's parsing. Re-exporting rqe_optimizer's AtomicCostEntry makes measured_at typed (MeasuredAt with a required items_per_instance) where it was opaque JSON, and it drops deny_unknown_fields. A greedy cost document that #796 accepted can now fail to parse, and misspelled optional fields (merge_acuracy, measured-at) are now silently ignored instead of rejected. Was that intended?
There was a problem hiding this comment.
Intended. ASAPQuery now uses sketch-bench's type directly. sketch-bench's reduce_one always writes the full measured_at block, so the typed form matches every table it emits. Unknown-field strictness is tracked in ProjectASAP/sketch-bench#161 §2.
| Ok(req) => { | ||
| let key = OptimizerItemKey::from_rqe(&req, rqe); | ||
| let entry = acc.entry(key).or_insert_with(|| (req, Vec::new(), 0.0)); | ||
| let entry = acc.entry(key).or_insert_with(|| (req, Vec::new(), 0)); |
There was a problem hiding this comment.
Repeated leaves within one query are counted twice. occurrences goes up once per matching leaf, so sum by (job)(x) / sum by (job)(x) gives occurrences = 2. build_milp_workload then emits two Raqes and charges query and merge CPU twice per interval, even though the query runs once. Should identical leaves within a single RQE be deduplicated?
There was a problem hiding this comment.
Keeping two Raqes: the engine evaluates each arm separately. handle_binary_expr_promql calls evaluate_binary_arm for lhs and rhs (asap-query-engine/src/engines/simple_engine/promql.rs:562,565), each running the full fetch and merge pipeline with no memo. Range queries build and run one context per arm (promql.rs:803-828), and there is no result cache. So sum(x) / sum(x) really pays query and merge CPU twice.
| pub query_frequency_hz: f64, | ||
|
|
||
| /// Leaf occurrences merged into this item, across all RQEs. | ||
| pub occurrences: usize, |
There was a problem hiding this comment.
nit: query_frequency_hz is always occurrences * 1000 / t_repeat_ms, so storing both lets them drift. The tests already set the two independently and inconsistently. A query_frequency_hz() method computed from these fields would remove the redundant state.
There was a problem hiding this comment.
Leaving this one: query_frequency_hz is read only by the greedy path (greedy.rs:76, cost_model.rs:193, candidate_gen_dump.rs:81). The MILP uses occurrences. Greedy is commented out in A4 (#790), so the redundant field goes with it.
|
|
||
| fn run_milp(args: &Args, config: &ControllerConfig) -> anyhow::Result<()> { | ||
| config.warn_default_slas(); | ||
| let Some(hints) = config.metrics.as_deref() else { |
There was a problem hiding this comment.
nit: this repeats the MissingMetricHints check that extract_hinted_items already does inside build_milp_workload. Having one guard (e.g. let load_workload_facts take the config) would keep the two from diverging.
There was a problem hiding this comment.
Leaving as is. The CLI guard has to run before load_workload_facts, which needs the hints, and that happens before build_milp_workload runs. Moving the check into a facts loader that takes the config would just move the duplicate.
… objective - Accuracy tolerances get a 1e-9 slack so `1 - sla` rounding doesn't reject a cost row measured exactly at the boundary. - `asap-optimizer-cli --milp` rejects `--w-cpu`/`--w-mem` both 0, which would make the solver's pick arbitrary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
milindsrivastava1997
left a comment
There was a problem hiding this comment.
Code review: 8 findings (3 correctness), verified against pinned sketch-bench rqe-optimizer @ 373ea6b. Tests were not run.
|
|
||
| // TopK keeps one heap per `topk by` bucket; its `grouping_labels` is the | ||
| // output label set (every label), which would cost one heap per series. | ||
| let grouping_labels: LabelSet = match capability { |
There was a problem hiding this comment.
Correctness: item_to_raqe drops topk_count_events, so count-weighted and value-weighted TopK items become identical Raqes (same capability/metric/grouping). topk(5, count_over_time(x[5m])) and topk(5, sum_over_time(x[5m])) at the same cadence can both be assigned to one cms-heap-topk-fastpath-vector2d deployment, but the engine needs two distinct CountMinSketchWithHeap configs. One query is then answered with the wrong weighting, and cost is under-counted by one deployment. (The PR notes this isn't modeled, but the resulting plan can't be deployed. Consider at least making the weightings ineligible to share.)
| } | ||
|
|
||
| pub type AtomicCostTable = Vec<AtomicCostEntry>; | ||
| pub use rqe_optimizer::{AtomicCostEntry, AtomicCostTable}; |
There was a problem hiding this comment.
Correctness: Re-exporting sketch-bench's AtomicCostEntry loses deny_unknown_fields, and the rejection test was removed. A typo such as merge_acuracy now parses silently with merge_accuracy empty, so accuracy_at falls back to single-instance query_accuracy. KLL/HLL rows can then pass accuracy_ok_for at L/W merges while their merged error exceeds the SLA.
| // query_frequency_hz must stay in Hz (queries per real second) | ||
| // regardless of t_repeat_ms's internal unit — 1000.0 / ms, not 1.0 / ms. | ||
| entry.2 += 1000.0 / rqe.t_repeat_ms as f64; | ||
| entry.2 += 1; |
There was a problem hiding this comment.
Correctness: occurrences is incremented per leaf even when the same leaf repeats inside one query (the contains check dedups only query_strings). rate(x[5m]) / rate(x[5m]) * 100 at T=60s gives occurrences=2, so two Raqes (#0, #1) are emitted and query cost is charged twice per interval for one panel.
| let file: FactsFile = serde_yaml::from_str(yaml)?; | ||
| let mut facts = WorkloadFacts::new(); | ||
| for entry in file.metrics { | ||
| let Some(hint) = hints.iter().find(|h| h.metric == entry.metric) else { |
There was a problem hiding this comment.
With duplicate metrics: hints for one metric, this find takes the first hint's labels while schema_from_hints keeps the last (add_metric overwrites). Extraction and facts then disagree on the label set, which shows up as a confusing 'not a subset' error from validate_facts or a wrong series count. Suggest rejecting duplicate hints up front.
| "milp: built candidates" | ||
| ); | ||
| let mut missing = Vec::new(); | ||
| for raqe in &workload.raqes { |
There was a problem hiding this comment.
This reimplements rqe_optimizer::enumerate::unservable(raqes, deployments), which minimize's doc points callers to. If upstream changes the definition, this copy drifts and a Raqe can reach minimize, which assert!s and panics instead of returning MilpError::Unservable.
| costs: &[AtomicCostEntry], | ||
| objective: Objective, | ||
| ) -> Result<(Vec<Deployment>, MilpSolution), MilpError> { | ||
| let deployments = build_all_candidates(&workload.raqes, costs, facts, false); |
There was a problem hiding this comment.
The full R×D eligibility matrix is computed 3× (pruning in build_all_candidates, the unservable check below, and inside minimize), and R scales with one Raqe per occurrence. With many duplicate panels, planning time grows linearly for no change in the plan. Consider computing eligibility once, or collapsing identical Raqes before the check.
| pub query_frequency_hz: f64, | ||
|
|
||
| /// Leaf occurrences merged into this item, across all RQEs. | ||
| pub occurrences: usize, |
There was a problem hiding this comment.
occurrences and query_frequency_hz are two sources of truth (freq = occurrences * 1000 / t_repeat_ms). Fixtures already set them inconsistently: the greedy test helper takes an arbitrary freq_hz with occurrences: 1. Suggest deriving the frequency with a query_frequency_hz() method.
| for k in 0..count { | ||
| raqes.push(Raqe { | ||
| // The item index keeps ids unique across T and SLAs. | ||
| id: format!("{index}:{}#{k}", raqe.id), |
There was a problem hiding this comment.
Raqe ids start with the sorted item index, so adding or removing any query renumbers every later item (3:q#0 → 4:q#0). If A3 keys configs or diffs on these ids, unchanged queries will look new. A content-based key (query, T, SLAs) would keep the ids stable.
Implements A2b (inputs → Raqe) of #790 (session 2b). Builds on #797 (
latency_sla_msrename, merged).What
optimizer/milp.rsbuild_milp_workload(config, facts, scrape_interval_ms) -> MilpWorkload { items, raqes, raqe_items }turns optimizer items intorqe_optimizer::Raqes and runsvalidate_facts.solve_milp(workload, facts, costs, objective) -> (deployments, MilpSolution)runsbuild_all_candidates(deployable families only), checks for unservable Raqes, thenmilp::minimize.optimizer/workload_facts.rs: loads the new facts file intoWorkloadFacts.pipeline.rs: hint checking moved into a sharedextract_hinted_itemsthat greedy and MILP both use.atomic_costs.rs:AtomicCostEntry/AtomicCostTablere-exportrqe_optimizer's types instead of a local copy, someasured_at(feat(planner): accept optional measured_at on atomic-cost entries #796) is now sketch-bench's typedMeasuredAt, and rows also carrymerge_accuracy. sketch-bench's type does not deny unknown fields, so feat(planner): accept optional measured_at on atomic-cost entries #796's "other unknown fields are refused" assertion is dropped; themeasured_atload tests stay. The drift-canary test is removed.asap-optimizer-cli --milp:--workload-facts,--atomic-costs <flat rqe_atomic_costs.json>,--w-cpu,--w-mem. Prints deployments, Raqe → deployment mapping, latency, and per-phase cost. Writes no YAML (that's A3).-v) at each step: facts per metric, item → Raqe (all fields), candidate count, eligible count per Raqe, solve result.Cargo.lock: sketch-benchrqe-optimizerbumped to373ea6b(includes Explore if we can have static checking of configs generated by Utilities experiments scripts for various modules #145/QueryEngineRust should not do an exact match of an incoming query against inference_config for SQL queries #148, plus Tracking issue for OSS release #155measured_atand Revert sketchlib-rust path dependency once it is open sourced #154 accuracy checked at the query's merge count).MetricFacts.value_range(sizes DDSketch only, not deployable) is set toNone.tests/rqe_optimizer_smoke.rsremoved: it used the pre-QueryEngineRust should not do an exact match of an incoming query against inference_config for SQL queries #148 API. The newsolve_*tests cover HiGHS linking.Design decisions (also on #790)
[patch]path dep: sketch-bench main already has S1/S2/S6.minimize_cost/EC2 pricing; the objective isObjective::AUCCost { w_cpu, w_mem }via--w-cpu/--w-mem(default: rqe-optimizer'sObjective::default()).Raqehas no frequency weight and query CPU is charged once per Raqe per interval. Two dashboards running the same query → two identical Raqes (<query>#0,#1) served by one shared deployment.accuracy_slais required accuracy (e.g. 0.99), in [0, 1]. Exact families, Cardinality:relative_error ≤ 1 − sla. Quantile:max_rank_err ≤ 1 − sla. TopK:precision_at_k ≥ sla. Worst-case metrics throughout.latency_sla_ms(#797) passes straight through toRaqe.latency_sla_ms.validate_facts).metrics: [{metric, groups: [{labels, cardinality}]}]. The all-labels entry is the series count; arrival rate = series / scrape interval (sketch-bench). Labels come from the workloadmetrics:hints, scrape interval from the CLI. New loader; the oldlabel_set_factsstays for greedy until A4.grouping_labels=topk_by_labels({}for baretopk), not the all-labels output set, which would cost one heap per series.kandtopk_count_eventsare not modeled.Facts example:
Interface for later sessions
{item index}:{query}#{k}, unique per Raqe.OptimizerItem.occurrencesis the exact leaf count;query_frequency_hzis derived from it.load_flat_atomic_cost_tableerrors on non-finite or negative rows.SumOrCountcapability, so after the avg rewrite one exact-sum deployment can serve both. Don't deploy avg plans until Sketchlib rust migration #161 splits the capability.MilpWorkload.raqe_items[i]is theOptimizerItemindex thatraqes[i]came from. Several Raqes can share one item.solve_milpreturns the full candidate list plusMilpSolution.mapping, which indexes into that list.--allow-undeployable-familiesis not wired;solve_milppassesfalse.--milpalready exists onasap-optimizer-cli. A4 removes greedy, the--label-set-facts/--atomic-cost-workloadflags, andlabel_set_facts.rs.extract_hinted_itemsstill usesLabelSetFactsError's hint variants, so move those when deleting.Smoke run
Real cost export
sketch-bench/out_10_6_26_1342/rqe_atomic_costs.json, scrape 15s. Workload:sum by (job)×2 (two groups),rate[5m],quantile_over_time[5m],topk(5),max_over_time[1h], all at T=60s,accuracy_sla: 0.99.Test plan
accuracy_sla> 1 rejectedUnservablecargo clippy -p asap_planner --all-targets -D warnings,cargo test -p asap_planner(pre-commit also ran workspace check/clippy/test).🤖 Generated with Claude Code