Skip to content

rqe-optimizer: per-phase cost model, workload facts, weighted AUCCost objective - #145

Merged
milindsrivastava1997 merged 18 commits into
mainfrom
rqe-milp-objective
Oct 6, 2026
Merged

milindsrivastava1997 merged 18 commits into
mainfrom
rqe-milp-objective

Conversation

@milindsrivastava1997

@milindsrivastava1997 milindsrivastava1997 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Reworks rqe-optimizer's cost model and MILP objective. Covers #143 S3, S7, S2 (plus metric/grouping_labels from S1) and half of S6.

  • Objective: milp::minimize(..., Objective::AUCCost { w_cpu, w_mem }) minimizes w_cpu · CPU + w_mem · memory_GiB, default (1, 0). minimize_tco, minimize_cost, MachineFamily and the EC2 pricing snapshot are gone.
  • Per-phase costs: ingest, merge, query and storage, each with CPU and memory (table below).
  • Workload facts (S2): MetricFacts { labels, scrape_interval_secs, cardinality } replaces LabelSetInfo. λ is derived from it.
  • Retained memory (S7): closed windows are (L − x)/y + 1.
  • Candidates (S6, half): x and y must be multiples of the scrape interval.

Also: analytical_cost_model.rs (formerly objectives.rs) now holds the one copy of the cost formulas, which score() and the MILP share (they were duplicated before), and candidate dominance compares ingest memory and closed-window storage.

Not run end to end: small_problem --milp needs a fresh scripts/export_rqe_optimizer_costs.sh run. The older local table has no exact-increase rows, so the rate RQEs are unservable with it. --candidates-only runs.

Cost model

Deployment with grouping labels G, window x, slide y. RQE with lookback L, interval T. Measured per-instance costs m, c_ins, c_mrg, c_qry.

Phase CPU (CPU-sec/sec) Memory (bytes)
Ingest λ · (x/y) · c_ins card(G) · m · x/y (open windows)
Merge card(G) · (L/x − 1) · c_mrg / T card(G) · m (one accumulator per group), 0 when L == x
Query card(G) · c_qry / T card(G) · 8 B; TopK card(G) · 32 · 16 B
Storage 0 card(G) · m · ((max L − x)/y + 1) (closed windows)

λ = cardinality[metric.labels] / scrape_interval_secs.

$$\min\ w_{cpu} \cdot \text{CPU} + w_{mem} \cdot \text{Memory}_{GiB}$$ $$\text{CPU} = \sum_d \text{ingest}^{cpu}_d\,u_d + \sum_{(r,d)\in E}\left(\text{merge}^{cpu}_{rd} + \text{query}^{cpu}_{rd}\right) z_{rd}$$ $$\text{Memory} = \sum_d \left(\text{ingest}^{mem}_d\,u_d + \text{storage}^{mem}_d\right) + \sum_{(r,d)\in E}\left(\text{merge}^{mem}_{rd} + \text{query}^{mem}_{rd}\right) z_{rd}$$ $$\text{storage}^{mem}_d \ge \text{card}(G_d)\, m_d \left(\tfrac{L_r - x_d}{y_d} + 1\right) z_{rd} \quad \forall (r,d)\in E$$

Shared constraints: $\sum_d z_{rd} = 1$, $z_{rd} \le u_d$, $u_d \le \sum_r z_{rd}$. Latency bounds exclude pairs, so $z_{rd} = 0$ when $\text{lat}_{rd}$ exceeds the bound.

CPU is the mean, i.e. the area under the curve. Plans are sized for average load, not bursts.

Design decisions

Settled in a grilling session. Each entry gives the question and the answer as agreed.

Before grilling

  • Scope: "Let's just tackle them all here" in rqe-optimizer: per-phase cost model, workload facts, weighted AUCCost objective #145, rather than splitting into separate issues.
  • Ingest memory: open windows only. Closed windows go in a separate Storage phase with CPU = 0.
  • Merge memory: card(G) · m, one accumulator per group, with all groups merged at once. The windows folded in are live objects already counted in storage, so they are not counted again. A direct query (L == x) merges nothing and has no merge memory.
  • Query labels: a query is served only by a deployment with exactly the same G for now. No regrouping of finer deployments.
  • Cost scaling with keys: costs should ideally grow with keys per instance. Superseded by Q15.
  • Objective: remove Tco, and rename Cost to AUCCost.

Q1. Do S1 and S2 come into #145? S2 in full, plus only metric and grouping_labels from S1. The Raqe rename, milliseconds, spatial_filter and the latency SLA on the query stay in S1.

Q2. How are costs scaled with keys per instance? Per-family laws with measured_keys. Superseded by Q15.

Q3. Which parts of S4b come in? Shape (PerGroup/Shared) × law (Fixed/PerKey) in. Mergeable/subtractable, the key tracker and HydraKLL stay in S4b. Shape × law later dropped by Q15.

