Skip to content

Commit 81fb4e3

Browse files
committed
fix(cost): make streaming summary evidence conservative
1 parent af8486a commit 81fb4e3

2 files changed

Lines changed: 161 additions & 76 deletions

File tree

‎crates/asap-aware-mapping/src/analytical_streaming_cost.rs‎

Lines changed: 120 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ pub struct StreamingPhysicalInputEvidence {
3434
pub active_window_count: u64,
3535
/// Completed windows retained for query coverage.
3636
pub retained_window_count: u64,
37-
/// Independent state instances per window: one for shared
37+
/// Independent summary-state instances per window: one for shared
3838
/// multi-subpopulation state, otherwise the resolved group count.
39-
pub physical_sketch_count: u64,
39+
pub physical_summary_count: u64,
4040
/// Resident bytes of one concrete state instance.
41-
pub state_bytes_per_sketch: u64,
41+
pub state_bytes_per_summary: u64,
4242
}
4343

4444
/// Workload-normalized inputs for incremental maintenance over one finite
@@ -54,8 +54,8 @@ pub struct StreamingSummaryInputs {
5454
pub horizon_ms: u64,
5555
pub active_window_count: u64,
5656
pub retained_window_count: u64,
57-
pub physical_sketch_count: u64,
58-
pub state_bytes_per_sketch: u64,
57+
pub physical_summary_count: u64,
58+
pub state_bytes_per_summary: u64,
5959
pub evaluation_count: u64,
6060
}
6161

