Skip to content

Commit f14bf7a

Browse files
zzylolclaude
andcommitted
feat(planner): declare counter metrics so CMS+heap can prove non-negative weights
DataWorkload gains `metric_types` (metric name -> Counter | Gauge, the Prometheus TYPE metadata). plan_stages passes it to Stage 1, and Pass 1 attaches the new `CounterSamples` non-negativity proof to a keyed summary update weighted by the sample value when the target's input is samples of a declared counter, through a time range and plain sums (sum_over_time). Any other operator, a gauge or an undeclared metric (whatever its suffix) gets no proof. Stage 3 and the runtime already accept any NonNegative proof for Count-Min, so their rule is unchanged. Example 1 declares http_requests_total a counter: all 64 candidates are valid and compile (24 Count-Min + heap were invalid). Stage 3 now selects P60 (Count-Min + heap over the exact sum_over_time accumulator, shared input) at 46.201 against P58 (all exact) at 52.201, now 8th. Refs #509, #580. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 6575cfe commit f14bf7a

14 files changed

Lines changed: 1889 additions & 468 deletions

File tree

‎crates/devtools/src/bin/stage_pipeline.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ use asap_types::ir::{OperatorNode, QueryRoot};
4444
use asap_types::types::AccuracyTarget;
4545
use asap_types::workload::{
4646
AccuracyRequirement, BatchEntry, DataArrival, DataDistribution, DataWorkload, DurationMs,
47-
Evidence, EvidenceSource, LatencyRequirement, PlanningWorkload, Predictability, Query,
48-
QueryLanguage, QueryRecurrence, QueryRequirements, QueryTimeScope, QueryWorkload, Rate,
47+
Evidence, EvidenceSource, LatencyRequirement, MetricType, PlanningWorkload, Predictability,
48+
Query, QueryLanguage, QueryRecurrence, QueryRequirements, QueryTimeScope, QueryWorkload, Rate,
4949
RepeatedDemand, RepeatingEntry, RepetitionInterval, TimeSelection,
5050
};
5151
use serde_json::{json, Value};
@@ -406,6 +406,8 @@ fn planner_layering_example1() -> PlanningWorkload {
406406
ingestion_rate: declared(Rate(1_000_000.0 / 15.0)),
407407
input_cardinality: declared(1_000_000),
408408
distribution: declared(DataDistribution::Zipf),
409+
// `http_requests_total` is a counter: its samples are never negative.
410+
metric_types: [("http_requests_total".into(), MetricType::Counter)].into(),
409411
}),
410412
}
411413
}

‎crates/integration-tests/tests/planner_layering_example1.rs‎

Lines changed: 56 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ use asap_types::ir::schema::FieldDataType;
2323
use asap_types::types::AccuracyTarget;
2424
use asap_types::workload::{
2525
AccuracyRequirement, DataArrival, DataDistribution, DataWorkload, DurationMs, Evidence,
26-
EvidenceSource, LatencyRequirement, PlanningWorkload, Predictability, Query, QueryLanguage,
27-
QueryRequirements, QueryTimeScope, QueryWorkload, Rate, RepeatedDemand, RepeatingEntry,
28-
RepetitionInterval, TimeSelection,
26+
EvidenceSource, LatencyRequirement, MetricType, PlanningWorkload, Predictability, Query,
27+
QueryLanguage, QueryRequirements, QueryTimeScope, QueryWorkload, Rate, RepeatedDemand,
28+
RepeatingEntry, RepetitionInterval, TimeSelection,
2929
};
3030

3131
/// Adapters from the Phase C stage APIs to the shapes these tests were written
@@ -331,6 +331,8 @@ fn example1_workload() -> PlanningWorkload {
331331
ingestion_rate: declared(Rate(1_000_000.0 / 15.0)),
332332
input_cardinality: declared(1_000_000),
333333
distribution: declared(DataDistribution::Zipf),
334+
// `http_requests_total` is a counter: its samples are never negative.
335+
metric_types: [("http_requests_total".into(), MetricType::Counter)].into(),
334336
}),
335337
}
336338
}
@@ -504,12 +506,31 @@ fn relational(payload: &LogicalASAPOperatorPayload) -> Option<String> {
504506
}
505507
}
506508

