1#![allow(dead_code)]
9
10use anyhow::Result;
11use chrono::{DateTime, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, VecDeque};
14
15use crate::DebugConfig;
16
17#[derive(Debug)]
19pub struct AnomalyDetector {
20 config: AnomalyDetectorConfig,
21 detected_anomalies: Vec<Anomaly>,
22 start_time: DateTime<Utc>,
23 recovery_attempts: Vec<RecoveryAttempt>,
24 monitoring_stats: MonitoringStats,
25 performance_history: VecDeque<f64>,
26 gradient_history: HashMap<String, VecDeque<f64>>,
27 loss_history: VecDeque<f64>,
28 weight_baseline: HashMap<String, Vec<f32>>,
29 training_control: Option<Box<dyn TrainingControl>>,
34}
35
36pub trait TrainingControl: std::fmt::Debug + Send + Sync {
50 fn reset_gradients(&mut self) -> Result<()>;
52 fn reduce_learning_rate(&mut self, factor: f64) -> Result<()>;
54 fn clip_gradients(&mut self, max_norm: f64) -> Result<()>;
56 fn restart_optimizer(&mut self) -> Result<()>;
58 fn skip_batch(&mut self) -> Result<()>;
60 fn reset_weights(&mut self, layer_name: &str) -> Result<()>;
62 fn apply_weight_decay(&mut self, rate: f64) -> Result<()>;
64 fn emergency_stop(&mut self) -> Result<()>;
66}
67#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct AnomalyDetectorConfig {
75 pub enable_nan_detection: bool,
76 pub enable_inf_detection: bool,
77 pub enable_gradient_explosion: bool,
78 pub enable_gradient_vanishing: bool,
79 pub gradient_threshold: f64,
80 pub enable_memory_leak_detection: bool,
81 pub enable_numerical_instability_detection: bool,
82 pub enable_gradient_conflict_detection: bool,
83 pub enable_performance_monitoring: bool,
84 pub enable_weight_divergence_detection: bool,
85 pub enable_activation_dead_detection: bool,
86 pub enable_loss_anomaly_detection: bool,
87 pub enable_auto_recovery: bool,
88 pub numerical_instability_threshold: f64,
89 pub performance_degradation_threshold: f64,
90 pub weight_divergence_threshold: f64,
91 pub loss_spike_threshold: f64,
92 pub monitoring_window_size: usize,
93 pub recovery_attempts_limit: usize,
94}
95
96impl Default for AnomalyDetectorConfig {
97 fn default() -> Self {
98 Self {
99 enable_nan_detection: true,
100 enable_inf_detection: true,
101 enable_gradient_explosion: true,
102 enable_gradient_vanishing: true,
103 gradient_threshold: 1e6,
104 enable_memory_leak_detection: true,
105 enable_numerical_instability_detection: true,
106 enable_gradient_conflict_detection: true,
107 enable_performance_monitoring: true,
108 enable_weight_divergence_detection: true,
109 enable_activation_dead_detection: true,
110 enable_loss_anomaly_detection: true,
111 enable_auto_recovery: false, numerical_instability_threshold: 1e-12,
113 performance_degradation_threshold: 0.5, weight_divergence_threshold: 5.0,
115 loss_spike_threshold: 10.0, monitoring_window_size: 100,
117 recovery_attempts_limit: 3,
118 }
119 }
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
124pub enum AnomalyType {
125 NaN,
126 Infinity,
127 GradientExplosion,
128 GradientVanishing,
129 MemoryLeak,
130 UnusualActivation,
131 NumericalInstability,
132 GradientConflict,
133 PerformanceDegradation,
134 WeightDivergence,
135 ActivationDead,
136 LossAnomalous,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct Anomaly {
142 pub anomaly_type: AnomalyType,
143 pub timestamp: DateTime<Utc>,
144 pub location: String,
145 pub description: String,
146 pub severity: AnomalySeverity,
147 pub metadata: HashMap<String, String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub enum AnomalySeverity {
153 Low,
154 Medium,
155 High,
156 Critical,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub enum RecoveryAction {
162 None,
163 ResetGradients,
164 ReduceLearningRate { factor: f64 },
165 ClipGradients { max_norm: f64 },
166 RestartOptimizer,
167 SkipBatch,
168 ResetWeights { layer_name: String },
169 ApplyWeightDecay { rate: f64 },
170 EmergencyStop,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
175pub struct RecoveryAttempt {
176 pub anomaly_id: String,
177 pub action: RecoveryAction,
178 pub timestamp: DateTime<Utc>,
179 pub success: bool,
180 pub error_message: Option<String>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct MonitoringStats {
186 pub total_anomalies: usize,
187 pub anomalies_per_type: HashMap<String, usize>,
188 pub recovery_attempts: usize,
189 pub successful_recoveries: usize,
190 pub average_detection_time_ms: f64,
191 pub monitoring_window: Vec<AnomalySnapshot>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct AnomalySnapshot {
197 pub timestamp: DateTime<Utc>,
198 pub anomaly_count: usize,
199 pub severity_distribution: HashMap<String, usize>,
200 pub performance_metrics: HashMap<String, f64>,
201}
202
203impl AnomalyDetector {
204 pub fn new(_config: &DebugConfig) -> Self {
206 let monitoring_window_size = AnomalyDetectorConfig::default().monitoring_window_size;
207 Self {
208 config: AnomalyDetectorConfig::default(),
209 detected_anomalies: Vec::new(),
210 start_time: Utc::now(),
211 recovery_attempts: Vec::new(),
212 monitoring_stats: MonitoringStats {
213 total_anomalies: 0,
214 anomalies_per_type: HashMap::new(),
215 recovery_attempts: 0,
216 successful_recoveries: 0,
217 average_detection_time_ms: 0.0,
218 monitoring_window: Vec::new(),
219 },
220 performance_history: VecDeque::with_capacity(monitoring_window_size),
221 gradient_history: HashMap::new(),
222 loss_history: VecDeque::with_capacity(monitoring_window_size),
223 weight_baseline: HashMap::new(),
224 training_control: None,
225 }
226 }
227
228 pub fn set_training_control(&mut self, control: Box<dyn TrainingControl>) {
233 self.training_control = Some(control);
234 }
235
236 pub fn clear_training_control(&mut self) {
240 self.training_control = None;
241 }
242
243 pub fn has_training_control(&self) -> bool {
245 self.training_control.is_some()
246 }
247
248 pub async fn start(&mut self) -> Result<()> {
250 self.start_time = Utc::now();
251 self.detected_anomalies.clear();
252 Ok(())
253 }
254
255 pub fn check_nan(&mut self, values: &[f32], location: &str) -> Result<()> {
257 if !self.config.enable_nan_detection {
258 return Ok(());
259 }
260
261 if values.iter().any(|v| v.is_nan()) {
262 self.report_anomaly(Anomaly {
263 anomaly_type: AnomalyType::NaN,
264 timestamp: Utc::now(),
265 location: location.to_string(),
266 description: "NaN values detected in tensor".to_string(),
267 severity: AnomalySeverity::High,
268 metadata: HashMap::new(),
269 });
270 }
271
272 Ok(())
273 }
274
275 pub fn check_inf(&mut self, values: &[f32], location: &str) -> Result<()> {
277 if !self.config.enable_inf_detection {
278 return Ok(());
279 }
280
281 if values.iter().any(|v| v.is_infinite()) {
282 self.report_anomaly(Anomaly {
283 anomaly_type: AnomalyType::Infinity,
284 timestamp: Utc::now(),
285 location: location.to_string(),
286 description: "Infinite values detected in tensor".to_string(),
287 severity: AnomalySeverity::High,
288 metadata: HashMap::new(),
289 });
290 }
291
292 Ok(())
293 }
294
295 pub fn check_gradient_explosion(&mut self, gradient_norm: f64, location: &str) -> Result<()> {
297 if !self.config.enable_gradient_explosion {
298 return Ok(());
299 }
300
301 if gradient_norm > self.config.gradient_threshold {
302 self.report_anomaly(Anomaly {
303 anomaly_type: AnomalyType::GradientExplosion,
304 timestamp: Utc::now(),
305 location: location.to_string(),
306 description: format!("Gradient explosion detected: norm = {}", gradient_norm),
307 severity: AnomalySeverity::Critical,
308 metadata: {
309 let mut meta = HashMap::new();
310 meta.insert("gradient_norm".to_string(), gradient_norm.to_string());
311 meta
312 },
313 });
314 }
315
316 Ok(())
317 }
318
319 pub fn check_gradient_vanishing(&mut self, gradient_norm: f64, location: &str) -> Result<()> {
321 if !self.config.enable_gradient_vanishing {
322 return Ok(());
323 }
324
325 let vanishing_threshold = 1e-8;
326 if gradient_norm < vanishing_threshold {
327 self.report_anomaly(Anomaly {
328 anomaly_type: AnomalyType::GradientVanishing,
329 timestamp: Utc::now(),
330 location: location.to_string(),
331 description: format!("Vanishing gradient detected: norm = {}", gradient_norm),
332 severity: AnomalySeverity::High,
333 metadata: {
334 let mut meta = HashMap::new();
335 meta.insert("gradient_norm".to_string(), gradient_norm.to_string());
336 meta.insert("threshold".to_string(), vanishing_threshold.to_string());
337 meta
338 },
339 });
340 }
341
342 Ok(())
343 }
344
345 pub fn check_numerical_instability(&mut self, values: &[f32], location: &str) -> Result<()> {
347 let mut metadata = HashMap::new();
348
349 let near_zero_count = values.iter().filter(|&&v| v.abs() < 1e-10 && v != 0.0).count();
351 if near_zero_count > values.len() / 10 {
352 metadata.insert("near_zero_count".to_string(), near_zero_count.to_string());
353 metadata.insert("total_values".to_string(), values.len().to_string());
354
355 self.report_anomaly(Anomaly {
356 anomaly_type: AnomalyType::UnusualActivation,
357 timestamp: Utc::now(),
358 location: location.to_string(),
359 description: format!(
360 "Numerical instability: {} values near zero",
361 near_zero_count
362 ),
363 severity: AnomalySeverity::Medium,
364 metadata: metadata.clone(),
365 });
366 }
367
368 let extreme_count = values.iter().filter(|&&v| v.abs() > 1e6).count();
370 if extreme_count > 0 {
371 metadata.insert("extreme_count".to_string(), extreme_count.to_string());
372
373 self.report_anomaly(Anomaly {
374 anomaly_type: AnomalyType::UnusualActivation,
375 timestamp: Utc::now(),
376 location: location.to_string(),
377 description: format!("Numerical instability: {} extreme values", extreme_count),
378 severity: AnomalySeverity::High,
379 metadata,
380 });
381 }
382
383 Ok(())
384 }
385
386 pub fn check_activation_saturation(
388 &mut self,
389 activations: &[f32],
390 activation_type: &str,
391 location: &str,
392 ) -> Result<()> {
393 let saturation_threshold = match activation_type.to_lowercase().as_str() {
394 "sigmoid" | "tanh" => 0.01, "relu" => 0.0, _ => 0.01,
397 };
398
399 let saturated_count = match activation_type.to_lowercase().as_str() {
400 "sigmoid" => activations
401 .iter()
402 .filter(|&&v| v < saturation_threshold || v > 1.0 - saturation_threshold)
403 .count(),
404 "tanh" => activations.iter().filter(|&&v| v.abs() > 1.0 - saturation_threshold).count(),
405 "relu" => activations.iter().filter(|&&v| v == 0.0).count(),
406 _ => activations.iter().filter(|&&v| v.abs() < saturation_threshold).count(),
407 };
408
409 let saturation_ratio = saturated_count as f32 / activations.len() as f32;
410
411 if saturation_ratio > 0.9 {
412 let mut metadata = HashMap::new();
413 metadata.insert("activation_type".to_string(), activation_type.to_string());
414 metadata.insert("saturated_count".to_string(), saturated_count.to_string());
415 metadata.insert("total_count".to_string(), activations.len().to_string());
416 metadata.insert("saturation_ratio".to_string(), saturation_ratio.to_string());
417
418 self.report_anomaly(Anomaly {
419 anomaly_type: AnomalyType::UnusualActivation,
420 timestamp: Utc::now(),
421 location: location.to_string(),
422 description: format!(
423 "Activation saturation detected: {:.1}% of {} activations saturated",
424 saturation_ratio * 100.0,
425 activation_type
426 ),
427 severity: AnomalySeverity::High,
428 metadata,
429 });
430 }
431
432 Ok(())
433 }
434
435 pub fn check_memory_leak(
437 &mut self,
438 current_memory_mb: usize,
439 expected_memory_mb: Option<usize>,
440 location: &str,
441 ) -> Result<()> {
442 if !self.config.enable_memory_leak_detection {
443 return Ok(());
444 }
445
446 let mut should_report = false;
447 let mut description = String::new();
448 let mut metadata = HashMap::new();
449
450 metadata.insert(
451 "current_memory_mb".to_string(),
452 current_memory_mb.to_string(),
453 );
454
455 if let Some(expected) = expected_memory_mb {
456 metadata.insert("expected_memory_mb".to_string(), expected.to_string());
457
458 let growth_ratio = current_memory_mb as f64 / expected as f64;
459 if growth_ratio > 2.0 {
460 should_report = true;
461 description = format!(
462 "Memory usage {}MB is {:.1}x expected {}MB",
463 current_memory_mb, growth_ratio, expected
464 );
465 metadata.insert("growth_ratio".to_string(), growth_ratio.to_string());
466 }
467 } else {
468 if current_memory_mb > 8192 {
470 should_report = true;
472 description = format!("High memory usage detected: {}MB", current_memory_mb);
473 }
474 }
475
476 if should_report {
477 self.report_anomaly(Anomaly {
478 anomaly_type: AnomalyType::MemoryLeak,
479 timestamp: Utc::now(),
480 location: location.to_string(),
481 description,
482 severity: if current_memory_mb > 16384 {
483 AnomalySeverity::Critical
484 } else {
485 AnomalySeverity::High
486 },
487 metadata,
488 });
489 }
490
491 Ok(())
492 }
493
494 pub fn check_weight_explosion(&mut self, weights: &[f32], layer_name: &str) -> Result<()> {
496 let weight_threshold = 10.0;
497 let extreme_weights: Vec<f32> =
498 weights.iter().filter(|&&w| w.abs() > weight_threshold).cloned().collect();
499
500 if !extreme_weights.is_empty() {
501 let mut metadata = HashMap::new();
502 metadata.insert("layer_name".to_string(), layer_name.to_string());
503 metadata.insert(
504 "extreme_weight_count".to_string(),
505 extreme_weights.len().to_string(),
506 );
507 metadata.insert("total_weight_count".to_string(), weights.len().to_string());
508 metadata.insert(
509 "max_weight".to_string(),
510 extreme_weights.iter().map(|w| w.abs()).fold(0.0f32, f32::max).to_string(),
511 );
512
513 self.report_anomaly(Anomaly {
514 anomaly_type: AnomalyType::UnusualActivation,
515 timestamp: Utc::now(),
516 location: layer_name.to_string(),
517 description: format!(
518 "Weight explosion in {}: {} weights > {}",
519 layer_name,
520 extreme_weights.len(),
521 weight_threshold
522 ),
523 severity: AnomalySeverity::High,
524 metadata,
525 });
526 }
527
528 Ok(())
529 }
530
531 fn report_anomaly(&mut self, anomaly: Anomaly) {
533 tracing::warn!(
534 "🚨 Anomaly detected: {} at {}",
535 anomaly.description,
536 anomaly.location
537 );
538
539 self.monitoring_stats.total_anomalies += 1;
541 let anomaly_type_key = format!("{:?}", anomaly.anomaly_type);
542 *self.monitoring_stats.anomalies_per_type.entry(anomaly_type_key).or_insert(0) += 1;
543
544 self.detected_anomalies.push(anomaly);
545 }
546
547 pub fn get_anomalies(&self) -> &[Anomaly] {
549 &self.detected_anomalies
550 }
551
552 pub fn clear_anomalies(&mut self) {
554 self.detected_anomalies.clear();
555 }
556
557 pub fn check_gradient_conflict(
559 &mut self,
560 layer_gradients: &HashMap<String, Vec<f32>>,
561 ) -> Result<()> {
562 if !self.config.enable_gradient_conflict_detection {
563 return Ok(());
564 }
565
566 let layer_names: Vec<_> = layer_gradients.keys().cloned().collect();
567
568 for i in 0..layer_names.len() {
569 for j in i + 1..layer_names.len() {
570 let layer1 = &layer_names[i];
571 let layer2 = &layer_names[j];
572
573 if let (Some(grad1), Some(grad2)) =
574 (layer_gradients.get(layer1), layer_gradients.get(layer2))
575 {
576 let conflict_score = self.compute_gradient_conflict(grad1, grad2);
577
578 if conflict_score > 0.8 {
579 let mut metadata = HashMap::new();
580 metadata.insert("layer1".to_string(), layer1.clone());
581 metadata.insert("layer2".to_string(), layer2.clone());
582 metadata.insert("conflict_score".to_string(), conflict_score.to_string());
583
584 self.report_anomaly(Anomaly {
585 anomaly_type: AnomalyType::GradientConflict,
586 timestamp: Utc::now(),
587 location: format!("{}↔{}", layer1, layer2),
588 description: format!(
589 "Gradient conflict detected between {} and {} (score: {:.2})",
590 layer1, layer2, conflict_score
591 ),
592 severity: AnomalySeverity::High,
593 metadata,
594 });
595 }
596 }
597 }
598 }
599
600 Ok(())
601 }
602
603 pub fn check_weight_divergence(
605 &mut self,
606 layer_name: &str,
607 current_weights: &[f32],
608 ) -> Result<()> {
609 if !self.config.enable_weight_divergence_detection {
610 return Ok(());
611 }
612
613 if !self.weight_baseline.contains_key(layer_name) {
615 self.weight_baseline.insert(layer_name.to_string(), current_weights.to_vec());
616 return Ok(());
617 }
618
619 let Some(baseline) = self.weight_baseline.get(layer_name) else {
620 return Ok(());
621 };
622 if baseline.len() != current_weights.len() {
623 return Ok(()); }
625
626 let divergence = self.compute_weight_divergence(baseline, current_weights);
627
628 if divergence > self.config.weight_divergence_threshold {
629 let mut metadata = HashMap::new();
630 metadata.insert("layer_name".to_string(), layer_name.to_string());
631 metadata.insert("divergence_score".to_string(), divergence.to_string());
632 metadata.insert(
633 "threshold".to_string(),
634 self.config.weight_divergence_threshold.to_string(),
635 );
636
637 self.report_anomaly(Anomaly {
638 anomaly_type: AnomalyType::WeightDivergence,
639 timestamp: Utc::now(),
640 location: layer_name.to_string(),
641 description: format!(
642 "Weight divergence in {}: {:.2} (threshold: {:.2})",
643 layer_name, divergence, self.config.weight_divergence_threshold
644 ),
645 severity: if divergence > self.config.weight_divergence_threshold * 2.0 {
646 AnomalySeverity::Critical
647 } else {
648 AnomalySeverity::High
649 },
650 metadata,
651 });
652 }
653
654 Ok(())
655 }
656
657 pub fn check_performance_degradation(
659 &mut self,
660 current_performance: f64,
661 location: &str,
662 ) -> Result<()> {
663 if !self.config.enable_performance_monitoring {
664 return Ok(());
665 }
666
667 if self.performance_history.len() >= self.config.monitoring_window_size {
669 self.performance_history.pop_front();
670 }
671 self.performance_history.push_back(current_performance);
672
673 if self.performance_history.len() >= 10 {
675 let recent_avg = self.performance_history.iter().rev().take(5).sum::<f64>() / 5.0;
676 let baseline_avg = self.performance_history.iter().take(5).sum::<f64>() / 5.0;
677
678 let degradation_ratio = (baseline_avg - recent_avg) / baseline_avg;
679
680 if degradation_ratio > self.config.performance_degradation_threshold {
681 let mut metadata = HashMap::new();
682 metadata.insert("baseline_performance".to_string(), baseline_avg.to_string());
683 metadata.insert("current_performance".to_string(), recent_avg.to_string());
684 metadata.insert(
685 "degradation_ratio".to_string(),
686 degradation_ratio.to_string(),
687 );
688
689 self.report_anomaly(Anomaly {
690 anomaly_type: AnomalyType::PerformanceDegradation,
691 timestamp: Utc::now(),
692 location: location.to_string(),
693 description: format!(
694 "Performance degradation detected: {:.1}% drop from baseline",
695 degradation_ratio * 100.0
696 ),
697 severity: if degradation_ratio > 0.8 {
698 AnomalySeverity::Critical
699 } else {
700 AnomalySeverity::High
701 },
702 metadata,
703 });
704 }
705 }
706
707 Ok(())
708 }
709
710 pub fn check_loss_anomaly(&mut self, current_loss: f64, location: &str) -> Result<()> {
712 if !self.config.enable_loss_anomaly_detection {
713 return Ok(());
714 }
715
716 if self.loss_history.len() >= self.config.monitoring_window_size {
718 self.loss_history.pop_front();
719 }
720 self.loss_history.push_back(current_loss);
721
722 if self.loss_history.len() >= 3 {
724 let prev_loss = self.loss_history[self.loss_history.len() - 2];
725 let loss_ratio = current_loss / prev_loss;
726
727 if loss_ratio > self.config.loss_spike_threshold {
728 let mut metadata = HashMap::new();
729 metadata.insert("previous_loss".to_string(), prev_loss.to_string());
730 metadata.insert("current_loss".to_string(), current_loss.to_string());
731 metadata.insert("spike_ratio".to_string(), loss_ratio.to_string());
732
733 self.report_anomaly(Anomaly {
734 anomaly_type: AnomalyType::LossAnomalous,
735 timestamp: Utc::now(),
736 location: location.to_string(),
737 description: format!(
738 "Loss spike detected: {:.2}x increase (from {:.6} to {:.6})",
739 loss_ratio, prev_loss, current_loss
740 ),
741 severity: if loss_ratio > 100.0 {
742 AnomalySeverity::Critical
743 } else {
744 AnomalySeverity::High
745 },
746 metadata,
747 });
748 }
749 }
750
751 Ok(())
752 }
753
754 pub async fn attempt_recovery(&mut self, anomaly: &Anomaly) -> Result<RecoveryAction> {
756 if !self.config.enable_auto_recovery {
757 return Ok(RecoveryAction::None);
758 }
759
760 let action = self.determine_recovery_action(anomaly);
761 let anomaly_id = format!(
762 "{:?}_{}",
763 anomaly.anomaly_type,
764 anomaly.timestamp.timestamp()
765 );
766
767 let (success, error_message) = self.execute_recovery_action(&action).await?;
768
769 self.recovery_attempts.push(RecoveryAttempt {
770 anomaly_id: anomaly_id.clone(),
771 action: action.clone(),
772 timestamp: Utc::now(),
773 success,
774 error_message,
775 });
776
777 self.monitoring_stats.recovery_attempts += 1;
778 if success {
779 self.monitoring_stats.successful_recoveries += 1;
780 }
781
782 Ok(action)
783 }
784
785 pub fn get_monitoring_stats(&self) -> &MonitoringStats {
787 &self.monitoring_stats
788 }
789
790 pub fn get_recovery_attempts(&self) -> &[RecoveryAttempt] {
792 &self.recovery_attempts
793 }
794
795 pub fn update_monitoring_window(&mut self) -> Result<()> {
797 let mut severity_distribution = HashMap::new();
798 for anomaly in &self.detected_anomalies {
799 let key = format!("{:?}", anomaly.severity);
800 *severity_distribution.entry(key).or_insert(0) += 1;
801 }
802
803 let mut performance_metrics = HashMap::new();
804 if let Some(latest_perf) = self.performance_history.back() {
805 performance_metrics.insert("latest_performance".to_string(), *latest_perf);
806 }
807 if let Some(latest_loss) = self.loss_history.back() {
808 performance_metrics.insert("latest_loss".to_string(), *latest_loss);
809 }
810
811 let snapshot = AnomalySnapshot {
812 timestamp: Utc::now(),
813 anomaly_count: self.detected_anomalies.len(),
814 severity_distribution,
815 performance_metrics,
816 };
817
818 self.monitoring_stats.monitoring_window.push(snapshot);
819
820 if self.monitoring_stats.monitoring_window.len() > self.config.monitoring_window_size {
822 self.monitoring_stats.monitoring_window.remove(0);
823 }
824
825 Ok(())
826 }
827
828 fn compute_gradient_conflict(&self, grad1: &[f32], grad2: &[f32]) -> f64 {
831 if grad1.len() != grad2.len() {
832 return 0.0;
833 }
834
835 let dot_product: f64 =
836 grad1.iter().zip(grad2.iter()).map(|(a, b)| (*a as f64) * (*b as f64)).sum();
837
838 let norm1: f64 = grad1.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
839 let norm2: f64 = grad2.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
840
841 if norm1 == 0.0 || norm2 == 0.0 {
842 return 0.0;
843 }
844
845 let cosine_sim = dot_product / (norm1 * norm2);
847
848 (1.0 - cosine_sim) / 2.0
850 }
851
852 fn compute_weight_divergence(&self, baseline: &[f32], current: &[f32]) -> f64 {
853 let mse: f64 = baseline
854 .iter()
855 .zip(current.iter())
856 .map(|(a, b)| (*a as f64 - *b as f64).powi(2))
857 .sum::<f64>()
858 / baseline.len() as f64;
859
860 mse.sqrt()
861 }
862
863 fn determine_recovery_action(&self, anomaly: &Anomaly) -> RecoveryAction {
864 match anomaly.anomaly_type {
865 AnomalyType::GradientExplosion => RecoveryAction::ClipGradients { max_norm: 1.0 },
866 AnomalyType::GradientVanishing => RecoveryAction::ReduceLearningRate { factor: 0.5 },
867 AnomalyType::NaN | AnomalyType::Infinity => RecoveryAction::ResetGradients,
868 AnomalyType::WeightDivergence => RecoveryAction::ApplyWeightDecay { rate: 0.01 },
869 AnomalyType::LossAnomalous => RecoveryAction::SkipBatch,
870 AnomalyType::MemoryLeak => RecoveryAction::RestartOptimizer,
871 AnomalyType::PerformanceDegradation => {
872 RecoveryAction::ReduceLearningRate { factor: 0.8 }
873 },
874 _ => RecoveryAction::None,
875 }
876 }
877
878 async fn execute_recovery_action(
887 &mut self,
888 action: &RecoveryAction,
889 ) -> Result<(bool, Option<String>)> {
890 let Some(control) = self.training_control.as_mut() else {
891 let reason = format!(
892 "no TrainingControl attached to this AnomalyDetector (see \
893 AnomalyDetector::set_training_control); recovery action {action:?} was not \
894 performed"
895 );
896 tracing::warn!("{reason}");
897 return Ok((false, Some(reason)));
898 };
899
900 let outcome = match action {
901 RecoveryAction::None => Ok(()),
902 RecoveryAction::ResetGradients => control.reset_gradients(),
903 RecoveryAction::ReduceLearningRate { factor } => control.reduce_learning_rate(*factor),
904 RecoveryAction::ClipGradients { max_norm } => control.clip_gradients(*max_norm),
905 RecoveryAction::RestartOptimizer => control.restart_optimizer(),
906 RecoveryAction::SkipBatch => control.skip_batch(),
907 RecoveryAction::ResetWeights { layer_name } => control.reset_weights(layer_name),
908 RecoveryAction::ApplyWeightDecay { rate } => control.apply_weight_decay(*rate),
909 RecoveryAction::EmergencyStop => control.emergency_stop(),
910 };
911
912 match (action, outcome) {
913 (RecoveryAction::EmergencyStop, Ok(())) => {
914 tracing::warn!("Executed recovery: Emergency stop");
918 Ok((false, None))
919 },
920 (_, Ok(())) => {
921 tracing::info!("Executed recovery action: {action:?}");
922 Ok((true, None))
923 },
924 (_, Err(e)) => {
925 let reason = e.to_string();
926 tracing::error!("Recovery action {action:?} failed: {reason}");
927 Ok((false, Some(reason)))
928 },
929 }
930 }
931
932 pub async fn quick_check(&self) -> Result<crate::QuickAnomalySummary> {
934 let anomaly_count = self.detected_anomalies.len();
935
936 let severity_level = match anomaly_count {
937 0 => "None",
938 1..=3 => "Low",
939 4..=10 => "Medium",
940 11..=20 => "High",
941 _ => "Critical",
942 }
943 .to_string();
944
945 let mut recommendations = Vec::new();
946 if anomaly_count > 0 {
947 recommendations.push("Review recent training metrics for instabilities".to_string());
948 }
949 if anomaly_count > 5 {
950 recommendations.push(
951 "Consider adjusting learning rate or implementing gradient clipping".to_string(),
952 );
953 }
954 if anomaly_count > 15 {
955 recommendations
956 .push("Training may need to be restarted with better configuration".to_string());
957 }
958 if anomaly_count == 0 {
959 recommendations.push("No anomalies detected, training appears stable".to_string());
960 }
961
962 Ok(crate::QuickAnomalySummary {
963 anomaly_count,
964 severity_level,
965 recommendations,
966 })
967 }
968
969 pub async fn generate_report(&self) -> Result<AnomalyDetectorReport> {
971 let mut anomaly_counts = HashMap::new();
972 for anomaly in &self.detected_anomalies {
973 let count = anomaly_counts.entry(format!("{:?}", anomaly.anomaly_type)).or_insert(0);
974 *count += 1;
975 }
976
977 Ok(AnomalyDetectorReport {
978 session_duration: Utc::now().signed_duration_since(self.start_time),
979 total_anomalies: self.detected_anomalies.len(),
980 anomaly_counts,
981 most_recent_anomalies: self.detected_anomalies.iter().rev().take(10).cloned().collect(),
982 config: self.config.clone(),
983 })
984 }
985}
986
987#[derive(Debug, Clone, Serialize, Deserialize)]
989pub struct AnomalyDetectorReport {
990 pub session_duration: chrono::Duration,
991 pub total_anomalies: usize,
992 pub anomaly_counts: HashMap<String, usize>,
993 pub most_recent_anomalies: Vec<Anomaly>,
994 pub config: AnomalyDetectorConfig,
995}
996
997#[cfg(test)]
998mod tests {
999 use super::*;
1000
1001 #[test]
1002 fn test_anomaly_detector_creation() {
1003 let config = DebugConfig::default();
1004 let detector = AnomalyDetector::new(&config);
1005 assert_eq!(detector.get_anomalies().len(), 0);
1006 }
1007
1008 #[test]
1009 fn test_nan_detection() {
1010 let config = DebugConfig::default();
1011 let mut detector = AnomalyDetector::new(&config);
1012
1013 let values = vec![1.0, 2.0, f32::NAN, 4.0];
1014 detector.check_nan(&values, "test_location").expect("operation failed in test");
1015
1016 assert_eq!(detector.get_anomalies().len(), 1);
1017 assert!(matches!(
1018 detector.get_anomalies()[0].anomaly_type,
1019 AnomalyType::NaN
1020 ));
1021 }
1022
1023 #[test]
1024 fn test_inf_detection() {
1025 let config = DebugConfig::default();
1026 let mut detector = AnomalyDetector::new(&config);
1027
1028 let values = vec![1.0, 2.0, f32::INFINITY, 4.0];
1029 detector.check_inf(&values, "test_location").expect("operation failed in test");
1030
1031 assert_eq!(detector.get_anomalies().len(), 1);
1032 assert!(matches!(
1033 detector.get_anomalies()[0].anomaly_type,
1034 AnomalyType::Infinity
1035 ));
1036 }
1037
1038 #[test]
1039 fn test_gradient_explosion_detection() {
1040 let config = DebugConfig::default();
1041 let mut detector = AnomalyDetector::new(&config);
1042
1043 detector
1044 .check_gradient_explosion(1e7, "test_layer")
1045 .expect("operation failed in test");
1046
1047 assert_eq!(detector.get_anomalies().len(), 1);
1048 assert!(matches!(
1049 detector.get_anomalies()[0].anomaly_type,
1050 AnomalyType::GradientExplosion
1051 ));
1052 }
1053
1054 #[test]
1055 fn test_gradient_vanishing_detection() {
1056 let config = DebugConfig::default();
1057 let mut detector = AnomalyDetector::new(&config);
1058
1059 detector
1060 .check_gradient_vanishing(1e-10, "test_layer")
1061 .expect("operation failed in test");
1062
1063 assert_eq!(detector.get_anomalies().len(), 1);
1064 assert!(matches!(
1065 detector.get_anomalies()[0].anomaly_type,
1066 AnomalyType::GradientVanishing
1067 ));
1068 }
1069
1070 #[test]
1071 fn test_numerical_instability_detection() {
1072 let config = DebugConfig::default();
1073 let mut detector = AnomalyDetector::new(&config);
1074
1075 let near_zero_values: Vec<f32> =
1077 (0..100).map(|i| if i < 50 { 1e-12 } else { 1.0 }).collect();
1078 detector
1079 .check_numerical_instability(&near_zero_values, "test_location")
1080 .expect("operation failed in test");
1081 assert_eq!(detector.get_anomalies().len(), 1);
1082
1083 detector.clear_anomalies();
1084
1085 let extreme_values = vec![1.0, 2.0, 1e7, 4.0];
1087 detector
1088 .check_numerical_instability(&extreme_values, "test_location")
1089 .expect("operation failed in test");
1090 assert_eq!(detector.get_anomalies().len(), 1);
1091 }
1092
1093 #[test]
1094 fn test_activation_saturation_detection() {
1095 let config = DebugConfig::default();
1096 let mut detector = AnomalyDetector::new(&config);
1097
1098 let relu_saturated: Vec<f32> = vec![0.0; 100];
1100 detector
1101 .check_activation_saturation(&relu_saturated, "relu", "test_layer")
1102 .expect("operation failed in test");
1103 assert_eq!(detector.get_anomalies().len(), 1);
1104
1105 detector.clear_anomalies();
1106
1107 let sigmoid_saturated: Vec<f32> = vec![0.999; 100];
1109 detector
1110 .check_activation_saturation(&sigmoid_saturated, "sigmoid", "test_layer")
1111 .expect("operation failed in test");
1112 assert_eq!(detector.get_anomalies().len(), 1);
1113 }
1114
1115 #[test]
1116 fn test_memory_leak_detection() {
1117 let config = DebugConfig::default();
1118 let mut detector = AnomalyDetector::new(&config);
1119
1120 detector
1122 .check_memory_leak(3072, Some(1024), "test_location")
1123 .expect("operation failed in test");
1124 assert_eq!(detector.get_anomalies().len(), 1);
1125 assert!(matches!(
1126 detector.get_anomalies()[0].anomaly_type,
1127 AnomalyType::MemoryLeak
1128 ));
1129
1130 detector.clear_anomalies();
1131
1132 detector
1134 .check_memory_leak(10240, None, "test_location")
1135 .expect("operation failed in test");
1136 assert_eq!(detector.get_anomalies().len(), 1);
1137 }
1138
1139 #[test]
1140 fn test_weight_explosion_detection() {
1141 let config = DebugConfig::default();
1142 let mut detector = AnomalyDetector::new(&config);
1143
1144 let weights = vec![1.0, 2.0, 15.0, 4.0, -20.0]; detector
1146 .check_weight_explosion(&weights, "test_layer")
1147 .expect("operation failed in test");
1148
1149 assert_eq!(detector.get_anomalies().len(), 1);
1150 assert!(matches!(
1151 detector.get_anomalies()[0].anomaly_type,
1152 AnomalyType::UnusualActivation
1153 ));
1154 }
1155
1156 #[test]
1157 fn test_gradient_conflict_detection() {
1158 let config = DebugConfig::default();
1159 let mut detector = AnomalyDetector::new(&config);
1160
1161 let mut layer_gradients = HashMap::new();
1162 layer_gradients.insert("layer1".to_string(), vec![1.0, 0.0, 0.0]);
1163 layer_gradients.insert("layer2".to_string(), vec![-1.0, 0.0, 0.0]); detector
1166 .check_gradient_conflict(&layer_gradients)
1167 .expect("operation failed in test");
1168
1169 assert_eq!(detector.get_anomalies().len(), 1);
1170 assert!(matches!(
1171 detector.get_anomalies()[0].anomaly_type,
1172 AnomalyType::GradientConflict
1173 ));
1174 }
1175
1176 #[test]
1177 fn test_weight_divergence_detection() {
1178 let config = DebugConfig::default();
1179 let mut detector = AnomalyDetector::new(&config);
1180
1181 let baseline_weights = vec![1.0, 2.0, 3.0, 4.0];
1182 let diverged_weights = vec![10.0, 20.0, 30.0, 40.0]; detector
1186 .check_weight_divergence("test_layer", &baseline_weights)
1187 .expect("operation failed in test");
1188 assert_eq!(detector.get_anomalies().len(), 0);
1189
1190 detector
1192 .check_weight_divergence("test_layer", &diverged_weights)
1193 .expect("operation failed in test");
1194 assert_eq!(detector.get_anomalies().len(), 1);
1195 assert!(matches!(
1196 detector.get_anomalies()[0].anomaly_type,
1197 AnomalyType::WeightDivergence
1198 ));
1199 }
1200
1201 #[test]
1202 fn test_performance_degradation_detection() {
1203 let config = DebugConfig::default();
1204 let mut detector = AnomalyDetector::new(&config);
1205
1206 for _ in 0..10 {
1208 detector
1209 .check_performance_degradation(100.0, "training")
1210 .expect("operation failed in test"); }
1212 assert_eq!(detector.get_anomalies().len(), 0);
1213
1214 for _ in 0..5 {
1216 detector
1217 .check_performance_degradation(20.0, "training")
1218 .expect("operation failed in test"); }
1220
1221 assert!(!detector.get_anomalies().is_empty());
1223 assert!(detector
1224 .get_anomalies()
1225 .iter()
1226 .any(|a| matches!(a.anomaly_type, AnomalyType::PerformanceDegradation)));
1227 }
1228
1229 #[test]
1230 fn test_loss_anomaly_detection() {
1231 let config = DebugConfig::default();
1232 let mut detector = AnomalyDetector::new(&config);
1233
1234 detector.check_loss_anomaly(1.0, "training").expect("operation failed in test");
1236 detector.check_loss_anomaly(0.9, "training").expect("operation failed in test");
1237 assert_eq!(detector.get_anomalies().len(), 0);
1238
1239 detector
1241 .check_loss_anomaly(100.0, "training")
1242 .expect("operation failed in test"); assert_eq!(detector.get_anomalies().len(), 1);
1244 assert!(matches!(
1245 detector.get_anomalies()[0].anomaly_type,
1246 AnomalyType::LossAnomalous
1247 ));
1248 }
1249
1250 #[tokio::test]
1251 async fn test_auto_recovery() {
1252 let config = DebugConfig::default();
1253 let mut detector = AnomalyDetector::new(&config);
1254 detector.config.enable_auto_recovery = true;
1255
1256 let anomaly = Anomaly {
1257 anomaly_type: AnomalyType::GradientExplosion,
1258 timestamp: Utc::now(),
1259 location: "test_layer".to_string(),
1260 description: "Test gradient explosion".to_string(),
1261 severity: AnomalySeverity::High,
1262 metadata: HashMap::new(),
1263 };
1264
1265 let action = detector.attempt_recovery(&anomaly).await.expect("temp file creation failed");
1266 assert!(matches!(action, RecoveryAction::ClipGradients { .. }));
1267 assert_eq!(detector.get_recovery_attempts().len(), 1);
1268
1269 let attempt = &detector.get_recovery_attempts()[0];
1274 assert!(
1275 !attempt.success,
1276 "must not report success when no TrainingControl was ever attached"
1277 );
1278 assert!(
1279 attempt.error_message.is_some(),
1280 "must record a real reason, not silently claim success"
1281 );
1282 }
1283
1284 #[derive(Debug, Default)]
1288 struct RecordingTrainingControl {
1289 calls: Vec<String>,
1290 fail_next: bool,
1291 }
1292
1293 impl TrainingControl for RecordingTrainingControl {
1294 fn reset_gradients(&mut self) -> Result<()> {
1295 self.calls.push("reset_gradients".to_string());
1296 Ok(())
1297 }
1298 fn reduce_learning_rate(&mut self, factor: f64) -> Result<()> {
1299 self.calls.push(format!("reduce_learning_rate({factor})"));
1300 Ok(())
1301 }
1302 fn clip_gradients(&mut self, max_norm: f64) -> Result<()> {
1303 self.calls.push(format!("clip_gradients({max_norm})"));
1304 if self.fail_next {
1305 anyhow::bail!("simulated clip_gradients failure");
1306 }
1307 Ok(())
1308 }
1309 fn restart_optimizer(&mut self) -> Result<()> {
1310 self.calls.push("restart_optimizer".to_string());
1311 Ok(())
1312 }
1313 fn skip_batch(&mut self) -> Result<()> {
1314 self.calls.push("skip_batch".to_string());
1315 Ok(())
1316 }
1317 fn reset_weights(&mut self, layer_name: &str) -> Result<()> {
1318 self.calls.push(format!("reset_weights({layer_name})"));
1319 Ok(())
1320 }
1321 fn apply_weight_decay(&mut self, rate: f64) -> Result<()> {
1322 self.calls.push(format!("apply_weight_decay({rate})"));
1323 Ok(())
1324 }
1325 fn emergency_stop(&mut self) -> Result<()> {
1326 self.calls.push("emergency_stop".to_string());
1327 Ok(())
1328 }
1329 }
1330
1331 #[tokio::test]
1336 async fn test_auto_recovery_with_training_control_actually_dispatches() {
1337 let config = DebugConfig::default();
1338 let mut detector = AnomalyDetector::new(&config);
1339 detector.config.enable_auto_recovery = true;
1340 assert!(!detector.has_training_control());
1341 detector.set_training_control(Box::new(RecordingTrainingControl::default()));
1342 assert!(detector.has_training_control());
1343
1344 let anomaly = Anomaly {
1345 anomaly_type: AnomalyType::GradientExplosion,
1346 timestamp: Utc::now(),
1347 location: "test_layer".to_string(),
1348 description: "Test gradient explosion".to_string(),
1349 severity: AnomalySeverity::High,
1350 metadata: HashMap::new(),
1351 };
1352
1353 let action = detector.attempt_recovery(&anomaly).await.expect("recovery should not error");
1354 assert!(matches!(action, RecoveryAction::ClipGradients { max_norm } if max_norm == 1.0));
1355
1356 let attempt = &detector.get_recovery_attempts()[0];
1357 assert!(
1358 attempt.success,
1359 "a real, successful TrainingControl call must report success"
1360 );
1361 assert!(attempt.error_message.is_none());
1362 }
1363
1364 #[tokio::test]
1369 async fn test_auto_recovery_reports_real_error_from_training_control() {
1370 let config = DebugConfig::default();
1371 let mut detector = AnomalyDetector::new(&config);
1372 detector.config.enable_auto_recovery = true;
1373 detector.set_training_control(Box::new(RecordingTrainingControl {
1374 calls: Vec::new(),
1375 fail_next: true,
1376 }));
1377
1378 let anomaly = Anomaly {
1379 anomaly_type: AnomalyType::GradientExplosion,
1380 timestamp: Utc::now(),
1381 location: "test_layer".to_string(),
1382 description: "Test gradient explosion".to_string(),
1383 severity: AnomalySeverity::High,
1384 metadata: HashMap::new(),
1385 };
1386
1387 detector.attempt_recovery(&anomaly).await.expect("recovery should not error");
1388 let attempt = &detector.get_recovery_attempts()[0];
1389 assert!(!attempt.success);
1390 let message = attempt.error_message.as_deref().unwrap_or_default();
1391 assert!(
1392 message.contains("simulated clip_gradients failure"),
1393 "must surface the real underlying error, not a generic placeholder: {message}"
1394 );
1395 assert_ne!(
1396 message, "Recovery failed",
1397 "must not be the old generic placeholder string"
1398 );
1399 }
1400
1401 #[test]
1402 fn test_monitoring_stats() {
1403 let config = DebugConfig::default();
1404 let mut detector = AnomalyDetector::new(&config);
1405
1406 detector.check_nan(&[f32::NAN], "test").expect("operation failed in test");
1408 detector.check_inf(&[f32::INFINITY], "test").expect("operation failed in test");
1409
1410 let stats = detector.get_monitoring_stats();
1411 assert_eq!(stats.total_anomalies, 2);
1412 assert!(stats.anomalies_per_type.contains_key("NaN"));
1413 assert!(stats.anomalies_per_type.contains_key("Infinity"));
1414 }
1415
1416 #[test]
1417 fn test_monitoring_window_update() {
1418 let config = DebugConfig::default();
1419 let mut detector = AnomalyDetector::new(&config);
1420
1421 detector.check_nan(&[f32::NAN], "test").expect("operation failed in test");
1422 detector.update_monitoring_window().expect("operation failed in test");
1423
1424 let stats = detector.get_monitoring_stats();
1425 assert_eq!(stats.monitoring_window.len(), 1);
1426 assert_eq!(stats.monitoring_window[0].anomaly_count, 1);
1427 }
1428}