@@ -101,8 +101,8 @@ impl StreamingSummaryInputs {
101101
horizon_ms,
102102
active_window_count: physical.active_window_count,
103103
retained_window_count: physical.retained_window_count,
104-
physical_sketch_count: physical.physical_sketch_count,
105-
state_bytes_per_sketch: physical.state_bytes_per_sketch,
104+
physical_summary_count: physical.physical_summary_count,
105+
state_bytes_per_summary: physical.state_bytes_per_summary,
106106
evaluation_count: evaluations_in_horizon(
107107
&query.recurrence,
108108
planning_time_ms,
@@ -119,19 +119,26 @@ impl StreamingSummaryInputs {
119119
));
120120
}
121121
for (name, value) in [
122-
("initial_input_rows", self.initial_input_rows),
123-
("initial_input_bytes", self.initial_input_bytes),
124122
("horizon_ms", self.horizon_ms),
125123
("active_window_count", self.active_window_count),
126-
("retained_window_count", self.retained_window_count),
127-
("physical_sketch_count", self.physical_sketch_count),
128-
("state_bytes_per_sketch", self.state_bytes_per_sketch),
124+
("physical_summary_count", self.physical_summary_count),
125+
("state_bytes_per_summary", self.state_bytes_per_summary),
129126
("evaluation_count", self.evaluation_count),
130127
] {
131128
if value == 0 {
132129
return Err(AnalyticalCostError::MissingOrZero(name));
133130
}
134131
}
132+
let bootstrap_is_consistent = if self.initial_input_rows == 0 {
133+
self.initial_input_bytes == 0 && self.initial_source_scan_bytes == 0
134+
} else {
135+
self.initial_input_bytes > 0 && self.initial_source_scan_bytes > 0
136+
};
137+
if !bootstrap_is_consistent {
138+
return Err(AnalyticalCostError::InconsistentOperatorStatistics(
139+
"streaming bootstrap rows, logical bytes, and source bytes disagree",
140+
));
141+
}
135142
if !self.ingestion_rate_per_second.is_finite() || self.ingestion_rate_per_second < 0.0 {
136143
return Err(AnalyticalCostError::InvalidIngestionRate(
137144
self.ingestion_rate_per_second,
@@ -176,7 +183,10 @@ pub fn estimate_incremental_summary_maintenance(
176183
validate_guarantee(guarantee, inputs.data_arrival)?;
177184
let arriving_input_rows = arriving_rows_for_lifecycle(inputs, guarantee)?;
178185
let counts = count_operations(root)?;
179-
if counts.state_builds == 0 {
186+
if counts.state_builds != 1 {
187+
// One flat evidence record cannot safely describe several summary
188+
// nodes with different inputs, state sizes, or algorithms. Complete
189+
// multi-node streaming DAGs use per-node evidence instead.
180190
return Err(AnalyticalCostError::UnsupportedCandidate);
181191
}
182192

@@ -200,19 +210,19 @@ pub fn estimate_incremental_summary_maintenance(
200210

201211
let build_inserts = inputs
202212
.initial_input_rows
203-
.checked_mul(counts.state_builds)
213+
.checked_mul(inputs.active_window_count)
214+
.and_then(|n| n.checked_mul(counts.state_builds))
204215
.ok_or(AnalyticalCostError::Overflow)?;
205216
let update_inserts = arriving_input_rows
206217
.checked_mul(inputs.active_window_count)
207218
.and_then(|n| n.checked_mul(counts.state_builds))
208219
.ok_or(AnalyticalCostError::Overflow)?;
209-
let instances = inputs.physical_sketch_count as f64;
220+
let instances = inputs.physical_summary_count as f64;
210221
let evaluations = inputs.evaluation_count as f64;
211-
let updates = arriving_input_rows as f64;
212222
let cpu_ops = (build_inserts as f64 + update_inserts as f64) * insert
213223
+ evaluations * counts.merges_per_read as f64 * instances * merge
214224
+ evaluations * counts.subtracts_per_read as f64 * instances * subtract
215-
+ updates * counts.deletes_per_update as f64 * instances * delete
225+
+ update_inserts as f64 * counts.deletes_per_update as f64 * delete
216226
+ evaluations * counts.readouts_per_read as f64 * instances * readout;
217227
if !cpu_ops.is_finite() {
218228
return Err(AnalyticalCostError::Overflow);
@@ -221,26 +231,30 @@ pub fn estimate_incremental_summary_maintenance(
221231
let state_instances = inputs
222232
.active_window_count
223233
.checked_add(inputs.retained_window_count)
224-
.and_then(|n| n.checked_mul(inputs.physical_sketch_count))
234+
.and_then(|n| n.checked_mul(inputs.physical_summary_count))
225235
.and_then(|n| n.checked_mul(counts.state_builds))
226236
.ok_or(AnalyticalCostError::Overflow)?;
227237
let retained_bytes = state_instances
228-
.checked_mul(inputs.state_bytes_per_sketch)
238+
.checked_mul(inputs.state_bytes_per_summary)
229239
.ok_or(AnalyticalCostError::Overflow)?;
230240
// Merge/subtract may stream over persistent inputs but still needs one
231241
// result state per physical instance. Persistent retained windows are
232242
// already included above and are not loaded a second time.
233243
let transient_bytes = if counts.merges_per_read > 0 || counts.subtracts_per_read > 0 {
234244
inputs
235-
.physical_sketch_count
236-
.checked_mul(inputs.state_bytes_per_sketch)
245+
.physical_summary_count
246+
.checked_mul(inputs.state_bytes_per_summary)
237247
.ok_or(AnalyticalCostError::Overflow)?
238248
} else {
239249
0
240250
};
241-
let bootstrap_row_buffer = inputs
242-
.initial_input_bytes
243-
.div_ceil(inputs.initial_input_rows);
251+
let bootstrap_row_buffer = if inputs.initial_input_rows == 0 {
252+
0
253+
} else {
254+
inputs
255+
.initial_input_bytes
256+
.div_ceil(inputs.initial_input_rows)
257+
};
244258
Ok(ResourceEstimate {
245259
cpu_ops,
246260
peak_memory_bytes: retained_bytes
@@ -269,14 +283,13 @@ fn arriving_rows_for_lifecycle(
269283
}
270284
let start = activate_at.0.max(inputs.planning_time_ms);
271285
let end = retire_at.0.min(horizon_end);
272-
end.saturating_sub(start)
273-
}
274-
SummaryMaintenanceLifecycle::Shared { retention } => {
275-
if retention.0 < inputs.horizon_ms {
286+
let active_ms = end.saturating_sub(start);
287+
if active_ms == 0 {
276288
return Err(AnalyticalCostError::IncompatibleLifecycleGuarantee);
277289
}
278-
inputs.horizon_ms
290+
active_ms
279291
}
292+
SummaryMaintenanceLifecycle::Shared { .. } => inputs.horizon_ms,
280293
SummaryMaintenanceLifecycle::ContinuouslyMaintained => inputs.horizon_ms,
281294
SummaryMaintenanceLifecycle::Ephemeral => {
282295
return Err(AnalyticalCostError::IncompatibleLifecycleGuarantee)
@@ -306,7 +319,7 @@ fn validate_guarantee(
306319

307320
fn required_cpu(name: &'static str, value: Option<f64>) -> Result<f64, AnalyticalCostError> {
308321
let value = value.ok_or(AnalyticalCostError::MissingOrStale(name))?;
309-
if !value.is_finite() || value < 0.0 {
322+
if !value.is_finite() || value <= 0.0 {
310323
return Err(AnalyticalCostError::InvalidOperationCost(name, value));
311324
}
312325
Ok(value)
@@ -412,8 +425,8 @@ mod tests {
412425
initial_source_scan_bytes: 640,
413426
active_window_count: 2,
414427
retained_window_count: 3,
415-
physical_sketch_count: 2,
416-
state_bytes_per_sketch: 100,
428+
physical_summary_count: 2,
429+
state_bytes_per_summary: 100,
417430
}
418431
}
419432

@@ -500,8 +513,8 @@ mod tests {
500513
horizon_ms: 5_000,
501514
active_window_count: 2,
502515
retained_window_count: 3,
503-
physical_sketch_count: 2,
504-
state_bytes_per_sketch: 100,
516+
physical_summary_count: 2,
517+
state_bytes_per_summary: 100,
505518
evaluation_count: 5,
506519
},
507520
SummaryOperationCpuEvidence {
@@ -511,8 +524,9 @@ mod tests {
511524
},
512525
)
513526
.unwrap();
514-
// 10 bootstrap + 10 arrivals into two active windows; two states read 5 times.
515-
assert_eq!(estimate.cpu_ops, 90.0);
527+
// 10 bootstrap + 10 arrivals, each routed to two active windows;
528+
// two physical summary instances are read 5 times.
529+
assert_eq!(estimate.cpu_ops, 110.0);
516530
assert_eq!(estimate.peak_memory_bytes, 1_000);
517531
assert_eq!(estimate.scan_bytes, 640);
518532
}
@@ -532,8 +546,8 @@ mod tests {
532546
horizon_ms: 1_000,
533547
active_window_count: 1,
534548
retained_window_count: 2,
535-
physical_sketch_count: 2,
536-
state_bytes_per_sketch: 10,
549+
physical_summary_count: 2,
550+
state_bytes_per_summary: 10,
537551
evaluation_count: 3,
538552
},
539553
SummaryOperationCpuEvidence {
@@ -545,7 +559,7 @@ mod tests {
545559
},
546560
)
547561
.unwrap();
548-
assert_eq!(estimate.cpu_ops, 5.0 + 40.0 + 12.0 + 18.0 + 42.0);
562+
assert_eq!(estimate.cpu_ops, 5.0 + 12.0 + 18.0 + 20.0 + 42.0);
549563
// Three persistent windows plus one transient result, for two instances.
550564
assert_eq!(estimate.peak_memory_bytes, 80);
551565
}
@@ -568,8 +582,8 @@ mod tests {
568582
horizon_ms: 1_000,
569583
active_window_count: 1,
570584
retained_window_count: 1,
571-
physical_sketch_count: 1,
572-
state_bytes_per_sketch: 8,
585+
physical_summary_count: 1,
586+
state_bytes_per_summary: 8,
573587
evaluation_count: 1,
574588
},
575589
SummaryOperationCpuEvidence {
@@ -598,8 +612,8 @@ mod tests {
598612
horizon_ms: 1_000,
599613
active_window_count: 1,
600614
retained_window_count: 1,
601-
physical_sketch_count: 1,
602-
state_bytes_per_sketch: 8,
615+
physical_summary_count: 1,
616+
state_bytes_per_summary: 8,
603617
evaluation_count: 1,
604618
},
605619
SummaryOperationCpuEvidence {
@@ -632,8 +646,8 @@ mod tests {
632646
horizon_ms: 1_000,
633647
active_window_count: 1,
634648
retained_window_count: 1,
635-
physical_sketch_count: 1,
636-
state_bytes_per_sketch: 8,
649+
physical_summary_count: 1,
650+
state_bytes_per_summary: 8,
637651
evaluation_count: 1,
638652
},
639653
SummaryOperationCpuEvidence {
@@ -670,22 +684,22 @@ mod tests {
670684
horizon_ms: 5_000,
671685
active_window_count: 1,
672686
retained_window_count: 1,
673-
physical_sketch_count: 1,
674-
state_bytes_per_sketch: 8,
687+
physical_summary_count: 1,
688+
state_bytes_per_summary: 8,
675689
evaluation_count: 1,
676690
},
677691
SummaryOperationCpuEvidence {
678692
insert_cpu_ops: Some(1.0),
679-
readout_cpu_ops: Some(0.0),
693+
readout_cpu_ops: Some(1.0),
680694
..SummaryOperationCpuEvidence::default()
681695
},
682696
)
683697
.unwrap();
684-
assert_eq!(estimate.cpu_ops, 12.0); // 10 bootstrap + 2 updates in [1s, 2s].
698+
assert_eq!(estimate.cpu_ops, 13.0); // 10 bootstrap + 2 updates + one read.
685699
}
686700

687701
#[test]
688-
fn shared_retention_must_cover_the_comparison_horizon() {
702+
fn shared_retention_is_not_confused_with_the_planning_horizon() {
689703
let guarantee = SummaryMaintenanceLifecycleGuarantee {
690704
summary_maintenance_lifecycle: SummaryMaintenanceLifecycle::Shared {
691705
retention: asap_types::workload::DurationMs(999),
@@ -694,32 +708,62 @@ mod tests {
694708
evaluation_schedule: EvaluationSchedule::PerUpdate,
695709
output_representation: OutputRepresentation::SummaryState,
696710
};
697-
assert_eq!(
698-
estimate_incremental_summary_maintenance(
699-
&summary_with_operations(false, false, false),
700-
&guarantee,
701-
StreamingSummaryInputs {
702-
data_arrival: DataArrival::ContinuouslyIngesting,
703-
initial_input_rows: 1,
704-
initial_input_bytes: 8,
705-
initial_source_scan_bytes: 8,
706-
ingestion_rate_per_second: 1.0,
707-
planning_time_ms: 0,
708-
horizon_ms: 1_000,
709-
active_window_count: 1,
710-
retained_window_count: 1,
711-
physical_sketch_count: 1,
712-
state_bytes_per_sketch: 8,
713-
evaluation_count: 1,
714-
},
715-
SummaryOperationCpuEvidence {
716-
insert_cpu_ops: Some(1.0),
717-
readout_cpu_ops: Some(1.0),
718-
..SummaryOperationCpuEvidence::default()
719-
},
720-
),
721-
Err(AnalyticalCostError::IncompatibleLifecycleGuarantee)
722-
);
711+
assert!(estimate_incremental_summary_maintenance(
712+
&summary_with_operations(false, false, false),
713+
&guarantee,
714+
StreamingSummaryInputs {
715+
data_arrival: DataArrival::ContinuouslyIngesting,
716+
initial_input_rows: 1,
717+
initial_input_bytes: 8,
718+
initial_source_scan_bytes: 8,
719+
ingestion_rate_per_second: 1.0,
720+
planning_time_ms: 0,
721+
horizon_ms: 1_000,
722+
active_window_count: 1,
723+
retained_window_count: 1,
724+
physical_summary_count: 1,
725+
state_bytes_per_summary: 8,
726+
evaluation_count: 1,
727+
},
728+
SummaryOperationCpuEvidence {
729+
insert_cpu_ops: Some(1.0),
730+
readout_cpu_ops: Some(1.0),
731+
..SummaryOperationCpuEvidence::default()
732+
},
733+
)
734+
.is_ok());
735+
}
736+
737+
#[test]
738+
fn an_empty_bootstrap_is_valid_for_a_new_stream() {
739+
let estimate = estimate_incremental_summary_maintenance(
740+
&summary_with_operations(false, false, false),
741+
&continuous_guarantee(),
742+
StreamingSummaryInputs {
743+
data_arrival: DataArrival::ContinuouslyIngesting,
744+
initial_input_rows: 0,
745+
initial_input_bytes: 0,
746+
initial_source_scan_bytes: 0,
747+
ingestion_rate_per_second: 1.0,
748+
planning_time_ms: 0,
749+
horizon_ms: 1_000,
750+
active_window_count: 1,
751+
retained_window_count: 0,
752+
physical_summary_count: 1,
753+
state_bytes_per_summary: 8,
754+
evaluation_count: 1,
755+
},
756+
SummaryOperationCpuEvidence {
757+
insert_cpu_ops: Some(1.0),
758+
readout_cpu_ops: Some(1.0),
759+
..SummaryOperationCpuEvidence::default()
760+
},
761+
)
762+
.unwrap();
763+
764+
assert_eq!(estimate.cpu_ops, 2.0);
765+
assert_eq!(estimate.peak_memory_bytes, 8);
766+
assert_eq!(estimate.scan_bytes, 0);
723767
}
724768

725769
fn summary_with_operations(merge: bool, subtract: bool, delete: bool) -> Rc<SummaryNode> {

0 commit comments

Comments
 (0)