Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/devtools/src/bin/stage_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ fn run(args: Vec<String>) -> Result<(), Failure> {
let mut cost_inputs_path: Option<String> = None;
let mut enforce_accuracy_guarantees = false;
let mut exhaustive = false;
// `--milp-time-limit-s` limits the MILP search.
// `--milp-time-limit-s` bounds the facade's planning budget (Pass 2 and
// its rounds); Stage 3's MILP always runs to optimality (#1185).
let mut milp_time_limit_ms: Option<u64> = None;
let mut max_combinations_given = false;
let mut write_memo = true;
Expand Down
5 changes: 3 additions & 2 deletions crates/devtools/tests/corpus_snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ fn hit_time_limit(document: &Value) -> bool {
report["pass1_ms"].as_f64().unwrap_or(0.0) + rounds >= limit
}

/// The selected plan's search status (`optimal` or `deadline_reached`), or
/// null without a plan.
/// The selected plan's search status (`optimal`; `deadline_reached` in
/// documents from before the MILP ran without a time limit), or null
/// without a plan.
fn search_status(document: &Value) -> &Value {
&document["stage3_plan_selection"]["search_report"]["status"]
}
Expand Down
2 changes: 1 addition & 1 deletion crates/devtools/tests/corpus_snapshots/o11y_bench/q05.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"accuracy_guarantees": "enforced",
"materialized_views": [],
"materialized_views_error": "raw-data window of node 1 is unsupported: a subquery or a TimeShift above the range",
"proven_optimal": false,
"proven_optimal": true,
"query": "max_over_time((sum(rate(http_requests_total{job=\"order-service\",status=~\"5..\"}[5m])) / sum(rate(http_requests_total{job=\"order-service\"}[5m])))[6h:1m])",
"selected": {
"accepted": [
Expand Down
2 changes: 1 addition & 1 deletion crates/devtools/tests/corpus_snapshots/o11y_bench/q19.json
Original file line number Diff line number Diff line change
Expand Up @@ -6242,7 +6242,7 @@
"stage2_node_id": 7
}
],
"proven_optimal": false,
"proven_optimal": true,
"query": "sum(increase(http_requests_total{job=\"payment-service\",status=~\"5..\"}[1h])) / sum(increase(http_requests_total{job=\"payment-service\"}[1h]))",
"selected": {
"accepted": [
Expand Down
2 changes: 1 addition & 1 deletion crates/devtools/tests/corpus_snapshots/o11y_bench/q25.json
Original file line number Diff line number Diff line change
Expand Up @@ -4562,7 +4562,7 @@
"stage2_node_id": 7
}
],
"proven_optimal": false,
"proven_optimal": true,
"query": "sum(rate(http_requests_total{status=~\"5..\",job=~\"user-service|order-service|payment-service\"}[1h])) / sum(rate(http_requests_total{job=~\"user-service|order-service|payment-service\"}[1h]))",
"selected": {
"accepted": [
Expand Down
13 changes: 2 additions & 11 deletions crates/devtools/tests/sketch_plan_snapshots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,7 @@
//! select a different plan on a slower machine. Only a search that proves its
//! plan optimal is independent of speed, so every workload must end with
//! status `optimal` and no tumbling-window width left unsearched; any other ending
//! fails instead of being recorded. A workload with a
//! `deadline_reached_reason` in the manifest may instead end with status
//! `deadline_reached` (no tumbling-window width left unsearched); its snapshot records
//! that status, so the plan, cost and status must still match. Each
//! kept workload gave the same plan in three runs and on one core.
//! fails instead of being recorded. Each kept workload gave the same plan in three runs and on one core.
//! Workloads whose search stops at a budget (the raw-retained memo examples,
//! full random-mixed workloads, three or more quantile queries over 10,000
//! series, the `or` and `COUNT(DISTINCT)` workloads, single quantiles under
Expand Down Expand Up @@ -146,12 +142,7 @@ fn sketch_plans_match_snapshots() {
));
continue;
}
// A `deadline_reached_reason` accepts a search that ended at its
// deadline after every tumbling-window width; plan and cost are still compared.
let deadline_accepted = entry.get("deadline_reached_reason").is_some()
&& search_status(&document) == "deadline_reached"
&& document["planning_report"]["tumbling_window_widths_pending"] != json!(true);
if !proven_optimal(&document) && !deadline_accepted {
if !proven_optimal(&document) {
failures.push(format!(
"{id}: the plan is not proven optimal (status {}, tumbling-window widths \
pending {}), so a round's budget may have chosen it; drop the \
Expand Down
6 changes: 2 additions & 4 deletions crates/devtools/tests/sketch_plan_snapshots/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@
"id": "shared-quantiles-1h-mvp",
"covers": "two queries share one DDSketch pane chain (q0.5 and q0.9 over 1 h)",
"workload": "tools/dag-viewer/memo-examples/promql/rangeheavy-t5-q05-q09-1h.json",
"deployment": "mvp",
"deadline_reached_reason": "the MILP stalls numerically (lower bound about 8e-7 relative below the cost) and reports deadline_reached; the plan and cost are the same as when it was proven optimal"
"deployment": "mvp"
},
{
"id": "nested-windows-15m-1h-mvp",
Expand All @@ -37,8 +36,7 @@
"id": "quantile-single-mvp",
"covers": "single quantile with an accuracy target selects a KLL pane chain",
"workload": "crates/devtools/tests/sketch_plan_snapshots/workloads/quantile-single.json",
"deployment": "mvp",
"deadline_reached_reason": "the MILP stalls numerically (lower bound about 8e-7 relative below the cost) and reports deadline_reached; the plan and cost are the same as when it was proven optimal"
"deployment": "mvp"
},
{
"id": "quantile-grouped-mvp",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1142,11 +1142,11 @@
"stage2_node_id": 1
}
],
"proven_optimal": false,
"proven_optimal": true,
"queries": [
"quantile_over_time(0.99, data[5m])"
],
"search_status": "deadline_reached",
"search_status": "optimal",
"selected": {
"accepted": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2282,12 +2282,12 @@
"stage2_node_id": 1
}
],
"proven_optimal": false,
"proven_optimal": true,
"queries": [
"quantile_over_time(0.5, data[1h])",
"quantile_over_time(0.9, data[1h])"
],
"search_status": "deadline_reached",
"search_status": "optimal",
"selected": {
"accepted": [
{
Expand Down
11 changes: 4 additions & 7 deletions crates/devtools/tests/stage_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2605,13 +2605,10 @@ fn milp_search_objective_equals_plan_cost_on_example1_and_rangeheavy() {
let selection = &document["stage3_plan_selection"];
assert!(selects_a_plan(selection), "{args:?}: {selection}");
let report = &selection["search_report"];
// Optimal or not by the deadline, the selected plan's `plan_cost`
// equals the MILP's objective: the search errors on a difference,
// and no term is encoded inexactly here.
assert!(
report["status"] == "optimal" || report["status"] == "deadline_reached",
"{args:?}: {report}"
);
// The MILP runs to proven optimality, and the selected plan's
// `plan_cost` equals its objective: the search errors on a
// difference, and no term is encoded inexactly here.
assert_eq!(report["status"], "optimal", "{args:?}: {report}");
assert!(
report.get("inexact_objective").is_none(),
"{args:?}: {report}"
Expand Down
126 changes: 52 additions & 74 deletions crates/plan-selection/src/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
//! grow with the range-evaluation span of the queries reaching it
//! ([`asap_cost::tumbling_window_retention_ms`], [`asap_cost::retained_tumbling_windows`]).
//!
//! Every combination of these options is costed (at most
//! [`MAX_CONTEXTS`]); a combination `asap-cost` cannot cost is skipped
//! Every combination of these options is costed; a combination
//! `asap-cost` cannot cost is skipped
//! (no plan can use it). When the numbers agree, the usage is
//! plan-independent; otherwise [`CandidateData::usage_depends_on`] says which
//! options changed it, and [`CandidateData::usage`] holds the first costed
Expand Down Expand Up @@ -125,11 +125,6 @@ use crate::SelectionModels;

use asap_types::stages::{PhysicalCandidateId, ReaderConstraint};

/// The most contexts (combinations of producer outputs, scan windows and
/// pane retentions) one candidate is costed under; beyond it the candidate's usage
/// is marked [`UsageContext::TooManyContexts`].
pub(crate) const MAX_CONTEXTS: usize = 256;

/// What the MILP is built from (module documentation).
#[derive(Debug, Clone)]
pub(crate) struct MilpInputs {
Expand Down Expand Up @@ -233,11 +228,6 @@ impl Default for CandidateData {
}
}

/// The most producer choices [`CandidateData::usage_per_producers`] lists; beyond
/// it the candidate's usage is marked [`UsageContext::TooManyContexts`] and
/// its first costed usage stands for every choice.
pub(crate) const MAX_PRODUCER_CHOICES: usize = 256;

/// One candidate's usage, from [`asap_cost::node_usage`] (module
/// documentation).
#[derive(Debug, Clone, Copy, PartialEq)]
Expand Down Expand Up @@ -300,8 +290,6 @@ pub(crate) enum UsageContext {
/// window from a source without `rows_held`); the usage is the first
/// costed one's.
UncostableContext,
/// More than [`MAX_CONTEXTS`] combinations: not all were costed.
TooManyContexts,
}

/// A candidate dominance deleted.
Expand Down Expand Up @@ -330,9 +318,8 @@ pub(crate) enum UnusableReason {
/// Some output's guarantee misses the target
/// ([`RejectionCause::AccuracyTargetMissed`](crate::report::RejectionCause::AccuracyTargetMissed)).
AccuracyTargetMissed,
/// Not checked, so not usable: more than
/// [`accuracy_positions::MAX_COMPOSITIONS`] outputs to build, or a node
/// that does not rebuild over its producers.
/// Not checked, so not usable: a node that does not rebuild over its
/// producers.
NotChecked,
}

Expand Down Expand Up @@ -1129,7 +1116,7 @@ fn window_read_through(path: &[&PhysicalASAPDAGNode]) -> ScanWindow {
/// windows (one per path through time shifts) and, per execution context,
/// the unions of the window kinds its readers there read (one scan run read
/// by several readers reads the union, `PhysicalASAPDAG::scan_window`):
/// every union of up to [`MAX_WINDOW_KINDS`] kinds, else the union of all.
/// every union of the kinds.
/// A read as an output or as a shareable candidate's own input is
/// `Unbounded`. Distinct; `Unbounded` alone when nothing reads it.
fn scan_window_options(
Expand Down Expand Up @@ -1239,24 +1226,21 @@ fn window_contains(outer: ScanWindow, inner: ScanWindow) -> bool {
}
}

/// The most window kinds of one scan run whose every union is costed (and
/// encoded exactly, [`crate::milp::cost_terms`]); beyond it only the union
/// of all.
pub(crate) const MAX_WINDOW_KINDS: usize = 4;

/// Adds to `windows` every union of a nonempty subset of `kinds` (only the
/// union of all beyond [`MAX_WINDOW_KINDS`]).
/// Adds to `windows` every union of a nonempty subset of `kinds`.
fn add_unions(kinds: &[ScanWindow], windows: &mut Vec<ScanWindow>) {
use crate::milp::cost_terms::scan_window_read_at as window_union;
let mut push = |window: ScanWindow| {
if !windows.contains(&window) {
windows.push(window);
}
};
if kinds.len() > MAX_WINDOW_KINDS {
push(window_union(kinds.iter().copied()));
return;
}
// Every union of the kinds is enumerated (#1185: no cap); a shift past
// the word size would silently enumerate nothing.
assert!(
kinds.len() < usize::BITS as usize,
"a scan run with {} window kinds has too many unions to enumerate",
kinds.len()
);
for set in 1usize..(1 << kinds.len()) {
push(window_union(
(0..kinds.len())
Expand Down Expand Up @@ -1624,12 +1608,12 @@ fn candidate_data(
.map(Vec::len)
.chain([windows.len(), retained.len()])
.try_fold(1usize, |total, options| total.checked_mul(options))
.unwrap_or(usize::MAX);
.expect("the number of context combinations fits in usize");
// Each costed combination: its usage, its output, and its picks
// (the option per input, the window and the retained panes).
let mut costed: Vec<(CandidateUsage, ProducerOutput<'_>, Picks)> = Vec::new();
let mut error = None;
for index in 0..combinations.min(MAX_CONTEXTS) {
for index in 0..combinations {
let mut rest = index;
let mut pick = |options: usize| {
let picked = rest % options;
Expand Down Expand Up @@ -1787,48 +1771,45 @@ fn candidate_data(
.try_fold(1usize, |total, producers| {
total.checked_mul(producers.len())
})
.unwrap_or(usize::MAX);
if choices > MAX_PRODUCER_CHOICES {
depends_on.push(UsageContext::TooManyContexts);
} else {
for index in 0..choices {
let mut rest = index;
let chosen: Vec<&(ProducerChoice, Vec<usize>)> = options_of_producers
.iter()
.map(|producers| {
let picked = &producers[rest % producers.len()];
rest /= producers.len();
picked
})
.collect();
let matching: Vec<&(CandidateUsage, usize)> = costed
.iter()
.zip(&given)
.filter(|((_, _, picks), _)| {
inputs_changing_usage.iter().zip(&chosen).all(
|(&input, (_, options))| options.contains(&picks.options[input]),
)
})
.map(|(_, gives)| gives)
.collect();
let costliest = matching.iter().copied().max_by(|a, b| {
let total = |usage: &CandidateUsage| {
usage.cost_per_execution.unwrap_or(0.0)
+ usage.standing_cost_per_second.unwrap_or(0.0)
+ usage.work_ms_per_execution
};
total(&a.0).total_cmp(&total(&b.0))
});
if matching.iter().any(|gives| Some(*gives) != costliest) {
result[class.0].producer_usage_ambiguous = true;
}
result[class.0].usage_per_producers.push((
chosen.iter().map(|(choice, _)| *choice).collect(),
costliest.copied(),
));
.expect("the number of producer choices fits in usize");
for index in 0..choices {
let mut rest = index;
let chosen: Vec<&(ProducerChoice, Vec<usize>)> = options_of_producers
.iter()
.map(|producers| {
let picked = &producers[rest % producers.len()];
rest /= producers.len();
picked
})
.collect();
let matching: Vec<&(CandidateUsage, usize)> = costed
.iter()
.zip(&given)
.filter(|((_, _, picks), _)| {
inputs_changing_usage
.iter()
.zip(&chosen)
.all(|(&input, (_, options))| options.contains(&picks.options[input]))
})
.map(|(_, gives)| gives)
.collect();
let costliest = matching.iter().copied().max_by(|a, b| {
let total = |usage: &CandidateUsage| {
usage.cost_per_execution.unwrap_or(0.0)
+ usage.standing_cost_per_second.unwrap_or(0.0)
+ usage.work_ms_per_execution
};
total(&a.0).total_cmp(&total(&b.0))
});
if matching.iter().any(|gives| Some(*gives) != costliest) {
result[class.0].producer_usage_ambiguous = true;
}
result[class.0].inputs_changing_usage = inputs_changing_usage;
result[class.0].usage_per_producers.push((
chosen.iter().map(|(choice, _)| *choice).collect(),
costliest.copied(),
));
}
result[class.0].inputs_changing_usage = inputs_changing_usage;
}
if retained.len() > 1 {
let retained_changes_usage = changes(&|a: &Picks, b: &Picks| {
Expand Down Expand Up @@ -1886,9 +1867,6 @@ fn candidate_data(
if uncostable_context {
depends_on.push(UsageContext::UncostableContext);
}
if combinations > MAX_CONTEXTS {
depends_on.push(UsageContext::TooManyContexts);
}
result[class.0].output_count = produced.len();
outputs[class.0] = Some(Ok(produced));
result[class.0].usage = Ok(first);
Expand Down
Loading
Loading