Skip to main content

cbtop/adaptive_ml/
mod.rs

1//! Dynamic Adaptive Thresholds with ML (PMAT-049)
2//!
3//! Self-learning workload-specific thresholds using multivariate models.
4//!
5//! # Design
6//!
7//! - Workload fingerprinting using CV pattern analysis
8//! - Multivariate modeling with feature correlation
9//! - Confidence scoring with uncertainty estimation
10//! - Drift detection with 24h re-calibration triggers
11//!
12//! # Falsification (FKR-050)
13//!
14//! Hâ‚€: ML thresholds cannot reduce false positives compared to static thresholds
15//! Test: Compare precision/recall on labeled dataset with injected anomalies
16
17mod threshold;
18mod types;
19
20pub use threshold::{LearnedWorkloadThreshold, MlThresholdConfig};
21pub use types::{
22    AnomalyResult, ClassificationMetrics, MlThresholdError, MlThresholdResult, TimeSeriesFeatures,
23    WorkloadClass,
24};
25
26use std::collections::HashMap;
27
28/// ML-based adaptive threshold system
29#[derive(Debug)]
30pub struct AdaptiveThresholdMl {
31    /// Configuration
32    config: MlThresholdConfig,
33    /// Per-workload learned thresholds
34    thresholds: HashMap<WorkloadClass, LearnedWorkloadThreshold>,
35    /// Classification metrics
36    metrics: ClassificationMetrics,
37    /// Training mode enabled
38    training_mode: bool,
39    /// Global baseline threshold (used during cold start)
40    global_threshold: f64,
41}
42
43impl AdaptiveThresholdMl {
44    /// Create a new adaptive threshold system
45    pub fn new(config: MlThresholdConfig) -> Self {
46        Self {
47            config,
48            thresholds: HashMap::new(),
49            metrics: ClassificationMetrics::default(),
50            training_mode: true,
51            global_threshold: 15.0,
52        }
53    }
54
55    /// Classify workload from features
56    pub fn classify_workload(&self, features: &TimeSeriesFeatures) -> WorkloadClass {
57        // Simple rule-based classification based on CV and autocorrelation
58        if features.cv < 10.0 && features.autocorr_lag1 > 0.5 {
59            WorkloadClass::ComputeBound
60        } else if features.cv > 18.0 && features.autocorr_lag1 < 0.3 {
61            WorkloadClass::MemoryBound
62        } else if features.cv < 12.0 {
63            WorkloadClass::Matmul
64        } else if features.cv > 15.0 {
65            WorkloadClass::Ffn
66        } else {
67            WorkloadClass::Attention
68        }
69    }
70
71    /// Get threshold for a workload
72    pub fn get_threshold(&self, workload: WorkloadClass) -> f64 {
73        if let Some(learned) = self.thresholds.get(&workload) {
74            if learned.confidence >= self.config.min_confidence
75                && learned.training_samples >= self.config.min_training_samples
76                && !learned.is_stale(self.config.max_threshold_age)
77            {
78                return learned.cv_threshold;
79            }
80        }
81
82        // Cold start: use default with conservative multiplier
83        workload.default_cv_threshold() * self.config.cold_start_multiplier
84    }
85
86    /// Detect anomaly in a time series
87    pub fn detect_anomaly(&self, values: &[f64]) -> MlThresholdResult<AnomalyResult> {
88        let features =
89            TimeSeriesFeatures::extract(values).ok_or(MlThresholdError::InsufficientData {
90                have: values.len(),
91                need: 10,
92            })?;
93
94        let workload = self.classify_workload(&features);
95        let threshold = self.get_threshold(workload);
96
97        let is_anomaly = features.cv > threshold;
98        let score = features.cv / threshold;
99
100        let confidence = self
101            .thresholds
102            .get(&workload)
103            .map(|t| t.confidence)
104            .unwrap_or(0.0);
105
106        let reason = if is_anomaly {
107            format!("CV {:.2}% exceeds threshold {:.2}%", features.cv, threshold)
108        } else {
109            format!("CV {:.2}% within threshold {:.2}%", features.cv, threshold)
110        };
111
112        Ok(AnomalyResult {
113            is_anomaly,
114            score,
115            threshold,
116            confidence,
117            workload_class: workload,
118            reason,
119        })
120    }
121
122    /// Train on labeled sample
123    pub fn train(&mut self, values: &[f64], is_anomaly: bool) -> MlThresholdResult<()> {
124        let features =
125            TimeSeriesFeatures::extract(values).ok_or(MlThresholdError::InsufficientData {
126                have: values.len(),
127                need: 10,
128            })?;
129
130        let workload = self.classify_workload(&features);
131
132        // Get or create threshold for this workload
133        let threshold = self
134            .thresholds
135            .entry(workload)
136            .or_insert_with(|| LearnedWorkloadThreshold::new(workload));
137
138        threshold.update(&features, is_anomaly);
139
140        // Update classification metrics
141        let predicted = features.cv > threshold.cv_threshold;
142        match (predicted, is_anomaly) {
143            (true, true) => self.metrics.true_positives += 1,
144            (true, false) => self.metrics.false_positives += 1,
145            (false, false) => self.metrics.true_negatives += 1,
146            (false, true) => self.metrics.false_negatives += 1,
147        }
148
149        Ok(())
150    }
151
152    /// Check for drift in recent samples
153    pub fn check_drift(&self, values: &[f64]) -> MlThresholdResult<Option<f64>> {
154        let features =
155            TimeSeriesFeatures::extract(values).ok_or(MlThresholdError::InsufficientData {
156                have: values.len(),
157                need: 10,
158            })?;
159
160        let workload = self.classify_workload(&features);
161
162        if let Some(threshold) = self.thresholds.get(&workload) {
163            Ok(threshold.check_drift(&features))
164        } else {
165            Ok(None)
166        }
167    }
168
169    /// Get classification metrics
170    pub fn get_metrics(&self) -> &ClassificationMetrics {
171        &self.metrics
172    }
173
174    /// Reset classification metrics
175    pub fn reset_metrics(&mut self) {
176        self.metrics = ClassificationMetrics::default();
177    }
178
179    /// Get learned threshold for workload
180    pub fn get_learned_threshold(
181        &self,
182        workload: WorkloadClass,
183    ) -> Option<&LearnedWorkloadThreshold> {
184        self.thresholds.get(&workload)
185    }
186
187    /// Get all workload classes with learned thresholds
188    pub fn learned_workloads(&self) -> Vec<WorkloadClass> {
189        self.thresholds.keys().copied().collect()
190    }
191
192    /// Export model state for persistence
193    pub fn export_state(&self) -> HashMap<String, (f64, f64, usize)> {
194        self.thresholds
195            .iter()
196            .map(|(k, v)| {
197                (
198                    k.name().to_string(),
199                    (v.cv_threshold, v.confidence, v.training_samples),
200                )
201            })
202            .collect()
203    }
204
205    /// Import model state
206    pub fn import_state(&mut self, state: HashMap<String, (f64, f64, usize)>) {
207        for (name, (threshold, confidence, samples)) in state {
208            let Some(workload) = WorkloadClass::from_name(&name) else {
209                continue;
210            };
211
212            let mut learned = LearnedWorkloadThreshold::new(workload);
213            learned.cv_threshold = threshold;
214            learned.confidence = confidence;
215            learned.training_samples = samples;
216
217            self.thresholds.insert(workload, learned);
218        }
219    }
220
221    /// Get configuration
222    pub fn config(&self) -> &MlThresholdConfig {
223        &self.config
224    }
225}
226
227/// Default minimum training samples
228pub const DEFAULT_MIN_TRAINING_SAMPLES: usize = 50;
229
230/// Default minimum confidence
231pub const DEFAULT_MIN_CONFIDENCE: f64 = 0.7;
232
233/// Default drift z-score threshold
234pub const DEFAULT_DRIFT_ZSCORE: f64 = 3.0;
235
236#[cfg(test)]
237mod tests;