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}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct AnomalyDetectorConfig {
34 pub enable_nan_detection: bool,
35 pub enable_inf_detection: bool,
36 pub enable_gradient_explosion: bool,
37 pub enable_gradient_vanishing: bool,
38 pub gradient_threshold: f64,
39 pub enable_memory_leak_detection: bool,
40 pub enable_numerical_instability_detection: bool,
41 pub enable_gradient_conflict_detection: bool,
42 pub enable_performance_monitoring: bool,
43 pub enable_weight_divergence_detection: bool,
44 pub enable_activation_dead_detection: bool,
45 pub enable_loss_anomaly_detection: bool,
46 pub enable_auto_recovery: bool,
47 pub numerical_instability_threshold: f64,
48 pub performance_degradation_threshold: f64,
49 pub weight_divergence_threshold: f64,
50 pub loss_spike_threshold: f64,
51 pub monitoring_window_size: usize,
52 pub recovery_attempts_limit: usize,
53}
54
55impl Default for AnomalyDetectorConfig {
56 fn default() -> Self {
57 Self {
58 enable_nan_detection: true,
59 enable_inf_detection: true,
60 enable_gradient_explosion: true,
61 enable_gradient_vanishing: true,
62 gradient_threshold: 1e6,
63 enable_memory_leak_detection: true,
64 enable_numerical_instability_detection: true,
65 enable_gradient_conflict_detection: true,
66 enable_performance_monitoring: true,
67 enable_weight_divergence_detection: true,
68 enable_activation_dead_detection: true,
69 enable_loss_anomaly_detection: true,
70 enable_auto_recovery: false, numerical_instability_threshold: 1e-12,
72 performance_degradation_threshold: 0.5, weight_divergence_threshold: 5.0,
74 loss_spike_threshold: 10.0, monitoring_window_size: 100,
76 recovery_attempts_limit: 3,
77 }
78 }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub enum AnomalyType {
84 NaN,
85 Infinity,
86 GradientExplosion,
87 GradientVanishing,
88 MemoryLeak,
89 UnusualActivation,
90 NumericalInstability,
91 GradientConflict,
92 PerformanceDegradation,
93 WeightDivergence,
94 ActivationDead,
95 LossAnomalous,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct Anomaly {
101 pub anomaly_type: AnomalyType,
102 pub timestamp: DateTime<Utc>,
103 pub location: String,
104 pub description: String,
105 pub severity: AnomalySeverity,
106 pub metadata: HashMap<String, String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum AnomalySeverity {
112 Low,
113 Medium,
114 High,
115 Critical,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub enum RecoveryAction {
121 None,
122 ResetGradients,
123 ReduceLearningRate { factor: f64 },
124 ClipGradients { max_norm: f64 },
125 RestartOptimizer,
126 SkipBatch,
127 ResetWeights { layer_name: String },
128 ApplyWeightDecay { rate: f64 },
129 EmergencyStop,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct RecoveryAttempt {
135 pub anomaly_id: String,
136 pub action: RecoveryAction,
137 pub timestamp: DateTime<Utc>,
138 pub success: bool,
139 pub error_message: Option<String>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct MonitoringStats {
145 pub total_anomalies: usize,
146 pub anomalies_per_type: HashMap<String, usize>,
147 pub recovery_attempts: usize,
148 pub successful_recoveries: usize,
149 pub average_detection_time_ms: f64,
150 pub monitoring_window: Vec<AnomalySnapshot>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct AnomalySnapshot {
156 pub timestamp: DateTime<Utc>,
157 pub anomaly_count: usize,
158 pub severity_distribution: HashMap<String, usize>,
159 pub performance_metrics: HashMap<String, f64>,
160}
161
162impl AnomalyDetector {
163 pub fn new(_config: &DebugConfig) -> Self {
165 let monitoring_window_size = AnomalyDetectorConfig::default().monitoring_window_size;
166 Self {
167 config: AnomalyDetectorConfig::default(),
168 detected_anomalies: Vec::new(),
169 start_time: Utc::now(),
170 recovery_attempts: Vec::new(),
171 monitoring_stats: MonitoringStats {
172 total_anomalies: 0,
173 anomalies_per_type: HashMap::new(),
174 recovery_attempts: 0,
175 successful_recoveries: 0,
176 average_detection_time_ms: 0.0,
177 monitoring_window: Vec::new(),
178 },
179 performance_history: VecDeque::with_capacity(monitoring_window_size),
180 gradient_history: HashMap::new(),
181 loss_history: VecDeque::with_capacity(monitoring_window_size),
182 weight_baseline: HashMap::new(),
183 }
184 }
185
186 pub async fn start(&mut self) -> Result<()> {
188 self.start_time = Utc::now();
189 self.detected_anomalies.clear();
190 Ok(())
191 }
192
193 pub fn check_nan(&mut self, values: &[f32], location: &str) -> Result<()> {
195 if !self.config.enable_nan_detection {
196 return Ok(());
197 }
198
199 if values.iter().any(|v| v.is_nan()) {
200 self.report_anomaly(Anomaly {
201 anomaly_type: AnomalyType::NaN,
202 timestamp: Utc::now(),
203 location: location.to_string(),
204 description: "NaN values detected in tensor".to_string(),
205 severity: AnomalySeverity::High,
206 metadata: HashMap::new(),
207 });
208 }
209
210 Ok(())
211 }
212
213 pub fn check_inf(&mut self, values: &[f32], location: &str) -> Result<()> {
215 if !self.config.enable_inf_detection {
216 return Ok(());
217 }
218
219 if values.iter().any(|v| v.is_infinite()) {
220 self.report_anomaly(Anomaly {
221 anomaly_type: AnomalyType::Infinity,
222 timestamp: Utc::now(),
223 location: location.to_string(),
224 description: "Infinite values detected in tensor".to_string(),
225 severity: AnomalySeverity::High,
226 metadata: HashMap::new(),
227 });
228 }
229
230 Ok(())
231 }
232
233 pub fn check_gradient_explosion(&mut self, gradient_norm: f64, location: &str) -> Result<()> {
235 if !self.config.enable_gradient_explosion {
236 return Ok(());
237 }
238
239 if gradient_norm > self.config.gradient_threshold {
240 self.report_anomaly(Anomaly {
241 anomaly_type: AnomalyType::GradientExplosion,
242 timestamp: Utc::now(),
243 location: location.to_string(),
244 description: format!("Gradient explosion detected: norm = {}", gradient_norm),
245 severity: AnomalySeverity::Critical,
246 metadata: {
247 let mut meta = HashMap::new();
248 meta.insert("gradient_norm".to_string(), gradient_norm.to_string());
249 meta
250 },
251 });
252 }
253
254 Ok(())
255 }
256
257 pub fn check_gradient_vanishing(&mut self, gradient_norm: f64, location: &str) -> Result<()> {
259 if !self.config.enable_gradient_vanishing {
260 return Ok(());
261 }
262
263 let vanishing_threshold = 1e-8;
264 if gradient_norm < vanishing_threshold {
265 self.report_anomaly(Anomaly {
266 anomaly_type: AnomalyType::GradientVanishing,
267 timestamp: Utc::now(),
268 location: location.to_string(),
269 description: format!("Vanishing gradient detected: norm = {}", gradient_norm),
270 severity: AnomalySeverity::High,
271 metadata: {
272 let mut meta = HashMap::new();
273 meta.insert("gradient_norm".to_string(), gradient_norm.to_string());
274 meta.insert("threshold".to_string(), vanishing_threshold.to_string());
275 meta
276 },
277 });
278 }
279
280 Ok(())
281 }
282
283 pub fn check_numerical_instability(&mut self, values: &[f32], location: &str) -> Result<()> {
285 let mut metadata = HashMap::new();
286
287 let near_zero_count = values.iter().filter(|&&v| v.abs() < 1e-10 && v != 0.0).count();
289 if near_zero_count > values.len() / 10 {
290 metadata.insert("near_zero_count".to_string(), near_zero_count.to_string());
291 metadata.insert("total_values".to_string(), values.len().to_string());
292
293 self.report_anomaly(Anomaly {
294 anomaly_type: AnomalyType::UnusualActivation,
295 timestamp: Utc::now(),
296 location: location.to_string(),
297 description: format!(
298 "Numerical instability: {} values near zero",
299 near_zero_count
300 ),
301 severity: AnomalySeverity::Medium,
302 metadata: metadata.clone(),
303 });
304 }
305
306 let extreme_count = values.iter().filter(|&&v| v.abs() > 1e6).count();
308 if extreme_count > 0 {
309 metadata.insert("extreme_count".to_string(), extreme_count.to_string());
310
311 self.report_anomaly(Anomaly {
312 anomaly_type: AnomalyType::UnusualActivation,
313 timestamp: Utc::now(),
314 location: location.to_string(),
315 description: format!("Numerical instability: {} extreme values", extreme_count),
316 severity: AnomalySeverity::High,
317 metadata,
318 });
319 }
320
321 Ok(())
322 }
323
324 pub fn check_activation_saturation(
326 &mut self,
327 activations: &[f32],
328 activation_type: &str,
329 location: &str,
330 ) -> Result<()> {
331 let saturation_threshold = match activation_type.to_lowercase().as_str() {
332 "sigmoid" | "tanh" => 0.01, "relu" => 0.0, _ => 0.01,
335 };
336
337 let saturated_count = match activation_type.to_lowercase().as_str() {
338 "sigmoid" => activations
339 .iter()
340 .filter(|&&v| v < saturation_threshold || v > 1.0 - saturation_threshold)
341 .count(),
342 "tanh" => activations.iter().filter(|&&v| v.abs() > 1.0 - saturation_threshold).count(),
343 "relu" => activations.iter().filter(|&&v| v == 0.0).count(),
344 _ => activations.iter().filter(|&&v| v.abs() < saturation_threshold).count(),
345 };
346
347 let saturation_ratio = saturated_count as f32 / activations.len() as f32;
348
349 if saturation_ratio > 0.9 {
350 let mut metadata = HashMap::new();
351 metadata.insert("activation_type".to_string(), activation_type.to_string());
352 metadata.insert("saturated_count".to_string(), saturated_count.to_string());
353 metadata.insert("total_count".to_string(), activations.len().to_string());
354 metadata.insert("saturation_ratio".to_string(), saturation_ratio.to_string());
355
356 self.report_anomaly(Anomaly {
357 anomaly_type: AnomalyType::UnusualActivation,
358 timestamp: Utc::now(),
359 location: location.to_string(),
360 description: format!(
361 "Activation saturation detected: {:.1}% of {} activations saturated",
362 saturation_ratio * 100.0,
363 activation_type
364 ),
365 severity: AnomalySeverity::High,
366 metadata,
367 });
368 }
369
370 Ok(())
371 }
372
373 pub fn check_memory_leak(
375 &mut self,
376 current_memory_mb: usize,
377 expected_memory_mb: Option<usize>,
378 location: &str,
379 ) -> Result<()> {
380 if !self.config.enable_memory_leak_detection {
381 return Ok(());
382 }
383
384 let mut should_report = false;
385 let mut description = String::new();
386 let mut metadata = HashMap::new();
387
388 metadata.insert(
389 "current_memory_mb".to_string(),
390 current_memory_mb.to_string(),
391 );
392
393 if let Some(expected) = expected_memory_mb {
394 metadata.insert("expected_memory_mb".to_string(), expected.to_string());
395
396 let growth_ratio = current_memory_mb as f64 / expected as f64;
397 if growth_ratio > 2.0 {
398 should_report = true;
399 description = format!(
400 "Memory usage {}MB is {:.1}x expected {}MB",
401 current_memory_mb, growth_ratio, expected
402 );
403 metadata.insert("growth_ratio".to_string(), growth_ratio.to_string());
404 }
405 } else {
406 if current_memory_mb > 8192 {
408 should_report = true;
410 description = format!("High memory usage detected: {}MB", current_memory_mb);
411 }
412 }
413
414 if should_report {
415 self.report_anomaly(Anomaly {
416 anomaly_type: AnomalyType::MemoryLeak,
417 timestamp: Utc::now(),
418 location: location.to_string(),
419 description,
420 severity: if current_memory_mb > 16384 {
421 AnomalySeverity::Critical
422 } else {
423 AnomalySeverity::High
424 },
425 metadata,
426 });
427 }
428
429 Ok(())
430 }
431
432 pub fn check_weight_explosion(&mut self, weights: &[f32], layer_name: &str) -> Result<()> {
434 let weight_threshold = 10.0;
435 let extreme_weights: Vec<f32> =
436 weights.iter().filter(|&&w| w.abs() > weight_threshold).cloned().collect();
437
438 if !extreme_weights.is_empty() {
439 let mut metadata = HashMap::new();
440 metadata.insert("layer_name".to_string(), layer_name.to_string());
441 metadata.insert(
442 "extreme_weight_count".to_string(),
443 extreme_weights.len().to_string(),
444 );
445 metadata.insert("total_weight_count".to_string(), weights.len().to_string());
446 metadata.insert(
447 "max_weight".to_string(),
448 extreme_weights.iter().map(|w| w.abs()).fold(0.0f32, f32::max).to_string(),
449 );
450
451 self.report_anomaly(Anomaly {
452 anomaly_type: AnomalyType::UnusualActivation,
453 timestamp: Utc::now(),
454 location: layer_name.to_string(),
455 description: format!(
456 "Weight explosion in {}: {} weights > {}",
457 layer_name,
458 extreme_weights.len(),
459 weight_threshold
460 ),
461 severity: AnomalySeverity::High,
462 metadata,
463 });
464 }
465
466 Ok(())
467 }
468
469 fn report_anomaly(&mut self, anomaly: Anomaly) {
471 eprintln!(
472 "🚨 Anomaly detected: {} at {}",
473 anomaly.description, anomaly.location
474 );
475
476 self.monitoring_stats.total_anomalies += 1;
478 let anomaly_type_key = format!("{:?}", anomaly.anomaly_type);
479 *self.monitoring_stats.anomalies_per_type.entry(anomaly_type_key).or_insert(0) += 1;
480
481 self.detected_anomalies.push(anomaly);
482 }
483
484 pub fn get_anomalies(&self) -> &[Anomaly] {
486 &self.detected_anomalies
487 }
488
489 pub fn clear_anomalies(&mut self) {
491 self.detected_anomalies.clear();
492 }
493
494 pub fn check_gradient_conflict(
496 &mut self,
497 layer_gradients: &HashMap<String, Vec<f32>>,
498 ) -> Result<()> {
499 if !self.config.enable_gradient_conflict_detection {
500 return Ok(());
501 }
502
503 let layer_names: Vec<_> = layer_gradients.keys().cloned().collect();
504
505 for i in 0..layer_names.len() {
506 for j in i + 1..layer_names.len() {
507 let layer1 = &layer_names[i];
508 let layer2 = &layer_names[j];
509
510 if let (Some(grad1), Some(grad2)) =
511 (layer_gradients.get(layer1), layer_gradients.get(layer2))
512 {
513 let conflict_score = self.compute_gradient_conflict(grad1, grad2);
514
515 if conflict_score > 0.8 {
516 let mut metadata = HashMap::new();
517 metadata.insert("layer1".to_string(), layer1.clone());
518 metadata.insert("layer2".to_string(), layer2.clone());
519 metadata.insert("conflict_score".to_string(), conflict_score.to_string());
520
521 self.report_anomaly(Anomaly {
522 anomaly_type: AnomalyType::GradientConflict,
523 timestamp: Utc::now(),
524 location: format!("{}↔{}", layer1, layer2),
525 description: format!(
526 "Gradient conflict detected between {} and {} (score: {:.2})",
527 layer1, layer2, conflict_score
528 ),
529 severity: AnomalySeverity::High,
530 metadata,
531 });
532 }
533 }
534 }
535 }
536
537 Ok(())
538 }
539
540 pub fn check_weight_divergence(
542 &mut self,
543 layer_name: &str,
544 current_weights: &[f32],
545 ) -> Result<()> {
546 if !self.config.enable_weight_divergence_detection {
547 return Ok(());
548 }
549
550 if !self.weight_baseline.contains_key(layer_name) {
552 self.weight_baseline.insert(layer_name.to_string(), current_weights.to_vec());
553 return Ok(());
554 }
555
556 let Some(baseline) = self.weight_baseline.get(layer_name) else {
557 return Ok(());
558 };
559 if baseline.len() != current_weights.len() {
560 return Ok(()); }
562
563 let divergence = self.compute_weight_divergence(baseline, current_weights);
564
565 if divergence > self.config.weight_divergence_threshold {
566 let mut metadata = HashMap::new();
567 metadata.insert("layer_name".to_string(), layer_name.to_string());
568 metadata.insert("divergence_score".to_string(), divergence.to_string());
569 metadata.insert(
570 "threshold".to_string(),
571 self.config.weight_divergence_threshold.to_string(),
572 );
573
574 self.report_anomaly(Anomaly {
575 anomaly_type: AnomalyType::WeightDivergence,
576 timestamp: Utc::now(),
577 location: layer_name.to_string(),
578 description: format!(
579 "Weight divergence in {}: {:.2} (threshold: {:.2})",
580 layer_name, divergence, self.config.weight_divergence_threshold
581 ),
582 severity: if divergence > self.config.weight_divergence_threshold * 2.0 {
583 AnomalySeverity::Critical
584 } else {
585 AnomalySeverity::High
586 },
587 metadata,
588 });
589 }
590
591 Ok(())
592 }
593
594 pub fn check_performance_degradation(
596 &mut self,
597 current_performance: f64,
598 location: &str,
599 ) -> Result<()> {
600 if !self.config.enable_performance_monitoring {
601 return Ok(());
602 }
603
604 if self.performance_history.len() >= self.config.monitoring_window_size {
606 self.performance_history.pop_front();
607 }
608 self.performance_history.push_back(current_performance);
609
610 if self.performance_history.len() >= 10 {
612 let recent_avg = self.performance_history.iter().rev().take(5).sum::<f64>() / 5.0;
613 let baseline_avg = self.performance_history.iter().take(5).sum::<f64>() / 5.0;
614
615 let degradation_ratio = (baseline_avg - recent_avg) / baseline_avg;
616
617 if degradation_ratio > self.config.performance_degradation_threshold {
618 let mut metadata = HashMap::new();
619 metadata.insert("baseline_performance".to_string(), baseline_avg.to_string());
620 metadata.insert("current_performance".to_string(), recent_avg.to_string());
621 metadata.insert(
622 "degradation_ratio".to_string(),
623 degradation_ratio.to_string(),
624 );
625
626 self.report_anomaly(Anomaly {
627 anomaly_type: AnomalyType::PerformanceDegradation,
628 timestamp: Utc::now(),
629 location: location.to_string(),
630 description: format!(
631 "Performance degradation detected: {:.1}% drop from baseline",
632 degradation_ratio * 100.0
633 ),
634 severity: if degradation_ratio > 0.8 {
635 AnomalySeverity::Critical
636 } else {
637 AnomalySeverity::High
638 },
639 metadata,
640 });
641 }
642 }
643
644 Ok(())
645 }
646
647 pub fn check_loss_anomaly(&mut self, current_loss: f64, location: &str) -> Result<()> {
649 if !self.config.enable_loss_anomaly_detection {
650 return Ok(());
651 }
652
653 if self.loss_history.len() >= self.config.monitoring_window_size {
655 self.loss_history.pop_front();
656 }
657 self.loss_history.push_back(current_loss);
658
659 if self.loss_history.len() >= 3 {
661 let prev_loss = self.loss_history[self.loss_history.len() - 2];
662 let loss_ratio = current_loss / prev_loss;
663
664 if loss_ratio > self.config.loss_spike_threshold {
665 let mut metadata = HashMap::new();
666 metadata.insert("previous_loss".to_string(), prev_loss.to_string());
667 metadata.insert("current_loss".to_string(), current_loss.to_string());
668 metadata.insert("spike_ratio".to_string(), loss_ratio.to_string());
669
670 self.report_anomaly(Anomaly {
671 anomaly_type: AnomalyType::LossAnomalous,
672 timestamp: Utc::now(),
673 location: location.to_string(),
674 description: format!(
675 "Loss spike detected: {:.2}x increase (from {:.6} to {:.6})",
676 loss_ratio, prev_loss, current_loss
677 ),
678 severity: if loss_ratio > 100.0 {
679 AnomalySeverity::Critical
680 } else {
681 AnomalySeverity::High
682 },
683 metadata,
684 });
685 }
686 }
687
688 Ok(())
689 }
690
691 pub async fn attempt_recovery(&mut self, anomaly: &Anomaly) -> Result<RecoveryAction> {
693 if !self.config.enable_auto_recovery {
694 return Ok(RecoveryAction::None);
695 }
696
697 let action = self.determine_recovery_action(anomaly);
698 let anomaly_id = format!(
699 "{:?}_{}",
700 anomaly.anomaly_type,
701 anomaly.timestamp.timestamp()
702 );
703
704 let success = self.execute_recovery_action(&action).await?;
705
706 self.recovery_attempts.push(RecoveryAttempt {
707 anomaly_id: anomaly_id.clone(),
708 action: action.clone(),
709 timestamp: Utc::now(),
710 success,
711 error_message: if success { None } else { Some("Recovery failed".to_string()) },
712 });
713
714 self.monitoring_stats.recovery_attempts += 1;
715 if success {
716 self.monitoring_stats.successful_recoveries += 1;
717 }
718
719 Ok(action)
720 }
721
722 pub fn get_monitoring_stats(&self) -> &MonitoringStats {
724 &self.monitoring_stats
725 }
726
727 pub fn get_recovery_attempts(&self) -> &[RecoveryAttempt] {
729 &self.recovery_attempts
730 }
731
732 pub fn update_monitoring_window(&mut self) -> Result<()> {
734 let mut severity_distribution = HashMap::new();
735 for anomaly in &self.detected_anomalies {
736 let key = format!("{:?}", anomaly.severity);
737 *severity_distribution.entry(key).or_insert(0) += 1;
738 }
739
740 let mut performance_metrics = HashMap::new();
741 if let Some(latest_perf) = self.performance_history.back() {
742 performance_metrics.insert("latest_performance".to_string(), *latest_perf);
743 }
744 if let Some(latest_loss) = self.loss_history.back() {
745 performance_metrics.insert("latest_loss".to_string(), *latest_loss);
746 }
747
748 let snapshot = AnomalySnapshot {
749 timestamp: Utc::now(),
750 anomaly_count: self.detected_anomalies.len(),
751 severity_distribution,
752 performance_metrics,
753 };
754
755 self.monitoring_stats.monitoring_window.push(snapshot);
756
757 if self.monitoring_stats.monitoring_window.len() > self.config.monitoring_window_size {
759 self.monitoring_stats.monitoring_window.remove(0);
760 }
761
762 Ok(())
763 }
764
765 fn compute_gradient_conflict(&self, grad1: &[f32], grad2: &[f32]) -> f64 {
768 if grad1.len() != grad2.len() {
769 return 0.0;
770 }
771
772 let dot_product: f64 =
773 grad1.iter().zip(grad2.iter()).map(|(a, b)| (*a as f64) * (*b as f64)).sum();
774
775 let norm1: f64 = grad1.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
776 let norm2: f64 = grad2.iter().map(|x| (*x as f64).powi(2)).sum::<f64>().sqrt();
777
778 if norm1 == 0.0 || norm2 == 0.0 {
779 return 0.0;
780 }
781
782 let cosine_sim = dot_product / (norm1 * norm2);
784
785 (1.0 - cosine_sim) / 2.0
787 }
788
789 fn compute_weight_divergence(&self, baseline: &[f32], current: &[f32]) -> f64 {
790 let mse: f64 = baseline
791 .iter()
792 .zip(current.iter())
793 .map(|(a, b)| (*a as f64 - *b as f64).powi(2))
794 .sum::<f64>()
795 / baseline.len() as f64;
796
797 mse.sqrt()
798 }
799
800 fn determine_recovery_action(&self, anomaly: &Anomaly) -> RecoveryAction {
801 match anomaly.anomaly_type {
802 AnomalyType::GradientExplosion => RecoveryAction::ClipGradients { max_norm: 1.0 },
803 AnomalyType::GradientVanishing => RecoveryAction::ReduceLearningRate { factor: 0.5 },
804 AnomalyType::NaN | AnomalyType::Infinity => RecoveryAction::ResetGradients,
805 AnomalyType::WeightDivergence => RecoveryAction::ApplyWeightDecay { rate: 0.01 },
806 AnomalyType::LossAnomalous => RecoveryAction::SkipBatch,
807 AnomalyType::MemoryLeak => RecoveryAction::RestartOptimizer,
808 AnomalyType::PerformanceDegradation => {
809 RecoveryAction::ReduceLearningRate { factor: 0.8 }
810 },
811 _ => RecoveryAction::None,
812 }
813 }
814
815 async fn execute_recovery_action(&self, action: &RecoveryAction) -> Result<bool> {
816 match action {
819 RecoveryAction::None => Ok(true),
820 RecoveryAction::ResetGradients => {
821 tracing::info!("Executing recovery: Reset gradients");
822 Ok(true)
823 },
824 RecoveryAction::ReduceLearningRate { factor } => {
825 tracing::info!(
826 "Executing recovery: Reduce learning rate by factor {}",
827 factor
828 );
829 Ok(true)
830 },
831 RecoveryAction::ClipGradients { max_norm } => {
832 tracing::info!(
833 "Executing recovery: Clip gradients to max norm {}",
834 max_norm
835 );
836 Ok(true)
837 },
838 RecoveryAction::RestartOptimizer => {
839 tracing::info!("Executing recovery: Restart optimizer");
840 Ok(true)
841 },
842 RecoveryAction::SkipBatch => {
843 tracing::info!("Executing recovery: Skip current batch");
844 Ok(true)
845 },
846 RecoveryAction::ResetWeights { layer_name } => {
847 tracing::info!("Executing recovery: Reset weights for layer {}", layer_name);
848 Ok(true)
849 },
850 RecoveryAction::ApplyWeightDecay { rate } => {
851 tracing::info!("Executing recovery: Apply weight decay with rate {}", rate);
852 Ok(true)
853 },
854 RecoveryAction::EmergencyStop => {
855 tracing::warn!("Executing recovery: Emergency stop");
856 Ok(false) },
858 }
859 }
860
861 pub async fn quick_check(&self) -> Result<crate::QuickAnomalySummary> {
863 let anomaly_count = self.detected_anomalies.len();
864
865 let severity_level = match anomaly_count {
866 0 => "None",
867 1..=3 => "Low",
868 4..=10 => "Medium",
869 11..=20 => "High",
870 _ => "Critical",
871 }
872 .to_string();
873
874 let mut recommendations = Vec::new();
875 if anomaly_count > 0 {
876 recommendations.push("Review recent training metrics for instabilities".to_string());
877 }
878 if anomaly_count > 5 {
879 recommendations.push(
880 "Consider adjusting learning rate or implementing gradient clipping".to_string(),
881 );
882 }
883 if anomaly_count > 15 {
884 recommendations
885 .push("Training may need to be restarted with better configuration".to_string());
886 }
887 if anomaly_count == 0 {
888 recommendations.push("No anomalies detected, training appears stable".to_string());
889 }
890
891 Ok(crate::QuickAnomalySummary {
892 anomaly_count,
893 severity_level,
894 recommendations,
895 })
896 }
897
898 pub async fn generate_report(&self) -> Result<AnomalyDetectorReport> {
900 let mut anomaly_counts = HashMap::new();
901 for anomaly in &self.detected_anomalies {
902 let count = anomaly_counts.entry(format!("{:?}", anomaly.anomaly_type)).or_insert(0);
903 *count += 1;
904 }
905
906 Ok(AnomalyDetectorReport {
907 session_duration: Utc::now().signed_duration_since(self.start_time),
908 total_anomalies: self.detected_anomalies.len(),
909 anomaly_counts,
910 most_recent_anomalies: self.detected_anomalies.iter().rev().take(10).cloned().collect(),
911 config: self.config.clone(),
912 })
913 }
914}
915
916#[derive(Debug, Clone, Serialize, Deserialize)]
918pub struct AnomalyDetectorReport {
919 pub session_duration: chrono::Duration,
920 pub total_anomalies: usize,
921 pub anomaly_counts: HashMap<String, usize>,
922 pub most_recent_anomalies: Vec<Anomaly>,
923 pub config: AnomalyDetectorConfig,
924}
925
926#[cfg(test)]
927mod tests {
928 use super::*;
929
930 #[test]
931 fn test_anomaly_detector_creation() {
932 let config = DebugConfig::default();
933 let detector = AnomalyDetector::new(&config);
934 assert_eq!(detector.get_anomalies().len(), 0);
935 }
936
937 #[test]
938 fn test_nan_detection() {
939 let config = DebugConfig::default();
940 let mut detector = AnomalyDetector::new(&config);
941
942 let values = vec![1.0, 2.0, f32::NAN, 4.0];
943 detector.check_nan(&values, "test_location").expect("operation failed in test");
944
945 assert_eq!(detector.get_anomalies().len(), 1);
946 assert!(matches!(
947 detector.get_anomalies()[0].anomaly_type,
948 AnomalyType::NaN
949 ));
950 }
951
952 #[test]
953 fn test_inf_detection() {
954 let config = DebugConfig::default();
955 let mut detector = AnomalyDetector::new(&config);
956
957 let values = vec![1.0, 2.0, f32::INFINITY, 4.0];
958 detector.check_inf(&values, "test_location").expect("operation failed in test");
959
960 assert_eq!(detector.get_anomalies().len(), 1);
961 assert!(matches!(
962 detector.get_anomalies()[0].anomaly_type,
963 AnomalyType::Infinity
964 ));
965 }
966
967 #[test]
968 fn test_gradient_explosion_detection() {
969 let config = DebugConfig::default();
970 let mut detector = AnomalyDetector::new(&config);
971
972 detector
973 .check_gradient_explosion(1e7, "test_layer")
974 .expect("operation failed in test");
975
976 assert_eq!(detector.get_anomalies().len(), 1);
977 assert!(matches!(
978 detector.get_anomalies()[0].anomaly_type,
979 AnomalyType::GradientExplosion
980 ));
981 }
982
983 #[test]
984 fn test_gradient_vanishing_detection() {
985 let config = DebugConfig::default();
986 let mut detector = AnomalyDetector::new(&config);
987
988 detector
989 .check_gradient_vanishing(1e-10, "test_layer")
990 .expect("operation failed in test");
991
992 assert_eq!(detector.get_anomalies().len(), 1);
993 assert!(matches!(
994 detector.get_anomalies()[0].anomaly_type,
995 AnomalyType::GradientVanishing
996 ));
997 }
998
999 #[test]
1000 fn test_numerical_instability_detection() {
1001 let config = DebugConfig::default();
1002 let mut detector = AnomalyDetector::new(&config);
1003
1004 let near_zero_values: Vec<f32> =
1006 (0..100).map(|i| if i < 50 { 1e-12 } else { 1.0 }).collect();
1007 detector
1008 .check_numerical_instability(&near_zero_values, "test_location")
1009 .expect("operation failed in test");
1010 assert_eq!(detector.get_anomalies().len(), 1);
1011
1012 detector.clear_anomalies();
1013
1014 let extreme_values = vec![1.0, 2.0, 1e7, 4.0];
1016 detector
1017 .check_numerical_instability(&extreme_values, "test_location")
1018 .expect("operation failed in test");
1019 assert_eq!(detector.get_anomalies().len(), 1);
1020 }
1021
1022 #[test]
1023 fn test_activation_saturation_detection() {
1024 let config = DebugConfig::default();
1025 let mut detector = AnomalyDetector::new(&config);
1026
1027 let relu_saturated: Vec<f32> = vec![0.0; 100];
1029 detector
1030 .check_activation_saturation(&relu_saturated, "relu", "test_layer")
1031 .expect("operation failed in test");
1032 assert_eq!(detector.get_anomalies().len(), 1);
1033
1034 detector.clear_anomalies();
1035
1036 let sigmoid_saturated: Vec<f32> = vec![0.999; 100];
1038 detector
1039 .check_activation_saturation(&sigmoid_saturated, "sigmoid", "test_layer")
1040 .expect("operation failed in test");
1041 assert_eq!(detector.get_anomalies().len(), 1);
1042 }
1043
1044 #[test]
1045 fn test_memory_leak_detection() {
1046 let config = DebugConfig::default();
1047 let mut detector = AnomalyDetector::new(&config);
1048
1049 detector
1051 .check_memory_leak(3072, Some(1024), "test_location")
1052 .expect("operation failed in test");
1053 assert_eq!(detector.get_anomalies().len(), 1);
1054 assert!(matches!(
1055 detector.get_anomalies()[0].anomaly_type,
1056 AnomalyType::MemoryLeak
1057 ));
1058
1059 detector.clear_anomalies();
1060
1061 detector
1063 .check_memory_leak(10240, None, "test_location")
1064 .expect("operation failed in test");
1065 assert_eq!(detector.get_anomalies().len(), 1);
1066 }
1067
1068 #[test]
1069 fn test_weight_explosion_detection() {
1070 let config = DebugConfig::default();
1071 let mut detector = AnomalyDetector::new(&config);
1072
1073 let weights = vec![1.0, 2.0, 15.0, 4.0, -20.0]; detector
1075 .check_weight_explosion(&weights, "test_layer")
1076 .expect("operation failed in test");
1077
1078 assert_eq!(detector.get_anomalies().len(), 1);
1079 assert!(matches!(
1080 detector.get_anomalies()[0].anomaly_type,
1081 AnomalyType::UnusualActivation
1082 ));
1083 }
1084
1085 #[test]
1086 fn test_gradient_conflict_detection() {
1087 let config = DebugConfig::default();
1088 let mut detector = AnomalyDetector::new(&config);
1089
1090 let mut layer_gradients = HashMap::new();
1091 layer_gradients.insert("layer1".to_string(), vec![1.0, 0.0, 0.0]);
1092 layer_gradients.insert("layer2".to_string(), vec![-1.0, 0.0, 0.0]); detector
1095 .check_gradient_conflict(&layer_gradients)
1096 .expect("operation failed in test");
1097
1098 assert_eq!(detector.get_anomalies().len(), 1);
1099 assert!(matches!(
1100 detector.get_anomalies()[0].anomaly_type,
1101 AnomalyType::GradientConflict
1102 ));
1103 }
1104
1105 #[test]
1106 fn test_weight_divergence_detection() {
1107 let config = DebugConfig::default();
1108 let mut detector = AnomalyDetector::new(&config);
1109
1110 let baseline_weights = vec![1.0, 2.0, 3.0, 4.0];
1111 let diverged_weights = vec![10.0, 20.0, 30.0, 40.0]; detector
1115 .check_weight_divergence("test_layer", &baseline_weights)
1116 .expect("operation failed in test");
1117 assert_eq!(detector.get_anomalies().len(), 0);
1118
1119 detector
1121 .check_weight_divergence("test_layer", &diverged_weights)
1122 .expect("operation failed in test");
1123 assert_eq!(detector.get_anomalies().len(), 1);
1124 assert!(matches!(
1125 detector.get_anomalies()[0].anomaly_type,
1126 AnomalyType::WeightDivergence
1127 ));
1128 }
1129
1130 #[test]
1131 fn test_performance_degradation_detection() {
1132 let config = DebugConfig::default();
1133 let mut detector = AnomalyDetector::new(&config);
1134
1135 for _ in 0..10 {
1137 detector
1138 .check_performance_degradation(100.0, "training")
1139 .expect("operation failed in test"); }
1141 assert_eq!(detector.get_anomalies().len(), 0);
1142
1143 for _ in 0..5 {
1145 detector
1146 .check_performance_degradation(20.0, "training")
1147 .expect("operation failed in test"); }
1149
1150 assert!(!detector.get_anomalies().is_empty());
1152 assert!(detector
1153 .get_anomalies()
1154 .iter()
1155 .any(|a| matches!(a.anomaly_type, AnomalyType::PerformanceDegradation)));
1156 }
1157
1158 #[test]
1159 fn test_loss_anomaly_detection() {
1160 let config = DebugConfig::default();
1161 let mut detector = AnomalyDetector::new(&config);
1162
1163 detector.check_loss_anomaly(1.0, "training").expect("operation failed in test");
1165 detector.check_loss_anomaly(0.9, "training").expect("operation failed in test");
1166 assert_eq!(detector.get_anomalies().len(), 0);
1167
1168 detector
1170 .check_loss_anomaly(100.0, "training")
1171 .expect("operation failed in test"); assert_eq!(detector.get_anomalies().len(), 1);
1173 assert!(matches!(
1174 detector.get_anomalies()[0].anomaly_type,
1175 AnomalyType::LossAnomalous
1176 ));
1177 }
1178
1179 #[tokio::test]
1180 async fn test_auto_recovery() {
1181 let config = DebugConfig::default();
1182 let mut detector = AnomalyDetector::new(&config);
1183 detector.config.enable_auto_recovery = true;
1184
1185 let anomaly = Anomaly {
1186 anomaly_type: AnomalyType::GradientExplosion,
1187 timestamp: Utc::now(),
1188 location: "test_layer".to_string(),
1189 description: "Test gradient explosion".to_string(),
1190 severity: AnomalySeverity::High,
1191 metadata: HashMap::new(),
1192 };
1193
1194 let action = detector.attempt_recovery(&anomaly).await.expect("temp file creation failed");
1195 assert!(matches!(action, RecoveryAction::ClipGradients { .. }));
1196 assert_eq!(detector.get_recovery_attempts().len(), 1);
1197 }
1198
1199 #[test]
1200 fn test_monitoring_stats() {
1201 let config = DebugConfig::default();
1202 let mut detector = AnomalyDetector::new(&config);
1203
1204 detector.check_nan(&[f32::NAN], "test").expect("operation failed in test");
1206 detector.check_inf(&[f32::INFINITY], "test").expect("operation failed in test");
1207
1208 let stats = detector.get_monitoring_stats();
1209 assert_eq!(stats.total_anomalies, 2);
1210 assert!(stats.anomalies_per_type.contains_key("NaN"));
1211 assert!(stats.anomalies_per_type.contains_key("Infinity"));
1212 }
1213
1214 #[test]
1215 fn test_monitoring_window_update() {
1216 let config = DebugConfig::default();
1217 let mut detector = AnomalyDetector::new(&config);
1218
1219 detector.check_nan(&[f32::NAN], "test").expect("operation failed in test");
1220 detector.update_monitoring_window().expect("operation failed in test");
1221
1222 let stats = detector.get_monitoring_stats();
1223 assert_eq!(stats.monitoring_window.len(), 1);
1224 assert_eq!(stats.monitoring_window[0].anomaly_count, 1);
1225 }
1226}