Q4. Formula for storage? Adopt the S7 formula: closed windows (L − x)/y + 1, for the longest L the deployment serves.

Q5 / Q6. How do the phases feed the objective, and who provides the weights? Simplify the objective to w_cpu · CPU + w_mem · memory, starting at (1, 0). Memory is option C: the sum of all memory terms, as if every query evaluates at once. The weights are machine-family constants named w_cpu and w_mem.

Q7. How does #145 relate to #141? Independently. #141 (CostModel::Usage/Peak, minimize_model_cost) overlaps with this and will conflict in milp.rs.

Q8. What happens to MachineFamily's vcpu, memory_gib and usd_per_hour? Drop them; no $/h reporting.

Q9. Where do the weights come from? Option A: delete the EC2 pricing infrastructure (snapshot, fetch_ec2_pricing.py, from_pricing_json, --machine-family). The weights are given directly.

Q10. Is MachineFamily still needed? No, delete it. The weights live in Objective::AUCCost { w_cpu, w_mem }.

Q11. Should merge and query CPU scale with keys for PerKey families? Agreed, scale merge and query too. Superseded by Q15.

Q12. Is K (key labels) needed, and is the top-k heap modeled? Skip K, since no in-scope family is PerKey with non-empty K. Treat top-k as fixed at the benchmark's CMS_HEAP_TOP_K = 32: the heap is excluded from memory and included in CPU.

Q13. What is the S2 data model? Final shape:

  • MetricFacts { labels, scrape_interval_secs, cardinality: BTreeMap<LabelSet, u64> }, keyed by metric name.
  • λ = cardinality[labels] / scrape_interval, derived only, no override. This reverses Tracking: make rqe-optimizer the solver for ASAPQuery's planner #143's "arrival rate is an explicit per-metric input".
  • Rqe and Deployment carry metric and grouping_labels. Eligibility and sharing match on (capability, metric, grouping_labels).
  • x and y must be multiples of the scrape interval (half of S6).
  • Validation happens up front and reports every problem at once:
    • a metric is missing
    • G ⊄ labels
    • a cardinality entry is missing for G or for labels
    • card = 0 or scrape = 0
    • card(G) > card(labels)

Q14. What do the scored output and the Pareto front look like?

  • Keep the Pareto front, with the vector (cpu, memory, {latency}).
  • Objectives becomes per phase: ingest, merge, query and storage, each a PhaseCost { cpu_secs_per_sec, memory_bytes }, plus query_latency_secs. It has cpu() and memory_bytes() totals. Later renamed PlanCost, in analytical_cost_model.rs (formerly objectives.rs), since it is a plan's cost and Objective already names what the MILP minimizes.
  • Remove peak_query_memory_bytes, retained_memory_bytes and tco_cpu_secs_per_sec.
  • w_mem is per GiB.

Q15. Drop shape × law and measured_keys? Yes. The export already divides the exact accumulators' memory and merge by groups_per_instance (aqpbm-core/src/atomic_costs.rs), so card(G) × m is already correct for every in-scope family. Defer both to #142, where HydraKLL (Shared + Fixed) first needs them.

Q16. How many bytes per output entry for query memory? Constants: 8 B per value, and 16 B per top-k (key, count) entry, with 32 entries per group.

Q17. Final scope

In #145:

  • S3 (also remove the peak-memory bound)
  • the weighted AUCCost objective
  • the per-phase costs
  • S7
  • S2, plus metric and grouping_labels
  • the S6 scrape-multiples filter
  • per-phase Objectives, and the Pareto front kept
  • docs and tests

Not in #145:

Overlap

Test plan

  • MILP matches brute force on the new objective at weights (1, 0), (0, 1) and (1, 4), including µs-scale costs
  • w_mem flips the choice between a CPU-heavy and a memory-heavy deployment
  • a unit test for each phase formula, the S7 closed-window count, a direct query (L == x), and storage sized by the longest lookback of a shared deployment
  • validation reports each kind of bad input at once
  • scrape-interval filter on candidates
  • cargo test --workspace, cargo clippy --workspace --all-targets -- -D warnings (pre-commit)
  • small_problem --milp on a fresh cost export

🤖 Generated with Claude Code

