Skip to main content

trustformers_debug/
anomaly_detector.rs

1//! Anomaly Detection for Model Debugging
2//!
3//! Detects unusual patterns in model execution, tensor values, and gradients
4//! to help identify potential issues during training and inference.
5// reason: debug/profiling scaffolding — structs are constructed and their fields/methods
6// are retained for the data model, serialization completeness, and future consumers that
7// do not yet read every member. Consolidated from many item-level #[allow(dead_code)].
8#![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/// Anomaly detector for model execution
18#[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/// Configuration for anomaly detection
32#[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, // Conservative default
71            numerical_instability_threshold: 1e-12,
72            performance_degradation_threshold: 0.5, // 50% degradation
73            weight_divergence_threshold: 5.0,
74            loss_spike_threshold: 10.0, // 10x loss increase
75            monitoring_window_size: 100,
76            recovery_attempts_limit: 3,
77        }
78    }
79}
80
81/// Types of anomalies that can be detected
82#[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/// An detected anomaly
99#[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/// Severity level of an anomaly
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum AnomalySeverity {
112    Low,
113    Medium,
114    High,
115    Critical,
116}
117
118/// Auto-recovery action that can be taken
119#[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/// Recovery attempt record
133#[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/// Real-time monitoring statistics
143#[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/// Snapshot of anomaly state for monitoring window
154#[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    /// Create a new anomaly detector
164    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    /// Start the anomaly detector
187    pub async fn start(&mut self) -> Result<()> {
188        self.start_time = Utc::now();
189        self.detected_anomalies.clear();
190        Ok(())
191    }
192
193    /// Check for NaN values in tensors
194    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    /// Check for infinite values in tensors
214    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    /// Check for gradient explosion
234    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    /// Check for vanishing gradients
258    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    /// Check for numerical instability
284    pub fn check_numerical_instability(&mut self, values: &[f32], location: &str) -> Result<()> {
285        let mut metadata = HashMap::new();
286
287        // Check for values close to zero that might cause division problems
288        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        // Check for extreme values that might cause overflow
307        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    /// Check for activation saturation
325    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, // Close to 0 or 1 for sigmoid, -1 or 1 for tanh
333            "relu" => 0.0,              // Zero values for ReLU
334            _ => 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    /// Check for memory leaks by tracking memory usage patterns
374    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            // Check for absolute high memory usage
407            if current_memory_mb > 8192 {
408                // 8GB threshold
409                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    /// Check for weight explosion in model parameters
433    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    /// Report an anomaly
470    fn report_anomaly(&mut self, anomaly: Anomaly) {
471        eprintln!(
472            "🚨 Anomaly detected: {} at {}",
473            anomaly.description, anomaly.location
474        );
475
476        // Update monitoring stats
477        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    /// Get all detected anomalies
485    pub fn get_anomalies(&self) -> &[Anomaly] {
486        &self.detected_anomalies
487    }
488
489    /// Clear all detected anomalies
490    pub fn clear_anomalies(&mut self) {
491        self.detected_anomalies.clear();
492    }
493
494    /// Check for gradient conflicts between layers
495    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    /// Check for weight divergence from baseline
541    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        // Initialize baseline if not exists
551        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(()); // Skip if dimensions don't match
561        }
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    /// Check for performance degradation
595    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        // Add to history
605        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        // Check for degradation if we have enough history
611        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    /// Check for loss anomalies
648    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        // Add to history
654        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        // Check for loss spikes
660        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    /// Attempt automatic recovery from an anomaly
692    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    /// Get monitoring statistics
723    pub fn get_monitoring_stats(&self) -> &MonitoringStats {
724        &self.monitoring_stats
725    }
726
727    /// Get recovery attempts history
728    pub fn get_recovery_attempts(&self) -> &[RecoveryAttempt] {
729        &self.recovery_attempts
730    }
731
732    /// Update monitoring window with current state
733    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        // Keep only recent snapshots
758        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    // Private helper methods for new functionality
766
767    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        // Cosine similarity - conflicts are indicated by negative correlation
783        let cosine_sim = dot_product / (norm1 * norm2);
784
785        // Convert to conflict score (0 = no conflict, 1 = maximum conflict)
786        (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        // In a real implementation, this would interface with the training system
817        // For now, we'll simulate the actions
818        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) // This would actually stop training
857            },
858        }
859    }
860
861    /// Quick anomaly check for simplified interface
862    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    /// Generate anomaly detection report
899    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/// Report generated by the anomaly detector
917#[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        // Test near-zero values
1005        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        // Test extreme values
1015        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        // Test ReLU saturation (all zeros)
1028        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        // Test sigmoid saturation (all ones)
1037        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        // Test memory growth detection (3x growth should trigger)
1050        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        // Test absolute high memory
1062        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]; // Two weights exceed threshold of 10.0
1074        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]); // Opposing gradients
1093
1094        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]; // Significant divergence
1112
1113        // First call establishes baseline
1114        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        // Second call detects divergence
1120        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        // Add baseline performance metrics
1136        for _ in 0..10 {
1137            detector
1138                .check_performance_degradation(100.0, "training")
1139                .expect("operation failed in test"); // Good performance
1140        }
1141        assert_eq!(detector.get_anomalies().len(), 0);
1142
1143        // Add degraded performance metrics - just enough to trigger once
1144        for _ in 0..5 {
1145            detector
1146                .check_performance_degradation(20.0, "training")
1147                .expect("operation failed in test"); // Poor performance
1148        }
1149
1150        // Should have at least one degradation anomaly
1151        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        // Add normal loss values
1164        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        // Add loss spike
1169        detector
1170            .check_loss_anomaly(100.0, "training")
1171            .expect("operation failed in test"); // 100x spike
1172        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        // Create some anomalies to generate stats
1205        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}