509+
/// Example 1 with `http_requests_total` declared `metric_type`, or undeclared.
510+
fn example1_workload_with(metric_type: Option<MetricType>) -> PlanningWorkload {
511+
let mut workload = example1_workload();
512+
let data = workload.data_workload.as_mut().expect("data workload");
513+
data.metric_types = metric_type
514+
.map(|t| [("http_requests_total".to_string(), t)].into())
515+
.unwrap_or_default();
516+
workload
517+
}
518+
507519
fn pipeline() -> (
508520
PlanningWorkload,
509521
Vec<LogicalCandidate>,
510522
Vec<PhysicalCandidate>,
511523
) {
512-
let workload = example1_workload();
524+
pipeline_for(example1_workload())
525+
}
526+
527+
fn pipeline_for(
528+
workload: PlanningWorkload,
529+
) -> (
530+
PlanningWorkload,
531+
Vec<LogicalCandidate>,
532+
Vec<PhysicalCandidate>,
533+
) {
513534
let logical = stage1_logical_asap(&workload, &stage0_logical(&workload));
514535
let physical = stage2_physical(&workload, &logical);
515536
(workload, logical, physical)
@@ -887,21 +908,18 @@ fn compile_in_runtime(p: &PhysicalCandidate) -> Result<(), String> {
887908
.map_err(|e| format!("{} ({}): {e}", p.id, p.label))
888909
}
889910

890-
/// Runtime capability check (added by the implementer, not part of the
891-
/// spec): the physical planner compiles every candidate Stage 3 finds valid,
892-
/// and rejects the invalid ones (Count-Min over weights not proven
893-
/// non-negative) for the same reason Stage 3 gives.
894-
#[test]
895-
fn stage2_runtime_compiles_exactly_the_candidates_stage3_finds_valid() {
896-
let (workload, _, physical) = pipeline();
911+
/// The physical planner compiles every candidate Stage 3 finds valid, and
912+
/// rejects the invalid ones (Count-Min over weights not proven non-negative)
913+
/// for the same reason Stage 3 gives. Returns the number of invalid ones.
914+
fn assert_runtime_agrees_with_stage3(workload: PlanningWorkload) -> usize {
915+
let (workload, _, physical) = pipeline_for(workload);
897916
let selection = stage3_select(&workload, &physical, PlanningModels::builtin());
898917
let invalid: BTreeMap<_, _> = selection
899918
.rejected
900919
.iter()
901920
.filter(|r| !r.valid)
902921
.map(|r| (r.id.as_str(), r.reason.as_str()))
903922
.collect();
904-
assert_eq!(invalid.len(), 24, "the Count-Min + heap candidates");
905923
for p in &physical {
906924
let compiled = compile_in_runtime(p);
907925
match invalid.get(p.id.as_str()) {
@@ -920,6 +938,28 @@ fn stage2_runtime_compiles_exactly_the_candidates_stage3_finds_valid() {
920938
}
921939
}
922940
}
941+
invalid.len()
942+
}
943+
944+
/// Runtime capability check (added by the implementer, not part of the
945+
/// spec): with `http_requests_total` declared a counter, every candidate,
946+
/// Count-Min + heap included, is valid in Stage 3 and compiles.
947+
#[test]
948+
fn stage2_runtime_compiles_every_candidate_over_a_declared_counter() {
949+
assert_eq!(assert_runtime_agrees_with_stage3(example1_workload()), 0);
950+
}
951+
952+
/// Without the counter declaration, or with a gauge, the Count-Min + heap
953+
/// candidates are invalid in Stage 3 and the runtime rejects them.
954+
#[test]
955+
fn stage2_count_min_needs_a_counter_declaration() {
956+
for metric_type in [None, Some(MetricType::Gauge)] {
957+
assert_eq!(
958+
assert_runtime_agrees_with_stage3(example1_workload_with(metric_type)),
959+
24,
960+
"{metric_type:?}: the Count-Min + heap candidates"
961+
);
962+
}
923963
}
924964

925965
/// A CountSketch+heap top-k readout compiles: the IR's derived readout schema
@@ -1030,8 +1070,8 @@ fn stage3_charges_each_node_once() {
10301070
}
10311071

10321072
/// Sharing the input never costs more than reading it separately, for the
1033-
/// same local choices. Count-Min + heap is invalid here and has no cost
1034-
/// (Hydra: see `stage1_q2_summary_families_are_heap_sketches_and_hydra`).
1073+
/// same local choices (Hydra: see
1074+
/// `stage1_q2_summary_families_are_heap_sketches_and_hydra`).
10351075
#[test]
10361076
fn stage3_shared_input_is_not_costlier() {
10371077
let (workload, _, physical) = pipeline();
@@ -1046,7 +1086,9 @@ fn stage3_shared_input_is_not_costlier() {
10461086
.collect();
10471087
for option in [
10481088
Q2Option::Exact,
1089+
Q2Option::CountMinHeapPerJob,
10491090
Q2Option::CountSketchHeapPerJob,
1091+
Q2Option::WholeCountMinHeapPerJob,
10501092
Q2Option::WholeCountSketchHeapPerJob,
10511093
] {
10521094
assert!(

0 commit comments

Comments
 (0)