@milindsrivastava1997
milindsrivastava1997 marked this pull request as ready for review October 5, 2026 19:22
@milindsrivastava1997 milindsrivastava1997 changed the title rqe-optimizer: one MILP entry point with an Objective enum rqe-optimizer: one MILP entry point, AUCCost as the only objective Oct 5, 2026
milindsrivastava1997 and others added 2 commits October 5, 2026 20:21
Replace minimize_tco/minimize_cost with minimize(..., Objective), where
Objective::{Tco, Cost(&MachineFamily)} owns its scaling units, extra
variables, rows and goal. The shared assignment/activation/bound rows no
longer branch on the family, and TCO no longer adds an unused instances
variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MILP now only minimizes hourly price on a machine family. AUCCost
names what it prices: mean CPU-sec/sec, the area under the load curve,
so bursts are not priced. small_problem --milp now requires
--machine-family. The former TCO tests run under AUCCost with the same
expected mappings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@milindsrivastava1997
milindsrivastava1997 changed the base branch from rqe-optimizer-capabilities to main October 6, 2026 00:23
@milindsrivastava1997
milindsrivastava1997 marked this pull request as draft October 6, 2026 00:23
@milindsrivastava1997
milindsrivastava1997 marked this pull request as ready for review October 6, 2026 00:24
@milindsrivastava1997 milindsrivastava1997 changed the title rqe-optimizer: one MILP entry point, AUCCost as the only objective rqe-optimizer: per-phase cost model, workload facts, weighted AUCCost objective Oct 6, 2026
milindsrivastava1997 and others added 14 commits October 5, 2026 21:37
- Costs split into ingest, merge, query and storage, each with CPU and
  memory. objectives.rs holds the one copy of the formulas; score() and
  the MILP share it.
- Storage counts closed windows as (L - x)/y + 1 for the longest lookback
  (#143 S7). Ingest memory is open windows only.
- MetricFacts { labels, scrape_interval_secs, cardinality } replaces
  LabelSetInfo (#143 S2). The arrival rate is derived as
  card(labels) / scrape interval. validate_facts reports every bad fact.
- Rqe and Deployment carry metric and grouping_labels.
- Candidate windows and slides are multiples of the scrape interval
  (#143 S6, half).
- Objective::AUCCost { w_cpu, w_mem } minimizes w_cpu * CPU +
  w_mem * memory GiB, default (1, 0). MachineFamily, the EC2 pricing
  snapshot and fetch_ec2_pricing.py are removed, as is the peak-memory
  bound (#143 S3).
- Objectives is per phase; the Pareto vector is (CPU, memory, latencies).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Per deployment or per RQE, per group, per window and per key, plus what
is not shared between RQEs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Descriptive names in place of single letters, named ingest helpers in
candidate pruning, and shorter comments without issue references.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
State that gcd(x, T) is the largest slide serving an RQE, why each subset
of RQEs gets the gcd of theirs, and why one pass computes all of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stored windows are live objects already counted in storage, so a merge
adds only the accumulator per group. A query with L == x merges nothing.
Candidate pruning now compares merge memory per served RQE, since it is
no longer the same instance size for every RQE.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
candidate_dominates and strictly_better walked the same costs twice. A
single dominance() collects (replacement, original) cost pairs once and
reports strictly better, equal or not dominating.

Docs: define open and closed instances, fix the candidate pseudocode
indentation, and reword card(G) and merge memory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Defines family, configuration, instance and the other terms the cost
model uses, links scripts/export_rqe_optimizer_costs.sh, explains what a
key is and why quantile sketches are treated as fixed size, and drops
the per-cost breakdown table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… to PlanCost

The module holds the per-phase cost formulas and score(); its output is
a plan's cost, not an objective, and Objective already names what the
MILP minimizes. In milp.rs, name the storage variables and cost closures
for what they hold: deployment_storage_cost,
storage_cost_for_rqe_and_deployment and merge_and_query_cost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
in which case it appears as another subset gcd.
`g_i = gcd(x, T_i)` is the largest slide that serves RAQE `i`.

**Windows.** An RAQE merges `S_i / x` whole windows, so `x` must divide `S_i`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Given that you model the window sharing in the doc, can you model the sharing across queries in the doc as well, which will guide the MILP search space?

objectives.tco_cpu_secs_per_sec,
];
vector.extend(&objectives.query_latency_secs);
fn pareto_vector(plan_cost: &PlanCost) -> Vec<f64> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What does pareto in milp refer to?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. MILP doesn't use the Pareto vector. This was added earlier for manual exploration of different plans that are the Pareto frontier of "CPU, memory, latency". Dead code right now.

@milindsrivastava1997
milindsrivastava1997 merged commit c8cf451 into main Oct 6, 2026
2 checks passed
zzylol added a commit that referenced this pull request Oct 6, 2026
Runner for ASAPQuery#777's comparison on main's planner: one metric per
stream of the evaluation table, ASAP by milp::minimize, PerQuery by
minimize per RQE, AutoSketch by autosketch::plan, all scored by
analytical_cost_model::score (#145) at #777's weights (CPU only; Fargate
$/vCPU-h and $/GB-h) and SLA grid {0.01, 0.1, 1, 10} ms and none. Each
RQE's accuracy rides in the cost entries: acc@<id> (with merged values in
merge_accuracy, #154) for ASAP, as@<id> for AutoSketch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zzylol added a commit that referenced this pull request Oct 6, 2026
The two cost models are gone (#145's weighted objective replaces them, in
the runner of the PRs below). This keeps ASAP's one-time profiling time
(#777 section 7) and documents how the reduced grid is run; results are
committed after the full run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants