Skip to content

Tracking: make rqe-optimizer the solver for ASAPQuery's planner #143

Open

Description

@milindsrivastava1997

Goal

Make rqe-optimizer's MILP the solver behind ASAPQuery's asap-planner-rs. ASAPQuery calls it as a library (git dependency, pinned rev), converts its query workload into Raqes, and converts the returned plan into StreamingConfig / InferenceConfig. ASAPQuery side: see the linked ASAPQuery tracker.

Out of scope for this round: Subtract query method (follow-up), spatial-filter sharing (ASAPQuery errors on spatial filters for now), memory bounds.

TODO

  • S1. Raqe input model
    • Rename Rqe → Raqe with fields: id, capability, metric, grouping_labels, spatial_filter, lookback_ms, interval_ms, accuracy_metric, accuracy_sla, accuracy_direction, latency_sla.
    • Latency limit moves from MilpBounds into Raqe.
    • Units: milliseconds instead of whole seconds.
    • Stream identity for eligibility and sharing = (metric, spatial filter, grouping labels).
  • S2. Workload facts input (replaces LabelSetInfo) (sketch-bench#145)
    • Per metric: full label list and arrival_rate_per_sec.
    • Cardinality per (metric, label set); input/output cardinality of a Raqe come from this.
  • S3. Objective cleanup (sketch-bench#145)
    • Keep only minimize_cost (EC2 machine family); remove minimize_tco.
    • Remove the peak query-memory bound.
  • S4a. Capabilities and families (sketch-bench#144)
    • Capabilities: SumOrCount, MinOrMax, RateOrIncrease, Quantile (+ HydraKLL), Cardinality, TopK. Freq removed.
  • S4b. Family properties and key tracker (includes the cost-model part of sketch-bench#142)
    • Per-family property table: mergeable, subtractable, subpopulation-aware, needs key tracker.
    • Non-mergeable families eligible only when L == x.
    • Subpopulation-aware families: one instance, not × cardinality, in CPU and memory.
    • Key tracker: deployments of families that need one also pay a DeltaSet's ingest CPU and retained memory (∝ output cardinality); the plan flags it.
  • S5. Cost rows
  • S6. Candidate generation inputs
    • scrape_interval_ms: candidate x and y must be multiples.
    • Allowed families: only listed variants become candidates.
  • S7. Retained memory (sketch-bench#145)
    • Closed windows (L − x)/y + 1 plus open x/y (today: (x + L)/y).
  • S8. Plan output (sketch-bench#168)
    • Per deployment: variant, sketch_config, metric, grouping_labels, x, y, key tracker, retained instance count (max over the Raqes it serves).
    • Per Raqe: deployment index, merged instance count n = L/x.
    • Objectives including $/h.

Follow-ups

  • S9 (follow-up). Subtract query method (needs subtract_cpu_secs; asap_sketchlib has no subtract yet).

Order and dependencies (both repos)

Labels: S1–S8 = sketch-bench #143 items in order (S1 Raqe model, S2 facts, S3 objective cleanup, S4a capabilities, S4b properties + key tracker, S5 cost rows, S6 candidate inputs, S7 retained memory, S8 plan output). A1–A6 = ASAPQuery ProjectASAP/ASAPQuery#790 items (A1 dependency/build, A2a avg rewrite, A2b inputs → Raqe, A3 plan → configs, A4 disable old optimizer, A5 wiring, A6 housekeeping).

sketch-bench                                   ASAPQuery
S3 objective cleanup ──┐                       A1 dependency + HiGHS build (vs current main)
                       │                       A2a avg → sum/count rewrite
S1+S2 Raqe + facts ────┼─► S6 scrape / allowed families ─┐
S4a capabilities ──────┤                                 ├─► S8 plan output ─┐
S5 cost rows ──────────┴─► S4b properties + key tracker ─┘                   │
                               + S7 retained memory                          ▼
                                        A2b inputs → Raqe ◄── S1, S2, S4a, S6
                                        A3 plan → configs ◄── S8
                                        A4+A5 disable old + wire CLI ◄── A2b, A3
                                        A5' asap-planner --planner milp
                                        A6 housekeeping (any time)
Phase Work Depends on
0 (now, parallel) S3; S4a (commit first: S1/S4b conflict with it); S5; A1 (via [patch] path dep); A2a —
1 S1+S2 (one PR), then S6 S3, S4a
2 S4b+S7 (one PR: same per-deployment memory/CPU formulas) S4a, S5, S1
3 S8 plan output → pin sketch-bench rev in ASAPQuery S4b, S7, S6
4 A2b and A3 in parallel; then A4+A5 (one PR, keeps CLI runnable); then A5′ --planner milp S1, S2, S4a, S6 / S8
any A6 housekeeping —

Critical path: S1+S2 → S4b+S7 → S8 → A3 → A4+A5. Pin the sketch-bench rev only after S8; until then ASAPQuery uses a [patch] path dependency.

Sessions (for agents)

If you are an agent told "you are session X": find your row, check that the PRs for everything in Needs are merged (gh pr list --state merged, and the ticked boxes above), read the merged PRs' descriptions for interface changes, then do Items. Don't edit files belonging to other sessions' items.

Session Phase Start in Items Needs
0a 0 sketch-bench finish + merge sketch-bench#144 (S4a), then S3 —
0b 0 sketch-bench S5 remainder: run the export, DeltaSet benchmark, HydraKLL benchmark (benchmark part of sketch-bench#142) —
0c 0 ASAPQuery A1, A2a, A6 —
1a 1 parent dir with both repos S1+S2, then S6 0a
2a 2 sketch-bench S4b+S7 (includes the cost-model part of sketch-bench#142) 1a, 0b
2b 2 ASAPQuery, [patch] path dep on local sketch-bench A2b 1a, 0c
3a 3 parent dir with both repos S8 + A3, then pin the sketch-bench rev in ASAPQuery 2a, 2b
4a 4 ASAPQuery A4+A5, then A5′ (--planner legacy|milp) 3a

Done means: tests and clippy pass; one PR per item group; Conventional Commit title; tick the item here; describe any interface change later sessions depend on in the PR description. Use a git worktree per session. Follow ASAPQuery AGENTS.md. Never git commit --amend after a failed pre-commit hook; re-run the commit.

Decisions (don't revisit)

  • ASAPQuery calls sketch-bench's rqe-optimizer as a library: git dependency pinned to a rev (pin after S8); [patch] path dependency during development.
  • Objective: minimize_cost (EC2 machine family) only; minimize_tco and the peak-memory bound are removed.
  • The Freq capability was removed on purpose. CMS/CountSketch appear only as top-k heaps.
  • Subtract is deferred: query methods are Direct (L == x) and Merge only.
  • Spatial filters: ASAPQuery errors on them for now; sharing across filters is later.
  • Arrival rate is an explicit per-metric input (not derived from cardinality). Units are milliseconds.
  • x and y must be multiples of the scrape interval (sketch-bench input).
  • Families the engine can't deploy are excluded via an allowed-families input; --allow-undeployable-families prints the plan and $/h without writing YAML.
  • Exact families report their accuracy metrics as 0.
  • Accuracy: ASAPQuery maps accuracy_sla to one (accuracy_metric, accuracy_direction) per capability; families within a capability share metric names.
  • Every family has properties: mergeable, subtractable, subpopulation-aware, needs key tracker. Non-mergeable → only L == x. HydraKLL needs a DeltaSet key tracker, costed in the MILP.
  • Merged instance count n = L/x is per Raqe (query CPU, Direct/Merge, read counts). Retained instance count is per deployment: open x/y plus closed (L−x)/y + 1 for the longest L it serves (memory cost, store retention).
  • ASAPQuery's old optimizer modules (candidate_gen, cost_model, sketch_properties, constants, greedy) are commented out, not deleted, for now.

🤖 Generated with Claude Code

Activity

  1. milindsrivastava1997 commented on Oct 5, 2026

    @milindsrivastava1997
    ContributorAuthor
  2. milindsrivastava1997 commented on Oct 6, 2026

    @milindsrivastava1997
    ContributorAuthor

    Decision: the per-phase cost model goes into #145, along with the rest of S3

    #145 removed minimize_tco. Only the cost objective is left, now named Objective::AUCCost. It prices mean CPU-sec/sec (the area under the load curve), so it sizes for average load, not bursts. #145 will also take on:

    • The rest of S3: remove the peak query-memory bound (MilpBounds.max_peak_query_memory_bytes).
    • An explicit per-phase cost model. Each phase (ingest, merge, query, storage) gets a CPU function and a memory function, and each formula is confirmed separately. Today the terms are mixed together: retained_memory combines open and closed windows, and query memory (output size) isn't modeled at all.

    Decisions so far (deployment groups G, sketch keys K, n_G = card(G)):

    Phase CPU Memory
    Ingest λ · (x/y) · c_ins open windows only: n_G · m · x/y
    Merge n_G · (L/x − 1) · c_mrg / T n_G · m (all groups merged at once)
    Query n_G · c_qry / T output size: n_G values, n_G · k for TopK (new)
    Storage 0 closed windows still inside the longest lookback (S7)
    1. Per-instance costs (m, c_ins, c_mrg) should grow with the number of keys per instance, card(G ∪ K) / n_G, rather than stay constants measured at the benchmark's key count.
    2. Ingest memory counts only open windows. Closed windows move to a separate Storage phase that has memory but CPU = 0.
    3. Merge memory is n_G · m: every group's merged accumulator is held at the same time.
    4. A query is served only by a deployment with exactly the same G. A finer deployment that gets regrouped at query time is out of scope.

    TopK note: a G = {service} deployment keyed on K = {endpoint} (one CMS+heap per service) answers topk by (service) (sum_over_time(...)) only if G ∪ K covers all of the metric's labels. So K is "the metric's labels minus G", which follows from S1 (grouping_labels) plus S2 (the full label list).

    We're settling the exact scope in a grilling session. #145's description will list which S-items it covers once that's done.

  3. milindsrivastava1997 commented on Oct 6, 2026

    @milindsrivastava1997
    ContributorAuthor

    Scope of #145 is settled, and it changes two Decisions here. The full Q&A is in #145's description.

    #145 now covers S3 (including removing the peak-memory bound), S7, S2 plus metric/grouping_labels from S1, and half of S6 (x and y must be multiples of the scrape interval). The rest of S1, S4b, S5, the rest of S6 and S8 are unchanged.

    Changes to "Decisions (don't revisit)":

    • Arrival rate is now derived, not an explicit input: λ = cardinality[metric.labels] / scrape_interval_secs, from MetricFacts { labels, scrape_interval_secs, cardinality }. There's no override, and validation requires a cardinality entry for the metric's full label set. Previously λ and C were independent inputs, and nothing kept them consistent.
    • The objective is no longer minimize_cost on an EC2 family. It is Objective::AUCCost { w_cpu, w_mem }, which minimizes w_cpu · CPU + w_mem · memory_GiB, default (1, 0). Memory sums the ingest, storage, merge and query terms. MachineFamily and the EC2 pricing snapshot are removed.

    Note for S4b and #142: the export already divides the exact accumulators' memory and merge cost by groups_per_instance, so card(G) × m prices them correctly today. The "subpopulation-aware" property is only needed once a Shared + Fixed family such as HydraKLL arrives.

  4. milindsrivastava1997 commented on Oct 6, 2026

    @milindsrivastava1997
    ContributorAuthor

    #145 covers S2, S3 and S7 completely (ticked above), and S1 and S6 partially.

    Complete:

    • S2, workload facts: MetricFacts { labels, scrape_interval_secs, cardinality } replaces LabelSetInfo. A RAQE's input cardinality is card(G); its output is card(G) values, or card(G) × 32 for top-k. The arrival rate is derived as card(labels) / scrape_interval, not given; see the earlier comment.
    • S3, objective cleanup: minimize_tco and the peak query-memory bound are removed. The objective is Objective::AUCCost { w_cpu, w_mem }, not minimize_cost on an EC2 family; see the earlier comment.
    • S7, retained memory: closed windows (L − x)/y + 1 as the storage phase, and open windows x/y as ingest memory.

    Partial:

    • S1: done: metric and grouping_labels on RAQEs and deployments. Left: the Rqe → Raqe rename (only the design doc says RAQE so far), millisecond units, spatial_filter, and moving the latency SLA from MilpBounds onto the RAQE.
    • S6: done: candidate x and y are multiples of the metric's scrape interval (per metric, in seconds). Left: the allowed-families input.

    The cost table also needs work to fit the new model; that's tracked in #147.

  5. milindsrivastava1997 commented on Oct 6, 2026

    @milindsrivastava1997
    ContributorAuthor

    S5 follow-up from S4b: HydraKLL accuracy at more group counts

    S4b gates subpopulation-aware families on group count: hydra-kll is eligible only when card(G) ≤ query_accuracy["subpopulations"] of its row. Today that's the single point the export measures, about 200 groups on hydra_kll_disjoint_labels.yaml. So hydra-kll is never planned for larger groupings, even where its accuracy might still meet the SLA.

    To loosen the gate, S5 should measure hydra-kll rows at several group counts (e.g. 10², 10³, 10⁴, 10⁵ via the key1 population in the spec). The optimizer then takes the smallest-memory row that covers card(G) and meets the SLA, with no further optimizer change. Interpolating a curve between points is a later step if needed.

    Caveat: Hydra inserts every label subset of its key, so its error really depends on the total subpopulation count, not just card(G). The gate ignores this for now.

    🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions